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](#plsetfilename) | Sets the record file path when in "file storage" mode |
| [plSetServer](#plsetserver) | Sets the server IP address and port when in "connected" mode |
| [plInitAndStart](#plinitandstart) | Initializes and starts the service |
| [plStopAndUninit](#plstopanduninit) | Stops and uninitializes the event tracing service |
| [plGetStats](#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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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 **name** of the application (mandatory)
- ex: "Pacman", "Space invaders"
- the event **tracing mode**: `PL_MODE_CONNECTED`, `PL_MODE_STORE_IN_FILE` and `PL_MODE_INACTIVE`
- an optional **build name**, to describe more precisely the version of the program
- an optional **timeout (ms)** to configure the duration that the program shall wait for the connection to the server before giving up.
- the value `0` means infinite waiting
- the value `-1` means no wait (default)
The three modes of the event tracing are:
- `PL_MODE_CONNECTED` (default): connect to the server to enable remote recording and program control.
- If `waitForServerConnection` is `true,` the initialization waits indefinitely for the established connection.
- If `waitForServerConnection` is `false` (default), one connection to the server is tried. If it fails, the mode falls back to `PL_MODE_INACTIVE`.
- The used parameters are the server and port values, configured with [`plSetServer`](#plsetserver)
- `PL_MODE_STORE_IN_FILE`: the raw record is directly written in a file without any external connection.
- The "raw record" data is in fact the data that would have been sent to the server.
- In this mode, the remote control is inactive (no server...)
- The used parameter is the storage filename, configured with [`plSetFilename`](#plsetfilename).
- `PL_MODE_INACTIVE`: event collection and remote control are inactive, even if the `Palanteer` code is present
On top of these parameters, its behavior depends also on the configuration of the compilation flags:
- If `USE_PL` is not equal to 1, the function `plInitAndStart` does nothing at all.
- If `USE_PL` is 1, the behavior of this initialization function is:
- if `PL_IMPL_CATCH_SIGNALS` is 1, it installs the signal handlers
- if `PL_IMPL_STACKTRACE` is 1, it initializes the symbol decoder in case of crash (Windows only)
- if the `mode` is set to `PL_MODE_INACTIVE` at run-time or both `PL_NOEVENT` and `PL_NOCONTROL` are set to 1 (compile-time disabling), the function returns here.
- if `PL_NOEVENT` is not 1, the transmission thread is created and the context switches are initialized if enabled (compile time switch and enough run-time privilege)
- if `PL_NOCONTROL` is not 1, the command reception thread is created.
The declaration is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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](base_concepts.md.html#baseconcepts/c++specific/eventcollectionmechanism) section.
Almost all memory allocations of the `Palanteer` services are done in this function. For details, refer to the section [memory usage](index.html#c++memoryusage).
!!!
`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.
- It flushes to the server the last collected events, up to the call
- the last events are often critical for a debugging investigation
- It properly stops the `Palanteer` threads
- It cleans the resources (i.e. threads and memory)
The declaration is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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](base_concepts.md.html#baseconcepts/c++specific/eventcollectionmechanism) 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](#pldeclarethread) | Declares a thread | X | X |
| [plScope](#plscope) | Declares a scope (a named time range with optional children) | X | X |
| [plFunction](#plfunction) | Declares a scope with the current function name | X | X |
| [plBegin and plEnd](#plbeginandplend) | 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:
- it is easier to recognize
- it has a persistent identifier which makes scripting more reliable
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! warning
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.
!!! tip
In case of duplicated thread name, a suffix `#` 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).
* The start of the scope is the call of this function `plScope`
* The end of the scope corresponds to the end of the scope in the language
!!! Tip
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`](#plbeginandplend) and [`plEnd`](#plbeginandplend) described below in this chapter.
It has both group and dynamic string variants.
The declaration is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error 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.
!!! warning
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`](#plscope) whenever possible (see also [Manual scope](base_concepts.md.html#baseconcepts/c++specific/manualscopes)).
The declaration is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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.
!!! warning 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 `##`:
* The unit is stripped from the name, it is not displayed
* The unit increases the semantic of the events and make is more useful for the users
* Only events with consistent units can be plotted together
* "hexa" is a special and hardcoded unit which displays integer in hexadecimal
Ex: `"Duration##nanosecond"`, `"pointer##hexa"`, `"Banana##fruit"`
!!! note 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](#pldata) | Trace a named numerical value | X | (always dynamic) |
| [plText](#pltext) | Trace a text message | X | (always static) |
| [plVar](#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).
!!! warning
A string value is processed as a dynamic string.
To trace a static string value, use `plMakeString()` 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// Traces a value
void plData(const char* name, value);
// If the group is enabled, traces a value
void plgData(const char* group, const char* name, value);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Example of usage:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error
A hell of preprocessor and compiler errors are triggered if a non-static string is passed as a value. These errors usually start with:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// "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:
- a message from the application
- formatted through a printf-like interface
- automatically timestamped
- with a user specified level
- with a user specified "category" (i.e. a name to help filtering)
| Structure API | Description | Group variant |
|-----------------------------------------------|---------------------------------------------------------------------|:-------------:|
| [plSetLogLevelRecord](#plsetloglevelrecord) | Set the minimum log level to be recorded (default=all) | |
| [plSetLogLevelConsole](#plsetloglevelconsole) | Set the minimum log level to be displayed on console (default=warn) | |
| [plLogDebug](#pllogdebug) | Log a message with the `debug` level | X |
| [plLogInfo](#plloginfo) | Log a message with the `info` level | X |
| [plLogWarn](#pllogwarn) | Log a message with the `warn` level | X |
| [plLogError](#pllogerror) | Log a message with the `error` level | X |
In the viewer:
- a dedicated log window provides filtering capabilities on threads, levels and categories
- an indicator is visible for each log on top of the timeline for the associated thread (configurable level)
Four log levels are defined, in this order:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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.
!!! warning
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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.
!!! warning
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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`](#pllogdebug) function but logs with the level `info`.
### plLogWarn
This function is similar to the [`plLogDebug`](#pllogdebug) function but logs with the level `warn`.
### plLogError
This function is similar to the [`plLogDebug`](#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`.
!!! warning
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:
* `plMarkerDyn("category", "%s", dynamic_message)`
* `plLogWarn("category", "%s", dynamic_message)` or any log level
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](#pllockwait) | Trace a start of waiting on a lock | X | X | X |
| [plLockState](#pllockstate) | Trace a state of the lock (taken or not) | X | X | X |
| [plLockScopeState](#pllockscopestate) | Trace a state of the lock with an automatic unlocking | X | X | |
| [plLockNotify](#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](base_concepts.md.html#plstring_t).
Some examples of instrumentation of several usual primitives are shown [after the description of the lock API](#examplesoflockinstrumentation)
!!!
The locking process has two phases:
- waiting for the lock: the wait for lock is started, then ended with a positive or negative outcome
- using the lock: the lock is first taken, then released.
### 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:
- the end of the waiting phase
- the state of the lock: `true` if the lock is taken, or `false` if not.
!!!
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
- `true` state means the lock is taken by the thread
- `false` state means the lock is released (or not used) by the thread
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! tip
`plLockState` **must** be called just after the end of the waiting phase, if any, to mark its end
!!! tip
`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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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):
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
{
plLockWait("Resource"); // Start waiting for the lock "Resource"
std::unique_lock 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! tip
`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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
std::mutex database_mutex;
std::map databse;
void addUrl(const std::string& url, const std::string& result)
{
plLockWait("Database"); // Start waiting for the lock "Database"
std::lock_guard 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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 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 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.
!!! warning
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](#clihandler), the name of the CLI, the [CLI parameter specification](base_concepts.md.html#cliparameterspecification) 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void plRegisterCli(plCliHandler_t cliHandler, const char* name, const char* param_specification, const char* description);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An example of CLI registration is (see [Remote control](index.html#remotecontrol) for full example):
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
typedef void (*plCliHandler_t)(plCliIo& cio);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This "communication" object has two roles:
* Provide the input parameters via typed accessors
* These parameters are already checked for consistency versus the [CLI parameter specification](base_concepts.md.html#cliparameterspecification)
* Using the wrong accessor type is a bug and leads to an assertion failure.
* Format the command output, namely the binary success state and the text response
* Initial response is empty and texts provided by `addToResponse` are concatenated.
* A call to `setErrorState` indicate an execution failure (non-cancellable)
* Previous text response is cleared
* Some text can be provided directly in the call. Subsequent calls to `addToResponse` add up.
Its full prototype is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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](index.html#remotecontrol) and declared with the parameter spec "`min=int max=int`" is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Python
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:
- Use the compilation option [`PL_VIRTUAL_THREADS=1`](instrumentation_configuration_cpp.md.html#pl_virtual_threads) (in all files)
- Notify `Palanteer` of any virtual thread switch through the [`plAttachVirtualThread`](#plattachvirtualthread) and [`plDetachVirtualThread`](#pldetachvirtualthread) API
- this notification shall be called inside the assigned worker thread for proper association between the virtual thread and the OS worker thread
- typically in the virtual thread switch hook of the virtual thread framework
Optionally but recommended, the virtual thread name can be declared with [`plDeclareVirtualThread`](#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:
- Each virtual thread is seen as a "normal" thread on the server side (viewer or scripting)
- All events generated during the execution of a virtual thread are associated to this virtual thread, not to the OS thread.
- in the viewer, an interruption of the execution of a virtual thread is indicated as a "SOFTIRQ Suspend" for this thread
- Worker threads (OS thread) look "empty"
- only the CPU context switches, if enabled, are associated with them
- To get an usage overview, a "resource" with the name of each worker thread tracks slices of its time used by virtual threads.
### plDeclareVirtualThread
As for [OS threads](#pldeclarethread), 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
plDeclareVirtualThread("Fibers/Fiber %d", fiberId);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Also as for OS threads, virtual threads can be grouped.
!!! warning 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.
!!! warning
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.
!!! note Important
Always detach a thread before attaching a new one, else the resource will not correctly indicate the new virtual thread.
The declaration is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// externalVirtualThreadId: a unique external virtual thread identifier
void plAttachVirtualThread(uint32_t externalVirtualThreadId);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Example of usage:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
plAttachVirtualThread(newFiberId);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### plDetachVirtualThread
This function notifies `Palanteer` that the current virtual thread is detached from the current OS thread.
The declaration is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// 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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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:
- that the group is indeed defined, if you use groups
- that the string is really static, if you use an API which requires a static string
- that the logged variable is indeed one of the traceable type (i.e. C-string or a numerical value)
**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:
- `Palanteer` handles the non ambiguous definition (from C++11) int8_t, uint8_t, int16_t, uint16_t, int32_t uint32_t, int64_t and uint64_t
- these types do not correspond always to the "old" types. For instance, long long int is not always a int64_t
- some compilers get crazy when having to deal with such casts, hence the overload ambiguity
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](#plfunction): some "old" C++ compilers do not consider `__FUNCTION__` as `constexpr`.
In such case, either:
- switch to `plFunctionDyn()`, with the drawback of the non optimal dynamic string management
- use `plScope("manually copied function name")`
- use a more recent compiler, if possible
**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:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#define PL_GROUP_PL_VERBOSE 0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~