browsertime
Version:
Get performance metrics from your web page using Browsertime.
197 lines (193 loc) • 6.52 kB
JavaScript
/**
* Trace-event → group classifier.
*
* The original list (from @sitespeed.io/tracium 0.3.3, extracted from
* Lighthouse circa 2017) is too narrow for modern Chrome traces — it
* only knew about ~30 event names, so half the trace fell through to
* "other" on a busy 2026-era page. This expanded list adds the
* events we actually see today, drawing from:
*
* - Modern Lighthouse `core/lib/tracehouse/task-groups.js`
* - WebPageTest's `MAIN_THREAD_CATEGORY_MAP` in waterfall-tools
* (`src/core/mainthread-categories.js`, mirroring the reference
* PHP implementation at Sample/Implementations/webpagetest/
* www/waterfall.inc#L437-L491)
* - Direct sampling of trace.json from real cnet / theverge runs
*
* The seven-bucket structure is unchanged so the existing UI
* categories keep working. Append-only when adding events for
* future Chrome versions — don't move events between groups
* without checking how it affects the rollups in
* `parseCpuTrace.js`.
*/
export const taskGroups = {
parseHTML: {
id: 'parseHTML',
label: 'Parse HTML & CSS',
traceEventNames: [
'ParseHTML',
'ParseAuthorStyleSheet',
// Document parsing / navigation pipeline — these fire during
// the initial HTML/CSS parse phase, even though they're not
// strictly "parse" events. Counting them as parse-time gives
// a more honest "time spent loading the document" signal.
'CommitLoad',
'DocumentLoader::CommitNavigation',
'DecodedDataDocumentParser::AppendBytes',
// Resource lifecycle events emitted on the main thread for
// the document and its sub-resources. waterfall-tools groups
// them with parsing for the same reason — they're part of the
// page-load critical path, not arbitrary "other" work.
'ResourceSendRequest',
'ResourceReceiveResponse',
'ResourceReceivedData',
'ResourceReceivedResponse',
'ResourceFinish'
]
},
styleLayout: {
id: 'styleLayout',
label: 'Style & Layout',
traceEventNames: [
'ScheduleStyleRecalculation',
'UpdateLayoutTree', // previously RecalculateStyles
'RecalculateStyles', // older Chrome name
'InvalidateLayout',
'Layout',
// IntersectionObserver callbacks query layout to compute
// which observed elements have crossed their thresholds; on
// ad-heavy pages this can be hundreds of ms.
'IntersectionObserverController::computeIntersections'
]
},
paintCompositeRender: {
id: 'paintCompositeRender',
label: 'Rendering',
traceEventNames: [
'Animation',
'HitTest',
'PaintSetup',
'Paint',
'PaintImage',
'RasterTask', // Previously Rasterize
'Rasterize',
'ScrollLayer',
'UpdateLayer',
'UpdateLayerTree',
'CompositeLayers',
// Modern compositor pipeline phases (Chrome ~M100+) that
// didn't exist when the original list was written.
'PrePaint',
'Commit',
'Layerize',
'BeginFrame',
'BeginMainThreadFrame',
'DrawFrame',
// Image decode work happens on the main thread when the
// off-thread decoder can't keep up.
'DecodeImage',
'Decode Image',
'ImageDecodeTask',
'GPUTask',
'SetLayerTreeId'
]
},
scriptParseCompile: {
id: 'scriptParseCompile',
label: 'Script Parsing & Compilation',
traceEventNames: [
'v8.compile',
'v8.compileModule',
'v8.parseOnBackground',
'v8.parseFunction',
// Context creation + V8 snapshot deserialization — these are
// the V8 startup costs that fire before any user JS runs.
'V8.DeserializeContext',
'LocalWindowProxy::CreateContext'
]
},
scriptEvaluation: {
id: 'scriptEvaluation',
label: 'Script Evaluation',
traceEventNames: [
'EventDispatch',
'EvaluateScript',
'v8.evaluateModule',
'FunctionCall',
'TimerFire',
'TimerInstall',
'TimerRemove',
'FireIdleCallback',
'FireAnimationFrame',
'RunMicrotasks',
'V8.Execute',
// Modern V8 entry points (Chrome ~M115+) — `v8.run` and
// `v8.callFunction` are the wrappers Chrome added when V8
// reorganized its tracing categories.
'v8.run',
'v8.callFunction',
'v8.callModuleMethod',
'XHRLoad',
'XHRReadyStateChange'
]
},
garbageCollection: {
id: 'garbageCollection',
label: 'Garbage Collection',
traceEventNames: [
'MinorGC', // Previously GCEvent
'MajorGC',
'BlinkGC.AtomicPhase',
'BlinkGCMarking',
'ThreadState::performIdleLazySweep',
'ThreadState::completeSweep',
'Heap::collectGarbage',
// V8 GC events — there are many specific phase names but they
// all share the `V8.GC` prefix (V8.GCScavenger, V8.GCFinalizeMC,
// V8.GC_SCAVENGER_SCAVENGE_PARALLEL_PHASE, etc.). Specific
// names are listed for fast exact-match; the prefix fallback
// in groupForEvent() catches the rest.
'V8.GCScavenger',
'V8.GCFinalizeMC',
'V8.GCMarkCompact',
'V8.GCIncrementalMarking',
'V8.GCCompactor',
'V8.GC_SCAVENGER_SCAVENGE_PARALLEL_PHASE'
]
},
other: {
id: 'other',
label: 'Other',
traceEventNames: [
'MessageLoop::RunTask',
'TaskQueueManager::ProcessTaskFromWorkQueue',
'ThreadControllerImpl::DoWork',
// The top-level scheduler wrapper. Its self-time is the
// residual after children run — the "Chrome scheduling
// overhead" portion. Was missing from the old list and
// dominated the "other" bucket on busy pages.
'RunTask',
'ThreadControllerImpl::RunTask'
]
}
};
export const taskNameToGroup = {};
for (const group of Object.values(taskGroups)) {
for (const traceEventName of group.traceEventNames) {
taskNameToGroup[traceEventName] = group;
}
}
/**
* Look up the group for a trace event by name. Falls through to
* prefix matching for event-name families that share a structural
* pattern (`V8.GC*` for any V8 GC phase) so we don't have to
* enumerate every phase name Chrome ships. Returns undefined when
* no group matches; callers should fall back to `taskGroups.other`.
*/
export function groupForEvent(name) {
const exact = taskNameToGroup[name];
if (exact) return exact;
if (typeof name === 'string' && name.startsWith('V8.GC')) {
return taskGroups.garbageCollection;
}
}