Repository navigation
Profile Lighthouse + Chromium tracing #17403
Description
🤖 Created by Gemini 4 Argon.
Chromium Runtime & CPU Overhead When Running Lighthouse (https://www.cnn.com/)
Executive Summary
To isolate and quantify the Chromium runtime overhead introduced exclusively when Lighthouse and Perfetto/DevTools tracing are active, we profiled a Release build with symbols of Chromium 157.0.8088.0 (is_debug = false, dcheck_always_on = false, symbol_level = 1) loading https://www.cnn.com/ under three controlled configurations using a custom LD_PRELOAD per-thread CPU-time stack sampling profiler (lhprof.cc, CLOCK_THREAD_CPUTIME_ID at 500 Hz / 2.0 ms per sample across all browser, renderer, GPU, and utility processes):
baseline: Standard Chromium navigation tohttps://www.cnn.com/via Lighthouse'sDriverwith no tracing or Lighthouse gatherers active.tracing_only: Identical navigation with only Lighthouse's defaultTracing.startcategories enabled during navigation andTracing.enddrained afterward.full_lh: Full Lighthouse navigation run (navigation-runner.js) with all 28 default gatherers (prepare,startInstrumentation,startSensitiveInstrumentation, navigation,stopSensitiveInstrumentation,stopInstrumentation, andgetArtifact).
| Metric (Release Build) | baseline |
tracing_only |
full_lh |
tracing_only Overhead vs. baseline |
full_lh Overhead vs. baseline |
|---|---|---|---|---|---|
| Total Chromium CPU Time | 6,704 ms (6.70 s) | 12,236 ms (12.24 s) | 19,162 ms (19.16 s) | +5,532 ms (+82.5%) | +12,458 ms (+185.8%) |
| Direct Instrumentation Stacks | 1,000 ms | 3,860 ms | 7,208 ms | +2,860 ms (+286.0%) | +6,208 ms (+620.8%) |
| Phase 1: Setup & Prepare (CPU / Wall) | 714 ms / 1.26 s | 720 ms / 1.25 s | 714 ms / 1.24 s | +6 ms / -0.01 s | +0 ms / -0.02 s |
| Phase 2: Navigation (CPU / Wall) | 5,990 ms / 7.76 s | 8,164 ms / 8.49 s | 8,370 ms / 8.49 s | +2,174 ms (+36.3%) / +0.73 s | +2,380 ms (+39.7%) / +0.73 s |
| Phase 3: Stop Tracing & Drain (CPU / Wall) | — | 3,352 ms / 3.01 s | 3,684 ms / 3.51 s | +3,352 ms / +3.01 s | +3,684 ms / +3.51 s |
Phase 4: Post-Nav getArtifact (CPU / Wall) |
— | 0 ms / 0.00 s | 6,394 ms / 10.75 s | — | +6,394 ms / +10.75 s |
| Total End-to-End Wall Time | 9.03 s | 12.75 s | 23.98 s | +3.72 s (+41.2%) | +14.95 s (+165.6%) |
| Collected Trace Events | — | 78,377 events |
80,749 events |
+78,377 events |
+80,749 events |
Important
Top 5 Architectural Findings in Chromium (Release Build) + Lighthouse:
-
Redundant
JSON -> CBOR -> JSONRound-Trip & Byte-by-Byte WebSocket Copy DuringTracing.end(1,852 msdirect Trace Export CPU +348 msConvertCBORToJSON+194 msEncodeFrameHybi17in Phase 3): Because Lighthouse'sTracegatherer omitstransferMode: 'ReturnAsStream',utility:tracingserializes Perfetto trace packets to JSON (836 msinConsumerHost::TracingSession::OnTraceData,586 msinExportJson) and sends chunks toCrBrowserMain, whereTracingHandler::OnTraceDataCollected(948 msCPU) scans braces inUpdateTraceDataBuffer(226 msCPU) and parses the entire JSON trace into CBOR viaConvertJSONToCBOR(790 msCPU) only forDevToolsSession::DispatchProtocolMessageToClient(322 msCPU) to immediately convert that CBOR back into JSON viaConvertCBORToJSON(348 msCPU), followed byEncodeFrameHybi17copying the payload one byte at a time viastd::back_inserter(194 msCPU). (Fixed in PR #17407 — cuts Browser Phase 3 CPU by 45% and eliminates948 msofOnTraceDataCollected.) -
Linear
$O(N)$ Rule Scans DuringstartRuleUsageTracking, Quadratic$O(M \times N)$ Rule Mapping inInspectorStyleSheet, & TripleCSS.enable/CSS.disableThrashing (1,658 msdirect CPU):-
InspectorCSSAgent::startRuleUsageTracking(672 msCPU) forces a synchronous full-document style recalculation whereElementRuleCollector::SortAndTransferMatchedRules(666 ms) performs linear scans across all stylesheet rules viaFindStyleSheet(616 ms) andFindStyleRule(604 ms). -
InspectorStyleSheet::SourceDataForRule(446 ms) callsRemapSourceDataToCSSOMIfNecessary(588 msCPU) on every single rule lookup, walking the entire stylesheet's CSSOM rule tree (CollectFlatRules:300 msself inLiveCSSRuleList::length,116 msinCSSStyleSheet::item) per rule ($O(M \times N)$). - Three post-navigation gatherers (
CSSUsage,Stylesheets, andImageElements) independently callCSS.enableandCSS.disable, destroying and re-parsing allInspectorStyleSheetobjects three times. (KeepingDOMandCSSalive in PR #17408 reducesRemapSourceDataToCSSOMIfNecessaryby 25% andCSSdispatcher CPU by 17%.)
-
-
In-Page Gatherer Script Execution (
Runtime.evaluate) & Induced Style/Layout Recalculations (1,316 msdirect CDP in Phase 4 +1,664 msextra style/layout CPU):V8RuntimeAgentImpl::evaluate(1,198 mstotal:512 msin Phase 1 setup +686 msin Phase 4) andcallFunctionOn(58 ms) execute Lighthouse's DOM-inspection scripts (Accessibility,ImageElements,TraceElements,AnchorElements), increasingDocument::UpdateStyleAndLayoutTreeForThisDocumentfrom728 ms(baseline) to2,392 ms(full_lh). -
Synchronous JPEG + Base64 Encoding of Trace Screenshots on
CrBrowserMain& GPU Frame Readback (424 msdirect CPU ++1,168 msGPU process CPU in Phase 2):TracingHandler::OnFrameFromVideoConsumer(350 msCPU) recordsdisabled-by-default-devtools.screenshotevents whoseDevToolsTraceableScreenshot::AppendAsTraceFormat(274 msCPU) synchronously runsgfx::JPEGCodec::Encode(libjpeg_turbo:118 msself inencode_mcu_gather) andbase::Base64EncodeAppenddirectly on the Browser UI thread (CrBrowserMain), while video frame capture increasesgpu-processPhase 2 CPU from272 ms(baseline) to1,440 ms(full_lh). -
Unintended Stylesheet Re-Parsing on
CrRendererMainDuring Navigation viaAnimation.enable(110 msCPU in Phase 2):TraceElementsenables theAnimationdomain before navigation solely to capture animation names. Every animation update during page load triggersInspectorAnimationAgent::AnimationUpdated(108 ms) ->CreateCSSId(82 ms) ->InspectorCSSAgent::StyleSheetId(60 ms), synchronously binding and re-parsing stylesheets on the renderer main thread during navigation.
1. Methodology & 4-Phase Architecture
All runs used a local Release-with-symbols Chromium build (commit 0a167bc56d56d, is_debug = false, dcheck_always_on = false, symbol_level = 1) with the custom stack profiler liblhprof.so (lhprof.cc) preloaded into the root browser process and inherited across all child processes (renderer, gpu-process, utility:tracing, utility:network, utility:storage).
Each run is partitioned into four synchronized phases via shared memory (control.bin):
flowchart LR
P1["Phase 1: Setup & Prepare<br/>(Target/Network/Page/DOM/Debugger enable,<br/>startInstrumentation)"] --> P2["Phase 2: Navigation<br/>(Tracing.start + gotoURL<br/>https://www.cnn.com/)"]
P2 --> P3["Phase 3: Stop & Drain<br/>(Tracing.end + dataCollected,<br/>takePreciseCoverage)"]
P3 --> P4["Phase 4: Post-Navigation<br/>(28 Gatherers getArtifact:<br/>CSSUsage, Stylesheets, ImageElements,<br/>Accessibility, TraceElements, etc.)"]
2. Process & Thread CPU Breakdown Across Configurations (Release Build)
2.1 CPU Time by Process Type (ms)
| Process Type | baseline (ms) |
tracing_only (ms) |
full_lh (ms) |
tracing_only Delta |
full_lh Delta |
Primary Cause of Overhead |
|---|---|---|---|---|---|---|
renderer |
4,638 | 6,134 | 11,444 | +1,496 (+32.3%) | +6,806 (+146.7%) | Phase 2 trace emission & Animation.enable; Phase 3 flush; Phase 4 CSS (1,650 ms), Runtime.evaluate (1,198 ms), & induced style recalcs |
browser |
1,056 | 2,820 | 3,982 | +1,764 (+167.0%) | +2,926 (+277.1%) | Phase 2 screenshot JPEG encode (354 ms); Phase 3 TracingHandler JSON->CBOR->JSON & WebSocket send (1,420 ms); Phase 4 BFCache & screenshots (918 ms) |
gpu-process |
290 | 1,534 | 1,852 | +1,244 (+429.0%) | +1,562 (+538.6%) | FrameSinkVideoCapturerImpl video capture for devtools.screenshot (+1,168 ms in Phase 2) + extra compositing in Phase 3/4 |
utility:tracing |
0 | 1,094 | 1,032 | +1,094 (new) | +1,032 (new) | Perfetto TracingServiceImpl::ReadBuffersIntoConsumer, TraceSorter, ProtoTraceParser, and ExportJson |
utility:network |
670 | 608 | 792 | -62 (-9.3%) | +122 (+18.2%) | NetworkServiceDevToolsObserver raw request/response CDP serialization & trace events |
utility:storage / other |
50 | 46 | 60 | -4 | +10 | Service worker storage & proxy resolver utility processes |
| Total CPU Time | 6,704 | 12,236 | 19,162 | +5,532 (+82.5%) | +12,458 (+185.8%) |
2.2 Top Threads by Total & Direct-Instrumentation CPU Time
Thread (process::thread) |
baseline Total / Inst (ms) |
tracing_only Total / Inst (ms) |
full_lh Total / Inst (ms) |
full_lh Overhead vs. baseline |
|---|---|---|---|---|
renderer::chrome (CrRendererMain) |
3,662 / 872 | 5,380 / 1,226 | 9,184 / 4,012 | +5,522 ms (+150.8%) |
browser::chrome (CrBrowserMain) |
476 / 100 | 2,528 / 1,514 | 2,684 / 1,964 | +2,208 ms (+463.9%) |
gpu-process::VizCompositorThread |
228 / 10 | 1,522 / 96 | 1,718 / 98 | +1,490 ms (+653.5%) |
renderer::ThreadPoolForeground |
638 / 12 | 754 / 28 | 1,190 / 24 | +552 ms (+86.5%) |
utility:tracing::chrome (TracingService) |
0 / 0 | 1,058 / 968 | 976 / 940 | +976 ms (100% tracing) |
renderer::Compositor |
212 / 0 | 412 / 18 | 816 / 30 | +604 ms (+284.9%) |
utility:network::Chrome_ChildIOThread |
468 / 2 | 440 / 2 | 574 / 2 | +106 ms (+22.6%) |
browser::Chrome_DevTools (WebSocket I/O) |
66 / 2 | 268 / 4 | 442 / 26 | +376 ms (+569.7%) |
browser::Chrome_IOThread |
136 / 2 | 290 / 18 | 388 / 28 | +252 ms (+185.3%) |
3. Direct Instrumentation CPU Attribution by Subsystem (Release Build)
By classifying every symbolized stack frame against Chromium's tracing, Perfetto, and DevTools Inspector subsystems (analyze_profile.py), 7,208 ms (37.6% of total Chromium CPU) in full_lh occurs directly inside tracing/DevTools C++ stack frames:
| Instrumentation Subsystem | baseline (ms) |
tracing_only (ms) |
full_lh Total (ms) |
full_lh P1 (ms) |
full_lh P2 Nav (ms) |
full_lh P3 Drain (ms) |
full_lh P4 Gather (ms) |
|---|---|---|---|---|---|---|---|
1. CDP: Transport, Session Dispatch & Runtime.evaluate |
568 | 578 | 2,052 | 514 | 94 | 128 | 1,316 |
| 2. Trace Export & JSON Serialization | 6 | 1,874 | 1,852 | 0 | 6 | 1,840 | 6 |
3. CDP: Animation & CSS Inspector (CSS / RuleUsage) |
0 | 0 | 1,658 | 0 | 104 | 6 | 1,548 |
| 4. Perfetto / Trace Event Emission & Buffering | 34 | 490 | 528 | 4 | 412 | 94 | 18 |
5. DevTools Trace Screenshots (devtools.screenshot) |
2 | 422 | 424 | 0 | 424 | 0 | 0 |
6. V8 Debugger & Async Call Stacks (Debugger.enable) |
190 | 324 | 230 | 2 | 196 | 4 | 28 |
7. CDP: Network Inspector (InspectorNetworkAgent) |
134 | 108 | 220 | 0 | 128 | 0 | 92 |
| 8. CDP: DOM, Audits, Page, Log & WebMCP Agents | 52 | 54 | 192 | 2 | 54 | 10 | 126 |
9. V8 Precise JS Coverage (takePreciseCoverage) |
14 | 8 | 50 | 0 | 8 | 40 | 2 |
10. WebDX Feature Usage Tracking (blink.webdx_feature_usage) |
0 | 2 | 2 | 0 | 0 | 0 | 2 |
| Total Direct Instrumentation Stack Time | 1,000 | 3,860 | 7,208 | 522 | 1,426 | 2,122 | 3,138 |
| Normal Browser Work (Non-Instrumentation) | 5,704 | 8,376 | 11,954 | 192 | 6,944 | 1,562 | 3,256 |
| Total Chromium CPU Time | 6,704 | 12,236 | 19,162 | 714 | 8,370 | 3,684 | 6,394 |
(Note: In baseline, the 514 ms in Phase 1 is prepareTargetForNavigationMode evaluating user-agent/emulation setup via Runtime.evaluate, and the remaining 486 ms in Phase 2 is baseline Network.enable, Page.enable, and Debugger.enable probes used by Lighthouse's Driver to monitor page load.)
4. Deep-Dive Ranked C++ Hotspots (Release Build)
Hotspot #1: Trace Export, Redundant JSON -> CBOR -> JSON Conversion, and Byte-by-Byte WebSocket Framing (1,852 ms Trace Export + 542 ms CBOR->JSON & WebSocket Copy in Phase 3)
When NavigationRunner finishes Phase 2 navigation and calls Trace.stopSensitiveInstrumentation (Tracing.end), Chromium spends 3.51 seconds of wall-clock time and 2.39 seconds of CPU time across three threads (utility:tracing::chrome, browser::chrome, and browser::Chrome_DevTools) just to export and deliver the 80,749-event trace to Lighthouse:
sequenceDiagram
participant U as utility:tracing::chrome<br/>(908 ms CPU)
participant B as browser::chrome (CrBrowserMain)<br/>(1,270 ms CPU)
participant D as browser::Chrome_DevTools<br/>(196 ms CPU)
participant L as Lighthouse (Node.js)
U->>U: TracingServiceImpl::ReadBuffersIntoConsumer (908 ms)<br/>1. ProtoTraceParser / TrackEventEventImporter (164 ms)<br/>2. ExportJson -> JsonExporter::ExportSlices (586 ms)
U->>B: Mojo IPC: ConsumerHost::OnTraceData (JSON chunks)
B->>B: TracingHandler::OnTraceDataCollected (948 ms)<br/>1. UpdateTraceDataBuffer scans braces/strings (226 ms)<br/>2. TracingNotification::AppendSerialized:<br/> ConvertJSONToCBOR parses JSON -> CBOR (790 ms)<br/>3. DevToolsSession::DispatchProtocolMessageToClient:<br/> ConvertCBORToJSON converts CBOR -> JSON (348 ms)
B->>D: PostTask to Chrome_DevTools I/O thread
D->>L: ServerWrapper::SendOverWebSocket<br/>EncodeFrameHybi17 -> base::Extend copies payload<br/>byte-by-byte via std::back_inserter (194 ms)
Call Stacks & Symbolized CPU Breakdown (Phase 3 of full_lh)
-
utility:tracingProcess (908 msCPU in Trace Export):perfetto::tracing_service::TracingServiceImpl::ReadBuffersIntoConsumer(908 msincl)tracing::ConsumerHost::TracingSession::OnTraceData(836 msincl)- Step 1A — Protobuf Trace Ingestion & Sorting (
164 msincl):perfetto::trace_processor::TraceProcessorStorageImpl::NotifyEndOfFile(164 msincl) ->TraceSorter::ExtractEventsForced(160 msincl) ->ProtoTraceParserImpl::ParseTrackEvent(138 msincl) ->TrackEventEventImporter::Import(128 msincl).
- Step 1B — JSON Serialization in
utility:tracing(586 msincl):tracing::ConsumerHost::TracingSession::ExportJson(586 msincl) ->perfetto::trace_processor::json::ExportJson(586 msincl) ->JsonExporter::ExportSlices(376 msincl) ->TraceFormatWriter::DoWriteEvent(244 msincl) ->JsonSerializer::AppendEscapedString(130 msincl,122 msself).
-
browser::chrome(CrBrowserMain,948 msinOnTraceDataCollected+322 msinDispatchProtocolMessageToClient):- Because Lighthouse's
Tracegatherer callsTracing.startwithouttransferMode: 'ReturnAsStream', Chromium usesDevToolsTraceEndpointProxyto emitTracing.dataCollectedprotocol notifications. TracingHandler::OnTraceDataCollected(948 msincl):- Calls
TracingHandler::UpdateTraceDataBuffer(226 msincl,78 msself), scanning every byte of the JSON string to find balanced{...}object boundaries. - Wraps each JSON slice in
TracingNotification, whoseAppendSerializedcallscrdtp::json::ConvertJSONToCBOR(790 msincl) ->JsonParser::ParseValue(788 msincl,250 msinDecodeString,82 msself inParseToken) to parse the JSON string into binary CBOR! - Immediately afterward,
DevToolsSession::DispatchProtocolMessageToClient(322 msincl) callscrdtp::json::ConvertCBORToJSON(348 msincl) to serialize the CBOR right back into JSON for the WebSocket client.
- Calls
- Because Lighthouse's
-
browser::Chrome_DevTools(196 msCPU inEncodeTextFrame):ServerWrapper::SendOverWebSocket->net::WebSocket::Send->net::WebSocketEncoder::EncodeTextFrame(196 msincl) ->EncodeFrameHybi17(194 msincl,156 msself) callsbase::Extend(frame, message).- Because
frameisstd::vector<char>andmessageisstd::string_view(const charvschar), it hits the genericbase::Extendoverload atbase/containers/extend.h:33which runsstd::ranges::transform(..., std::back_inserter(dst), ...)— copying the WebSocket payload byte-by-byte instead ofmemcpy.
Hotspot #2: InspectorCSSAgent & InspectorAnimationAgent Overhead During Both Navigation and Post-Navigation (1,658 ms Direct CPU)
2A. Unintended Stylesheet Re-Parsing on CrRendererMain During Navigation (110 ms CPU in Phase 2)
Before navigation starts, TraceElements.startInstrumentation sends Animation.enable. During Phase 2 page load on CNN.com:
blink::probe::AnimationUpdatedImpl(110 msincl) invokesInspectorAnimationAgent::AnimationUpdated(108 msincl).AnimationUpdatedcallsBuildObjectForAnimation(82 ms) ->InspectorAnimationAgent::CreateCSSId(82 ms).CreateCSSIdcallsInspectorCSSAgent::StyleSheetId(60 ms) ->InspectorCSSAgent::BindStyleSheet->InspectorStyleSheet::ParseText->CSSParser::ParseSheetForInspector.- This synchronously re-parses the page's CSS stylesheets on
CrRendererMainin the middle of the traced navigation, perturbing main-thread responsiveness even thoughCSS.enablewas never called in Phase 2.
2B. Linear $O(N)$ Rule Scans in startRuleUsageTracking, Quadratic $O(M \times N)$ Rule Mapping in SourceDataForRule, & Triple CSS.enable / CSS.disable Thrashing in Phase 4 (1,650 ms CPU)
In Phase 4, blink::protocol::CSS::DomainDispatcherImpl::Dispatch consumes 1,650 ms of CPU time on CrRendererMain:
| CDP Command | Inclusive CPU (ms) | Calling Lighthouse Gatherer(s) | Underlying Blink C++ Function |
|---|---|---|---|
CSS.startRuleUsageTracking |
672 ms | CSSUsage |
InspectorCSSAgent::startRuleUsageTracking -> UpdateStyleAndLayoutTree -> SortAndTransferMatchedRules (666 ms) |
CSS.stopRuleUsageTracking |
456 ms | CSSUsage |
InspectorCSSAgent::stopRuleUsageTracking -> takeCoverageDelta (450 ms) -> SourceDataForRule (446 ms) |
CSS.getMatchedStylesForNode |
326 ms | ImageElements |
InspectorCSSAgent::getMatchedStylesForNode (258 ms) -> BuildArrayForMatchedRuleList (220 ms) |
CSS.enable (x3) |
160 ms | CSSUsage, Stylesheets, ImageElements |
InspectorCSSAgent::CompleteEnabled -> SetActiveStyleSheets (158 ms) |
CSS.getStyleSheetText |
50 ms | Stylesheets |
InspectorCSSAgent::getStyleSheetText |
Three compounding issues make Phase 4 CSS gathering so expensive:
-
Linear
$O(N)$ CSSOM scans duringstartRuleUsageTracking(666 msCPU): WhenStyleRuleUsageTrackeris active during the forced full-document style recalculation,ElementRuleCollector::SortAndTransferMatchedRulescallsFindStyleSheet(616 msincl) ->FindStyleRule<CSSStyleSheet>(604 msincl) andFindStyleRule<CSSRuleList>(402 msincl,128 msself), linearly iterating through every CSSOM rule in every stylesheet (LiveCSSRuleList::length,CSSStyleSheet::item) to locate the parentCSSStyleSheetof each matchedStyleRule. -
Quadratic
$O(M \times N)$ CSSOM traversal insideInspectorStyleSheet::SourceDataForRule(446 msCPU):- For every used CSS rule in
takeCoverageDeltaand every matched rule ingetMatchedStylesForNode,SourceDataForRule(rule)callsRemapSourceDataToCSSOMIfNecessary()(588 mstotal) andcssom_flat_rules_.Find(rule). -
RemapSourceDataToCSSOMIfNecessary()unconditionally creates a freshCSSRuleVectorand walks the entire stylesheet's CSSOM tree viaCollectFlatRules(page_style_sheet_.Get(), &cssom_rules)on every single rule lookup, spending300 msself time inLiveCSSRuleList<CSSStyleRule>::length(),42 msself time inLiveCSSRuleList<CSSGroupingRule>::length(), and116 ms(96 msself) inCSSStyleSheet::item.
- For every used CSS rule in
-
Repeated
CSS.disableresets:CSSUsage,Stylesheets, andImageElementseach callCSS.enablefollowed byCSS.disable.InspectorCSSAgent::disablecallsReset(), destroying allInspectorStyleSheetobjects so the next gatherer must re-parse every stylesheet on the page (BindStyleSheet->ParseText:198 ms) and re-runMapSourceDataToCSSOM(318 ms).
Hotspot #3: Post-Navigation Gatherers (Runtime.evaluate, Network.getResponseBody, DOM, Accessibility, BFCacheFailures) in Phase 4
During Phase 4 (getArtifact), full_lh spends 6,394 ms of CPU time (10.75 s wall time). Outside of InspectorCSSAgent (1,548 ms in Phase 4, covered in Hotspot #2), the remaining Phase 4 CPU time is driven by:
- Injected DOM & Layout Inspection Scripts via
Runtime.evaluate/Runtime.callFunctionOn(1,316 msdirect CDP in Phase 4 + Induced Blink Style/Layout):V8RuntimeAgentImpl::evaluate(1,198 mstotal:512 msin Phase 1 setup +686 msin Phase 4) andV8RuntimeAgentImpl::callFunctionOn(58 msin Phase 4) execute Lighthouse'spageFunctions.getElementsInDocument,getComputedStyle, andgetBoundingClientRectacrossAccessibility,ImageElements,TraceElements,AnchorElements,MetaElements, andLinkElements.- Across
full_lh,blink::Document::UpdateStyleAndLayoutTreeForThisDocument(2,392 mstotal vs728 msinbaseline, +1,664 ms) andblink::StyleResolver::ResolveStyle(2,096 mstotal vs624 msinbaseline, +1,472 ms) account for a 3.3x increase in Blink style resolution.
Network.getResponseBody&InspectorNetworkAgent(220 msdirect CPU):- Phase 2 network event recording (
DidFinishLoading56 ms,DidReceiveData26 ms, andNetworkServiceDevToolsObserver40 ms):128 msCPU. - Phase 4
InspectorNetworkAgent::getResponseBody(74 msincl, called byMainDocumentContent/Scripts):92 msCPU.
- Phase 2 network event recording (
BFCacheFailuresGatherer Navigation in Phase 4:BFCacheFailures.getArtifact(~1.89 swall time) navigates away toabout:blankand back tohttps://www.cnn.com/at the end of Phase 4, triggering an additional round of page lifecycle/compositor work when the page is not BFCache-eligible.
Hotspot #4: Perfetto TrackEvent Emission During Navigation (528 ms Direct CPU in Release)
During Phase 2 navigation (412 ms) and Phase 3 flush (94 ms), emitting trace events across all threads accounts for 528 ms of direct CPU time in the Release build:
- Task & Scheduler Trace Lambdas (
126 msCPU):base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImplemitsThreadControllerImpl::RunTask/DoWorktrace events on every task dispatch across all threads.
- V8 & Blink Trace Event Serialization (
140 msCPU):v8::tracing::EscapeAndAppendString(54 ms),v8::internal::Script::TraceScriptRundownSources(26 ms),cc::frame_viewer_instrumentation::ScopedRasterTask(20 ms), andcc::CompositorFrameReporter::ReportCompositorLatencyTraceEvents(18 ms).
- V8 Debugger & Async Call Stacks (
230 msCPU):- Enabled in
prepare.js(Debugger.enable+Debugger.setAsyncCallStackDepth({maxDepth: 8})). Costs196 msduring Phase 2 navigation (primarilyv8::internal::Debug::ProcessCompileEventat104 ms) and28 msin Phase 4.
- Enabled in
Hotspot #5: DevTools Trace Screenshots (disabled-by-default-devtools.screenshot, 424 ms Direct CPU + +1,168 ms GPU Process CPU in Phase 2)
Enabling disabled-by-default-devtools.screenshot in Trace._defaultTraceCategories activates continuous frame capture from the GPU compositor into the Browser UI thread during Phase 2 navigation:
- GPU Process (
VizCompositorThread,64 msdirectDidCopyFrameCPU ++1,168 msinduced GPU compositing/readback in Phase 2):viz::FrameSinkVideoCapturerImpl::DidCopyFrame(64 ms) reads back compositor surfaces and converts pixel formats (ReadI420Planes,I422ToARGBRow_AVX2), driving totalgpu-processPhase 2 CPU from272 ms(baseline) to1,404 ms(tracing_only) and1,440 ms(full_lh).
- Browser Process (
CrBrowserMain,354 msdirect CPU):content::DevToolsVideoConsumer::OnFrameCaptured(354 ms) delivers each capturedmedia::VideoFrametoTracingHandler::OnFrameFromVideoConsumer(350 ms).OnFrameFromVideoConsumeremits aTRACE_EVENT_INSTANTwith aDevToolsTraceableScreenshot, which Perfetto serializes synchronously onCrBrowserMainviaDevToolsTraceableScreenshot::AppendAsTraceFormat(274 ms) ->gfx::JPEGCodec::Encode(274 msincl,118 msself inencode_mcu_gather) andbase::Base64EncodeAppend.
5. Actionable Optimization Recommendations & Validation Results
In Lighthouse (GoogleChrome/lighthouse)
- Stream Trace Collection via
transferMode: 'ReturnAsStream'inTrace— Opened as PR #17407:- Passing
transferMode: 'ReturnAsStream'toTracing.startand incrementally parsing chunks viaIO.readafterTracing.tracingCompleteeliminatesTracingHandler::OnTraceDataCollected(948 ms -> 0 ms),UpdateTraceDataBuffer(226 ms -> 0 ms), and the entireConvertJSONToCBORtrace round-trip (790 ms -> 0 ms).
- Passing
- Keep
DOMandCSSDomains Enabled Across Phase 4 Gatherers Instead of TogglingCSS.enable/CSS.disable3 Times — Opened as PR #17408:- Keeping
DOMandCSSenabled acrossCSSUsage,Stylesheets, andImageElementsavoids callingInspectorCSSAgent::Reset()three times and reducesRemapSourceDataToCSSOMIfNecessaryby 25% (588 ms -> 440 ms). - Additionally, remove the redundant
window.getComputedStyle(document.body)evaluation inCSSUsageandStylesheets(asInspectorCSSAgent::startRuleUsageTrackingalready forces a fullUpdateStyleAndLayoutTree()).
- Keeping
- Avoid
Animation.enableDuring Navigation inTraceElements:Animation.enableis only used to resolve animation names forTraceElements, yet it causesInspectorAnimationAgent::AnimationUpdatedto synchronously bind and re-parse CSS stylesheets onCrRendererMainduring Phase 2 page load (110 msRelease CPU).
Validated Impact of PR #17407 + PR #17408 on Chromium Release Build
| Metric / Symbol (Release Build) | full_lh Baseline (main) |
full_lh After (PR #17407 + PR #17408) |
Change |
|---|---|---|---|
content::protocol::TracingHandler::OnTraceDataCollected |
948 ms | 0 ms | -100.0% (-948 ms) |
crdtp::json::ConvertJSONToCBOR (in OnTraceDataCollected) |
790 ms | 0 ms | -100.0% (-790 ms) |
content::protocol::TracingHandler::UpdateTraceDataBuffer |
226 ms | 0 ms | -100.0% (-226 ms) |
Browser Process Trace Export & JSON Serialization |
978 ms | 16 ms – 28 ms | -97.8% (-950 ms to -962 ms) |
| Total Browser Process CPU in Phase 3 (Stop Instrumentation) | 1,420 ms | 774 ms – 794 ms | -44.8% (-626 ms to -646 ms) |
Total Trace Export & JSON Serialization (All Processes) |
1,852 ms | 928 ms – 940 ms | -49.6% (-912 ms to -924 ms) |
blink::InspectorStyleSheet::RemapSourceDataToCSSOMIfNecessary |
588 ms | 440 ms – 446 ms | -24.7% (-142 ms to -148 ms) |
blink::InspectorCSSAgent::BindStyleSheet / ParseText |
198 ms | 136 ms | -31.3% (-62 ms) |
blink::protocol::CSS::DomainDispatcherImpl::Dispatch |
1,650 ms | 1,358 ms – 1,390 ms | -16.7% (-260 ms to -292 ms) |
In Chromium (chromium/chromium)
-
Eliminate Linear
$O(N)$ FindStyleSheet/FindStyleRuleScans inElementRuleCollector::SortAndTransferMatchedRules(616 msRelease CPU) and Quadratic$O(M \times N)$ CSSOM Re-Traversal inInspectorStyleSheet::SourceDataForRule(446 msRelease CPU):- Cache the
StyleRule -> CSSStyleSheetmapping duringstartRuleUsageTrackinginstead of scanning every stylesheet'sCSSRuleListlinearly inFindStyleSheet/FindStyleRule(616 msCPU). - Track a dirty bit or mutation counter on
CSSStyleSheetsoRemapSourceDataToCSSOMIfNecessary()is a no-op when the CSSOM has not mutated, and replace the linearcssom_flat_rules_.Find(rule)with a hash map lookup (446 msCPU).
- Cache the
-
Avoid
ConvertJSONToCBOR+ConvertCBORToJSONinTracingNotification:-
TracingHandler::OnTraceDataCollectedalready holds valid JSON from Perfetto'sExportJson; when the client session uses JSON over WebSocket, pass pre-serialized JSON directly instead of round-tripping through CBOR (~1,138 msRelease CPU acrossConvertJSONToCBOR+ConvertCBORToJSON).
-
-
Fix Byte-by-Byte
std::back_inserterinnet::WebSocketEncoder::EncodeFrameHybi17:- Replace
base::Extend(frame, message)(which falls back tostd::ranges::transformwithstd::back_inserterbecauseframeisstd::vector<char>andmessageisstd::string_view, i.e.,const char) withframe.insert(frame.end(), message.begin(), message.end())(194 msRelease CPU).
- Replace
-
Offload Screenshot JPEG + Base64 Encoding From
CrBrowserMaininTracingHandler::OnFrameFromVideoConsumer:- Move
DevToolsVideoConsumer::GetSkBitmapFromFrameandgfx::JPEGCodec::Encodeonto abase::ThreadPoolworker task before emitting the trace event (354 msRelease CPU removed from the Browser UI thread).
- Move
Activity
connorjclark commentedon Oct 21, 2026
Ignoring. Don't want to regress features.
Ignoring. @paulirish is working on this already.
Ignoring. Don't care about debug builds.
connorjclark commentedon Oct 22, 2026
Ignoring. Doesn't help Lighthouse once we've moved to streamed tracing.
connorjclark commentedon Oct 22, 2026
https://chromium-review.git.corp.google.com/c/chromium/src/+/8626947
Benchmark Results (
https://www.cnn.com/, Chromium Release Build)1. On Lighthouse
mainmain)EncodeFrameHybi17Self CPU (per-byte loop)EncodeFrameHybi17Inclusive CPU (encoding +memcpy+ alloc)net::WebSocketEncoder::EncodeTextFrameInclusive CPUnet::WebSocket::SendInclusive CPUChrome_DevToolsHandlerThreadCPU (entire run)2. With Lighthouse #17407 (
ReturnAsStream) + #17408 (Keep DOM/CSS Alive)EncodeFrameHybi17Self CPU (per-byte loop)EncodeFrameHybi17Inclusive CPU (encoding +memcpy+ alloc)net::WebSocketEncoder::EncodeTextFrameInclusive CPUnet::WebSocket::SendInclusive CPUChrome_DevToolsHandlerThreadCPU (entire run)