webperf-snippets 1.16.0 → 1.17.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 +56 -10
- package/package.json +3 -2
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +190 -188
- package/snippets/Loading/Content-Visibility.js +273 -264
- package/snippets/SCHEMA.md +13 -0
- package/src/baseline.js +21 -6
- package/src/bin.js +112 -34
- package/src/interactions.js +63 -6
- package/src/live-baseline.js +76 -0
- package/src/mcp/server.js +2 -9
- package/src/mcp/steps.js +12 -0
- package/src/reporters/baseline-view.js +39 -9
- package/src/reporters/human.js +8 -2
- package/src/reporters/markdown.js +11 -4
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { inconclusiveForTheWorse } from "../baseline.js";
|
|
2
|
+
|
|
1
3
|
// The comparison with a baseline as the human and markdown reports draw it: one row per metric with
|
|
2
|
-
// its two values, the change and the verdict,
|
|
3
|
-
// numbers with their unit, for each report to format.
|
|
4
|
+
// its two values, the change and the verdict, why an unchanged one did not count and what to do with an
|
|
5
|
+
// inconclusive one. The values stay numbers with their unit, for each report to format.
|
|
4
6
|
|
|
5
7
|
const MINUS = "−";
|
|
6
8
|
const signed = (n, text) => (n > 0 ? `+${text}` : n < 0 ? `${MINUS}${text}` : text);
|
|
@@ -16,14 +18,20 @@ function describeChange(metric, { delta, deltaPercent }) {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
// Three decimals where the verdict is decided, two above it: "0.028", "0.35"
|
|
19
|
-
|
|
21
|
+
// Three decimals up to 0.1, so a p-value next to the 0.05 it is compared with is not rounded away from it
|
|
22
|
+
const pValue = (p) => (p < 0.001 ? "< 0.001" : p < 0.1 ? p.toFixed(3) : p.toFixed(2));
|
|
20
23
|
const amountOf = (metric, n) => (metric === "CLS" ? signed(n, Math.abs(n).toFixed(4)) : signed(n, `${Math.abs(Math.round(n))}ms`));
|
|
21
24
|
|
|
25
|
+
// What a person does with an inconclusive metric: the runs can tell neither a change nor its absence
|
|
26
|
+
const MEASURE_AGAIN = "measure again, or with more runs";
|
|
27
|
+
|
|
22
28
|
// The evidence of a change measured with five runs a side: its p-value, its shift and the 95 % interval
|
|
23
|
-
// of the shift; for a change over the threshold that
|
|
29
|
+
// of the shift; for a change over the threshold that is inconclusive, that the runs gave no evidence and
|
|
30
|
+
// what to do about it
|
|
24
31
|
function evidenceOf(metric, m) {
|
|
25
32
|
const { p, shift, ci } = m.stats;
|
|
26
|
-
if (m.verdict === "
|
|
33
|
+
if (m.verdict === "inconclusive") return `no evidence: p = ${pValue(p)}; ${MEASURE_AGAIN}`;
|
|
34
|
+
if (m.verdict === "unchanged") return null;
|
|
27
35
|
return `p = ${pValue(p)}, ${amountOf(metric, shift)}, 95% CI ${amountOf(metric, ci[0])} to ${amountOf(metric, ci[1])}`;
|
|
28
36
|
}
|
|
29
37
|
|
|
@@ -36,8 +44,15 @@ function noteOf(metric, m) {
|
|
|
36
44
|
return `under ${metric === "CLS" ? m.threshold : `${Math.round(m.threshold)}ms`}`;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
// "LCP, FCP inconclusive for the worse after 5 runs a side: both pages measured 2 more, up to 7"
|
|
48
|
+
const describeExtended = ({ after, metrics }, runs) =>
|
|
49
|
+
`${metrics.join(", ")} inconclusive for the worse after ${after} runs a side: both pages measured ${runs.count - after} more, up to ${runs.count}`;
|
|
50
|
+
|
|
51
|
+
// A baseline from a file is named by its file, with the URL it measured; a live one, measured in this
|
|
52
|
+
// run, by its URL, with the runs it took and, when it measured more, why
|
|
53
|
+
export function baselineView({ file, live, url, runs, metrics, errors, warnings, extended }) {
|
|
54
|
+
const runsText = runs.count === 1 ? "1 run" : `${runs.count} runs, median by ${runs.medianBy}`;
|
|
55
|
+
const [source, summary] = live ? [url, `measured in this run, ${runsText}`] : [file, `${url}, ${runsText}`];
|
|
41
56
|
const rows = Object.entries(metrics).map(([metric, m]) => ({
|
|
42
57
|
metric,
|
|
43
58
|
unit: unitOf(metric),
|
|
@@ -47,7 +62,7 @@ export function baselineView({ file, url, runs, metrics, errors, warnings }) {
|
|
|
47
62
|
verdict: m.verdict,
|
|
48
63
|
note: m.verdict === "not measured" ? null : noteOf(metric, m),
|
|
49
64
|
}));
|
|
50
|
-
return {
|
|
65
|
+
return { source, summary, ...(extended ? { extended: describeExtended(extended, runs) } : {}), rows, newErrors: errors.new, fixedErrors: errors.fixed, warnings };
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
// What got worse, one line each, for --fail-on-regression: the metrics that regressed and the errors
|
|
@@ -61,13 +76,23 @@ export function describeRegressions({ metrics, errors }) {
|
|
|
61
76
|
];
|
|
62
77
|
}
|
|
63
78
|
|
|
79
|
+
// The metrics inconclusive for the worse, one line each, for --fail-on-inconclusive: their change, their
|
|
80
|
+
// p-value and what to do. An inconclusive improvement never fails.
|
|
81
|
+
export function describeInconclusive(comparison) {
|
|
82
|
+
return inconclusiveForTheWorse(comparison).map((metric) => {
|
|
83
|
+
const m = comparison.metrics[metric];
|
|
84
|
+
return `${metric} ${describeChange(metric, m)}, p = ${pValue(m.stats.p)}; ${MEASURE_AGAIN}`;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
64
88
|
// The change of a metric in a cell of the matrix table: the figure only when it counts, so the table
|
|
65
89
|
// reads at a glance. "▲ +380ms", "▼ −400ms", "=" for unchanged, "·" when one side did not measure it.
|
|
66
90
|
export function changeCell(metric, m) {
|
|
67
91
|
if (m.verdict === "not measured") return { text: "·", verdict: m.verdict };
|
|
68
92
|
if (m.verdict === "unchanged") return { text: "=", verdict: m.verdict };
|
|
69
93
|
const amount = metric === "CLS" ? Math.abs(m.delta).toFixed(4) : `${Math.abs(Math.round(m.delta))}ms`;
|
|
70
|
-
|
|
94
|
+
// An inconclusive change counts for nothing yet: a question mark keeps it apart from a regression
|
|
95
|
+
return { text: `${arrow(m.delta)} ${signed(m.delta, amount)}${m.verdict === "inconclusive" ? "?" : ""}`, verdict: m.verdict };
|
|
71
96
|
}
|
|
72
97
|
|
|
73
98
|
// The comparison of a matrix below its table. A warning that every compared combination has, such as
|
|
@@ -94,3 +119,8 @@ export function matrixComparisonView(root, combinations) {
|
|
|
94
119
|
export function describeMatrixRegressions(combinations) {
|
|
95
120
|
return combinations.filter((c) => c.baseline).flatMap((c) => describeRegressions(c.baseline).map((r) => `${c.id}: ${r}`));
|
|
96
121
|
}
|
|
122
|
+
|
|
123
|
+
// The inconclusive metrics for the worse of every combination, each named by its combination
|
|
124
|
+
export function describeMatrixInconclusive(combinations) {
|
|
125
|
+
return combinations.filter((c) => c.baseline).flatMap((c) => describeInconclusive(c.baseline).map((line) => `${c.id}: ${line}`));
|
|
126
|
+
}
|
package/src/reporters/human.js
CHANGED
|
@@ -148,6 +148,11 @@ function renderResult(r, verbose) {
|
|
|
148
148
|
return ` ${styleText("dim", "·")} ${pad(r.id, 16)} ${styleText("dim", `not measured: ${r.message ?? "nothing recorded"}`)}`;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// The browser lacks the API the snippet reads, so there is no value to show
|
|
152
|
+
if (r.status === "unsupported") {
|
|
153
|
+
return ` ${styleText("dim", "·")} ${pad(r.id, 16)} ${styleText("dim", `not supported: ${r.error ?? "the browser lacks the API"}`)}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
151
156
|
if (r.script === "Fonts-Preloaded-Loaded-and-used-above-the-fold") {
|
|
152
157
|
return renderFontsResult(r);
|
|
153
158
|
}
|
|
@@ -257,7 +262,7 @@ function renderRuns(runs) {
|
|
|
257
262
|
return lines;
|
|
258
263
|
}
|
|
259
264
|
|
|
260
|
-
const VERDICT_STYLE = { regressed: "red", improved: "green", unchanged: "dim", "not measured": "dim", new: "cyan" };
|
|
265
|
+
const VERDICT_STYLE = { regressed: "red", improved: "green", unchanged: "dim", inconclusive: "yellow", "not measured": "dim", new: "cyan" };
|
|
261
266
|
|
|
262
267
|
// Every budget the run was given, passed or not, so the report says which one decided the exit code
|
|
263
268
|
function renderBudgets(budgets) {
|
|
@@ -273,7 +278,8 @@ function renderBudgets(budgets) {
|
|
|
273
278
|
// trust the table, then a row per metric and the errors that appeared or went away
|
|
274
279
|
function renderBaseline(comparison) {
|
|
275
280
|
const view = baselineView(comparison);
|
|
276
|
-
const lines = ["", styleText("bold", `Compared with ${view.
|
|
281
|
+
const lines = ["", styleText("bold", `Compared with ${view.source} (${view.summary})`)];
|
|
282
|
+
if (view.extended) lines.push(styleText("dim", ` ${view.extended}`));
|
|
277
283
|
for (const warning of view.warnings) lines.push(` ${styleText("yellow", `Warning: ${warning}`)}`);
|
|
278
284
|
lines.push(styleText("dim", ` ${pad("Metric", 9)}${pad("Baseline", 11)}${pad("Now", 10)}Change`));
|
|
279
285
|
for (const row of view.rows) {
|
|
@@ -48,6 +48,11 @@ function renderNotMeasuredRow(r) {
|
|
|
48
48
|
return `| ${r.id} | · not measured | ${cell(r.message ?? "nothing recorded")} |`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// The browser lacks the API the snippet reads, so there is no value to show
|
|
52
|
+
function renderUnsupportedRow(r) {
|
|
53
|
+
return `| ${r.id} | · not supported | ${cell(r.error ?? "the browser lacks the API")} |`;
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
// A result with a count and no value: its count, and its first numbers as the human report gives them
|
|
52
57
|
function renderSummaryRow(r) {
|
|
53
58
|
return `| ${r.id} | · ${r.count ?? ""} | ${summaryNumbers(r).join(" · ")} |`;
|
|
@@ -57,6 +62,7 @@ function renderRow(r) {
|
|
|
57
62
|
if (r.status === "skipped") return renderSkippedRow(r);
|
|
58
63
|
if (r.status === "error") return renderErrorRow(r);
|
|
59
64
|
if (r.status === "tracking") return renderNotMeasuredRow(r);
|
|
65
|
+
if (r.status === "unsupported") return renderUnsupportedRow(r);
|
|
60
66
|
if (Array.isArray(r.issues)) return renderAuditRow(r);
|
|
61
67
|
if (isSummaryResult(r)) return renderSummaryRow(r);
|
|
62
68
|
return renderMetricRow(r);
|
|
@@ -162,13 +168,14 @@ function renderRuns(runs) {
|
|
|
162
168
|
return lines;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
const VERDICT_ICON = { regressed: "❌", improved: "✅", unchanged: "➖", "not measured": "·" };
|
|
171
|
+
const VERDICT_ICON = { regressed: "❌", improved: "✅", unchanged: "➖", inconclusive: "❔", "not measured": "·" };
|
|
166
172
|
|
|
167
173
|
// The comparison with --baseline. The warnings go in the comment itself: whoever reads the pull
|
|
168
174
|
// request does not see the stderr of the CI.
|
|
169
175
|
function renderBaseline(comparison) {
|
|
170
176
|
const view = baselineView(comparison);
|
|
171
|
-
const lines = ["", `### Compared with \`${view.
|
|
177
|
+
const lines = ["", `### Compared with \`${view.source}\``, `> ${view.summary}`];
|
|
178
|
+
if (view.extended) lines.push(">", `> ${view.extended}`);
|
|
172
179
|
for (const warning of view.warnings) lines.push(">", `> ⚠️ ${warning}`);
|
|
173
180
|
lines.push("", "| Metric | Baseline | Now | Change | Verdict |", "|--------|----------|-----|--------|---------|");
|
|
174
181
|
for (const row of view.rows) {
|
|
@@ -176,8 +183,8 @@ function renderBaseline(comparison) {
|
|
|
176
183
|
const verdict = `${VERDICT_ICON[row.verdict]} ${row.verdict}${row.note ? ` (${row.note})` : ""}`;
|
|
177
184
|
lines.push(`| ${row.metric} | ${value(row.baseline)} | ${value(row.current)} | ${row.change ?? "·"} | ${verdict} |`);
|
|
178
185
|
}
|
|
179
|
-
if (view.newErrors.length) lines.push("", "#### New errors", ...view.newErrors.map((e) => `- \`${e.id}\`: ${e.message}`));
|
|
180
|
-
if (view.fixedErrors.length) lines.push("", "#### Fixed errors", ...view.fixedErrors.map((e) => `- \`${e.id}\`: ${e.message}`));
|
|
186
|
+
if (view.newErrors.length) lines.push("", "#### New errors", ...view.newErrors.map((e) => `- \`${e.id}\`: ${md(oneLine(e.message))}`));
|
|
187
|
+
if (view.fixedErrors.length) lines.push("", "#### Fixed errors", ...view.fixedErrors.map((e) => `- \`${e.id}\`: ${md(oneLine(e.message))}`));
|
|
181
188
|
return lines;
|
|
182
189
|
}
|
|
183
190
|
|