Recording In-App Traces with the C SDK

In this guide, you'll learn how to:

  • Use the Perfetto C SDK to add custom trace points to a C application.
  • Record a trace containing your custom events, fully in-process.
  • Visualize the trace in the Perfetto UI.
  • Programmatically analyze the trace using PerfettoSQL.

The Perfetto C SDK is the ABI-stable foundation that all of Perfetto's language bindings are built on. Use it when you need a stable-by-design ABI, a shared-library boundary, no C++17 dependency, or an FFI target for another language. If you are writing a plain C++ application, the C++ SDK is more ergonomic — see Choosing a Perfetto SDK to decide.

WARNING: The C SDK is not yet stable — its API and ABI are subject to change. See ABI stability before depending on it.

TIP: The complete, runnable code for this guide is in the repository at examples/shared_lib/example_shlib_in_process.c.

Setup

The C SDK is distributed as the libperfetto_c shared library plus the public headers under include/perfetto/public/. Check out Perfetto and build the library:

git clone https://github.com/google/perfetto.git
cd perfetto
tools/install-build-deps
tools/gn gen out/linux
tools/ninja -C out/linux libperfetto_c

This produces out/linux/libperfetto_c.so. To build an application against it, point your compiler at the checkout's include/ directory for the headers and at that shared library to link.

Save the complete example for this guide — example_shlib_in_process.c, the code assembled across the sections below — locally as example.c, then build it with whichever toolchain fits your project:

NOTE: The C SDK requires C11 or later (for atomics), built with the compiler's default GNU dialect — which is what you get out of the box, so no -std flag is needed. Do not force strict -std=c11: it disables the POSIX extensions the headers rely on (e.g. syscall()), and the build will fail. If you must set the standard explicitly, use -std=gnu11 (or add -D_GNU_SOURCE). On Windows, C11 atomics are not well supported, so the C SDK targets Linux, macOS and Android.

libperfetto_c is a shared library, so the dynamic loader must find it at run time. Because it lives in the GN output directory rather than a system path, point LD_LIBRARY_PATH at it when you run the program (on macOS, use DYLD_LIBRARY_PATH):

LD_LIBRARY_PATH="$PERFETTO_OUT" ./example

On success the program writes example.pftrace to the current directory and prints:

Wrote example.pftrace

NOTE: The in-process tracing service also logs informational messages to stderr while the trace runs — lines such as Configured tracing session 1, ... and Producer connected. These are normal and safe to ignore; the only real output is the Wrote example.pftrace line above. (A debug build of the library — the default from tools/gn gen out/linux — is the most verbose; building with tools/gn gen --args='is_debug=false' out/linux drops the debug-only lines.)

Adding your first instrumentation

Initialize Perfetto and declare your tracing categories. Categories are declared with an X-macro list so they can be defined once and registered together:

#include "perfetto/public/producer.h"
#include "perfetto/public/te_category_macros.h"
#include "perfetto/public/te_macros.h"
#include "perfetto/public/track_event.h"

// Declare the categories used by this program.
#define EXAMPLE_CATEGORIES(C) \
  C(rendering, "rendering", "Rendering events")

PERFETTO_TE_CATEGORIES_DEFINE(EXAMPLE_CATEGORIES)

int main(void) {
  struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
  args.backends = PERFETTO_BACKEND_IN_PROCESS;
  PerfettoProducerInit(args);

  PerfettoTeInit();
  PERFETTO_TE_REGISTER_CATEGORIES(EXAMPLE_CATEGORIES);
  // ...
}

You can now add instrumentation points. They emit events only when tracing is enabled for their category. The main building block is the PERFETTO_TE macro, whose first argument is a category and whose second argument is the event type:

  • PERFETTO_TE_SLICE_BEGIN("name") / PERFETTO_TE_SLICE_END() record a slice — a duration on a track. Slices nest.
  • PERFETTO_TE_INSTANT("name") records an instant — a single point in time.
  • PERFETTO_TE_COUNTER() records a counter value (see Track Events).

Additional arguments annotate the event. For example, PERFETTO_TE_ARG_INT64 attaches a debug annotation:

static void DrawPlayer(int player_number) {
  PERFETTO_TE(rendering, PERFETTO_TE_SLICE_BEGIN("DrawPlayer"),
              PERFETTO_TE_ARG_INT64("player_number", player_number));
  // ... draw the player ...
  PERFETTO_TE(rendering, PERFETTO_TE_SLICE_END());
}

static void DrawGame(void) {
  PERFETTO_TE(rendering, PERFETTO_TE_SLICE_BEGIN("DrawGame"));
  DrawPlayer(1);
  DrawPlayer(2);
  PERFETTO_TE(rendering, PERFETTO_TE_SLICE_END());
}

Collecting your first app trace

With the in-process backend, your program is both the producer of events and the controller of the tracing session. Recording has three steps: build a TraceConfig, start a session, and read the result back out.

The TraceConfig is a protobuf message. The C SDK builds it with the protozero serialization helpers and the generated .pzc.h accessors. This is more verbose than the C++ SDK's typed builders, but requires no C++ runtime:

#include "perfetto/public/abi/heap_buffer.h"
#include "perfetto/public/pb_msg.h"
#include "perfetto/public/protos/config/data_source_config.pzc.h"
#include "perfetto/public/protos/config/trace_config.pzc.h"
#include "perfetto/public/protos/config/track_event/track_event_config.pzc.h"
#include "perfetto/public/stream_writer.h"
#include "perfetto/public/tracing_session.h"

#include <stdio.h>   // fopen, fwrite, fclose
#include <stdlib.h>  // malloc, free
#include <string.h>  // strlen

// Returns a malloc'd serialized TraceConfig; caller frees it.
static void* BuildTraceConfig(size_t* size) {
  struct PerfettoPbMsgWriter writer;
  struct PerfettoHeapBuffer* hb = PerfettoHeapBufferCreate(&writer.writer);
  struct perfetto_protos_TraceConfig cfg;
  PerfettoPbMsgInit(&cfg.msg, &writer);

  // A 1 MiB in-memory buffer.
  struct perfetto_protos_TraceConfig_BufferConfig buffers;
  perfetto_protos_TraceConfig_begin_buffers(&cfg, &buffers);
  perfetto_protos_TraceConfig_BufferConfig_set_size_kb(&buffers, 1024);
  perfetto_protos_TraceConfig_end_buffers(&cfg, &buffers);

  // Enable the "track_event" data source with the "rendering" category.
  struct perfetto_protos_TraceConfig_DataSource data_sources;
  perfetto_protos_TraceConfig_begin_data_sources(&cfg, &data_sources);
  struct perfetto_protos_DataSourceConfig ds_cfg;
  perfetto_protos_TraceConfig_DataSource_begin_config(&data_sources, &ds_cfg);
  perfetto_protos_DataSourceConfig_set_cstr_name(&ds_cfg, "track_event");
  struct perfetto_protos_TrackEventConfig te_cfg;
  perfetto_protos_DataSourceConfig_begin_track_event_config(&ds_cfg, &te_cfg);
  perfetto_protos_TrackEventConfig_set_enabled_categories(&te_cfg, "rendering",
                                                          strlen("rendering"));
  perfetto_protos_DataSourceConfig_end_track_event_config(&ds_cfg, &te_cfg);
  perfetto_protos_TraceConfig_DataSource_end_config(&data_sources, &ds_cfg);
  perfetto_protos_TraceConfig_end_data_sources(&cfg, &data_sources);

  size_t sz = PerfettoStreamWriterGetWrittenSize(&writer.writer);
  void* buf = malloc(sz);
  PerfettoHeapBufferCopyInto(hb, &writer.writer, buf, sz);
  PerfettoHeapBufferDestroy(hb, &writer.writer);
  *size = sz;
  return buf;
}

NOTE: The example above is written for readability. To match the Perfetto tree's C style, the repository version hoists its declarations to the top of each scope. Both compile to the same thing.

Then set up the session, start recording, and run your workload:

size_t cfg_size = 0;
void* cfg = BuildTraceConfig(&cfg_size);

struct PerfettoTracingSessionImpl* session =
    PerfettoTracingSessionCreate(PERFETTO_BACKEND_IN_PROCESS);
PerfettoTracingSessionSetup(session, cfg, cfg_size);
free(cfg);
PerfettoTracingSessionStartBlocking(session);

DrawGame();  // Your instrumented workload.

TIP: Functions with Blocking in their name suspend the calling thread until the operation completes. Asynchronous variants (...Async) are also available.

Finally, stop tracing and write the buffered data to a file. The read API delivers the trace in chunks through a callback:

static void ReadTraceCb(struct PerfettoTracingSessionImpl* session,
                        const void* data, size_t size, bool has_more,
                        void* user_arg) {
  fwrite(data, 1, size, (FILE*)user_arg);
}

// ...
PerfettoTracingSessionStopBlocking(session);
FILE* f = fopen("example.pftrace", "wb");
PerfettoTracingSessionReadTraceBlocking(session, ReadTraceCb, f);
fclose(f);
PerfettoTracingSessionDestroy(session);

Visualizing your first app trace

Open the example.pftrace file with https://ui.perfetto.dev/. You will see the DrawGame and DrawPlayer slices captured by your instrumentation:

Track event example

Querying your first app trace

As well as visualizing traces, Perfetto can query them with SQL. In the Perfetto UI, open the “Query (SQL)” tab:

Perfetto UI Query SQL

Run a query with Ctrl/Cmd + Enter. For example, to see each DrawPlayer invocation, how long it took, and its player_number annotation:

SELECT
  dur AS duration_ns,
  EXTRACT_ARG(slice.arg_set_id, 'debug.player_number') AS player_number
FROM slice
WHERE slice.name = 'DrawPlayer';

SQL query example

Combined in-app and system tracing

In-app tracing shows your application in isolation. Its real power comes from combining it with a system-wide trace, so your events line up with CPU scheduling, memory and I/O.

To do that, initialize the SDK with the system backend instead of the in-process backend:

struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
args.backends = PERFETTO_BACKEND_SYSTEM;
PerfettoProducerInit(args);
PerfettoTeInit();
PERFETTO_TE_REGISTER_CATEGORIES(EXAMPLE_CATEGORIES);

Your application now only produces events; the external traced service controls when tracing starts and stops. You no longer create a PerfettoTracingSession in-process — instead, record a trace with the standard system tracing tools, enabling the track_event data source alongside any system data sources (e.g. linux.ftrace). Your app's custom tracks appear on the same timeline as the system tracks. See examples/shared_lib/example_shlib_track_event.c for a system-backend example.

Next steps

  • Track Events: the full range of event types — counters, flows, custom tracks, and more.
  • Custom Data Sources: emit strongly-typed, high-volume data with your own protobuf schema.
  • C SDK Reference: headers, functions, and the stability contract.