Skip to content

Commit

Permalink
Add support of virtual threads (code, doc, tests, example)
Browse files Browse the repository at this point in the history
  • Loading branch information
dfeneyrou committed Jun 26, 2021
1 parent bc19c9f commit 0e306d7
Show file tree
Hide file tree
Showing 21 changed files with 602 additions and 62 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Type of build" FORCE)
endif()
message("Build type: ${CMAKE_BUILD_TYPE} (change with -DCMAKE_BUILD_TYPE=<Debug|Release|RelWithDebInfo|MinSizeRel|Asan>)")
message("Custom configuration flags can be passed with -DCUSTOM_FLAGS=\"<flags>\"")

# Store output in an easy location
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,16 @@ The complete documentation is accessible inside the repository, and online:
- [Scripting API](https://dfeneyrou.github.io/palanteer/scripting_api.md.html)
- [More](https://dfeneyrou.github.io/palanteer/more.md.html)

## OS Support

Viewer and scripting library:
- Linux 64 bits
- Windows 10

Instrumentation libraries:
- Linux 32 or 64 bits (tested on PC and armv7l)
- Windows 10
- Support for [virtual threads](docs/instrumentation_api_cpp.md.html#c++instrumentationapi/virtualthreads) (userland threads, like fibers)

## Requirements

Expand Down
251 changes: 227 additions & 24 deletions c++/palanteer.h

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions c++/test/test_instru_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,21 @@ def test_build_instru24():
def test_build_instru25():
"""USE_PL=0 PL_IMPL_STACKTRACE=0"""
build_target("testprogram", test_build_instru25.__doc__)

# Virtual threads feature
@declare_test("build instrumentation")
def test_build_instru26():
"""USE_PL=1 PL_VIRTUAL_THREADS=1"""
build_target("testprogram", test_build_instru26.__doc__)

# Virtual threads feature
@declare_test("build instrumentation")
def test_build_instru27():
"""USE_PL=1 PL_NOEVENT=0 PL_VIRTUAL_THREADS=1"""
build_target("testprogram", test_build_instru27.__doc__)

# Virtual threads feature
@declare_test("build instrumentation")
def test_build_instru28():
"""USE_PL=0 PL_VIRTUAL_THREADS=1"""
build_target("testprogram", test_build_instru28.__doc__)
4 changes: 3 additions & 1 deletion c++/testprogram/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CUSTOM_FLAGS}")
add_executable ("testprogram" testProgram.cpp testPart.cpp testPart.h)
target_link_libraries ("testprogram" ${STACKTRACE_LIBS} Threads::Threads)
target_include_directories("testprogram" PRIVATE ../../c++)
target_compile_options ("testprogram" PRIVATE -DUSE_PL=1)
if(NOT CUSTOM_FLAGS MATCHES ".*USE_PL=0.*")
target_compile_options ("testprogram" PRIVATE -DUSE_PL=1)
endif()

# Test program executable with deactivated Palanteer
# =======================
Expand Down
130 changes: 130 additions & 0 deletions c++/testprogram/testPart.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,133 @@ associatedTask(int groupNbr, const char* groupName, int crashKind)
plVar(dummyValue);
plEnd("Final result");
}



#if USE_PL==1 && PL_VIRTUAL_THREADS==1

// ================================
// Functions of the "Fiber" tasks
// ================================

// Fibers are shared
std::mutex fibersMx;


// Thread entry point
void
fiberWorkerTask(int workerThreadNbr, std::vector<Fiber>* fiberPool, std::vector<Fiber>* fiberWaitingList, std::atomic<int>* sharedJobIndex)
{
// This tasks stimulates the 3 required API for enabling support of virtual threads:
// 1) plDeclareVirtualThread, to associate the external thread ID to a name
// 2) plAttachVirtualThread, to attach a virtual threads to the current worker thread
// 3) plDetachVirtualThread, to detach the virtual thread. The thread is back to the OS thread

// Declare the worker thread name with a dynamic string
char tmpStr[64];
snprintf(tmpStr, sizeof(tmpStr), "Fiber workers/Fiber worker %d", workerThreadNbr+1);
plDeclareThreadDyn(tmpStr);

// Log on the OS thread
plMarker("threading", "Fiber worker thread creation");

// Same job definition on all workers
constexpr int JOBS_QTY = 6;
const char* jobNames[JOBS_QTY] = { "Load texture", "Update particules", "Animate chainsaw",
"Skeleton interpolation", "Fog of War generation", "Free arena memory pools" };

int iterationNbr = 0;
while(iterationNbr<50 || !fiberWaitingList->empty()) {
++iterationNbr;
Fiber fiber;

// Dice roll
int dice = globalRandomGenerator.get(0, 99);

// 1/3 chance to resume a waiting job (unless we are at the end)
if(!fiberWaitingList->empty() && (dice<33 || iterationNbr>=20)) {
fibersMx.lock();
if(fiberWaitingList->empty()) {
fibersMx.unlock();
continue;
}
// Take a random waiting fiber
int idx = globalRandomGenerator.get(0, fiberWaitingList->size());
fiber = (*fiberWaitingList)[idx];
fiberWaitingList->erase(fiberWaitingList->begin()+idx);
fibersMx.unlock();
}

// 1/4 chance to idle
else if(dice>75) {
std::this_thread::sleep_for(std::chrono::milliseconds(globalRandomGenerator.get(10, 30)));
continue;
}

// Else take a new job if some fibers are available
else {
fibersMx.lock();
if(fiberPool->empty()) {
fibersMx.unlock();
continue;
}
// Pick a fiber
fiber = fiberPool->back(); fiberPool->pop_back();
fibersMx.unlock();
fiber.currentJobId = -1;
}
// From here, we have a fiber (to start using or interrupted)

// Switch to this "fiber" =
plAttachVirtualThread(fiber.id); // ==> First API under check
if(!fiber.isNameAlreadyDeclared) {
snprintf(tmpStr, sizeof(tmpStr), "Fibers/Fiber %d", fiber.id);
plDeclareVirtualThread(fiber.id, tmpStr); // ==> Second API under check
fiber.isNameAlreadyDeclared = true;
}

// Job start?
bool doEndJob = true;
if(fiber.currentJobId<0) {
// Refill by picking the next job
fiber.currentJobId = (sharedJobIndex->fetch_add(1)%JOBS_QTY);

// And start it
plBeginDyn(jobNames[fiber.currentJobId]);
std::this_thread::sleep_for(std::chrono::milliseconds(globalRandomGenerator.get(10, 30)));
plData("Worker Id", workerThreadNbr);
plData("Fiber-job Id", fiber.currentJobId);

// Dice roll: 60% chance to end the job without interruption. Else it will go on the waiting list
doEndJob = (globalRandomGenerator.get(0, 99)>40);
plData("Scheduling", doEndJob? "One chunk" : "Interrupted");
}

if(doEndJob) {
// End the job
std::this_thread::sleep_for(std::chrono::milliseconds(globalRandomGenerator.get(10, 30)));
plEndDyn(jobNames[fiber.currentJobId]);
fiber.currentJobId = -1;

// Put back the fiber in the pool
fibersMx.lock();
fiberPool->push_back(fiber);
fibersMx.unlock();
plDetachVirtualThread(false); // Third API to check
}
else {
// Interrupt the job, put the fiber on the waiting list
fibersMx.lock();
fiberWaitingList->push_back(fiber);
fibersMx.unlock();
plDetachVirtualThread(true); // Switch back to the OS thread
}

} // End of loop on iterations

plDetachVirtualThread(false); // Switch back to the OS thread
plMarker("threading", "Fiber worker thread end");
}


#endif
12 changes: 12 additions & 0 deletions c++/testprogram/testPart.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,15 @@ extern RandomLCM globalRandomGenerator;
void associatedTask(int groupNbr, const char* groupName, int crashKind);

float busyWait(int kRoundQty);

#if USE_PL==1 && PL_VIRTUAL_THREADS==1

struct Fiber {
int id;
int currentJobId = -1; // -1 means none
bool isNameAlreadyDeclared = false;
};

void fiberWorkerTask(int workerThreadNbr, std::vector<Fiber>* fiberPool, std::vector<Fiber>* fiberWaitingList,
std::atomic<int>* sharedJobIndex);
#endif
25 changes: 25 additions & 0 deletions c++/testprogram/testProgram.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,31 @@ collectInterestingData(plMode mode, const char* buildName, int durationMultiplie
(crashThreadGroupNbr==threadGroupNbr)? crashKind : -1));
}

#if USE_PL==1 && PL_VIRTUAL_THREADS==1
// This stimulation is added only if the "virtual threads" feature is activated.
// A "virtual thread" means a thread that is not controlled by the OS kernel but managed in user space. Typical usages are "fibers"
// or DES simulations.
//
// The goal here is to test the specific APIs that enable the feature, and obviously not to implement such framework.
// Some OS worker threads are created, they share and run the fake "fibers" (as explained above, no saving of stack context nor registers...)
// Jobs are represented by a number, they shall be executed one after the other, in loop.
constexpr int WORKER_THREAD_QTY = 2;
constexpr int FIBERS_QTY = 10; // Caution if you change it: it is also subjected to the thread limitation for tracking, even if not real threads.
std::atomic<int> sharedJobIndex(0);
std::vector<Fiber> fiberPool;
std::vector<Fiber> fiberWaitingList;
for(int i=0; i<FIBERS_QTY; ++i) {
Fiber f;
f.id = FIBERS_QTY-1-i; // Small numbers on top, as it is a stack
fiberPool.push_back(f);
}
// Create the worker threads that will schedule shared jobs in loop. They will stop by themselves.
for(int workerThreadNbr=0; workerThreadNbr<WORKER_THREAD_QTY; ++workerThreadNbr) {
threads.push_back(std::thread(fiberWorkerTask, workerThreadNbr, &fiberPool, &fiberWaitingList, &sharedJobIndex));
}

#endif

// Wait for threads completion
plLockWait("Global Synchro");
for(std::thread& t : threads) t.join();
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ <h1> @@ <a href="#">Overview</a> </h1>
plgVar(MYGROUP, monsterHealth, monsterAttackCoef, monsterName);
// Text logging
plgText(MYGROUP, "Level one reached");
plgText(MYGROUP, "stage", "Level one reached");
// Scope tracking (automatically closed at scope end)
plgScope(MYGROUP, "superFunction");
Expand Down
102 changes: 99 additions & 3 deletions docs/instrumentation_api_cpp.md.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ <h1> @@ <a href="base_concepts.md.html">Base concepts</a> </h1>
<h1> @@ <a href="#">C++ Instrumentation API</a> </h1>


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

## Initialization

Expand Down Expand Up @@ -153,7 +153,7 @@ <h1> @@ <a href="#">C++ Instrumentation API</a> </h1>
| [plDeclareThread](#pldeclarethread) | Declares a thread | | 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 | |
| [plBegin and plEnd](#plbeginandplend) | Declares manually the start and the end of a scope (with moderation) | X | X |
### plDeclareThread
Expand Down Expand Up @@ -298,6 +298,19 @@ <h1> @@ <a href="#">C++ Instrumentation API</a> </h1>
// 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);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Expand All @@ -307,7 +320,7 @@ <h1> @@ <a href="#">C++ Instrumentation API</a> </h1>

!!! warning Important
Such events **must** be located inside a scope, never at the tree root. <br/>
Indeed, they carry only the logged data and are not timestamped.
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. <br/>
They can optionally be associated to a `unit` by ending the event name with `##<unit>`:
Expand Down Expand Up @@ -896,7 +909,90 @@ <h1> @@ <a href="#">C++ Instrumentation API</a> </h1>
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 exemples are "fibers", or a simulated OS environment. <br/>
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

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. <br/>
This function associates the provided name to the external virtual thread identifier. The name of the OS thread is unchanged.

It can be called from any thread.

The declaration is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
// externalVirtualThreadId: a unique external virtual thread identifier. Shall not be 0xFFFFFFFF
// name: the name of the virtual thread
void plDeclareVirtualThread(uint32_t externalVirtualThreadId, const char* name);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Example of usage:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int fiberId = 37;
char tmpStr[64];
snprintf(tmpStr, sizeof(tmpStr), "Fibers/Fiber %d", fiberId+1);
plDeclareVirtualThread(fiberId, tmpStr);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Also as for OS threads, virtual threads can be grouped.

### plAttachVirtualThread

This function notifies `Palanteer` of a virtual thread attachment to the current OS thread. <br/>

!!! 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. <br/>
This information is used to display the time slice when the thread is inactive.
Example of usage:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
plDetachVirtualThread(false);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

## Troubleshootings

Expand Down
Loading

0 comments on commit 0e306d7

Please sign in to comment.