webperf-snippets 1.10.0 → 1.12.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/README.md +25 -7
- package/package.json +1 -1
- package/snippets/CoreWebVitals/INP.js +20 -4
- package/snippets/CoreWebVitals/LCP-Trail.js +52 -40
- package/snippets/Interaction/Input-Latency-Breakdown.js +3 -0
- package/snippets/Interaction/Interactions.js +4 -0
- package/snippets/SCHEMA.md +33 -5
- package/src/baseline.js +51 -12
- package/src/bin.js +20 -19
- package/src/crux.js +11 -4
- package/src/mcp/baseline.js +26 -0
- package/src/mcp/queue.js +72 -0
- package/src/mcp/server.js +291 -59
- package/src/mcp/summary.js +30 -8
- package/src/runner.js +26 -1
- package/src/runs.js +22 -0
- package/src/snippet-names.js +90 -19
package/src/bin.js
CHANGED
|
@@ -15,7 +15,7 @@ import { MissingBrowserError } from "./browser.js";
|
|
|
15
15
|
import { parseHeader } from "./headers.js";
|
|
16
16
|
import { NETWORK_PRESETS, parseCpuFactor } from "./throttling.js";
|
|
17
17
|
import { parseOwnDomains } from "./own-domains.js";
|
|
18
|
-
import { MEDIAN_METRICS,
|
|
18
|
+
import { MEDIAN_METRICS, measureRuns } from "./runs.js";
|
|
19
19
|
import { loadMatrix } from "./matrix.js";
|
|
20
20
|
import { blockingErrors } from "./blocking.js";
|
|
21
21
|
import { MEASURED_BY, checkBudgets, parseBudgets, unmeasuredMetrics, violationsOf } from "./budgets.js";
|
|
@@ -89,6 +89,7 @@ Options:
|
|
|
89
89
|
--verbose Show all items, even for passing checks
|
|
90
90
|
--headed Show the browser window (debug)
|
|
91
91
|
-h, --help Show this help
|
|
92
|
+
-v, --version Show the version
|
|
92
93
|
|
|
93
94
|
Examples:
|
|
94
95
|
npx webperf-snippets https://example.com
|
|
@@ -182,6 +183,7 @@ async function main() {
|
|
|
182
183
|
verbose: { type: "boolean" },
|
|
183
184
|
headed: { type: "boolean" },
|
|
184
185
|
help: { type: "boolean", short: "h" },
|
|
186
|
+
version: { type: "boolean", short: "v" },
|
|
185
187
|
},
|
|
186
188
|
allowPositionals: true,
|
|
187
189
|
});
|
|
@@ -196,6 +198,11 @@ async function main() {
|
|
|
196
198
|
return;
|
|
197
199
|
}
|
|
198
200
|
|
|
201
|
+
if (values.version) {
|
|
202
|
+
process.stdout.write(`${cliVersion}\n`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
199
206
|
const url = positionals[0];
|
|
200
207
|
const matrixPath = values.matrix;
|
|
201
208
|
if (url && matrixPath) fail("Use a URL or --matrix, not both");
|
|
@@ -325,7 +332,7 @@ async function main() {
|
|
|
325
332
|
if (runCount == null) {
|
|
326
333
|
payload = await measureWith(conditions);
|
|
327
334
|
} else {
|
|
328
|
-
const median = await
|
|
335
|
+
const median = await measureRunsWithProgress(() => measureWith(conditions), runCount, medianBy, (i) => `[${i}/${runCount}] ${url} · run ${i}/${runCount}`);
|
|
329
336
|
if (!median.payload) throw new Error(`Every run failed: ${median.errors.join("; ")}`);
|
|
330
337
|
payload = { ...median.payload, runs: median.runs };
|
|
331
338
|
}
|
|
@@ -388,22 +395,16 @@ async function main() {
|
|
|
388
395
|
process.exit(blockingErrors(payload.results).length > 0 ? 1 : 0);
|
|
389
396
|
}
|
|
390
397
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (err instanceof MissingBrowserError) throw err;
|
|
402
|
-
runs.push({ error: err.message });
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return summarizeRuns(runs, { medianBy });
|
|
406
|
-
}
|
|
398
|
+
// Each run is reported on stderr as it starts, so stdout keeps only the report
|
|
399
|
+
const measureRunsWithProgress = (measure, count, medianBy, progress) =>
|
|
400
|
+
measureRuns(
|
|
401
|
+
(i) => {
|
|
402
|
+
process.stderr.write(`${progress(i)}\n`);
|
|
403
|
+
return measure();
|
|
404
|
+
},
|
|
405
|
+
count,
|
|
406
|
+
{ medianBy, isFatal: (err) => err instanceof MissingBrowserError }
|
|
407
|
+
);
|
|
407
408
|
|
|
408
409
|
// Every combination of the matrix, in order, each one `matrix.runs` times. Each combination reports
|
|
409
410
|
// its median run, and the exit code follows the same rules as one page, for every combination.
|
|
@@ -417,7 +418,7 @@ async function runMatrix(matrixPath, matrix, measureWith, { values, waitMs, repo
|
|
|
417
418
|
const combinations = [];
|
|
418
419
|
for (const c of matrix.combinations) {
|
|
419
420
|
const conditions = { url: c.url, viewport: VIEWPORT_PRESETS[c.viewport], storageState: c.storageState, headers: c.headers, cpu: c.cpu, network: c.network, ownDomains: c.ownDomains };
|
|
420
|
-
const median = await
|
|
421
|
+
const median = await measureRunsWithProgress(() => measureWith(conditions), matrix.runs, matrix.medianBy, (i) => `[${String(++started).padStart(width)}/${total}] ${c.id} · run ${i}/${matrix.runs}`);
|
|
421
422
|
const identity = { id: c.id, name: c.name, device: c.device, ...(c.variant ? { variant: c.variant } : {}), url: c.url };
|
|
422
423
|
combinations.push(
|
|
423
424
|
median.payload
|
package/src/crux.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_THRESHOLDS } from "./baseline.js";
|
|
1
2
|
import { rate } from "./ratings.js";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_ENDPOINT = "https://chromeuxreport.googleapis.com/v1/records:queryRecord";
|
|
@@ -122,12 +123,17 @@ export function trendOf(p75s) {
|
|
|
122
123
|
|
|
123
124
|
const BARS = "▁▂▃▄▅▆▇█";
|
|
124
125
|
|
|
125
|
-
// The series as bars
|
|
126
|
-
|
|
126
|
+
// The series as bars, a space for a period without data. The bars span the range of the series, or
|
|
127
|
+
// `minRange` when the series is narrower, centred in it: a CLS of 0.20 to 0.21 drawn from its lowest
|
|
128
|
+
// to its highest value looked like a big swing. Eight equal steps, so a flat series sits in the middle.
|
|
129
|
+
export function sparkline(values, minRange = 0) {
|
|
127
130
|
const known = values.filter((v) => v !== null);
|
|
128
131
|
const min = Math.min(...known);
|
|
129
132
|
const max = Math.max(...known);
|
|
130
|
-
|
|
133
|
+
const range = Math.max(max - min, minRange);
|
|
134
|
+
if (range === 0) return values.map((v) => (v === null ? " " : BARS[4])).join("");
|
|
135
|
+
const low = max - min >= minRange ? min : (min + max) / 2 - range / 2;
|
|
136
|
+
return values.map((v) => (v === null ? " " : BARS[Math.min(7, Math.floor(((v - low) / range) * 8))])).join("");
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
const dateOf = ({ year, month, day }) => `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
@@ -202,7 +208,8 @@ export function historyView(history) {
|
|
|
202
208
|
const rows = METRICS.filter((m) => history.metrics[m.name]).map((m) => {
|
|
203
209
|
const { p75s, first, last, change, trend } = history.metrics[m.name];
|
|
204
210
|
const changeText = change === null ? "·" : `${change > 0 ? "▲ +" : change < 0 ? "▼ −" : "= "}${Math.abs(change)}%`;
|
|
205
|
-
|
|
211
|
+
// The smallest change --baseline counts for the metric is the least the bars span
|
|
212
|
+
return { metric: m.name, unit: m.unit, first, last, change: changeText, trend: trend ?? "·", sparkline: sparkline(p75s, DEFAULT_THRESHOLDS[m.name].absolute) };
|
|
206
213
|
});
|
|
207
214
|
return { title, rows };
|
|
208
215
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { loadBaseline, compareRun, parseThreshold, thresholdsWith } from "../baseline.js";
|
|
2
|
+
import { describeRegressions } from "../reporters/baseline-view.js";
|
|
3
|
+
|
|
4
|
+
// The baseline of run_snippet and run_workflow, as --baseline of the CLI: the runId of an earlier run of
|
|
5
|
+
// the connection, or the path of a page the CLI wrote with --json. The comparison is the one of the CLI
|
|
6
|
+
// (baseline.js), so both give the same verdict for the same two pages.
|
|
7
|
+
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
|
|
10
|
+
// Read and checked before the page loads, as the CLI does before the browser opens: a wrong baseline
|
|
11
|
+
// fails at once. Throws an error whose message says what to do.
|
|
12
|
+
export function prepareBaseline({ baseline, thresholds = [] }, runs) {
|
|
13
|
+
const parsed = thresholdsWith(thresholds.map(parseThreshold));
|
|
14
|
+
// An id of a run the connection no longer keeps is not a file to look for: runs.page says so
|
|
15
|
+
if (runs.has(baseline) || UUID.test(baseline)) return { data: runs.page(baseline), source: { runId: baseline }, thresholds: parsed };
|
|
16
|
+
const { kind, data } = loadBaseline(baseline);
|
|
17
|
+
if (kind === "matrix") throw new Error(`${baseline} is the baseline of a matrix: this server measures one page; compare it with webperf-snippets --matrix <file> --baseline ${baseline}`);
|
|
18
|
+
return { data, source: { file: baseline }, thresholds: parsed };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The page of this run against its baseline: the `baseline` of the JSON of the CLI, named by its runId or
|
|
22
|
+
// its file, with the lines --fail-on-regression would print for it
|
|
23
|
+
export function compareWithBaseline({ data, source, thresholds }, current) {
|
|
24
|
+
const { file, ...comparison } = compareRun(data, current, { file: source.file, thresholds });
|
|
25
|
+
return { ...source, ...comparison, regressions: describeRegressions(comparison) };
|
|
26
|
+
}
|
package/src/mcp/queue.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The reason of an abort is whatever the client sent, often a string, or nothing: a caller gets an error
|
|
2
|
+
const gaveUp = () => new Error("The caller gave up while it waited for its turn");
|
|
3
|
+
|
|
4
|
+
// One task at a time, in the order they arrive. A caller that gives up while it waits leaves the line
|
|
5
|
+
// at once, so it never runs and holds up no one. A task that fails passes the turn like any other, and
|
|
6
|
+
// its failure stays with its caller.
|
|
7
|
+
//
|
|
8
|
+
// A waiting caller hears how many tasks are ahead when it starts to wait, whenever the line moves, and
|
|
9
|
+
// every `reportEveryMs` while it does not: a client can restart its timeout on each report, and a task
|
|
10
|
+
// ahead can take minutes.
|
|
11
|
+
export function createQueue({ reportEveryMs = 5000 } = {}) {
|
|
12
|
+
const waiting = [];
|
|
13
|
+
let taken = false;
|
|
14
|
+
|
|
15
|
+
// A report is a courtesy: one that fails, as one to a client that left, changes nothing
|
|
16
|
+
const report = async ({ onWait }, ahead) => {
|
|
17
|
+
try {
|
|
18
|
+
await onWait?.(ahead);
|
|
19
|
+
} catch {
|
|
20
|
+
// nothing to tell, and no one to tell it to
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const reportAll = () => waiting.forEach((waiter, i) => report(waiter, i + 1));
|
|
24
|
+
|
|
25
|
+
function take({ signal, onWait }) {
|
|
26
|
+
if (signal?.aborted) return Promise.reject(gaveUp());
|
|
27
|
+
if (!taken) {
|
|
28
|
+
taken = true;
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
}
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const waiter = { onWait };
|
|
33
|
+
const leave = () => {
|
|
34
|
+
waiting.splice(waiting.indexOf(waiter), 1);
|
|
35
|
+
clearInterval(timer);
|
|
36
|
+
signal?.removeEventListener("abort", cancel);
|
|
37
|
+
};
|
|
38
|
+
const cancel = () => {
|
|
39
|
+
leave();
|
|
40
|
+
reject(gaveUp());
|
|
41
|
+
reportAll();
|
|
42
|
+
};
|
|
43
|
+
waiter.enter = () => {
|
|
44
|
+
leave();
|
|
45
|
+
resolve();
|
|
46
|
+
};
|
|
47
|
+
const timer = setInterval(() => report(waiter, waiting.indexOf(waiter) + 1), reportEveryMs);
|
|
48
|
+
// The timer alone must not keep the process alive
|
|
49
|
+
timer.unref?.();
|
|
50
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
51
|
+
waiting.push(waiter);
|
|
52
|
+
report(waiter, waiting.length);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pass() {
|
|
57
|
+
taken = waiting.length > 0;
|
|
58
|
+
waiting[0]?.enter();
|
|
59
|
+
reportAll();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function run(task, options = {}) {
|
|
63
|
+
await take(options);
|
|
64
|
+
try {
|
|
65
|
+
return await task();
|
|
66
|
+
} finally {
|
|
67
|
+
pass();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { run };
|
|
72
|
+
}
|