npm package: @paulirish/trace_engine
Description: This package contains the trace engine implementation used by the DevTools Performance Panel.
Latest version: 0.0.64

# Trace Model (NOT FOR PUBLIC CONSUMPTION)

This package contains the trace engine implementation used by the DevTools Performance Panel.

⚠️ The API is not stable and it's fairly likely that upgrades will break you (at some point).
But the breakages should be obvious exceptions or type failures.

## API quickstart

```js
import * as TraceModel from '@paulirish/trace_engine';

polyfillDOMRect();
const engine = TraceModel.Processor.TraceProcessor.createWithAllHandlers();

await engine.parse(traceEvents);
console.log(engine.data) // TraceParseData
```

**Note:** To run in Node, you'll need to polyfill `window.DOMRect`. 😜

See the included `analyze-trace.mjs` a runnable invocation.

### Types

You'll probably use something like…

    @type {import('@paulirish/trace_engine').Types.TraceEvents.TraceEventData[]}
    @type {import('@paulirish/trace_engine').Handlers.Types.TraceParseData}

## Maintainer cheatsheet

See also http://go/btlax

```sh
# Build devtools and prep a package, then invoke the copy script to prepare $HOME/code/trace_engine
scripts/trace/prep-trace-engine-package.sh

# switch to standalone
cd $HOME/code/trace_engine

# bump
npm version v0.0.XXX   # Manually determine next version. `npm info @paulirish/trace_engine --registry https://registry.npmjs.org | grep latest` + 1
npm publish --access public --dry-run # everything look good?

# do auth and publish
npm whoami --registry https://registry.npmjs.org/ || npm login --registry https://registry.npmjs.org/
npm publish --access public --registry https://registry.npmjs.org/
```

## High level architecture

```
     ┌──────────────┐
     │  Model#parse ├───┐
     └──────────────┘   │
                        │
             ┌──────────▼──────────┐
             │async processor#parse│
             └──────────┬──────────┘
                        │
             ┌──────────▼────────────┐
             │for handler of handlers│
             └───┬────────────────┬──┘
                 │                │
┌────────────────▼────┐     ┌─────▼────────────────┐
│NetworkRequestHandler│     │...many more handlers │
│                     │     │                      │
│     reset()         │     │                      │
│                     │     │                      │
│     handleEvent()   │     │                      │
│                     │     │                      │
│     finalize()      │     │                      │
│                     │     │                      │
│     data()          │  │  │                      │
└─────────────────────┘  │  └──────────────────────┘
                         │
                         │
      ┌──────────────────▼─────────────────┐
      │const data = model.parsedTrace()    │
      └────────────────────────────────────┘
```

`Model#parse` is the entrypoint into the engine and is the public interface that consumers use to initiate tracing and to fetch data back.

All the processing is done by the `Processor`. The processor contains a series of *Handlers*, each of which is responsible for parsing events of a particular category.

The trace processor loops over every event in the trace and calls each handler in turn (done this way so we only loop over the trace file once, rather than doing it once-per-handler). A Handler is a file that exposes a set of methods, most importantly `handleEvent()` and `data()`. The `handleEvent` function will be called for each event in the trace, and it is up to an individual handler to do something with that event if it needs to. The `data` method should return the final data that has been parsed and generated by the handler.

Once processing is done (read on for more details on how to track this), you can use the `parsedTrace()` method to fetch the result of parsing a given trace.

## Enabled handlers and creating a model

We use `Model.createWithAllHandlers()`, which initializes a model configured correctly with the right handlers.

If you want to strictly control the set of handlers that are run (for example, if you only want to run one particular handler), you can initialize the model yourself and pass in the set of handlers:

```ts
const model = new Model({
  NetworkRequestHandler: Handlers.ModelHandlers.NetworkRequestHandler,
})
```

## Parsing a trace and getting data back

Once you have an instance of the model, you can call the `parse` method to take a set of raw events and parse them. Once parsed, you then have to call the `parsedTrace` method, providing an index of the trace you want to have the data for. This is because any model can store a number of traces. Each trace is given an index, which starts at 0 and increments by one as a new trace is parsed.

If you are managing multiple traces, you should store them in some form of indexed data structure so you can easily know which index to use to fetch any data from the model. You may delete a trace with `deleteTraceByIndex`, which will then update the indexes of all other traces too.

If you need to check how many traces you have, you can call `model.size()`. The latest trace's index is therefore always `model.size() - 1`.

## Waiting for updates from the model

When you call `parse` you have two options. You can `await` it, which will wait until the trace is fully parsed:

```ts
await this.model.parse();
```

But it's likely preferable to instead use events, to avoid blocking the UI whilst parsing is in progress. You can listen to the `ModelUpdateEvent` for updates:

```ts
this.model.addEventListener(Model.ModelUpdateEvent.eventName, event => {
  const {data} = event as Model.ModelUpdateEvent;

  if (data.data === 'done') {
    // trace is complete
    const newestData = this.model.parsedTrace(this.model.size() - 1);
  } else {
    // data.data will be an object: { index: X, total: Y}, which represents how many events (X) have been processed out of a total (Y).
    // This can be used to show a progress bar, for example.
  }
})
```

## The structure of the final data object

The object returned from `parsedTrace()` is an object of key-value pairs where each key is the name of a handler, and the value is the data that was parsed and returned from that handler.

```ts
{
  NetworkRequestHandler: ReturnType<typeof NetworkRequestHandler['data']>,
  LayoutShiftHandler: ReturnType<typeof LayoutShiftHandler['data']>,
  // and so on for each enabled Handler
}
```

## Pairing begin & end events

Note: this detail is not useful if you are using the Trace Engine, but it is if you are working on it.

Trace events are often emitted as `begin` & `end` events to represent the lifetime of the event. These have the `b` and `e` phase.

When we find these events, we often try to pair them into what we call a "Synthetic" event. This is a trace event that doesn't exist in the raw trace, but one that we create to make it easier to deal with. This means we can represent a `b` & `e` pair as a single event rather than pass two events around.

When we pair these events, we look for an ID. Some events have a top level `id` field, others have a nested `id2.local` field (this is for various historical reasons). `getSyntheticId` in `Trace.Helpers` takes care of this, and tries to account for potential collisions by appending a few other pieces of metadata onto the ID.

This approach worked well until July 2025 when an upstream change in Perfetto [https://chromium.googlesource.com/external/github.com/google/perfetto.git/+/aef636b27ffbf379fd722e7798030da2c5c4d699] meant that Perfetto will try to minimise the amount of unique IDs it uses. A consequence of this change is that IDs can be reused by consecutive, non-overlapping events.

For example, take the following set of events:

```
=== E1 === === E2 ===
```

These could have the same ID, because they do not overlap.

Whereas these events will have different IDs because otherwise you cannot reliably pair them up:

```
=== E1 ===
  === E2 ===
```