Referencia gratuita
El manual no oficial de eventos de trace de Chrome
Chrome escribe un trace de rendimiento en su propio vocabulario interno y no lo documenta. Esta es la referencia: cada evento que modela DevTools, qué significa cada argumento y un ejemplo real de cada uno.
Open a Chrome performance trace and you get a list of events with terse names and arguments nobody documents. There is no reference for them. Not on developer.chrome.com, not in the DevTools docs, not in the Chromium tree. The names are internal C++ and TypeScript identifiers that leaked into a file format people are expected to read.
So I wrote one. This manual covers all 169 events DevTools models, plus 20 that it does not model at all: what each event does, what every argument means, a real example of each, and what you can do with it.
Loading and the network
Document parsing and the five events that make up every request.
HTMLDocumentParser::PumpTokenizerIfPossiblenot in DevTools recordingsnot in DevTools' model
Chrome tries to read another chunk of your HTML.
A DevTools Performance recording does not contain this event. Record blink with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
Spec
Blink's HTML parser tests whether it is able to tokenize, and tokenizes when it is, on the renderer's main thread.
Arguments
| path | declared | observed | |
|---|---|---|---|
args.parser | - | 41.0% |
Observed 145,565 times on 182 of 300 sites: 59,689 complete slices and 42,935 flow pairs, which carry no arguments.
Behaviour
Three events cover this work at three levels, nested inside one another:
PumpTokenizerIfPossible the gate: may the parser run? blink
PumpTokenizer the chunk loop: run until the budget blink
ParseHTML the parse itself devtools.timelineChrome calls the outer gate whenever something has happened that might allow parsing to continue: more bytes have arrived from the network, a blocking script has finished running, document.write() has pushed markup into the stream, or the document is being brought to a close. Each of those is a reason to go and look, and looking is what this event records.
The answer is often no. The gate checks whether a blocking stylesheet has arrived, then tests the parser's state, and if the parser is stopped or paused it returns having consumed nothing at all. That is what IfPossible is doing in the name, and it is why a slice here is not evidence that any markup was read. A gate that contains no ParseHTML did nothing.
When the parser does run and then yields, having spent its budget, the gate schedules the next attempt before returning.
Derivations
Each attempt is flow-linked to the one it schedules, so following that chain from the first gives you every attempt on a single document in order: the ones that parsed, the ones that did nothing, and the time between them. DevTools will not draw that for you at any zoom level.
The transitions are where the value is. Find a run of gates that did nothing followed by one that parsed, and whatever completed in between is what unblocked the parser, usually a ResourceFinish for a script or a stylesheet. That is root-cause attribution for a stalled parse, from the trace rather than from inference.
Traps
All three events are on the blink category, which a DevTools Performance recording never asks for. The entire chain is invisible in the Performance panel, so this is a Perfetto or custom trace configuration job.
HTMLDocumentParser::PumpTokenizer, the middle of the three, is where endLine ends up for the ParseHTML inside it. See the traps on that entry.
{
"name": "HTMLDocumentParser::PumpTokenizerIfPossible",
"cat": "blink",
"ph": "X",
"ts": 1102933188,
"dur": 6372,
"tdur": 3193,
"tts": 35974,
"pid": 8257,
"tid": 8257,
"args": {
"parser": "0x1df400529100"
}
}See also ParseHTML · EvaluateScript
- categories
- blink
- usage
- high
- duration
- typical
- references
- html_document_parser.cc
ParseHTMLParse HTML
Chrome reads your HTML and builds the DOM from it.
Spec
Blink's HTMLDocumentParser runs its tokenizer and tree builder over part of the document, on the renderer's main thread.
Arguments
| path | declared | observed | |
|---|---|---|---|
args.beginData.frame | yes | 100.0% | |
args.beginData.sampleTraceId | opt | 100.0% | |
args.beginData.stackTrace[].columnNumber | - | 89.4% | |
args.beginData.stackTrace[].functionName | - | 89.4% | |
args.beginData.stackTrace[].lineNumber | - | 89.4% | |
args.beginData.stackTrace[].scriptId | - | 89.4% | |
args.beginData.stackTrace[].url | - | 89.4% | |
args.beginData.startLine | yes | 100.0% | |
args.beginData.url | yes | 100.0% | |
args.endData | opt | 0.0% | declared, never observed |
args.endData.endLine | yes | 0.0% | declared, never observed |
args.parsed_bytes | - | 100.0% | undeclared, always present |
args.parsed_tokens | - | 100.0% | undeclared, always present |
Observed 58,996 times on 182 of 300 sites.
Behaviour
Chrome parses a document in bursts rather than in one continuous stretch, and each ParseHTML brackets a single burst. Chromium calls a burst a pump, which is where the sibling event HTMLDocumentParser::PumpTokenizerIfPossible takes its name.
A burst ends for one of two reasons. The parser reaches a synchronous <script> and has to wait while it is fetched and executed, or it spends its token budget: the first two bursts of a document are capped at 250 tokens each, after which the cap is lifted and the parser tries to consume everything remaining in one task. A page therefore produces a chain of these events separated by gaps, and those gaps have two quite different causes that the event alone does not tell apart.
Derivations
ts plus dur of one event, to ts of the next, is time the parser spent blocked rather than parsing, and the next event's startLine is the line of your HTML it was stuck at. Sum those gaps across a load and you have the total the parser spent waiting, located in your own source rather than inferred from where the <script> tags sit.
Short gaps are the parser rescheduling itself after spending its token budget; long ones are a script being fetched and executed. An EvaluateScript between the two events confirms which.
Traps
endLine is not on this event, though trace_engine declares it. Blink does emit it, but the begin and end pairs of ParseHTML and HTMLDocumentParser::PumpTokenizer interleave rather than nest, so each end closes the other's slice. endLine arrives on PumpTokenizer, and PumpTokenizer's own counters arrive here as parsed_bytes and parsed_tokens. Record the blink category if you want the end line.
A stackTrace means JavaScript was on the stack when the burst began, which is true of nearly every parse on a modern site. It does not identify innerHTML or document.write. The field is gated behind disabled-by-default-devtools.timeline.stack, which also gates sampleTraceId and makes every burst take a V8 CPU profiler sample it would not otherwise take.
parsed_bytes counts UTF-16 code units in the parser's buffer, which is not the number of bytes the network delivered.
{
"name": "ParseHTML",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1102952623,
"dur": 86,
"tdur": 86,
"tts": 47032,
"pid": 8257,
"tid": 8257,
"args": {
"beginData": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"sampleTraceId": 6909218608594550,
"stackTrace": [
{
"columnNumber": 20,
"functionName": "",
"lineNumber": 78,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"startLine": 0,
"url": "http://127.0.0.1:8801/"
},
"parsed_bytes": 1960,
"parsed_tokens": 420
}
}See also HTMLDocumentParser::PumpTokenizerIfPossible · EvaluateScript
- categories
- devtools.timeline
- usage
- moderate
- duration
- typical
- undeclared args
args.parsed_bytes,args.parsed_tokens,args.beginData.stackTrace[].columnNumber,args.beginData.stackTrace[].functionName,args.beginData.stackTrace[].lineNumber,args.beginData.stackTrace[].scriptId,args.beginData.stackTrace[].url- references
- inspector_trace_events.cc, html_document_parser.cc, source_location.cc, v8-stack-trace-impl.cc, v8-debugger.cc, segmented_string.h
ResourceFinishFinish loading
This event tells you a resource request has finished.
Spec
Blink closes out a request, on the thread that owned the fetch.
Arguments
| path | declared | observed | |
|---|---|---|---|
args.data.decodedBodyLength | yes | 100.0% | |
args.data.didFail | yes | 100.0% | |
args.data.encodedDataLength | yes | 100.0% | |
args.data.finishTime | yes | 87.9% | |
args.data.requestId | yes | 100.0% |
Observed 36,088 times on 182 of 300 sites.
Behaviour
The terminal event of a request's trace record, and the only one that says whether the request failed.
Two call sites write the same event name through one payload builder. The success path forwards real values. The failure path hardcodes a null timestamp, didFail: true, and zero for both lengths, so an abort that moved bytes reports none: one capture emitted six data events totalling 35 bytes and then closed with encodedDataLength: 0.
A request blocked before a loader exists, by CSP or a mixed-content check, emits no finish event at all.
Derivations
Whether a request failed. didFail appears nowhere else in the lifecycle.
The cache tier, given a success: a ResourceMarkAsCached on the same request id means Blink's memory cache; fromCache: true on the response with no such marker means the HTTP disk cache; neither means the network.
Body size on the wire, on network loads only: this event's encodedDataLength minus the response event's. Verified to within a few bytes of content-length on HTTP/3. It is meaningless on cache hits, on 304s, and wherever the response reports -1.
How busy the thread was when the request landed: ts minus finishTime is the emitting thread's queueing lag. In one trace the six largest gaps, about 88 ms each, all fell inside a single 87.5 ms script task.
Traps
decodedBodyLength / encodedDataLength is not a compression ratio. The denominator includes the header block, so an uncompressed file reads below 1.0, and by more the smaller it is. This entry's own example event is the trap: 3,880 over 4,073 reads as 0.95 "compression" for a fixture file that is exactly 3,880 bytes on disk and is served with no compression of any kind. The field that would make the ratio honest exists in Chromium, is carried all the way into Blink, and is never written to the trace. The ratio is undefined on cache hits, 0/0 on failures, and 0.0 on a 304.
encodedDataLength: 0 means one of four things: a genuine disk-cache hit, a hardcoded zero for a memory-cache hit, a hardcoded zero for a failure that may have transferred plenty, or an empty body. Summing it undercounts every aborted request by its full size.
A missing finishTime is not a failure signal. Across eight traces, 117 absences split 93 memory-cache hits and 24 failures and nothing else. A present finishTime does not mean bytes came off the wire either, because disk-cache hits have one.
ts is not when the download ended. It is when the probe ran, which waits for the current task on that thread. Use finishTime for duration.
The payload carries no url and no frame, so it cannot be attributed to an iframe without joining on the request id. There is no error code, no error text and no distinction between failed, cancelled and aborted.
trace_engine types finishTime as required. It is absent on roughly one event in eight. Guard it.
{
"name": "ResourceFinish",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102939672,
"tts": 39266,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"decodedBodyLength": 3880,
"didFail": false,
"encodedDataLength": 4073,
"finishTime": 1102.922565,
"requestId": "F19F977AB13E669964965F7427E6F7B2"
}
}
}See also ResourceSendRequest · ResourceReceivedData · ResourceMarkAsCached
- categories
- devtools.timeline
- usage
- moderate
- references
- url_loader_completion_status.h, inspector_trace_events.cc, resource_loader.cc, url_loader.cc, NetworkRequestsHandler.ts
ResourceMarkAsCached
Chrome already had the file in memory and never asked for it.
Spec
Blink satisfies a subresource request out of the renderer process's own memory, on the renderer's main thread.
Arguments
| path | declared | observed | |
|---|---|---|---|
args.data.requestId | yes | 100.0% |
Observed 2,959 times on 175 of 300 sites.
Behaviour
Not the HTTP cache. The guard is Blink's own memory cache, which covers two things: an entry in the process-wide MemoryCache, and a resource Blink materialised in-process such as a data: URI or an MHTML subresource. The second kind fires on first use, before any cache could have mattered.
The marker sits at a fixed slot inside a burst of five events that Blink manufactures for a request that never left the process: send, this marker, response, data, finish. The whole burst is typically 20 to 150 microseconds.
It only fires the first time a given document takes a url out of the process-wide cache. A second request for the same url in the same document emits nothing, so the event counts distinct memory-satisfiable urls per document, not reuses. Frames only; workers never emit it.
Derivations
Join on the request id to ResourceSendRequest for the url, frame and priority. This event carries none of them, so any rule phrased over "the url of this event" cannot be implemented.
An http or https scheme marker means another document in the same renderer process already held those bytes. Usually an earlier navigation, but a same-origin iframe in the same page load does it too.
On the matching ResourceFinish: no finishTime, encodedDataLength of zero, and didFail false. Do not add decodedBodyLength > 0 to that fingerprint, because it is zero for every data: URI.
Traps
It is not a repeat-view signal. Across eight cold loads with fresh profiles, 38 of these fired and 35 were inline data: URIs. One site produced 18 on a cold profile.
It is not the opposite of fromCache either. The two are independent and co-occur routinely, because a memory-cache entry replays the response it was stored with: if that response came from the disk cache, both appear on the same request. Measured at 6 of 8 in a warm-profile capture, and on a cold load of reddit.com.
The replayed ResourceReceiveResponse looks like a real network response, with a status code, protocol and socket id all carried over from the original fetch, and a timing block pointing backwards.
Whether the event appears at all can depend on your recorder. Chrome's two hard-coded 1x1 transparent placeholder GIFs emit the full burst when a CDP client is attached and nothing under --trace-startup, so the same page produces a different population under Puppeteer than under Perfetto.
A deferred resource does not emit it. An inline data: font without rel=preload, or an inline data: image with loading="lazy", loads normally and produces four events with no marker.
{
"name": "ResourceMarkAsCached",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1119408173,
"tts": 531693,
"pid": 8591,
"tid": 8591,
"s": "t",
"args": {
"data": {
"requestId": "8591.369"
}
}
}See also ResourceReceiveResponse · ResourceFinish · ResourceSendRequest
- categories
- devtools.timeline
- usage
- moderate
- references
- inspector_trace_events.cc
ResourceReceivedDataReceive data
Chrome hands part of a downloaded file to the page.
Spec
Blink hands a span of response-body bytes to the loader's observer, on the thread that owns the fetch.
Arguments
| path | declared | observed | |
|---|---|---|---|
args.data.encodedDataLength | yes | 100.0% | |
args.data.frame | yes | 100.0% | |
args.data.requestId | yes | 100.0% |
Observed 73,689 times on 182 of 300 sites.
Behaviour
This is a delivery notification, not an arrival notification. The bytes were already in the renderer process, and on two of the four code paths that emit it the whole body had finished downloading before the first event was written.
Four emitters reach it: document subresources, worker fetches, the main document itself, and a synthetic single event replaying a memory-cache hit. Each builds the span differently, which is why one event is neither one network chunk nor one fixed-size read.
Some requests carry a full body and emit nothing at all. A 204 or a 304 has no body; a fetch() resolved to a blob, a stream read by script, or a streamed script all have their body drained by someone else before the loader reads it. Those bytes may reappear later under a different request id.
Derivations
Summing over one request id gives the decompressed bytes received so far. It is the only byte figure available for a request that was still in flight when the recording stopped, and the only place an aborted request's bytes are visible at all, since ResourceFinish reports zeros for a failure.
Gaps between consecutive events on one request, lined up against main-thread tasks, show the renderer starving its own download. The reads happen in renderer tasks, so a blocked thread pushes them later with no change on the network: 11 to 44 microseconds from response to first data on quiet requests in one trace, against 41,802 microseconds on a busy one in the same load.
Traps
encodedDataLength here is decoded body bytes, not wire bytes. A gzipped stylesheet that was 255 bytes on the wire reports 200,020 here and 473 on ResourceFinish: a 424-fold gap between two fields with the same name. "Encoded" is Blink's raw-bytes-versus-text axis, not HTTP's content encoding. Summing these gives decoded size and never transfer size.
The timestamp is not a network timestamp. On the background-processed path the entire response, data and finish sequence can land in a burst of about 100 microseconds after the body already downloaded and V8 already compiled it off-thread.
Event count is not a streaming signal in either direction. One event can mean a small body, a busy main thread, a pre-assembled buffer or a cache replay. One or two is normal: 16 of 21 body-bearing requests in one trace emitted exactly one, and a 2 MiB stylesheet produced six.
Chunk sizes are buffer artefacts. The only real constant is a roughly 1 MiB per-task budget, and it binds only bodies the loader reads itself: a fetch() body pulled by JavaScript produced a single 1,114,112-byte event.
A memory-cache hit emits one event carrying the whole resource, and looks exactly like a fast network response unless you notice the ResourceMarkAsCached beside it.
DevTools collects these events and never reads them. Every byte figure in the Performance panel comes from ResourceFinish.
{
"name": "ResourceReceivedData",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102926911,
"tts": 30693,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"encodedDataLength": 3880,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"requestId": "F19F977AB13E669964965F7427E6F7B2"
}
}
}See also ResourceReceiveResponse · ResourceFinish
- categories
- devtools.timeline
- usage
- moderate
- references
- inspector_trace_events.cc, resource_loader.cc
ResourceReceiveResponseReceive response
Chrome has the response headers for a request.
Spec
Blink takes delivery of a response head, on the thread that owns the fetch.
Arguments
| path | declared | observed | |
|---|---|---|---|
args.data.cacheStorageCacheName | - | 0.0% | |
args.data.connectionId | yes | 100.0% | |
args.data.connectionReused | yes | 100.0% | |
args.data.encodedDataLength | yes | 100.0% | |
args.data.frame | yes | 100.0% | |
args.data.fromCache | yes | 100.0% | |
args.data.fromServiceWorker | yes | 100.0% | |
args.data.headers.name | yes | 0.0% | declared, never observed |
args.data.headers.value | yes | 0.0% | declared, never observed |
args.data.headers[].name | - | 99.1% | undeclared, always present |
args.data.headers[].value | - | 99.1% | undeclared, always present |
args.data.mimeType | yes | 100.0% | |
args.data.protocol | yes | 100.0% | |
args.data.requestId | yes | 100.0% | |
args.data.responseTime | yes | 95.5% | |
args.data.serviceWorkerResponseSource | - | 0.5% | |
args.data.staticRoutingInfo.matchedSourceType | - | 0.4% | |
args.data.staticRoutingInfo.ruleIdMatched | - | 0.4% | |
args.data.statusCode | yes | 100.0% | |
args.data.timing.connectEnd | - | 95.3% | |
args.data.timing.connectStart | - | 95.3% | |
args.data.timing.dnsEnd | - | 95.3% | |
args.data.timing.dnsStart | - | 95.3% | |
args.data.timing.proxyEnd | - | 95.3% | |
args.data.timing.proxyStart | - | 95.3% | |
args.data.timing.pushEnd | - | 95.3% | |
args.data.timing.pushStart | - | 95.3% | |
args.data.timing.receiveHeadersEnd | - | 95.3% | |
args.data.timing.receiveHeadersStart | - | 95.3% | |
args.data.timing.requestTime | - | 95.3% | |
args.data.timing.sendEnd | - | 95.3% | |
args.data.timing.sendStart | - | 95.3% | |
args.data.timing.sslEnd | - | 95.3% | |
args.data.timing.sslStart | - | 95.3% | |
args.data.timing.workerReady | - | 95.3% | |
args.data.timing.workerStart | - | 95.3% |
Observed 34,142 times on 182 of 300 sites.
Behaviour
One event per request, and the only one carrying the status code, MIME type, protocol, headers, cache flags, socket identity and the network timing block. Nothing else in the lifecycle repeats any of it.
Redirect hops do not produce one. Only the final response does.
It fires for Blink memory-cache hits too, where it replays a stored response object rather than describing a fetch. In that case ResourceMarkAsCached fires first on the same request id, and everything in the payload describes the earlier load.
Derivations
The timing block is absolutely placeable, because requestTime is seconds on the same monotonic clock as ts. Every other offset in the block is milliseconds relative to it, so any phase lands at requestTime 1e6 + offset 1e3 in trace units.
That gives you when the headers really arrived, and subtracting it from ts gives you how long Blink took to pick them up: a main-thread congestion signal you cannot get anywhere else.
Server think time is receiveHeadersStart - sendEnd, which is what DevTools computes.
Connection setup is dnsEnd - dnsStart, connectEnd - connectStart, sslEnd - sslStart. These are blocking times, not occurrence times, so a zero means the request did not wait, as with a preconnected socket.
Traps
encodedDataLength here is five different numbers. On a fresh network response it is the response header bytes as they went over the wire, so on h2 and h3 it is the compressed HEADERS frame and lands far below the header text: a 204 with around 809 bytes of headers reported 57. It is 0 on an unvalidated disk-cache hit and on data:, and -1 on blob:, chrome:// and chrome-extension:. On a memory-cache replay it is the entire wire transfer of the original load, body included: one stylesheet reported 189 on the fetch that got it and 5,214 on the replay.
fromCache is not a flag. It is derived from two timestamps, true whenever the response predates the request, which is why HTTP/2 pushed resources and service-worker responses report it. A 304 revalidation reports false, and reports its status as 200, because the 304's headers are merged into the stored entry.
fromCache: true and ResourceMarkAsCached are not mutually exclusive. A memory-cache entry replays whatever response it was stored with, so one that originally came off the disk cache sets both. DevTools marks such a request as disk-cached and memory-cached at once, deliberately.
On a memory-cache replay the timing block belongs to the earlier fetch, measured 1.3 seconds stale in one capture and 2.5 seconds in another, and connectionId is a closed socket. There is no flag on this event to tell you; you have to join to ResourceMarkAsCached. How much of a trace is affected depends entirely on how you recorded: a single navigation has almost none, a two-navigation recording can be three quarters stale.
ts is not when the headers arrived. No timestamp is passed to the trace macro, so it is when the Blink probe ran, always at or after the headers completed and by tens of milliseconds on a busy thread.
pushStart and pushEnd are absolute seconds with a 0 sentinel, sitting among fifteen millisecond offsets that use -1.
protocol can be a url scheme. Filter data, blob, chrome and chrome-extension out of any protocol histogram.
Server think time understates on any origin sending 103 Early Hints, because receiveHeadersStart can be the 1xx, and the field that would correct it is recorded by Blink and never written to the trace.
{
"name": "ResourceReceiveResponse",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1236061260,
"tts": 450196,
"pid": 10309,
"tid": 10309,
"s": "t",
"args": {
"data": {
"cacheStorageCacheName": "devsite.pwa_RUNTIME_v2.1",
"connectionId": 0,
"connectionReused": false,
"encodedDataLength": 0,
"frame": "614FC5655E48B642B0235ABCA128B0C6",
"fromCache": true,
"fromServiceWorker": true,
"headers": [
{
"name": "content-encoding",
"value": "gzip"
},
{
"name": "age",
"value": "161242"
},
{
"name": "report-to",
"value": "{\"group\":\"devrel-devsite\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/devrel-devsite\"}]}"
},
"... [16 more items]"
],
"mimeType": "image/svg+xml",
"protocol": "h2",
"requestId": "10309.250",
"responseTime": 1789291418105.672,
"serviceWorkerResponseSource": "cacheStorage",
"statusCode": 200,
"timing": {
"connectEnd": -1,
"connectStart": -1,
"dnsEnd": -1,
"dnsStart": -1,
"proxyEnd": -1,
"proxyStart": -1,
"pushEnd": 0,
"pushStart": 0,
"receiveHeadersEnd": 2.19,
"receiveHeadersStart": 2.19,
"requestTime": 1236.054677,
"sendEnd": 0.002,
"sendStart": 0.002,
"sslEnd": -1,
"sslStart": -1,
"workerReady": 0.118,
"workerStart": 0.002
}
}
}
}See also ResourceSendRequest · ResourceReceivedData · ResourceFinish · ResourceMarkAsCached
- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.timing.connectEnd,args.data.timing.connectStart,args.data.timing.dnsEnd,args.data.timing.dnsStart,args.data.timing.proxyEnd,args.data.timing.proxyStart,args.data.timing.pushEnd,args.data.timing.pushStartand 13 more- references
- load_timing_info.h, resource_load_timing.cc, inspector_trace_events.cc, web_url_response.cc, url_loader_util.cc, url_loader.cc and 3 more
ResourceSendRequestSend request
Chrome asks for a file.
Spec
Blink hands a request down to be fetched, on the thread that owns it.
Arguments
| path | declared | observed | |
|---|---|---|---|
args.data.fetchPriorityHint | opt | 100.0% | |
args.data.frame | yes | 100.0% | |
args.data.initiator.columnNumber | - | 46.1% | |
args.data.initiator.fetchType | - | 97.2% | |
args.data.initiator.lineNumber | - | 46.1% | |
args.data.initiator.type | - | 97.2% | |
args.data.initiator.url | - | 51.3% | |
args.data.isLinkPreload | opt | 97.2% | |
args.data.priority | yes | 100.0% | |
args.data.renderBlocking | opt | 45.2% | |
args.data.requestId | yes | 100.0% | |
args.data.requestMethod | opt | 100.0% | |
args.data.resourceType | opt | 100.0% | |
args.data.sampleTraceId | - | 100.0% | undeclared, always present |
args.data.stackTrace[].columnNumber | - | 42.0% | |
args.data.stackTrace[].functionName | - | 42.0% | |
args.data.stackTrace[].lineNumber | - | 42.0% | |
args.data.stackTrace[].scriptId | - | 42.0% | |
args.data.stackTrace[].url | - | 42.0% | |
args.data.url | yes | 100.0% |
Observed 34,467 times on 182 of 300 sites.
Behaviour
For a subresource this is Blink's fetcher passing the request on, before the network service, before DNS, before the socket. The queueing between this moment and the wire is why ResourceReceiveResponse carries a timing block at all.
For the document it is something else entirely. Blink writes it at navigation commit, from DocumentLoader, long after the browser sent the request and after the response headers have arrived. Chromium's own comment at that line reads "The fetch has already started in the browser". Measured on a real site, the renderer's event landed 110 ms after the browser's ResourceWillSendRequest and 0.16 ms before the response.
The payload is the richest of the six and none of the later events repeat it: url, method, resource type, priority, and where the request came from. Redirects emit one event per hop on a shared request id.
Derivations
Attribution. initiator.url falls back to stackTrace[0].url, which is exactly what DevTools does before matching the result against other requests to build a dependency tree rather than a flat waterfall.
A render-blocking audit: filter renderBlocking for blocking and in_body_parser_blocking to get the requests that actually held up first paint.
Queueing for a subresource: requestTime on the response event, minus this event's ts.
Traps
Never compute document time to first byte from this event. It is a commit timestamp, and the answer comes out around 0.1 ms.
Stack depth measures your recorder, not the page. V8 captures one frame unless a client has the Runtime domain enabled, and then up to 200. The same fetch() from an eleven-frame call chain gave eleven frames under Puppeteer and one frame under --enable-tracing. Never compare stack depth across traces made with different tools.
The stack itself is gated on disabled-by-default-devtools.timeline.stack. Without that category stackTrace and sampleTraceId are simply gone, and nothing in the output distinguishes that from no stack being available.
renderBlocking is not a property of the resource. It is whatever the code path that triggered this particular request handed over, so the same font url carries non_blocking on a cold load and loses the field entirely on a memory-cache hit. Absent is not a third value.
isLinkPreload means an explicit rel=preload directive, not "Chrome preloaded this". Anything the preload scanner found speculatively is false.
priority is the priority at send time. A separate ResourceChangePriority event can supersede it.
fetchPriorityHint is present on every event because auto is written unconditionally. Only low and high carry author intent.
On a redirecting navigation the earlier events' urls are not reliably the intermediate hops. Count the events for the hop count; do not reconstruct the chain from them.
{
"name": "ResourceSendRequest",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1236185423,
"tts": 104095,
"pid": 10389,
"tid": 10389,
"s": "t",
"args": {
"data": {
"fetchPriorityHint": "auto",
"frame": "7EC3978477DE6DAE91460AF9D4ECB53A",
"initiator": {
"columnNumber": 13,
"fetchType": "css",
"lineNumber": 258,
"type": "parser",
"url": "https://www.google.com/recaptcha/api2/anchor?ar=1&k=6Lfqf9YdAAAAAKUVvj6y53E_tMMlnj8dt1fpo-FV&co=aHR0cHM6Ly93ZWItZG90LWRldnNpdGUtdjItcHJvZC0zcC5hcHBzcG90LmNvbTo0... [98 more chars]"
},
"isLinkPreload": false,
"priority": "VeryHigh",
"renderBlocking": "non_blocking",
"requestId": "10389.14",
"requestMethod": "GET",
"resourceType": "Font",
"sampleTraceId": 4395473783086954,
"stackTrace": [
{
"columnNumber": 81,
"functionName": "",
"lineNumber": 736,
"scriptId": "5",
"url": "https://www.gstatic.com/recaptcha/releases/BnqMGSY_YP4cCmbNINHpJPkd/recaptcha__en.js"
},
{
"columnNumber": 413,
"functionName": "",
"lineNumber": 613,
"scriptId": "5",
"url": "https://www.gstatic.com/recaptcha/releases/BnqMGSY_YP4cCmbNINHpJPkd/recaptcha__en.js"
},
{
"columnNumber": 49,
"functionName": "LX.Bl",
"lineNumber": 1223,
"scriptId": "5",
"url": "https://www.gstatic.com/recaptcha/releases/BnqMGSY_YP4cCmbNINHpJPkd/recaptcha__en.js"
},
"... [13 more items]"
],
"url": "https://fonts.gstatic.com/s/roboto/v48/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2"
}
}
}See also ResourceWillSendRequest · ResourceReceiveResponse · ResourceFinish
- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.initiator.fetchType,args.data.initiator.type,args.data.initiator.url,args.data.sampleTraceId,args.data.initiator.columnNumber,args.data.initiator.lineNumber,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionNameand 3 more- references
- inspector_trace_events.cc, inspector_network_agent.cc, image_loader.cc, preload_helper.cc, preload_request.cc, resource_fetcher.cc and 7 more
ResourceWillSendRequestWill send request
Chrome is about to request a page or an iframe.
Spec
The browser process marks a navigation request as it is about to be sent, on the browser's main thread.
Arguments
| path | declared | observed | |
|---|---|---|---|
args.data.requestId | yes | 100.0% |
Observed 952 times on 182 of 300 sites.
Behaviour
This is the only one of the six request events the browser process writes. The other five come from the renderer, which is visible in the trace itself: this event is process-scoped, "s": "p", while the others are thread-scoped, "s": "t".
It is written for cross-document navigations only, never for a subresource. A page, an iframe and a data: frame all get one; a back-forward cache restore, a prerender activation, a same-document navigation and about:blank do not. Chrome writes it before the URL loader exists and before any service worker sees the request, so a navigation answered offline by respondWith() still produces one.
Redirects produce one event per hop, all sharing a request id. N events means N-1 redirects.
Derivations
This is where a navigation actually started. The renderer's ResourceSendRequest for the same document cannot tell you, because Blink writes that one at commit, once the response is already in hand: measured at 0.014 to 0.092 ms before the response event across six captures.
So honest time to first byte for the main document runs from this event to requestTime + receiveHeadersEnd on ResourceReceiveResponse. In one capture the browser sent at 0 ms, the request reached the server at 92.60 ms after two redirects, headers completed at 129.40 ms, and the renderer wrote its ResourceSendRequest at 132.93 ms.
Consecutive timestamps on one request id give the cost of each redirect hop as the browser experienced it. A hop of a couple of milliseconds is an internal redirect, an HTTP to HTTPS upgrade rather than a round trip. DevTools reads these timestamps in preference to the renderer's.
A request id with this event and nothing else is a navigation that never committed: hung, superseded, a 204, or a download.
Traps
The gap to ResourceSendRequest is not network time. It contains browser dispatch, redirects, the wire, IPC and the renderer's commit. The non-network residual measured 2.7 to 5.0 ms on a warm headless machine and grows when a renderer has to be spawned.
The event does not mean bytes went on the wire. Service-worker-answered navigations and data: frames emit it with no network activity at all.
A failed navigation is not an orphan. A DNS failure still commits an error page, and the renderer emits the full set with the url chrome-error://chromewebdata/.
DevTools will not show you an orphan either: it drops any request with no send event, so you have to read the raw trace.
The request id is the navigation token, 32 uppercase hex characters, shared by every hop and by the renderer's four events for the same document. Subresource ids look nothing like it.
{
"name": "ResourceWillSendRequest",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102919608,
"pid": 8178,
"tid": 8178,
"s": "p",
"args": {
"data": {
"requestId": "F19F977AB13E669964965F7427E6F7B2"
}
}
}See also ResourceSendRequest · ResourceReceiveResponse
- categories
- devtools.timeline
- usage
- low
- references
- devtools_instrumentation.cc
Style, layout and paint
The rendering pipeline, from style recalculation to pixels.
AnimationAnimation
Chrome runs a CSS animation or a Web Animations API animation.
Animation records a CSS animation or a Web Animations API animation.
args.data.compositeFailed and args.data.unsupportedProperties are the whole point of the event. Together they tell you the animation could not be run on the compositor and why, naming the properties that forced it onto the main thread. An animation of transform or opacity should composite. One that animates width, top, box-shadow or similar will not, and will drive layout or paint on every frame. nodeName, nodeId, id, name and state are also present.
Reading these tells you directly whether an optimisation worked, which few events in a trace do. CLSCulprits consumes the event, because a main-thread animation that moves layout-affecting properties causes layout shifts as well as dropped frames. If you fix one thing off the back of a trace, a populated unsupportedProperties is usually the highest-value single finding available.
{
"name": "Animation",
"cat": "blink.animations,devtools.timeline,benchmark,rail",
"ph": "b",
"ts": 1109196689,
"pid": 8475,
"tid": 8475,
"id2": {
"local": "0x38f4005fc1d8"
},
"scope": "blink.animations,devtools.timeline,benchmark,rail",
"args": {
"data": {
"displayName": "margin-bottom",
"id": "1",
"name": "",
"nodeId": 40,
"nodeName": "BUTTON class='message-component message-button no-children focusable button sp_choice_type_11'",
"state": "running"
},
"endData": {
"state": "finished"
}
}
}- categories
- blink.animations, devtools.timeline, benchmark, rail
- usage
- moderate
- undeclared args
args.endData.state- references
- animation.cc, inspector_animation_agent.cc
BeginFrameFrame start
The compositor opens a new frame.
BeginFrame marks the compositor opening a frame. layerTreeId identifies the layer tree and frameSeqId is the frame's sequence number.
Use frameSeqId as the join key for frame analysis. Match it across BeginFrame, Commit and DrawFrame to reconstruct one frame's full journey. FramesHandler does exactly this to build the frame model behind the Performance panel's frames track.
{
"name": "BeginFrame",
"cat": "disabled-by-default-devtools.timeline.frame",
"ph": "I",
"ts": 1102922548,
"tts": 1503,
"pid": 8257,
"tid": 8278,
"s": "t",
"args": {
"frameSeqId": 6,
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline.frame
- usage
- moderate
- references
- devtools_instrumentation.h, layer_tree_host_impl.cc, compositor_frame_reporting_controller.cc
BeginMainThreadFrameFrame start (main thread)
The main thread begins its turn in a frame, where it runs animation callbacks, style, layout and paint.
BeginMainThreadFrame opens the main thread's turn in a frame, where it runs rAF callbacks first, then style, layout and paint. layerTreeId identifies the layer tree and args.data.frameId identifies the frame.
The distance from here to Commit is your main-thread frame budget in practice. If that span regularly exceeds the frame interval, the main thread is your bottleneck. If it is short but frames still drop, look at raster and GPU instead.
{
"name": "BeginMainThreadFrame",
"cat": "disabled-by-default-devtools.timeline.frame",
"ph": "I",
"ts": 1102922990,
"tts": 28292,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frameId": 2
},
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline.frame
- usage
- moderate
- references
- devtools_instrumentation.h, layer_tree_host.cc, layer_tree_host.h
Blink.ForcedStyleAndLayout.UpdateTimenot in DevTools recordingsnot in DevTools' model
Chrome measures how long a style and layout update took when something outside the rendering pipeline forced it.
A DevTools Performance recording does not contain this event. Record blink with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
Blink.ForcedStyleAndLayout.UpdateTime measures how long a style and layout update took when something other than the rendering lifecycle asked for it. The name is a UMA histogram string reused verbatim as a trace slice by LocalFrameMetricsAggregator::BeginForcedLayout and EndForcedLayout in third_party/blink/renderer/core/frame/local_frame_metrics_aggregator.cc, so searching for it finds histogram docs rather than trace docs. In practice the caller is JavaScript reading offsetHeight, getBoundingClientRect() or getComputedStyle(), which forces Blink to lay out immediately.
args.preFCP is the one argument. It is a boolean written at END rather than BEGIN, and it is literally fcp_state_ == kBeforeFCPSignal. preFCP=true marks a forced layout that happened before First Contentful Paint, the subset that directly delays LCP.
Reading these events tells you how much main thread time went on forced layout, and preFCP separates the part that delayed the first paint. Re-entrant calls fold into one slice: WillStartForcedLayout keeps a forced_layout_stack_depth_ and only the outermost call is timed, so the count is the number of outermost forced layouts, not the number of geometry reads.
{
"name": "Blink.ForcedStyleAndLayout.UpdateTime",
"cat": "blink",
"ph": "X",
"ts": 1102948046,
"dur": 5,
"tdur": 2,
"tts": 44138,
"pid": 8257,
"tid": 8257,
"args": {
"preFCP": false
}
}- categories
- blink
- usage
- very high
- duration
- short
- references
- local_frame_metrics_aggregator.cc
CommitCommit
The main thread hands the page's updated layers over to the compositor thread.
Commit marks the main thread handing the updated layer tree over to the compositor thread. DevTools describes it as the step where layers are sent to the compositor.
layerTreeId identifies the layer tree. frameSeqId is the frame's sequence number, and it is the useful one. It lets you stitch this commit to the BeginFrame and DrawFrame events for the same frame, and so measure the full main thread to screen latency for that frame.
Use it as the handoff point in the frame lifecycle: everything before it is main thread, everything after is compositor and GPU. A frame that misses its deadline either took too long to reach commit, or stalled after it. Which side of Commit the time sits on tells you which team's problem it is.
{
"name": "Commit",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102923583,
"dur": 59,
"tdur": 53,
"tts": 28651,
"pid": 8257,
"tid": 8257,
"args": {
"frameSeqId": 6,
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline, cc, benchmark, disabled-by-default-devtools.timeline.frame, input, input.scrolling
- usage
- high
- duration
- typical
- references
- devtools_instrumentation.h, proxy_main.cc, single_thread_proxy.cc, compositor_frame_reporter.cc, event_latency_tracing_recorder.cc
CompositeLayersComposite layersno longer emitted
The older name for the main thread handing the page's layers to the compositor thread.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
CompositeLayers recorded the main thread handing the layer tree to the compositor thread until Chrome 111, when it was renamed Commit. Commit e8a4175e91, "Add the new Commit devtools timeline event", swapped the name in ScopedCommitTrace and kept the disabled-by-default-devtools.timeline category.
layerTreeId was its only field.
TraceEvents.ts in DevTools states the compatibility position directly: "CompositeLayers has been replaced by "Commit", but we support both to not break old traces being imported."
If you are parsing traces across Chrome versions, handle both names. Traces from Chrome 111 onward only have Commit.
Renamed to Commit in Chrome 111, and the collection recorded Commit. DevTools still accepts both names so that old traces keep importing.
- references
- devtools_instrumentation.cc
ComputeIntersectionsCompute intersectionsnever written by Chrome
Chrome works out which of your IntersectionObserver targets are currently in view.
No Chrome build writes this name into a trace file. It exists in the model, not on the wire.
ComputeIntersections is the DevTools name for Blink computing IntersectionObserver intersections, but Chrome has never written that string. Chrome writes the work as IntersectionObserverController::computeIntersections, fully qualified and with a lower case c, on blink,devtools.timeline from intersection_observer_controller.cc. That name has not changed since at least Chrome 80. The old DevTools TimelineModel mapped ComputeIntersections to it, but the new trace model's Name enum kept only the bare key, so search a trace for the full name.
The event has no args, which makes attribution awkward: you can see the cost but not which observer caused it. Cost scales with the number of observed elements and with layout complexity, since each intersection test needs current geometry.
Check these on pages with lazy-loading libraries, infinite scroll, or analytics viewability tracking, all of which tend to observe large numbers of elements. The work runs as part of the rendering pipeline, so it delays frames directly.
Chrome never writes this name. A fixture page running an IntersectionObserver produced the real one, IntersectionObserverController::computeIntersections, fully qualified and with a lower case c.
- references
- TraceEvents.ts
Decode ImageImage decode
Chrome turns one image's compressed bytes into a bitmap it can paint.
Decode Image measures one image being decoded from its compressed bytes into a bitmap. Phase X, so dur is real decode cost. Note the space in the wire name. It is "Decode Image", not DecodeImage.
args.imageType gives the format, and that is what you act on. JPEG and PNG decode markedly more cheaply than the same dimensions in a format the platform has no hardware path for, and large progressive JPEGs are consistently worse than their file size suggests.
On its own this event does not tell you which image was decoded. ImagePaintingHandler recovers that by associating the decode with a PaintImage on the same thread, and where that fails, via the Decode LazyPixelRef and Draw LazyPixelRef pair. Decode frequently happens off the main thread, but a synchronous decode on the main thread before a paint is a real stall and one of the more common causes of a late LCP.
{
"name": "Decode Image",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1108423719,
"dur": 99,
"tdur": 99,
"tts": 929,
"pid": 8407,
"tid": 8471,
"args": {
"imageType": "png"
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- duration
- long
- references
- image_decoder.cc, jpeg_image_decoder.cc
Document::UpdateStyleAndLayoutnot in DevTools recordingsnot in DevTools' model
Chrome updates a document's style and layout immediately, because something asked for a clean layout tree.
A DevTools Performance recording does not contain this event. Record blink with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
Document::UpdateStyleAndLayout marks the entry point for a synchronous style and layout update on a document, in third_party/blink/renderer/core/dom/document.cc. Every path that needs a clean layout tree arrives here: UpdateStyleAndLayoutForNode, UpdateStyleAndLayoutForRange, printing, plugins, hit testing.
There are no trace arguments. The interesting line is the branch on the C++ DocumentUpdateReason argument. If the reason is anything other than kBeginMainFrame, Blink calls WillStartForcedLayout(reason) and the work is charged as forced layout. That one comparison is the whole definition of layout thrashing inside Chromium. The reason never reaches the trace args, so you recover it from the surrounding stack.
Reading these events gives you the boundary DevTools draws its purple Layout block around, one level up from where the decision is made. The function also recurses up through LocalOwner() first, so a forced layout inside an iframe drags the parent document's layout with it.
{
"name": "Document::UpdateStyleAndLayout",
"cat": "blink",
"ph": "X",
"ts": 1102948044,
"dur": 8,
"tdur": 5,
"tts": 44136,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- blink
- usage
- very high
- duration
- short
- references
- document.cc
DrawFrameDraw frame
The compositor draws the frame to the screen.
DrawFrame marks the compositor actually drawing the frame, the point at which the work becomes something a user can see. layerTreeId and frameSeqId are its two fields.
Pair it with BeginFrame on frameSeqId and you have closed the frame lifecycle. A missing DrawFrame for a BeginFrame that did occur means the frame was dropped. That is how dropped-frame counts are derived, rather than by inferring them from timing gaps.
{
"name": "DrawFrame",
"cat": "disabled-by-default-devtools.timeline.frame",
"ph": "I",
"ts": 1102924416,
"tts": 2021,
"pid": 8257,
"tid": 8278,
"s": "t",
"args": {
"frameSeqId": 6,
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline.frame
- usage
- moderate
- references
- devtools_instrumentation.h, layer_tree_host_impl.cc
firstContentfulPaintFirst Contentful Paint
This event tells you when the first text, image, canvas or SVG was painted.
firstContentfulPaint marks the first paint of text, an image, a non-white canvas or an SVG. Phase R.
args.frame names the frame and args.data.navigationId identifies the navigation. PageLoadMetricsHandler consumes the event, and that handler is what turns these raw marks into the metric values the Performance panel reports.
FCP is bounded below by render-blocking resources, so a late FCP is usually a request-chain problem rather than a rendering one. Check what was blocking in the network waterfall before you look at main-thread work.
{
"name": "firstContentfulPaint",
"cat": "loading,rail,devtools.timeline",
"ph": "R",
"ts": 1102971047,
"tts": 133797,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"navigationId": "F19F977AB13E669964965F7427E6F7B2"
},
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7"
}
}- categories
- loading, rail, devtools.timeline
- usage
- rare
- references
- inspector_trace_events.cc, paint_timing.cc
firstPaintFirst Paint
This event tells you when the browser first painted anything at all, including a plain background colour.
firstPaint marks the first time the browser painted anything at all, a background colour included. Phase R (mark), so it has a timestamp and no duration.
args.frame names the frame. args.data.navigationId identifies the navigation, and it matters on pages with soft navigations: without checking it you will attribute a mark to the wrong navigation.
As a target metric it is rarely useful, since it fires for a background-colour paint with no content. Use it instead as a floor. Nothing contentful can precede it, so a late firstPaint bounds every other paint metric.
{
"name": "firstPaint",
"cat": "loading,rail,devtools.timeline",
"ph": "R",
"ts": 1102971047,
"tts": 133866,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"navigationId": "F19F977AB13E669964965F7427E6F7B2"
},
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7"
}
}- categories
- loading, rail, devtools.timeline
- usage
- rare
- references
- inspector_trace_events.cc, paint_timing.cc
HitTestHit test
Chrome works out which element sits under a given point on the page.
HitTest measures Blink determining which element is under a given point. DevTools describes it as "the process the browser uses to determine a target element for a pointer event".
trace_engine models it with no args, but the raw event gives you more. Six fields sit in args.endData. nodeId and nodeName identify the element that was actually hit, x and y are the point that was tested, and rectilinear and move describe the test. The node name makes a hit test one of the few events that resolves a coordinate to a real element without any correlation work. move distinguishes a hit test caused by pointer movement from one caused by a click, which matters because the first kind fires constantly and the second does not.
These appear during input handling and also during scroll and IntersectionObserver work. Individually they are cheap. They become interesting when they show up repeatedly inside a single input task, which usually means a deep or complex layout tree is making each test expensive. That is a contributor to poor INP which is easy to overlook, because no single event looks bad.
{
"name": "HitTest",
"cat": "blink,devtools.timeline",
"ph": "X",
"ts": 1104756124,
"dur": 13,
"tdur": 12,
"tts": 207556,
"pid": 8257,
"tid": 8257,
"args": {
"endData": {
"move": true,
"nodeId": 8,
"nodeName": "DIV class='box'",
"rectilinear": true,
"x": 400,
"y": 300
}
}
}- categories
- blink, devtools.timeline, input
- usage
- moderate
- duration
- typical
- undeclared args
args.endData.nodeId,args.endData.nodeName,args.endData.rectilinear,args.endData.x,args.endData.y,args.endData.move,args.endData.rect,args.endData.listBased- references
- layout_view.cc, hit_test_request.h, event_handler.cc, hit_test_location.cc, input_handler_proxy.cc
InlineNode::ShapeTextIncludingFirstLinenot in DevTools recordingsnot in DevTools' model
Chrome turns a run of text and a font into positioned glyphs.
A DevTools Performance recording does not contain this event. Record blink with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
InlineNode::ShapeTextIncludingFirstLine measures text shaping for one inline formatting context, emitted by third_party/blink/renderer/core/layout/inline/inline_node.cc. This is where Blink turns a string plus a font into positioned glyphs via HarfBuzz, the layout cost that scales with how much text you have rather than how many elements.
Nothing is recorded beyond the duration, and the name is two passes. ShapeText() shapes the block, then ShapeTextForFirstLineIfNeeded() runs again, but only if StyleEngine::UsesFirstLineRules() is true and the first line style differs from the block style. A ::first-line rule that changes text-transform forces a full reshape of the paragraph, not just the first line, because the transform rewrites the character stream.
Reading these events tells you how often text is being reshaped rather than reused, which no other tool surfaces at all. Individual shapes run faster than most trace events, and there are a lot of them on every site in the corpus, so the signal is volume. Thousands in one layout typically come from font swaps, width changes, or a failed SetTextWithOffset.
{
"name": "InlineNode::ShapeTextIncludingFirstLine",
"cat": "blink",
"ph": "X",
"ts": 1102943300,
"dur": 249,
"tdur": 153,
"tts": 40884,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- blink
- usage
- high
- duration
- short
- references
- inline_node.cc
InvalidateLayoutInvalidate Layout
Chrome marks a node's layout as out of date, so it has to be laid out again.
InvalidateLayout fires when a node dirties layout. It is an instant event and it marks the cause. The resulting work shows up as a Layout event later.
args.data.frame and args.data.nodeId are the only two fields. The event is deliberately cheap. Its value is entirely relational: InitiatorsHandler walks back from a Layout to the InvalidateLayout that triggered it, which is how the Performance panel can tell you what forced the relayout rather than merely that one happened.
Use these when hunting forced synchronous layout. A JS frame containing an InvalidateLayout immediately followed by a Layout inside the same task is the layout-thrashing signature: the script wrote to the DOM, then read a geometry property, forcing Blink to lay out on the spot.
{
"name": "InvalidateLayout",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1102950855,
"tts": 45968,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"nodeId": 3,
"sampleTraceId": 6909218608594504,
"stackTrace": [
{
"columnNumber": 81,
"functionName": "note",
"lineNumber": 24,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
},
{
"columnNumber": 5,
"functionName": "",
"lineNumber": 47,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
]
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- source_location.cc, v8-stack-trace-impl.cc, v8-debugger.cc, local_frame_view.cc
largestContentfulPaint::CandidateLargest Contentful Paint
A contentful element has painted that is the largest so far, making it the current LCP candidate.
largestContentfulPaint::Candidate records one LCP candidate. Chrome emits one every time a larger contentful element paints, so a normal page load produces several of them.
args.data.candidateIndex says which candidate this is. args.data.nodeId identifies the element. type, loadingAttr and isOutermostMainFrame complete the argument set.
Only the last candidate before the LCP is finalised is the actual LCP. Taking the first, or treating each as a metric, is the single most common mistake made when reading LCP out of a raw trace. loadingAttr deserves attention too: an LCP element with loading="lazy" set is a self-inflicted delay, and it shows up right here in the trace without needing any inference. LCP is finalised when the user interacts or the page is hidden, which is why the candidate stream simply stops rather than being explicitly closed.
{
"name": "largestContentfulPaint::Candidate",
"cat": "loading,rail,devtools.timeline",
"ph": "R",
"ts": 1108451601,
"tts": 119589,
"pid": 8407,
"tid": 8407,
"s": "t",
"args": {
"data": {
"candidateIndex": 1,
"imageDiscoveryTime": 70.19999999995343,
"imageLoadEnd": 135.9000000001397,
"imageLoadStart": 117,
"isMainFrame": true,
"isOutermostMainFrame": true,
"loadingAttr": "eager",
"navigationId": "BAAF482071D29F77645D385F984F23B2",
"nodeId": 274,
"nodeName": "IMG class='Image-styles__ImageStyled-sc-8c99a12b-0 cVsHni'",
"performanceTimelineNavigationId": 4052,
"size": 213752,
"type": "image"
},
"frame": "2EA8810E92C12C680C55B2941251AA98"
}
}- categories
- loading, rail, devtools.timeline
- usage
- low
- undeclared args
args.data.imageDiscoveryTime,args.data.imageLoadEnd,args.data.imageLoadStart,args.data.performanceTimelineNavigationId,args.data.size- references
- image_resource.cc, largest_contentful_paint_calculator.cc, resource_fetcher.cc, performance.cc, window_performance.h, paint_timing_record.h
largestContentfulPaint::Invalidateno longer emitted
Chrome withdrew an LCP candidate it had already reported.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
largestContentfulPaint::Invalidate marked a previously reported LCP candidate being withdrawn until Chrome 88, when it was removed. Up to Chrome 87, an LCP element that was removed from the page stopped counting, and this instant on loading,rail,devtools.timeline recorded that.
Commit a5484e6310, "[LargestContentfulPaint] Allow removed content by default", changed the rule in October 2020. Removed content now keeps its LCP entry, and a candidate is only ever replaced by a larger one. With nothing left to invalidate, the event was deleted.
There are no typed args in trace_engine, and no handler consumes it, so DevTools does not surface it. It matters only if you compute LCP yourself from traces recorded on Chrome 87 or earlier. Ignoring invalidation in those traces means reporting an LCP element that Chrome had already discarded. In current traces, work from largestContentfulPaint::Candidate alone.
Removed in Chrome 88, when LCP stopped withdrawing a candidate whose element left the page. The collection recorded largestContentfulPaint::Candidate instead.
- references
- largest_contentful_paint_calculator.cc
LayerizeLayerize
The compositor works out which layers to create for the page.
Layerize covers the compositing decision step, in which cc works out which layers to create. DevTools' own description is exactly that: "Layerize is a step where we calculate which layers to create."
No args. Its cost scales with the number of composited layers, so a page that promotes a lot of elements (will-change, transform: translateZ(0), fixed positioning, video, canvas) pays here on every frame that re-layerizes.
Check it after you have "optimised" a page by promoting many elements to their own layers. The promotion is not free, and this is one of the places the bill arrives.
{
"name": "Layerize",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1102923357,
"dur": 208,
"tdur": 104,
"tts": 28532,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"isMainFrame": true,
"isOutermostMainFrame": true,
"page": "14A831DCB73EB1205B67CDB89FC6EBD7"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- typical
- undeclared args
args.data.frame,args.data.isMainFrame,args.data.isOutermostMainFrame,args.data.page- references
- inspector_trace_events.cc, document.cc, local_dom_window.cc, local_frame_view.cc
LayoutLayout
Chrome computes the size and position of every element that needs laying out.
Layout measures the layout pass, also called reflow, in which Blink computes geometry for the dirty part of the layout tree. Phase X (complete), so dur is the real cost.
The interesting data sits in args.beginData:
dirtyObjects: LayoutObjects marked as needing layout entering this pass.totalObjects: the size of the layout tree.partialLayout: whether Blink scoped the work to a subtree.
args.endData.layoutRoots lists the subtree roots that were laid out, each one with a nodeId, a depth and quads (the resulting geometry). That is your attribution path back to actual elements.
The ratio is what matters, not either number alone. dirtyObjects close to totalObjects means you relaid out essentially the whole document. A small fraction means Blink successfully scoped the work. DevTools' own DOM-size insight thresholds directly on dirtyObjects.
totalObjects is the other thing to read. Every layout walks the whole layout tree, so a big one makes every subsequent layout more expensive. That is the real argument for keeping node counts down. The node count is not itself the cost. What it does to every layout after it is.
{
"name": "Layout",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103040878,
"dur": 138,
"tdur": 135,
"tts": 131619,
"pid": 8257,
"tid": 8257,
"args": {
"beginData": {
"dirtyObjects": 6,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"partialLayout": false,
"sampleTraceId": 6909218608593003,
"stackTrace": [
{
"columnNumber": 39,
"functionName": "",
"lineNumber": 85,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"totalObjects": 10
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- typical
- undeclared args
args.beginData.stackTrace[].columnNumber,args.beginData.stackTrace[].functionName,args.beginData.stackTrace[].lineNumber,args.beginData.stackTrace[].scriptId,args.beginData.stackTrace[].url- references
- local_frame_view.cc, layout_subtree_root_list.cc, source_location.cc, v8-stack-trace-impl.cc, v8-debugger.cc
LocalFrameView::ScheduleAnimationnot in DevTools recordingsnot in DevTools' model
Something on the page has asked Chrome for a new frame.
A DevTools Performance recording does not contain this event. Record cc with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
LocalFrameView::ScheduleAnimation fires when something asks Chrome for a new frame. It is emitted under the cc category by LocalFrameView::ScheduleAnimation in local_frame_view.cc, once per request, before the call reaches the chrome client.
The args are the point. location is a base::Location, giving you location.file_name, location.function_name and location.line_number for the exact call site that requested the frame. The common corpus value is ScheduleAnimationIfNeeded at third_party/blink/renderer/core/dom/scripted_animation_controller.cc:250, the rAF and callback path. delay is usually 0. frame expands to frame.is_main_frame, frame.is_cross_origin_to_parent and frame.document.url.
This is the only cheap way to attribute a wasted frame to the code that asked for it. On a page with iframes the frame fields also tell you which document is driving the compositor.
{
"name": "LocalFrameView::ScheduleAnimation",
"cat": "cc",
"ph": "X",
"ts": 1102925982,
"dur": 9,
"tdur": 8,
"tts": 30078,
"pid": 8257,
"tid": 8257,
"args": {
"delay": 0,
"frame": {
"document": {
"url": "http://127.0.0.1:8801/"
},
"is_cross_origin_to_outermost_main_frame": false,
"is_cross_origin_to_parent": false,
"is_main_frame": true,
"is_outermost_main_frame": true
},
"location": {
"file_name": "third_party/blink/renderer/core/dom/scripted_animation_controller.cc",
"function_name": "ScheduleAnimationIfNeeded",
"line_number": 248
}
}
}- categories
- cc
- usage
- high
- duration
- short
- references
- scripted_animation_controller.cc, frame.h, location.cc, document.cc, local_frame_view.cc, traced_value_support.h and 2 more
LocalFrameView::UpdateStyleAndLayoutnot in DevTools recordingsnot in DevTools' model
Chrome runs style and layout for one frame's view.
A DevTools Performance recording does not contain this event. Record blink with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
LocalFrameView::UpdateStyleAndLayout measures the actual style and layout run for one frame view, from third_party/blink/renderer/core/frame/local_frame_view.cc. It wraps UpdateStyleAndLayoutInternal(), then the autosize loop, then counter updates for generated scroll markers.
There are no arguments, so the duration is all you get, and the short slices are where the meaning sits. The function opens with a guard that returns immediately if the view IsInPerformLayout(), ShouldThrottleRendering(), the document is not active, the frame is provisional, or the lifecycle is postponed. A near zero slice means the update was refused, not that layout was cheap. Throttled cross origin iframes generate a lot of these.
Reading these events separates refused updates from real ones, which is a distinction DevTools folds away into a single Layout slice per frame. Real durations nest under Document::UpdateStyleAndLayout and, when the caller was script rather than the rendering lifecycle, under Blink.ForcedStyleAndLayout.UpdateTime. These run faster than most trace events, so anything in the tens of milliseconds is a layout to open up.
{
"name": "LocalFrameView::UpdateStyleAndLayout",
"cat": "blink",
"ph": "X",
"ts": 1102923242,
"dur": 4,
"tdur": 4,
"tts": 28430,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- blink
- usage
- very high
- duration
- very short
- references
- local_frame_view.cc
PaintPaint
Chrome records the drawing operations for one layer, ready to be turned into pixels later.
Paint records the drawing operations Blink captures for one layer. No pixels exist until RasterTask plays that recording back, so nothing is rasterised here in spite of the name. A long Paint is an expensive recording, not expensive pixel work.
args.data gives you layerId, the nodeId and nodeName of the painted element, and clip, which is the paint rect as a quad. The clip is the useful part when you are trying to work out how much was repainted rather than merely that something was.
InvalidationsHandler consumes this alongside the invalidation events, which is how DevTools answers "why did this repaint" rather than just "this repainted".
{
"name": "Paint",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1102944950,
"dur": 444,
"tdur": 287,
"tts": 41633,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"clip": [0, 0, 1280, 0, 1280, 1100, 0, 1100],
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"layerId": 0,
"nodeId": 3,
"nodeName": "#document",
"sampleTraceId": 6909218608594407
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- short
- undeclared args
args.data.sampleTraceId- references
- paint_layer_painter.cc, node.cc
PaintImagePaint image
Chrome paints one image onto the page.
PaintImage records one image being painted. width and height are the size it was painted at, and srcWidth and srcHeight are its intrinsic size. url is the image source. nodeId and nodeName identify the element. loading and srcset are those attributes as authored, and isCSS says whether the image came from a background property or an <img> element.
Reading PaintImage events tells you which images were served larger than they were displayed. Divide the intrinsic dimensions by the painted dimensions, per image. An image whose srcWidth is ten times its painted width delivered about a hundred times the pixel data it needed. This is a direct measurement rather than a heuristic, which is what makes it worth more than any estimate a tool can offer.
It is also the anchor for image attribution generally. ImagePaintingHandler ties Decode Image and Resize Image back to a PaintImage, so decode cost can be blamed on a specific element and URL, and LayoutShiftsHandler consumes it because images that paint late are a common cause of shifts.
{
"name": "PaintImage",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1108423745,
"dur": 121,
"tdur": 120,
"tts": 91874,
"pid": 8407,
"tid": 8407,
"args": {
"data": {
"frame": "2EA8810E92C12C680C55B2941251AA98",
"height": 346.5,
"isCSS": false,
"isPicture": false,
"loadingAttribute": "eager",
"nodeId": 274,
"nodeName": "IMG class='Image-styles__ImageStyled-sc-8c99a12b-0 cVsHni'",
"srcHeight": 360,
"srcWidth": 640,
"srcsetAttribute": "https://ichef.bbci.co.uk/news/240/cpsprodpb/09f1/live/aaa670e0-af38-11f1-a540-61c3f7fc4e6c.jpg.webp 240w,https://ichef.bbci.co.uk/news/320/cpsprodpb/09f1/live/a... [578 more chars]",
"url": "https://ichef.bbci.co.uk/news/640/cpsprodpb/09f1/live/aaa670e0-af38-11f1-a540-61c3f7fc4e6c.jpg.webp",
"width": 616,
"x": 332,
"y": 529
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- duration
- typical
- references
- inspector_trace_events.cc, image_painter.cc
PaintSetupPaint setupno longer emitted
Chrome timed the setup work the compositor did before painting a layer's contents.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
PaintSetup timed the setup cc did before painting a layer's contents, and Chrome has not written it since Chrome 45. Its last emitter was BitmapContentLayerUpdater, part of the cc painting path that predates impl-side painting, deleted in June 2015 by commit 4dac85e885. Impl-side painting had already left that updater unused, so real traces may have lost the event a release or two earlier.
The kPaintSetup constant is still defined in cc/base/devtools_instrumentation.h, but nothing uses it. DevTools also keeps the name, labelled "Paint setup", with a translator note calling it a step before the Paint event.
In a trace from Chrome 44 or earlier, treat it as part of the paint bucket when summing rendering cost. Lighthouse groups it under "Rendering" together with Paint, PaintImage and the raster events. In a current trace, the step before Paint is PrePaint.
Chrome has not written this since Chrome 45, when its last emitter in cc was deleted. The name survives only as an unused constant in cc and an entry in DevTools.
PrePaintPre-paint
Chrome works out what actually needs repainting before any painting starts.
PrePaint updates the paint property trees (transform, clip, effect) and computes what actually needs repainting, before any pixels are touched. DevTools describes it as "a step before the 'Paint' event".
It has no useful args of its own. Its significance is positional. It runs after layout and before paint, and LayoutShiftsHandler consumes it because layout shift scores are finalised around this point in the frame.
Older Chrome named it UpdateLayerTree. The rename landed in crrev.com/c/3519012, shipping in Chrome 102, and Lighthouse's task-groups still lists both names for back-compat. If you parse traces across Chrome versions you must handle both.
{
"name": "PrePaint",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1102923261,
"dur": 32,
"tdur": 27,
"tts": 28446,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"isMainFrame": true,
"isOutermostMainFrame": true,
"page": "14A831DCB73EB1205B67CDB89FC6EBD7"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- typical
- undeclared args
args.data.frame,args.data.isMainFrame,args.data.isOutermostMainFrame,args.data.page- references
- inspector_trace_events.cc, document.cc, local_dom_window.cc, local_frame_view.cc
RasterTaskRasterize paint
A raster worker thread turns recorded drawing operations into pixels for one tile.
RasterTask turns recorded paint operations into pixels for one tile. It runs on a raster worker thread, not the main thread, which is why a page can have heavy raster cost while the main thread looks idle.
Four fields sit in args.tileData: layerId, tileId, tileResolution and sourceFrameNumber. tileResolution distinguishes low-resolution placeholder raster from full-resolution work. layerId is your route back to which layer, and via LayerTreeHandler which element, is expensive to draw.
Heavy raster usually comes from large composited layers, from expensive paint operations such as large blurs, complex filters and box-shadow over big areas, or simply from too much promoted area. It is also the work most likely to be missed entirely if you only look at main-thread flame charts.
{
"name": "RasterTask",
"cat": "cc,disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102928322,
"dur": 35,
"tdur": 33,
"tts": 1420,
"pid": 8178,
"tid": 8209,
"args": {
"tileData": {
"layerId": 37,
"sourceFrameNumber": 1,
"tileId": {
"id_ref": "0x13f4017a89a0"
},
"tileResolution": "HIGH_RESOLUTION"
}
}
}- categories
- cc, disabled-by-default-devtools.timeline
- usage
- high
- duration
- typical
- references
- frame_viewer_instrumentation.cc, tile_manager.cc, picture_layer_impl.cc, devtools_instrumentation.h, picture_layer.cc, layer_tree_host.h and 3 more
RequestMainThreadFrameRequest main thread frame
The compositor asks the main thread to produce a frame.
RequestMainThreadFrame marks the compositor asking the main thread to produce a frame. Frames start on the compositor, not on the main thread. layerTreeId is its only field.
FramesHandler consumes it as part of frame lifecycle reconstruction. Use it as the start marker when measuring how long the main thread took to respond to a frame request. A long gap between this and the corresponding BeginMainThreadFrame means the main thread was busy with something else and the frame was delayed before any rendering work even began.
{
"name": "RequestMainThreadFrame",
"cat": "disabled-by-default-devtools.timeline.frame",
"ph": "I",
"ts": 1102922942,
"tts": 1523,
"pid": 8257,
"tid": 8278,
"s": "t",
"args": {
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline.frame
- usage
- moderate
- references
- devtools_instrumentation.h, proxy_impl.cc
ScheduleStyleRecalculationSchedule style recalculation
Chrome marks an element's style as stale and queues a style recalculation.
ScheduleStyleRecalculation marks the moment Blink decided some element's style is stale and queued a recalculation. It is an instant event, and it is not the recalculation itself. That work is UpdateLayoutTree, which usually lands later in the same frame.
args.data.reason names the trigger and is the most useful field here; it is also the field most likely to be absent on older traces. args.data.subtree tells you whether the invalidation was scoped to one element or poisoned an entire subtree. args.data.nodeId identifies the element that caused it.
Reading these events tells you where an expensive style pass came from. A subtree invalidation on a node high in the tree is what turns a cheap pass into an expensive one. If you are chasing a long UpdateLayoutTree, find the ScheduleStyleRecalculation that preceded it. DevTools uses exactly this pairing to draw its "caused by" initiator arrow.
{
"name": "ScheduleStyleRecalculation",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1102950872,
"tts": 45984,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"sampleTraceId": 6909218608594505,
"stackTrace": [
{
"columnNumber": 81,
"functionName": "note",
"lineNumber": 24,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
},
{
"columnNumber": 5,
"functionName": "",
"lineNumber": 47,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
]
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- source_location.cc, v8-stack-trace-impl.cc, v8-debugger.cc, document.cc
ScrollLayerScroll
This event tells you a layer has scrolled.
ScrollLayer fires when a layer scrolls. args.data.frame names the frame. args.data.nodeId identifies the scrolling element and is not always present.
FramesHandler consumes it. Use these to confirm whether scrolling is being handled on the compositor or has fallen back to the main thread. If you see main-thread work correlating with these on every scroll frame, something has pulled scrolling back onto the main thread: a non-passive listener, or a scroll-linked effect.
{
"name": "ScrollLayer",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1104989335,
"dur": 190,
"tdur": 85,
"tts": 219835,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"nodeId": 3,
"nodeName": "#document"
}
}
}- categories
- devtools.timeline
- usage
- low
- duration
- typical
- undeclared args
args.data.nodeName- references
- node.cc, paint_layer_scrollable_area.cc
StyleResolver::ResolveStyleneeds Enable CSS selector stats (slow) or Invalidation trackingnot in DevTools' model
Chrome works out the computed style for one element.
A DevTools Performance recording contains this event only with Enable CSS selector stats (slow) or Invalidation tracking turned on, which records disabled-by-default-devtools.timeline.invalidationTracking.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
StyleResolver::ResolveStyle records one computed style produced for one element. It is emitted as an instant event from StyleResolver::ResolveStyle in third_party/blink/renderer/core/css/resolver/style_resolver.cc, behind an InvalidationTracingFlag::IsEnabled() check, so it only exists when the disabled-by-default-devtools.timeline.invalidationTracking category is on.
args.data comes from inspector_style_resolver_resolve_style_event::Data in inspector_trace_events.cc. nodeId is the element. parentNodeId is the parent or shadow host, and is 0 at the top. pseudoId is 0 for a real element and non-zero for ::before, ::first-line and the rest. There is no duration.
Reading these events tells you which elements Blink keeps recomputing. The value is the count per nodeId. An element resolved hundreds of times is being re-resolved every frame, in practice a script writing inline styles or toggling classes inside rAF. DevTools gives you a recalc slice and an element count, never the node that earned it. Blink's independent inherited properties optimisation skips ResolveStyle entirely, so this undercounts total recalc and overcounts the expensive path.
{
"name": "StyleResolver::ResolveStyle",
"cat": "disabled-by-default-devtools.timeline.invalidationTracking",
"ph": "I",
"ts": 1102940676,
"tts": 40052,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"nodeId": 2,
"parentNodeId": 0,
"pseudoId": 0
}
}
}- categories
- disabled-by-default-devtools.timeline.invalidationTracking
- usage
- very high
- references
- style_resolver.cc, inspector_trace_events.cc, computed_style_constants.h
UpdateLayerUpdate layer
Chrome updates one composited layer.
UpdateLayer covers Blink updating one composited layer. layerId and layerTreeId are the entire payload.
LayerTreeHandler consumes it and maintains the layer-tree model that lets DevTools resolve a layerId seen elsewhere, in Paint or in RasterTask's tileData, back to something meaningful. On its own the event is low-signal. As part of that model it is what makes layer attribution possible at all.
{
"name": "UpdateLayer",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102926233,
"dur": 460,
"tdur": 372,
"tts": 186859,
"pid": 8178,
"tid": 8178,
"args": {
"layerId": 29,
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- high
- duration
- very short
- references
- devtools_instrumentation.h, picture_layer.cc, frame_viewer_instrumentation.cc, tile_manager.cc
UpdateLayerTreeUpdate layer treeno longer emitted
Chrome worked out what needed repainting, under the name this step carried before Chrome 102.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
UpdateLayerTree recorded the pre-paint pass until Chrome 102, when it was renamed PrePaint. The rename is commit 3a09b7b7dd, "Add a Pre-Paint devtools timeline event", reviewed as crrev.com/c/3519012 and landed in March 2022. The old event was on devtools.timeline, and no current Chromium code writes the old name.
You will still see it in traces captured on Chrome 101 and earlier, and Lighthouse deliberately lists both names in the same task group so historical traces keep categorising correctly.
If you are writing a trace parser, treat UpdateLayerTree and PrePaint as the same pipeline stage.
Renamed to PrePaint in Chrome 102. The collection recorded PrePaint on every site and this name on none.
- references
- local_frame_view.cc
UpdateLayoutTreeRecalculate style
Chrome runs the style recalculation pass, working out the computed style of your elements.
UpdateLayoutTree runs the style recalculation pass. The Performance panel shows it as "Recalculate Style", not as "UpdateLayoutTree". DevTools aliases it, and TraceEvents.d.ts says so outright: "The real trace event is called 'UpdateLayoutTree' but we've aliased it for convenience." If you are grepping a raw trace for "RecalcStyle" you will find nothing.
args.elementCount is the number of elements whose computed style was recalculated in this pass. Divide that number by the duration and you have your cost per element. A large elementCount with a short duration is healthy. A small one with a long duration points at expensive selectors.
For selector-level attribution you need SelectorStats enabled. The SelectorStatsHandler consumes this event alongside those stats to tell you which selectors cost the time, which you cannot get from the aggregate.
Long style passes have two usual causes: invalidating too much, which you chase through ScheduleStyleRecalculation, or selectors that are expensive to match.
{
"name": "UpdateLayoutTree",
"cat": "blink,devtools.timeline",
"ph": "X",
"ts": 1103040787,
"dur": 83,
"tdur": 76,
"tts": 131537,
"pid": 8257,
"tid": 8257,
"args": {
"beginData": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"sampleTraceId": 6909218608593002,
"stackTrace": [
{
"columnNumber": 39,
"functionName": "",
"lineNumber": 85,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
]
},
"elementCount": 1
}
}- categories
- blink, devtools.timeline
- usage
- moderate
- duration
- long
- undeclared args
args.beginData.stackTrace[].columnNumber,args.beginData.stackTrace[].functionName,args.beginData.stackTrace[].lineNumber,args.beginData.stackTrace[].scriptId,args.beginData.stackTrace[].url- references
- document.cc, source_location.cc, v8-stack-trace-impl.cc, v8-debugger.cc, style_resolver.cc
Invalidation tracking
Why Blink decided something was dirty. Needs its own trace category.
LayoutImageUnsized
The layout engine had to size an image from the decoded file rather than from CSS.
LayoutImageUnsized flags an image that the layout engine had to size from the decoded bitmap rather than from CSS. It is a TRACE_EVENT_INSTANT on devtools.timeline from third_party/blink/renderer/core/layout/layout_image.cc, and it depends on the invalidation tracking category being recorded, so its absence proves nothing.
args.data.nodeId identifies the image element and args.data.frameId identifies the frame. Chrome is naming the element for you. No heuristic, no inference from rect deltas.
Get the definition right, because it is stricter than people assume. LayoutImage::IsUnsizedImage() counts an image as sized when it has both a fixed or percentage width and height, or an aspect-ratio plus one of the two. A width on its own does not satisfy it. Unsized images are one of the most common causes of layout shift, and the Web Almanac 2025 found 62% of mobile pages ship at least one image without explicit dimensions. Its only consumer is LayoutShiftsHandler, and the reason is obvious: an unsized image plus a later shift affecting the same node id is a node-id-level causal link, not a timing correlation.
{
"name": "LayoutImageUnsized",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1109175726,
"tts": 95991,
"pid": 8475,
"tid": 8475,
"s": "t",
"args": {
"data": {
"frameId": "9AE6572E1DCD19954F9F2627AE88998A",
"nodeId": 16
}
}
}- categories
- devtools.timeline
- usage
- moderate
- references
- layout_image.cc, identifiers_factory.cc
LayoutInvalidationTrackingneeds Enable CSS selector stats (slow) or Invalidation tracking
This event tells you what marked an element's layout dirty.
A DevTools Performance recording contains this event only with Enable CSS selector stats (slow) or Invalidation tracking turned on, which records disabled-by-default-devtools.timeline.invalidationTracking.
LayoutInvalidationTracking records something marking layout dirty. Phase I. The event exists only in traces recorded with disabled-by-default-devtools.timeline.invalidationTracking, a category that is off in a default DevTools recording, which is why most traces contain none of these.
nodeId and nodeName identify the node. reason is the crucial one. It is a string from Blink naming the actual cause: a style change, an attribute change, a size change on a replaced element, fonts arriving.
That is a level of attribution nothing else in the trace offers. InvalidateLayout tells you layout was dirtied and gives you a stack when a script did it. This event tells you why Blink considered it dirty, including for the many invalidations no script triggered. LayoutShiftsHandler consumes it, which tells you what it is for: connecting a shift to the invalidation that preceded it. If you are debugging layout thrash and you only have InvalidateLayout events with no stacks, re-record with invalidation tracking on before concluding the cause is unknowable.
{
"name": "LayoutInvalidationTracking",
"cat": "disabled-by-default-devtools.timeline.invalidationTracking",
"ph": "I",
"ts": 1102950826,
"tts": 45939,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"nodeId": 14,
"nodeName": "#text",
"reason": "Removed from layout",
"stackTrace": [
{
"columnNumber": 81,
"functionName": "note",
"lineNumber": 24,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
},
{
"columnNumber": 5,
"functionName": "",
"lineNumber": 47,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
]
}
}
}- categories
- disabled-by-default-devtools.timeline.invalidationTracking
- usage
- very high
- undeclared args
args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- v8-debugger.cc, layout_invalidation_reason.h, capture_source_location.cc, node.cc, source_location.cc, v8-stack-trace-impl.cc and 1 more
ScheduleStyleInvalidationTrackingneeds Enable CSS selector stats (slow) or Invalidation tracking
Chrome has spotted a change to an element and scheduled a style invalidation for it.
A DevTools Performance recording contains this event only with Enable CSS selector stats (slow) or Invalidation tracking turned on, which records disabled-by-default-devtools.timeline.invalidationTracking.
ScheduleStyleInvalidationTracking fires when Blink schedules a style invalidation, which is Blink noticing that something might need its style recomputed. Phase I, and it shows up only when the trace was captured with invalidation tracking recorded.
The args are the most specific in the entire trace for CSS work: changedClass, changedAttribute, changedId, invalidationSet, invalidatedSelectorId, reason, nodeId, nodeName, and a stackTrace.
Reach for changedClass first. It names the exact class that was added or removed. A framework toggling is-active on a container fifty times a second shows up here by name, with a stack pointing at the code doing it. Remember that this is scheduling, not work: the cost lands later in UpdateLayoutTree. A pile of these collapsing into one recalc is Blink batching correctly. A pile of them each followed by their own recalc is the thrash pattern to fix.
{
"name": "ScheduleStyleInvalidationTracking",
"cat": "disabled-by-default-devtools.timeline.invalidationTracking",
"ph": "I",
"ts": 1108824349,
"tts": 460859,
"pid": 8407,
"tid": 8407,
"s": "t",
"args": {
"data": {
"changedClass": "jUHzNV",
"frame": "2EA8810E92C12C680C55B2941251AA98",
"invalidatedSelectorId": "class",
"invalidationSet": "0x2764030ccf40",
"nodeId": 130,
"nodeName": "DIV class='Backdrop-styles__BackdropStyled-sc-9f7d4825-0 dya-dBg'",
"stackTrace": [
{
"columnNumber": 8015,
"functionName": "E",
"lineNumber": 1,
"scriptId": "46",
"url": "https://static.files.bbci.co.uk/bbcdotcom/web/20260907-085256-bea8b058c0-web-3.21.0/_next/static/chunks/0y-nyl-getpga.js"
},
{
"columnNumber": 91540,
"functionName": "oq",
"lineNumber": 1,
"scriptId": "46",
"url": "https://static.files.bbci.co.uk/bbcdotcom/web/20260907-085256-bea8b058c0-web-3.21.0/_next/static/chunks/0y-nyl-getpga.js"
},
{
"columnNumber": 90771,
"functionName": "oQ",
"lineNumber": 1,
"scriptId": "46",
"url": "https://static.files.bbci.co.uk/bbcdotcom/web/20260907-085256-bea8b058c0-web-3.21.0/_next/static/chunks/0y-nyl-getpga.js"
},
"... [112 more items]"
]
}
}
}- categories
- disabled-by-default-devtools.timeline.invalidationTracking
- usage
- moderate
- undeclared args
args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url,args.data.changedPseudo- references
- v8-debugger.cc, rule_invalidation_data.cc, invalidation_set.cc, capture_source_location.cc, source_location.cc, v8-stack-trace-impl.cc and 1 more
SelectorStatsneeds Enable CSS selector stats (slow)
Chrome reports how often each CSS selector was matched and how long that matching took.
A DevTools Performance recording contains this event only with Enable CSS selector stats (slow) turned on, which records disabled-by-default-blink.debug.
SelectorStats reports per-selector matching statistics. Phase X, with everything inside args.selector_stats.
For each selector Blink reports how many times it was matched, how long matching took, and how often it fast-rejected. This is what powers the "CSS selector costs" view in DevTools, and it makes slow selectors rankable by real measured cost rather than by how complicated they look.
The finding it produces is usually not the selector you would guess. Complex-looking selectors often fast-reject cheaply. The expensive ones tend to be broad selectors evaluated against large subtrees, repeatedly. Recording all this has real overhead, so it sits behind the disabled-by-default-blink.debug category and is absent from most traces. The DevTools setting "Enable CSS selector stats" works by switching that category on, and any trace that enables the category gets the event.
Not seen, because the collection never enabled the disabled-by-default-blink.debug category that gates it. The CSS selector stats setting in DevTools works by switching that category on, and any trace that enables it produces the event.
- categories
- disabled-by-default-blink.debug
- references
- element_rule_collector.cc
StyleInvalidatorInvalidationTrackingneeds Enable CSS selector stats (slow) or Invalidation tracking
Chrome names the CSS selectors responsible for invalidating an element's style.
A DevTools Performance recording contains this event only with Enable CSS selector stats (slow) or Invalidation tracking turned on, which records disabled-by-default-devtools.timeline.invalidationTracking.
StyleInvalidatorInvalidationTracking records the selector-level detail behind a style invalidation. Phase I. Your trace will not include it unless invalidation tracking was on when you recorded.
args.data.invalidationList gives an id and the classes involved for each invalidation. args.data.selectors is an array, and every entry in it has the literal selector text plus the style_sheet_id it came from.
That means you can name the CSS rule responsible for an invalidation, in source form, and say which stylesheet it lives in. For a page with expensive style recalculation this is the difference between "CSS is slow" and "this descendant selector in this file is being re-evaluated against a large subtree". The event pairs with StyleRecalcInvalidationTracking, which supplies the subtree flag for the same invalidation, and the SelectorStatsHandler comment documents the pairing explicitly.
{
"name": "StyleInvalidatorInvalidationTracking",
"cat": "disabled-by-default-devtools.timeline.invalidationTracking",
"ph": "I",
"ts": 1235492157,
"tts": 207175,
"pid": 10309,
"tid": 10309,
"s": "t",
"args": {
"data": {
"frame": "614FC5655E48B642B0235ABCA128B0C6",
"invalidationList": [
{
"attributes": ["collapsed", "active", "fixed", "position", "animatable"],
"classes": ["hide-collapsed-panel", "devsite-concierge-panel", "devsite-concierge-panel-chat-input", "devsite-sidebar", "devsite-book-nav-toggle", "devsite-floating-action-buttons", "mobile-view-not-enabled", "devsite-concierge-menu-icon", "... [13 more items]"],
"id": "0x109400913c00",
"ids": ["gc-wrapper"],
"tagNames": ["devsite-header", "devsite-footer-promos", "devsite-concierge", "devsite-panel", "google-codelab", "devsite-footer-linkboxes", "devsite-book-nav", "devsite-concierge-ai-panel", "... [3 more items]"]
}
],
"nodeId": 22,
"nodeName": "SECTION class='devsite-wrapper'",
"reason": "Invalidation set matched class",
"selectorCount": 1,
"selectorPart": "devsite-wrapper",
"selectors": [
{
"selector": "body[ready] .devsite-wrapper",
"style_sheet_id": "style-sheet-10309-1"
}
]
}
}
}- categories
- disabled-by-default-devtools.timeline.invalidationTracking
- usage
- high
- undeclared args
args.data.invalidationList[].allDescendantsMightBeInvalid,args.data.selectorCount,args.data.selectorPart,args.data.invalidationList[].tagNames[],args.data.invalidationList[].customPseudoNames[],args.data.invalidationList[].treeBoundaryCrossing,args.data.invalidationList[].attributes[],args.data.invalidationList[].ids[]and 3 more- references
- invalidation_set.cc, style_invalidator.cc, invalidation_set_to_selector_map.h, identifiers_factory.cc, inspector_trace_events.h
StyleRecalcInvalidationTrackingneeds Enable CSS selector stats (slow) or Invalidation tracking
Chrome marks an element as needing its style recalculated.
A DevTools Performance recording contains this event only with Enable CSS selector stats (slow) or Invalidation tracking turned on, which records disabled-by-default-devtools.timeline.invalidationTracking.
StyleRecalcInvalidationTracking marks an element as needing style recalculation. Phase I. Your trace includes it only when disabled-by-default-devtools.timeline.invalidationTracking was recorded.
Five fields sit in args.data: nodeId, nodeName, reason, extraData and a subtree flag.
subtree is the field that matters for cost. true means the invalidation applies to the element and everything beneath it, so a subtree invalidation near the root is effectively a whole-document style recalc no matter how small the change that caused it looked. Per the SelectorStatsHandler source, a subtree invalidation arrives as a pair: a StyleInvalidatorInvalidationTracking with the selector list, then this event with the subtree flag. Read them together or you get half the picture.
{
"name": "StyleRecalcInvalidationTracking",
"cat": "disabled-by-default-devtools.timeline.invalidationTracking",
"ph": "I",
"ts": 1102950866,
"tts": 45978,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"extraData": "",
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"nodeId": 15,
"nodeName": "#text",
"reason": "Node was inserted into tree",
"stackTrace": [
{
"columnNumber": 81,
"functionName": "note",
"lineNumber": 24,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
},
{
"columnNumber": 5,
"functionName": "",
"lineNumber": 47,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"subtree": false
}
}
}- categories
- disabled-by-default-devtools.timeline.invalidationTracking
- usage
- very high
- undeclared args
args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- v8-debugger.cc, style_change_reason.h, style_change_reason.cc, node.cc, element.cc, node.h and 3 more
JavaScript and V8
Execution, compilation, timers, scheduling and garbage collection.
AbortPostTaskCallbackCancel postTask
A task the page scheduled with postTask() was cancelled before it ever ran.
AbortPostTaskCallback marks a scheduled task being dropped before it ran, because an AbortSignal fired. taskId identifies the task that was dropped, while frame and stackTrace are optional.
The stackTrace tells you which teardown path did the cancelling. Read these when auditing whether a page correctly cancels scheduled work on navigation or teardown. Abandoned tasks that still run after their context is gone are a common source of wasted main-thread time in single-page apps.
{
"name": "AbortPostTaskCallback",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103043890,
"dur": 22,
"tdur": 21,
"tts": 133996,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"sampleTraceId": 6909218608593085,
"stackTrace": [
{
"columnNumber": 27,
"functionName": "",
"lineNumber": 55,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"taskId": 1
}
}
}- categories
- devtools.timeline
- references
- source_location.cc, v8-debugger.cc, dom_scheduler.h, dom_task.cc, script_loader.cc, heap.cc
BlinkGC.AtomicPhaseno longer emitted
Blink's garbage collector stopped all other work on the thread while it collected C++ objects.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
BlinkGC.AtomicPhase timed the stop-the-world phase of Oilpan, the garbage collector for DOM nodes and other Blink C++ objects, until Chrome 84. Blink's own heap code in thread_state.cc wrote it on blink_gc,devtools.timeline, and it is gone from that file by Chrome 85. Blink's heap now runs on V8's cppgc, which writes CppGC.* scopes such as CppGC.IncrementalSweep on the cppgc category instead.
That heap is separate from V8's JavaScript heap. trace_engine models no arguments for the old event, and DevTools still labels it "DOM GC" in the Performance panel.
Being atomic, the phase could not be interrupted, so its duration was main-thread time the page could not reclaim. Large detached DOM trees are the usual reason C++ collections grow: nodes removed from the document but still referenced by JavaScript stay alive and must be traced on every collection.
Chrome stopped writing this in Chrome 85, when it left Blink's own heap code. Blink's garbage collector now runs on V8's cppgc, which writes CppGC.* events instead.
- references
- thread_state.cc
CancelAnimationFrameCancel animation frame
The page called cancelAnimationFrame() to drop a callback it had already registered.
CancelAnimationFrame marks a call to cancelAnimationFrame(). trace_engine leaves the event untyped, so the callback id is not available through the typed API, although the raw trace does record it along with a stackTrace.
On its own it is low signal. It earns its place when you reconcile RequestAnimationFrame counts against FireAnimationFrame counts: cancellations explain registrations that never fired, and a scheduling analysis that does not account for them looks broken when it is not.
{
"name": "CancelAnimationFrame",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1109193538,
"tts": 107873,
"pid": 8475,
"tid": 8475,
"s": "t",
"args": {
"data": {
"frame": "9AE6572E1DCD19954F9F2627AE88998A",
"id": 1,
"sampleTraceId": 6899808067347206,
"stackTrace": [
{
"columnNumber": 136403,
"functionName": "n",
"lineNumber": 1,
"scriptId": "7",
"url": "https://cdn.privacy-mgmt.com/Notice.97af9.js"
}
]
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.frame,args.data.id,args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- source_location.cc, v8-debugger.cc, frame_request_callback_collection.cc, inspector_trace_events.cc, script_loader.cc, heap.cc
ConsoleTimeConsole timenever written by Chrome
Your own console.time() measurement, under the name DevTools files it by.
No Chrome build writes this name into a trace file. It exists in the model, not on the wire.
ConsoleTime is the DevTools name for a console.time() span, but Chrome never writes an event called that. TraceFilter maps every event on the blink.console category to the type ConsoleTime, which is how DevTools lets you filter the flame chart down to your own marks.
In the trace itself, Blink writes the span as an async pair on blink.console, named after the label passed to console.time(), from thread_debugger_common_impl.cc. V8 separately writes V8Console::Time and V8Console::TimeEnd on disabled-by-default-v8.inspector, which time the two calls rather than the span between them.
This is developer-authored instrumentation rather than browser behaviour. Reading these tells you what the author of the code thought mattered.
Chrome never writes this name, which DevTools gives to any event on the blink.console category. A fixture page calling console.time and console.timeEnd produced V8Console::Time, V8Console::TimeEnd and one event named after the label.
- references
- TraceFilter.ts
CppGC.IncrementalSweepC++ GC
Chrome's C++ garbage collector frees dead objects, one chunk of the heap at a time.
CppGC.IncrementalSweep covers one chunk of sweeping in Oilpan, Chrome's C++ garbage collector.
No arguments are modelled, so duration and timing are all you get.
Sweeping is split into chunks specifically to avoid long pauses, which is why individual events are short. Their frequency is the signal instead. Sustained sweeping indicates significant C++ side object churn, usually from heavy DOM creation and destruction.
Not seen. The fixture page was traced with the cppgc category on, and no C++ garbage collection ran while it was being recorded. The site traces did not enable cppgc at all.
- categories
- cppgc
- references
- stats-collector.h
DoDecryptDecrypt
Chrome decrypts data for a call to Web Crypto's decrypt().
DoDecrypt marks one call to Web Crypto's decrypt().
No arguments are attached. In every other respect the event behaves like DoEncrypt.
Symmetric decryption is hardware accelerated on essentially all current CPUs, so a decrypt() that becomes visible in a trace is usually a volume problem rather than an algorithm problem. No site in the 35 site corpus emitted one at all.
{
"name": "DoDecrypt",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102938330,
"dur": 6,
"tdur": 5,
"tts": 38495,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- references
- webcrypto_impl.cc
DoDecryptReplyDecrypt reply
The decrypted data reaches JavaScript and the promise from decrypt() resolves.
DoDecryptReply fires when the promise returned by decrypt() resolves with the plaintext.
Nothing is attached to the event, so pairing it with DoDecrypt to time one decrypt() is all it gives you.
The promise resolves as a microtask on the thread that made the call, which means the continuation runs on the main thread even though the decryption did not.
{
"name": "DoDecryptReply",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102938337,
"dur": 1,
"tdur": 1,
"tts": 38500,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- references
- webcrypto_impl.cc
DoDigestDigest
Chrome hashes a block of data for a call to Web Crypto's digest().
DoDigest times one call to digest(), the Web Crypto hashing entry point.
No arguments are attached, so neither the algorithm nor the size of the input is in the trace.
SHA-2 is hardware accelerated on most current CPUs, so the choice of hash is rarely what makes one of these slow. Hashing large payloads is the most common way Web Crypto shows up as a real cost. Subresource integrity checks and client-side file hashing both land here. Five of the 35 sites in the corpus produced any, at low volume and a typical duration for a trace event.
{
"name": "DoDigest",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102937607,
"dur": 150,
"tdur": 56,
"tts": 38231,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- duration
- short
- references
- webcrypto_impl.cc
DoDigestReplyDigest reply
The finished hash reaches JavaScript and the promise from digest() settles.
DoDigestReply fires when the promise returned by digest() settles with the hash.
The event has no arguments, so the span back to DoDigest is the whole of what it offers.
Use that span to time a single hash. If the same bytes are hashed more than once in a trace, the result was not cached and could have been.
{
"name": "DoDigestReply",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102937758,
"dur": 10,
"tdur": 9,
"tts": 38287,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- duration
- short
- references
- webcrypto_impl.cc
DoEncryptEncrypt
Chrome encrypts data for a call to Web Crypto's encrypt().
DoEncrypt marks a Web Crypto encrypt() call running. The event is untyped, so neither the algorithm nor the data size is modelled and the duration is all you get.
Web Crypto runs off the main thread for most algorithms, so these usually do not block rendering. In the 35 site run they appeared on 3 sites at low volume, at a typical duration for a trace event. Where encryption does become visible is in bulk: encrypting many small payloads individually is far more expensive than one batched operation.
{
"name": "DoEncrypt",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102938184,
"dur": 11,
"tdur": 10,
"tts": 38446,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- low
- duration
- typical
- references
- webcrypto_impl.cc
DoEncryptReplyEncrypt reply
The ciphertext reaches JavaScript and the promise from encrypt() settles.
DoEncryptReply fires when the promise returned by Web Crypto's encrypt() settles and the ciphertext reaches JavaScript.
The event is untyped, so the timestamp is the only thing it contributes.
Pair it with DoEncrypt to get the latency of one operation. That gap includes queueing, so a long gap with short surrounding work means crypto operations are queued behind each other.
{
"name": "DoEncryptReply",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102938196,
"dur": 5,
"tdur": 4,
"tts": 38456,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- low
- duration
- short
- references
- webcrypto_impl.cc
DoSignSign
Chrome produces a signature over a block of bytes for a call to Web Crypto's sign().
DoSign times one call to Web Crypto's sign(), the operation that produces a signature over a block of bytes.
The event is untyped, so it tells you that sign() ran and nothing about what was signed or with which key. The duration is the whole signal.
Asymmetric signing costs far more than hashing. A page that signs per request rather than per session will show that here. Only one of the 35 sites in the corpus signed anything.
{
"name": "DoSign",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102938397,
"dur": 201,
"tdur": 86,
"tts": 38554,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- low
- duration
- long
- references
- webcrypto_impl.cc
DoSignReplySign reply
The finished signature reaches JavaScript and the promise from sign() resolves.
DoSignReply fires when the promise returned by sign() resolves and the signature is available to JavaScript.
There are no arguments, so pair it with DoSign for the cost of one signature.
Almost all of that cost is the asymmetric operation itself rather than marshalling data in and out. There is nothing to optimise inside a single call, so signing less often is the only lever you have.
{
"name": "DoSignReply",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102938598,
"dur": 11,
"tdur": 10,
"tts": 38640,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- low
- duration
- typical
- references
- webcrypto_impl.cc
DoVerifyVerify
Chrome checks a signature against the signed data and a public key for verify().
DoVerify marks one call to Web Crypto's verify(), which checks a signature against the signed data and a public key.
Nothing is attached to the event, so the trace never tells you which algorithm was used.
That gap matters here more than it does elsewhere in Web Crypto, because whether verification is cheaper than signing depends on the algorithm. With RSA it is much cheaper, because the public exponent is tiny. With ECDSA it costs more than signing does. Read the durations only once you know which key type the page uses.
{
"name": "DoVerify",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102938619,
"dur": 67,
"tdur": 66,
"tts": 38659,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- references
- webcrypto_impl.cc
DoVerifyReplyVerify reply
The true or false answer from verify() reaches JavaScript and the promise settles.
DoVerifyReply fires when the promise returned by verify() settles with the true or false result.
The event is untyped, so the span from DoVerify to here is the only latency available.
Count the pairs before you look at their durations. A page verifying a token on every request rather than once per session shows a steady drip of them.
{
"name": "DoVerifyReply",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102938686,
"dur": 2,
"tdur": 1,
"tts": 38725,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- references
- webcrypto_impl.cc
EvaluateScriptEvaluate script
V8 runs a script's top-level code, as opposed to a function inside it being called later.
EvaluateScript covers the top-level evaluation of a script. It covers the script's body running, as opposed to a function inside it being called later.
trace_engine models no arguments for it. Real traces still identify the script: args.data.url is the script URL, scriptId is V8's id for it, and lineNumber and columnNumber locate it. Some events also record a stackTrace.
Its children tell you what the script actually did. A long EvaluateScript with little beneath it is slow top-level code. A long one full of nested FunctionCall and Layout events is a script doing work that could often be deferred. For a classic render-blocking <script>, this is the event sitting between the parser stopping and the parser resuming, which makes it the direct, measurable cost of not using defer or async.
{
"name": "EvaluateScript",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1108624821,
"dur": 3546,
"tdur": 3335,
"tts": 278721,
"pid": 8407,
"tid": 8407,
"args": {
"data": {
"columnNumber": 1,
"frame": "2EA8810E92C12C680C55B2941251AA98",
"lineNumber": 1,
"sampleTraceId": 5267982676863753,
"stackTrace": [
{
"columnNumber": 40894,
"functionName": "preLoad",
"lineNumber": 1,
"scriptId": "12",
"url": "https://gn-web-assets.api.bbc.com/ngas/latest/dotcom-bootstrap.js"
},
{
"columnNumber": 32813,
"functionName": "Xe",
"lineNumber": 1,
"scriptId": "12",
"url": "https://gn-web-assets.api.bbc.com/ngas/latest/dotcom-bootstrap.js"
},
{
"columnNumber": 33897,
"functionName": "",
"lineNumber": 1,
"scriptId": "12",
"url": "https://gn-web-assets.api.bbc.com/ngas/latest/dotcom-bootstrap.js"
},
"... [3 more items]"
],
"url": ""
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- long
- undeclared args
args.data.columnNumber,args.data.frame,args.data.lineNumber,args.data.sampleTraceId,args.data.scriptId,args.data.url,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionNameand 3 more- references
- classic_script.cc, inspector_trace_events.cc, script_loader.cc, source_location.cc, v8-debugger.cc, v8_script_runner.cc and 1 more
EventDispatchEvent
Chrome delivers a DOM event, such as a click, to the listeners registered for it.
EventDispatch records a DOM event being dispatched to its listeners. args.data.type is the event name: click, pointerdown, scroll.
This is the backbone of INP analysis. An interaction's processing time is the work inside the EventDispatch events for that interaction, and the event type tells you which listener chain to blame.
Watch the high-frequency types: pointermove, scroll, mousemove. Each dispatch is individually cheap, and the aggregate saturates the main thread. Non-passive touchstart and wheel listeners are particularly costly because they also block scrolling.
{
"name": "EventDispatch",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1108494551,
"dur": 16,
"tdur": 16,
"tts": 151477,
"pid": 8407,
"tid": 8407,
"args": {
"data": {
"sampleTraceId": 5267982676862248,
"stackTrace": [
{
"columnNumber": 105003,
"functionName": "n.appendTo",
"lineNumber": 2,
"scriptId": "14",
"url": "https://cdn.optimizely.com/public/4621041136/s/bbcx_prod.js"
},
{
"columnNumber": 187223,
"functionName": "",
"lineNumber": 2,
"scriptId": "14",
"url": "https://cdn.optimizely.com/public/4621041136/s/bbcx_prod.js"
},
{
"columnNumber": 4150,
"functionName": "X",
"lineNumber": 2,
"scriptId": "14",
"url": "https://cdn.optimizely.com/public/4621041136/s/bbcx_prod.js"
},
"... [7 more items]"
],
"type": "readystatechange"
}
}
}- categories
- devtools.timeline
- usage
- high
- duration
- short
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- source_location.cc, v8-debugger.cc, inspector_trace_events.cc, script_loader.cc, heap.cc, event_dispatcher.cc and 1 more
FireAnimationFrameAnimation frame fired
Chrome runs a callback the page queued with requestAnimationFrame().
FireAnimationFrame covers a queued animation frame callback running. args.data.id is the registration id and frame identifies the frame.
These run at the start of the main thread's frame work, before style and layout, so time spent here directly delays rendering. Across the 35 site run their duration is typical for a trace event, which puts a callback running for whole milliseconds well outside normal.
The gap between the RequestAnimationFrame with the same id and this event is how long the callback waited. A large gap means the main thread was busy, and the animation was already going to be janky before the callback ran.
{
"name": "FireAnimationFrame",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103043074,
"dur": 35,
"tdur": 24,
"tts": 133358,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"id": 1,
"sampleTraceId": 6909218608593039
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- long
- undeclared args
args.data.sampleTraceId- references
- frame_request_callback_collection.cc, inspector_trace_events.cc
FireIdleCallbackFire idle callback
Chrome runs a callback the page queued with requestIdleCallback().
FireIdleCallback records deferred work running, and unusually for a callback event it also reports the circumstances it ran under.
args.data.id matches the registration. allottedMilliseconds is the budget the browser offered. timedOut is true when the callback ran because its deadline expired rather than because the browser went idle. frame identifies the frame.
Both extra fields matter. timedOut: true means this did not run during idle time at all. The timeout forced it, so it is now competing with rendering exactly like any other task.
WarningsHandler consumes this event specifically to flag callbacks that ran longer than their allotment, which is a real defect: idle callbacks that overrun their budget cause the jank the API exists to avoid. Comparing the duration against allottedMilliseconds is how you find them yourself.
{
"name": "FireIdleCallback",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103056100,
"dur": 51,
"tdur": 43,
"tts": 136144,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"allottedMilliseconds": 16.446,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"id": 1,
"sampleTraceId": 6909218608593143,
"timedOut": false
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- long
- undeclared args
args.data.sampleTraceId- references
- scripted_idle_task_controller.cc, inspector_trace_events.cc
FunctionCallFunction call
Chrome calls into a JavaScript function, such as an event handler or a timer callback.
FunctionCall wraps a JavaScript function being invoked from the browser. An event handler, a timer callback or a requestAnimationFrame callback all appear as one of these.
args.data.frame is the frame the call ran in and args.data.isolate is the V8 isolate. The modelled type does not include the function name or its source location. Naming comes from the CPU profile by way of ProfileCall events, which is why a trace captured without JavaScript sampling shows anonymous function calls.
ScriptsHandler and the ForcedReflow insight both consume it. The insight is the interesting one: a Layout occurring synchronously inside a FunctionCall is forced synchronous layout, and that pairing is how it is detected. This is also the event you look at first when INP is poor, because long FunctionCalls inside an input task are the direct cause.
{
"name": "FunctionCall",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103042343,
"dur": 34,
"tdur": 28,
"tts": 132806,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"columnNumber": 26,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"functionName": "x.onreadystatechange",
"isolate": 13986237028531430000,
"lineNumber": 70,
"sampleTraceId": 6909218608593055,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
}
}- categories
- devtools.timeline
- usage
- high
- duration
- typical
- undeclared args
args.data.columnNumber,args.data.functionName,args.data.lineNumber,args.data.sampleTraceId,args.data.scriptId,args.data.url- references
- inspector_trace_events.cc, v8-inspector-impl.cc, thread_debugger_common_impl.cc, heap.cc, source_location.cc, v8_script_runner.cc
GCEventGC eventno longer emitted
Chrome collected garbage, without recording which kind of collection it was.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
GCEvent marked a garbage collection without recording which kind it was, and Chrome last wrote it in Chrome 41. Blink wrote it from V8GCController on disabled-by-default-devtools.timeline. From Chrome 42 the same spot wrote MinorGC and MajorGC instead, and today V8's heap.cc writes both. DevTools keeps the old name only so that old traces still load.
The event is untyped in trace_engine.
In any current trace, read MajorGC and MinorGC, which say whether the old or the young generation was collected. Garbage collection is not inherently a problem. It is the price of allocation. GC appearing inside an input-handling task is a problem, because it lengthens the very task you need to be short. Frequent GC is an allocation-rate symptom rather than a GC problem, so fix the allocation.
Chrome last wrote this in Chrome 41. From Chrome 42 the same collections appear as MinorGC and MajorGC, which the collection recorded.
MajorGCMajor GC
V8 collects the old generation of the JavaScript heap, a full garbage collection.
MajorGC times a full garbage collection, meaning V8 collecting the old generation. The phase is X, and its duration is main-thread time the page did not get.
v8/src/heap/heap.cc emits it with three arguments that trace_engine does not model. usedHeapSizeBefore and usedHeapSizeAfter are the heap sizes either side of the collection, and the difference between them is how much was actually reclaimed, which is the only honest way to tell a productive collection from a wasted one. type is V8's garbage collection reason, and the same reason set feeds both MajorGC and MinorGC, so do not read a value as exclusive to either. The common ones are allocation failure, allocation limit, task, memory reducer, and finalize incremental marking via stack guard or via task when a concurrent marking cycle is being wrapped up. Around thirty exist in total, including external memory pressure, low memory notification and CppHeap allocation failure.
These are rare next to scavenges and far more expensive. 18 of the 35 sites in the corpus produced any, at rare volume and in the slowest tenth of trace events by duration. A major GC that reclaims little is the signature of a real leak. V8 keeps paying full collection cost and gets almost nothing back.
{
"name": "MajorGC",
"cat": "devtools.timeline,v8",
"ph": "X",
"ts": 1128651815,
"dur": 4266,
"tdur": 3980,
"tts": 941986,
"pid": 8798,
"tid": 8798,
"args": {
"type": "finalize incremental marking via stack guard",
"usedHeapSizeAfter": 21610676,
"usedHeapSizeBefore": 25730556
}
}- categories
- devtools.timeline, v8
- usage
- low
- duration
- very long
- undeclared args
args.type,args.usedHeapSizeAfter,args.usedHeapSizeBefore- references
- heap.cc, globals.h
MarkDOMContentDOMContentLoaded event
Chrome marks the moment the DOMContentLoaded event fires, once your HTML has been parsed.
MarkDOMContent marks the DOMContentLoaded event, which fires when HTML parsing completes and deferred scripts have run.
The phase is instant. args.data.frame identifies the frame that fired it and args.data.page identifies the page. args.data.isMainFrame and args.data.isOutermostMainFrame place that frame in the frame tree.
Check isOutermostMainFrame before you use the timestamp. Iframes fire their own, and attributing a subframe's DCL to the page is a common measurement error. The mark is bounded by parser-blocking resources, so a late DCL is a markup and delivery problem rather than a rendering one.
{
"name": "MarkDOMContent",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102947852,
"tts": 43960,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"isMainFrame": true,
"isOutermostMainFrame": true,
"page": "14A831DCB73EB1205B67CDB89FC6EBD7"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- references
- inspector_trace_events.cc, document.cc, local_dom_window.cc
MarkLoadOnload event
Chrome marks the moment the load event fires, once every subresource has finished loading.
MarkLoad marks the load event, which fires once all subresources including images and stylesheets have finished.
The same four frame identification fields appear as on MarkDOMContent. PageLoadMetricsHandler consumes the event.
The timestamp correlates poorly with user experience, since a single slow image below the fold delays it without affecting anything the user sees. It stays useful as an upper bound and as a comparison against DCL, where a large gap between the two is a subresource weight story. As with DCL, filter on isOutermostMainFrame.
{
"name": "MarkLoad",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102948019,
"tts": 44114,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"isMainFrame": true,
"isOutermostMainFrame": true,
"page": "14A831DCB73EB1205B67CDB89FC6EBD7"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- references
- inspector_trace_events.cc, document.cc, local_dom_window.cc
MinorGCMinor GC
V8 runs a small garbage collection, clearing out short-lived objects.
MinorGC times a scavenge, V8 collecting the young generation where short-lived objects live. The phase is X, and next to a full collection these are short, although against other trace events they land in the slowest tenth by duration.
usedHeapSizeBefore and usedHeapSizeAfter are the heap sizes either side of the collection, and type is the reason V8 gives for running it. Those are the same three arguments V8 emits on MajorGC, trace_engine models none of them, and the reason set is shared between the two events rather than partitioned. allocation failure is the value you will see most, and despite the name it is routine: the young generation filled up, which is exactly what triggers a scavenge.
Frequent minor GCs are not by themselves a problem. They become one when the allocation rate is high enough that scavenges start landing inside frames, which is what turns object churn in a render loop into dropped frames.
{
"name": "MinorGC",
"cat": "devtools.timeline,v8",
"ph": "X",
"ts": 1102954420,
"dur": 991,
"tdur": 764,
"tts": 48681,
"pid": 8257,
"tid": 8257,
"args": {
"type": "allocation failure",
"usedHeapSizeAfter": 1566416,
"usedHeapSizeBefore": 2167292
}
}- categories
- devtools.timeline, v8
- usage
- moderate
- duration
- very long
- undeclared args
args.type,args.usedHeapSizeAfter,args.usedHeapSizeBefore- references
- heap.cc, globals.h
ProfileCallJS frame
A single JavaScript function on the stack, reconstructed from V8's profiler samples.
ProfileCall represents a JavaScript stack frame reconstructed from V8's CPU profile samples. It is not a real Chrome trace event. DevTools synthesises it by integrating sampled profile data into the event timeline, which is work that SamplesIntegrator does.
That synthesis is why these give you function names and source locations when FunctionCall does not, and why they are sampled rather than exact. A ProfileCall duration is an estimate derived from sample counts, not a measured interval.
Use them to answer which function was hot, and use real events for timing. Do not sum ProfileCall durations and expect them to reconcile exactly with measured event durations.
DevTools builds this while parsing a trace. It is never written to a trace file, so there is no sample to show.
- references
- Trace.ts, SamplesIntegrator.ts, SamplesHandler.ts
RequestAnimationFrameRequest animation frame
Your code called requestAnimationFrame() and Chrome queued the callback.
RequestAnimationFrame records a call to requestAnimationFrame(), the moment a callback is queued. It marks the registration only, never the callback running.
It is an instant event. args.data.id is the registration id, frame identifies the frame, and stackTrace is optional in the modelled type.
Reading the stackTrace tells you which code scheduled the callback, and that is often more useful than the callback itself, especially inside third-party bundles. In the 35 site run every registration recorded one, so the attribution is reliably there. The queued callback runs later under FireAnimationFrame, and matching id across the two gives you scheduling latency.
{
"name": "RequestAnimationFrame",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1103041064,
"tts": 131793,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"id": 1,
"sampleTraceId": 6909218608593005,
"stackTrace": [
{
"columnNumber": 3,
"functionName": "",
"lineNumber": 87,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
]
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- source_location.cc, v8-debugger.cc, frame_request_callback_collection.cc, inspector_trace_events.cc, script_loader.cc, heap.cc
RequestIdleCallbackRequest idle callback
Your code called requestIdleCallback() to defer a callback until the browser goes idle.
RequestIdleCallback marks a call to requestIdleCallback(), which defers a callback until the browser goes idle.
args.data.id is the registration id. timeout is the deadline after which the callback runs anyway. frame identifies the frame, and stackTrace is optional.
timeout is the important field. It is the promise that the callback will run even if the browser never becomes idle, so a short timeout defeats the purpose of the API and turns deferred work back into work that competes with rendering. Read it before you accept that a page has moved work out of the way.
InitiatorsHandler uses the event to link the registration to the eventual FireIdleCallback.
{
"name": "RequestIdleCallback",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1103041104,
"tts": 131827,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"id": 1,
"sampleTraceId": 6909218608593006,
"stackTrace": [
{
"columnNumber": 35,
"functionName": "",
"lineNumber": 88,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"timeout": 0
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- scripted_idle_task_controller.cc, source_location.cc, v8-debugger.cc, inspector_trace_events.cc, script_loader.cc, heap.cc
RunMicrotasksRun microtasks
Chrome drains the microtask queue, running promise continuations and other microtasks.
RunMicrotasks covers a microtask checkpoint: promise continuations, queueMicrotask callbacks and mutation observer callbacks draining after a task completes.
trace_engine models no arguments. Real traces record args.microtask_count, which counts the microtasks drained in this checkpoint.
That count is the signal, because the queue drains completely before the browser can render and can be extended while it drains. A long RunMicrotasks therefore delays the next frame just as surely as a long task does, and it is easy to miss because the work is not attributed to whatever scheduled it. Promise-heavy code that chains many continuations, or a recursive queueMicrotask, shows up here.
{
"name": "RunMicrotasks",
"cat": "v8.execute",
"ph": "X",
"ts": 1102937773,
"dur": 1760,
"tdur": 846,
"tts": 38300,
"pid": 8257,
"tid": 8257,
"args": {
"microtask_count": 7
}
}- categories
- v8.execute
- usage
- moderate
- duration
- typical
- undeclared args
args.microtask_count- references
- microtask-queue.cc
RunPostTaskCallbackFire postTask
The browser's scheduler runs a callback that was handed to it with postTask().
RunPostTaskCallback covers the scheduler running a task it has picked up. It repeats taskId, priority and delay from the scheduling event, and frame identifies the frame.
The interval between the matching SchedulePostTaskCallback and this event, compared against delay, shows whether the scheduler honoured the priority or the main thread was simply too busy. Background-priority tasks running promptly during a busy load is a sign the page is not as busy as it looks. User-blocking tasks running late is a sign it is worse.
{
"name": "RunPostTaskCallback",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103046262,
"dur": 45,
"tdur": 36,
"tts": 135621,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"delay": 20,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"priority": "background",
"taskId": 0
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- long
- references
- dom_task.cc, inspector_trace_events.cc, dom_scheduler.cc, dom_scheduler.h
SchedulePostTaskCallbackSchedule postTask
Your code called postTask() to hand a callback to the browser's scheduler.
SchedulePostTaskCallback marks a call to postTask(), which hands a callback to the browser's scheduler.
args.data.taskId is the id the scheduler will run the task under. priority is user-blocking, user-visible or background. delay is the requested delay before the task becomes eligible. frame and stackTrace are both optional.
priority is what distinguishes this API from setTimeout. Code using postTask with explicit priorities is cooperating with the scheduler. Code using it at default priority everywhere is not getting much beyond setTimeout.
Seeing the event at all already tells you something. Three of the 35 sites in the corpus scheduled anything this way, so a page that uses it is unusual, and the priorities it picked will tell you how deliberate its scheduling is. taskId pairs the registration with RunPostTaskCallback.
{
"name": "SchedulePostTaskCallback",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102951455,
"tts": 46242,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"delay": 20,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"priority": "background",
"sampleTraceId": 6909218608594506,
"stackTrace": [
{
"columnNumber": 17,
"functionName": "",
"lineNumber": 52,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"taskId": 0
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- dom_task.cc, inspector_trace_events.cc, source_location.cc, v8-debugger.cc, dom_scheduler.cc, dom_scheduler.h and 2 more
TimerFireTimer fired
Chrome runs a timer callback now that its delay has elapsed.
TimerFire covers a timer callback running once its delay has elapsed. args.data.timerId and frame are the only payload.
Match timerId back to TimerInstall to recover both the requested delay and the installing stack. The difference between the requested timeout and the actual gap is timer lag. If it is large, the main thread was blocked, and any code assuming timer precision is already misbehaving. setTimeout(fn, 0) used as a yield shows up here too, and whether it actually yielded is visible in the same gap.
{
"name": "TimerFire",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103043776,
"dur": 150,
"tdur": 136,
"tts": 133891,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"timerId": 1
}
}
}- categories
- devtools.timeline
- usage
- high
- duration
- typical
- references
- dom_timer.cc
TimerInstallInstall timer
Your code called setTimeout or setInterval, and Chrome registered the timer.
TimerInstall records a setTimeout or setInterval registration. Both APIs register through this one event, and args.data.singleShot is what tells them apart.
timerId is the id used when the timer fires or is cleared. timeout is the requested delay. frame identifies the frame, and stackTrace is optional in the modelled type but present on almost every real event.
The stackTrace is the attribution path back to whatever installed the timer, which is usually the question you have. InitiatorsHandler pairs the event with TimerFire through timerId. A singleShot: false timer with a small timeout is a recurring main-thread tax, and these often survive long after the code that needed them.
{
"name": "TimerInstall",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102951549,
"tts": 46330,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"sampleTraceId": 6909218608594508,
"singleShot": true,
"stackTrace": [
{
"columnNumber": 7,
"functionName": "",
"lineNumber": 55,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"timeout": 60,
"timerId": 1
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- dom_timer.cc, source_location.cc, v8-debugger.cc, script_loader.cc, heap.cc
TimerRemoveRemove timer
Your code called clearTimeout or clearInterval to cancel a timer.
TimerRemove marks a call to clearTimeout or clearInterval. It is untyped in trace_engine, so the timerId being cleared is not modelled, although the raw trace does record it.
Its value is reconciliation. It accounts for installed timers that never fire, which is what you need when auditing for leaked intervals: a repeating timer with no matching TimerRemove is exactly that leak.
{
"name": "TimerRemove",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1108608925,
"tts": 266324,
"pid": 8407,
"tid": 8407,
"s": "t",
"args": {
"data": {
"frame": "2EA8810E92C12C680C55B2941251AA98",
"sampleTraceId": 5267982676862134,
"stackTrace": [
{
"columnNumber": 13686,
"functionName": "_clearTimeouts",
"lineNumber": 1,
"scriptId": "41",
"url": "https://static.files.bbci.co.uk/bbcdotcom/web/20260907-085256-bea8b058c0-web-3.21.0/_next/static/chunks/0a7h_kuj3flvo.js"
},
{
"columnNumber": 13015,
"functionName": "retry",
"lineNumber": 1,
"scriptId": "41",
"url": "https://static.files.bbci.co.uk/bbcdotcom/web/20260907-085256-bea8b058c0-web-3.21.0/_next/static/chunks/0a7h_kuj3flvo.js"
},
{
"columnNumber": 12959,
"functionName": "d",
"lineNumber": 1,
"scriptId": "41",
"url": "https://static.files.bbci.co.uk/bbcdotcom/web/20260907-085256-bea8b058c0-web-3.21.0/_next/static/chunks/0a7h_kuj3flvo.js"
},
"... [6 more items]"
],
"timerId": 0
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.frame,args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url,args.data.timerId- references
- source_location.cc, v8-debugger.cc, dom_timer.cc, script_loader.cc, heap.cc
UserTimingUser timingnever written by Chrome
Your own performance.mark() or performance.measure() entry, under the name DevTools files it by.
No Chrome build writes this name into a trace file. It exists in the model, not on the wire.
UserTiming is the DevTools name for a performance.mark() or performance.measure() entry, but Chrome never writes an event called that. TraceFilter maps every event on the blink.user_timing category to the type UserTiming, exactly as it maps blink.console events to ConsoleTime.
In the trace itself, core/timing/performance_user_timing.cc writes each mark and measure as an event named after the mark or measure, on blink.user_timing. A measure also writes UserTiming::Measure on devtools.timeline.
The names are author supplied rather than chosen by the browser. This is the mechanism for getting application level semantics into a trace. Labels such as "route change started" and "hydration complete" express meaning the browser could not possibly infer. When you are analysing a framework heavy app, these are often the only way to map browser events onto application phases. Most major frameworks emit them, so check before assuming you need to add your own.
Chrome never writes this name, which DevTools gives to any event on the blink.user_timing category. A fixture page calling performance.mark and performance.measure produced one event named after each mark and measure, plus UserTiming::Measure.
- references
- performance_user_timing.cc, TraceFilter.ts
V8.BytecodeBudgetInterruptnot in DevTools' model
V8 stops a running function to decide whether to optimise it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
V8.BytecodeBudgetInterrupt fires when V8 stops to decide whether to optimise a function. Interpreted and Sparkplug functions burn an interrupt budget as they run, and when it hits zero they call into the runtime, which emits this from BytecodeBudgetInterrupt in v8/src/runtime/runtime-internal.cc and then calls isolate->tiering_manager()->OnInterruptTick(function, code_kind).
code_kind says which tier tripped: INTERPRETED_FUNCTION for Ignition, BASELINE for Sparkplug, MAGLEV for Maglev. A sibling event, V8.BytecodeBudgetInterruptWithStackCheck, folds a stack overflow check into the same trip.
This is the visible marker for "this function is now hot enough to tier up". Each tick is cheap, faster than most trace events, but a V8.MaglevTask or V8.OptimizeConcurrentPrepare following one is not. Use them as anchors: a tick inside an interaction is where V8 started spending your INP budget on compilation instead of on the work you asked for. No profiler in DevTools marks that moment.
{
"name": "V8.BytecodeBudgetInterrupt",
"cat": "v8.execute",
"ph": "X",
"ts": 1102953017,
"dur": 5,
"tdur": 4,
"tts": 47312,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- v8.execute
- usage
- very high
- duration
- short
- references
- runtime-internal.cc
V8.CollectSourcePositionsnot in DevTools recordingsnot in DevTools' model
V8 reparses a function it already compiled, to rebuild the source position table it discarded.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
V8.CollectSourcePositions measures V8 reparsing a function it already compiled, purely to rebuild the source position table it threw away. Compiler::CollectSourcePositions in v8/src/codegen/compiler.cc emits it, sets flags.set_is_reparse(true) and set_collect_source_positions(true), and runs a fresh NewSourcePositionCollectionJob.
Nothing is recorded but the duration. V8 discards source positions by default to save memory and pays them back lazily. The triggers are all things your code does: capturing Error.stack, throwing, attaching a debugger, opening DevTools, running a profiler. One call costs several times what generating that function's bytecode cost in the first place.
Reading these events puts a cost on your error handling, which nothing else will. A moderate volume of them on all 35 real sites is a lot of reparsing for stack traces nobody reads. Clustered ones point at a library constructing Error objects on a hot path: error based control flow, an eager logging wrapper, or a monitoring SDK sampling stacks.
{
"name": "V8.CollectSourcePositions",
"cat": "disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102929806,
"dur": 51,
"tdur": 46,
"tts": 33166,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-v8.compile
- usage
- high
- duration
- typical
- references
- compiler.cc
v8.compileCompile script
V8 compiles one script into code it can run.
v8.compile records one script being compiled by V8.
args.data tells the full code-cache story:
consumedCacheSize,cacheRejectedandcacheKindonly appear when V8consumedCacheSizeis the size of the cache V8 consumed, in bytes.cacheRejectedis true when a cache existed but V8 refused it, typicallycacheKindis full when V8 consumed a full code cache, which Chromestreamedsays whether V8 compiled the script as it downloaded, andeageris true when functions were compiled eagerly rather than lazily.
consumed a code cache for this script. When all three are missing, no cache was used and the script was compiled from source.
because the script's content or V8 itself changed.
produces for example for scripts a service worker stores in Cache Storage, and normal otherwise.
notStreamedReason says why not when it did not.
url, lineNumber and columnNumber locate the script.
No other V8 event in the trace gives you this much diagnostic detail, so start here and reach for notStreamedReason first. Streaming compilation overlaps compile with download, and losing it means compile time lands entirely after the bytes have arrived. Inline scripts and very small scripts are the common causes. Across the 35 site run, a little over half of all v8.compile events recorded a notStreamedReason at all.
cacheRejected: true on a repeat view is a real finding. You are paying full compile cost on every visit despite the cache existing. Aggressive cache-busting in build output is the usual cause.
{
"name": "v8.compile",
"cat": "v8,devtools.timeline",
"ph": "X",
"ts": 1186295713,
"dur": 16,
"tdur": 15,
"tts": 235936,
"pid": 9580,
"tid": 9580,
"args": {
"data": {
"cacheKind": "normal",
"cacheRejected": false,
"columnNumber": 1,
"consumedCacheSize": 177776,
"lineNumber": 1,
"notStreamedReason": "Backgound streaming will be used",
"streamed": false,
"url": "https://stackoverflow.com/Content/Js/webpack-chunks/svelte.en.js?v=f2f4cf305940"
},
"fileName": "https://stackoverflow.com/Content/Js/webpack-chunks/svelte.en.js?v=f2f4cf305940"
}
}- categories
- v8, devtools.timeline
- usage
- moderate
- duration
- typical
- undeclared args
args.data.scriptId- references
- inspector_trace_events.cc, v8_script_runner.cc, classic_script.cc, script_loader.cc, v8_code_cache.cc, cache.cc and 3 more
V8.CompileCodeCompile codenot in DevTools recordings
V8 carries out one compilation job, below the level of a whole script.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
V8.CompileCode measures a single V8 compilation job. It is untyped in trace_engine and no args are modelled.
It sits at a lower level than v8.compile, so treat it as a cost bucket rather than a source of attribution. It is useful when summing total compile cost, since not all compilation surfaces as v8.compile. In the 35 site run it is one of the most numerous events in the set, on all 35 sites, at a typical per event duration, so whatever it costs a page, it costs in aggregate rather than in any one event. If you want to change something, work from v8.compile instead.
{
"name": "V8.CompileCode",
"cat": "disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102929130,
"dur": 47,
"tdur": 44,
"tts": 32695,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-v8.compile
- usage
- very high
- duration
- typical
- references
- compiler.cc
V8.CompileIgnitionnot in DevTools recordingsnot in DevTools' model
V8 generates the bytecode for one function, for its interpreter to run.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
V8.CompileIgnition measures bytecode generation for one function, emitted by InterpreterCompilationJob::ExecuteJobImpl in v8/src/interpreter/interpreter.cc. Ignition is V8's interpreter, and this job produces the BytecodeArray it will run.
V8.CompileIgnition sits one level below the v8.compile event. DevTools shows v8.compile as one opaque block. V8.CompileIgnition is the per function breakdown inside it, with V8.CompileIgnitionFinalization as the matching FinalizeJobImpl that installs the result. The body calls local_isolate_->ParkIfOnBackgroundAndExecute, so these can run off the main thread during streaming compilation.
What that buys you is separating parse cost from codegen cost, since V8.ParseFunction and V8.CompileIgnition are siblings rather than one lump. These run faster than most trace events, which says codegen is rarely the problem. If v8.compile is heavy and these are not, the cost is parsing or source position collection.
{
"name": "V8.CompileIgnition",
"cat": "disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102929163,
"dur": 3,
"tdur": 3,
"tts": 32726,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-v8.compile
- usage
- very high
- duration
- short
- references
- interpreter.cc
V8.CompileModuleCompile modulenever written by Chrome
V8 compiles one ES module.
No Chrome build writes this name into a trace file. It exists in the model, not on the wire.
V8.CompileModule is the DevTools name for compiling an ES module, but neither V8 nor Blink writes that string. Blink writes the work as v8.compileModule, with a lower case v, on v8,devtools.timeline from V8ScriptRunner::CompileModule in bindings/core/v8/v8_script_runner.cc. Older DevTools used v8.compileModule too, until the enum value was capitalised. In the real event, args.data names the module url and says whether it was streamed.
Module graphs compile per module, so a deep import graph produces many of these rather than one large compile. That fragmentation is why module-heavy pages can show substantial aggregate compile cost without any single event looking expensive. Sum them before concluding compilation is cheap.
Neither V8 nor Blink writes this name. A fixture page loading an ES module produced v8.compileModule, which is how Blink writes module compilation, and V8.CompileStreamedModule.
- references
- v8_script_runner.cc, TraceEvents.ts
V8.CompileScriptnot in DevTools recordings
V8 compiles one whole script, timed inside V8 rather than by Blink.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
V8.CompileScript measures the script-level compile step inside V8. It is untyped in trace_engine and not consumed by any handler.
It overlaps conceptually with v8.compile, which is the Blink-side event with the useful args. If both appear, prefer v8.compile for analysis and use this one only to account for time.
{
"name": "V8.CompileScript",
"cat": "disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102934802,
"dur": 97,
"tdur": 91,
"tts": 36773,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-v8.compile
- usage
- moderate
- duration
- typical
- references
- api.cc
v8.deserializeOnBackgroundDeserialize code cache
V8 unpacks cached bytecode for a script on a background thread.
v8.deserializeOnBackground records V8 deserialising cached bytecode on a background thread, which is the code cache being consumed rather than produced.
The event has no arguments.
A working code cache looks like this: bytecode restored off thread instead of source compiled on the main thread. Pair it with v8.compile's consumedCacheSize to confirm how much was actually reused.
Not seen. Consuming the code cache needs a repeat visit, and every page in the collection was loaded cold.
- categories
- v8, devtools.timeline, disabled-by-default-v8.compile
- references
- script_cache_consumer.cc
v8.evaluateModuleEvaluate module
V8 runs the body of an ES module.
v8.evaluateModule covers the body of an ES module being evaluated. It has no typed arguments.
Module evaluation is ordered by the import graph, so a slow module blocks everything that imports it. When tracing a module-based app, follow the evaluation order here rather than assuming network order. The two frequently differ.
For classic scripts the equivalent event is EvaluateScript, so a page that mixes both needs reading through both.
{
"name": "v8.evaluateModule",
"cat": "v8,devtools.timeline",
"ph": "X",
"ts": 1102947560,
"dur": 243,
"tdur": 228,
"tts": 43689,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- v8, devtools.timeline
- usage
- low
- duration
- long
- references
- v8_script_runner.cc
V8.Executeno longer emitted
Chrome used to record V8 running your JavaScript under this name.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
V8.Execute marked a span of V8 executing JavaScript, and no shipping Chrome writes it. The call sites are still in v8/src/api/api.cc, in Script::Run, Module::Evaluate, Object::CallAsFunction and others, as TRACE_EVENT_CALL_STATS_SCOPED on the v8 category. That macro compiles to nothing unless the build defines V8_RUNTIME_CALL_STATS. Release branches already left it off, and since V8 commit 90a44a1c93 in April 2026 every branch does, so trunk builds such as Canary stopped writing it too.
A trace that shows V8.Execute came from a trunk build older than April 2026 or from a custom build with runtime call stats turned on.
It is untyped in trace_engine, no handler consumes it, and it is not given a DevTools label, although isJSInvocationEvent still counts it as a JavaScript entry point. It is a broad container rather than a specific operation. For attribution, read the events nested inside it: FunctionCall, EvaluateScript and ProfileCall.
Only builds with V8 runtime call stats turned on write this event, and no shipping Chrome has them. The Chrome 141 stable build that recorded the collection could not produce it.
- references
- api.cc, trace-event.h
V8.FinalizeDeserializationProfiling overheadnot in DevTools recordings
V8 finishes the last step of unpacking compiled code from the code cache.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
V8.FinalizeDeserialization marks the last step of code cache deserialisation, which completes on the main thread.
trace_engine models no arguments for it.
It is short by design, but it is the part of cache consumption that is not free of the main thread. Count it when you are accounting for where load time main-thread cost goes.
Not seen, for the same reason as v8.deserializeOnBackground: every load was cold.
- categories
- disabled-by-default-v8.compile
- usage
- rare
- duration
- very long
- references
- code-serializer.cc
V8.MaglevTasknot in DevTools recordingsnot in DevTools' model
A background worker wakes up to run Maglev, V8's mid-tier optimising compiler.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
V8.MaglevTask records a Maglev compilation worker waking up. MaglevConcurrentDispatcher::JobTask::Run in v8/src/maglev/maglev-concurrent-dispatcher.cc emits it once per worker run, wrapping however many jobs that worker drains from the queue.
Maglev is V8's mid tier optimising compiler, sitting between Sparkplug and TurboFan, and the nested events are the ones you want. V8.MaglevBackground is one job compiling, and a perfetto::Flow::ProcessScoped(job->trace_id()) ties it to the steps around it. V8.MaglevConcurrentPrepare (from v8/src/codegen/compiler.cc) and V8.MaglevConcurrentFinalize sit at either end of that flow and both run on the main thread. Background compilation is the expensive part of the three.
Following the flow pays, because it splits optimisation into the part you are charged for and the part you are not: prepare and finalize are your INP cost, background is not. V8.MaglevPrepare in place of V8.MaglevConcurrentPrepare means the concurrent dispatcher was unavailable and V8 compiled on the main thread.
{
"name": "V8.MaglevTask",
"cat": "disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102947863,
"dur": 1942,
"tdur": 708,
"tts": 208,
"pid": 8257,
"tid": 8305,
"args": {
}
}- categories
- disabled-by-default-v8.compile
- usage
- high
- duration
- typical
- references
- maglev-concurrent-dispatcher.cc, compiler.cc
V8.OptimizeCodenot in DevTools recordings
V8 optimises a hot function, compiling it to faster machine code.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
V8.OptimizeCode measures V8 optimising a hot function, compiling it to optimised machine code after the interpreter has seen it run enough times.
The event is untyped, so you cannot tell which function was optimised from the event alone. Individual events are short, landing in the typical band for trace event duration, at moderate volume on 20 of the 35 sites.
Its presence is normal and generally good, because it means code is hot enough to deserve optimising. Look instead for repeated optimisation of the same code. That suggests deoptimisation cycles: a function being optimised, hitting a type it was not optimised for, bailing out, and being optimised again. That pattern costs more than never optimising at all.
{
"name": "V8.OptimizeCode",
"cat": "disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102956998,
"dur": 452,
"tdur": 228,
"tts": 50962,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-v8.compile
- usage
- moderate
- duration
- typical
- references
- compiler.cc
V8.ParseFunctionnot in DevTools recordingsnot in DevTools' model
V8 parses one function body in full and builds the syntax tree for it.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
V8.ParseFunction measures the real parse of one function body. Parser::ParseFunction in v8/src/parsing/parser.cc emits it, and the source notes it only ever runs on the main thread.
No arguments are attached. It fires when a previously preparsed function is finally called, the second half of the lazy compilation deal, so this is not load time work, it is first call work. It builds the AST that V8.CompileIgnition turns into bytecode, which is why the counts track each other, at roughly three bytecode compilations across the corpus for every two parses. Each parse is quick relative to other trace events.
What matters is where in the timeline they cluster. A dense band inside an interaction task is first call parsing on the critical path, and the fix is to warm the code earlier or ship less of it, not to make functions smaller. A dense band during load means your bundle's top level calls almost everything it defines, defeating lazy parsing entirely. DevTools rolls all of it into compile and script evaluation blocks that hide which of the two you have.
{
"name": "V8.ParseFunction",
"cat": "disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102929814,
"dur": 29,
"tdur": 27,
"tts": 33173,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-v8.compile
- usage
- very high
- duration
- short
- references
- parser.cc
v8.parseOnBackgroundParsingParse and compile
V8 parses a script on a background thread rather than on the main thread.
v8.parseOnBackgroundParsing records script parsing running on a background thread instead of on the main thread.
No arguments are attached. Duration is the measure. These run slower than most trace events, and the spread is wide, with the tail running far longer than the middle.
Presence is the good case. Parsing overlapped with download and stayed off the main thread, which means streaming compilation is working. Absence for large scripts is the finding, and v8.compile's notStreamedReason tells you why.
Inline scripts are not streamed in default Chrome. Blink records them under an explicit reason, kInlineScript, because there is no separate network response to parse off the main thread. Chromium does have an experimental InlineScriptStreamer behind the PrecompileInlineScripts flag, but it ships disabled. That is one concrete reason to avoid large inline bundles.
Streaming is not automatic for external scripts either. Blink suppresses it for a list of reasons including kScriptTooSmall, kHasCodeCache, kNotHTTP, kModuleScript and kSecondScriptResourceUse.
{
"name": "v8.parseOnBackgroundParsing",
"cat": "v8,devtools.timeline,disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102942276,
"dur": 360,
"tdur": 353,
"tts": 946,
"pid": 8257,
"tid": 8272,
"args": {
}
}- categories
- v8, devtools.timeline, disabled-by-default-v8.compile
- usage
- moderate
- duration
- long
- references
- script_streamer.cc
v8.parseOnBackgroundWaitingWaiting for network
A background parse sits blocked, waiting for more script bytes to arrive from the network.
v8.parseOnBackgroundWaiting measures a background parse sitting blocked, waiting on more bytes to arrive from the network.
There are no typed arguments. The duration is the wait itself, and these land in the slowest tenth of trace events by duration.
The parser outran the download wherever one of these appears. None of that time is main-thread cost. What it tells you is that script delivery is the constraint for that resource, not script processing.
{
"name": "v8.parseOnBackgroundWaiting",
"cat": "v8,devtools.timeline,disabled-by-default-v8.compile",
"ph": "X",
"ts": 1108382883,
"dur": 102,
"tdur": 13,
"tts": 560,
"pid": 8407,
"tid": 8466,
"args": {
}
}- categories
- v8, devtools.timeline, disabled-by-default-v8.compile
- usage
- moderate
- duration
- very long
- references
- script_streamer.cc
V8.PreParsenot in DevTools recordingsnot in DevTools' model
V8 skims a function body for its shape rather than parsing it in full.
A DevTools Performance recording does not contain this event. Record disabled-by-default-v8.compile with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
V8.PreParse measures V8 skimming a function body without building an AST. It is emitted from Parser::SkipFunction in v8/src/parsing/parser.cc, immediately before that function calls reusable_preparser()->PreParseFunction(...).
There are no arguments, only a duration. Lazy compilation is the default, so on first pass V8 records only what it needs to compile the function later: scope shape, variable references, whether it is a generator. The source comment is explicit, "with no cached data, we partially parse the function, without building an AST". Individually it is among the faster events in the corpus, and it is also one of the most numerous, seen on every site.
Read total preparse time against total V8.ParseFunction time, because every function later parsed for real was preparsed first and paid twice. That comparison exists nowhere in DevTools. A bundle where the two are comparable ships a lot of code that runs immediately, and wants splitting rather than micro optimisation. A preparse error V8 cannot classify sets allow_lazy_ = false and forces a full eager reparse.
{
"name": "V8.PreParse",
"cat": "disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102929824,
"dur": 15,
"tdur": 14,
"tts": 33182,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- disabled-by-default-v8.compile
- usage
- very high
- duration
- short
- references
- parser.cc
v8.produceCacheCache script code
V8 writes the compiled bytecode for a classic script into the code cache.
v8.produceCache measures V8 writing compiled bytecode to the code cache for a classic script.
Arguments appear only on the events that also sit in the devtools.timeline category, 269 of the 1860 in the corpus. On those, args.data.producedCacheSize is the size of the cache entry written, args.data.url and args.data.scriptId identify the script, and args.data.lineNumber and args.data.columnNumber locate it. The other 1591 arrive with nothing attached.
This is the cost side of code caching. You pay a little now to save compile time on subsequent visits, so seeing it on a first view is correct and desirable. Seeing it on every view means the cache is never being consumed, which points at changing URLs or headers that prevent reuse. Cross-check with v8.compile's consumedCacheSize and cacheRejected.
{
"name": "v8.produceCache",
"cat": "v8,devtools.timeline",
"ph": "X",
"ts": 1130706242,
"dur": 1417,
"tdur": 1416,
"tts": 2962526,
"pid": 8798,
"tid": 8798,
"args": {
"data": {
"columnNumber": 1,
"lineNumber": 1,
"producedCacheSize": 208096,
"url": "https://pagead2.googlesyndication.com/tag/js/gpt.js"
},
"fileName": "https://pagead2.googlesyndication.com/tag/js/gpt.js"
}
}- categories
- v8, devtools.timeline
- usage
- moderate
- duration
- typical
- undeclared args
args.data.columnNumber,args.data.lineNumber,args.data.producedCacheSize,args.data.scriptId,args.data.url,args.fileName- references
- inspector_trace_events.cc, classic_script.cc, script_loader.cc, v8_code_cache.cc, v8_script_runner.cc
v8.produceModuleCacheCache module code
V8 writes the compiled bytecode for an ES module into the code cache.
v8.produceModuleCache records V8 writing compiled bytecode to the code cache for an ES module.
The same argument set applies as for classic scripts, and it is almost never populated. A tiny minority of the events in the corpus arrived with producedCacheSize and a URL attached.
Module caching is per module, so a large import graph produces many of these, and all 35 sites in the corpus emitted some. Read them exactly as you read v8.produceCache, which covers the same operation for classic scripts. Repeated production without consumption means the cache is not working.
{
"name": "v8.produceModuleCache",
"cat": "v8",
"ph": "X",
"ts": 1102948845,
"dur": 2,
"tdur": 1,
"tts": 44610,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- v8, devtools.timeline
- usage
- moderate
- duration
- very short
- undeclared args
args.data.columnNumber,args.data.lineNumber,args.data.producedCacheSize,args.data.scriptId,args.data.url,args.fileName- references
- v8_code_cache.cc
v8.wasm.cachedModuleCached Wasm module
A WebAssembly module goes into or comes out of the Wasm code cache.
v8.wasm.cachedModule marks a WebAssembly module moving through the Wasm code cache. One event name covers both directions, the module being written into the cache and the module being served from it.
The event is untyped, so the direction is not recorded. You infer it from whether this is a first load or a repeat one.
Wasm caching matters more than JavaScript code caching, because the compile cost being avoided is larger. Its presence on a first load is the cache being populated.
Not seen. Writing to the WebAssembly code cache needs a repeat visit that reuses it, which a single cold load cannot produce.
- categories
- disabled-by-default-devtools.timeline
- references
- v8_wasm_response_extensions.cc
v8.wasm.compiledModuleCompiled Wasm module
V8 has finished compiling a WebAssembly module from its bytes.
v8.wasm.compiledModule marks a WebAssembly module that V8 has finished compiling from its bytes.
Nothing is attached to the event, so module size and identity stay out of the trace.
Wasm compilation cost scales with module size and is substantial for large modules. Seeing this event on every load means no caching is in effect. Compare it against v8.wasm.moduleCacheHit to see which of the two paths a repeat visit took.
Not seen on a fixture page that compiled and instantiated a WebAssembly module. That produced wasm.AsyncCompile, wasm.BaselineFinished and v8.wasm.compileConsume.
- categories
- disabled-by-default-devtools.timeline
- references
- v8_wasm_response_extensions.cc
v8.wasm.moduleCacheHitWasm module cache hit
A cached WebAssembly module is reused, so V8 skips compiling it.
v8.wasm.moduleCacheHit fires when a cached WebAssembly module is reused and compilation is skipped entirely.
The event has no arguments. Its presence is the whole message.
This is the outcome you want on repeat visits. Absence, combined with v8.wasm.compiledModule appearing every time, means you are recompiling a large module on every load.
Not seen. A cache hit needs a repeat visit, which a single cold load cannot produce. The collection did record wasm.GetNativeModuleFromCache.
- categories
- disabled-by-default-devtools.timeline
- references
- v8_wasm_response_extensions.cc
v8.wasm.moduleCacheInvalidWasm module cache invalid
V8 rejects a cached WebAssembly module as invalid and compiles it again from scratch.
v8.wasm.moduleCacheInvalid fires when V8 rejects a cached WebAssembly module as invalid and compiles the module from scratch instead.
The event is untyped, as every event in the Wasm cache group is.
A cache entry existed and could not be used, usually because the module changed or the V8 version did. Both of those are normal invalidation. Recurring on deployments where the module has not changed, it points at a caching configuration problem instead.
Not seen. It requires a cached module that Chrome then rejects.
- categories
- disabled-by-default-devtools.timeline
- references
- v8_wasm_response_extensions.cc
v8.wasm.streamFromResponseCallbackStreaming Wasm response
V8 starts compiling WebAssembly straight from a fetch response, as instantiateStreaming() does.
v8.wasm.streamFromResponseCallback fires when streaming WebAssembly compilation starts from a fetch response, which is what WebAssembly.instantiateStreaming() and compileStreaming() do.
No arguments are attached, so the event marks which path was taken rather than measuring it.
This is the preferred path, because compilation overlaps with download. Its presence means the module is being compiled as it arrives rather than after. If you are loading Wasm and do not see this event, the code is probably buffering the response into an ArrayBuffer first, which serialises download and compile.
{
"name": "v8.wasm.streamFromResponseCallback",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1102949854,
"tts": 45133,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- rare
- references
- v8_wasm_response_extensions.cc
V8Console::runTaskRun console task
V8 runs a task through its console and inspector machinery.
V8Console::runTask records a task run through V8's console and inspector task mechanism. args.data.sampleTraceId is its only argument.
AsyncJSCallsHandler consumes it to reconstruct async causality, linking a callback back to the async operation that scheduled it across an await or a promise boundary. That reconstruction is what makes async stack traces work in the Performance panel.
It is rarely something you optimise and frequently something you rely on to understand what caused what.
{
"name": "V8Console::runTask",
"cat": "disabled-by-default-v8.inspector",
"ph": "X",
"ts": 1236450911,
"dur": 4604,
"tdur": 4085,
"tts": 228477,
"pid": 10353,
"tid": 10353,
"args": {
"data": {
"sampleTraceId": 87
}
}
}- categories
- disabled-by-default-v8.inspector
- usage
- moderate
- duration
- typical
- references
- v8-console.cc, trace-id.h
WebSocketCreateCreate WebSocket
The page constructs a WebSocket.
WebSocketCreate marks the construction of a WebSocket.
args.data.identifieris the socket id used by every later event for thisurlis the socket endpoint.websocketProtocolis the subprotocol negotiated for it.frameidentifies the frame, and is optional.workerIdis set when the socket was opened inside a worker.stackTraceis optional, and points at the code that opened it.
socket.
identifier is the join key for the whole socket lifecycle. InitiatorsHandler and NetworkRequestsHandler both consume the event, so sockets appear in network analysis rather than being invisible to it. workerId being present tells you the socket belongs to a worker, which matters because worker sockets do not contend for the main thread the way page sockets do.
What these tell you first is when the connection opened relative to everything else in the load. None of the 35 sites in the corpus opened a socket during the traced page load, so to see socket events at all you generally have to trace an app session rather than a cold start.
{
"name": "WebSocketCreate",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102952120,
"tts": 46667,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"identifier": 5,
"sampleTraceId": 6909218608594544,
"stackTrace": [
{
"columnNumber": 16,
"functionName": "",
"lineNumber": 61,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"url": "ws://127.0.0.1:8801/ws"
}
}
}- categories
- devtools.timeline
- usage
- rare
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url,args.data.webSocketProtocol- references
- source_location.cc, v8-debugger.cc, inspector_websocket_events.cc, unique_identifier.cc, websocket_common.cc, script_loader.cc and 2 more
WebSocketDestroyDestroy WebSocket
A WebSocket closes and is torn down.
WebSocketDestroy marks a socket closing. args.data.identifier is the socket id and frame identifies the frame that owned it.
Matched with WebSocketCreate on identifier, it gives you connection lifetime. The more useful reading is the negative one: an identifier that is created and keeps sending with no WebSocketDestroy anywhere in the trace is a socket nobody closed, which on long-lived single-page app sessions is a real leak.
Not seen, including on a fixture page that opened a socket, exchanged a message and closed it. The other five WebSocket events all appeared there.
- categories
- devtools.timeline
- usage
- rare
- undeclared args
args.data.sampleTraceId- references
- websocket_channel_impl.cc
WebSocketReceiveReceive WebSocket message
One message arrives from the server over a WebSocket.
WebSocketReceive marks one message arriving from the server. identifier is the socket id, url is the endpoint, and dataLength is the size of the message in bytes. frame and workerId are optional.
The same per-socket aggregation applies inbound as on WebSocketSend, and comparing the two on one identifier tells you which direction the chatter runs. Line these events up against main-thread work as well. A socket delivering messages faster than the page can process them produces a steadily growing backlog, and that surfaces as jank with no obvious cause.
{
"name": "WebSocketReceive",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1103045469,
"tts": 135154,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"dataLength": 29,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"identifier": 5,
"sampleTraceId": 6909218608593094
}
}
}- categories
- devtools.timeline
- usage
- low
- undeclared args
args.data.sampleTraceId- references
- websocket_channel_impl.cc, inspector_websocket_events.cc, unique_identifier.cc
WebSocketSendSend WebSocket message
The page sends one message over a WebSocket.
WebSocketSend marks one message the page sent over a socket. identifier is the socket id, url is the endpoint, and dataLength is the size of the message in bytes. frame and workerId are optional.
Summed per identifier, dataLength gives you outbound volume per socket. The spacing between the events tells you the rest: high-frequency small sends are usually a batching opportunity, and each one still costs a main-thread trip.
No handler consumes this event, so nothing in DevTools aggregates it for you. Per-socket volume is something you count in the raw trace JSON yourself.
{
"name": "WebSocketSend",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1103044203,
"tts": 134272,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"dataLength": 22,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"identifier": 5,
"sampleTraceId": 6909218608593060,
"stackTrace": [
{
"columnNumber": 28,
"functionName": "ws.onopen",
"lineNumber": 62,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
]
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- source_location.cc, v8-debugger.cc, websocket_channel_impl.cc, inspector_websocket_events.cc, unique_identifier.cc, script_loader.cc and 1 more
XHRLoad`XHR` load
An XMLHttpRequest has finished loading.
XHRLoad marks an XMLHttpRequest completing. It is untyped in trace_engine, although real traces record args.data.url, the requested URL, and frame.
XHR is legacy and far from extinct: it appeared on 15 of the 35 sites in the corpus, mostly because plenty of third-party tags still use it. Synchronous XHR in particular blocks the main thread outright, so if you see one of these inside a long task with no network overlap, check for async: false.
{
"name": "XHRLoad",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103042670,
"dur": 11,
"tdur": 5,
"tts": 133098,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"sampleTraceId": 6909218608593028,
"url": "http://127.0.0.1:8801/mod.mjs"
}
}
}- categories
- devtools.timeline
- usage
- low
- duration
- typical
- undeclared args
args.data.frame,args.data.sampleTraceId,args.data.url,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- inspector_trace_events.cc, xml_http_request.cc
XHRReadyStateChange`XHR` `readyState` change
An XMLHttpRequest moves to a new readyState.
XHRReadyStateChange marks one readyState transition on an XMLHttpRequest. It is untyped, although the raw trace records args.data.readyState as the new state and args.data.url as the request.
Most of these are noise. A dense cluster of them with handler work attached is not: it means a readystatechange listener is doing work on every transition rather than only on completion. That is a small but real waste, and it is easy to fix.
{
"name": "XHRReadyStateChange",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1102952240,
"dur": 14,
"tdur": 11,
"tts": 46784,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"readyState": 1,
"sampleTraceId": 6909218608594546,
"stackTrace": [
{
"columnNumber": 5,
"functionName": "",
"lineNumber": 69,
"scriptId": "7",
"url": "http://127.0.0.1:8801/"
}
],
"url": "http://127.0.0.1:8801/mod.mjs"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- typical
- undeclared args
args.data.frame,args.data.readyState,args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].urland 1 more- references
- xml_http_request.cc, source_location.cc, v8-debugger.cc, inspector_trace_events.cc, script_loader.cc, heap.cc
Page load metrics and vitals
Navigation, layout shift and interaction timing.
CommitLoad
The renderer commits a navigation and starts building the new document.
CommitLoad records the renderer committing a navigation, meaning the point at which it starts building the new document.
url is the document being committed and frame is its frame id. isMainFrame marks the main frame. page is the page id. parent names the parent frame, name is the frame's name attribute, and nodeId identifies the node the frame is attached to.
The phase is one to watch. trace_engine types the event as instant, but every instance across the 35 site run is phase X with a duration, and those durations are typical for a trace event. Code that switches on the declared phase will not find these events where it expects them.
Its real value is the frame tree. parent plus nodeId lets you reconstruct which iframe sits inside which, and PageFramesHandler exists for precisely this.
On a timeline it marks the boundary between still showing the old page and building the new one, which is the honest start of rendering work even though the metrics are measured from navigationStart.
{
"name": "CommitLoad",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1108626385,
"dur": 12,
"tdur": 12,
"tts": 280151,
"pid": 8407,
"tid": 8407,
"args": {
"data": {
"frame": "C2F2BAF874B0B58792981F585E0AC5A3",
"isMainFrame": false,
"isOutermostMainFrame": false,
"name": "__tcfapiLocator",
"nodeId": 1617,
"page": "2EA8810E92C12C680C55B2941251AA98",
"parent": "2EA8810E92C12C680C55B2941251AA98",
"url": "about:blank"
}
}
}- categories
- devtools.timeline
- usage
- low
- duration
- typical
- undeclared args
args.data.isOutermostMainFrame- references
- inspector_trace_events.cc, tracing_handler.cc, document.cc, local_dom_window.cc, document_loader.cc
EventTimingEvent timing
Chrome times one dispatched event, from when it arrived, through its handlers, to the screen.
EventTiming records one entry of the Event Timing API: a single dispatched event, when it arrived, when its handlers ran, and when the result reached the screen.
trace_engine models only the end interface, so the fields below are from observed traces:
typeis the DOM event name andtimeStampis when the event occurred.processingStartandprocessingEndbracket the handlers running.durationis the whole event, fromtimeStampthrough to presentation.interactionIdgroups the events of one interaction under a single id.nodeIdidentifies the target element.enqueuedToMainThreadTimeis when the event was queued to the main thread,cancelable,interactionOffsetandframecomplete the shape.
and commitFinishTime is when the resulting frame committed.
These are the source of INP, and the three phases of an interaction come straight out of them:
- input delay =
processingStart - timeStamp - processing =
processingEnd - processingStart - presentation delay =
(timeStamp + duration) - processingEnd
interactionId: 0 means it is not an interaction. Hover, pointermove, pointerover and scroll all emit EventTiming with a zero id and are excluded from INP by design. A trace of a page load alone can produce dozens of these and still have no INP at all. Filter on interactionId > 0 before you compute anything, or you will report a number that does not exist.
Several events share one interactionId (pointerdown, pointerup, click for a tap; keydown, keypress, keyup for a keystroke). The interaction's duration is the longest of them, not the sum.
Note timeStamp is milliseconds from the time origin, already relative to navigation. It is not a trace timestamp and does not need converting.
{
"name": "EventTiming",
"cat": "devtools.timeline",
"ph": "b",
"ts": 1104754827,
"pid": 8257,
"tid": 8257,
"id": "0x97d6cfef",
"scope": "devtools.timeline",
"args": {
"data": {
"cancelable": true,
"commitFinishTime": 1839.039,
"duration": 1.735,
"enqueuedToMainThreadTime": 1838.179,
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"interactionId": 0,
"interactionOffset": 0,
"nodeId": 8,
"processingEnd": 1838.712,
"processingStart": 1838.699,
"timeStamp": 1837.316,
"type": "pointerover"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.cancelable,args.data.commitFinishTime,args.data.duration,args.data.enqueuedToMainThreadTime,args.data.frame,args.data.interactionId,args.data.interactionOffset,args.data.nodeIdand 4 more- references
- performance_event_timing.cc, window_performance.cc, performance_event_timing.h, performance_timeline_entry_id_generator.h
InteractiveTime
Chrome marks the moment it considers the page interactive, the point known as Time to Interactive.
InteractiveTime marks Time to Interactive. Phase R (mark).
args.args.total_blocking_time_ms is Total Blocking Time, reported alongside TTI on the same event.
Treat this one with care. TTI was removed from the Lighthouse performance score in Lighthouse 10, which shipped in Chrome 112. Lighthouse's own reason was that "the way it's defined makes it overly sensitive to outlier network requests and long tasks". Its 10% weight moved to CLS. The raw value survives in the Lighthouse JSON at weight zero, and the trace event still fires, so you will still see it. It is rare in practice: exactly one appears across the 35 sites in the corpus.
The total_blocking_time_ms field is still useful. TBT is the sum of the blocking portion of every long task between FCP and TTI, and it is the best lab proxy for INP that a load trace can give you, since a load trace usually has no interactions in it at all.
Report the TBT. Be cautious about reporting the TTI.
{
"name": "InteractiveTime",
"cat": "loading,rail",
"ph": "R",
"ts": 1164471963,
"tts": 1647230,
"pid": 9307,
"tid": 9307,
"s": "t",
"args": {
"args": {
"had_user_input_before_interactive": false,
"total_blocking_time_ms": 254.181
},
"frame": "3037F57D3EE99AA57D01190FF40E7359"
}
}- categories
- loading, rail
- usage
- rare
- undeclared args
args.args.had_user_input_before_interactive- references
- interactive_detector.cc
LayoutShiftLayout shift
Chrome records one rendered frame in which visible content moved position on screen.
LayoutShift records one frame in which visible content moved. Phase I, emitted during PrePaint, and the raw material of CLS.
trace_engine types args.data loosely, so the shape below is from observed traces and from layout_shift_tracker.cc, which writes it:
scoreis this shift's score: the share of the viewport the moving contentcumulative_scoreis a running total ofscorefor this frame, and it ishad_recent_inputis true when the shift followed a user input closelyimpacted_nodeslists up to five of the nodes that moved, as `{node_id,region_rectsis the area the moving content covered, before and after
covered, multiplied by how far it moved as a share of the viewport's larger side. weighted_score_delta is the same score weighted for the frame, and only differs from score inside an iframe.
not CLS. It never resets, it is not weighted, and every frame keeps its own. CLS takes the worst five second session window of weighted shifts across the whole page. On a page with one burst of shifts and no iframes the two happen to match, which is why reading the last cumulative_score as the page's CLS works right up until it does not.
enough to be excluded from CLS. last_input_timestamp is when that input happened, in milliseconds on the page's own clock, and 0 when there has been none.
old_rect, new_rect}. Rects are [x, y, width, height]` in viewport pixels.
together. frame_max_distance is the furthest anything moved in this shift and overall_max_distance the furthest this frame has recorded so far. is_main_frame is true when the shift happened in the top-level frame. Reading these tells you exactly what moved. Go to impacted_nodes first. It gives you the before and after geometry per node, which means you can name the element and state how far it moved and how its box changed, rather than reporting a score nobody can act on. Chrome stops at five, so when a whole list of cards moves you get some of them, not all of them. There are rarely many shifts to read, because shifts are a low volume event and only 25 of the 35 sites in the corpus produced any.
The hard part is attribution, because the shift is reported in the rendering lifecycle, not in the task that caused it. Walking up the event tree from a LayoutShift reaches PrePaint and RunTask, never the script. To find the cause you correlate backwards: invalidations on the same thread since that frame's previous PrePaint, and resources that finished in the same window. Nothing in the trace format links the two, so that join is an inference you build, not a fact the trace gives you.
One reliable shortcut exists. PaintImage uses the same node id space as impacted_nodes, so an image that painted into a node that moved is a node-id-level match, not a timing guess.
{
"name": "LayoutShift",
"cat": "loading",
"ph": "I",
"ts": 1108583389,
"tts": 242293,
"pid": 8407,
"tid": 8407,
"s": "t",
"args": {
"data": {
"cumulative_score": 0.0029692105611165364,
"frame_max_distance": 32.078125,
"had_recent_input": false,
"impacted_nodes": [
{
"new_rect": [262, 81, 756, 41],
"node_id": 37,
"old_rect": [279, 81, 722, 41]
},
{
"new_rect": [262, 123, 756, 32],
"node_id": 95,
"old_rect": [272, 123, 736, 32]
},
{
"new_rect": [16, 566, 300, 271],
"node_id": 278,
"old_rect": [16, 583, 300, 238]
}
],
"is_main_frame": true,
"last_input_timestamp": 0,
"overall_max_distance": 32.078125,
"region_rects": [
[262, 81, 756, 41],
[262, 123, 756, 32],
[16, 566, 300, 271]
],
"score": 0.0029692105611165364,
"weighted_score_delta": 0.0029692105611165364
},
"frame": "2EA8810E92C12C680C55B2941251AA98"
}
}- categories
- loading
- usage
- low
- undeclared args
args.data.cumulative_score,args.data.frame_max_distance,args.data.had_recent_input,args.data.impacted_nodes[].new_rect[],args.data.impacted_nodes[].node_id,args.data.impacted_nodes[].old_rect[],args.data.is_main_frame,args.data.last_input_timestampand 4 more- references
- layout_shift_tracker.cc, layout_shift.h
SyntheticLayoutShiftLayout shiftsynthetic
One layout shift as DevTools rebuilds it, with the weighted score and cluster filled in.
DevTools builds this event while parsing a trace. You will not find it in a trace file.
SyntheticLayoutShift is built by DevTools while it parses a trace, wrapping a raw LayoutShift in resolved data.
The resolved data is what the raw event leaves implicit: the computed weighted score, the cluster the shift belongs to, and screenshot references.
You will never find it in a trace JSON file. If you are reading a raw trace, work from LayoutShift. If you are working inside trace_engine, this is what LayoutShiftsHandler actually hands you.
Know it exists so that you do not go hunting for the name in a trace and conclude your capture is broken.
DevTools builds this while parsing a trace. It is never written to a trace file, so there is no sample to show.
- references
- LayoutShiftsHandler.ts
SyntheticLayoutShiftClusterLayout shift clustersynthetic
A run of layout shifts that DevTools groups into a single session window.
DevTools builds this event while parsing a trace. You will not find it in a trace file.
SyntheticLayoutShiftCluster groups raw layout shifts into session windows. DevTools constructs it while parsing, so like SyntheticLayoutShift it never appears in a trace file.
The windowing rule is what people get wrong about CLS. A cluster ends after a gap of 1 second with no shift, or after 5 seconds total, whichever comes first. LayoutShiftsHandler states the constant in a comment: "the maximum time we will allow a cluster to go before we reset it".
CLS is not the sum of every shift on the page. It is the score of the worst cluster. A page that shifts steadily for 30 seconds is scored on its worst 5-second window, not the total. When your summed shift scores do not match the CLS a tool reports, this is usually why.
DevTools builds this while parsing a trace. It is never written to a trace file, so there is no sample to show.
- references
- LayoutShiftsHandler.ts
Compositor, raster and GPU
Frame production, image decode and everything off the main thread.
ActivateLayerTree
The compositor switches over to the new copy of the page's layers and can start drawing it.
ActivateLayerTree marks the compositor activating a newly committed layer tree, meaning the pending tree becomes the active one and its content can be drawn. Phase I, with args.layerTreeId and args.frameId.
It marks the boundary between "the main thread has handed over new content" and "the compositor can now present it". Together with Commit and BeginCommitCompositorFrame it lets you measure handover latency: the gap between the main thread finishing and the compositor being able to use the result.
Consumed by FramesHandler, the adapted TimelineFrameModel, which is what builds the frames track in DevTools.
{
"name": "ActivateLayerTree",
"cat": "disabled-by-default-devtools.timeline.frame",
"ph": "I",
"ts": 1102924040,
"tts": 1794,
"pid": 8257,
"tid": 8278,
"s": "t",
"args": {
"frameId": 2,
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline.frame
- usage
- moderate
- references
- devtools_instrumentation.h, layer_tree_impl.cc, layer_tree_host.cc
BeginCommitCompositorFrame
Chrome starts handing the next frame of page content over to the compositor.
BeginCommitCompositorFrame marks the start of committing a compositor frame. Phase I, with args.frame and args.is_mobile_optimized.
is_mobile_optimized reflects whether the page is considered mobile-optimised, which feeds decisions about how aggressively the compositor may handle input without consulting the main thread.
Its consumer is UserInteractionsHandler, which is the tell: this event sits on the path between an interaction and the pixels that answer it, so it is part of measuring presentation delay, the phase of INP that gets the least attention.
{
"name": "BeginCommitCompositorFrame",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1102923601,
"tts": 28668,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"is_mobile_optimized": true
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- references
- web_frame_widget_impl.cc, mobile_optimized_viewport_util.cc
cc::DisplayItemListno longer emitted
This captured the list of drawing commands Chrome had recorded for one part of the page.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
cc::DisplayItemList snapshotted a recorded paint list as a traced object from Chrome 47 to Chrome 148, and Chrome 149 renamed it cc::DisplayItemList:snapshot. It replaced cc::Picture when it arrived. The rename landed on main on 2026-05-01 in "[tracing] Migrate legacy TRACE_EVENT_OBJECT macros in cc". Chrome 141, the version behind the examples on this page, still wrote the old name. Look for cc::DisplayItemList in traces from Chrome 148 and earlier, and for cc::DisplayItemList:snapshot from Chrome 149 on.
DisplayItemList::EmitTraceSnapshot in cc/paint/display_item_list.cc writes it under both names on three categories at once: disabled-by-default-cc.debug.display_items, disabled-by-default-cc.debug.picture and disabled-by-default-devtools.timeline.picture, the last of which is what DevTools turns on for advanced paint instrumentation. Up to Chrome 148 it was a phase O object snapshot. From Chrome 149 it is an instant with a flow keyed on the display list.
Under args.snapshot, params.layer_rect and a top-level skp64 are always present, under either name. skp64 is the base64 serialised SkPicture of the whole list. Only if disabled-by-default-cc.debug.display_items is also enabled does params.items appear, one entry per PaintOp with name, visual_rect and its own skp64. That array is what makes these events enormous.
It fires from RecordingSource::FinishDisplayItemListUpdate when a recording is finalised, and again for every layer at the start of a new trace via RasterSource::DidBeginTracing.
Chrome 141 still wrote this name, but only on disabled-by-default-devtools.timeline.picture and two disabled-by-default-cc.debug categories, none of which the collection enabled. From Chrome 149 the same snapshot is written as cc::DisplayItemList:snapshot.
- references
- display_item_list.cc
cc::LayerTreeHostImplno longer emitted
This captured the state of the compositor's own copy of the page's layers.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
cc::LayerTreeHostImpl snapshotted the compositor's impl-side tree up to Chrome 148, and Chrome 149 renamed it LayerTreeHostImpl:snapshot. The rename landed on main on 2026-05-01 in "[tracing] Migrate legacy TRACE_EVENT_OBJECT macros in cc". The example on this page comes from Chrome 141, so it shows the old name. Look for cc::LayerTreeHostImpl in traces from Chrome 148 and earlier, and for LayerTreeHostImpl:snapshot from Chrome 149 on.
Under either name the event is state rather than a unit of work. Up to Chrome 148, TRACE_EVENT_OBJECT_SNAPSHOT_WITH_ID in cc/trees/layer_tree_host_impl.cc wrote it as phase O, with an id equal to the LayerTreeHostImpl id and the whole payload under args.snapshot. Its dur is meaningless. The old name also marked the object being created and deleted, on disabled-by-default-cc.debug. From Chrome 149 the snapshot is an instant with a process-scoped flow on the same id, the payload stays under args.snapshot, and creation and deletion are LayerTreeHostImpl:created and LayerTreeHostImpl:deleted.
Both names use the categories from frame_viewer_instrumentation::CategoryLayerTree(): disabled-by-default-cc.debug, disabled-by-default-viz.quads and disabled-by-default-devtools.timeline.layers. The payload is built by AsValueWithFrameInto: activation_state, device_viewport_size, an active_tiles array of every prioritized tile, tile_manager_basic_state, active_tree, pending_tree and frame. One is written per draw, which makes it one of the heaviest things in a trace.
{
"name": "cc::LayerTreeHostImpl",
"cat": "disabled-by-default-cc.debug,disabled-by-default-viz.quads,disabled-by-default-devtools.timeline.layers",
"ph": "O",
"ts": 1111534383,
"tts": 37131,
"pid": 8407,
"tid": 8421,
"id": "0x1",
"args": {
"snapshot": {
"activation_state": {
"lthi": {
"id_ref": "0x276400553000"
},
"tile_manager": {
"did_oom_on_last_assign": false,
"global_state": {
"hard_memory_limit_in_bytes": 536870912,
"memory_limit_policy": "ALLOW_PREPAINT_ONLY",
"num_resources_limit": 10000000,
"soft_memory_limit_in_bytes": 536870912,
"tree_priority": "SMOOTHNESS_TAKES_PRIORITY"
},
"tile_count": 63
}
},
"active_tiles": [
{
"cat": "disabled-by-default-cc.debug",
"combined_priority": {
"distance_to_visible": 0,
"is_occluded": false,
"priority_bin": "NOW",
"resolution": "HIGH_RESOLUTION"
},
"content_rect": [0, 0, 512, 128],
"contents_scale": 1,
"draw_info": {
"is_solid_color": false,
"is_transparent": false
},
"gpu_memory_usage": 262144,
"has_resource": true,
"id": "cc::Tile/0x27640167a4c0",
"is_using_gpu_memory": true,
"layer_id": 11,
"picture_pile": {
"id_ref": "0x276400563a20"
},
"raster_transform": {
"scale": [1, 1],
"translation": [0, 0]
},
"resolution": "HIGH_RESOLUTION",
"scheduled_priority": 1,
"use_picture_analysis": true
},
{
"cat": "disabled-by-default-cc.debug",
"combined_priority": {
"distance_to_visible": 0,
"is_occluded": false,
"priority_bin": "NOW",
"resolution": "HIGH_RESOLUTION"
},
"content_rect": [510, 0, 512, 128],
"contents_scale": 1,
"draw_info": {
"is_solid_color": false,
"is_transparent": false
},
"gpu_memory_usage": 262144,
"has_resource": true,
"id": "cc::Tile/0x27640167a920",
"is_using_gpu_memory": true,
"layer_id": 11,
"picture_pile": {
"id_ref": "0x276400563a20"
},
"raster_transform": {
"scale": [1, 1],
"translation": [0, 0]
},
"resolution": "HIGH_RESOLUTION",
"scheduled_priority": 2,
"use_picture_analysis": true
},
{
"cat": "disabled-by-default-cc.debug",
"combined_priority": {
"distance_to_visible": 0,
"is_occluded": false,
"priority_bin": "NOW",
"resolution": "HIGH_RESOLUTION"
},
"content_rect": [1020, 0, 512, 128],
"contents_scale": 1,
"draw_info": {
"is_solid_color": false,
"is_transparent": false
},
"gpu_memory_usage": 262144,
"has_resource": true,
"id": "cc::Tile/0x276400bd3800",
"is_using_gpu_memory": true,
"layer_id": 11,
"picture_pile": {
"id_ref": "0x276400563a20"
},
"raster_transform": {
"scale": [1, 1],
"translation": [0, 0]
},
"resolution": "HIGH_RESOLUTION",
"scheduled_priority": 3,
"use_picture_analysis": true
},
"... [60 more items]"
],
"active_tree": {
"id": "cc::LayerTreeImpl/0x2764000bcc00",
"layers": [
{
"base_type": "cc::LayerImpl",
"bounds": {
"height": 0,
"width": 0
},
"cat": "disabled-by-default-cc.debug",
"clip_tree_index": 1,
"contents_opaque": false,
"draws_content": 0,
"effect_tree_index": 1,
"gpu_memory_usage": 0,
"hit_testable": false,
"id": "cc::LayerImpl/0x27640005aa00",
"layer_id": 7,
"layer_name": "root",
"layer_quad": [0, 0, 0, 0, 0, 0, 0, 0],
"opacity": 1,
"opaque_to_hit_test": false,
"position": [0, 0],
"scroll_tree_index": 1,
"sorting_context_id": 0,
"transform_tree_index": 1
},
{
"base_type": "cc::LayerImpl",
"bounds": {
"height": 900,
"width": 1280
},
"cat": "disabled-by-default-cc.debug",
"clip_tree_index": 1,
"compositing_reason_ids": ["OverflowScrolling"],
"compositing_reasons": ["Is a scrollable overflow element using accelerated scrolling."],
"contents_opaque": false,
"draws_content": 0,
"effect_tree_index": 1,
"element_id": {
"id_": 644
},
"gpu_memory_usage": 0,
"hit_testable": true,
"id": "cc::LayerImpl/0x2764013b9180",
"layer_id": 8,
"layer_name": "LayoutView #document",
"layer_quad": [0, 0, 1280, 0, 1280, 900, 0, 900],
"opacity": 1,
"opaque_to_hit_test": true,
"owner_node": 7,
"position": [0, 0],
"scroll_tree_index": 3,
"sorting_context_id": 0,
"transform_tree_index": 4
},
{
"base_type": "cc::LayerImpl",
"bounds": {
"height": 6660,
"width": 1280
},
"can_have_tilings_state": {
"can_have_tilings": true,
"draws_content": true,
"max_contents_scale": 1,
"min_contents_scale": 0.0007812500116415322,
"raster_source_has_recordings": true,
"raster_source_solid_color": false
},
"cat": "disabled-by-default-cc.debug",
"clip_tree_index": 2,
"compositing_reason_ids": ["RootScroller", "OverflowScrolling"],
"compositing_reasons": ["Is the document.rootScroller.", "Is a scrollable overflow element using accelerated scrolling."],
"contents_opaque": true,
"coverage_tiles": [
{
"...": "[nested too deep]"
},
{
"...": "[nested too deep]"
},
{
"...": "[nested too deep]"
},
"... [52 more items]"
],
"draws_content": 1,
"effect_tree_index": 1,
"geometry_contents_scale": 1,
"gpu_memory_usage": 12582912,
"hit_testable": true,
"id": "cc::PictureLayerImpl/0x27640002bc00",
"ideal_contents_scale": 1,
"ideal_scales": {
"contents_scale": ["..."],
"device_scale": 1,
"page_scale": 1,
"source_scale": ["..."]
},
"invalidation": [1024, 4903, 1, 1, 964, 4904, 51, 14, "... [268 more items]"],
"layer_id": 9,
"layer_name": "Scrolling background of LayoutView #document",
"layer_quad": [0, -900, 1280, -900, 1280, 5760, 0, 5760],
"lcd_text_disallowed_reason": "none",
"opacity": 1,
"opaque_to_hit_test": false,
"owner_node": 7,
"pictures": [
{
"...": "[nested too deep]"
}
],
"position": [0, 0],
"raster_scales": {
"contents_scale": ["..."],
"device_scale": 1,
"page_scale": 1,
"source_scale": ["..."]
},
"screen_space_transform": [1, 0, 0, 0, 0, 1, 0, -900, "... [8 more items]"],
"scroll_tree_index": 3,
"sorting_context_id": 0,
"tile_priority_rect": [0, 900, 1280, 900],
"tilings": [
{
"...": "[nested too deep]"
}
],
"transform_tree_index": 5,
"visible_rect": [0, 900, 1280, 900]
},
"... [6 more items]"
],
"render_surface_layer_list": [
{
"id_ref": "0x27640005c980"
},
{
"id_ref": "0x2764033eda00"
},
{
"id_ref": "0x2764013b9180"
},
"... [1 more items]"
],
"source_frame_number": 28,
"swap_promise_trace_ids": [0, 0]
},
"device_viewport_size": {
"height": 900,
"width": 1280
},
"frame": {
"has_no_damage": false
},
"pending_tree": {
"id": "cc::LayerTreeImpl/0x2764006e9400",
"layers": [
{
"base_type": "cc::LayerImpl",
"bounds": {
"height": 0,
"width": 0
},
"cat": "disabled-by-default-cc.debug",
"clip_tree_index": 1,
"contents_opaque": false,
"draws_content": 0,
"effect_tree_index": 1,
"gpu_memory_usage": 0,
"hit_testable": false,
"id": "cc::LayerImpl/0x2764013b8380",
"layer_id": 7,
"layer_name": "root",
"layer_quad": [0, 0, 0, 0, 0, 0, 0, 0],
"opacity": 1,
"opaque_to_hit_test": false,
"position": [0, 0],
"scroll_tree_index": 1,
"sorting_context_id": 0,
"transform_tree_index": 1
},
{
"base_type": "cc::LayerImpl",
"bounds": {
"height": 900,
"width": 1280
},
"cat": "disabled-by-default-cc.debug",
"clip_tree_index": 1,
"compositing_reason_ids": ["OverflowScrolling"],
"compositing_reasons": ["Is a scrollable overflow element using accelerated scrolling."],
"contents_opaque": false,
"draws_content": 0,
"effect_tree_index": 1,
"element_id": {
"id_": 644
},
"gpu_memory_usage": 0,
"hit_testable": true,
"id": "cc::LayerImpl/0x2764013b9500",
"layer_id": 8,
"layer_name": "LayoutView #document",
"layer_quad": [0, 0, 1280, 0, 1280, 900, 0, 900],
"opacity": 1,
"opaque_to_hit_test": true,
"owner_node": 7,
"position": [0, 0],
"scroll_tree_index": 3,
"sorting_context_id": 0,
"transform_tree_index": 4
},
{
"base_type": "cc::LayerImpl",
"bounds": {
"height": 6660,
"width": 1280
},
"can_have_tilings_state": {
"can_have_tilings": true,
"draws_content": true,
"max_contents_scale": 1,
"min_contents_scale": 0.0007812500116415322,
"raster_source_has_recordings": true,
"raster_source_solid_color": false
},
"cat": "disabled-by-default-cc.debug",
"clip_tree_index": 2,
"compositing_reason_ids": ["RootScroller", "OverflowScrolling"],
"compositing_reasons": ["Is the document.rootScroller.", "Is a scrollable overflow element using accelerated scrolling."],
"contents_opaque": true,
"coverage_tiles": [
{
"...": "[nested too deep]"
}
],
"draws_content": 1,
"effect_tree_index": 1,
"geometry_contents_scale": 1,
"gpu_memory_usage": 0,
"hit_testable": true,
"id": "cc::PictureLayerImpl/0x276400028000",
"ideal_contents_scale": 1,
"ideal_scales": {
"contents_scale": ["..."],
"device_scale": 1,
"page_scale": 1,
"source_scale": ["..."]
},
"layer_id": 9,
"layer_name": "Scrolling background of LayoutView #document",
"layer_quad": [0, -900, 1280, -900, 1280, 5760, 0, 5760],
"lcd_text_disallowed_reason": "none",
"opacity": 1,
"opaque_to_hit_test": false,
"owner_node": 7,
"pictures": [
{
"...": "[nested too deep]"
}
],
"position": [0, 0],
"raster_scales": {
"contents_scale": ["..."],
"device_scale": 1,
"page_scale": 1,
"source_scale": ["..."]
},
"screen_space_transform": [1, 0, 0, 0, 0, 1, 0, -900, "... [8 more items]"],
"scroll_tree_index": 3,
"sorting_context_id": 0,
"tile_priority_rect": [0, 900, 1280, 900],
"tilings": [
{
"...": "[nested too deep]"
}
],
"transform_tree_index": 5,
"visible_rect": [0, 900, 1280, 900]
},
"... [6 more items]"
],
"render_surface_layer_list": [
{
"id_ref": "0x27640005b480"
},
{
"id_ref": "0x2764033f0c00"
},
{
"id_ref": "0x2764013b9500"
},
"... [1 more items]"
],
"source_frame_number": 29,
"swap_promise_trace_ids": [0]
},
"tile_manager_basic_state": {
"did_oom_on_last_assign": false,
"global_state": {
"hard_memory_limit_in_bytes": 536870912,
"memory_limit_policy": "ALLOW_PREPAINT_ONLY",
"num_resources_limit": 10000000,
"soft_memory_limit_in_bytes": 536870912,
"tree_priority": "SMOOTHNESS_TAKES_PRIORITY"
},
"tile_count": 63
}
}
}
}- references
- layer_tree_host_impl.cc, picture_layer_impl.cc, layer_impl.cc, math_util.cc, picture_layer_impl.h, traced_value.cc and 23 more
cc::Pictureno longer emitted
This captured one recorded drawing, saved so it could be replayed when that part of the page was painted.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
cc::Picture snapshotted one recorded drawing as a traced object, and Chrome has not written it since Chrome 46. The change "cc: Remove Picture" deleted it on 2015-09-24. cc/playback/picture.cc is present in Chrome 46 and gone in Chrome 47, when the Picture and PicturePile model was replaced by DisplayItemList. The event to read instead is cc::DisplayItemList in Chrome 47 to 148, and cc::DisplayItemList:snapshot from Chrome 149 on.
It came from Picture::EmitTraceSnapshot, phase O, on disabled-by-default-cc.debug.picture and disabled-by-default-devtools.timeline.picture. The payload was small: params.layer_rect plus skp64, a base64 serialised SkPicture of the recorded drawing, which is what the old DevTools paint profiler replayed. A sibling EmitTraceSnapshotAlias wrote the same name for pictures that merely pointed at another recording.
If you are parsing a trace from any supported Chrome you will never see this event, and any tool still keyed on it is reading a 2015 format.
Deleted in September 2015, so Chrome 47 and later never write it. Its successor is cc::DisplayItemList, renamed cc::DisplayItemList:snapshot in Chrome 149.
Decode LazyPixelRef
Chrome turns one compressed image into pixels, on a thread other than the main one.
Decode LazyPixelRef measures one image being decoded on a raster thread. Phase X, so the duration is real decode cost. The main thread has its own version of this event, Decode Image.
args.LazyPixelRef is a numeric id, not a URL. That is the whole difficulty with this event: on its own it tells you an image was decoded and nothing about which one. ImagePaintingHandler exists to solve exactly this, joining the id back to a PaintImage so the decode can be blamed on a specific element. The id can legitimately be 0, which is observed in real traces, so treat 0 as a valid key rather than a missing value.
Reading these events tells you about decode work that never appears in main thread totals, because it happens off the main thread. If your scripting and rendering numbers look fine but frames are still being missed, look at the raster threads. See also Decode Image, the main-thread counterpart, whose arguments name the image URL.
{
"name": "Decode LazyPixelRef",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1108423665,
"dur": 162,
"tdur": 158,
"tts": 877,
"pid": 8407,
"tid": 8471,
"args": {
"LazyPixelRef": 3
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- duration
- very long
- references
- image.cc, paint_image.cc, ImagePaintingHandler.ts, decoding_image_generator.cc
DirectRenderer::DrawRenderPassnot in DevTools recordingsnot in DevTools' model
Chrome draws one pass of the finished frame, either onto the screen or into an offscreen buffer.
A DevTools Performance recording does not contain this event. Record viz with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
DirectRenderer::DrawRenderPass measures viz drawing one render pass of the aggregated compositor frame, from components/viz/service/display/direct_renderer.cc. The root pass is the screen, and every additional pass is an intermediate texture that something forced.
args.id is the AggregatedRenderPassId and args.NumberOfQuads is render_pass->quad_list.size().
Count the distinct id values in a frame and you have counted the offscreen buffers your CSS is asking for. Backdrop filters, masks, non trivial opacity on a group, and rounded corner clipping of composited content each create one, and each costs an allocate, a draw and a sample. Nothing in DevTools puts a number on that. The function early returns via CanSkipRenderPass(), so undamaged passes still emit a tiny slice. These run slower than most trace events, and the fat instances are almost always the root pass with a large quad list, or a filter pass redrawn every frame.
{
"name": "DirectRenderer::DrawRenderPass",
"cat": "viz",
"ph": "X",
"ts": 1102925542,
"dur": 991,
"tdur": 605,
"tts": 3172,
"pid": 8211,
"tid": 8266,
"args": {
"NumberOfQuads": 13
}
}- categories
- viz
- usage
- high
- duration
- typical
- references
- direct_renderer.cc
DisplayItemList::Rasternot in DevTools recordingsnot in DevTools' model
Chrome replays its recorded drawing commands to produce the actual pixels for one patch of the page.
A DevTools Performance recording does not contain this event. Record cc with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
DisplayItemList::Raster records the playback of one recorded paint op buffer onto a raster canvas, from cc/paint/display_item_list.cc. There is one per tile, on a raster worker thread.
Two args are recorded at opposite ends of the slice. total_op_count is written at BEGIN and is every paint op in the list. rastered_op_count is written at END and counts only the ops that survived the r-tree query for this tile's clip bounds, including extras unfolded by PaintOp::OpAdditionalOpCount.
The ratio measures how badly your layer spatialises. A tile that rasters most of the list's ops has an r-tree that culled almost nothing, meaning the layer's paint ops overlap across its whole area and every tile pays for the whole page. Full width backgrounds, box shadows and absolutely positioned overlays do this. Duration is typical for a trace event and the volume is high, so a handful of bad tiles is what a slow raster pass usually turns out to be.
{
"name": "DisplayItemList::Raster",
"cat": "cc",
"ph": "X",
"ts": 1102928342,
"dur": 11,
"tdur": 10,
"tts": 1440,
"pid": 8178,
"tid": 8209,
"args": {
"rastered_op_count": 13,
"total_op_count": 13
}
}- categories
- cc
- usage
- high
- duration
- typical
- references
- display_item_list.cc, paint_op.h, paint_op_buffer.h
Draw LazyPixelRef
Chrome draws an already decoded image into one patch of the page's pixels.
Draw LazyPixelRef marks a decoded image being drawn into a raster tile. Phase I. Its only argument is args.LazyPixelRef, the image id, which is also all its decode counterpart gives you.
Use it as a join key. ImagePaintingHandler connects a pixel ref id to the PaintImage that produced it, which is what makes off-main-thread decode attributable to a real element.
Multiple draws of the same id are normal and expected: one decoded image drawn into several tiles. Counting them as separate images will overstate your image work considerably.
{
"name": "Draw LazyPixelRef",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1108423863,
"tts": 91991,
"pid": 8407,
"tid": 8407,
"s": "t",
"args": {
"LazyPixelRef": 2
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- references
- image.cc, paint_image.cc, ImagePaintingHandler.ts, bitmap_image.cc
DroppedFrame
This event tells you a frame never made it to the screen.
DroppedFrame marks a frame that was not presented. Phase I, with args.layerTreeId, args.frameSeqId and args.hasPartialUpdate.
hasPartialUpdate changes the interpretation. true means some of the frame did make it to screen, so the user saw an update, just not a complete one. false is a frame entirely lost.
frameSeqId is a monotonic counter, so gaps between consecutive presented frames tell you how many were missed in a row. One dropped frame is invisible. Six consecutive is a visible stutter, and the sequence ids are how you tell them apart.
The event requires disabled-by-default-devtools.timeline.frame. Its absence from a trace means the category was off, not that no frames were dropped. For the reason a frame was dropped rather than the fact of it, read the PipelineReporter covering the same frameSeqId.
{
"name": "DroppedFrame",
"cat": "disabled-by-default-devtools.timeline.frame",
"ph": "I",
"ts": 1108805646,
"tts": 24480,
"pid": 8407,
"tid": 8421,
"s": "t",
"args": {
"frameSeqId": 37,
"hasPartialUpdate": true,
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline.frame
- usage
- moderate
- references
- devtools_instrumentation.h, compositor_frame_reporter.cc, FramesHandler.ts
GPUTaskGPU
Chrome runs a piece of work in the GPU process, the part that talks to the graphics hardware.
GPUTask measures work running in the GPU process. Phase X.
args.data.renderer_pid attributes the work to a specific renderer, which matters when a trace covers several tabs or a page with out-of-process iframes. args.data.used_bytes reports GPU memory in use, sampled per task.
Reading these events gives you the only routine window into a process most tooling ignores, and they are among the most numerous events in a trace. Tracked over a recording, used_bytes gives you a memory curve for free, and a steadily climbing one during interaction is a real finding: it usually means layers or textures are being created and not released.
Individual tasks are short. Their significance is aggregate: a large total here with a healthy main thread means your bottleneck is compositing or texture upload, not JavaScript.
{
"name": "GPUTask",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1128617562,
"dur": 25,
"tdur": 22,
"tts": 113166,
"pid": 8768,
"tid": 8768,
"args": {
"data": {
"renderer_pid": 8798,
"used_bytes": 16515072
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- duration
- typical
- references
- command_buffer_stub.cc, gpu_channel.cc
Graphics.Pipelinenot in DevTools recordingsnot in DevTools' model
One step on the path from Chrome asking for a frame to that frame reaching the screen.
A DevTools Performance recording does not contain this event. Record viz, benchmark or graphics.pipeline with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
Graphics.Pipeline marks one step of frame production, somewhere between the begin frame request and pixels on screen. One emitter is CompositorFrameSinkSupport::OnBeginFrame in components/viz/service/frame_sinks/compositor_frame_sink_support.cc.
args.chrome_graphics_pipeline.step is the discriminator, a StepName enum in base/tracing/protos/chrome_track_event.proto: STEP_ISSUE_BEGIN_FRAME, STEP_RECEIVE_BEGIN_FRAME, STEP_SUBMIT_COMPOSITOR_FRAME, STEP_SURFACE_AGGREGATION, STEP_DRAW_AND_SWAP, STEP_SWAP_BUFFERS_ACK, and the failure outcome STEP_DID_NOT_PRODUCE_FRAME.
Two ids stitch it together and confusing them is the usual mistake. surface_frame_trace_id links the client side steps up to frame submission, display_trace_id links everything after surface aggregation, and STEP_SURFACE_AGGREGATION emits aggregated_surface_frame_trace_ids to join the halves. More than twenty five call sites across cc, viz and the GPU process all emit this one name, so the step field and the ids are the only way to tell them apart. Chase one id and you get true begin frame to pixels latency for a single frame, which is the number DevTools never shows you.
{
"name": "Graphics.Pipeline",
"cat": "viz,benchmark,graphics.pipeline",
"ph": "X",
"ts": 1102922635,
"dur": 138,
"tdur": 119,
"tts": 1896,
"pid": 8211,
"tid": 8266,
"args": {
"chrome_graphics_pipeline": {
"possible_deadlines": {
"frame_time_us": 1102922548
},
"step": "STEP_ISSUE_BEGIN_FRAME",
"surface_frame_trace_id": 4269900944451596300
},
"current_task": {
"event_offset_from_task_start_time_us": 27,
"task_queued_time_us": 1102905949,
"task_queueing_time_us": 16692,
"task_start_time_us": 1102922614
}
}
}- categories
- viz, benchmark, graphics.pipeline
- usage
- very high
- duration
- typical
- references
- chrome_track_event.proto, compositor_frame_sink_support.cc, task_annotator.cc, proxy_impl.cc, layer_tree_host_impl.cc, async_layer_tree_frame_sink.cc and 2 more
ImageDecodeTask
The task Chrome queues up to decode one image, run on a worker thread.
ImageDecodeTask measures a raster-thread task that decodes an image. Phase X.
trace_engine does not model its arguments, but real traces show args.pixelRefId on it. Note the field name differs from the LazyPixelRef events, which use args.LazyPixelRef for the same kind of identifier. Tooling that looks for one name will silently miss the other.
It is the scheduling wrapper around decode work, so it typically encloses the actual Decode LazyPixelRef. Use ImageDecodeTask for "how much raster thread time went on decoding" and the inner event for per-image attribution.
Observed durations are short individually. The cost shows up in aggregate on image-heavy pages, and it competes with raster for the same threads.
{
"name": "ImageDecodeTask",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1108421996,
"dur": 1834,
"tdur": 303,
"tts": 733,
"pid": 8407,
"tid": 8471,
"args": {
"pixelRefId": 43310469240832
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- duration
- typical
- undeclared args
args.pixelRefId- references
- devtools_instrumentation.cc, software_image_decode_cache.cc, gpu_image_decode_cache.cc
ImageUploadTask
Chrome uploads an already decoded image to the graphics hardware so it can be drawn.
ImageUploadTask measures the GPU texture upload of an already decoded image. It is a begin/end pair from ScopedImageUploadTask in cc/base/devtools_instrumentation.cc, category disabled-by-default-devtools.timeline, so the duration is real.
There is exactly one construction site, ImageUploadTaskImpl::RunOnWorkerThread in cc/tiles/gpu_image_decode_cache.cc. You therefore only see it under GPU raster. The software cache emits ImageDecodeTask and no upload event.
args.pixelRefId is a trap. It is a raw pointer cast to an integer, not an image id: the constructor is handed the PaintImage address and stores reinterpret_cast<uint64_t>(image_ptr). It will not join to args.LazyPixelRef, and ImageDecodeTask supplies a different pointer again. Use it only to pair an upload with its decode inside the same cache.
Sub-millisecond values are normal. The task declares SupportsConcurrentExecution::kNo, so a burst of uploads serialises onto one worker and reads as a raster-thread queue rather than one slow event.
Not seen. It is only written when images are uploaded for GPU rasterization, and the headless Chrome that recorded the collection composited in software.
LazyPixelRefno longer emitted
Chrome tracked one image it was holding to draw but had not decoded yet.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
LazyPixelRef tracked the lifetime of a lazily decoded image in Blink until August 2014, and no Chrome since has written an event under that bare name. It was an object lifetime marker rather than a timed event. Blink's PlatformInstrumentation::LazyPixelRefTracker wrapped a TraceScopedTrackableObject that wrote object-created and object-deleted pairs named LazyPixelRef on the plain devtools category.
Its only user, LazyDecodingPixelRef, was deleted on 2014-08-21 in "Image decoding: Remove deprecated code path". The tracker class stayed behind unused until October 2018, when it was removed too.
The id survives in two other events. Draw LazyPixelRef is an instant written from third_party/blink/renderer/platform/graphics/image.cc and bitmap_image.cc when a lazily decoded image is drawn. Decode LazyPixelRef is a begin/end pair from decoding_image_generator.cc.
Both write args.LazyPixelRef, which is PaintImage::stable_id(), a process-unique image id. That id is the join key that ties decode work on a raster thread back to the PaintImage event that names the image URL and node.
Chrome stopped writing this bare name in August 2014, when its only user was deleted. The collection recorded Decode LazyPixelRef and Draw LazyPixelRef, which use the same image id.
- references
- image.cc, PlatformInstrumentation.h
NeedsBeginFrameChanged
The compositor changes its mind about whether the page needs new frames drawn at all.
NeedsBeginFrameChanged fires when the compositor toggles whether it wants frames at all. Phase I, with args.layerTreeId and args.data.needsBeginFrame.
needsBeginFrame: 0 means nothing on the page requires updating and Chrome stops producing frames. 1 means something does.
Use it to explain apparent gaps in a trace. A long stretch with no frame activity is usually not a stall: it is the browser correctly idling because nothing changed. Reading those gaps as jank is a common and avoidable misdiagnosis.
Conversely, needsBeginFrame stuck at 1 on a visually static page means something is requesting frames forever, typically a requestAnimationFrame loop or a CSS animation on an offscreen element. That is real battery cost with nothing on screen to show for it.
{
"name": "NeedsBeginFrameChanged",
"cat": "disabled-by-default-devtools.timeline.frame",
"ph": "I",
"ts": 1103089474,
"tts": 8529,
"pid": 8257,
"tid": 8278,
"s": "t",
"args": {
"data": {
"needsBeginFrame": 0
},
"layerTreeId": 1
}
}- categories
- disabled-by-default-devtools.timeline.frame
- usage
- moderate
- references
- devtools_instrumentation.h, scheduler.cc
PipelineReporter
Chrome follows one whole attempt to produce a frame, from the request to the outcome.
PipelineReporter covers one frame production attempt from end to end. It is an async pair, phase b and e, one pair per attempt, which makes it one of the highest-volume events present.
In real traces every argument sits under args.frame_reporter. The fields there, among others:
stateandframe_type, which record whether the frame was presented,has_high_latencyandaffects_smoothnesscheckerboarded_needs_rasterandcheckerboarded_needs_recordhas_compositor_animation,has_main_animation,has_smooth_input_mainhas_missing_content,scroll_state,frame_sequence,
dropped, or partially updated
layer_tree_host_id, display_trace_id, surface_frame_trace_id Reading one gives you a per-frame verdict from the compositor's own scheduler. Reach for affects_smoothness first, because it distinguishes a dropped frame nobody could perceive from one that broke an animation. checkerboarded_needs_raster means the compositor had to show blank space because raster had not caught up, which is the technical name for the grey flash during fast scrolling.
Two things limit its reach today. It lives in the cc, benchmark and disabled-by-default-devtools.timeline.frame categories, so a default recording may not include it. And trace_engine models none of its arguments, with ScreenshotsHandler as its only consumer there. For the most information-dense event in a Chrome trace, the analysis value is almost entirely untapped by existing tooling.
{
"name": "PipelineReporter",
"cat": "cc,benchmark,disabled-by-default-devtools.timeline.frame",
"ph": "b",
"ts": 1104989132,
"pid": 8257,
"tid": 8278,
"id2": {
"local": "0x38"
},
"args": {
"frame_reporter": {
"affects_smoothness": false,
"checkerboarded_needs_raster": false,
"checkerboarded_needs_record": false,
"display_trace_id": 4269900944451596000,
"frame_sequence": 130,
"frame_source": 4294967296,
"frame_type": "FORKED",
"has_compositor_animation": false,
"has_high_latency": false,
"has_main_animation": true,
"has_missing_content": false,
"has_smooth_input_main": false,
"layer_tree_host_id": 1,
"scroll_state": "SCROLL_NONE",
"state": "STATE_PRESENTED_ALL",
"surface_frame_trace_id": 4269900944451596000
}
}
}- categories
- cc, benchmark, disabled-by-default-devtools.timeline.frame
- usage
- high
- undeclared args
args.frame_reporter.affects_smoothness,args.frame_reporter.checkerboarded_needs_raster,args.frame_reporter.checkerboarded_needs_record,args.frame_reporter.display_trace_id,args.frame_reporter.frame_sequence,args.frame_reporter.frame_source,args.frame_reporter.has_compositor_animation,args.frame_reporter.has_high_latencyand 8 more- references
- chrome_track_event.proto, compositor_frame_reporter.cc
Rasterizenever written by Chrome
Turning your page's drawing commands into pixels, under the name DevTools lists it by.
No Chrome build writes this name into a trace file. It exists in the model, not on the wire.
Rasterize is a name DevTools still lists, but Chrome has never written it as a trace event. It lives only as Name.RASTERIZE in front_end/models/trace/types/TraceEvents.ts, and its single use is a switch case in TimelineUIUtils.ts that labels the related node "Layer root". There is no interface, no type guard, no handler and no style entry, because nothing produces it.
The name is a leftover from the DevTools Timeline that predates tracing. From 2013 to early 2015, Blink's InspectorTimelineAgent read the RasterTask trace events cc wrote and turned them into Timeline protocol records of type Rasterize. That record type was never a trace event name, and the enum entry outlived the agent.
The event Chrome writes is RasterTask, from ScopedRasterTask in cc/tiles/frame_viewer_instrumentation.cc on cc,disabled-by-default-devtools.timeline, with args.tileData giving tileId, tileResolution, sourceFrameNumber and layerId.
Chrome has never written this name as a trace event. cc writes the work as RasterTask, which the collection recorded.
- references
- TraceEvents.ts, frame_viewer_instrumentation.cc
RasterizerTaskImpl::RunOnWorkerThreadnot in DevTools recordingsnot in DevTools' model
The whole job of turning one patch of the page into pixels, run on a background worker thread.
A DevTools Performance recording does not contain this event. Record cc with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
RasterizerTaskImpl::RunOnWorkerThread measures a single tile raster task executing on a cc worker thread. The trace name does not match the class that emits it: the call site is RasterTaskImpl::RunOnWorkerThread in cc/tiles/tile_manager.cc, so searching Chromium for "RasterizerTaskImpl" finds only the string literal.
args.source_prepare_tiles_id is prepare_tiles_count_ at the moment the task was created, the same counter TileManager::PrepareTiles emits as prepare_tiles_id.
Group raster tasks by that id and you have every tile scheduled by one PrepareTiles pass, the real unit of "this scroll caused this much raster", and a grouping DevTools has no view for. One construction detail explains the scheduling you see: GPU rasterised tasks are built with SupportsConcurrentExecution::kNo, software ones with kYes. Wide fan out across worker threads means you are on software raster.
{
"name": "RasterizerTaskImpl::RunOnWorkerThread",
"cat": "cc",
"ph": "X",
"ts": 1102928313,
"dur": 45,
"tdur": 41,
"tts": 1412,
"pid": 8178,
"tid": 8209,
"args": {
"source_prepare_tiles_id": 3
}
}- categories
- cc
- usage
- high
- duration
- typical
- references
- tile_manager.cc
RasterSource::PerformSolidColorAnalysisnot in DevTools recordingsnot in DevTools' model
Chrome checks whether one patch of the page is nothing but a single flat colour.
A DevTools Performance recording does not contain this event. Record cc with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
RasterSource::PerformSolidColorAnalysis records Chrome checking whether a tile is just one colour, in cc/raster/raster_source.cc. It scales the layer rect by the recording scale factor and calls DisplayItemList::GetColorIfSolidInRect with a max_ops_to_analyze cap.
There are no arguments. When the analysis succeeds the tile needs no raster buffer, no worker task and no GPU memory, because cc just draws a solid colour quad. Every one that returns true is a tile you did not have to raster, which is why flat colour sections really are cheaper than gradients that look identical.
Cost is trivial, faster than most trace events, so it never shows up as a problem itself. Read it as an indicator instead. A run of these with no matching DisplayItemList::Raster is the compositor skipping work, and a page where they barely appear is a page where nearly every tile has real content. DevTools shows you neither the skipped tiles nor the skipping.
{
"name": "RasterSource::PerformSolidColorAnalysis",
"cat": "cc",
"ph": "X",
"ts": 1102927340,
"dur": 3,
"tdur": 2,
"tts": 187789,
"pid": 8178,
"tid": 8178,
"args": {
}
}- categories
- cc
- usage
- high
- duration
- short
- references
- raster_source.cc
SetLayerTreeId
Chrome ties the compositor's id for a set of layers to the document they belong to.
SetLayerTreeId associates a layer tree id with a frame. Phase I.
It is pure bookkeeping, and you will not analyse it directly. It matters because almost every compositor event identifies itself by layerTreeId rather than by frame or URL, and this event is the mapping that makes those ids meaningful.
Without it you can measure compositor work but cannot say which page it belongs to, which becomes important as soon as a trace covers more than one frame.
{
"name": "SetLayerTreeId",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1102923256,
"tts": 28441,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"layerTreeId": 1
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- moderate
- references
- local_frame_view.cc
SoftwareRenderer::DoDrawQuadnot in DevTools recordingsnot in DevTools' model
The CPU, rather than the graphics hardware, paints one rectangle of the composited page.
A DevTools Performance recording does not contain this event. Record viz with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
SoftwareRenderer::DoDrawQuad measures one quad painted by the CPU renderer, in components/viz/service/display/software_renderer.cc.
There are no arguments, and the duration is not the point either. The body does an SkAutoCanvasRestore save whenever there is a draw region, an enabled scissor, or a rounded corner clip, then sets a full matrix per quad. Each quad is of typical duration for a trace event, and this is one of the most numerous events in the corpus, seen on every site, so the aggregate is real even though each quad is cheap.
Presence is the headline. SoftwareRenderer runs when there is no GPU compositing: blocklisted drivers, --disable-gpu, headless, a lost context, or a fallback after a GPU process crash. If these appear in a trace, that machine composited the page on the CPU, and nothing you conclude about compositor cost transfers to GPU users. That matters most for automated trace collection, where headless Chrome quietly takes the software path unless configured otherwise, and where no tool will warn you.
{
"name": "SoftwareRenderer::DoDrawQuad",
"cat": "viz",
"ph": "X",
"ts": 1102925575,
"dur": 611,
"tdur": 243,
"tts": 3203,
"pid": 8211,
"tid": 8266,
"args": {
}
}- categories
- viz
- usage
- very high
- duration
- typical
- references
- software_renderer.cc
ZeroCopyRasterBuffer::Playbacknot in DevTools recordingsnot in DevTools' model
Chrome draws one patch of the page straight into memory the graphics hardware can read.
A DevTools Performance recording does not contain this event. Record cc with your own trace configuration to get it.
DevTools has no entry for this event in its trace model, so the Performance panel shows its raw name with no label and no explanation.
ZeroCopyRasterBuffer::Playback records the raster of one tile straight into a GPU mappable buffer, from cc/raster/zero_copy_raster_buffer_provider.cc. Zero copy means cc maps the shared image, rasters into that memory, and never uploads a separate texture.
No arguments are recorded, so read the body of the function instead. If resource_has_previous_content_ is set the playback rect is intersected with raster_dirty_rect, so a re-rastered tile only repaints the damaged part. Otherwise the whole tile is repainted.
Reading these events tells you whether partial raster is working. A tile that keeps rastering at full size every frame is failing partial raster, usually because its content id changed wholesale rather than a sub rect. The source has a TODO saying partial raster is not implemented for GPU compositing, so on that path the dirty rect optimisation does not apply. Its duration is near identical to that of its parent RasterizerTaskImpl::RunOnWorkerThread, because playback is the whole raster task.
{
"name": "ZeroCopyRasterBuffer::Playback",
"cat": "cc",
"ph": "X",
"ts": 1102928326,
"dur": 30,
"tdur": 29,
"tts": 1424,
"pid": 8178,
"tid": 8209,
"args": {
}
}- categories
- cc
- usage
- high
- duration
- typical
- references
- zero_copy_raster_buffer_provider.cc
Frames, input and messaging
Long animation frames, input latency and postMessage.
AnimationFrame
Chrome builds and renders one animation frame, timed end to end by the Long Animation Frames API.
AnimationFrame records one Long Animation Frame, meaning one rendering frame measured end to end by the Long Animation Frames API. Phase s/f (async), which is why it is easy to miss: it will not show up if you are only scanning complete events.
trace_engine models it as a bare pairable async event with no args. Real traces populate args.animation_frame_timing_info:
duration_msis the whole frame.blocking_duration_msis the portion that blocked the main thread.num_scriptscounts the scripts involved.begin_frame_id(sequence_numberandsource_id) joins to the
compositor's frame events. This is LoAF surfaced in the trace, the successor to Long Tasks as the diagnostic behind INP, and it fixes the main weakness of the older metric: a slow interaction is often not one long task but several short ones inside a single frame, which Long Tasks misses entirely and LoAF catches.
Read blocking_duration_ms first. duration_ms includes time the frame legitimately spent waiting, while the blocking portion is what actually delayed a response.
num_scripts of zero on a long frame is a real finding: the frame was slow with no script involved, which points at style, layout or raster instead.
Join to AnimationFrame::Presentation by id for when the frame reached the screen.
{
"name": "AnimationFrame",
"cat": "devtools.timeline",
"ph": "b",
"ts": 1102906537,
"pid": 8257,
"tid": 8257,
"id2": {
"local": "0x6"
},
"args": {
"animation_frame_timing_info": {
"begin_frame_id": {
"sequence_number": 6,
"source_id": 4294967296
},
"blocking_duration_ms": 0,
"duration_ms": 16,
"num_scripts": 0
},
"id": "d9b88be5d0d73d6d"
}
}- categories
- devtools.timeline
- usage
- high
- undeclared args
args.animation_frame_timing_info.begin_frame_id.sequence_number,args.animation_frame_timing_info.begin_frame_id.source_id,args.animation_frame_timing_info.blocking_duration_ms,args.animation_frame_timing_info.duration_ms,args.animation_frame_timing_info.num_scripts,args.id- references
- animation_frame_timing_monitor.cc, begin_frame_args.h, chrome_track_event.proto, animation_frame_timing_info.h, animation_frame_timing_info.cc
AnimationFrame::Presentation
Chrome presented an animation frame to the screen.
AnimationFrame::Presentation marks the moment an animation frame was presented to the screen. Phase n (async instant).
args.id matches the AnimationFrame this frame belongs to. args.begin_frame_id matches the compositor's frame identity.
It closes the loop on AnimationFrame. That event tells you what the frame cost, and this one tells you when the user could see the result.
The gap between the two is presentation delay, which is the phase of INP most analysis ignores. A handler that finishes in well under a millisecond, on a frame that presents tens of milliseconds later, is not a slow handler. Optimising that JavaScript will achieve nothing. Real causes are a busy compositor, raster that has not finished, or simply waiting for the next display refresh.
begin_frame_id.sequence_number matches the compositor's own frame sequence, so this is the join between main-thread work and the frame pipeline in PipelineReporter and DroppedFrame.
{
"name": "AnimationFrame::Presentation",
"cat": "devtools.timeline",
"ph": "n",
"ts": 1102955679,
"pid": 8257,
"tid": 8257,
"id2": {
"local": "0x6"
},
"args": {
"begin_frame_id": {
"sequence_number": 6,
"source_id": 4294967296
},
"id": "d9b88be5d0d73d6d"
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.begin_frame_id.sequence_number,args.begin_frame_id.source_id- references
- animation_frame_timing_monitor.cc, begin_frame_args.h, frame_timing_details.h, animation_frame_timing_info.cc
HandlePostMessageOn message
Chrome runs the handler for a postMessage on the receiving side.
HandlePostMessage measures a postMessage handler running on the receiving side. Phase X, so its duration is the real cost of that handler.
args.data.traceId matches the SchedulePostMessage that sent the message.
Two numbers come out of the pair. The gap between the two events is transit plus queueing, meaning how long the message waited before anything ran. The duration of this event is the handler cost.
They fail differently. A long gap with a fast handler means the receiving thread was busy with something else, and optimising the handler is pointless. A short gap with a long handler is your code.
As trace events go these run slower than most, and the spread is wide, with the tail running far longer than the middle. That tail is where to look. They appeared on 15 of the 35 sites.
DevTools labels it "On message".
{
"name": "HandlePostMessage",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1109117136,
"dur": 113,
"tdur": 89,
"tts": 728203,
"pid": 8407,
"tid": 8407,
"args": {
"data": {
"traceId": "11003058272715615593"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- long
- references
- inspector_trace_events.cc, local_dom_window.cc, worker_global_scope.cc
InputHandlerProxy::HandleGestureFling::startedno longer emitted
The compositor started a fling, the inertial scroll that carries on after the finger lifts.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
InputHandlerProxy::HandleGestureFling::started marked the compositor thread beginning a fling, the inertial scroll that continues after the finger or trackpad lifts. The event no longer exists.
It was an async begin on category input,benchmark,rail in InputHandlerProxy::HandleGestureFlingStart(), ended from CancelCurrentFlingWithoutNotifyingClient(). args.vx and args.vy were the velocity components of the fling, because for years the compositor thread ran that decaying velocity curve itself.
It shipped last in Chrome 68. Chrome 69 moved fling handling to the browser process wholesale and deleted the function along with the trace event. Today input_handler_proxy.cc explicitly drops kGestureFlingStart and kGestureFlingCancel, commented "handled only in the browser process".
The replacement is FlingController::HandlingGestureFling on category input, from components/input/fling_controller.cc, still reporting vx and vy, with ProgressFling instants on the same track per generated scroll update.
Finding this name in a trace dates the trace to before Chrome 69.
Deleted in July 2018, so Chrome 69 and later never write it. Fling now runs in the browser process and is traced as FlingController::HandlingGestureFling.
- references
- fling_controller.cc, input_handler_proxy.cc
InputLatency::MouseMove
Chrome follows one mouse move through its input pipeline, from arrival to the frame that shows the result.
InputLatency::MouseMove covers one mouse move through Chrome's input pipeline, from the browser process receiving the event to the frame presenting its result. It is an async slice.
The name is generated rather than a literal. GetTraceNameFromType() in components/input/render_input_router_latency_tracker.cc expands every blink::WebInputEvent::Type into "InputLatency::" #t, so the family has one member per enum value, about forty of them. No literal "InputLatency::MouseMove" string exists anywhere in the tree.
Begin fires when INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT is added, backdated to the platform timestamp. End comes from LatencyInfo::Terminate() in ui/latency/latency_info.cc, and the payload is a typed ChromeLatencyInfo2 proto rather than JSON args. trace_id identifies the event, is_coalesced says whether it was merged into another, and a component list gives one timestamp per stage through to FRAME_SWAP. Coalesced moves terminate at ack and never reach a frame.
Gated on the category group benchmark,latencyInfo,rail,input.scrolling, which DevTools never requests. Use EventLatency for per-stage input analysis instead.
{
"name": "InputLatency::MouseMove",
"cat": "benchmark,latencyInfo,rail,input.scrolling",
"ph": "b",
"ts": 1104754827,
"pid": 8178,
"tid": 8178,
"id": "0x76",
"args": {
"chrome_latency_info": {
"component_info": [
{
"component_type": "COMPONENT_INPUT_EVENT_LATENCY_BEGIN_RWH",
"time_us": 1104755271
},
{
"component_type": "COMPONENT_INPUT_EVENT_LATENCY_ORIGINAL",
"time_us": 1104754827
},
{
"component_type": "COMPONENT_INPUT_EVENT_LATENCY_RENDERER_MAIN",
"time_us": 1104756056
}
],
"is_coalesced": false,
"trace_id": 2669798973887637500
}
}
}- categories
- benchmark, latencyInfo, rail, input.scrolling
- usage
- low
- undeclared args
args.chrome_latency_info.component_info[].component_type,args.chrome_latency_info.component_info[].time_us,args.chrome_latency_info.is_coalesced,args.chrome_latency_info.trace_id- references
- latency_info.cc, render_input_router_latency_tracker.cc, chrome_track_event.proto, latency_info.h
InputLatency::MouseWheel
Chrome follows one wheel event (not the scroll it causes) through its input pipeline to the screen.
InputLatency::MouseWheel covers one kMouseWheel event through Chrome's input pipeline, from the browser process receiving it to the frame presenting its result.
Emitted by components/input/render_input_router_latency_tracker.cc and gated on benchmark,latencyInfo,rail,input.scrolling, with a ChromeLatencyInfo2 payload rather than JSON args. The name is generated from the blink::WebInputEvent::Type enum, like every other InputLatency:: slice.
Wheel behaves unlike the rest of the family, because a wheel event is not a scroll. components/input/mouse_wheel_event_queue.cc queues the wheel, sends it to the renderer for a blocking wheel listener ack, and only then synthesises GestureScrollBegin and GestureScrollUpdate from it. One notch therefore produces InputLatency::MouseWheel for the DOM event plus a separate InputLatency::GestureScrollUpdate for the scroll it caused.
The jank a user feels lives in the second one. A passive wheel listener lets the queue generate the scroll without waiting on the ack, which is the whole point of passive: true.
Modern equivalent: EventLatency, from cc/metrics/event_latency_tracing_recorder.cc.
{
"name": "InputLatency::MouseWheel",
"cat": "benchmark,latencyInfo,rail,input.scrolling",
"ph": "b",
"ts": 1104956465,
"pid": 8178,
"tid": 8178,
"id": "0x81",
"args": {
"chrome_latency_info": {
"component_info": [
{
"component_type": "COMPONENT_INPUT_EVENT_LATENCY_BEGIN_RWH",
"time_us": 1104972852
},
{
"component_type": "COMPONENT_INPUT_EVENT_LATENCY_ORIGINAL",
"time_us": 1104956465
}
],
"is_coalesced": false,
"trace_id": 2669798973887637500
}
}
}SchedulePostMessageSchedule postMessage
This event tells you a postMessage has been sent.
SchedulePostMessage marks a postMessage being sent. Phase I.
args.data.traceId is the join key. Real traces also populate a full stackTrace and a sampleTraceId.
traceId matches the HandlePostMessage that receives this message. That pairing is what lets you measure the latency of a message crossing a boundary, and attribute work in a worker back to the code that asked for it.
The stackTrace is the sender, named. For a page using a Web Worker, this is how you follow a chain from application code, across the boundary, into the worker and back, rather than seeing two unrelated islands of activity.
DevTools labels it "Schedule postMessage" under a messaging category.
{
"name": "SchedulePostMessage",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1109114892,
"tts": 726339,
"pid": 8407,
"tid": 8407,
"s": "t",
"args": {
"data": {
"sampleTraceId": 5267982676864360,
"stackTrace": [
{
"columnNumber": 32,
"functionName": "",
"lineNumber": 158,
"scriptId": "89",
"url": "https://uk-script.dotmetrics.net/door.js?d=www.bbc.com&t=newsstudio"
},
{
"columnNumber": 21,
"functionName": "",
"lineNumber": 15,
"scriptId": "89",
"url": "https://uk-script.dotmetrics.net/door.js?d=www.bbc.com&t=newsstudio"
}
],
"traceId": "11003058272715615593"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- source_location.cc, v8-stack-trace-impl.cc, v8-debugger.cc, inspector_trace_events.cc, local_dom_window.cc, dedicated_worker.cc
Profiling and counters
Sampled CPU profiles, JIT bookkeeping and page counters.
CpuProfile
A saved CPU profile that DevTools has loaded in place of a trace.
CpuProfile is a synthetic event that trace_engine builds when DevTools loads a CPU profile instead of a trace. No Chrome writes it. SamplesIntegrator.createFakeTraceFromCpuProfile takes a CDP Profiler.Profile, for example a saved .cpuprofile file, and wraps it in a fake trace of one event, with the whole profile under args.data.cpuProfile.
Its doc comment is unusually explicit about this: a fake trace event created to support CDP.Profiler.Profiles, deliberately not extending the normal synthetic interface because there is no raw event behind it.
You will not find it in a trace that Chrome recorded. A profile inside a real trace arrives as Profile plus its ProfileChunks, and SamplesHandler reads both shapes into the same profile data. MetaHandler also counts the name, alongside TracingStartedInBrowser, as a sign that the data came from Chrome rather than from some other Trace Event producer.
DevTools builds this when it loads a CPU profile instead of a trace. It is never written to a trace file, so there is no sample to show.
- references
- SamplesIntegrator.ts
CpuProfiler::StartProfilingProfiling overhead
V8 is starting up its CPU profiler.
CpuProfiler::StartProfiling measures the profiler starting up. DevTools labels it "Profiling overhead" and its description is candid: time spent in an operation that only happens when the profiler is active.
That makes it one of the few events whose correct use is to be subtracted. It is measurement cost, not page cost. Anything attributed to it would not exist if you were not recording.
Surface it rather than hiding it, because it sets the noise floor for the trace. If profiling overhead is large relative to what you are investigating, the trace is not telling you much about real users.
{
"name": "CpuProfiler::StartProfiling",
"cat": "v8",
"ph": "X",
"ts": 1102929435,
"dur": 3025,
"tdur": 2557,
"tts": 32892,
"pid": 8257,
"tid": 8257,
"args": {
}
}- categories
- v8
- usage
- low
- duration
- very long
- references
- cpu-profiler.cc
EmbedderCallbackEmbedder callback
Android WebView is calling back into the app that hosts it.
EmbedderCallback measures time Android WebView spends calling back into the app that hosts it, rather than time spent on the page. Only Android WebView writes it, from ScopedEmbedderCallbackTask in android_webview/common/devtools_instrumentation.h, used by aw_contents.cc, aw_contents_client_bridge.cc and aw_contents_io_thread_client.cc. Desktop Chrome, Chrome on Android and other Chromium embedders do not write it. DevTools categorises it under scripting and describes it as time in the embedder of the WebView.
Its arguments are unmodelled.
Where it matters is an Android app that shows web content in a WebView, including in-app browsers built on WebView. There, time attributed here is the host application's, not the page's. If you are profiling a page inside someone else's Android app and the numbers do not add up, this event is where the missing time may be.
Only Android WebView writes this event. Every trace in the collection came from desktop Chrome.
- categories
Java,devtools, disabled-by-default-devtools.timeline
JitCodeAddedno longer emitted
V8 added a newly compiled piece of machine code.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
JitCodeAdded recorded a code map entry rather than a performance event. The old sampling profiler installed a v8::JitCodeEvent handler and forwarded every CODE_ADDED to the trace as metadata on disabled-by-default-v8.cpu_profile. It died with content/renderer/devtools/v8_sampling_profiler.cc in October 2016, so no current Chrome emits it.
Four fields sat under args.data. code_start gave the instruction address as a hex string. code_len gave the length. name used V8's mangled form, with a * prefix for optimized, ~ for optimizable and Script: for a whole script. script_id appeared when V8 had one. Its only consumer was DevTools, which used it to turn V8Sample addresses into call frames.
V8 still builds these events: JitLogger in v8/src/logging/log.cc populates CODE_ADDED for both JIT code and bytecode, and include/v8-callbacks.h still exposes SetJitCodeEventHandler. Nothing in Chromium routes that to tracing any more. The only remaining caller is the VTune JIT bridge, behind a build flag that is off in shipping Chrome.
Deleted in October 2016 with the legacy sampling profiler.
- references
- log.cc, v8-callbacks.h
JitCodeMovedno longer emitted
V8's garbage collector moved a piece of compiled code to a new address.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
JitCodeMoved recorded V8's GC relocating generated code. Chrome stopped emitting it in October 2016, when the legacy sampling profiler was deleted, and it appears in no modern trace.
Relocation invalidates any address-to-function map built from earlier CODE_ADDED events, so CODE_MOVED patched the map. Three fields sat under args.data: code_start, code_len and new_code_start. DevTools did exactly one thing with it: codeMap.moveEntry(code_start, new_code_start, code_len). A third sibling, JitCodeRemoved, gave code_start and code_len, and DevTools never read it at all. All of them went with JitCodeAdded and the profiler itself.
The problem they solved no longer exists. V8 symbolizes inside the profiler, and ProfileChunk ships cpuProfile.nodes with stable node ids and resolved callFrame objects, so a moving code address never reaches the trace consumer in the first place.
Deleted in October 2016 with the legacy sampling profiler.
JSSample
One sample from the V8 CPU profile, placed on the timeline alongside everything else.
JSSample is a synthetic event that trace_engine creates by expanding a sampled profile into one event per sample, so samples can sit on the same timeline as everything else. Each one gets args.data.stackTrace. The doc comment is one line: a JS Sample reflects a single sample from the V8 CPU Profile.
Its purpose is making sampled data joinable with instrumented data. A trace gives you exact durations for instrumented events like FunctionCall, and statistical attribution from sampling for everything inside them. JSSample is how the second gets laid against the first.
Remember what a sample is: evidence that a stack was on the CPU at one instant. Ten samples in a function is not ten milliseconds, it is ten observations at the profiler's interval. Treat sampled numbers as estimates with error bars, and instrumented durations as measurements.
DevTools builds this while parsing a trace. It is never written to a trace file, so there is no sample to show.
- references
- SamplesIntegrator.ts
Profile
V8 opens a CPU profile, with the start time its samples are counted from.
Profile opens a V8 CPU profile and acts as its header. Phase P (sample), with args.data.startTime and an optional source.
It records no samples itself. ProfileChunk events fill those in, and this event's id is what ties the chunks to a thread.
source matters more than it looks. Per SamplesHandler, several profiles can exist for the same thread from different origins: Internal when the Performance panel started it, Inspector when a user or automation started one via CDP. Merging them produces double-counted time, so the handler prioritises one source. If you are parsing profiles yourself, do the same.
{
"name": "Profile",
"cat": "disabled-by-default-v8.cpu_profiler",
"ph": "P",
"ts": 1102929272,
"tts": 32832,
"pid": 8257,
"tid": 8257,
"id": "0x1",
"args": {
"data": {
"startTime": 1102929267
}
}
}- categories
- disabled-by-default-v8.cpu_profiler
- usage
- low
- references
- profile-generator.cc, SamplesHandler.ts
ProfileChunk
V8 delivers a batch of CPU profile samples, with the part of the call tree they point into.
ProfileChunk records a batch of CPU profile samples. Phase P, and in real traces easily among the highest-volume events present.
args.data.cpuProfile.nodes is the call tree, each node with an id, a parent and a callFrame of functionName, scriptId, url, lineNumber, columnNumber and codeType. samples is an array of node ids. Alongside them sit timeDeltas, plus optional lines and columns.
The shape is the thing to understand. Samples are ids into a tree that is being built incrementally across chunks, and timeDeltas are gaps between consecutive samples, not absolute times. You reconstruct a timeline by accumulating the deltas from the parent Profile's startTime, and you reconstruct a stack by walking parent links up the node tree. Read a chunk in isolation and the ids will not resolve.
codeType distinguishes JS from other execution. Synthetic frames like (root), (program) and (idle) appear as real nodes and must be filtered before you attribute time to anyone's code.
This is the data behind the flame chart's bottom-up and call-tree views, and the only source of function-level attribution in a trace.
{
"name": "ProfileChunk",
"cat": "disabled-by-default-v8.cpu_profiler",
"ph": "P",
"ts": 1128567258,
"tts": 78923,
"pid": 8798,
"tid": 8857,
"id": "0x1",
"args": {
"data": {
"columns": [42718, 42718, 42718, 42718, 42718, 42718, 42718, 42718, "... [92 more items]"],
"cpuProfile": {
"nodes": [
{
"callFrame": {
"codeType": "JS",
"columnNumber": 42713,
"functionName": "n",
"lineNumber": 1,
"scriptId": 304,
"url": "https://dd.nytimes.com/tags.js"
},
"id": 3574,
"parent": 3570
},
{
"callFrame": {
"codeType": "JS",
"functionName": "setTimeout",
"scriptId": 0
},
"id": 3575,
"parent": 3574
},
{
"callFrame": {
"codeType": "JS",
"functionName": "setTimeout",
"scriptId": 0
},
"id": 3576,
"parent": 3570
}
],
"samples": [3573, 3573, 3573, 3573, 3573, 3573, 3573, 3573, "... [92 more items]"],
"trace_ids": {
"482": 3573,
"483": 3573,
"484": 3573,
"485": 3573,
"486": 3573,
"487": 3573,
"488": 3573,
"489": 3573,
"490": 3575,
"491": 3576,
"492": 3576,
"493": 3576,
"494": 3576,
"495": 3576,
"496": 3576,
"497": 3576,
"498": 3576,
"499": 3576,
"500": 3576,
"501": 3576,
"502": 3576,
"503": 3576,
"504": 3576,
"505": 3576,
"506": 3576,
"507": 3576,
"508": 3576,
"509": 3576,
"510": 3576,
"511": 3576,
"512": 3576,
"513": 3576,
"514": 3576,
"515": 3576,
"516": 3576,
"517": 3576,
"518": 3576,
"519": 3576,
"520": 3576,
"521": 3576,
"522": 3576,
"523": 3576,
"524": 3576,
"525": 3576,
"526": 3576,
"527": 3576,
"528": 3576,
"529": 3576,
"5289008964953680": 3576,
"5289008964953681": 3576,
"5289008964953682": 3573,
"5289008964953683": 3575,
"5289008964953684": 3576,
"5289008964953685": 3576,
"5289008964953686": 3576,
"5289008964953687": 3576,
"5289008964953688": 3573,
"5289008964953689": 3573,
"5289008964953692": 3573,
"5289008964953693": 3573,
"5289008964953694": 3573,
"5289008964953695": 3573,
"5289008964953738": 3576,
"5289008964953739": 3576,
"5289008964953760": 3576,
"5289008964953761": 3576,
"5289008964953762": 3576,
"5289008964953763": 3576,
"5289008964953764": 3576,
"5289008964953765": 3576,
"5289008964953766": 3576,
"5289008964953767": 3576,
"5289008964953768": 3576,
"5289008964953769": 3576,
"5289008964953770": 3576,
"5289008964953771": 3576,
"5289008964953772": 3576,
"5289008964953773": 3576,
"5289008964953774": 3576,
"5289008964953775": 3576,
"5289008964953776": 3576,
"5289008964953777": 3576,
"5289008964953778": 3576,
"5289008964953779": 3576,
"5289008964953780": 3576,
"5289008964953781": 3576,
"5289008964953782": 3576,
"5289008964953783": 3576,
"5289008964953784": 3576,
"5289008964953785": 3576,
"5289008964953786": 3576,
"5289008964953787": 3576,
"5289008964953788": 3576,
"5289008964953789": 3576,
"5289008964953790": 3576,
"5289008964953791": 3576
}
},
"lines": [2, 2, 2, 2, 2, 2, 2, 2, "... [92 more items]"],
"timeDeltas": [4, 13, 2, 10, 2, 10, 2, 9, "... [92 more items]"]
}
}
}- categories
- disabled-by-default-v8.cpu_profiler
- usage
- high
- undeclared args
args.data.cpuProfile.nodes[].callFrame.codeType,args.data.cpuProfile.nodes[].callFrame.functionName,args.data.cpuProfile.nodes[].callFrame.scriptId,args.data.cpuProfile.nodes[].id,args.data.cpuProfile.samples[],args.data.cpuProfile.trace_ids.2514011177137445,args.data.cpuProfile.nodes[].callFrame.columnNumber,args.data.cpuProfile.nodes[].callFrame.lineNumberand 79 more- references
- profile-generator.cc, SamplesIntegrator.ts, profiler-listener.cc, compiler.cc, log.cc, symbolizer.cc and 7 more
UpdateCounters
Chrome takes a periodic reading of page counters like heap size, DOM nodes, documents and event listeners.
UpdateCounters samples page-level counters on a periodic tick. Phase I, emitted continuously through a recording.
args.data reports jsHeapSizeUsed, nodes, documents and jsEventListeners, plus gpuMemoryLimitKB when present.
Because the event repeats, these are time series, not snapshots, which is what makes them far more useful than the attention they get:
jsHeapSizeUsedclimbing and never dropping across several GCs is a leaknodesclimbing steadily is DOM growth, which raises the cost of everyjsEventListenersclimbing without bound is a listener leak: handlersdocumentscounts live documents, so a climbing value means detached
signature.
subsequent style recalc and layout. It is also the number behind the DOM size insight.
attached on every render and never removed.
documents are being retained, usually by a reference from an iframe. Plotting these four against the timeline costs nothing and routinely finds problems no single event reveals.
{
"name": "UpdateCounters",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1102939550,
"tts": 39159,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"documents": 3,
"jsEventListeners": 0,
"jsHeapSizeUsed": 1187656,
"nodes": 40
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- high
- references
- inspector_trace_events.cc, instance_counters.h, document.cc, js_based_event_listener.cc, node.cc
v8.parseOnBackgroundStreaming compile task
V8 parses a script on a background thread while it is still downloading.
v8.parseOnBackground covers V8 parsing a script on a background thread while it is still downloading. Phase X.
Real traces put url and requestId under args.data, which is unusually convenient: requestId joins directly to the network events, so you can line background parse up against the request that produced it without guessing.
DevTools labels it "Streaming compile task", and its presence is good news. Streaming means parse overlapped download instead of starting after it. When streaming does not happen, v8.compile gives a notStreamedReason explaining why, and the cost lands on the main thread after the bytes have arrived.
So the useful check is comparative: for your large scripts, is there a v8.parseOnBackground, or is there a notStreamedReason instead?
{
"name": "v8.parseOnBackground",
"cat": "v8,devtools.timeline,disabled-by-default-v8.compile",
"ph": "X",
"ts": 1102942273,
"dur": 364,
"tdur": 355,
"tts": 944,
"pid": 8257,
"tid": 8272,
"args": {
"data": {
"requestId": "8257.2",
"url": "http://127.0.0.1:8801/mod.mjs"
}
}
}- categories
- v8, devtools.timeline, disabled-by-default-v8.compile
- usage
- moderate
- duration
- very long
- undeclared args
args.data.requestId,args.data.url- references
- script_streamer.cc, inspector_trace_events.cc, extension_script_streamer.cc
V8Sampleno longer emitted
Chromium's old sampling profiler recorded one stack sample, as raw code addresses.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
V8Sample recorded one raw stack sample from Chromium's renderer-side V8 sampling profiler, emitted by content/renderer/devtools/v8_sampling_profiler.cc under disabled-by-default-v8.cpu_profile. Note the missing trailing "r": that is not today's v8.cpu_profiler category. The module landed in December 2014 and was deleted in October 2016 by "Remove legacy V8 sampling profiler", so no current Chrome emits it. Sampling moved into V8 itself as Profile plus ProfileChunk on disabled-by-default-v8.cpu_profiler, emitted from v8/src/profiler/profile-generator.cc, streaming cpuProfile.nodes with resolved callFrame objects so no address fixup is needed.
Two fields sat under args.data. vm_state was one of js, gc, compiler, other, external or idle. stack was an array of raw instruction pointers as hex strings. Those addresses meant nothing alone. DevTools built a code map from JitCodeAdded and JitCodeMoved, resolved each address, and rewrote the sample as a JSSample.
V8Sample now exists only as an unreferenced constant in DevTools' event name enum.
Deleted in October 2016 with the renderer-side V8 sampling profiler. The collection recorded Profile and ProfileChunk instead.
- references
- profile-generator.cc
Trace metadata
Threads, frames, workers and screenshots.
domLoading
This event tells you the document object now exists and has entered its loading state, so parsing can begin.
domLoading marks the document's readyState reaching loading, which is the point at which the document object exists and parsing can begin. It is a TRACE_EVENT_MARK_WITH_TIMESTAMP1 from DocumentTiming::MarkDomLoading in core/dom/document_timing.cc, on blink.user_timing,rail, with a single arg frame.
It is still emitted in current Chromium, unlike the Navigation Timing attribute it is named after. domLoading was dropped from PerformanceNavigationTiming in Navigation Timing Level 2 and survives in the API only on the deprecated performance.timing. The trace mark stayed, because Blink keeps the whole family in that one file: domInteractive, domContentLoadedEventStart, domContentLoadedEventEnd, domComplete.
Do not go looking for it in the Timings track. DevTools lists it among the ignored nav timing names in UserTimingsHandler.ts precisely to keep it out. Read it from the raw trace, one per frame per document.
{
"name": "domLoading",
"cat": "blink.user_timing,rail",
"ph": "R",
"ts": 1102925859,
"tts": 29962,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7"
}
}- categories
- blink.user_timing, rail
- usage
- moderate
- references
- document_timing.cc
FrameStartedLoadingFrame started loading
Chrome switches a frame's loading state on, which is the spinner starting to turn.
FrameStartedLoading fires when a frame's loading state turns on, which is the spinner, not the parser. It is a TRACE_EVENT_INSTANT on devtools.timeline from InspectorTraceEvents::FrameStartedLoading in core/inspector/inspector_trace_events.cc, driven by the probe::FrameStartedLoading hook.
The frame id is a top-level arg, args.frame, not args.data.frame. It is the DevTools frame token, the same string TracingStartedInBrowser, FrameCommittedInBrowser and navigationStart use, so it joins across them.
The call site is ProgressTracker::ProgressStarted in core/loader/progress_tracker.cc, guarded by if (!frame_->IsLoading()). You get one per frame per load, at navigation start, before any response byte, and nothing at all if the frame was already loading. ProgressCompleted emits the matching FrameStoppedLoading.
DevTools gives it the label "Frame started loading" but marks it hidden, so you only ever see it in the raw JSON.
{
"name": "FrameStartedLoading",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102924537,
"tts": 29174,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7"
}
}- categories
- devtools.timeline
- usage
- low
- undeclared args
args.frame- references
- inspector_trace_events.cc, progress_tracker.cc
MetaCharsetCheck
Chrome checks which character encoding the document declares.
MetaCharsetCheck fires when Chrome checks the document's character encoding declaration. Phase I, with args.data.disposition and frame.
disposition records what was found: a declaration in a meta tag, one from the HTTP header, or none, in which case Chrome sniffs.
It matters more than it sounds. If the encoding is not declared early, the parser may have to restart once it discovers the document is not the encoding it assumed, throwing away parsing already done. Consumed by PageLoadMetricsHandler, which is where that cost would surface.
The fix is old advice that still holds: declare the charset in the first 1024 bytes of the document, or in the Content-Type header.
No trace in the collection produced this event.
- categories
- devtools.timeline
- usage
- moderate
- references
- decoded_data_document_parser.cc
ParseMetaViewport
Chrome parses the viewport meta tag and records the value it was given.
ParseMetaViewport marks the viewport meta tag being parsed. Phase I, with args.data.content, the literal attribute value, plus node_id and frame.
Having the raw string is what makes it useful. user-scalable=no and maximum-scale=1 are accessibility failures that are trivially detectable here, and a missing or malformed viewport explains an entire class of mobile layout problems at a stroke.
Its consumer is UserInteractionsHandler, and the reason is tap handling. On a page Chrome does not consider mobile-optimized, double-tap-to-zoom stays enabled and a tap is held back by the double-tap timeout, 300ms on Android from Android's own ViewConfiguration. Since Chrome 32 a mobile-optimized viewport disables double-tap zoom outright, so taps dispatch immediately.
Chrome counts a page as mobile-optimized if the content already fits the viewport, or the page scale is fixed, or the meta viewport says so (cc/trees/mobile_optimized_viewport_util.cc). So this tag directly affects measured interaction latency.
{
"name": "ParseMetaViewport",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1108345866,
"tts": 41386,
"pid": 8407,
"tid": 8407,
"s": "t",
"args": {
"data": {
"content": "width=device-width, initial-scale=1",
"frame": "2EA8810E92C12C680C55B2941251AA98",
"node_id": 5
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- rare
- references
- mobile_optimized_viewport_util.cc, html_meta_element.cc
RenderFrameImpl::createChildFrame
Chrome creates a child frame, which in practice means an iframe coming into existence.
RenderFrameImpl::createChildFrame records a child frame being created, which means an iframe coming into existence. Phase is untyped, and args has two fields, frame_token and child_frame_token.
Those two tokens are a parent-child edge, so a trace's worth of these reconstructs the frame tree as it was built rather than as it was at recording start.
Its consumer is the surprise: LayoutShiftsHandler. The reason is sound. An iframe appearing is a classic shift cause, and shifts inside a child frame need attributing to the parent that created it. Ads and embeds are the usual offenders.
{
"name": "RenderFrameImpl::createChildFrame",
"cat": "navigation,rail",
"ph": "X",
"ts": 1108493846,
"dur": 901,
"tdur": 842,
"tts": 150808,
"pid": 8407,
"tid": 8407,
"args": {
"child_frame_token": "579F80F6245DF32A5438B7C3C5DF03C3",
"frame_token": "2222CA87E8277AFC343EE54482341A96"
}
}- categories
- navigation, rail
- usage
- low
- duration
- very long
- references
- render_frame_impl.cc, frame.cc
Screenshot
Chrome captures a picture of the viewport, carried in the event as a JPEG.
Screenshot is one captured frame of the viewport. args.dataUri is a base64 JPEG.
Note the interface name: LegacySyntheticScreenshot. DevTools wraps the raw event, and the "legacy" marks an older capture path still present in traces.
Keep two practical points in mind when you read them. Screenshots are sampled, not per-frame, typically tens of milliseconds apart, so the frame nearest a metric is a neighbour of the moment you care about, not the moment itself. Never present one as "the frame in which X happened".
And they are scaled down, uniformly. That is useful. Because the scale is uniform, one factor (rendered width divided by viewport width) maps trace rectangles such as LayoutShift.impacted_nodes onto the image exactly, which is how you draw a shift onto a real screenshot.
They are also a large contributor to trace file size. Recording without them produces noticeably smaller traces.
{
"name": "Screenshot",
"cat": "disabled-by-default-devtools.screenshot",
"ph": "I",
"ts": 1102955957,
"tts": 202801,
"pid": 8178,
"tid": 8178,
"s": "t",
"args": {
"expected_display_time": 1102955880,
"frame_sequence": 6,
"snapshot": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgo... [1500 more chars]",
"source_id": 4294967296
}
}- categories
- disabled-by-default-devtools.screenshot
- usage
- moderate
- undeclared args
args.expected_display_time,args.frame_sequence,args.snapshot,args.source_id- references
- tracing_handler.cc, begin_frame_args.h, traced_value_support.h, devtools_traceable_screenshot.cc
SyntheticNetworkRequestsynthetic
One whole network request, as DevTools assembles it from the separate events Chrome records.
DevTools builds this event while parsing a trace. You will not find it in a trace file.
SyntheticNetworkRequest is constructed by DevTools while it parses a trace, and it never appears in a trace file. DevTools assembles it from the five raw network events (ResourceWillSendRequest, ResourceSendRequest, ResourceReceiveResponse, ResourceReceivedData, ResourceFinish) correlated by requestId, producing one object per request with timing phases resolved.
If you are reading a raw trace you must do that correlation yourself. There is no single "request" event in the file.
NetworkRequestsHandler also notes one thing that catches people out: its index is URL to requestId[], plural, because the same URL can be requested several times in one trace. Keying your own analysis by URL rather than by request id will silently merge distinct requests.
It is also consumed by the third-party insight, which classifies requests by origin to attribute cost to code the site does not control.
DevTools builds this while parsing a trace. It is never written to a trace file, so there is no sample to show.
- references
- NetworkRequestsHandler.ts
thread_name
This event tells you the name of one thread.
thread_name is a metadata record that declares the name of one thread. Phase M, and args.name is the name itself.
These records have no timing and no payload beyond that name, and nothing else in the trace replaces them. Every other event identifies its thread only by pid/tid, so without these you can measure per-thread work but cannot say which thread. The names you will care about are CrRendererMain (the main thread), Compositor, CrBrowserMain, CrGpuMain, CompositorTileWorker (raster), and DedicatedWorker thread.
Use them to build the thread map before you interpret anything else, and mind two traps. A trace contains several CrRendererMain threads, one per renderer process, so "the main thread" is only meaningful once you know which renderer owns the page. Metadata events can also appear anywhere in the file rather than at the start, so build the map in a full pass before resolving anything.
RendererHandler uses these to build the per-thread event hierarchy that the flame chart is drawn from.
{
"name": "thread_name",
"cat": "__metadata",
"ph": "M",
"ts": 0,
"pid": 0,
"tid": 0,
"args": {
"name": "swapper"
}
}- categories
- __metadata
- usage
- moderate
- references
- export_json.cc, export_json.cc
TracingSessionIdForWorker
Chrome ties a worker to the page and the script that started it.
TracingSessionIdForWorker ties a worker to the page that created it. Phase I, and args.data has four fields: workerId, workerThreadId, url and frame.
Without it, worker activity is an unlabelled thread doing unattributable work. With it you can say which script the worker runs and which frame started it, which is the difference between knowing that some thread was busy and knowing which of your workers was busy.
WorkersHandler exists for this. On pages that move work off the main thread deliberately, this event is what lets you confirm the work actually moved rather than merely being duplicated.
{
"name": "TracingSessionIdForWorker",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1184930633,
"tts": 1332,
"pid": 9564,
"tid": 9595,
"s": "t",
"args": {
"data": {
"frame": "8343B9DBEF932C61609598ED6CF43CDA",
"url": "blob:https://stackoverflow.com/7720b4e0-d93d-4be0-9f29-6522c3295349",
"workerId": "1540A612C89860B1E86EF468DC102834",
"workerThreadId": 9595
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- low
- references
- worker_inspector_controller.cc, WorkersHandler.ts, inspector_trace_events.cc, dedicated_worker.cc, worker_thread.cc
TracingStartedInBrowser
Chrome's browser process announces that trace recording has started.
TracingStartedInBrowser declares that recording has started, and the browser process emits it. Phase I.
args.data has three fields: frameTreeNodeId, persistentIds and frames. frames is an array describing the frame tree at the moment recording began, giving each frame's id, its URL, its name and its parent.
That snapshot is the seed for frame identity. Nearly every event identifies its frame with an opaque frame hash and nothing else, and this is where those hashes first get attached to URLs. Skip it and you can tell that some frame did something, without being able to say which.
persistentIds indicates whether frame ids remain stable across navigations, which determines whether you can follow a frame through a navigation or must treat it as new.
{
"name": "TracingStartedInBrowser",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1102915651,
"tts": 179472,
"pid": 8178,
"tid": 8178,
"s": "t",
"args": {
"data": {
"frameTreeNodeId": 2,
"frames": [
{
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"isInPrimaryMainFrame": true,
"isOutermostMainFrame": true,
"name": "",
"processId": 8257,
"url": "about:blank"
}
],
"persistentIds": true
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- rare
- undeclared args
args.data.frames[].frame,args.data.frames[].isInPrimaryMainFrame,args.data.frames[].isOutermostMainFrame,args.data.frames[].name,args.data.frames[].processId,args.data.frames[].url- references
- tracing_handler.cc, MetaHandler.ts, inspector_trace_events.cc
TracingStartedInPageno longer emitted
The renderer used to announce that a DevTools recording had started.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
TracingStartedInPage declared the start of a DevTools recording from inside the renderer. Blink's InspectorTracingAgent emitted it on disabled-by-default-devtools.timeline as a thread-scoped instant. Its final payload was args.data, whose fields were sessionId, page (the frame id of the local frame root), persistentIds: true and a frames array.
It is gone. The emitter is present in branch-head 3497 (Chrome 69) at core/inspector/inspector_tracing_agent.cc and absent in 3538 (Chrome 70). The browser process took the job over with TracingStartedInBrowser, emitted by TracingHandler::EmitFrameTree in content/browser/devtools/protocol/tracing_handler.cc.
Treat it as a version stamp. The only thing the modern MetaHandler does with it is count it, alongside TracingStartedInBrowser and CpuProfile, as proof the trace came from Chrome rather than some other Trace Event producer.
Removed in Chrome 70, when the browser process took over with TracingStartedInBrowser. The collection recorded TracingStartedInBrowser on every site.
- references
- tracing_handler.cc, inspector_tracing_agent.cc
UserTiming::Measure
One of your own performance.measure() calls from the page, recorded in the trace.
UserTiming::Measure records a performance.measure() call made by the page.
This is your own instrumentation, surfacing in the trace alongside Chrome's. Its value is that it is the only event in the file that knows what your application considers a meaningful unit of work: "cart updated", "route rendered", "search results painted".
Chrome measures what the browser did. User timing measures what your application thinks it did. Correlating the two is how you turn a layout cost into a statement about which part of your application paid it.
Per the UserTimingsHandler source there is a UserTimings.md alongside it in trace_engine documenting the parsing rules. Read that if you are implementing this yourself.
{
"name": "UserTiming::Measure",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1103071587,
"dur": 8,
"tdur": 8,
"tts": 137154,
"pid": 8257,
"tid": 8257,
"args": {
"sampleTraceId": 3744044438,
"traceId": 3744044438
}
}- categories
- devtools.timeline
- usage
- moderate
- duration
- typical
- references
- performance_user_timing.cc
Tasks, fonts and hints
The event loop, async plumbing, web fonts and resource hints.
AsyncTaskAsync taskno longer emitted
Chrome linked an async task being scheduled to the moment it later ran.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
AsyncTask was Blink's flow event tying the scheduling of an async task to the task that eventually ran, until Chromium deleted it in January 2021. AsyncTaskScheduled in core/probe/core_probes.cc wrote a TRACE_EVENT_FLOW_BEGIN named AsyncTask on devtools.timeline.async, with args.data.name set to the probe name and the task pointer as the flow id. Running the task wrote a flow step for a recurring task and a flow end otherwise, and cancelling it wrote a flow end. Only DevTools rendered it.
The change "[devtools] Remove devtools.timeline.async category" removed the events and the whole category. Today Blink writes AsyncTask Scheduled from core/probe/async_task_context.cc and AsyncTask Run from core/probe/core_probes.cc, both on the plain blink category using Perfetto flows, and those two names do show up in current traces.
What you will actually find in a DevTools trace is V8's pair: v8::Debugger::AsyncTaskScheduled and v8::Debugger::AsyncTaskRun on disabled-by-default-v8.inspector, from v8/src/inspector/v8-debugger.cc, which put taskName in a top-level argument and join schedule to run with a process-scoped flow.
Removed in January 2021. The collection recorded AsyncTask Scheduled and AsyncTask Run on the blink category.
- references
- core_probes.cc, async_task_context.cc, v8-debugger.cc
BeginRemoteFontLoad
Chrome has started downloading a web font.
BeginRemoteFontLoad fires when a web font starts downloading. Phase X, with args.url, args.id and args.display.
display is the CSS font-display value in effect, observed directly rather than inferred from the stylesheet. swap, block, optional and fallback each imply a different visual outcome, and this is the trace telling you which one Chrome actually applied. id joins to RemoteFontLoaded.
Read display first, because that one value decides whether a slow font blocks text or swaps it, and everything else you conclude about the font hangs off it. Its consumer is LayoutShiftsHandler, which is the point. font-display: swap means text paints in a fallback face and then reflows when the real font arrives, which is a layout shift by design. optional avoids the shift. block trades the shift for text the user cannot read while the download runs.
So an entry here with display: swap, followed by a shift affecting text nodes, is a font-swap shift with the mechanism documented in the trace rather than guessed at. An entry with display: block and a long gap to its matching RemoteFontLoaded is the other failure, invisible text, and it costs you nothing in CLS while costing the reader everything.
{
"name": "BeginRemoteFontLoad",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1108361321,
"dur": 86,
"tdur": 84,
"tts": 54919,
"pid": 8407,
"tid": 8407,
"args": {
"display": "block",
"id": 188,
"url": "https://static.files.bbci.co.uk/fonts/reith/2.512/BBCReithSans_W_Md.woff2"
}
}- categories
- devtools.timeline
- usage
- low
- duration
- typical
- references
- remote_font_face_source.cc, font_resource.cc, resource.h, identifiers_factory.cc, css_font_face.cc
CancelIdleCallbackCancel idle callback
Chrome cancels an idle callback your page had queued.
CancelIdleCallback fires on a cancelIdleCallback() call. It is an instant event on devtools.timeline from ScriptedIdleTaskController::CancelCallback in core/scheduler/scripted_idle_task_controller.cc.
args.data is {id, frame}, built by GenericIdleCallbackEvent in core/inspector/inspector_trace_events.cc, the same shape shared with RequestIdleCallback and FireIdleCallback. The id is what matches the three up. SetCallStack adds sampleTraceId and stackTrace when disabled-by-default-devtools.timeline.stack is enabled, which DevTools does by default, so you usually get the calling site too.
One trap: the event is emitted before IsValidCallbackId(id) and before the task is looked up, so cancelling a bogus or already-fired id still records an event.
The pattern to hunt for is a RequestIdleCallback whose id is followed by a cancel and never a FireIdleCallback.
{
"name": "CancelIdleCallback",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1169242252,
"tts": 1628869,
"pid": 9307,
"tid": 9307,
"s": "t",
"args": {
"data": {
"frame": "3037F57D3EE99AA57D01190FF40E7359",
"id": 11,
"sampleTraceId": 6501970321791987,
"stackTrace": [
{
"columnNumber": 13690,
"functionName": "cancelScheduledFlush",
"lineNumber": 2,
"scriptId": "20",
"url": "https://github.githubassets.com/assets/app-runtime-79275629e212a8de.js"
},
{
"columnNumber": 13058,
"functionName": "flushBatch",
"lineNumber": 2,
"scriptId": "20",
"url": "https://github.githubassets.com/assets/app-runtime-79275629e212a8de.js"
},
{
"columnNumber": 12007,
"functionName": "boundFlush",
"lineNumber": 2,
"scriptId": "20",
"url": "https://github.githubassets.com/assets/app-runtime-79275629e212a8de.js"
}
]
}
}
}- categories
- devtools.timeline
- usage
- moderate
- undeclared args
args.data.frame,args.data.id,args.data.sampleTraceId,args.data.stackTrace[].columnNumber,args.data.stackTrace[].functionName,args.data.stackTrace[].lineNumber,args.data.stackTrace[].scriptId,args.data.stackTrace[].url- references
- scripted_idle_task_controller.cc, inspector_trace_events.cc, source_location.cc, v8-stack-trace-impl.cc, v8-debugger.cc
LinkPreconnect
Chrome acts on a <link rel="preconnect"> and opens a connection to that origin early.
LinkPreconnect fires when Chrome acts on a <link rel="preconnect">. Phase I, with args.data.url and node_id.
Use these to verify preconnects rather than trust them. Preconnect is easy to get wrong: pointing at the wrong origin, missing crossorigin for fonts, or preconnecting to an origin nothing subsequently requests. This event tells you which preconnects Chrome actually performed, and the network events tell you which origins were actually used.
The diff is the finding. A preconnect with no matching request wasted a connection. A heavily used origin with no preconnect is a missed opportunity, visible as DNS and connect time on its first request.
{
"name": "LinkPreconnect",
"cat": "disabled-by-default-devtools.timeline",
"ph": "I",
"ts": 1108346393,
"tts": 41725,
"pid": 8407,
"tid": 8407,
"s": "t",
"args": {
"data": {
"frame": "2EA8810E92C12C680C55B2941251AA98",
"node_id": 6,
"url": "https://www.bbc.com/"
}
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- low
- references
- html_link_element.cc, identifiers_factory.cc
PreloadRenderBlockingStatusChange
This event tells you a preloaded resource has changed whether it blocks rendering.
PreloadRenderBlockingStatusChange marks a preloaded resource changing its render-blocking status. Phase I, with args.data.requestId, url and renderBlocking. It joins to the request by requestId.
The scenario is this. A resource is preloaded, then later discovered by the parser in a position that makes it render-blocking, or its blocking status is otherwise revised. Chrome records the change rather than only the final state.
That matters because renderBlocking on ResourceSendRequest is the status at request time. If it changed afterwards, only this event knows. Any analysis that reads render-blocking status once and never revisits it can be wrong on exactly the resources you most need to understand.
{
"name": "PreloadRenderBlockingStatusChange",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1109023633,
"tts": 21706,
"pid": 8475,
"tid": 8475,
"s": "t",
"args": {
"data": {
"renderBlocking": "in_body_parser_blocking",
"requestId": "8475.2",
"url": "https://cdn.privacy-mgmt.com/Notice.1c267.css"
}
}
}- categories
- devtools.timeline
- usage
- moderate
- references
- resource_load_observer_for_frame.cc, resource_fetcher.cc, inspector_trace_events.cc, render_blocking_behavior.h, preload_helper.cc
ProgramOtherno longer emitted
Chrome wrapped one task on the renderer main thread's message loop.
Chrome no longer emits this event. The code that wrote it was found in the history and then found to have been deleted.
Program bracketed one renderer main thread message loop task, as a bare begin/end duration pair with no arguments whatsoever. Blink emitted it from WebDevToolsAgentImpl::willProcessTask and didProcessTask on disabled-by-default-devtools.timeline. That was its entire job.
Blink removed it in May 2015, in "Timeline: use toplevel event category instead of Program record to mark top-level task". Its successor is RunTask, emitted on the same category from base/task/sequence_manager/thread_controller_with_message_pump_impl.cc. DevTools keeps a Program entry labelled "Other" only so that decade-old trace files still render.
The V8 profiler frame is a separate mechanism. (program) is CodeEntry::kProgramEntryName in v8/src/profiler/profile-generator.cc, a synthetic frame substituted when a sample's vm_state is OTHER, EXTERNAL, IDLE_EXTERNAL or LOGGING. It arrives inside ProfileChunk node data, never as an event name. Treating the two as the same thing will send you hunting for scripting time that was never there.
Removed in May 2015. The collection recorded RunTask, its successor.
RemoteFontLoaded
Chrome has finished downloading a web font.
RemoteFontLoaded fires when a web font download has completed. Phase X, with args.url and args.name, the resolved font family name as Blink knows it.
Having the real family name is more useful than it sounds. It survives the indirection of @font-face blocks, CSS variables and font stacks, so you can say which face actually loaded rather than which one you think you asked for.
Pair it with BeginRemoteFontLoad and measure the gap. That gap is the window during which text either was invisible or was showing a fallback, depending on font-display. For a text LCP that window is frequently the thing gating the metric, since text cannot paint in a font that has not arrived.
LayoutShiftsHandler consumes this event too, for the swap-induced reflow. One slow font can therefore cost you twice, once in LCP while the download runs and again in CLS when the text reflows into the face that finally arrived.
{
"name": "RemoteFontLoaded",
"cat": "devtools.timeline",
"ph": "X",
"ts": 1108495652,
"dur": 7,
"tdur": 3,
"tts": 152399,
"pid": 8407,
"tid": 8407,
"args": {
"name": "彿",
"url": "https://static.files.bbci.co.uk/fonts/reith/2.512/BBCReithSans_W_Bd.woff2"
}
}- categories
- devtools.timeline
- usage
- low
- duration
- short
- references
- css_font_face.cc, font_custom_platform_data.cc, remote_font_face_source.cc
RunTaskTask
Chrome runs one task on a thread's event loop.
RunTask wraps one task on a thread's event loop. Phase X, and it is the outermost container for nearly everything else on the main thread.
It has no arguments at all. Its value is entirely structural: every FunctionCall, Layout, ParseHTML and PrePaint is nested inside one, so RunTask is how you attribute work to a task and measure the task's total cost rather than the cost of its parts.
It is also the most numerous event in a trace by a wide margin, mostly very short. Filtering to tasks over 50ms gives you long tasks, the definition behind TBT: every task over 50ms contributes its excess above 50ms to Total Blocking Time.
A long task blocks input for its entire duration, which is the whole reason INP suffers on busy pages. But note the modern refinement: several short tasks inside one frame can ruin an interaction without any single task crossing 50ms, which is exactly the gap AnimationFrame and LoAF exist to close.
WarningsHandler consumes it to flag long tasks, and FramesHandler uses it to decide which work belongs to which frame.
{
"name": "RunTask",
"cat": "disabled-by-default-devtools.timeline",
"ph": "X",
"ts": 1102913025,
"dur": 20,
"tdur": 18,
"tts": 2325,
"pid": 8211,
"tid": 8262,
"args": {
}
}- categories
- disabled-by-default-devtools.timeline
- usage
- very high
- duration
- typical
- references
- thread_controller_with_message_pump_impl.cc
TimeStampTimestamp
Chrome records a console.timeStamp() call made by your code.
TimeStamp marks a console.timeStamp() call. Phase untyped. args.data always has message and name, and optionally adds start, end, track, trackGroup, color and devtools.
Those last fields are the modern extended API, and they do considerably more than the event name suggests. console.timeStamp() can place a named, coloured entry on its own custom track in the Performance panel, with an explicit start and end. It is no longer just a marker.
That makes it the cheapest way to get application-level structure into a trace. Your router, your hydration phases, your data fetches can each appear as their own track next to Chrome's, without any external tooling.
Parsed by UserTimingsHandler, alongside performance.measure().
{
"name": "TimeStamp",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1102936210,
"tts": 37738,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"message": "fixture-stamp",
"name": "fixture-stamp",
"sampleTraceId": 6909218608594394
}
}
}- categories
- devtools.timeline
- usage
- low
- undeclared args
args.data.frame- references
- inspector_trace_events.cc, v8-console.cc, thread_debugger_common_impl.cc
v8::Debugger::AsyncTaskRun
V8 runs an async continuation that was queued earlier.
v8::Debugger::AsyncTaskRun records a scheduled async continuation actually running. Phase X, in the disabled-by-default-v8.inspector category.
AsyncJSCallsHandler consumes it, pairing each run with the v8::Debugger::AsyncTaskScheduled that queued it. Its job is exactly that: reconstructing a logical async call chain from two events that are structurally unrelated in the event tree.
The gap between scheduled and run is queueing delay for that continuation. A promise that resolves immediately but whose continuation runs 200ms later was not slow to resolve; the thread was busy. That distinction matters, because the fix for one is optimising the promise and the fix for the other is unblocking the thread.
Requires disabled-by-default-v8.inspector.
{
"name": "v8::Debugger::AsyncTaskRun",
"cat": "disabled-by-default-v8.inspector",
"ph": "X",
"ts": 1102940419,
"dur": 12,
"tdur": 10,
"tts": 39920,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"sampleTraceId": 1
}
}
}- categories
- disabled-by-default-v8.inspector
- usage
- very high
- duration
- typical
- undeclared args
args.data.sampleTraceId- references
- v8-debugger.cc, trace-id.h
v8::Debugger::AsyncTaskScheduled
V8 queues an async continuation to run later.
v8::Debugger::AsyncTaskScheduled marks an async continuation being scheduled inside V8. Phase X, in the disabled-by-default-v8.inspector category, and in real traces two arguments appear: args.taskName and args.data.sampleTraceId.
taskName is a short label for the kind of async work: a promise, a timer, an event. Observed values include framework and platform names such as pagereveal.
This is the event that makes await traceable. Ordinary containment breaks at an async boundary because the continuation runs in a later, unrelated task. These events, paired with AsyncTaskRun by id, are the link across that gap, and they are numerous: over a thousand in a single ordinary page load.
If you have ever lost a causal chain at an await and concluded the trace could not answer the question, this is the pair that answers it.
{
"name": "v8::Debugger::AsyncTaskScheduled",
"cat": "disabled-by-default-v8.inspector",
"ph": "X",
"ts": 1102925977,
"dur": 3,
"tdur": 2,
"tts": 30075,
"pid": 8257,
"tid": 8257,
"args": {
"data": {
"sampleTraceId": 0
},
"taskName": "pagereveal"
}
}- categories
- disabled-by-default-v8.inspector
- usage
- high
- duration
- short
- undeclared args
args.data.sampleTraceId- references
- v8-debugger.cc, trace-id.h
WebSocketReceiveHandshakeResponse
Chrome has received the server's reply to a WebSocket handshake.
WebSocketReceiveHandshakeResponse fires when the server's handshake response arrives and the WebSocket connection is open. Phase I.
Its arguments are the same WebSocketInfo shape as WebSocketSendHandshakeRequest, and identifier matches the two up.
The gap between the send and this response is connection establishment latency for the socket, which is a real user-facing number on anything realtime. A chat or live-updating page cannot show anything until this completes.
Consumed by NetworkRequestsHandler and InitiatorsHandler, so the socket appears in the network picture rather than being invisible to it, which is a common blind spot in performance tooling.
{
"name": "WebSocketReceiveHandshakeResponse",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1103043996,
"tts": 134076,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"identifier": 5,
"sampleTraceId": 6909218608593087
}
}
}- categories
- devtools.timeline
- usage
- rare
- undeclared args
args.data.sampleTraceId- references
- inspector_websocket_events.cc, unique_identifier.cc, websocket_channel_impl.cc
WebSocketSendHandshakeRequest
Chrome sends the opening handshake asking a server to upgrade the connection to a WebSocket.
WebSocketSendHandshakeRequest records a WebSocket opening handshake being sent, which is the HTTP upgrade request that precedes a WebSocket connection.
args.data always has identifier and url, and optionally frame and workerId. identifier is the connection id that ties handshake, frames and close together. workerId matters because sockets are frequently opened from workers, and this is how you attribute one to the worker rather than the page.
It shares an interface with its response counterpart, one WebSocketInfo shape under two wire names. That is one of the five alias cases in trace_engine's enum, and a reason the enum count overstates the number of distinct events.
{
"name": "WebSocketSendHandshakeRequest",
"cat": "devtools.timeline",
"ph": "I",
"ts": 1103042994,
"tts": 133286,
"pid": 8257,
"tid": 8257,
"s": "t",
"args": {
"data": {
"frame": "14A831DCB73EB1205B67CDB89FC6EBD7",
"identifier": 5,
"sampleTraceId": 6909218608593036
}
}
}- categories
- devtools.timeline
- usage
- rare
- undeclared args
args.data.frame,args.data.identifier,args.data.sampleTraceId- references
- inspector_websocket_events.cc, unique_identifier.cc, websocket_channel_impl.cc
Trace categories
Every event is filed under one or more categories, and the categories switched on when tracing starts decide what ends up in the file. An event is recorded when any one of its categories is on.
devtools.timeline
devtools.timeline is the category Blink's inspector instrumentation writes its main timeline events to: ParseHTML, UpdateLayoutTree, Layout, Paint, EventDispatch, FunctionCall, TimerFire, and the network events from ResourceSendRequest to ResourceFinish. Page milestones such as CommitLoad, MarkDOMContent and MarkLoad are filed here too, as are EventTiming and AnimationFrame. RunTask is not; it is in disabled-by-default-devtools.timeline.
DevTools records this category on every Performance panel recording.
- references
- inspector_trace_events.h
disabled-by-default-devtools.timeline
disabled-by-default-devtools.timeline records the lower level half of the timeline: RunTask around each scheduled task, the compositor's Commit and RasterTask, image decoding (Decode Image, ImageDecodeTask, PaintImage), and invalidation markers such as InvalidateLayout and ScheduleStyleRecalculation. It also writes TracingStartedInBrowser, which maps frame ids to URLs, and UpdateCounters with DOM node and JS heap counts.
DevTools records this category on every Performance panel recording.
disabled-by-default-devtools.timeline.frame
disabled-by-default-devtools.timeline.frame records the compositor's frame bookkeeping: BeginFrame, NeedsBeginFrameChanged, RequestMainThreadFrame, BeginMainThreadFrame, ActivateLayerTree, DrawFrame and DroppedFrame. PipelineReporter and its stage spans, from BeginImplFrameToSendBeginMainFrame to SwapEndToPresentationCompositorFrame, are filed here as well. Use it to tell frames that were presented from frames that were dropped.
DevTools records this category on every Performance panel recording.
disabled-by-default-devtools.timeline.stack
disabled-by-default-devtools.timeline.stack adds no events of its own. It makes Blink add a stackTrace and a sampleTraceId to timeline events such as TimerInstall, EvaluateScript, InvalidateLayout, ScheduleStyleRecalculation and ResourceSendRequest, and asks V8 for a CPU profile sample at that moment. Without it those events say what happened but not which script did it.
DevTools records this category on every Performance panel recording.
- references
- inspector_trace_events.cc
disabled-by-default-devtools.timeline.invalidationTrackingDevTools setting
disabled-by-default-devtools.timeline.invalidationTracking records why Blink marked style or layout dirty: ScheduleStyleInvalidationTracking, StyleInvalidatorInvalidationTracking, StyleRecalcInvalidationTracking, LayoutInvalidationTracking and StyleResolver::ResolveStyle. Each names the node and a reason, such as the class or attribute that changed, and most add the JavaScript stack that was running. InvalidateLayout and ScheduleStyleRecalculation tell you an invalidation happened; these events tell you what triggered it.
DevTools records this category only with Enable CSS selector stats (slow) or Invalidation tracking turned on. Those settings are off by default.
- references
- inspector_trace_events.cc, style_resolver.cc
disabled-by-default-devtools.screenshot
disabled-by-default-devtools.screenshot records Screenshot instant events: JPEG captures of the page, base64 encoded, each tagged with a frame_sequence and an estimated display time. The browser takes them from a video capture of the page's compositor output and stops after a maximum number, so a long recording can run out of screenshots before it ends.
DevTools records this category while the Screenshots checkbox is ticked, which it is by default.
disabled-by-default-layout_shift.debug
disabled-by-default-layout_shift.debug adds no events of its own. It makes Blink add a debug_name to each entry in a LayoutShift event's impacted_nodes, so a moved node can be read by name instead of only by node_id.
DevTools records this category on every Performance panel recording.
- references
- layout_shift_tracker.cc
blink.user_timing
blink.user_timing records performance.mark() as instant events and performance.measure() as spans, under the names the page gave them. Blink also files navigation timing marks here, such as fetchStart, domInteractive and loadEventEnd. It is the direct way to line up your own application phases against the browser's work.
DevTools records this category on every Performance panel recording.
blink.console
blink.console records console.time() and console.timeEnd() pairs as spans named after their label, and a ConsoleMessage::Error instant event for each error level console message. console.timeStamp() is not here; it writes a TimeStamp event to devtools.timeline.
DevTools records this category on every Performance panel recording.
disabled-by-default-devtools.target-rundown
disabled-by-default-devtools.target-rundown records ScriptCompiled and ModuleEvaluated events that tie each script id to the frame, URL, V8 isolate and execution context it ran in. They let a trace reader tell which frame a script belongs to, and which JavaScript world it ran in, the page's own or an isolated one.
DevTools records this category on every Performance panel recording.
- references
- v8_script_runner.cc, inspector_trace_events.cc
disabled-by-default-devtools.timeline.layersDevTools setting
disabled-by-default-devtools.timeline.layers records a LayerTreeHostImpl:snapshot event each time the compositor generates a frame, with the whole layer tree as its data. Turning it on also makes Blink keep extra paint debug information while the trace runs.
DevTools records this category only with Enable advanced paint instrumentation (slow) turned on. That setting is off by default.
disabled-by-default-devtools.timeline.pictureDevTools setting
disabled-by-default-devtools.timeline.picture records cc::DisplayItemList:snapshot events whenever a layer's recorded paint is updated, with the paint serialized as a base64 Skia picture (skp64). That picture is what lets a viewer replay the draw commands behind a Paint.
DevTools records this category only with Enable advanced paint instrumentation (slow) turned on. That setting is off by default.
- references
- display_item_list.cc, recording_source.cc
v8.execute
v8.execute records V8's execution plumbing rather than script calls: RunMicrotasks around each microtask checkpoint, V8.StackGuard and V8.HandleInterrupts when V8 services an interrupt, and V8.BytecodeBudgetInterrupt when a function has used up its budget and V8 considers optimizing it. For the call into a script itself, look at FunctionCall and v8.callFunction.
DevTools records this category on every Performance panel recording.
- references
- microtask-queue.cc, stack-guard.cc, runtime-internal.cc
v8
v8 records Blink's entry points into V8, such as v8.compile, v8.run, v8.callFunction and v8.produceCache, along with context setup (LocalWindowProxy::Initialize), deoptimization (V8.DeoptimizeCode) and a few GC spans such as V8.GCScavenger. MinorGC and MajorGC are filed here together with devtools.timeline.
DevTools records this category on every Performance panel recording.
- references
- v8_script_runner.cc, local_window_proxy.cc
disabled-by-default-v8.compilenot in DevTools
disabled-by-default-v8.compile breaks compilation down per function and per tier: V8.ParseProgram, V8.PreParse, V8.ParseFunction and V8.CompileIgnition for bytecode, V8.MaglevBackground and V8.OptimizeBackground for optimized code, and script streaming spans such as V8.CompileStreamedScript. It is one of the largest categories by event count, and the one that separates parse and compile cost from execution.
DevTools never asks for this category. Record it with your own trace configuration, for example in Perfetto or Puppeteer.
- references
- trace-categories.h
disabled-by-default-v8.cpu_profiler
disabled-by-default-v8.cpu_profiler starts V8's sampling profiler and records its output as Profile and ProfileChunk events, phase P, which a trace viewer stitches into a call tree over time. Without it you see which script a task ran but not which functions inside it used the time.
DevTools records this category unless Disable JavaScript samples is ticked, and that setting is off by default.
- references
- tracing-cpu-profiler.cc, profile-generator.cc
disabled-by-default-v8.cpu_profiler.hires
disabled-by-default-v8.cpu_profiler.hires is not registered or checked anywhere in current Chromium or V8 source, so turning it on changes nothing. The JavaScript samples come from disabled-by-default-v8.cpu_profiler.
DevTools records this category on every Performance panel recording.
- references
- builtin_categories.h, trace-categories.h
disabled-by-default-devtools.v8-source-rundown
disabled-by-default-devtools.v8-source-rundown records a ScriptCatchup event for each top level script V8 compiles, and for every script already loaded when profiling starts: its scriptId, URL, execution context, and sourceMapUrl when the script declares one. The sourceMapUrl is what lets a trace viewer map minified functions in the CPU profile back to their original names after the recording.
DevTools records this category on every Performance panel recording.
- references
- script.cc, compiler.cc, cpu-profiler.cc
disabled-by-default-devtools.v8-source-rundown-sources
disabled-by-default-devtools.v8-source-rundown-sources records the source text of those same scripts: ScriptCatchup for scripts up to 1 MB, LargeScriptCatchup in 1 MB pieces for bigger ones, and TooLargeScriptCatchup with no text above 25 MB. It makes a trace self contained, at the price of writing every script on the page into the file.
DevTools records this category on every Performance panel recording.
- references
- script.cc, compiler.cc, cpu-profiler.cc
disabled-by-default-v8.gcnot in DevTools
disabled-by-default-v8.gc records V8 garbage collection in detail: the V8.GC_* phase scopes for scavenges, incremental marking and sweeping (V8.GC_SCAVENGER, V8.GC_MC_INCREMENTAL), the Marking, Sweeping and ObservablePause spans, and V8.GCTraceGCNVP summaries. Many of its V8.GC_* scopes are also filed under devtools.timeline.
DevTools never asks for this category. Record it with your own trace configuration, for example in Perfetto or Puppeteer.
- references
- trace-categories.h
cppgc
cppgc records garbage collection of Oilpan, the C++ heap Blink allocates DOM and other garbage collected objects on: CppGC.AtomicMark, CppGC.IncrementalMark, CppGC.IncrementalSweep, CppGC.ConcurrentMark and related scopes, with .Minor appended for minor collections. The finer grained scopes are written to disabled-by-default-cppgc instead.
DevTools records this category on every Performance panel recording.
- references
- stats-collector.h, trace-categories.h
disabled-by-default-v8.inspector
disabled-by-default-v8.inspector records V8 inspector work: v8::Debugger::AsyncTaskScheduled, v8::Debugger::AsyncTaskRun and v8::Debugger::AsyncTaskCanceled, which link async work back to the code that scheduled it, console calls such as V8Console::Log, and V8StackTraceImpl::capture.
DevTools records this category on every Performance panel recording.
- references
- v8-debugger.cc, v8-stack-trace-impl.cc
disabled-by-default-v8.stack_tracenot in DevTools
disabled-by-default-v8.stack_trace records the cost of V8 walking the JavaScript stack: V8StackTraceImpl::capture and toFramesVector in the inspector, and spans around building error.stack and looking up the current script. It measures stack capture; it does not add stack traces to other events.
DevTools never asks for this category. Record it with your own trace configuration, for example in Perfetto or Puppeteer.
- references
- isolate.cc, messages.cc, v8-stack-trace-impl.cc
blinknot in DevTools
blink is Blink's general category for its own internals, mostly under C++ function names rather than DevTools labels: HTMLDocumentParser::PumpTokenizerIfPossible, Document::UpdateStyleAndLayout, InlineNode::ShapeTextIncludingFirstLine. A few names are shared with devtools.timeline, such as UpdateLayoutTree and HitTest.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
blink_stylenot in DevTools
blink_style records style engine work below UpdateLayoutTree: Document::updateStyle, Document::recalcStyle, Document::rebuildLayoutTree, StyleEngine::updateActiveStyleSheets and CSSParserImpl::parseStyleSheet. Every event in it is also filed under blink.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- document.cc, style_engine.cc
disabled-by-default-blink.debugDevTools setting
disabled-by-default-blink.debug records Blink debugging detail, most usefully SelectorStats: per selector time, match attempts, fast rejects and matches, collected during style recalculation only while the category is on. It also logs scroll anchoring decisions such as ScrollAnchor::Adjust and spans like EventDispatcher::dispatch.
DevTools records this category only with Enable CSS selector stats (slow) turned on. That setting is off by default.
blink.animationsnot in DevTools
blink.animations records an Animation async span for each CSS animation, CSS transition or Web Animation, with its node, name and state. When an animation could not run on the compositor, the event's compositeFailed and unsupportedProperties fields say why.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- animation.cc, inspector_trace_events.cc
ccnot in DevTools
cc is the Chromium compositor's own instrumentation: commits and tree activation (LayerTreeHostImpl::CommitComplete), tile management and raster (TileManager::PrepareTiles), the frame scheduler (Scheduler::BeginImplFrame), and compositor side scrolling (ScrollTree::SetScrollOffset). It is often the largest category in a trace by event count, and the one to open when the main thread is idle but frames still arrive late.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
benchmarknot in DevTools
benchmark is a label Chromium adds next to other categories on compositor, viz and input pipeline events, and it almost never appears alone. PipelineReporter, EventLatency, Graphics.Pipeline and Scheduler::BeginFrame are all filed under it, which makes it more useful than its name suggests.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- builtin_categories.h
graphics.pipelinenot in DevTools
graphics.pipeline records Graphics.Pipeline events, one per step of a frame's journey: issuing and receiving the BeginFrame, generating and submitting the compositor frame, surface aggregation, and draw and swap. The renderer compositor and viz tag their steps with shared trace ids and flow arrows, so one frame can be followed across processes.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
viznot in DevTools
viz records the display compositor, which runs in the GPU process and turns the compositor frames every renderer submits into what is on screen: DisplayScheduler::BeginFrame, Surface::CommitFrame, Display::DrawAndSwap, DirectRenderer::DrawFrame.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- display.cc
disabled-by-default-display.framedisplayednot in DevTools
disabled-by-default-display.framedisplayed records Display::FrameDisplayed, an instant event viz writes at the presentation time reported for a frame it drew. It is always filed together with benchmark and viz.
DevTools never asks for this category. Record it with your own trace configuration, for example in Perfetto or Puppeteer.
- references
- display.cc
gpunot in DevTools
gpu records GPU process and GPU client work: the GPU scheduler (Scheduler::RunTask), command buffer flushes (CommandBuffer::Flush), shared image creation (SharedImageStub::CreateSharedImage), and GL calls from the renderer such as GLES2::ReadPixels.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
raf_investigationnot in DevTools
raf_investigation records a single event, EarlyOut_NoUpdates, written when a main frame the compositor asked for ended with nothing to commit. It is always filed together with cc.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- proxy_main.cc
disabled-by-default-blink.debug.layoutnot in DevTools
disabled-by-default-blink.debug.layout adds nothing of its own in current source: its only event, LocalFrameView::performLayout, is also filed under blink, benchmark and rail. The layout tree snapshots are written to a separate category, disabled-by-default-blink.debug.layout.trees.
DevTools never asks for this category. Record it with your own trace configuration, for example in Perfetto or Puppeteer.
- references
- local_frame_view.cc
disabled-by-default-blink.graphics_context_annotationsDevTools setting
disabled-by-default-blink.graphics_context_annotations is not registered or written to anywhere in current Chromium source, so turning it on adds nothing to a trace.
DevTools records this category only with Enable advanced paint instrumentation (slow) turned on. That setting is off by default.
- references
- builtin_categories.h
inputnot in DevTools
input records input routing across processes: InputRouterImpl::FilterAndSendWebInputEvent and TouchActionFilter::SetTouchAction in the browser, WidgetInputHandlerManager::DidHandleInputEventSentToMain and MainThreadEventQueue::HandleEvent in the renderer, and the EventLatency spans that time an input from its generation to the frame that presented it.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- event_latency_tracing_recorder.cc
input.scrollingnot in DevTools
input.scrolling labels the input events that matter for scroll and input latency: EventLatency and its stage spans such as RendererCompositorProcessing and RendererMainProcessing, compositor scroll results such as InputHandlerProxy::HandleGestureScrollUpdate_Result, and scroll jank tracking. The stages show whether an input was handled on the compositor or had to reach the main thread.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
latencyInfonot in DevTools
latencyInfo records InputLatency async spans, such as InputLatency::MouseDown, that follow one input by trace id from the browser to the frame swap that showed its result, plus LatencyInfo.Flow instants at each hop in between. The spans list every latency component with its timestamp.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- latency_info.cc, widget_input_handler_manager.cc
loading
loading records the page load milestones and the paint timing behind the metrics: firstContentfulPaint, largestContentfulPaint::Candidate, LargestImagePaint::Candidate and LayoutShift. It also follows document and resource loading in the renderer, such as DocumentLoader::CommitNavigation and ThrottlingURLLoader::OnReceiveResponse.
DevTools records this category on every Performance panel recording.
blink.resourcenot in DevTools
blink.resource records Blink's resource fetcher: ResourceFetcher::requestResource for each request, and warnings for preloads that did not match or went unused, such as ResourceFetcher::PrintPreloadMismatch and ResourceFetcher::WarnUnusedPreloads. Every event in it is also filed under blink.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- resource_fetcher.cc
interactionsnot in DevTools
interactions labels, together with loading, the page load metric spans Chrome's browser process writes, such as PageLoadMetrics.NavigationToFirstContentfulPaint and PageLoadMetrics.NavigationToLargestContentfulPaint. Despite the name, the event timing behind INP is not here; that is EventTiming, in devtools.timeline.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- uma_page_load_metrics_observer.cc
rail
rail is a label from the RAIL performance model that Chromium adds next to other categories and does not use on its own. It marks events tied to loading, input response and animation, such as firstContentfulPaint, RenderFrameImpl::didFinishLoad, WebFrameWidgetImpl::HandleInputEvent and Animation.
DevTools records this category on every Performance panel recording.
- references
- builtin_categories.h
toplevelnot in DevTools
toplevel records the outermost span around each task a Chromium thread runs: ThreadControllerImpl::RunTask on scheduler threads such as the renderer main thread, ThreadPool_RunTask on pool workers, and message pump events such as EpollEvent. It is the category to read for task length before you know what ran inside.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
toplevel.flownot in DevTools
toplevel.flow links tasks to the code that posted them: Chromium writes an instant event when a task is posted and a matching flow id on the task when it runs, so a viewer can draw an arrow between the two. Mojo message sends and the GPU scheduler use it the same way.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- task_annotator.cc, send_message_helper.cc, scheduler.cc
browsernot in DevTools
browser records browser process work outside any renderer, much of it Chrome UI such as TabSearchPageHandler:GetProfileTabs, plus navigation calls filed together with navigation, such as Navigator::Navigate and NavigationControllerImpl::LoadURLWithParams.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- tab_search_page_handler.cc
contentnot in DevTools
content labels events from Chromium's content layer, the multi process browser core between Chrome's UI and Blink. In a page trace it mostly marks WebContentsImpl calls filed together with navigation, such as WebContentsImpl::DidNavigateMainFramePostCommit.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- web_contents_impl.cc
renderernot in DevTools
renderer records widget work on the renderer side: WidgetBase::WasShown, WidgetBase::WasHidden and WidgetBase::UpdateVisualProperties, and input handling such as WidgetBaseInputHandler::OnHandleInputEvent.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- widget_base.cc, widget_base_input_handler.cc
renderer_hostnot in DevTools
renderer_host records the browser side of the renderer relationship: RenderWidgetHostImpl::WasShown, RenderWidgetHostImpl::WasHidden, RenderViewHostImpl::CreateRenderView, and RenderProcessHostImpl::UpdateProcessPriority.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
medianot in DevTools
media records audio and video work: WebMediaPlayerImpl calls such as WebMediaPlayerImpl::DoLoad, the audio and video renderers and decoders, and the Rendering span a video frame compositor writes while it is rendering video.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
WebCorenot in DevTools
WebCore is a category name left over from WebKit, which Blink was forked from. In a page trace its one event is LoadFinished, written by RenderFrameImpl when a frame finishes loading.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- render_frame_impl.cc
disabled-by-default-lighthouse
disabled-by-default-lighthouse is not registered or written to anywhere in current Chromium source, so turning it on adds nothing. Chrome once filed RunTask under it for Lighthouse; RunTask is now in disabled-by-default-devtools.timeline.
DevTools records this category on every Performance panel recording.
mojomnot in DevTools
mojom records Mojo IPC, the message system Chrome's processes and services talk over: Receive mojo message and Receive mojo reply around each incoming message a thread handles, filed together with toplevel. It is plumbing: useful for chasing a slow cross process call, noise for anything about the page.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- interface_endpoint_client.cc
startupnot in DevTools
startup marks browser and GPU process startup work, such as gpu_info_collector::CollectDawnInfo and DriverGL::InitializeStaticBindings. It describes Chrome starting up, not the page.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- gpu_init.cc
shutdownnot in DevTools
shutdown marks teardown in the browser and compositor, such as Compositor::destructor and BrowserMainLoop::ShutdownThreadsAndCleanUp. It describes Chrome closing things down, not the page.
DevTools only asks for this category when Show all events is on. Its events still show up in a normal recording when they are also filed under a category DevTools does ask for.
- references
- compositor.cc, browser_main_loop.cc
__metadata
__metadata is not a category any page event uses: the JSON trace exporter writes it on the thread_name, process_name and process_uptime_seconds records, phase M with ts 0, that put names to each pid and tid. Chrome's trace config always enables it. Without these records every thread in your trace is a number.
- references
- export_json.cc, consumer_host.cc, trace_config.cc
How this was built
Three independent sources, and they disagree with each other more than you might expect.
1. The event set. Extracted mechanically from @paulirish/trace_engine 0.0.65, the same package DevTools itself uses to parse traces. That yields 174 enum entries, which reduce to 169 distinct wire names: some entries are aliases, and 3 are synthetic events DevTools constructs while parsing that never appear in a trace file.
2. Real traces. 300 sites were traced with a wide set of categories: most of what a DevTools recording asks for, plus several it never asks for, such as cc, blink, toplevel and disabled-by-default-v8.compile. That produced 5,223 distinct event names, of which 144 matched a modelled event and confirmed its arguments against reality.
3. Chromium source. Where the package models nothing, or where the traces disagreed with it, the emitter was read directly in Chromium and V8. Entries cite the file that emits the event. That is how the dead events here are known to be dead: the code that emitted them was found, and then found to have been deleted.
4. Example events. 147 of the 189 entries show a real event lifted straight out of a trace file, with long strings and long arrays shortened and every cut marked. Where an ordinary page load cannot produce an event, the sample comes from a local fixture page that calls the API directly: WebSocket, Web Crypto, WebAssembly, scheduler.postTask, User Timing. The rest say plainly that the collection never produced one, and why. None of them is reconstructed.
5. Reading usage and duration. Every trace here was recorded on one machine, one Chrome build, headless, on one network. Exact counts and durations from that setup are not something you could reproduce, so this page does not print them. What survives the machine is the ranking, so each entry says where the event sits against the other 188: usage is how numerous it is, duration how long it runs. Both are quantile bands over the events on this page, so "long" means long for a trace event, not long in any absolute sense. Treat them as a relative ordering, never as a benchmark. The raw figures are in corpus.json for anyone rebuilding this.
6. When, and against what. Everything here was checked against the main branches of Chromium (V8 included), Perfetto and devtools-frontend, between 11 to 13 September 2026. Entries list the files they were checked against, and those links go to the main branch, so a file can have moved on since. The example events were recorded on Chrome 141.0.7390.54, so a field Chrome added or dropped after that build will not show in a sample. The event set comes from @paulirish/trace_engine 0.0.65. That number is the package's own release counter and does not correspond to any Chrome version.
Checked against the main branches of Chromium (V8 included), Perfetto and devtools-frontend, 11 to 13 September 2026. Example events recorded on Chrome 141.0.7390.54.
Event names, interfaces and argument shapes come from @paulirish/trace_engine 0.0.65 and Chromium, both BSD-3 licensed, and from DevTools' own UI strings. Argument shapes the package does not model were verified against real Chrome traces. Explanations are original.
169 distinct event names. The enum contains 174 entries, but 5 of those are aliases or synthetic events that DevTools constructs while parsing and that never appear in a trace file.