Look into Palanteer and get an omniscient view of your program

Look into Palanteer and get an omniscient view of your program
Improve software code quality - C++ and Python

Contents

(Top)
Overview
Getting started
Base concepts
C++ Instrumentation API
  4.1  Initialization
    4.1.1  plSetFilename
    4.1.2  plSetServer
    4.1.3  plInitAndStart
    4.1.4  plStopAndUninit
    4.1.5  plGetStats
  4.2  Structure tracing
    4.2.1  plDeclareThread
    4.2.2  plScope
    4.2.3  plFunction
    4.2.4  plBegin and plEnd
  4.3  Data tracing
    4.3.1  plData
    4.3.2  plText
    4.3.3  plVar
  4.4  Logs
    4.4.1  plSetLogLevelRecord
    4.4.2  plSetLogLevelConsole
    4.4.3  plLogDebug
    4.4.4  plLogInfo
    4.4.5  plLogWarn
    4.4.6  plLogError
    4.4.7  plMarker (deprecated)
  4.5  Lock tracing
    4.5.1  plLockWait
    4.5.2  plLockState
    4.5.3  plLockScopeState
    4.5.4  plLockNotify
    4.5.5  Examples of lock instrumentation
  4.6  CLIs
    4.6.1  plRegisterCli
    4.6.2  CLI handler
  4.7  Virtual threads
    4.7.1  plDeclareVirtualThread
    4.7.2  plAttachVirtualThread
    4.7.3  plDetachVirtualThread
  4.8  Troubleshootings
C++ Instrumentation configuration
Python Instrumentation API
Scripting API
More

  

@@ Overview

  

@@ Getting started

  

@@ Base concepts

  

@@ C++ Instrumentation API

This chapter describes the C++ API of the Palanteer instrumentation library.

   

Initialization

The service shall be initialized once, before any usage of event tracing.
Server address and filename values shall be configured before the initialization function plInitAndStart.

Control API Description
plSetFilename Sets the record file path when in “file storage” mode
plSetServer Sets the server IP address and port when in “connected” mode
plInitAndStart Initializes and starts the service
plStopAndUninit Stops and uninitializes the event tracing service
plGetStats Returns statistics about the collection process

   

plSetFilename

This function sets the record file path when in “file storage” mode. The path is copied, and its maximum size is 256 bytes.

To be taken into account, it shall be called before plInitAndStart function.

The default value is “record.pltraw”.

The declaration is:

// Sets the record file path when in "file storage" mode
void plSetFilename(const char* filename);
   

plSetServer

This function sets the server IP address and port when in “connected” mode (TCP socket).

It shall be called before plInitAndStart function to be taken into account.

The default value is “127.0.0.1" (IPv4 localhost) on port 59059.

The declaration is:

// Sets the server IP address and port when in "connected mode"
void plSetServer(const char* serverAddr, int serverPort);
   

plInitAndStart

This function initializes and starts the Palanteer service. It takes up to 4 parameters:

The three modes of the event tracing are:

On top of these parameters, its behavior depends also on the configuration of the compilation flags:

The declaration is:

enum plMode { PL_MODE_CONNECTED, PL_MODE_STORE_IN_FILE, PL_MODE_INACTIVE};

// Initializes and starts the events and remote control services
void plInitAndStart(const char* appName, plMode mode=PL_MODE_CONNECTED, const char* buildName=0, int serverConnectionTimeoutMsec=0);

Details on the created threads are shared in the collection mechanism section.

Almost all memory allocations of the Palanteer services are done in this function. For details, refer to the section memory usage.

plInitAndStart is supposed to be called at most once, until plStopAndUninit is called. This cycle can be repeated and each will generate a distinct recording session on the server side.
Once started, the event collection is running until the service is stopped with plStopAndUninit. This function also frees all resources.

   

plStopAndUninit

This function stops and uninitializes the Palanteer service. It is typically called before exiting the program.

The declaration is:

// Stops and de-initialized the Palanteer service
void plStopAndUninit();
   

plGetStats

This function returns statistics about the collection process.

It can be called at any time.

The declaration is:

// Collection statistic structure
struct plStats {
    uint32_t collectBufferSizeByteQty;     // Configured collection buffer size
    uint32_t collectBufferMaxUsageByteQty; // Maximum used size in the collection buffer
    uint32_t collectDynStringQty;          // Configured dynamic string qty
    uint32_t collectDynStringMaxUsageQty;  // Maximum used dynamic string qty
    uint32_t sentBufferQty;                // Buffer qty sent to the server
    uint32_t sentByteQty;                  // Byte qty sent to the server
    uint32_t sentEventQty;                 // Event qty sent to server
    uint32_t sentStringQty;                // Unique string qty sent to server
};

// Get the collection statistics
plStats plGetStats(void);

Refer to the double storage bank mechanism description for more details.

   

Structure tracing

The instrumentation in this chapter traces structural elements to form the hierarchy of the collected data.
The timings are contained in this structure.

Structure API Description Group variant Dyn. string variant
plDeclareThread Declares a thread X X
plScope Declares a scope (a named time range with optional children) X X
plFunction Declares a scope with the current function name X X
plBegin and plEnd Declares manually the start and the end of a scope (with moderation) X X

   

plDeclareThread

A thread can be given a name, at any moment during the recording, so that:

The default name is "Thread N" with N the integer thread order of appearance (so may differ from run to run).

It has both group and dynamic string variants.

The declaration is:

// Thread name declaration with a static string name
void plDeclareThread(threadName);

// If the group is enabled, declares a thread with the provided name as static string
void plgDeclareThread(const char* group, const char* threadName);

// Thread name declaration with a dynamic string name
void plDeclareThreadDyn(const char* nameFormat, ...);

// If the group is enabled, declares a thread with the provided name as dynamic string
void plgDeclareThreadDyn(const char* group, const char* nameFormat, ...);

Some example of usage:

// Example 1
plDeclareThread("Worker 1");

// Example 2
plDeclareThreadDyn(threadName.c_str());

// Example 3
plDeclareThreadDyn("Worker %d", workerIdx);

Threads can also be clustered by prefixing the thread name with a cluster name, separated with "/" (similar to UNIX pathname).
Only one hierarchical level is accepted.

Some example of thread cluster:

// Important: Calls here are purely for explanation. plDeclareThread shall of course be called in its respective thread

// Standalone thread, not in a cluster
// (Called in a thread)
plDeclareThread("Render");


// The calls below implicitly define a cluster "Workers" which contains 3 threads

// (Called at the start of another thread)
plDeclareThread("Workers/Worker 1");

// (Called at the start of yet another thread)
plDeclareThread("Workers/Worker 2");

// (Called at the start of yet another thread)
plDeclareThreadDyn("Workers/Worker %d", workerIdx);

Only the first call for each thread is taken into account, all subsequent ones are ignored.

This thread name declaration is persistent across starting/stopping the service, if done multiple times.
The only constraint is to be called before the end of the recording. Even before the service start is ok.

In case of duplicated thread name, a suffix #<unique thread="" id=""> is automatically added to the subsequent ones to make them unique.

   

plScope

This function defines a scope with the provided name.

A “scope” is a named time range with a start and an end, which can contains children (scopes or data).

Using scopes prevents from doing instrumentation mistakes.
Indeed, scopes are automatically closed and cannot be interlaced (RAII behavior). This is not the case with the functions plBegin and plEnd described below in this chapter.

It has both group and dynamic string variants.

The declaration is:

// Declares a scope with the provided name as static string
void plScope(const char* name);

// If the group is enabled, declares a scope with the provided name as static string
void plgScope(const char* group, const char* name);

// Declares a scope with the provided name as dynamic string
void plScopeDyn(const char* name);

// If the group is enabled, declares a scope with the provided name as dynamic string
void plgScopeDyn(const char* group, const char* name);
   

plFunction

This function automatically declares a scope with the current function name.
It is equivalent to

plScope(__FUNCTION__);

The start of the scope is the function call, its end is the one of the language scope.

It has both group and dynamic string variants.

The declaration is:

// Declares a scope with the current function name as static string (check your compiler for support, see note below)
void plFunction(void);

// If the group is enabled, declares a scope with the current function name as static string
void plgFunction(const char* group);

// Declares a scope with the current function name as dynamic string
void plFunctionDyn(void);

// If the group is enabled, declares a scope with the current function name as dynamic string
void plgFunctionDyn(const char* group);

Important consequences of plFunction using the preprocessor constant __FUNCTION__

GCC considers __FUNCTION__ as constexpr only since version 9.1. Before it, the more costly plFunctionDyn() shall be used.
Only GCC>=9.1 and CLANG>=6.0 can use the simpler plFunction() to mark the scope of a function.
If the preprocessor rants verbosely on such command, this is probably the issue.
In case of 'too old' compiler or for portability reasons, it is then recommended to use instead plScope("<function name>") where you manually insert the function name yourself.

   

plBegin and plEnd

These two functions are used together, they define an explicit start and end of a scope.

The provided name of the scope shall be the same for both functions, so that any mismatch can be detected on the server side.

This API shall be used with caution.
For instance, missing a plEnd call, typically in a multiple exit function, leads to a broken hierarchical data tree structure.
Also, interlaced scoped leads to mismatching “begin” and “end”.
Prefer plScope whenever possible (see also Manual scope).

The declaration is:

// Declares the start of a scope with the provided name as static strings
void plBegin(const char* name);

// Declares the end of a scope with the provided name as static strings
// The name shall match the begin name, so mismatches can be detected on viewer side and lead to warnings
void plEnd(const char* name);

// If the group is enabled, declares the start of a scope with the provided name as static strings
void plgBegin(const char* group, const char* name);

// If the group is enabled, declares the end of a scope with the provided name as static strings
void plgEnd(const char* group, const char* name);

// Declares the start of a scope with the provided name as a dynamic string
void plBeginDyn(const char* name);

// Declares the end of a scope with the provided name as a dynamic string
// The name shall match the begin name, so mismatchs can be detected on viewer side and lead to warnings
void plEndDyn(const char* name);

// If the group is enabled, declares the start of a scope with the provided name as a dynamic string
void plgBeginDyn(const char* group, const char* name);

// If the group is enabled, declares the end of a scope with the provided name as a dynamic string
void plgEndDyn(const char* group, const char* name);
   

Data tracing

The instrumentation in this chapter traces string or data of any numeric type inside the current scope.

Important

Such events must be located inside a scope, never at the tree root.
Indeed, they carry only the logged data and are not timestamped.

Such events can be visualized in the viewer under several forms: text, curve or histogram.
They can optionally be associated to a unit by ending the event name with ##<unit>:

Ex: "Duration##nanosecond", "pointer##hexa", "Banana##fruit"

Reminder:

As only string hash are considered, the unit declaration is part of the unique identifier for an event.

Data tracing API Description Group variant Dyn. string variant
plData Trace a named numerical value X (always dynamic)
plText Trace a text message X (always static)
plVar Trace one or more variable X n/a

   

plData

This function traces a named value.

The type of the value can be any integral types plus float, double and string (processed as dynamic string).

A string value is processed as a dynamic string.
To trace a static string value, use plMakeString(<string>) as a value, to compute the hash at compile time.
The alternative is to use the plText function in the next section, which does exactly that.

The declaration is:

// Traces a value
void plData(const char* name, <type> value);

// If the group is enabled, traces a value
void plgData(const char* group, const char* name, <type> value);

Example of usage:

int a = 15;
uint64_t b = 314159265359LL;
const char* c = "I am a dynamic string because not constexpr"; // Or just "char*"

// Various value tracing
plData("my value A", a);
plData("Truncated PI", 3.14159265359);
plData("Big PI", b);
plData("Dynamic string", c);

// Group variant
plgData(MY_DETAIL, "my value A", a); // PL_GROUP_MY_DETAIL must be defined with value 0 or 1

// Tracing of a static string, with the help of plMakeString
plData("Status", plMakeString("Low run time resource with such static string"));

// Another dynamic string message
const char* states[3] = { "Good automata state", "Average automata state", "Bad automata state" };
plData("State", states[i]);

// Compared to previous example, using plString_t reverts back to the static string performance and behavior
// Only the string selection is dynamic, the strings are processed and hashed at compile time.
plString_t static_states[3] = { plMakeString("Good automata state"), plMakeString("Average automata state"), plMakeString("Bad automata state") };
plData("State", static_states[i]);
   

plText

This function traces a named static string message.
It is a convenience function which calls plData with the plMakeString static string helper.

It has a group variant.

The declaration is:

// Traces a static string message
void plText(const char* name, const char* msg);

// If the group is enabled, traces a static string message
void plgText(const char* group, const char* name, const char* msg);

A hell of preprocessor and compiler errors are triggered if a non-static string is passed as a value. These errors usually start with:

palanteer.h: error: "string variable" is not a constant expression
| #define PL_STRINGHASH(s) plPriv::forceCompileTimeElseError_ < plPriv::fnv1a_(s,PL_FNV_HASH_OFFSET_) > ::compileTimeValue
... (then a lot more)

In that case, just switch to the plData function.

Example of usage:

// Static string message
plText("Stage", "Reaching a critical point");
   

plVar

This function is a convenience to trace the content of one or more variables under their respective variable name.

The type of the variable can be any integral types plus float, double and string (processed as dynamic string).

The declaration is:

// Traces the value of variables under the name of the variables
void plVar(variable1, variable2 ...);

// If the group is enabled, traces the value of variables under the name of the variables
void plgVar(const char* group, variable1, variable2 ...);

Between 1 and 10 variables or expressions are accepted.

Example of usage:

unsigned short myFirstVariable = 1000;
const char*    aStringVariable = "I am a string";

// Trace variables
plVar(myFirstVariable, aStringVariable);
// Group version
plgVar(MY_DETAIL, myFirstVariable, aStringVariable); // PL_GROUP_MY_DETAIL must be defined beforehand with value 0 or 1


// Longer (strictly) equivalent tracing:
plVar(myFirstVariable);
plVar(aStringVariable);
plgVar(MY_DETAIL, myFirstVariable);
plgVar(MY_DETAIL, aStringVariable);

// Even longer (strictly) equivalent tracing:
plData("myFirstVariable", myFirstVariable);
plData("aStringVariable", aStringVariable);
plgData(MY_DETAIL, "myFirstVariable", myFirstVariable);
plgData(MY_DETAIL, "aStringVariable", aStringVariable);

Note that plVar works with any expression:

// "i" will be incremented by one as expected. However, not recommended *at all*, as "i++" is not called when Palanteer is disabled
plVar(i++);
// Any expression is ok
plVar(state[i], htons(i), atan(i*3.14159/17.));

// The two previous calls are strictly equivalent to:
plData("i++", i++);
plData("state[i]", state[i]);
plData("htons[i]", htons[i]);
plData("atan(i*3.14159/17.)", atan(i*3.14159/17.));
   

Logs

The instrumentation in this chapter refers to “usual logging”, defined as the union of the definition below:

Structure API Description Group variant
plSetLogLevelRecord Set the minimum log level to be recorded (default=all)
plSetLogLevelConsole Set the minimum log level to be displayed on console (default=warn)
plLogDebug Log a message with the debug level X
plLogInfo Log a message with the info level X
plLogWarn Log a message with the warn level X
plLogError Log a message with the error level X

In the viewer:

Four log levels are defined, in this order:

enum plLogLevel {
    PL_LOG_LEVEL_ALL   = 0,  // Just for code clarity
    PL_LOG_LEVEL_DEBUG = 0,
    PL_LOG_LEVEL_INFO  = 1,
    PL_LOG_LEVEL_WARN  = 2,
    PL_LOG_LEVEL_ERROR = 3,
    PL_LOG_LEVEL_NONE  = 4
};
   

plSetLogLevelRecord

This function dynamically sets the minimum log level to be recorded. All logs with a level strictly below the provided level are dynamically filtered out from the record.
The default value is PL_LOG_LEVEL_ALL.

The declaration is:

void plSetLogLevelRecord(plLogLevel level);

A level of PL_LOG_LEVEL_ALL (equivalent to PL_LOG_LEVEL_DEBUG) includes all logs in the record.

A level of PL_LOG_LEVEL_NONE removes all logs from the record.

This function does not force any recording, it just dynamically configures the log filtering on the program side.

   

plSetLogLevelConsole

This function dynamically sets the minimum log level to be displayed on console. All logs with a level strictly below the provided level are dynamically filtered out from the console display.
The default value is PL_LOG_LEVEL_WARN.

The declaration is:

void plSetLogLevelConsole(plLogLevel level);

A level of PL_LOG_LEVEL_ALL (equivalent to PL_LOG_LEVEL_DEBUG) displays all logs on the console.

A level of PL_LOG_LEVEL_NONE does not display any log on the console.

The configuration flag PL_IMPL_CONSOLE_COLOR is used by the log console display.

The console display is active even if the Palanteer service is not initialized nor enabled. Its configuration is fully independent of the log recording.

The typical usage of the console display is to troubleshoot easily a problem.
As its run-time performance is poor due to the extra formatting and console output, it is not recommended to activate it in a context of performance observation.

   

plLogDebug

This function logs a printf-like message with a category and a debug level, automatically timestamped.
The purpose of the “category” attribute is to offer filtering per user-defined topic.

All standard printf argument formats are supported (except the strange and non remote compatible %n), and all provided parameters can be graphed individually.

This function has the group variant (note that the category and the format strings are always static strings).

The declaration is:

// Log a message
void plLogDebug(const char* category, const char* format, ...);

// If the group is enabled, log a message
void plgLogDebug(const char* group, const char* category, const char* format, ...);

Example of usage:

plLogDebug("input", "Key '%c' pressed", pressedKeyChar);

plLogDebug("output", "The resulting value of the phase %-20s is %g with the code 0x%08x", phaseStr, floatResult, errorCode);

plgLogDebug(MY_DETAIL, "phase", "End of a computation"); // PL_GROUP_MY_DETAIL must be defined with value 0 or 1
   

plLogInfo

This function is similar to the plLogDebug function but logs with the level info.

   

plLogWarn

This function is similar to the plLogDebug function but logs with the level warn.

   

plLogError

This function is similar to the plLogDebug function but logs with the level error.

   

plMarker (deprecated)

Markers are superseded by the previous log APIs, which are a kind of generalization. The plMarker APIs will be removed in a future release.

At the moment, they are mapped to a call to plLogWarn.

The backward compatibility is not complete for plMarkerDyn: a compilation error occurs if the message part is a dynamic string.
It is solved by replacing plMarkerDyn("category", dynamic_message) with one of these options:

Indeed, the format parameter shall be a static string in the new scheme.

   

Lock tracing

The instrumentation in this chapter traces actions performed on locks (as a general term).

The usage of locks in multithreaded environment can deeply modify the dynamic behavior.
Visualizing them becomes critical in complex programs, that is why a specific diagram in the viewer helps identifying the bottlenecks across threads.

Lock tracing API Description Group variant Dyn. string variant plString_t variant
plLockWait Trace a start of waiting on a lock X X X
plLockState Trace a state of the lock (taken or not) X X X
plLockScopeState Trace a state of the lock with an automatic unlocking X X
plLockNotify Trace a lock notification or post X X X

The lock API is “low level” so that it adapts to many existing lock primitives (mutex, semaphores, std::unique_lock, condition variables...), at the price of some less automatic instrumentation work.
This instrumentation becomes much lighter if the program uses an OS abstraction layer, as only this layer needs to be instrumented.

The dynamic variant allows to provide dynamic lock name string, at the cost of some extra runtime computations (one copy and one hashing).
The static variant allows to provide a static lock name through a function without extra runtime cost, which is convenient in some cases of wrapper implementation. It uses the structure plString_t.

Some examples of instrumentation of several usual primitives are shown after the description of the lock API

The locking process has two phases:

   

plLockWait

The locking process usually starts with waiting for the lock.
This function traces this first step.

When the wait ends, plLockState(...) or plLockScopeState(...) must be called to set both:

This “lock wait” information is crucial in a multithreaded program so it should be instrumented thoroughly.

This function has both group and dynamic string variants.

The declaration is:

// Set the start of waiting for a lock
void plLockWait(const char* name);

// If the group is enabled, set the start of waiting for a lock
void plgLockWait(const char* group, const char* name);

// Set the start of waiting for a lock which has a static name represented by a plString_t
void plLockWaitStatic(plString_t name);

// If the group is enabled, set the start of waiting for a lock which has a static name represented by a plString_t
void plgLockWaitStatic(const char* group, plString_t name);

// Set the start of waiting for a lock which has a dynamic name
void plLockWaitDyn(const char* name);

// If the group is enabled, set the start of waiting for a lock which has a dynamic name
void plgLockWaitDyn(const char* group, const char* name);

Example of usage for a pthread mutex lock:

plLockWait("Database access");        // Start waiting for the lock named "Database access"
pthread_mutex_lock(&_mutexDb);
plLockState("Database access", true); // >>> Do not forget this call! <<<  Lock is taken (see next section)
...
   

plLockState

This function is involved in both phases of the locking process.
Its main role is to set the state of the lock usage:

Its other and implicit role is to mark the end of the waiting process (if it was started previously), whatever the lock state value.

This function has both group and dynamic string variants.

The declaration is:

// Set the lock state
void plLockState(const char* name, bool lockUseState);

// If the group is enabled, set the lock state
void plgLockState(const char* group, const char* name, bool lockUseState);

// Set the lock state when the lock has a static name represented by a plString_t
void plLockStateStatic(plString_t name, bool lockUseState);

// If the group is enabled, set the lock state when the lock has a static name represented by a plString_t
void plgLockStateStatic(const char* group, plString_t name, bool lockUseState);

// Set the lock state when the lock has a dynamic name
void plLockStateDyn(const char* name, bool lockUseState);

// If the group is enabled, set the lock state when the lock has a dynamic name
void plgLockStateDyn(const char* group, const char* name, bool lockUseState);

plLockState must be called just after the end of the waiting phase, if any, to mark its end

plLockState shall be placed just before “unlock” call, so that there is no race condition on the tracing which inverse events chronology.

Example of usage for a pthread mutex unlock:

...
// Lock is released
plLockState("Database access", false); // Lock is released (trace before the release call)
pthread_mutex_unlock(&_mutexDb);
   

plLockScopeState

This function is required when the “unlock” is triggered by RAII (i.e. automatically at the end of the language scope).
It is a concatenation of an immediate plLockState call to set the lock state typically after a lock wait and an automatic call to plLockState(false) (aka unlock) at the end of the language scope.

It typically matches the behavior of std::unique_lock (and variants).

This function has both group and dynamic string variants.

The declaration is:

// Set the lock state and automatically unlocks at the end of the scope
void plLockScopeState(const char* name, bool lockUseState);

// If the group is enabled, set the lock state and automatically unlocks at the end of the scope
void plgLockScopeState(const char* group, const char* name, bool lockUseState);

// Set the lock state when the lock has a dynamic name and automatically unlocks at the end of the scope
void plLockScopeStateDyn(const char* name, bool lockUseState);

// If the group is enabled, set the lock state when the lock has a dynamic name and automatically unlocks at the end of the scope
void plgLockScopeStateDyn(const char* group, const char* name, bool lockUseState);

Example of usage for std::unique_lock (or std::lock_guard here):

{
    plLockWait("Resource");  // Start waiting for the lock "Resource"
    std::unique_lock<std::mutex> lk(globalCriticalResourceMutex, std::try_to_lock);
    plLockScopeState("Resource", lk.owns_lock()); // Stop the wait and set the state of the lock
    ...
} // If previously taken, the "unlock" state is automatically logged here at the end of the scope
   

plLockNotify

This function traces a notification or a “post” on a lock.
It has no direct effect on the lock state.

It typically matches the behavior of a condition_variable notify_one() call, or a semaphore post.

This function has both group and dynamic string variants.

The declaration is:

// Trace a lock notification
void plLockNotify(const char* name);

// If the group is enabled, trace a lock notification
void plgLockNotify(const char* group, const char* name);

// Trace a lock notification when the lock has a static name represented by a plString_t
void plLockNotifyStatic(plString_t name);

// If the group is enabled, trace a lock notification when the lock has a static name represented by a plString_t
void plgLockNotifyStatic(const char* group, plString_t name);

// Trace a lock notification when the lock has a dynamic name
void plLockNotifyDyn(const char* name);

// If the group is enabled, trace a lock notification when the lock has a dynamic name
void plgLockNotifyDyn(const char* group, const char* name);

plLockNotify shall be placed just before “notification” call, so that there is no race condition on the tracing which inverse events chronology.

Example of usage:

std::string lockName("Task wait");

if(shallWakeTheTask) {
    plLockNotifyDyn(lockName.c_str()); // Notify the "Task Wait" lock (linked to a condition variable)
    taskWaitConditionVariable.notify_one();
}
   

Examples of lock instrumentation

Example with pthread mutex:

pthread_mutex_t resultsLock;
pthread_mutex_init(&resultsLock, NULL); // Ok, we should check the returned value...

plLockWait("Result");                        // Start of the lock wait
if(pthread_mutex_lock(&resultsLock)!=0) {
    plLockState("Result", false);            // End of the lock wait and lock not taken
    return;
}
plLockState("Result", true);                 // End of the lock wait and lock taken

...

plLockState("Result", false);                // End of the lock usage, lock released
pthread_mutex_unlock(&_resultsLock);

Example with pthread semaphore:

sem_t sem;
sem_init(&sem, 0, 1);

// Thread 1 (displayer client)
while(1) {
    plLockWait("Wake");           // Start of the lock wait
    sem_wait(&sem);
    plLockState("Wake", false);   // End of the lock wait and no lock taken (only the wait matters for this semaphore)

    // Do something
    printf("Some work\n");
}


// Thread 2 (controller)
while(1) {
    plLockNotify("Wake");         // Send a notification for the lock. It shall indirectly stop the semaphore wait phase
    sem_post(&sem);

    // Wait a bit
    useconds_t r = random() % 100;
    usleep(r);
}

Example with std::mutex:

std::mutex database_mutex;
std::map<std::string, std::string=""> databse;

void addUrl(const std::string& url, const std::string& result)
{
    plLockWait("Database");                            // Start waiting for the lock "Database"
    std::lock_guard<std::mutex> guard(database_mutex);
    plLockScopeState("Database", true);                // End of the lock wait and lock taken each time by design
    database[url] = result;
} // Automatic trace of the unlock because we used plLockScopeState()

Example with std::condition_variable:

std::mutex mtx;             // mutex for critical section
std::condition_variable cv; // condition variable for critical section
bool doWakeUp = false;

// Thread 1 (displayer client)
while(1) {

    plLockWait("Wake");                             // Start of the scoped lock wait
                                                    // Note: sometimes, tracking the wait is not interesting. This line can then be omitted.
    std::unique_lock<std::mutex> lk(mtx);           // Locking the mutex of the condition variable
                                                    // The mutex is taken here, but this is not the end of the "condition variable waiting phase"
    cv.wait(lk, [&doWakeUp] { return doWakeUp; });  // Waiting until the condition is true
    plLockScopeState("Wake", true);                 // Lock always taken by design (with automatic RAII unlock)

    // Do something
    printf("Some work\n");

    // Reset the wake up
    doWakeUp = false;

} // Automatic trace of the unlock because we used plLockScopeState()


// Thread 2 (controller)
while(1) {

    // Wait a bit
    useconds_t r = random() % 100;
    usleep(r);

    // Make the wake up condition true
    std::unique_lock<std::mutex> lk(mtx);
    doWakeUp = true;
    plLockNotify("Wake");                          // Send a notification for the lock, which shall indirectly break the condition variable waiting phase
    cv.notify_one();
}
   

CLIs

“CLIs” (Command Line Interface) are functions inside the instrumented program that can be called remotely and with parameters.

These functions, also named “CLI handlers”, are called from the Palanteer reception thread.

When the compilation flag PL_NOCONTROL is set to 1, the Palanteer reception thread is not created and the CLI functionality is absent.

   

plRegisterCli

This function registers a CLI.
It requires a CLI handler, the name of the CLI, the CLI parameter specification and a user description.

The name of the command shall not contain spaces. The user description is purely for documentation purpose. These two strings must be static and are affected by the “external string” features.
The parameter specification must also be a static string, but it is never obfuscated because its content is used internally to validate the command syntax.

CLIs can be registered before Palanteer is initialized.
This is even recommended in order to remove any potential race condition about calling a not yet registered CLI.

The declaration is:

void plRegisterCli(plCliHandler_t cliHandler, const char* name, const char* param_specification, const char* description);

An example of CLI registration is (see Remote control for full example):

plRegisterCli(setBoundsCliHandler, "config:setRange", "min=int max=int", "Sets the value bounds of the random generator");
   

CLI handler

A CLI handler is a function triggered remotely that interacts with the controlled program.
It accepts typed parameters among integer, float or string and returns a status and a text.

The communication with Palanteer is done through a “communication helper” object. Its prototype is:

typedef void (*plCliHandler_t)(plCliIo& cio);

This “communication” object has two roles:

Its full prototype is:

class plCliIo {
public:
    // Input accessors
    int64_t     getParamInt   (int paramIdx) const;
    double      getParamFloat (int paramIdx) const;
    const char* getParamString(int paramIdx) const;

    // Output formatting
    void setErrorState(const char* format=0, ...); // Set the error state and take some optional text
    bool addToResponse(const char* format, ...);   // Returns false if the response buffer is full
    void clearResponse(void);                      // Resets the response buffer

An example of CLI handler extracted from Remote control and declared with the parameter spec "min=int max=int" is:

// Handler (=implementation) of the example CLI
void setBoundsCliHandler(plCliIo& cio)  // 'cio' is a communication helper present in each handler call
{
    int minValue = cio.getParamInt(0); // Get the 2 CLI parameters as integers
    int maxValue = cio.getParamInt(1);
    if(minValue>maxValue) {
        // CLI execution has failed. The text answer contains some information about it
        cio.setErrorState("Minimum value (%d) shall be lower than the maximum value (%d)\n", minValue, maxValue);
        return;
    }

    // Modify the state of the program
    globalMinValue = minValue;
    globalMaxValue = maxValue;
    // CLI execution was successful (no call to cio.setErrorState())
}

And a way to call it from a Python program with the Palanteer scripting module is:

palanteer.program_cli("config:setRange min=300 max=500")
   

Virtual threads

A “virtual thread” is a thread that is not managed by the OS. Some examples are “fibers”, or a simulated OS environment.
They require the support of “OS worker threads” which effectively run these virtual threads. A running virtual thread can be switched/exchanged with another one usually only at particular points (I/O call, yield call, etc...), and resumed later on any of the existing worker threads.

The support of virtual threads requires the following actions:

Optionally but recommended, the virtual thread name can be declared with plDeclareVirtualThread, typically in the virtual thread creation hook.

As an example, the Windows function SwitchToFiber call shall be preceded by a plDetachVirtualThread call and followed by a plAttachVirtualThread call.
The available information shall be if the “old” job is suspended for later resume or finished, and after the switch the ID of the new fiber.

The effects of using virtual threads are:

   

plDeclareVirtualThread

As for OS threads, a name can be given to virtual threads.
This function associates the provided name to the external virtual thread identifier. The name of the OS thread is unchanged.

This function can be called from any thread. Only the unique virtual thread identifier matters here.

The declaration is:

// externalVirtualThreadId: a unique external virtual thread identifier
// name: the name of the virtual thread
void plDeclareVirtualThread(uint32_t externalVirtualThreadId, const char* nameFormat, ...);

Example of usage:

plDeclareVirtualThread("Fibers/Fiber %d", fiberId);

Also as for OS threads, virtual threads can be grouped.

Important

The attachment scopes of non-declared virtual thread are not displayed in the “locks and resources” timeline.
It is thus recommended to declare the virtual threads at resource creation time, to get most of the information.

Only the first declaration of each virtual thread is taken into account, all subsequent ones are ignored.

This function can be called before the service is started.
It is also persistent across starting/stopping the service, if done multiple times.

   

plAttachVirtualThread

This function notifies Palanteer of a virtual thread attachment to the current OS thread.

Important

Always detach a thread before attaching a new one, else the resource will not correctly indicate the new virtual thread.

The declaration is:

// externalVirtualThreadId: a unique external virtual thread identifier
void plAttachVirtualThread(uint32_t externalVirtualThreadId);

Example of usage:

plAttachVirtualThread(newFiberId);
   

plDetachVirtualThread

This function notifies Palanteer that the current virtual thread is detached from the current OS thread.

The declaration is:

// isSuspended: true if this new virtual thread ID is suspended, false if it completed his previous task
void plDetachVirtualThread(bool isSuspended);

If the state suspended is not known, set the boolean to false.
This information is used to display the time slice when the thread is inactive.

Example of usage:

plDetachVirtualThread(false);
   

Troubleshootings

I get a lot of preprocessor errors...

The usage of cascaded macros in Palanteer, only practical way to do some introspection in C++, has the bad effect of creating a cascade of errors...
In such case, try to isolate the faulty line from your sources, and check:


I really get a lot of preprocessor errors while using logging, talking about ambiguous overload stuff...*

First, apologies for the quantity of thrown error, see the previous point.
This problem is independent of the string format. It is purely related to the provided parameters.

Of course, check first that the parameters can all be handled properly by printf (no struct, class, ...).
Then the issue is probably due to the fixed width integer types, especially for 64 bits integers:

To solve the issue, just cast the faulty parameter with its corresponding fixed width integer types (ex: (int64_t)myLongLongInt)


plFunction does not compile

You probably hit the issue described in plFunction: some “old” C++ compilers do not consider __FUNCTION__ as constexpr.
In such case, either:


I use clang with ASAN and the memory allocations are not logged...

Overloading new and delete operators in clang with ASAN does not work, this is a known issue in clang (see clang bugzilla https://bugs.llvm.org/show_bug.cgi?id=19660 ).


I have some unexpected crash in the instrumentation library

Ensure that the non-implementation configuration flags are consistent in all files (PL_EXTERNAL_STRINGS, PL_DYN_STRING_MAX_SIZE, ...).
This can really lead to undefined behaviors.


I traced some data but the viewer reports it as an instrumentation error

Data events (text, numerical value) are not timestamped.
Outside a time scope, which means only at the root level of the tree, they cannot be associated to any dated element (the knowledge that they are after the last one and before the next one is not used).
The fix is to move them inside a scope, or create a scope to hold them.

Note that printf-like logs and lock notifications do not have this constraint and can be located at the root level because they contain a timestamp.


What is the name of the function to break in when investigating a crash under a debugger?

Break in plCrash.


I have many threads in parallel and many “SATURATION” logs despite I allocated enough memory for the event collection buffers

If all CPUs are saturated, the collection task cannot run regularly enough and buffers will get full whatever their allocated size.
The consequence is that event tracing will start to block the threads, waiting for some space for tracing, and place a “SATURATION” error log.

The presence of the viewer or scripting module on the same machine is maybe the problem, as they use also some CPU and may interfere with the program under observation.
Ideally, the available CPU quantity shall be: at least the required quantity for your program + 3 (instrumentation thread + viewer recording thread + viewer display thread).

Recording on a file reduces this requirement to: at least the required quantity for your program + 1 (instrumentation thread), as the processing of the raw events will be done later.


What are the limitations on thread and event quantity?

The system can handle up to 254 threads and 2 billion events.
The estimated size of the record would be ~20 GB, still displayed smoothly.


I want to hide the Palanteer threads

The whole instrumentation of the implementation of the Palanteer threads is using the group PL_VERBOSE.
It can be fully disabled with the line below, to be inserted before the #include "palanteer.h" which implements the service:

#define PL_GROUP_PL_VERBOSE 0

  

@@ C++ Instrumentation configuration

  

@@ Python Instrumentation API

  

@@ Scripting API

  

@@ More

formatted by Markdeep 1.13