blob: 46712437fcfd97e2c69d2d9ae4e47bfd6a272c14 [file] [view] [edit]
# Extending TrackEvent with Custom Protos
Perfetto's trace format is extensible: you can attach your own strongly-typed
fields to `TrackEvent` without forking Perfetto or modifying its upstream proto
definitions. This is done with
[protobuf extensions](https://developers.google.com/protocol-buffers/docs/overview#extensions),
and it is a fully supported mechanism.
This is the recommended way to add custom structured data to your traces. It
works end-to-end: events are written with type-safe accessors from the C++
SDK or by emitting protobuf bytes directly when hand-generating a trace,
automatically parsed into the `args` table in Trace Processor, and displayed
in the Perfetto UI.
Use extensions when:
- You want more than unstructured debug annotations (strings, ints) — your event
has structure that SQL queries will benefit from.
- You don't want to (or cannot) upstream your proto definitions to Perfetto.
- You need the same event schema across producers, Trace Processor, and the
UI without coordinated rollouts.
This guide has two parts:
- [**Fundamentals**](#fundamentals) defining the `.proto`, delivering
descriptors to Trace Processor, and querying extension fields in SQL.
Read this first; it applies to every producer.
- **Emitting events** pick the path that fits your setup:
- [With the C++ SDK](#emitting-events-with-the-c-sdk) typed or inline
field access from `TRACE_EVENT`.
- [Without the SDK](#emitting-events-without-the-sdk) any tool that
writes Perfetto protobufs directly, e.g. when
[converting arbitrary data to Perfetto](/docs/getting-started/converting.md).
## Fundamentals
### Defining an extension
Split your schema across two `.proto` files: a *data* file that defines the
nested message types your fields carry, and an *extension* file that hooks
those types onto [`TrackEvent`](/docs/instrumentation/track-events.md) at
specific field numbers. This split is what the Python walkthrough relies on
(the data file is compiled for runtime use; the extension file is only
compiled to a `FileDescriptorSet` for Trace Processor), and it keeps the C++
SDK layout symmetric.
**File 1 `acme_data.proto`** is your regular data schema.
```protobuf
syntax = "proto2";
package com.acme;
message AcmeRequestMetadata {
optional string endpoint = 1;
optional uint32 priority = 2;
}
```
**File 2 `acme_extension.proto`** is the extension *hook*. By convention,
place the `extend` block inside a wrapper message: this is required if you
plan to generate C++ SDK bindings [Protozero](/docs/design-docs/protozero.md)
uses the wrapper message name as the generated class name and recommended
regardless for portability.
```protobuf
syntax = "proto2";
import "protos/perfetto/trace/perfetto_trace.proto";
import "acme_data.proto";
package com.acme;
message AcmeExtension {
extend perfetto.protos.TrackEvent {
optional string request_id = 9900;
repeated int32 retry_latencies_ms = 9901;
optional AcmeRequestMetadata request_metadata = 9902;
}
}
```
Place a copy of `perfetto_trace.proto`
([download from GitHub](https://github.com/google/perfetto/blob/main/protos/perfetto/trace/perfetto_trace.proto))
under `protos/perfetto/trace/` so the import resolves. The final layout:
```
project/
├── protos/perfetto/trace/perfetto_trace.proto # from the Perfetto repo
├── acme_data.proto
└── acme_extension.proto
```
Field numbers 1000 and above are reserved for extensions. Pick a range that
won't collide with other extension producers you share traces with.
### Making extensions visible to Trace Processor and the UI
Trace Processor needs the proto descriptors for your extensions in order to
parse them. Once the descriptors are available, every extension field is
automatically decoded and inserted into the `args` table — no per-field
registration is required in Trace Processor itself.
There are three ways to deliver descriptors:
#### Option 1: Embed descriptors in the trace (`ExtensionDescriptor` packet)
This is the most portable option: the trace is self-describing, so Trace
Processor can parse it anywhere without extra configuration.
Compile your `.proto` to a `FileDescriptorSet` (e.g. `protoc --include_imports
--descriptor_set_out=acme.desc acme_extension.proto`) and prepend an
[`ExtensionDescriptor`](/docs/reference/trace-packet-proto.autogen#ExtensionDescriptor)
packet to the trace containing the bytes of that descriptor set.
The tracing service can do this automatically if you pass the descriptor set
into `TracingService::InitOpts::extension_descriptors` when starting the
service. Set `TraceConfig.disable_extension_descriptors = true` if you need to
opt out for a particular session.
For writers that don't use the C++ SDK, the
[synthetic track event walkthrough](/docs/reference/synthetic-track-event.md#proto-extensions)
shows this approach end-to-end in Python, including how to compile a
descriptor set and embed it in the trace.
#### Option 2: Android system-wide descriptors
On Android, `traced` reads descriptor sets from
`/etc/tracing_descriptors.gz` and `/vendor/etc/tracing_descriptors.gz` at
startup and emits them into every trace as `ExtensionDescriptor` packets. Ship
your extension's descriptor set to one of these paths to cover all traces
recorded on the device.
> NOTE: This was added to Perfetto in Feb 2026 via
> [RFC-0017](https://github.com/google/perfetto/discussions/4783), so it only
> works on Android releases that bundle a Perfetto build from that date or
> later — concretely, Android 16 QPR2 and later major releases. On earlier
> releases `traced` does not read these paths; use Option 1 or Option 3
> instead.
#### Option 3: Extension Servers (UI side)
If you run a shared [Extension Server](/docs/visualization/extension-servers.md)
for your team, add your descriptors to it. The Perfetto UI fetches descriptors
from the server at startup and uses them when opening any trace — no per-trace
embedding required. This is handy when the producers cannot be modified (e.g.
recordings from older versions).
### Querying extension fields in SQL
Every extension field that Trace Processor can decode is exposed in the
[`args`](/docs/analysis/sql-tables.autogen#args) table, keyed by the extension
field name. The easiest way to read a value is with the `EXTRACT_ARG`
built-in, which takes an `arg_set_id` and a key and returns the matching
value. Keys use dot notation for nested messages and `[N]` indexing for
repeated fields:
```sql
SELECT
slice.name,
EXTRACT_ARG(slice.arg_set_id, 'request_id') AS request_id,
EXTRACT_ARG(slice.arg_set_id, 'request_metadata.endpoint') AS endpoint,
EXTRACT_ARG(slice.arg_set_id, 'retry_latencies_ms[0]') AS first_retry_ms
FROM slice
WHERE EXTRACT_ARG(slice.arg_set_id, 'request_id') IS NOT NULL;
```
If you need to iterate over all elements of a repeated field, join against
the `args` table directly and filter by key prefix.
For interactive exploration, the Perfetto UI's details panel also displays
extension fields on the selected slice.
### Limitations
- Extensions are currently parsed by Trace Processor only for `TrackEvent`.
Extending other messages works for writing but not for automatic
args-table decoding.
- The C++ SDK's Protozero code generation requires extensions to live inside
a wrapper message. Non-SDK producers can technically put the `extend`
block at file scope, but the wrapper convention is recommended for
portability.
## Emitting events with the C++ SDK
The [Tracing SDK](/docs/instrumentation/tracing-sdk.md) supports two styles of
extension emission.
### Typed field access
Pass your wrapper message as a template parameter to `ctx.event<...>()` to get
setters for the extended fields alongside all built-in `TrackEvent` fields:
```cpp
#include "acme_extension.pbzero.h" // Generated from your .proto.
TRACE_EVENT("my_cat", "HandleRequest", [&](perfetto::EventContext ctx) {
auto* event = ctx.event<perfetto::protos::pbzero::AcmeExtension>();
event->set_request_id("req-42");
event->add_retry_latencies_ms(12);
event->add_retry_latencies_ms(34);
event->set_request_metadata()->set_endpoint("/api/v1/search");
});
```
### Inline field access
For simple cases, pass field metadata and values directly as extra arguments to
`TRACE_EVENT`:
```cpp
TRACE_EVENT(
"my_cat", "HandleRequest",
perfetto::protos::pbzero::AcmeExtension::kRequestId, "req-42",
perfetto::protos::pbzero::AcmeExtension::kRetryLatenciesMs,
std::vector<int>{12, 34});
```
## Emitting events without the SDK
If you're hand-writing Perfetto protobufs for example, from Python, Java,
or any other language while
[converting arbitrary data to Perfetto](/docs/getting-started/converting.md)
extensions work the same way: set your extension field on the `TrackEvent`
message with your language's protobuf library, then deliver the descriptor
set as described in
[Making extensions visible to Trace Processor](#making-extensions-visible-to-trace-processor-and-the-ui).
For a complete worked walkthrough in Python — defining the `.proto` files,
compiling descriptors, emitting events with wire-format splicing, embedding
the descriptor set in the trace, and querying the result — see
[Attaching Custom Typed Fields with Proto Extensions](/docs/reference/synthetic-track-event.md#proto-extensions)
in the Advanced Guide to Programmatic Trace Generation.