Profiling and tracing HPX applications#
HPX can send events about its tasks, schedulers, futures and parcels to an external profiler. The profiler shows these events on a timeline, so you can see which task ran on which worker thread, when a task was suspended, when it ran again, and which future started a continuation. This page explains how to build HPX with one of the supported profilers and how to read the data they show.
For the performance counters that HPX collects itself, see Performance counters.
Choosing a backend#
HPX supports three profilers. They are called backends. Pick exactly one of
them when you configure HPX with CMake. If more than one of
HPX_WITH_TRACY, HPX_WITH_ITTNOTIFY and
HPX_WITH_APEX is ON, the configuration step stops with an error.
Backend |
CMake option |
What it records |
How to view it |
|---|---|---|---|
Task zones and fibers, task lifecycle, futures and continuations, work stealing, parcels, locks (optional) |
The Tracy profiler (Linux, Windows, macOS) |
||
Tasks, task lifecycle, work stealing, markers, locks (optional), performance counters |
Intel VTune |
||
Task timers, performance counters, parcels |
APEX output, TAU and other tools (see the APEX HPX documentation) |
If no backend is enabled, all tracing hooks in HPX are empty functions.
Tracy#
Tracy is a profiler with a graphical viewer. A program built with the Tracy client sends its events over the network to the viewer, which shows them on a timeline while the program runs. HPX shows each of its tasks to Tracy as a fiber, so you can follow a task across worker threads.
Building HPX with Tracy#
Configure HPX with HPX_WITH_TRACY=ON. To get readable task
names, also set HPX_WITH_THREAD_DEBUG_INFO=ON (see
Naming your tasks):
$ cmake -S hpx -B build -G Ninja \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DHPX_WITH_TRACY=ON \
-DHPX_WITH_THREAD_DEBUG_INFO=ON
$ cmake --build build
You do not need to install Tracy first. At configure time, CMake downloads the
Tracy client sources from github.com/wolfpld/tracy and builds them together with HPX. The
option HPX_WITH_TRACY_TAG selects the Tracy version (default v0.14.1).
For builds without network access, point FETCHCONTENT_SOURCE_DIR_TRACY at a
local Tracy source tree of the same version, and CMake uses it instead of the
download.
HPX builds the Tracy client with these settings:
TRACY_ENABLE: the client is active. Tracy 0.14 turns it off by default.TRACY_ON_DEMAND: the program records events only while a profiler is connected (see Capturing a trace).TRACY_FIBERS: HPX tasks are shown as Tracy fibers.TRACY_STATIC: the Tracy client is a static library that is linked into the HPX core library, also when HPX is built as shared libraries.
In Debug builds the Tracy client also prints extra diagnostic messages
(TRACY_VERBOSE). If your project already defines a tracy::tracy target
before HPX is configured, HPX uses that target and does not apply the
settings above.
On Windows, HPX sets TRACY_DBGHELP_LOCK=HpxDbgHelp on the Tracy client,
so DbgHelp calls from Tracy and from the symbol lookup in HPX share a single
mutex.
Getting the Tracy profiler#
The profiler (the viewer) is not built with HPX. Build it from the same
Tracy version that HPX uses (HPX_WITH_TRACY_TAG, default v0.14.1).
The network protocol of Tracy can change between releases. If it did, the
profiler cannot connect to the client and reports an incompatible protocol
version.
$ git clone --branch v0.14.1 https://github.com/wolfpld/tracy.git
$ cd tracy
$ cmake -B profiler/build -S profiler -DCMAKE_BUILD_TYPE=Release
$ cmake --build profiler/build --config Release --parallel
$ cmake -B capture/build -S capture -DCMAKE_BUILD_TYPE=Release
$ cmake --build capture/build --config Release --parallel
If you set HPX_WITH_TRACY_TAG to another tag, clone that tag instead, so
the profiler and HPX use the same Tracy version.
This builds tracy-profiler, the graphical viewer, and tracy-capture and
tracy-capture-daemon, two command line tools that record traces to files.
The configure step downloads further libraries, so it needs network access and
git. The Tracy manual (tracy.pdf on the release page) lists the libraries that you
need to install first. The release page also has prebuilt profiler binaries.
Naming your tasks#
Tracy labels each task with its HPX thread description. HPX stores these
descriptions only when it is built with HPX_WITH_THREAD_DEBUG_INFO=ON.
Without this option every task is called <unknown>. The option also turns
on other per-task debug data (target address, parent reference, thread phase),
which adds work to every task.
When descriptions are stored, a task gets its name from:
the name given with
hpx::annotated_function(fromhpx/functional.hpp) when the task is created,the name given with
hpx::execution::experimental::with_annotationfor all tasks created through an executor, for example by a parallel algorithm,the name of the action, for tasks that run an action,
otherwise the description of the task that created it, or
<unknown>if there is none.
#include <hpx/functional.hpp>
#include <hpx/future.hpp>
int compute(int n);
hpx::future<int> f =
hpx::async(hpx::annotated_function(&compute, "compute"), 42);
Tasks that run as stackless threads (created with
hpx::threads::thread_stacksize::nostack) never suspend. They get no task
zone and no fiber in Tracy. Their lifecycle messages are still recorded.
Capturing a trace#
Start
tracy-profiler.Start your application:
$ ./bin/work_stealing_smoke --hpx:threads=4
In the start window of the profiler, click your program in the list of discovered clients. You can also type the address of the machine the program runs on (for example
127.0.0.1) and click Connect.
Because the client runs in on-demand mode, it records events only while a
profiler is connected. Events from before the connection are not part of the
trace. Task zones that started before the connection are not recorded, and
locks that were created before it record no lock events (lock events also need
HPX_WITH_TRACING_LOCK_EVENTS).
To record a short program from its start, type the address and click
Connect before you start the program. The profiler keeps trying to connect
until the program is running. Also set TRACY_NO_EXIT=1 for such programs:
the program then does not exit before a profiler has connected.
To record a trace without the graphical viewer, for example on a cluster node,
use tracy-capture and open the file later in tracy-profiler:
$ tracy-capture -o trace.tracy -a 127.0.0.1
-o names the output file, -a gives the address of the program
(localhost if not given) and -p the port. If the program is not
running yet, tracy-capture waits for it. Press Ctrl+C to stop the
capture early.
The Tracy client listens on TCP port 8086. It also announces itself on the
local network with UDP broadcast messages, which the profiler shows as the list
of discovered clients. If the program runs on another machine that the
broadcast does not reach, connect to that machine by address, or forward the
port, for example with ssh -L 8086:localhost:8086 <host>. The environment
variable TRACY_PORT changes the port the client listens on.
Reading the timeline#
Worker threads and task zones#
Each OS thread that HPX starts appears as a row named after the thread, for
example worker-thread#0. In a run with more than one locality the name
starts with the locality. Each time a task runs on a worker, HPX opens a zone
on that row, named with the task description. The value of the zone is the
thread phase, which counts how many times the task has run so far. Without
HPX_WITH_THREAD_DEBUG_INFO the phase is always 0.
The scheduler also opens teal zones named hpx::background_work while a
worker does background work, such as polling the network. Blue zones mark
points of interest inside HPX and come from HPX_TRACING_MARK_EVENT, for
example idle_callback and do_some_work in the scheduler,
fork_join_executor::bulk_sync_execute, do_work_chunk in the bulk
executors, and send_parcel / recv_parcel / parcel_scheduled in the
parcel layer.
The zones that HPX opens do not record call stacks. For task zones the call stack would show the same scheduler frames for every task. Messages do record call stacks (see Messages).
Task fibers#
Each task, except stackless (nostack) tasks, also runs on a fiber row,
named <description>_<id>, where the id is the address of the HPX thread.
You can follow a task on its fiber row when it moves from one worker to
another. The color of a zone on a fiber row depends on the worker it ran on, so
a change of color means that the task moved.
HPX reuses thread objects. A later task that reuses the same thread object appears on the same fiber row, so a fiber row can show more than one task under one name.
Messages#
HPX sends short text messages for important events. They appear as marks on
the timeline and in the Messages window of the profiler, where you can search
them. On platforms where Tracy can collect call stacks, each message also
records the call stack where it was sent, up to 10 frames deep. In the
messages, <id> is the address of the HPX thread and W<n> or W#<n>
is the number of a worker thread.
Message |
Color |
When |
|---|---|---|
|
grey |
A task was requested, before its thread object exists. |
|
green |
The thread object of a task was created. |
|
magenta |
A task starts or continues running on worker |
|
orange |
A task gave up its worker but stays ready to run. |
|
red |
A task waits, for example on a future or a timer. |
|
cyan |
A suspended task runs again. It is sent from the task itself, after its
|
|
dark green |
A task finished. |
|
purple |
The thread object of a task was removed. |
|
amber |
Worker |
|
blue-grey |
Worker |
The (Parent: <id>) part is only present when the parent task is known. The
messages Task ... and Work Stolen are controlled by
HPX_WITH_TRACING_LIFECYCLE_EVENTS,
HPX_WITH_TRACING_WORK_STEALING_EVENTS and the sample rate (see
Reducing the overhead).
Futures and continuations#
These messages connect a future to the continuations it starts. The address in the message is the address of the shared state of the future, so you can search for the same address on the producer side and on the consumer side. If a task is running when the message is sent, the message is also added to the text of its zone on the fiber row.
Message |
Color |
When |
|---|---|---|
|
green |
A value was stored in a future (producer side). |
|
red |
An exception was stored in a future (producer side). |
|
yellow |
The continuations of a ready future are started. |
|
white |
A continuation (for example from |
The plot Active Continuations shows how many continuations are running.
These events are controlled by HPX_WITH_TRACING_CAUSAL_EVENTS and are
never sampled.
Other data#
Frames: calls to
hpx::tracing::frame_markin your code split the timeline into frames (see Instrumenting your own code).Plots: performance counters printed with
--hpx:print-counter(or--hpx:print-counter-reset) are sent to Tracy as plots, one plot per counter, named with the full counter name (for example/runtime{locality#0/total}/uptime). One name can match several counters, for example one for each worker thread, and each gets its own plot. A value is added each time HPX queries the counter, by default once at shutdown. Use--hpx:print-counter-intervalto get more than one value. This also works with--hpx:print-counter-destination=none, which prints nothing. Histogram counters and other counters with several values are not sent.Locks: if HPX was built with
HPX_WITH_TRACING_LOCK_EVENTSset toON(the default isOFF),hpx::mutex,hpx::spinlockandhpx::util::spinlockare reported to Tracy as locks, which the profiler draws on the timeline. Only locks that were created while a profiler was connected record lock events.Application information: the HPX version and build configuration are sent to Tracy as application information, which the profiler keeps with the trace.
Clicking on a zone opens the zone information window, with the timing of the zone, its text and the messages sent while it was open.
Profiling distributed runs#
Each HPX locality is a separate process with its own Tracy client, so you
record one trace per locality. The zones send_parcel, recv_parcel and
parcel_scheduled always appear. To also see which parcel they belong to,
configure HPX with HPX_WITH_PARCEL_PROFILING=ON (it is
OFF by default unless APEX is enabled). HPX then gives every parcel an
id that travels with it, and sends these messages:
Message |
Color |
When |
|---|---|---|
|
blue |
A parcel was sent. |
|
green |
A parcel arrived and its header was read. |
|
purple |
The action of a parcel was put on the scheduler later than the receive,
for example for zero-copy receives, for messages with more than one
parcel, or for parcels that were routed through AGAS. |
To follow a parcel, search for its id in the Messages window of the sending
and of the receiving trace. Always use both halves of the id: the lower half
alone starts again on every locality. L#unknown means that the locality was
not known yet, for example for parcels sent during startup.
When several localities run on the same machine, every Tracy client needs its
own port. If TRACY_PORT is not set and port 8086 is taken, a client tries
the next ports (8087, 8088, … up to 8105). Each locality then appears as its
own entry in the list of discovered clients, with its port. Connect one
profiler window, or one tracy-capture with -p, to each of them.
tracy-capture-daemon -o <directory> records all clients it finds through
their broadcast messages, each to its own file in <directory>.
tracy-merge (built from the merge folder of Tracy) combines several
trace files into one. The merged trace keeps zones, messages and plots, but not
call stacks or lock events, and the times of the input traces are not
synchronized. See the Tracy manual for details.
Reducing the overhead#
When no profiler is connected, most tracing hooks only check whether a profiler is connected and return. While a profiler is connected, HPX sends a few messages for every task, and each message records a call stack. For programs with many small tasks this can slow down the run. There are two ways to reduce it.
Event classes can be compiled out one by one:
HPX_WITH_TRACING_LIFECYCLE_EVENTS,
HPX_WITH_TRACING_CAUSAL_EVENTS,
HPX_WITH_TRACING_WORK_STEALING_EVENTS and
HPX_WITH_TRACING_LOCK_EVENTS. The first three are ON by default,
lock events are OFF. Turning one OFF makes its hooks empty, so neither
the connection check nor the event itself runs. The arguments at each call
site are still evaluated. The lifecycle, work stealing and lock options apply
to the Tracy and ITT backends, the causal option to the Tracy backend only.
Lock events are OFF by default because they cost time even when no
profiler is connected. With HPX_WITH_TRACING_LOCK_EVENTS set to
ON, creating or destroying an hpx::mutex, hpx::spinlock or
hpx::util::spinlock registers it with Tracy. Every future holds a lock, so
this costs time for every future, and the memory used grows with the number of
locks created.
Instead of turning lifecycle events off, you can sample them.
HPX_WITH_TRACING_SAMPLE_RATE sets the default rate: with a rate of N,
only one task in N sends lifecycle and work stealing messages. The default is
1 (every task). You can set a different rate when you start the program,
with the hpx.tracing.sample_rate setting, for example
--hpx:ini=hpx.tracing.sample_rate=10 (see
the hpx.tracing section). Values
below 1 count as 1. Causal events and Task Staged are never sampled.
Task Staged is sent before the task exists, so there is nothing to sample
yet.
Instrumenting your own code#
Include hpx/modules/tracing.hpp to use the tracing API in your own code.
The calls compile with every backend. With APEX, or if no backend is
enabled, they do nothing.
#include <hpx/modules/tracing.hpp>
for (int step = 0; step != num_steps; ++step)
{
// starts a new frame named "step" in Tracy
hpx::tracing::frame_mark("step");
{
// a zone named "exchange_halo" until the end of this scope
HPX_TRACING_MARK_EVENT("exchange_halo");
exchange_halo();
}
compute();
}
hpx::tracing::frame_mark(name)marks a frame boundary. With Tracy, the profiler shows the frames above the timeline. With ITT, it emits a marker.HPX_TRACING_MARK_EVENT(name)opens a zone namednamethat ends at the end of the current scope (Tracy), or an ITT event that starts there and ends at the end of the scope (ITT).HPX_TRACING_PAUSE()andHPX_TRACING_RESUME()pause and resume the data collection of an ITT tool. They do nothing with the other backends.
Use a string literal for name in both calls. Tracy uses the address of the
string to tell frames apart, and for HPX_TRACING_MARK_EVENT the ITT backend
reads the name only the first time a call site runs.
The examples work_stealing_smoke, causal_chain_smoke,
causal_exception_smoke and background_work_smoke in
examples/quickstart use this API and describe in their comments what to
look for in Tracy.
Intel ITT API and VTune#
The Intel ITT API is a C API that a program uses to describe its work to an
analysis tool such as Intel VTune. When HPX is built with
HPX_WITH_ITTNOTIFY=ON, it reports its tasks and other events
through this API.
Building HPX with the ITT API#
To build HPX, you need the ITT API header ittnotify.h and the
ittnotify library. You can build them from the open-source ittapi
project:
$ git clone https://github.com/intel/ittapi.git
$ cmake -S ittapi -B ittapi/build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$HOME/ittapi-install
$ cmake --build ittapi/build --target install
$ cmake -S hpx -B build -G Ninja \
-DHPX_WITH_ITTNOTIFY=ON \
-DAmplifier_ROOT=$HOME/ittapi-install
$ cmake --build build
Amplifier_ROOT (see Additional tools and libraries used by HPX) tells CMake where to
find the ITT API: CMake looks for include/ittnotify.h and for the library
in lib or lib64 below this folder. The name is historical, any
installation of the ITT API works. Intel VTune also ships the ITT API. To use that
copy, set Amplifier_ROOT to the folder of the Intel VTune installation that
contains include/ittnotify.h.
The ITT backend always stores thread descriptions, so you do not need
HPX_WITH_THREAD_DEBUG_INFO to get task names. Name your tasks as described
in Naming your tasks.
Enabling the events at runtime#
An HPX program built with the ITT API sends no events unless you turn them
on at runtime with the hpx.use_itt_notify setting:
$ ./bin/my_app --hpx:ini=hpx.use_itt_notify=1
Setting the environment variable HPX_HAVE_ITTNOTIFY=1 has the same effect.
The events go to the collector library of the tool that runs the program, for
example Intel VTune:
$ vtune -collect threading -- ./bin/my_app --hpx:ini=hpx.use_itt_notify=1
threading is one of the analysis types of Intel VTune. vtune -help collect
lists all of them.
Without Intel VTune, you can check the events with the reference collector of
ittapi. It writes the task, thread name, counter creation and pause and
resume calls to a log file. It does not log markers, lock events or counter
values. Configure ittapi with -DITT_API_REFERENCE_COLLECTOR=ON, build
it again, and run, where <ittapi> is the folder you cloned:
$ export INTEL_LIBITTNOTIFY64=<ittapi>/build/bin/libittnotify_refcol.so
$ export INTEL_LIBITTNOTIFY_LOG_DIR=$PWD/itt_logs
$ mkdir -p itt_logs
$ ./bin/my_app --hpx:ini=hpx.use_itt_notify=1
What the ITT backend records#
Tasks and markers use the ITT domain hpx.
Tasks: each time a task runs on a worker, HPX records an ITT task with the task description as its name. Background work is recorded as
hpx::background.Task lifecycle: every HPX task gets an ITT id. Its ITT task begins when it starts or continues running and ends when it yields, suspends or completes. A requested task is recorded as a marker. Controlled by
HPX_WITH_TRACING_LIFECYCLE_EVENTS. The sample rate does not apply, every task is recorded.Work stealing: a
work_stolenmarker with the metadatathief,victimandtask. Controlled byHPX_WITH_TRACING_WORK_STEALING_EVENTS.Markers:
os_thread_sleepwhen a worker finds no work, and a marker for each call tohpx::tracing::frame_mark.Events:
HPX_TRACING_MARK_EVENTrecords an ITT event that lasts until the end of the scope.Locks:
hpx::mutex,hpx::spinlockandhpx::util::spinlockare reported through the ITT synchronization API. Controlled byHPX_WITH_TRACING_LOCK_EVENTS, which isOFFby default.Counters: performance counters printed with
--hpx:print-counterare reported as ITT counters.Thread names: the names of the OS threads that HPX starts.
The ITT backend does not record futures and continuations, and it does not record parcel events.
APEX#
HPX provides integration with APEX, which is a framework for application profiling using task timers and various performance counters Huck et al.[1]. TAU is an optional dependency when using APEX.
To build HPX with APEX, add HPX_WITH_APEX=ON to your
CMake configuration, and either:
set
HPX_WITH_FETCH_APEX=ONto let CMake download APEX and build it together with HPX.HPX_WITH_APEX_TAGselects the version (defaulthpx_v1.11.0) andHPX_WITH_APEX_REPOSITORYthe repository, orset
Apex_ROOTto an existing APEX installation.
Optionally, add Tau_ROOT=$PATH_TO_TAU as well. With APEX enabled,
HPX_WITH_PARCEL_PROFILING is ON by default. Please see the
APEX HPX documentation for detailed instructions on using APEX with HPX.