browsertime 27.5.0 → 28.0.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 +27 -3
- package/bin/browsertime.js +28 -4
- package/lib/chrome/coverage.js +89 -19
- package/lib/chrome/mediawikiResourceLoader.js +223 -0
- package/lib/chrome/parseCpuTrace.js +87 -2
- package/lib/chrome/trace/blocking-time.js +189 -0
- package/lib/chrome/trace/domain-breakdown.js +86 -0
- package/lib/chrome/trace/frame-stability.js +135 -0
- package/lib/chrome/trace/function-costs.js +281 -0
- package/lib/chrome/trace/index.js +7 -0
- package/lib/chrome/trace/module-costs.js +137 -0
- package/lib/chrome/trace/non-composited-animations.js +50 -5
- package/lib/chrome/trace/selector-stats.js +110 -0
- package/lib/chrome/trace/style-invalidations.js +162 -0
- package/lib/chrome/trace/task-groups.js +28 -2
- package/lib/chrome/trace/timer-costs.js +183 -0
- package/lib/chrome/webdriver/chromium.js +114 -0
- package/lib/chrome/webdriver/traceUtilities.js +11 -1
- package/lib/core/engine/collector.js +12 -0
- package/lib/core/engine/index.js +6 -0
- package/lib/support/cli.js +1 -1
- package/lib/support/har/index.js +44 -1
- package/package.json +4 -4
- package/types/chrome/mediawikiResourceLoader.d.ts +11 -0
- package/types/chrome/mediawikiResourceLoader.d.ts.map +1 -0
- package/types/chrome/parseCpuTrace.d.ts +1 -26
- package/types/chrome/parseCpuTrace.d.ts.map +1 -1
- package/types/chrome/trace/blocking-time.d.ts +7 -0
- package/types/chrome/trace/blocking-time.d.ts.map +1 -0
- package/types/chrome/trace/domain-breakdown.d.ts +11 -0
- package/types/chrome/trace/domain-breakdown.d.ts.map +1 -0
- package/types/chrome/trace/frame-stability.d.ts +11 -0
- package/types/chrome/trace/frame-stability.d.ts.map +1 -0
- package/types/chrome/trace/function-costs.d.ts +6 -0
- package/types/chrome/trace/function-costs.d.ts.map +1 -0
- package/types/chrome/trace/index.d.ts +7 -0
- package/types/chrome/trace/index.d.ts.map +1 -1
- package/types/chrome/trace/module-costs.d.ts +20 -0
- package/types/chrome/trace/module-costs.d.ts.map +1 -0
- package/types/chrome/trace/non-composited-animations.d.ts +1 -25
- package/types/chrome/trace/non-composited-animations.d.ts.map +1 -1
- package/types/chrome/trace/selector-stats.d.ts +8 -0
- package/types/chrome/trace/selector-stats.d.ts.map +1 -0
- package/types/chrome/trace/style-invalidations.d.ts +9 -0
- package/types/chrome/trace/style-invalidations.d.ts.map +1 -0
- package/types/chrome/trace/task-groups.d.ts.map +1 -1
- package/types/chrome/trace/timer-costs.d.ts +10 -0
- package/types/chrome/trace/timer-costs.d.ts.map +1 -0
- package/types/chrome/webdriver/traceUtilities.d.ts.map +1 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timer pressure per script — how much setTimeout/setInterval churn
|
|
3
|
+
* a page runs, from trace events already collected with
|
|
4
|
+
* --chrome.timeline.
|
|
5
|
+
*
|
|
6
|
+
* Two sides are joined per URL:
|
|
7
|
+
* - TimerInstall events (instant): who schedules timers, how many,
|
|
8
|
+
* how many with timeout 0 (the "yield" pattern that turns into
|
|
9
|
+
* main-thread machine-gunning) and how many recurring
|
|
10
|
+
* (setInterval / singleShot: false). Attribution from the
|
|
11
|
+
* innermost stack frame with a URL.
|
|
12
|
+
* - TimerFire tasks from the main-thread task tree: what running
|
|
13
|
+
* those timers actually cost. main-thread-tasks.js already chains
|
|
14
|
+
* TimerFire back to the installing script's URLs, so the cost
|
|
15
|
+
* lands on the script that scheduled the timer even when the
|
|
16
|
+
* callback is anonymous.
|
|
17
|
+
*
|
|
18
|
+
* Polling loops show up as a high install/fire count with recurring
|
|
19
|
+
* installs on one URL; oversliced work shows up as hundreds of
|
|
20
|
+
* timeout-0 installs. Fire time is inclusive task duration — what
|
|
21
|
+
* the main thread was occupied with when the timer ran.
|
|
22
|
+
*
|
|
23
|
+
* Returns undefined when the trace has no timer events. Otherwise:
|
|
24
|
+
* { installs, fires, fireTime, timeoutZeroInstalls,
|
|
25
|
+
* recurringInstalls,
|
|
26
|
+
* byUrl: [{ url, installs, fires, fireTime,
|
|
27
|
+
* timeoutZeroInstalls, recurringInstalls }, …],
|
|
28
|
+
* sites: [{ url, line?, column?, functionName?, timeout,
|
|
29
|
+
* recurring, installs, fires, fireTime }, …] }
|
|
30
|
+
* fireTime in ms with one decimal; byUrl sorted by fireTime desc,
|
|
31
|
+
* capped at 10 entries with sub-0.1 ms/zero-install rows dropped.
|
|
32
|
+
* `sites` lists the individual timers grouped by install site
|
|
33
|
+
* (installer source position + timeout + kind) — the closest thing
|
|
34
|
+
* to a timer's identity the trace offers — top 10 by fire time.
|
|
35
|
+
* Positions are 1-based (trace stack-frame convention).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { compute } from './main-thread-tasks.js';
|
|
39
|
+
|
|
40
|
+
const MAX_URLS = 10;
|
|
41
|
+
const MAX_SITES = 10;
|
|
42
|
+
const UNKNOWN = 'unknown';
|
|
43
|
+
|
|
44
|
+
function round(ms) {
|
|
45
|
+
return Math.round(ms * 10) / 10;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function installFrame(data) {
|
|
49
|
+
for (const frame of data.stackTrace || []) {
|
|
50
|
+
if (frame.url) return frame;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function computeTimerCosts(trace) {
|
|
55
|
+
const byUrl = new Map();
|
|
56
|
+
// Individual timers grouped by install site (installer source
|
|
57
|
+
// position + timeout + kind): a polling loop reinstalling from the
|
|
58
|
+
// same line collapses into one row with its install/fire counts.
|
|
59
|
+
const bySite = new Map();
|
|
60
|
+
const siteForTimerId = new Map();
|
|
61
|
+
let installs = 0;
|
|
62
|
+
let fires = 0;
|
|
63
|
+
let fireTimeMs = 0;
|
|
64
|
+
let timeoutZeroInstalls = 0;
|
|
65
|
+
let recurringInstalls = 0;
|
|
66
|
+
|
|
67
|
+
function entryFor(url) {
|
|
68
|
+
let entry = byUrl.get(url);
|
|
69
|
+
if (!entry) {
|
|
70
|
+
entry = {
|
|
71
|
+
url,
|
|
72
|
+
installs: 0,
|
|
73
|
+
fires: 0,
|
|
74
|
+
fireTimeMs: 0,
|
|
75
|
+
timeoutZeroInstalls: 0,
|
|
76
|
+
recurringInstalls: 0
|
|
77
|
+
};
|
|
78
|
+
byUrl.set(url, entry);
|
|
79
|
+
}
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const event of trace.traceEvents) {
|
|
84
|
+
if (event.name !== 'TimerInstall') continue;
|
|
85
|
+
const data = (event.args && event.args.data) || {};
|
|
86
|
+
const frame = installFrame(data);
|
|
87
|
+
const entry = entryFor(frame ? frame.url : UNKNOWN);
|
|
88
|
+
installs++;
|
|
89
|
+
entry.installs++;
|
|
90
|
+
const timeout = data.timeout || 0;
|
|
91
|
+
if (timeout === 0) {
|
|
92
|
+
timeoutZeroInstalls++;
|
|
93
|
+
entry.timeoutZeroInstalls++;
|
|
94
|
+
}
|
|
95
|
+
const recurring = data.singleShot === false;
|
|
96
|
+
if (recurring) {
|
|
97
|
+
recurringInstalls++;
|
|
98
|
+
entry.recurringInstalls++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const siteKey = frame
|
|
102
|
+
? `${frame.url}|${frame.lineNumber}|${frame.columnNumber}|${timeout}|${recurring}`
|
|
103
|
+
: `${UNKNOWN}|${timeout}|${recurring}`;
|
|
104
|
+
let site = bySite.get(siteKey);
|
|
105
|
+
if (!site) {
|
|
106
|
+
site = {
|
|
107
|
+
url: frame ? frame.url : UNKNOWN,
|
|
108
|
+
timeout,
|
|
109
|
+
recurring,
|
|
110
|
+
installs: 0,
|
|
111
|
+
fires: 0,
|
|
112
|
+
fireTimeMs: 0
|
|
113
|
+
};
|
|
114
|
+
if (frame) {
|
|
115
|
+
site.line = frame.lineNumber;
|
|
116
|
+
site.column = frame.columnNumber;
|
|
117
|
+
if (frame.functionName) site.functionName = frame.functionName;
|
|
118
|
+
}
|
|
119
|
+
bySite.set(siteKey, site);
|
|
120
|
+
}
|
|
121
|
+
site.installs++;
|
|
122
|
+
if (data.timerId !== undefined) siteForTimerId.set(data.timerId, site);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const tasks = compute(trace);
|
|
126
|
+
for (const task of tasks) {
|
|
127
|
+
if (task.event.name !== 'TimerFire') continue;
|
|
128
|
+
const entry = entryFor(task.attributableURLs.at(-1) || UNKNOWN);
|
|
129
|
+
fires++;
|
|
130
|
+
fireTimeMs += task.duration;
|
|
131
|
+
entry.fires++;
|
|
132
|
+
entry.fireTimeMs += task.duration;
|
|
133
|
+
const fireData = (task.event.args && task.event.args.data) || {};
|
|
134
|
+
const site = siteForTimerId.get(fireData.timerId);
|
|
135
|
+
if (site) {
|
|
136
|
+
site.fires++;
|
|
137
|
+
site.fireTimeMs += task.duration;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (installs === 0 && fires === 0) return;
|
|
142
|
+
|
|
143
|
+
const urls = [...byUrl.values()]
|
|
144
|
+
.filter(entry => entry.fireTimeMs >= 0.1 || entry.installs > 0)
|
|
145
|
+
.toSorted((a, b) => b.fireTimeMs - a.fireTimeMs || b.installs - a.installs)
|
|
146
|
+
.slice(0, MAX_URLS)
|
|
147
|
+
.map(entry => ({
|
|
148
|
+
url: entry.url,
|
|
149
|
+
installs: entry.installs,
|
|
150
|
+
fires: entry.fires,
|
|
151
|
+
fireTime: round(entry.fireTimeMs),
|
|
152
|
+
timeoutZeroInstalls: entry.timeoutZeroInstalls,
|
|
153
|
+
recurringInstalls: entry.recurringInstalls
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
const sites = [...bySite.values()]
|
|
157
|
+
.toSorted((a, b) => b.fireTimeMs - a.fireTimeMs || b.installs - a.installs)
|
|
158
|
+
.slice(0, MAX_SITES)
|
|
159
|
+
.map(site => {
|
|
160
|
+
const published = {
|
|
161
|
+
url: site.url,
|
|
162
|
+
timeout: site.timeout,
|
|
163
|
+
recurring: site.recurring,
|
|
164
|
+
installs: site.installs,
|
|
165
|
+
fires: site.fires,
|
|
166
|
+
fireTime: round(site.fireTimeMs)
|
|
167
|
+
};
|
|
168
|
+
if (site.line !== undefined) published.line = site.line;
|
|
169
|
+
if (site.column !== undefined) published.column = site.column;
|
|
170
|
+
if (site.functionName) published.functionName = site.functionName;
|
|
171
|
+
return published;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
installs,
|
|
176
|
+
fires,
|
|
177
|
+
fireTime: round(fireTimeMs),
|
|
178
|
+
timeoutZeroInstalls,
|
|
179
|
+
recurringInstalls,
|
|
180
|
+
byUrl: urls,
|
|
181
|
+
sites
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -7,6 +7,10 @@ const log = getLogger('browsertime.chrome');
|
|
|
7
7
|
const { Type } = logging;
|
|
8
8
|
import { longTaskMetrics } from '../longTaskMetrics.js';
|
|
9
9
|
import { parseCPUTrace } from '../parseCpuTrace.js';
|
|
10
|
+
import { computeModuleCosts } from '../trace/module-costs.js';
|
|
11
|
+
import { computeFunctionCosts } from '../trace/function-costs.js';
|
|
12
|
+
import { computeStyleInvalidations } from '../trace/style-invalidations.js';
|
|
13
|
+
import { labelForUrl } from '../mediawikiResourceLoader.js';
|
|
10
14
|
import { getHar } from '../har.js';
|
|
11
15
|
import { getEmptyHAR, mergeHars } from '../../support/har/index.js';
|
|
12
16
|
import { toArray } from '../../support/util.js';
|
|
@@ -335,6 +339,12 @@ export class Chromium {
|
|
|
335
339
|
const coverage = await this.coverage.collect();
|
|
336
340
|
this.coverage = undefined;
|
|
337
341
|
if (coverage) {
|
|
342
|
+
// Bundle location resolvers are plumbing for the CPU trace
|
|
343
|
+
// parser (per-module CPU attribution), not part of the
|
|
344
|
+
// coverage result contract — lift them off before the
|
|
345
|
+
// coverage object is serialized.
|
|
346
|
+
this.resourceLoaderBundles = coverage.resourceLoaderBundles;
|
|
347
|
+
delete coverage.resourceLoaderBundles;
|
|
338
348
|
result.coverage = coverage;
|
|
339
349
|
}
|
|
340
350
|
}
|
|
@@ -416,6 +426,110 @@ export class Chromium {
|
|
|
416
426
|
result.extraJson[name] = trace;
|
|
417
427
|
|
|
418
428
|
const cpu = await parseCPUTrace(trace, result.url);
|
|
429
|
+
|
|
430
|
+
// Per-module CPU inside concatenated bundles (MediaWiki
|
|
431
|
+
// ResourceLoader). The only analysis that needs both the trace
|
|
432
|
+
// and the bundle boundaries from the coverage path, so it runs
|
|
433
|
+
// here where the two meet — only when coverage found bundles on
|
|
434
|
+
// the page, otherwise zero cost and no output key.
|
|
435
|
+
if (this.resourceLoaderBundles && this.resourceLoaderBundles.size > 0) {
|
|
436
|
+
try {
|
|
437
|
+
const moduleCosts = computeModuleCosts(
|
|
438
|
+
trace,
|
|
439
|
+
this.resourceLoaderBundles
|
|
440
|
+
);
|
|
441
|
+
if (moduleCosts.length > 0) {
|
|
442
|
+
for (const bundle of moduleCosts) {
|
|
443
|
+
const label = labelForUrl(bundle.url);
|
|
444
|
+
if (label) {
|
|
445
|
+
bundle.label = label;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
cpu.moduleCosts = moduleCosts;
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
log.debug('computeModuleCosts failed: %s', error.message);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// Per-function self/total time from the V8 sampling profiler —
|
|
455
|
+
// only produces output on runs with the cpu_profiler trace
|
|
456
|
+
// category (--enableProfileRun). Reuses the bundle resolvers so
|
|
457
|
+
// functions inside concatenated bundles get a module name.
|
|
458
|
+
try {
|
|
459
|
+
const functionCosts = computeFunctionCosts(
|
|
460
|
+
trace,
|
|
461
|
+
this.resourceLoaderBundles
|
|
462
|
+
);
|
|
463
|
+
if (functionCosts.length > 0) {
|
|
464
|
+
for (const functionCost of functionCosts) {
|
|
465
|
+
const label = labelForUrl(functionCost.url);
|
|
466
|
+
if (label) {
|
|
467
|
+
functionCost.label = label;
|
|
468
|
+
}
|
|
469
|
+
for (const callee of functionCost.callees || []) {
|
|
470
|
+
const calleeLabel = labelForUrl(callee.url);
|
|
471
|
+
if (calleeLabel) {
|
|
472
|
+
callee.label = calleeLabel;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
cpu.functionCosts = functionCosts;
|
|
477
|
+
}
|
|
478
|
+
} catch (error) {
|
|
479
|
+
log.debug('computeFunctionCosts failed: %s', error.message);
|
|
480
|
+
}
|
|
481
|
+
// Invalidation sources resolve to modules the same way —
|
|
482
|
+
// thousands of invalidations from one giant load.php URL say
|
|
483
|
+
// nothing, the per-module split says whose code churns the
|
|
484
|
+
// DOM. Recomputing with the bundles replaces the unresolved
|
|
485
|
+
// sources parseCpuTrace produced.
|
|
486
|
+
if (
|
|
487
|
+
cpu.styleInvalidations &&
|
|
488
|
+
this.resourceLoaderBundles &&
|
|
489
|
+
this.resourceLoaderBundles.size > 0
|
|
490
|
+
) {
|
|
491
|
+
try {
|
|
492
|
+
const enriched = computeStyleInvalidations(
|
|
493
|
+
trace,
|
|
494
|
+
this.resourceLoaderBundles
|
|
495
|
+
);
|
|
496
|
+
if (enriched) {
|
|
497
|
+
for (const source of enriched.sources) {
|
|
498
|
+
const label = labelForUrl(source.url);
|
|
499
|
+
if (label) {
|
|
500
|
+
source.label = label;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
cpu.styleInvalidations = enriched;
|
|
504
|
+
}
|
|
505
|
+
} catch (error) {
|
|
506
|
+
log.debug(
|
|
507
|
+
'computeStyleInvalidations with bundles failed: %s',
|
|
508
|
+
error.message
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Timer install sites inside concatenated bundles resolve to
|
|
513
|
+
// the owning module — "load.php[scripts]:418" says nothing,
|
|
514
|
+
// the module name says whose timer it is. Trace stack-frame
|
|
515
|
+
// positions are 1-based, matching the resolvers.
|
|
516
|
+
if (
|
|
517
|
+
cpu.timers &&
|
|
518
|
+
cpu.timers.sites &&
|
|
519
|
+
this.resourceLoaderBundles &&
|
|
520
|
+
this.resourceLoaderBundles.size > 0
|
|
521
|
+
) {
|
|
522
|
+
for (const site of cpu.timers.sites) {
|
|
523
|
+
if (site.line === undefined) continue;
|
|
524
|
+
const resolver = this.resourceLoaderBundles.get(site.url);
|
|
525
|
+
if (!resolver) continue;
|
|
526
|
+
const module_ = resolver.resolve(site.line, site.column);
|
|
527
|
+
if (module_) {
|
|
528
|
+
site.module = module_.name;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
this.resourceLoaderBundles = undefined;
|
|
419
533
|
result.cpu = cpu;
|
|
420
534
|
|
|
421
535
|
// Collect render blocking info
|
|
@@ -52,12 +52,22 @@ function getRecalculateStyleElementsAndTimeBefore(traceEvents, timestamp) {
|
|
|
52
52
|
);
|
|
53
53
|
let elements = 0;
|
|
54
54
|
let duration = 0;
|
|
55
|
+
let maxElements = 0;
|
|
56
|
+
let maxDuration = 0;
|
|
55
57
|
for (let a of updateLayoutTree) {
|
|
56
58
|
elements += a.args.elementCount;
|
|
57
59
|
duration += a.dur;
|
|
60
|
+
maxElements = Math.max(maxElements, a.args.elementCount);
|
|
61
|
+
maxDuration = Math.max(maxDuration, a.dur);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
count: updateLayoutTree.length,
|
|
66
|
+
elements,
|
|
67
|
+
durationInMillis: duration / 1000,
|
|
68
|
+
maxElements,
|
|
69
|
+
maxDurationInMillis: maxDuration / 1000
|
|
70
|
+
};
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export async function getRenderBlocking(trace) {
|
|
@@ -136,6 +136,18 @@ export class Collector {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Header values are strings so they are kept out of the statistics.
|
|
140
|
+
addMainDocument(url, mainDocument) {
|
|
141
|
+
const results =
|
|
142
|
+
this.allResults[url] || this.allResults[this.urlAndActualUrl[url]];
|
|
143
|
+
if (results) {
|
|
144
|
+
if (!results.mainDocument) {
|
|
145
|
+
results.mainDocument = [];
|
|
146
|
+
}
|
|
147
|
+
results.mainDocument.push(mainDocument);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
/**
|
|
140
152
|
* Collect all individual runs, add it to the statistics, and store
|
|
141
153
|
* data that needs to be on disk (trace logs, sceenshots etc).
|
package/lib/core/engine/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
getFullyLoaded,
|
|
20
20
|
getMainDocumentTimings,
|
|
21
|
+
getMainDocuments,
|
|
21
22
|
addExtraFieldsToHar
|
|
22
23
|
} from '../../support/har/index.js';
|
|
23
24
|
import { XVFB } from '../../support/xvfb.js';
|
|
@@ -374,6 +375,11 @@ export class Engine {
|
|
|
374
375
|
for (let timing of timings) {
|
|
375
376
|
collector.addMainDocumentTimings(timing.url, timing.timings);
|
|
376
377
|
}
|
|
378
|
+
|
|
379
|
+
const mainDocuments = getMainDocuments(extras.har);
|
|
380
|
+
for (let document of mainDocuments) {
|
|
381
|
+
collector.addMainDocument(document.url, document.mainDocument);
|
|
382
|
+
}
|
|
377
383
|
}
|
|
378
384
|
}
|
|
379
385
|
|
package/lib/support/cli.js
CHANGED
|
@@ -51,7 +51,7 @@ function hasbin(bin) {
|
|
|
51
51
|
function validateInput(argv) {
|
|
52
52
|
// Check NodeJS major version
|
|
53
53
|
const fullVersion = process.versions.node;
|
|
54
|
-
const minVersion =
|
|
54
|
+
const minVersion = 22;
|
|
55
55
|
const majorVersion = fullVersion.split('.')[0];
|
|
56
56
|
if (majorVersion < minVersion) {
|
|
57
57
|
return (
|
package/lib/support/har/index.js
CHANGED
|
@@ -269,6 +269,45 @@ export function getMainDocumentTimings(har) {
|
|
|
269
269
|
return [];
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
+
export function getMainDocuments(har) {
|
|
273
|
+
// Sometimes the HAR is dirty
|
|
274
|
+
try {
|
|
275
|
+
const mainDocuments = [];
|
|
276
|
+
for (let page of har.log.pages) {
|
|
277
|
+
const url = page._url;
|
|
278
|
+
if (url === undefined) continue;
|
|
279
|
+
|
|
280
|
+
const requests = getDocumentRequests(har.log.entries, page.id);
|
|
281
|
+
const finalEntry = requests.at(-1);
|
|
282
|
+
if (!finalEntry) continue;
|
|
283
|
+
|
|
284
|
+
const headers = {};
|
|
285
|
+
for (let header of finalEntry.response.headers || []) {
|
|
286
|
+
const name = header.name.toLowerCase();
|
|
287
|
+
// Always drop sensitive headers (the same list as
|
|
288
|
+
// --cleanSensitiveHeaders uses) since browsertime.json often ends
|
|
289
|
+
// up in dashboards even when the HAR stays local.
|
|
290
|
+
if (isSensitiveHeader(name)) continue;
|
|
291
|
+
headers[name] =
|
|
292
|
+
name in headers ? headers[name] + ', ' + header.value : header.value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
mainDocuments.push({
|
|
296
|
+
url,
|
|
297
|
+
mainDocument: {
|
|
298
|
+
url: finalEntry.request.url,
|
|
299
|
+
status: finalEntry.response.status,
|
|
300
|
+
redirects: requests.length - 1,
|
|
301
|
+
headers
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return mainDocuments;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
log.error('Could not get main document headers ' + error);
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
272
311
|
export function getFullyLoaded(har) {
|
|
273
312
|
const fullyLoaded = [];
|
|
274
313
|
const entries = [...har.log.entries];
|
|
@@ -464,8 +503,12 @@ export function addExtraFieldsToHar(totalResults, har, options) {
|
|
|
464
503
|
}
|
|
465
504
|
}
|
|
466
505
|
|
|
506
|
+
export function isSensitiveHeader(name) {
|
|
507
|
+
return sensitiveHeaders.has(name.toLowerCase());
|
|
508
|
+
}
|
|
509
|
+
|
|
467
510
|
export function cleanSensitiveHeaders(name, value) {
|
|
468
|
-
if (
|
|
511
|
+
if (isSensitiveHeader(name)) {
|
|
469
512
|
return '[REMOVED]';
|
|
470
513
|
}
|
|
471
514
|
return value;
|
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": "
|
|
4
|
+
"version": "28.0.0",
|
|
5
5
|
"bin": "./bin/browsertime.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"types": "./scripting.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@sitespeed.io/edgedriver": "149.0.4022",
|
|
12
12
|
"@sitespeed.io/geckodriver": "0.36.2",
|
|
13
13
|
"@sitespeed.io/log": "2.0.0",
|
|
14
|
-
"@sitespeed.io/throttle": "6.0.
|
|
14
|
+
"@sitespeed.io/throttle": "6.0.1",
|
|
15
15
|
"chrome-har": "1.3.1",
|
|
16
16
|
"chrome-remote-interface": "0.34.0",
|
|
17
17
|
"execa": "9.6.1",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"typescript": "5.9.3"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
43
|
+
"node": ">=22.0.0"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"CHANGELOG.md",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"url": "https://github.com/sitespeedio/browsertime.git"
|
|
93
93
|
},
|
|
94
94
|
"homepage": "https://www.sitespeed.io/documentation/browsertime/",
|
|
95
|
-
"license": "
|
|
95
|
+
"license": "Apache-2.0",
|
|
96
96
|
"ava": {
|
|
97
97
|
"files": [
|
|
98
98
|
"test/**/*",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function isResourceLoaderBundle(url: any, source: any): any;
|
|
2
|
+
export function labelForUrl(url: any): string;
|
|
3
|
+
export function labelForStyleAttributes(attributes: any): any;
|
|
4
|
+
export function resourceLoaderLocationResolver(source: any): {
|
|
5
|
+
resolve(lineNumber: any, columnNumber: any): {
|
|
6
|
+
name: any;
|
|
7
|
+
version: any;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export function resourceLoaderModuleCoverage(source: any, painted: any): any;
|
|
11
|
+
//# sourceMappingURL=mediawikiResourceLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediawikiResourceLoader.d.ts","sourceRoot":"","sources":["../../lib/chrome/mediawikiResourceLoader.js"],"names":[],"mappings":"AAmBA,mEAEC;AA6DD,8CAuBC;AAcD,8DAUC;AAwBD;;;;;EAkCC;AAOD,6EA4BC"}
|
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
export function parseCPUTrace(tracelog: any, url: any): Promise<{
|
|
2
|
-
categories: {
|
|
3
|
-
parseHTML: number;
|
|
4
|
-
styleLayout: number;
|
|
5
|
-
paintCompositeRender: number;
|
|
6
|
-
scriptParseCompile: number;
|
|
7
|
-
scriptEvaluation: number;
|
|
8
|
-
garbageCollection: number;
|
|
9
|
-
other: number;
|
|
10
|
-
};
|
|
11
|
-
events: {};
|
|
12
|
-
urls: {
|
|
13
|
-
url: string;
|
|
14
|
-
value: number;
|
|
15
|
-
}[];
|
|
16
|
-
scriptCosts: any[];
|
|
17
|
-
forcedReflows: any[];
|
|
18
|
-
nonCompositedAnimations: any[];
|
|
19
|
-
} | {
|
|
20
|
-
categories?: undefined;
|
|
21
|
-
events?: undefined;
|
|
22
|
-
urls?: undefined;
|
|
23
|
-
scriptCosts?: undefined;
|
|
24
|
-
forcedReflows?: undefined;
|
|
25
|
-
nonCompositedAnimations?: undefined;
|
|
26
|
-
}>;
|
|
1
|
+
export function parseCPUTrace(tracelog: any, url: any): Promise<{}>;
|
|
27
2
|
//# 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":"AAoCA,oEA8KC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocking-time.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/blocking-time.js"],"names":[],"mappings":"AAqJA;;;;;EAuCC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function computeDomainBreakdown(trace: any, pageUrl: any): {
|
|
2
|
+
firstPartyDomain: any;
|
|
3
|
+
firstParty: number;
|
|
4
|
+
thirdParty: number;
|
|
5
|
+
domains: {
|
|
6
|
+
domain: any;
|
|
7
|
+
value: number;
|
|
8
|
+
firstParty: boolean;
|
|
9
|
+
}[];
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=domain-breakdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-breakdown.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/domain-breakdown.js"],"names":[],"mappings":"AAmDA;;;;;;;;;EAkCC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-stability.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/frame-stability.js"],"names":[],"mappings":"AAwDA;;;;;;;;;EA8EC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"function-costs.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/function-costs.js"],"names":[],"mappings":"AAyFA;;;;IA+LC"}
|
|
@@ -16,4 +16,11 @@ export function computeMainThreadTasks(trace: any, options?: {
|
|
|
16
16
|
export { computeScriptCosts } from "./script-costs.js";
|
|
17
17
|
export { computeForcedReflows } from "./forced-reflows.js";
|
|
18
18
|
export { computeNonCompositedAnimations } from "./non-composited-animations.js";
|
|
19
|
+
export { computeFrameStability } from "./frame-stability.js";
|
|
20
|
+
export { computeFunctionCosts } from "./function-costs.js";
|
|
21
|
+
export { computeStyleInvalidations } from "./style-invalidations.js";
|
|
22
|
+
export { computeSelectorStats } from "./selector-stats.js";
|
|
23
|
+
export { computeTimerCosts } from "./timer-costs.js";
|
|
24
|
+
export { computeBlockingTime } from "./blocking-time.js";
|
|
25
|
+
export { computeDomainBreakdown } from "./domain-breakdown.js";
|
|
19
26
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/index.js"],"names":[],"mappings":"AAyBA;;;;;;;;;;;GAWG;AACH,6DAJG;IAA0B,OAAO,GAAzB,OAAO;CAEf,GAAU,KAAK,KAAQ,CAYzB"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} trace Parsed Chrome trace.json (with .traceEvents).
|
|
3
|
+
* @param {Map<string, {resolve: Function}>} bundles Bundle URL →
|
|
4
|
+
* location resolver, built while collecting coverage.
|
|
5
|
+
* @returns {Array<{url:string,
|
|
6
|
+
* modules:Array<{name:string, version:string, selfTime:number}>,
|
|
7
|
+
* otherTime:number}>}
|
|
8
|
+
*/
|
|
9
|
+
export function computeModuleCosts(trace: any, bundles: Map<string, {
|
|
10
|
+
resolve: Function;
|
|
11
|
+
}>): Array<{
|
|
12
|
+
url: string;
|
|
13
|
+
modules: Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
selfTime: number;
|
|
17
|
+
}>;
|
|
18
|
+
otherTime: number;
|
|
19
|
+
}>;
|
|
20
|
+
//# sourceMappingURL=module-costs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-costs.d.ts","sourceRoot":"","sources":["../../../lib/chrome/trace/module-costs.js"],"names":[],"mappings":"AAqFA;;;;;;;GAOG;AACH,wDANW,GAAG,CAAC,MAAM,EAAE;IAAC,OAAO,WAAU;CAAC,CAAC,GAE9B,KAAK,CAAC;IAAC,GAAG,EAAC,MAAM,CAAC;IAC1B,OAAO,EAAC,KAAK,CAAC;QAAC,IAAI,EAAC,MAAM,CAAC;QAAC,OAAO,EAAC,MAAM,CAAC;QAAC,QAAQ,EAAC,MAAM,CAAA;KAAC,CAAC,CAAC;IAC9D,SAAS,EAAC,MAAM,CAAA;CAAC,CAAC,CA6CtB"}
|
|
@@ -1,32 +1,8 @@
|
|
|
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
1
|
export function computeNonCompositedAnimations(trace: any): {
|
|
27
2
|
name: any;
|
|
28
3
|
id: any;
|
|
29
4
|
compositeFailed: any;
|
|
5
|
+
failureReasons: string[];
|
|
30
6
|
unsupportedProperties: any;
|
|
31
7
|
startTime: any;
|
|
32
8
|
}[];
|