browsertime 27.0.4 → 27.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/lib/chrome/parseCpuTrace.js +38 -2
- package/lib/chrome/trace/forced-reflows.js +78 -0
- package/lib/chrome/trace/index.js +41 -0
- package/lib/chrome/trace/main-thread-tasks.js +196 -0
- package/lib/chrome/trace/non-composited-animations.js +55 -0
- package/lib/chrome/trace/script-costs.js +88 -0
- package/lib/chrome/trace/task-groups.js +196 -0
- package/lib/chrome/trace/trace-of-tab.js +168 -0
- package/lib/chrome/trace/tracing-processor.js +95 -0
- package/lib/support/har/index.js +22 -0
- package/package.json +3 -4
- package/types/chrome/parseCpuTrace.d.ts +6 -0
- package/types/chrome/parseCpuTrace.d.ts.map +1 -1
- package/types/chrome/trace/forced-reflows.d.ts +8 -0
- package/types/chrome/trace/forced-reflows.d.ts.map +1 -0
- package/types/chrome/trace/index.d.ts +19 -0
- package/types/chrome/trace/index.d.ts.map +1 -0
- package/types/chrome/trace/main-thread-tasks.d.ts +31 -0
- package/types/chrome/trace/main-thread-tasks.d.ts.map +1 -0
- package/types/chrome/trace/non-composited-animations.d.ts +33 -0
- package/types/chrome/trace/non-composited-animations.d.ts.map +1 -0
- package/types/chrome/trace/script-costs.d.ts +14 -0
- package/types/chrome/trace/script-costs.d.ts.map +1 -0
- package/types/chrome/trace/task-groups.d.ts +65 -0
- package/types/chrome/trace/task-groups.d.ts.map +1 -0
- package/types/chrome/trace/trace-of-tab.d.ts +32 -0
- package/types/chrome/trace/trace-of-tab.d.ts.map +1 -0
- package/types/chrome/trace/tracing-processor.d.ts +24 -0
- package/types/chrome/trace/tracing-processor.d.ts.map +1 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace-event → group classifier.
|
|
3
|
+
*
|
|
4
|
+
* The original list (from @sitespeed.io/tracium 0.3.3, extracted from
|
|
5
|
+
* Lighthouse circa 2017) is too narrow for modern Chrome traces — it
|
|
6
|
+
* only knew about ~30 event names, so half the trace fell through to
|
|
7
|
+
* "other" on a busy 2026-era page. This expanded list adds the
|
|
8
|
+
* events we actually see today, drawing from:
|
|
9
|
+
*
|
|
10
|
+
* - Modern Lighthouse `core/lib/tracehouse/task-groups.js`
|
|
11
|
+
* - WebPageTest's `MAIN_THREAD_CATEGORY_MAP` in waterfall-tools
|
|
12
|
+
* (`src/core/mainthread-categories.js`, mirroring the reference
|
|
13
|
+
* PHP implementation at Sample/Implementations/webpagetest/
|
|
14
|
+
* www/waterfall.inc#L437-L491)
|
|
15
|
+
* - Direct sampling of trace.json from real cnet / theverge runs
|
|
16
|
+
*
|
|
17
|
+
* The seven-bucket structure is unchanged so the existing UI
|
|
18
|
+
* categories keep working. Append-only when adding events for
|
|
19
|
+
* future Chrome versions — don't move events between groups
|
|
20
|
+
* without checking how it affects the rollups in
|
|
21
|
+
* `parseCpuTrace.js`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const taskGroups = {
|
|
25
|
+
parseHTML: {
|
|
26
|
+
id: 'parseHTML',
|
|
27
|
+
label: 'Parse HTML & CSS',
|
|
28
|
+
traceEventNames: [
|
|
29
|
+
'ParseHTML',
|
|
30
|
+
'ParseAuthorStyleSheet',
|
|
31
|
+
// Document parsing / navigation pipeline — these fire during
|
|
32
|
+
// the initial HTML/CSS parse phase, even though they're not
|
|
33
|
+
// strictly "parse" events. Counting them as parse-time gives
|
|
34
|
+
// a more honest "time spent loading the document" signal.
|
|
35
|
+
'CommitLoad',
|
|
36
|
+
'DocumentLoader::CommitNavigation',
|
|
37
|
+
'DecodedDataDocumentParser::AppendBytes',
|
|
38
|
+
// Resource lifecycle events emitted on the main thread for
|
|
39
|
+
// the document and its sub-resources. waterfall-tools groups
|
|
40
|
+
// them with parsing for the same reason — they're part of the
|
|
41
|
+
// page-load critical path, not arbitrary "other" work.
|
|
42
|
+
'ResourceSendRequest',
|
|
43
|
+
'ResourceReceiveResponse',
|
|
44
|
+
'ResourceReceivedData',
|
|
45
|
+
'ResourceReceivedResponse',
|
|
46
|
+
'ResourceFinish'
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
styleLayout: {
|
|
50
|
+
id: 'styleLayout',
|
|
51
|
+
label: 'Style & Layout',
|
|
52
|
+
traceEventNames: [
|
|
53
|
+
'ScheduleStyleRecalculation',
|
|
54
|
+
'UpdateLayoutTree', // previously RecalculateStyles
|
|
55
|
+
'RecalculateStyles', // older Chrome name
|
|
56
|
+
'InvalidateLayout',
|
|
57
|
+
'Layout',
|
|
58
|
+
// IntersectionObserver callbacks query layout to compute
|
|
59
|
+
// which observed elements have crossed their thresholds; on
|
|
60
|
+
// ad-heavy pages this can be hundreds of ms.
|
|
61
|
+
'IntersectionObserverController::computeIntersections'
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
paintCompositeRender: {
|
|
65
|
+
id: 'paintCompositeRender',
|
|
66
|
+
label: 'Rendering',
|
|
67
|
+
traceEventNames: [
|
|
68
|
+
'Animation',
|
|
69
|
+
'HitTest',
|
|
70
|
+
'PaintSetup',
|
|
71
|
+
'Paint',
|
|
72
|
+
'PaintImage',
|
|
73
|
+
'RasterTask', // Previously Rasterize
|
|
74
|
+
'Rasterize',
|
|
75
|
+
'ScrollLayer',
|
|
76
|
+
'UpdateLayer',
|
|
77
|
+
'UpdateLayerTree',
|
|
78
|
+
'CompositeLayers',
|
|
79
|
+
// Modern compositor pipeline phases (Chrome ~M100+) that
|
|
80
|
+
// didn't exist when the original list was written.
|
|
81
|
+
'PrePaint',
|
|
82
|
+
'Commit',
|
|
83
|
+
'Layerize',
|
|
84
|
+
'BeginFrame',
|
|
85
|
+
'BeginMainThreadFrame',
|
|
86
|
+
'DrawFrame',
|
|
87
|
+
// Image decode work happens on the main thread when the
|
|
88
|
+
// off-thread decoder can't keep up.
|
|
89
|
+
'DecodeImage',
|
|
90
|
+
'Decode Image',
|
|
91
|
+
'ImageDecodeTask',
|
|
92
|
+
'GPUTask',
|
|
93
|
+
'SetLayerTreeId'
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
scriptParseCompile: {
|
|
97
|
+
id: 'scriptParseCompile',
|
|
98
|
+
label: 'Script Parsing & Compilation',
|
|
99
|
+
traceEventNames: [
|
|
100
|
+
'v8.compile',
|
|
101
|
+
'v8.compileModule',
|
|
102
|
+
'v8.parseOnBackground',
|
|
103
|
+
'v8.parseFunction',
|
|
104
|
+
// Context creation + V8 snapshot deserialization — these are
|
|
105
|
+
// the V8 startup costs that fire before any user JS runs.
|
|
106
|
+
'V8.DeserializeContext',
|
|
107
|
+
'LocalWindowProxy::CreateContext'
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
scriptEvaluation: {
|
|
111
|
+
id: 'scriptEvaluation',
|
|
112
|
+
label: 'Script Evaluation',
|
|
113
|
+
traceEventNames: [
|
|
114
|
+
'EventDispatch',
|
|
115
|
+
'EvaluateScript',
|
|
116
|
+
'v8.evaluateModule',
|
|
117
|
+
'FunctionCall',
|
|
118
|
+
'TimerFire',
|
|
119
|
+
'TimerInstall',
|
|
120
|
+
'TimerRemove',
|
|
121
|
+
'FireIdleCallback',
|
|
122
|
+
'FireAnimationFrame',
|
|
123
|
+
'RunMicrotasks',
|
|
124
|
+
'V8.Execute',
|
|
125
|
+
// Modern V8 entry points (Chrome ~M115+) — `v8.run` and
|
|
126
|
+
// `v8.callFunction` are the wrappers Chrome added when V8
|
|
127
|
+
// reorganized its tracing categories.
|
|
128
|
+
'v8.run',
|
|
129
|
+
'v8.callFunction',
|
|
130
|
+
'v8.callModuleMethod',
|
|
131
|
+
'XHRLoad',
|
|
132
|
+
'XHRReadyStateChange'
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
garbageCollection: {
|
|
136
|
+
id: 'garbageCollection',
|
|
137
|
+
label: 'Garbage Collection',
|
|
138
|
+
traceEventNames: [
|
|
139
|
+
'MinorGC', // Previously GCEvent
|
|
140
|
+
'MajorGC',
|
|
141
|
+
'BlinkGC.AtomicPhase',
|
|
142
|
+
'BlinkGCMarking',
|
|
143
|
+
'ThreadState::performIdleLazySweep',
|
|
144
|
+
'ThreadState::completeSweep',
|
|
145
|
+
'Heap::collectGarbage',
|
|
146
|
+
// V8 GC events — there are many specific phase names but they
|
|
147
|
+
// all share the `V8.GC` prefix (V8.GCScavenger, V8.GCFinalizeMC,
|
|
148
|
+
// V8.GC_SCAVENGER_SCAVENGE_PARALLEL_PHASE, etc.). Specific
|
|
149
|
+
// names are listed for fast exact-match; the prefix fallback
|
|
150
|
+
// in groupForEvent() catches the rest.
|
|
151
|
+
'V8.GCScavenger',
|
|
152
|
+
'V8.GCFinalizeMC',
|
|
153
|
+
'V8.GCMarkCompact',
|
|
154
|
+
'V8.GCIncrementalMarking',
|
|
155
|
+
'V8.GCCompactor',
|
|
156
|
+
'V8.GC_SCAVENGER_SCAVENGE_PARALLEL_PHASE'
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
other: {
|
|
160
|
+
id: 'other',
|
|
161
|
+
label: 'Other',
|
|
162
|
+
traceEventNames: [
|
|
163
|
+
'MessageLoop::RunTask',
|
|
164
|
+
'TaskQueueManager::ProcessTaskFromWorkQueue',
|
|
165
|
+
'ThreadControllerImpl::DoWork',
|
|
166
|
+
// The top-level scheduler wrapper. Its self-time is the
|
|
167
|
+
// residual after children run — the "Chrome scheduling
|
|
168
|
+
// overhead" portion. Was missing from the old list and
|
|
169
|
+
// dominated the "other" bucket on busy pages.
|
|
170
|
+
'RunTask',
|
|
171
|
+
'ThreadControllerImpl::RunTask'
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const taskNameToGroup = {};
|
|
177
|
+
for (const group of Object.values(taskGroups)) {
|
|
178
|
+
for (const traceEventName of group.traceEventNames) {
|
|
179
|
+
taskNameToGroup[traceEventName] = group;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Look up the group for a trace event by name. Falls through to
|
|
185
|
+
* prefix matching for event-name families that share a structural
|
|
186
|
+
* pattern (`V8.GC*` for any V8 GC phase) so we don't have to
|
|
187
|
+
* enumerate every phase name Chrome ships. Returns undefined when
|
|
188
|
+
* no group matches; callers should fall back to `taskGroups.other`.
|
|
189
|
+
*/
|
|
190
|
+
export function groupForEvent(name) {
|
|
191
|
+
const exact = taskNameToGroup[name];
|
|
192
|
+
if (exact) return exact;
|
|
193
|
+
if (typeof name === 'string' && name.startsWith('V8.GC')) {
|
|
194
|
+
return taskGroups.garbageCollection;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace → tab-of-interest extractor. Ported from @sitespeed.io/tracium
|
|
3
|
+
* 0.3.3. Picks the renderer pid/tid/frameId, locates navigation/paint
|
|
4
|
+
* landmark events, and returns the chronologically-stable subset of
|
|
5
|
+
* events that belong to the inspected tab plus its main-thread
|
|
6
|
+
* subset that the task-builder consumes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getLogger } from '@sitespeed.io/log';
|
|
10
|
+
import { findMainFrameIds } from './tracing-processor.js';
|
|
11
|
+
|
|
12
|
+
const log = getLogger('browsertime.chrome.trace');
|
|
13
|
+
|
|
14
|
+
const ACCEPTABLE_NAVIGATION_URL_REGEX = /^(chrome|https?|file):/;
|
|
15
|
+
|
|
16
|
+
function getTimestamp(event) {
|
|
17
|
+
return event && event.ts;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isNavigationStartOfInterest(event) {
|
|
21
|
+
return (
|
|
22
|
+
event.name === 'navigationStart' &&
|
|
23
|
+
(!event.args.data ||
|
|
24
|
+
!event.args.data.documentLoaderURL ||
|
|
25
|
+
ACCEPTABLE_NAVIGATION_URL_REGEX.test(event.args.data.documentLoaderURL))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stable sort by `ts`. JavaScript's Array.prototype.sort isn't
|
|
31
|
+
* guaranteed stable across engines historically, and trace event
|
|
32
|
+
* order matters when ts collides (B/E pairing breaks otherwise).
|
|
33
|
+
* The implementation sorts an indices array first, breaking ties by
|
|
34
|
+
* source index, so equal-ts events keep their original ordering.
|
|
35
|
+
*/
|
|
36
|
+
function filteredStableSort(traceEvents, filter) {
|
|
37
|
+
const indices = [];
|
|
38
|
+
for (const [srcIndex, traceEvent] of traceEvents.entries()) {
|
|
39
|
+
if (filter(traceEvent)) {
|
|
40
|
+
indices.push(srcIndex);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
indices.sort((indexA, indexB) => {
|
|
44
|
+
const result = traceEvents[indexA].ts - traceEvents[indexB].ts;
|
|
45
|
+
return result || indexA - indexB;
|
|
46
|
+
});
|
|
47
|
+
const sorted = [];
|
|
48
|
+
for (const index of indices) {
|
|
49
|
+
sorted.push(traceEvents[index]);
|
|
50
|
+
}
|
|
51
|
+
return sorted;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function computeTraceOfTab(trace) {
|
|
55
|
+
// Parse the trace for our key events and sort them by timestamp.
|
|
56
|
+
// The sort *must* be stable to keep events correctly nested.
|
|
57
|
+
const keyEvents = filteredStableSort(trace.traceEvents, e => {
|
|
58
|
+
return (
|
|
59
|
+
e.cat.includes('blink.user_timing') ||
|
|
60
|
+
e.cat.includes('loading') ||
|
|
61
|
+
e.cat.includes('devtools.timeline') ||
|
|
62
|
+
e.cat === '__metadata'
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const mainFrameIds = findMainFrameIds(keyEvents);
|
|
67
|
+
|
|
68
|
+
// Filter to just events matching the frame ID for sanity.
|
|
69
|
+
const frameEvents = keyEvents.filter(
|
|
70
|
+
e => e.args.frame === mainFrameIds.frameId
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Our navStart will be the last frame navigation in the trace.
|
|
74
|
+
const navigationStart = frameEvents.findLast(e =>
|
|
75
|
+
isNavigationStartOfInterest(e)
|
|
76
|
+
);
|
|
77
|
+
if (!navigationStart) throw new Error('NO_NAVSTART');
|
|
78
|
+
|
|
79
|
+
// Find our first paint of this frame.
|
|
80
|
+
const firstPaint = frameEvents.find(
|
|
81
|
+
e => e.name === 'firstPaint' && e.ts > navigationStart.ts
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// fMP follows at/after the FP.
|
|
85
|
+
let firstMeaningfulPaint = frameEvents.find(
|
|
86
|
+
e => e.name === 'firstMeaningfulPaint' && e.ts > navigationStart.ts
|
|
87
|
+
);
|
|
88
|
+
let fmpFellBack = false;
|
|
89
|
+
|
|
90
|
+
// If there was no firstMeaningfulPaint event found in the trace,
|
|
91
|
+
// network-idle detection may have not been triggered before the
|
|
92
|
+
// capture finished. Use the last firstMeaningfulPaintCandidate.
|
|
93
|
+
if (!firstMeaningfulPaint) {
|
|
94
|
+
const fmpCand = 'firstMeaningfulPaintCandidate';
|
|
95
|
+
fmpFellBack = true;
|
|
96
|
+
log.debug(
|
|
97
|
+
'trace-of-tab',
|
|
98
|
+
`No firstMeaningfulPaint found, falling back to last ${fmpCand}`
|
|
99
|
+
);
|
|
100
|
+
const lastCandidate = frameEvents.findLast(e => e.name === fmpCand);
|
|
101
|
+
if (!lastCandidate) {
|
|
102
|
+
log.debug(
|
|
103
|
+
'trace-of-tab',
|
|
104
|
+
'No `firstMeaningfulPaintCandidate` events found in trace'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
firstMeaningfulPaint = lastCandidate;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const load = frameEvents.find(
|
|
111
|
+
e => e.name === 'loadEventEnd' && e.ts > navigationStart.ts
|
|
112
|
+
);
|
|
113
|
+
const domContentLoaded = frameEvents.find(
|
|
114
|
+
e => e.name === 'domContentLoadedEventEnd' && e.ts > navigationStart.ts
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Subset all trace events to just our tab's process (incl threads
|
|
118
|
+
// other than main). Stable-sort to keep events correctly nested.
|
|
119
|
+
const processEvents = filteredStableSort(
|
|
120
|
+
trace.traceEvents,
|
|
121
|
+
e => e.pid === mainFrameIds.pid
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const mainThreadEvents = processEvents.filter(
|
|
125
|
+
e => e.tid === mainFrameIds.tid
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// traceEnd must exist since at least navigationStart was verified.
|
|
129
|
+
let traceEnd = trace.traceEvents[0];
|
|
130
|
+
for (const evt of trace.traceEvents) {
|
|
131
|
+
if (evt.ts > traceEnd.ts) traceEnd = evt;
|
|
132
|
+
}
|
|
133
|
+
const fakeEndOfTraceEvt = { ts: traceEnd.ts + (traceEnd.dur || 0) };
|
|
134
|
+
|
|
135
|
+
const timestamps = {
|
|
136
|
+
navigationStart: navigationStart.ts,
|
|
137
|
+
firstPaint: getTimestamp(firstPaint),
|
|
138
|
+
firstMeaningfulPaint: getTimestamp(firstMeaningfulPaint),
|
|
139
|
+
traceEnd: fakeEndOfTraceEvt.ts,
|
|
140
|
+
load: getTimestamp(load),
|
|
141
|
+
domContentLoaded: getTimestamp(domContentLoaded)
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const getTiming = ts => (ts - navigationStart.ts) / 1000;
|
|
145
|
+
const maybeGetTiming = ts => (ts === undefined ? undefined : getTiming(ts));
|
|
146
|
+
const timings = {
|
|
147
|
+
navigationStart: 0,
|
|
148
|
+
firstPaint: maybeGetTiming(timestamps.firstPaint),
|
|
149
|
+
firstMeaningfulPaint: maybeGetTiming(timestamps.firstMeaningfulPaint),
|
|
150
|
+
traceEnd: getTiming(timestamps.traceEnd),
|
|
151
|
+
load: maybeGetTiming(timestamps.load),
|
|
152
|
+
domContentLoaded: maybeGetTiming(timestamps.domContentLoaded)
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
timings,
|
|
157
|
+
timestamps,
|
|
158
|
+
processEvents,
|
|
159
|
+
mainThreadEvents,
|
|
160
|
+
mainFrameIds,
|
|
161
|
+
navigationStartEvt: navigationStart,
|
|
162
|
+
firstPaintEvt: firstPaint,
|
|
163
|
+
firstMeaningfulPaintEvt: firstMeaningfulPaint,
|
|
164
|
+
loadEvt: load,
|
|
165
|
+
domContentLoadedEvt: domContentLoaded,
|
|
166
|
+
fmpFellBack
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace pid/tid/frameId resolver, ported from @sitespeed.io/tracium
|
|
3
|
+
* 0.3.3 (Lighthouse 2018 lineage). Only `findMainFrameIds` is kept —
|
|
4
|
+
* the original module also exposed `getRiskToResponsiveness` and
|
|
5
|
+
* the `_riskPercentiles` math used by Lighthouse's TBT, but those
|
|
6
|
+
* aren't called from Browsertime's pipeline so they're dropped to
|
|
7
|
+
* keep the surface tight.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Locate the main renderer's pid/tid/frameId via three fallbacks, in
|
|
12
|
+
* order of how recent the trace events are: the modern
|
|
13
|
+
* TracingStartedInBrowser event (with a frames[] payload), the legacy
|
|
14
|
+
* TracingStartedInPage event, and finally pairing the first
|
|
15
|
+
* navigationStart with the first ResourceSendRequest. Throws when
|
|
16
|
+
* none of the three apply — which means the trace doesn't contain
|
|
17
|
+
* a renderable tab and the caller (parseCpuTrace) will degrade
|
|
18
|
+
* gracefully via its outer try/catch.
|
|
19
|
+
*/
|
|
20
|
+
export function findMainFrameIds(events) {
|
|
21
|
+
// Prefer the newer TracingStartedInBrowser event first, if it exists.
|
|
22
|
+
const startedInBrowserEvt = events.find(
|
|
23
|
+
e => e.name === 'TracingStartedInBrowser'
|
|
24
|
+
);
|
|
25
|
+
if (
|
|
26
|
+
startedInBrowserEvt &&
|
|
27
|
+
startedInBrowserEvt.args.data &&
|
|
28
|
+
startedInBrowserEvt.args.data.frames
|
|
29
|
+
) {
|
|
30
|
+
const mainFrame = startedInBrowserEvt.args.data.frames.find(
|
|
31
|
+
frame => !frame.parent
|
|
32
|
+
);
|
|
33
|
+
const frameId = mainFrame && mainFrame.frame;
|
|
34
|
+
const pid = mainFrame && mainFrame.processId;
|
|
35
|
+
|
|
36
|
+
const threadNameEvt = events.find(
|
|
37
|
+
e =>
|
|
38
|
+
e.pid === pid &&
|
|
39
|
+
e.ph === 'M' &&
|
|
40
|
+
e.cat === '__metadata' &&
|
|
41
|
+
e.name === 'thread_name' &&
|
|
42
|
+
e.args.name === 'CrRendererMain'
|
|
43
|
+
);
|
|
44
|
+
const tid = threadNameEvt && threadNameEvt.tid;
|
|
45
|
+
|
|
46
|
+
if (pid && tid && frameId) {
|
|
47
|
+
return { pid, tid, frameId };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Support legacy browser versions that do not emit
|
|
52
|
+
// TracingStartedInBrowser. The first TracingStartedInPage in the
|
|
53
|
+
// trace is the renderer thread of interest. Beware: the
|
|
54
|
+
// TracingStartedInPage event can appear slightly after a
|
|
55
|
+
// navigationStart, so the order of fallbacks matters.
|
|
56
|
+
const startedInPageEvt = events.find(e => e.name === 'TracingStartedInPage');
|
|
57
|
+
if (startedInPageEvt && startedInPageEvt.args && startedInPageEvt.args.data) {
|
|
58
|
+
const frameId = startedInPageEvt.args.data.page;
|
|
59
|
+
if (frameId) {
|
|
60
|
+
return { pid: startedInPageEvt.pid, tid: startedInPageEvt.tid, frameId };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Last resort: pair the first navigationStart that's loading the
|
|
65
|
+
// main frame with the first ResourceSendRequest from the same
|
|
66
|
+
// pid/tid. If those agree the renderer is whatever pid/tid they
|
|
67
|
+
// share.
|
|
68
|
+
const navStartEvt = events.find(e =>
|
|
69
|
+
Boolean(
|
|
70
|
+
e.name === 'navigationStart' &&
|
|
71
|
+
e.args &&
|
|
72
|
+
e.args.data &&
|
|
73
|
+
e.args.data.isLoadingMainFrame &&
|
|
74
|
+
e.args.data.documentLoaderURL
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
const firstResourceSendEvt = events.find(
|
|
78
|
+
e => e.name === 'ResourceSendRequest'
|
|
79
|
+
);
|
|
80
|
+
if (
|
|
81
|
+
navStartEvt &&
|
|
82
|
+
navStartEvt.args &&
|
|
83
|
+
navStartEvt.args.data &&
|
|
84
|
+
firstResourceSendEvt &&
|
|
85
|
+
firstResourceSendEvt.pid === navStartEvt.pid &&
|
|
86
|
+
firstResourceSendEvt.tid === navStartEvt.tid
|
|
87
|
+
) {
|
|
88
|
+
const frameId = navStartEvt.args.frame;
|
|
89
|
+
if (frameId) {
|
|
90
|
+
return { pid: navStartEvt.pid, tid: navStartEvt.tid, frameId };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw new Error('NO_TRACING_STARTED');
|
|
95
|
+
}
|
package/lib/support/har/index.js
CHANGED
|
@@ -359,6 +359,17 @@ export function getEmptyHAR(url, browser) {
|
|
|
359
359
|
}
|
|
360
360
|
};
|
|
361
361
|
}
|
|
362
|
+
// Tag the first entry of a page with the document URL it navigated to.
|
|
363
|
+
// Downstream consumers (waterfall-tools) read `entries[0]._documentURL`
|
|
364
|
+
// to flag cross-origin requests in the rendered waterfall — the first
|
|
365
|
+
// entry's own URL would also work for the no-redirect case, but breaks
|
|
366
|
+
// the moment the document follows a redirect chain to a different host.
|
|
367
|
+
function addDocumentURLToFirstEntry(harPage, entries, url) {
|
|
368
|
+
if (!harPage || !entries || !url) return;
|
|
369
|
+
const first = entries.find(entry => entry.pageref === harPage.id);
|
|
370
|
+
if (first) first._documentURL = url;
|
|
371
|
+
}
|
|
372
|
+
|
|
362
373
|
export function addExtraFieldsToHar(totalResults, har, options) {
|
|
363
374
|
if (har) {
|
|
364
375
|
let harPageNumber = 0;
|
|
@@ -384,6 +395,12 @@ export function addExtraFieldsToHar(totalResults, har, options) {
|
|
|
384
395
|
options
|
|
385
396
|
);
|
|
386
397
|
|
|
398
|
+
addDocumentURLToFirstEntry(
|
|
399
|
+
harPage,
|
|
400
|
+
har.log.entries,
|
|
401
|
+
totalResults[pageNumber].info.url
|
|
402
|
+
);
|
|
403
|
+
|
|
387
404
|
if (browserScript) {
|
|
388
405
|
addExtrasToHAR(
|
|
389
406
|
harPage,
|
|
@@ -417,6 +434,11 @@ export function addExtraFieldsToHar(totalResults, har, options) {
|
|
|
417
434
|
browserScript,
|
|
418
435
|
options
|
|
419
436
|
);
|
|
437
|
+
addDocumentURLToFirstEntry(
|
|
438
|
+
harPage,
|
|
439
|
+
har.log.entries,
|
|
440
|
+
totalResults[page].info.url
|
|
441
|
+
);
|
|
420
442
|
} else {
|
|
421
443
|
log.error(
|
|
422
444
|
'Could not add meta data to the HAR, miss page ' + harIndex
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browsertime",
|
|
3
3
|
"description": "Get performance metrics from your web page using Browsertime.",
|
|
4
|
-
"version": "27.0
|
|
4
|
+
"version": "27.1.0",
|
|
5
5
|
"bin": "./bin/browsertime.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"types": "./types/scripting.d.ts",
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
"@sitespeed.io/edgedriver": "143.0.3650",
|
|
12
12
|
"@sitespeed.io/geckodriver": "0.36.0",
|
|
13
13
|
"@sitespeed.io/log": "1.0.0",
|
|
14
|
-
"@sitespeed.io/throttle": "
|
|
15
|
-
"
|
|
16
|
-
"chrome-har": "1.2.1",
|
|
14
|
+
"@sitespeed.io/throttle": "6.0.0",
|
|
15
|
+
"chrome-har": "1.3.0",
|
|
17
16
|
"chrome-remote-interface": "0.33.3",
|
|
18
17
|
"execa": "9.6.1",
|
|
19
18
|
"fast-stats": "0.0.7",
|
|
@@ -13,9 +13,15 @@ export function parseCPUTrace(tracelog: any, url: any): Promise<{
|
|
|
13
13
|
url: string;
|
|
14
14
|
value: number;
|
|
15
15
|
}[];
|
|
16
|
+
scriptCosts: any[];
|
|
17
|
+
forcedReflows: any[];
|
|
18
|
+
nonCompositedAnimations: any[];
|
|
16
19
|
} | {
|
|
17
20
|
categories?: undefined;
|
|
18
21
|
events?: undefined;
|
|
19
22
|
urls?: undefined;
|
|
23
|
+
scriptCosts?: undefined;
|
|
24
|
+
forcedReflows?: undefined;
|
|
25
|
+
nonCompositedAnimations?: undefined;
|
|
20
26
|
}>;
|
|
21
27
|
//# sourceMappingURL=parseCpuTrace.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parseCpuTrace.d.ts","sourceRoot":"","sources":["../../lib/chrome/parseCpuTrace.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"parseCpuTrace.d.ts","sourceRoot":"","sources":["../../lib/chrome/parseCpuTrace.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;;;;;;;;;;;;;;;GA2GC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forced-reflows.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/forced-reflows.js"],"names":[],"mappings":"AAgDA;;;;;;IA6BC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute the hierarchical main-thread task tree for a trace.
|
|
3
|
+
* Each task has its kind classified into one of:
|
|
4
|
+
* parseHTML | styleLayout | paintCompositeRender |
|
|
5
|
+
* scriptParseCompile | scriptEvaluation | garbageCollection | other
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} trace Parsed Chrome trace.json (with .traceEvents).
|
|
8
|
+
* @param {Object} [options]
|
|
9
|
+
* @param {boolean} [options.flatten=false] When true, return all
|
|
10
|
+
* tasks flat (parents and children); when false, only top-level.
|
|
11
|
+
* @returns {Array<Object>}
|
|
12
|
+
*/
|
|
13
|
+
export function computeMainThreadTasks(trace: any, options?: {
|
|
14
|
+
flatten?: boolean;
|
|
15
|
+
}): Array<any>;
|
|
16
|
+
export { computeScriptCosts } from "./script-costs.js";
|
|
17
|
+
export { computeForcedReflows } from "./forced-reflows.js";
|
|
18
|
+
export { computeNonCompositedAnimations } from "./non-composited-animations.js";
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/index.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;GAWG;AACH,6DAJG;IAA0B,OAAO,GAAzB,OAAO;CAEf,GAAU,KAAK,KAAQ,CAYzB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function getMainThreadTasks(traceEvents: any, traceEndTs: any): {
|
|
2
|
+
event: any;
|
|
3
|
+
startTime: any;
|
|
4
|
+
endTime: any;
|
|
5
|
+
parent: any;
|
|
6
|
+
children: any[];
|
|
7
|
+
attributableURLs: any[];
|
|
8
|
+
group: {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
traceEventNames: string[];
|
|
12
|
+
};
|
|
13
|
+
duration: number;
|
|
14
|
+
selfTime: number;
|
|
15
|
+
}[];
|
|
16
|
+
export function compute(trace: any): {
|
|
17
|
+
event: any;
|
|
18
|
+
startTime: any;
|
|
19
|
+
endTime: any;
|
|
20
|
+
parent: any;
|
|
21
|
+
children: any[];
|
|
22
|
+
attributableURLs: any[];
|
|
23
|
+
group: {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
traceEventNames: string[];
|
|
27
|
+
};
|
|
28
|
+
duration: number;
|
|
29
|
+
selfTime: number;
|
|
30
|
+
}[];
|
|
31
|
+
//# sourceMappingURL=main-thread-tasks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main-thread-tasks.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/main-thread-tasks.js"],"names":[],"mappings":"AAkKA;;;;;;;;;;;;;;IA4BC;AAED;;;;;;;;;;;;;;IAGC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-composited animations — animations that triggered Layout or
|
|
3
|
+
* Paint instead of running entirely on the compositor thread.
|
|
4
|
+
*
|
|
5
|
+
* Compositor-only animations (transform, opacity) hand off to the
|
|
6
|
+
* GPU and don't block the main thread. Anything that touches
|
|
7
|
+
* `top` / `left` / `width` / `box-shadow` / non-transformable
|
|
8
|
+
* filters / etc. forces a per-frame layout or paint, which means
|
|
9
|
+
* jank on busy main threads.
|
|
10
|
+
*
|
|
11
|
+
* Chrome marks these in the trace via `Animation` events whose
|
|
12
|
+
* `args.data.compositeFailed` bitmask is non-zero, plus a list of
|
|
13
|
+
* `unsupportedProperties` strings. We surface the raw values; the
|
|
14
|
+
* consumer can map the bitmask to human reasons (see Lighthouse's
|
|
15
|
+
* non-composited-animations audit for the canonical mapping).
|
|
16
|
+
*
|
|
17
|
+
* Returns: [{ name, id, compositeFailed, unsupportedProperties,
|
|
18
|
+
* startTime }, …]
|
|
19
|
+
* name — args.data.nodeName when present (rare)
|
|
20
|
+
* id — args.data.id (compositor-internal)
|
|
21
|
+
* compositeFailed — bitmask of failure reasons
|
|
22
|
+
* unsupportedProperties — array of property names that blocked
|
|
23
|
+
* composite (e.g. ['top','box-shadow'])
|
|
24
|
+
* startTime — event ts in microseconds (raw trace timestamp)
|
|
25
|
+
*/
|
|
26
|
+
export function computeNonCompositedAnimations(trace: any): {
|
|
27
|
+
name: any;
|
|
28
|
+
id: any;
|
|
29
|
+
compositeFailed: any;
|
|
30
|
+
unsupportedProperties: any;
|
|
31
|
+
startTime: any;
|
|
32
|
+
}[];
|
|
33
|
+
//# sourceMappingURL=non-composited-animations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"non-composited-animations.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/non-composited-animations.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;IA4BC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} trace Parsed Chrome trace.json (with .traceEvents).
|
|
3
|
+
* @returns {Array<{url:string, parse:number, compile:number,
|
|
4
|
+
* execute:number, total:number}>}
|
|
5
|
+
* Per-URL JS cost in ms, sorted by total desc.
|
|
6
|
+
*/
|
|
7
|
+
export function computeScriptCosts(trace: any): Array<{
|
|
8
|
+
url: string;
|
|
9
|
+
parse: number;
|
|
10
|
+
compile: number;
|
|
11
|
+
execute: number;
|
|
12
|
+
total: number;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=script-costs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-costs.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/script-costs.js"],"names":[],"mappings":"AA6CA;;;;;GAKG;AACH,gDAJa,KAAK,CAAC;IAAC,GAAG,EAAC,MAAM,CAAC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAC,OAAO,EAAC,MAAM,CAAC;IACzC,OAAO,EAAC,MAAM,CAAC;IAAC,KAAK,EAAC,MAAM,CAAA;CAAC,CAAC,CAuCjD"}
|