fixed callstack tracker for multithreaded apps
authorFred T. Hamster <fred@feistymeow.org>
Tue, 10 Feb 2026 20:31:33 +0000 (15:31 -0500)
committerFred T. Hamster <fred@feistymeow.org>
Tue, 10 Feb 2026 20:31:33 +0000 (15:31 -0500)
made the tracker thread local, so it adheres to the particular thread and keeps the stack right.
added tests for callstack tracker in the mutex test, since it already had lots of cool thread activity.
the stack tests invoke our macro, which checks the trace's validity; there's no specific other test
of the callstack tracker in test_mutex, but the current approach will add tests to the overall count
and also complain about any errors that occur.

nucleus/library/application/callstack_tracker.cpp
nucleus/library/application/callstack_tracker.h
nucleus/library/tests_application/test_callstack_tracker.cpp
nucleus/library/tests_basis/test_mutex.cpp

index 00e5a43254ac18d70d2c506bd40b86c4aaee5ead..1c079db0f39ebe1e3a8859912ee03c75152eea2e 100644 (file)
@@ -56,7 +56,7 @@ const char *emptiness_note = "Empty Stack\n";
 
 basis::mutex &callstack_tracker::__callstack_tracker_synchronizer()
 {
-  static basis::mutex __global_synch_callstacks;
+  thread_local basis::mutex __global_synch_callstacks;
   return __global_synch_callstacks;
 }
 
@@ -65,24 +65,14 @@ basis::mutex &callstack_tracker::__callstack_tracker_synchronizer()
 //! the single instance of callstack_tracker.
 /*!
   this is also an ultra low-level object, although it's not as far down
-as the memory checker.  it can allocate c++ objects and that kind of thing
-just fine.  the object must be stored here rather than in the static basis
-library due to issues in windows dlls.
-  NOTE: the construction process for this is not thread-safe; the static
-program-wide object must be initialized before any threads have started.
-that is normally done in...
-uhhh....
-
-beuller?
-...
-
-
+  as the memory checker.  it can allocate c++ objects and that kind of thing
+  just fine.
 */
 callstack_tracker &program_wide_stack_trace()
 {
   auto_synchronizer l(callstack_tracker::__callstack_tracker_synchronizer());
 
-  static callstack_tracker *_hidden_trace = NULL_POINTER;
+  thread_local callstack_tracker *_hidden_trace = NULL_POINTER;
   if (!_hidden_trace) {
 #ifdef ENABLE_MEMORY_HOOK
     program_wide_memories().disable();
index 19fb94cd1652e9916c9a59f31a2710b7b5b06569..0ef6bf44e672e62cb11c0d23df6b5f1105228a64 100644 (file)
@@ -104,6 +104,36 @@ private:
 
 //////////////
 
+/*!
+  super helpful macro that shows the current stack trace and checks it for validity.
+  this shouldn't impact the trace, since all the code is embedded inline from the macro.
+  this does require that LOG() is defined, and that a failure return value is expected
+  from the embedding function.
+*/
+#define GET_AND_TEST_STACK_TRACE(header, failure_return) { \
+  int trace_size = program_wide_stack_trace().full_trace_size(); \
+  char *stack_trace = program_wide_stack_trace().full_trace(); \
+  ASSERT_TRUE(trace_size >= strlen(stack_trace) + 1, "insufficient estimated stack trace size"); \
+  if (trace_size < strlen(stack_trace) + 1) { \
+    /* error condition here; we are supposed to get the actual size we would need to allocate! */ \
+    LOG(a_sprintf("failure in stack trace return: estimated size (%d) was less than actual (%d)", \
+        trace_size, strlen(stack_trace))); \
+    /* mandatory free step for newly allocated string. */ \
+    free(stack_trace); \
+    return failure_return; \
+  } \
+  ASSERT_TRUE(strlen(stack_trace) > 1, "empty stack trace"); \
+  if (strlen(stack_trace) < 2) { \
+    LOG("failure in stack trace return: the trace output string was empty!"); \
+    return failure_return; \
+  } \
+  LOG(astring("\n\n################\n\n") + header + "\n" + stack_trace); \
+  /* mandatory free step for newly allocated string. */ \
+  free(stack_trace); \
+}
+
+//////////////
+
 //! a small object that represents a stack trace in progress.
 /*! the object will automatically be destroyed when the containing scope
 exits.  this enables a users of the stack tracker to simply label their
@@ -155,9 +185,8 @@ void update_current_stack_frame_line_number(int line);
   */
   inline void no_op() { /* do nothing. */ }
   #define frame_tracking_instance
+  #define GET_AND_TEST_STACK_TRACE(header, failure_return) no_op();
   #define __trail_of_function(p1, p2, p3, p4, p5) no_op();
-    // the above actually trades on the name of the object we'd normally
-    // define.  it must match the object name in the FUNCDEF macro.
   inline void update_current_stack_frame_line_number(int line) { /* more nothing. */ }
 #endif // ENABLE_CALLSTACK_TRACKING
 
index ba725e288d8ba5189eb4325e2890cdb322f21135..25711a360d041fbf3e63bb0677b7afb4a81a2eb6 100644 (file)
@@ -1,4 +1,4 @@
-/*****************************************************************************\
+/*
 *
 *  Name   : test_callstack_tracker
 *  Author : Chris Koeritz
@@ -7,14 +7,14 @@
 *
 *    Puts the callstack tracking code through its paces, a bit.
 *
-*******************************************************************************
-* Copyright (c) 1992-$now By Author.  This program is free software; you can  *
-* redistribute it and/or modify it under the terms of the GNU General Public  *
-* License as published by the Free Software Foundation; either version 2 of   *
-* the License or (at your option) any later version.  This is online at:      *
-*     http://www.fsf.org/copyleft/gpl.html                                    *
-* Please send any updates to: fred@gruntose.com                               *
-\*****************************************************************************/
+****
+* Copyright (c) 1992-$now By Author.  This program is free software; you can
+* redistribute it and/or modify it under the terms of the GNU General Public
+* License as published by the Free Software Foundation; either version 2 of
+* the License or (at your option) any later version.  This is online at:
+*     http://www.fsf.org/copyleft/gpl.html
+* Please send any updates to: fred@gruntose.com
+*/
 
 #include <basis/functions.h>
 #include <basis/guards.h>
@@ -31,8 +31,6 @@
 #include <structures/string_array.h>
 #include <unit_test/unit_base.h>
 
-//#include <string.h>
-
 using namespace application;
 using namespace basis;
 using namespace filesystem;
@@ -44,34 +42,6 @@ using namespace unit_test;
 
 //////////////
 
-/*
-  super helpful macro that shows the current stack trace and checks it for validity.
-  this shouldn't impact the trace, since it's all embedded inline from the macro.
-*/
-#define SHOW_TRACE_AND_CHECK_IT(message) { \
-  int trace_size = program_wide_stack_trace().full_trace_size(); \
-  char *stack_trace = program_wide_stack_trace().full_trace(); \
-  ASSERT_TRUE(trace_size >= strlen(stack_trace) + 1, "insufficient estimated stack trace size"); \
-  if (trace_size < strlen(stack_trace) + 1) { \
-    /* error condition here; we are supposed to get the actual size we would need to allocate! */ \
-    LOG(a_sprintf("failure in stack trace return: estimated size (%d) was less than actual (%d)", \
-        trace_size, strlen(stack_trace))); \
-    /* mandatory free step for newly allocated string. */ \
-    free(stack_trace); \
-    return 1; \
-  } \
-  ASSERT_TRUE(strlen(stack_trace) > 1, "empty stack trace"); \
-  if (strlen(stack_trace) < 2) { \
-    LOG("failure in stack trace return: the trace output string was empty!"); \
-    return 1; \
-  } \
-  LOG(astring("\n\n################\n\n") + message + "\n" + stack_trace); \
-  /* mandatory free step for newly allocated string. */ \
-  free(stack_trace); \
-}
-
-//////////////
-
 class test_callstack_tracker : virtual public unit_base, virtual public application_shell
 {
 public:
@@ -95,7 +65,7 @@ int test_callstack_tracker::run_filestack_simple()
   FUNCDEF("run_filestack_simple")
   #ifdef ENABLE_CALLSTACK_TRACKING
     // just shows this method's own stack.
-    SHOW_TRACE_AND_CHECK_IT("trace of simple stack:");
+    GET_AND_TEST_STACK_TRACE("trace of simple stack:", 1);
   #endif
   return 0;
 }
@@ -123,7 +93,7 @@ int test_callstack_tracker::sub_call_4()
   FUNCDEF("sub_call_4");
   #ifdef ENABLE_CALLSTACK_TRACKING
     // just shows this method's own stack.
-    SHOW_TRACE_AND_CHECK_IT("trace of middling stack:");
+    GET_AND_TEST_STACK_TRACE("trace of middling stack:", 1);
   #endif
   return 0;
 }
index 1783de12734fa7e7140436b04c79bbe5e7c04060..3d6867a5c64bfdb7d66a37dd44cbc9119ee1edf4 100644 (file)
@@ -12,6 +12,7 @@
 * Please send any updates to: fred@gruntose.com                               *
 \*****************************************************************************/
 
+#include <application/callstack_tracker.h>
 #include <application/hoople_main.h>
 #include <basis/astring.h>
 #include <basis/guards.h>
@@ -41,6 +42,8 @@ using namespace processes;
 using namespace structures;
 using namespace unit_test;
 
+class test_mutex;  // forward.
+
 #define DEBUG_MUTEX
   // uncomment for a verbose test run.
 
@@ -78,30 +81,27 @@ astring protected_string;
 #define LOG(to_print) CLASS_EMERGENCY_LOG(program_wide_logger::get(), to_print)
   // our macro for logging with a timestamp.
 
-// expects guardian mutex to already be locked once when coming in.
-void test_recursive_locking(chaos &_rando)
+//////////////
+
+#undef UNIT_BASE_THIS_OBJECT
+#define UNIT_BASE_THIS_OBJECT (*this)
+
+class test_mutex : virtual public unit_base, virtual public application_shell
 {
-  int test_attempts = _rando.inclusive(MIN_SAME_THREAD_LOCKING_TESTS,
-      MAX_SAME_THREAD_LOCKING_TESTS);
-  int locked = 0;
-  for (int i = 0; i < test_attempts; i++) {
-    bool lock = !!(_rando.inclusive(0, 1));
-    if (lock) {
-      guard().lock();
-      locked++;  // one more lock.
-    } else {
-      if (locked > 0) {
-        // must be sure we are not already locally unlocked completely.
-        guard().unlock();
-        locked--;
-      }
-    }
-  }
-  for (int j = 0; j < locked; j++) {
-    // drop any locks we had left during the test.
-    guard().unlock();
-  }
-}
+public:
+  chaos _rando;  // our randomizer.
+
+  test_mutex() : application_shell() {}
+
+  DEFINE_CLASS_NAME("test_mutex");
+
+  int execute();
+
+  void test_recursive_locking(chaos &_rando);
+    // invoked by the threads to do a little testing of locks.
+};
+
+//////////////
 
 //hmmm: how are these threads different so far?  they seem to do exactly
 //      the same thing.  maybe one should eat chars from the string.
@@ -113,9 +113,9 @@ class piranha : public ethread
 {
 public:
   chaos _rando;  // our randomizer.
-  unit_base &c_testing;  // provides for test recording.
+  test_mutex &c_testing;  // provides for test recording.
 
-  piranha(unit_base &testing) : ethread(0), c_testing(testing) {
+  piranha(test_mutex &testing) : ethread(0), c_testing(testing) {
     FUNCDEF("constructor");
     safe_add(concurrent_biters, 1);
     ASSERT_TRUE(concurrent_biters >= 1, "the piranha is very noticeable");
@@ -143,10 +143,15 @@ public:
       ASSERT_TRUE(grab_lock <= 1, "grab lock should not already be active");
       protected_string += char(_rando.inclusive('a', 'z'));
 
-      test_recursive_locking(_rando);
+      c_testing.test_recursive_locking(_rando);
 
       safe_add(grab_lock, -1);
     }
+
+    #ifdef ENABLE_CALLSTACK_TRACKING
+      GET_AND_TEST_STACK_TRACE(class_name() + "::" + func + " callstack:", );
+    #endif
+
     // dropped the lock.  snooze a bit.
     if (!should_stop())
       time_control::sleep_ms(_rando.inclusive(THREAD_PAUSE_LOWEST, THREAD_PAUSE_HIGHEST));
@@ -154,13 +159,15 @@ public:
 
 };
 
+//////////////
+
 class barracuda : public ethread
 {
 public:
   chaos _rando;  // our randomizer.
-  unit_base &c_testing;  // provides for test recording.
+  test_mutex &c_testing;  // provides for test recording.
 
-  barracuda(unit_base &testing) : ethread(0), c_testing(testing) {
+  barracuda(test_mutex &testing) : ethread(0), c_testing(testing) {
     FUNCDEF("constructor");
     safe_add(concurrent_biters, 1);
     ASSERT_TRUE(concurrent_biters >= 1, "our presence should have been noticed");
@@ -182,11 +189,16 @@ public:
     safe_add(grab_lock, 1);
     ASSERT_TRUE(grab_lock <= 1, "grab lock should not already be active");
 
-    test_recursive_locking(_rando);
+    c_testing.test_recursive_locking(_rando);
 
     protected_string += char(_rando.inclusive('a', 'z'));
     safe_add(grab_lock, -1);
     guard().unlock();
+
+    #ifdef ENABLE_CALLSTACK_TRACKING
+      GET_AND_TEST_STACK_TRACE(class_name() + "::" + func + " callstack:", );
+    #endif
+
     // done with the lock.  sleep for a while.
     if (!should_stop())
       time_control::sleep_ms(_rando.inclusive(THREAD_PAUSE_LOWEST, THREAD_PAUSE_HIGHEST));
@@ -198,18 +210,6 @@ public:
 #undef UNIT_BASE_THIS_OBJECT
 #define UNIT_BASE_THIS_OBJECT (*this)
 
-class test_mutex : virtual public unit_base, virtual public application_shell
-{
-public:
-  chaos _rando;  // our randomizer.
-
-  test_mutex() : application_shell() {}
-
-  DEFINE_CLASS_NAME("test_mutex");
-
-  int execute();
-};
-
 int test_mutex::execute()
 {
   FUNCDEF("execute");
@@ -243,6 +243,10 @@ int test_mutex::execute()
 #ifdef DEBUG_MUTEX
     LOG("about to exit scope and dump automatic objects.");
 #endif
+
+    #ifdef ENABLE_CALLSTACK_TRACKING
+      GET_AND_TEST_STACK_TRACE(class_name() + "::" + func + " mutex lock timing callstack:", 1);
+    #endif
   }
 
 #ifdef DEBUG_MUTEX
@@ -264,6 +268,11 @@ int test_mutex::execute()
       LOG(a_sprintf("indy %i: adding new piranha now.", i));
 #endif
     }
+
+    #ifdef ENABLE_CALLSTACK_TRACKING
+      GET_AND_TEST_STACK_TRACE(class_name() + "::" + func + a_sprintf(" starting fish indy #%d", i+1) + " callstack:", 1);
+    #endif
+
     thread_list.append(t);
     ethread *q = thread_list[thread_list.elements() - 1];
     ASSERT_EQUAL(q, t, "amorph pointer equivalence is required");
@@ -318,6 +327,10 @@ int test_mutex::execute()
 #ifdef DEBUG_MUTEX
   LOG("done exiting from all threads.");
 
+  #ifdef ENABLE_CALLSTACK_TRACKING
+    GET_AND_TEST_STACK_TRACE(class_name() + "::" + func + " after all threads have exited callstack:", 1);
+  #endif
+
   LOG(astring(astring::SPRINTF, "the accumulated string had %d characters "
       "which means\nthere were %d thread activations from %d threads.",
       protected_string.length(), protected_string.length(),
@@ -327,5 +340,38 @@ int test_mutex::execute()
   return final_report();
 }
 
+//////////////
+
+// expects guardian mutex to already be locked once when coming in.
+void test_mutex::test_recursive_locking(chaos &_rando)
+{
+  FUNCDEF("test_recursive_locking");
+  int test_attempts = _rando.inclusive(MIN_SAME_THREAD_LOCKING_TESTS,
+      MAX_SAME_THREAD_LOCKING_TESTS);
+  int locked = 0;
+  #ifdef ENABLE_CALLSTACK_TRACKING
+    GET_AND_TEST_STACK_TRACE(class_name() + "::" + func + " callstack:", );
+  #endif
+  for (int i = 0; i < test_attempts; i++) {
+    bool lock = !!(_rando.inclusive(0, 1));
+    if (lock) {
+      guard().lock();
+      locked++;  // one more lock.
+    } else {
+      if (locked > 0) {
+        // must be sure we are not already locally unlocked completely.
+        guard().unlock();
+        locked--;
+      }
+    }
+  }
+  for (int j = 0; j < locked; j++) {
+    // drop any locks we had left during the test.
+    guard().unlock();
+  }
+}
+
+//////////////
+
 HOOPLE_MAIN(test_mutex, )