@aethervtt/shard-verify 0.1.0 → 0.2.1
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/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/metrics/metrics.js +6 -4
- package/dist/node/capture.d.ts +13 -1
- package/dist/node/capture.js +18 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +2 -0
- package/dist/plan.d.ts +17 -2
- package/dist/plan.js +17 -0
- package/dist/record.d.ts +11 -2
- package/dist/record.js +25 -1
- package/dist/thresholds.d.ts +20 -0
- package/dist/thresholds.js +66 -5
- package/package.json +9 -9
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { compareImages, DEFAULT_TOLERANCE, deltaE2000, diffHeatmap, type ImageDiff, type RgbaImage, type Tolerance, } from './compare.js';
|
|
2
2
|
export { type CheckFailure, checkExpectations, deepEqual, valueAt } from './expect.js';
|
|
3
|
-
export { type BrowserName, type CapturePlan, type CaptureScope, capturePlanJsonSchema, type Expectations, type Matcher, type PlanClient, type PlanConditions, type PlanScenario, type PlanShot, type PlanStep, parsePlan, shotId, type ThresholdRule, type Thresholds, } from './plan.js';
|
|
3
|
+
export { type BrowserName, type CapturePlan, type CaptureScope, capturePlanJsonSchema, type Expectations, type Matcher, type PlanClient, type PlanConditions, type PlanScenario, type PlanShot, type PlanStep, parsePlan, type ScenarioThresholds, type SpanThreshold, shotId, type ThresholdRule, type Thresholds, } from './plan.js';
|
|
4
4
|
export { type PerfDevice, type PerfRecord, parsePerfRecord, perfRecordJsonSchema, rendererName, validateJson, } from './record.js';
|
|
5
5
|
export { type ReportShot, renderReport } from './report.js';
|
|
6
6
|
export { type Approval, type ApprovalsFile, addApproval, type CapturedShot, type CaptureManifest, } from './run.js';
|
|
7
|
-
export { type Breach, checkThresholds, type PerfCheck } from './thresholds.js';
|
|
7
|
+
export { type Breach, checkThresholds, type PerfCheck, spansThatGrew } from './thresholds.js';
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { renderReport } from "./report.js";
|
|
|
20
20
|
import {
|
|
21
21
|
addApproval
|
|
22
22
|
} from "./run.js";
|
|
23
|
-
import { checkThresholds } from "./thresholds.js";
|
|
23
|
+
import { checkThresholds, spansThatGrew } from "./thresholds.js";
|
|
24
24
|
export {
|
|
25
25
|
DEFAULT_TOLERANCE,
|
|
26
26
|
addApproval,
|
|
@@ -37,6 +37,7 @@ export {
|
|
|
37
37
|
renderReport,
|
|
38
38
|
rendererName,
|
|
39
39
|
shotId,
|
|
40
|
+
spansThatGrew,
|
|
40
41
|
validateJson,
|
|
41
42
|
valueAt
|
|
42
43
|
};
|
package/dist/metrics/metrics.js
CHANGED
|
@@ -11,7 +11,8 @@ import { Gpu, Graph, RenderScale, Window } from "@aethervtt/shard-render";
|
|
|
11
11
|
import {
|
|
12
12
|
AppControlResource,
|
|
13
13
|
definePlugin,
|
|
14
|
-
FrameDemand
|
|
14
|
+
FrameDemand,
|
|
15
|
+
perfBreakdown
|
|
15
16
|
} from "@aethervtt/shard-runtime";
|
|
16
17
|
import { distribution, percentile, round, SampleRing } from "../stats.js";
|
|
17
18
|
const FRAME_SAMPLES = 1 << 15;
|
|
@@ -130,7 +131,7 @@ class Metrics {
|
|
|
130
131
|
const device = this.app.world.tryResource(Gpu);
|
|
131
132
|
const memory = device?.memory() ?? { bytes: 0, byCategory: {} };
|
|
132
133
|
return {
|
|
133
|
-
version:
|
|
134
|
+
version: 2,
|
|
134
135
|
renderer: meta.renderer ?? this.renderer,
|
|
135
136
|
fixture: meta.fixture,
|
|
136
137
|
scenario: meta.scenario,
|
|
@@ -152,7 +153,8 @@ class Metrics {
|
|
|
152
153
|
frameTime,
|
|
153
154
|
longTasks: { count: tasks.length, totalMs: round(totalMs), maxMs: round(maxMs) },
|
|
154
155
|
gpuMemory: { bytes: memory.bytes, byCategory: memory.byCategory },
|
|
155
|
-
download: this.performance?.downloads?.() ?? { transferred: 0, decoded: 0 }
|
|
156
|
+
download: this.performance?.downloads?.() ?? { transferred: 0, decoded: 0 },
|
|
157
|
+
breakdown: perfBreakdown(this.app.world)
|
|
156
158
|
};
|
|
157
159
|
}
|
|
158
160
|
device() {
|
|
@@ -181,7 +183,7 @@ const frameStart = defineSystem({
|
|
|
181
183
|
const metricsMethods = [
|
|
182
184
|
{
|
|
183
185
|
name: "metrics.record",
|
|
184
|
-
description: "A performance record (spec 0062) of the running app: cold start, first usable frame, patch-to-frame latency, frame times, long tasks, GPU memory, and
|
|
186
|
+
description: "A performance record (spec 0062) of the running app: cold start, first usable frame, patch-to-frame latency, frame times, long tasks, GPU memory, downloads, and the breakdown (0074: the 10 CPU and GPU spans with the highest p95), over the window since metrics.reset (at most 30 s).",
|
|
185
187
|
params: defineSchema("verify/MetricsRecordParams", {
|
|
186
188
|
fixture: t.string({ required: true, description: 'What was loaded, e.g. "tabletop-small".' }),
|
|
187
189
|
scenario: t.string({ required: true, description: 'What ran, e.g. "tabletop-pan".' }),
|
package/dist/node/capture.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export interface CaptureOptions {
|
|
|
10
10
|
headed?: boolean;
|
|
11
11
|
/** Extra browser flags (also `SHARD_BROWSER_ARGS`, space-separated). */
|
|
12
12
|
browserArgs?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Chromium's release channel (also `SHARD_BROWSER_CHANNEL`): `chrome` runs the installed Chrome,
|
|
15
|
+
* headed at the display's refresh rate for frame-interval records. Default Playwright's Chromium.
|
|
16
|
+
*/
|
|
17
|
+
channel?: string;
|
|
13
18
|
/** Progress lines. */
|
|
14
19
|
log?: (message: string) => void;
|
|
15
20
|
}
|
|
@@ -29,8 +34,15 @@ export interface CaptureRun {
|
|
|
29
34
|
* three.js runs its tests), drawing without a Vulkan surface. `SHARD_BROWSER_HEADED=1` shows the
|
|
30
35
|
* window (CI runs it under `xvfb-run`), and `SHARD_BROWSER_ARGS` adds flags.
|
|
31
36
|
*/
|
|
32
|
-
export declare function browserLaunch(browser: BrowserName, dpr: number, options?: Pick<CaptureOptions, 'headed' | 'browserArgs'>): LaunchOptions;
|
|
37
|
+
export declare function browserLaunch(browser: BrowserName, dpr: number, options?: Pick<CaptureOptions, 'headed' | 'browserArgs' | 'channel'>): LaunchOptions;
|
|
33
38
|
/** Runs a capture plan in real browsers (0062). Checks that fail don't stop the run. */
|
|
34
39
|
export declare function runCapture(plan: CapturePlan, options: CaptureOptions): Promise<CaptureRun>;
|
|
35
40
|
/** The plan's URL with the backend it asks for, and the client's role and query parameters. */
|
|
36
41
|
export declare function clientUrl(url: string, client: PlanClient, backend?: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Headers for pages Shard serves to measure (0074): cross-origin isolation, so `performance.now()`
|
|
44
|
+
* steps 5 µs instead of 100 µs (`credentialless` COEP keeps cross-origin loads working without
|
|
45
|
+
* CORP headers), and `Document-Policy: js-profiling` for the JS Self-Profiling API. `shard dev`,
|
|
46
|
+
* the playground and the capture server send them.
|
|
47
|
+
*/
|
|
48
|
+
export declare const ISOLATION_HEADERS: Readonly<Record<string, string>>;
|
package/dist/node/capture.js
CHANGED
|
@@ -21,7 +21,8 @@ function browserLaunch(browser, dpr, options = {}) {
|
|
|
21
21
|
];
|
|
22
22
|
if (process.platform === "linux")
|
|
23
23
|
args.push("--enable-features=Vulkan", "--disable-vulkan-surface");
|
|
24
|
-
|
|
24
|
+
const channel = options.channel ?? process.env.SHARD_BROWSER_CHANNEL ?? "chromium";
|
|
25
|
+
return { headless, channel, args: [...args, ...extra] };
|
|
25
26
|
}
|
|
26
27
|
async function runCapture(plan, options) {
|
|
27
28
|
const playwright = await import("playwright");
|
|
@@ -170,6 +171,16 @@ async function runSteps(plan, steps, browser, dpr, clients, out, result, prefix)
|
|
|
170
171
|
{ run: step.run ?? step.name, args: step.args ?? null, trace: step.trace ?? false }
|
|
171
172
|
);
|
|
172
173
|
latency.set(client.plan.name, ran.latencyMs);
|
|
174
|
+
if (ran.result !== null && ran.result !== void 0) {
|
|
175
|
+
const file = join(
|
|
176
|
+
out,
|
|
177
|
+
"results",
|
|
178
|
+
`${browser}@${dpr}x-${client.plan.name}`,
|
|
179
|
+
`${name}.json`
|
|
180
|
+
);
|
|
181
|
+
await writeOut(file, `${JSON.stringify(ran.result, null, 2)}
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
173
184
|
} catch (err) {
|
|
174
185
|
failures.push({ step: name, client: client.plan.name, path: "", message: pageMessage(err) });
|
|
175
186
|
}
|
|
@@ -246,7 +257,13 @@ function pageMessage(err) {
|
|
|
246
257
|
const message = err.message ?? String(err);
|
|
247
258
|
return message.replace(/^page\.evaluate: /, "").split("\n")[0];
|
|
248
259
|
}
|
|
260
|
+
const ISOLATION_HEADERS = {
|
|
261
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
262
|
+
"Cross-Origin-Embedder-Policy": "credentialless",
|
|
263
|
+
"Document-Policy": "js-profiling"
|
|
264
|
+
};
|
|
249
265
|
export {
|
|
266
|
+
ISOLATION_HEADERS,
|
|
250
267
|
browserLaunch,
|
|
251
268
|
clientUrl,
|
|
252
269
|
runCapture
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { browserLaunch, type CaptureOptions, type CaptureRun, clientUrl, runCapture, } from './capture.js';
|
|
1
|
+
export { browserLaunch, type CaptureOptions, type CaptureRun, clientUrl, ISOLATION_HEADERS, runCapture, } from './capture.js';
|
|
2
2
|
export { approveShot, type CompareResult, type CompareRun, compareRun, perfCheck, readManifest, readPlan, readRecords, } from './files.js';
|
|
3
3
|
export { decodePng, encodePng, sha256 } from './png.js';
|
package/dist/node/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
browserLaunch,
|
|
3
3
|
clientUrl,
|
|
4
|
+
ISOLATION_HEADERS,
|
|
4
5
|
runCapture
|
|
5
6
|
} from "./capture.js";
|
|
6
7
|
import {
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
} from "./files.js";
|
|
14
15
|
import { decodePng, encodePng, sha256 } from "./png.js";
|
|
15
16
|
export {
|
|
17
|
+
ISOLATION_HEADERS,
|
|
16
18
|
approveShot,
|
|
17
19
|
browserLaunch,
|
|
18
20
|
clientUrl,
|
package/dist/plan.d.ts
CHANGED
|
@@ -104,8 +104,23 @@ export interface ThresholdRule {
|
|
|
104
104
|
maxRatioTo?: string;
|
|
105
105
|
ratio?: number;
|
|
106
106
|
}
|
|
107
|
-
/**
|
|
108
|
-
|
|
107
|
+
/**
|
|
108
|
+
* A budget on one span of a record's `breakdown` (0074): `{ "span": "partition/spawn", "p95": 2 }`.
|
|
109
|
+
* Its p50, p95 or max (ms) must stay at or under the number given.
|
|
110
|
+
*/
|
|
111
|
+
export interface SpanThreshold {
|
|
112
|
+
span: string;
|
|
113
|
+
p50?: number;
|
|
114
|
+
p95?: number;
|
|
115
|
+
max?: number;
|
|
116
|
+
}
|
|
117
|
+
/** A scenario's rules: metric path (`frameTime.p95`) → rule, and span budgets under `spans`. */
|
|
118
|
+
export interface ScenarioThresholds {
|
|
119
|
+
[metric: string]: ThresholdRule | readonly SpanThreshold[] | undefined;
|
|
120
|
+
spans?: readonly SpanThreshold[];
|
|
121
|
+
}
|
|
122
|
+
/** Scenario → its rules. */
|
|
123
|
+
export type Thresholds = Record<string, ScenarioThresholds>;
|
|
109
124
|
/** JSON Schema for capture plans (`.shard/schemas/capture-plan.schema.json`). */
|
|
110
125
|
export declare function capturePlanJsonSchema(): JsonSchema;
|
|
111
126
|
/**
|
package/dist/plan.js
CHANGED
|
@@ -122,6 +122,23 @@ function capturePlanJsonSchema() {
|
|
|
122
122
|
type: "object",
|
|
123
123
|
additionalProperties: {
|
|
124
124
|
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
spans: {
|
|
127
|
+
type: "array",
|
|
128
|
+
description: `Span budgets on records' breakdown (spec 0074): { "span": "partition/spawn", "p95": 2 }.`,
|
|
129
|
+
items: {
|
|
130
|
+
type: "object",
|
|
131
|
+
additionalProperties: false,
|
|
132
|
+
required: ["span"],
|
|
133
|
+
properties: {
|
|
134
|
+
span: { type: "string", minLength: 1 },
|
|
135
|
+
p50: { type: "number", minimum: 0 },
|
|
136
|
+
p95: { type: "number", minimum: 0 },
|
|
137
|
+
max: { type: "number", minimum: 0 }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
125
142
|
additionalProperties: {
|
|
126
143
|
type: "object",
|
|
127
144
|
additionalProperties: false,
|
package/dist/record.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JsonSchema } from '@aethervtt/shard-core';
|
|
1
|
+
import type { JsonSchema, SpanStats } from '@aethervtt/shard-core';
|
|
2
2
|
import { ShardError } from '@aethervtt/shard-core';
|
|
3
3
|
/**
|
|
4
4
|
* One measured run of one scenario on one renderer (0062). Shard's `metrics.record` writes it, and
|
|
@@ -6,7 +6,8 @@ import { ShardError } from '@aethervtt/shard-core';
|
|
|
6
6
|
* sizes bytes.
|
|
7
7
|
*/
|
|
8
8
|
export interface PerfRecord {
|
|
9
|
-
version
|
|
9
|
+
/** 2 adds `breakdown` (0074); version 1 records still read. */
|
|
10
|
+
version: 1 | 2;
|
|
10
11
|
/** `shard@<sha>`, `three@0.160.1`: the part before `@` names the renderer in thresholds. */
|
|
11
12
|
renderer: string;
|
|
12
13
|
fixture: string;
|
|
@@ -61,6 +62,14 @@ export interface PerfRecord {
|
|
|
61
62
|
transferred: number;
|
|
62
63
|
decoded: number;
|
|
63
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Where the time went (0074, version 2): the 10 spans with the highest p95, CPU (systems,
|
|
67
|
+
* schedules, render encodes, workers) and GPU passes, over the profiler's window.
|
|
68
|
+
*/
|
|
69
|
+
breakdown?: {
|
|
70
|
+
cpu: SpanStats[];
|
|
71
|
+
gpu: SpanStats[];
|
|
72
|
+
};
|
|
64
73
|
}
|
|
65
74
|
export interface PerfDevice {
|
|
66
75
|
ua: string;
|
package/dist/record.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { ShardError } from "@aethervtt/shard-core";
|
|
2
2
|
const ms = { type: "number", minimum: 0 };
|
|
3
3
|
const count = { type: "integer", minimum: 0 };
|
|
4
|
+
const spanStats = {
|
|
5
|
+
type: "array",
|
|
6
|
+
items: {
|
|
7
|
+
type: "object",
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
required: ["span", "track", "calls", "total", "p50", "p95", "max"],
|
|
10
|
+
properties: {
|
|
11
|
+
span: { type: "string", minLength: 1 },
|
|
12
|
+
track: { enum: ["main", "gpu", "async", "gc", "worker"] },
|
|
13
|
+
calls: count,
|
|
14
|
+
total: ms,
|
|
15
|
+
p50: ms,
|
|
16
|
+
p95: ms,
|
|
17
|
+
max: ms
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
4
21
|
function perfRecordJsonSchema() {
|
|
5
22
|
return {
|
|
6
23
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -26,7 +43,7 @@ function perfRecordJsonSchema() {
|
|
|
26
43
|
],
|
|
27
44
|
properties: {
|
|
28
45
|
$schema: { type: "string" },
|
|
29
|
-
version: {
|
|
46
|
+
version: { enum: [1, 2] },
|
|
30
47
|
renderer: {
|
|
31
48
|
type: "string",
|
|
32
49
|
minLength: 1,
|
|
@@ -98,6 +115,13 @@ function perfRecordJsonSchema() {
|
|
|
98
115
|
additionalProperties: false,
|
|
99
116
|
required: ["transferred", "decoded"],
|
|
100
117
|
properties: { transferred: count, decoded: count }
|
|
118
|
+
},
|
|
119
|
+
breakdown: {
|
|
120
|
+
type: "object",
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
required: ["cpu", "gpu"],
|
|
123
|
+
description: "Version 2 (spec 0074): the 10 CPU and 10 GPU spans with the highest p95 over the window.",
|
|
124
|
+
properties: { cpu: spanStats, gpu: spanStats }
|
|
101
125
|
}
|
|
102
126
|
}
|
|
103
127
|
};
|
package/dist/thresholds.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export interface Breach {
|
|
|
15
15
|
value: number | undefined;
|
|
16
16
|
};
|
|
17
17
|
message: string;
|
|
18
|
+
/** A span budget's span (0074). */
|
|
19
|
+
span?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Against a baseline: the spans whose p95 grew the most from its record to this one (0074), when
|
|
22
|
+
* both records have a breakdown.
|
|
23
|
+
*/
|
|
24
|
+
grew?: {
|
|
25
|
+
span: string;
|
|
26
|
+
p95: number;
|
|
27
|
+
baselineP95: number;
|
|
28
|
+
}[];
|
|
18
29
|
}
|
|
19
30
|
export interface PerfCheck {
|
|
20
31
|
pass: boolean;
|
|
@@ -29,3 +40,12 @@ export interface PerfCheck {
|
|
|
29
40
|
* run has one; with no baseline record, the rule fails.
|
|
30
41
|
*/
|
|
31
42
|
export declare function checkThresholds(records: readonly PerfRecord[], thresholds: Thresholds): PerfCheck;
|
|
43
|
+
/**
|
|
44
|
+
* The spans whose p95 grew the most from `baseline` to `record` (0074), largest growth first: a
|
|
45
|
+
* span missing from the baseline's breakdown counts from 0. Empty unless both have a breakdown.
|
|
46
|
+
*/
|
|
47
|
+
export declare function spansThatGrew(record: PerfRecord, baseline: PerfRecord, limit?: number): {
|
|
48
|
+
span: string;
|
|
49
|
+
p95: number;
|
|
50
|
+
baselineP95: number;
|
|
51
|
+
}[];
|
package/dist/thresholds.js
CHANGED
|
@@ -4,18 +4,28 @@ function checkThresholds(records, thresholds) {
|
|
|
4
4
|
const breaches = [];
|
|
5
5
|
let checked = 0;
|
|
6
6
|
for (const [scenario, rules] of Object.entries(thresholds)) {
|
|
7
|
+
const metrics = [];
|
|
8
|
+
for (const [metric, rule] of Object.entries(rules)) {
|
|
9
|
+
if (metric !== "spans" && rule && !Array.isArray(rule))
|
|
10
|
+
metrics.push([metric, rule]);
|
|
11
|
+
}
|
|
7
12
|
const baselines = new Set(
|
|
8
|
-
|
|
13
|
+
metrics.map(([, r]) => r.maxRatioTo).filter((r) => r !== void 0)
|
|
9
14
|
);
|
|
10
15
|
const subjects = records.filter(
|
|
11
16
|
(r) => r.scenario === scenario && !baselines.has(rendererName(r))
|
|
12
17
|
);
|
|
13
18
|
for (const record of subjects) {
|
|
14
|
-
for (const [metric, rule] of
|
|
19
|
+
for (const [metric, rule] of metrics) {
|
|
15
20
|
checked++;
|
|
16
21
|
const breach = apply(record, metric, rule, records);
|
|
17
22
|
if (breach) breaches.push(breach);
|
|
18
23
|
}
|
|
24
|
+
for (const rule of rules.spans ?? []) {
|
|
25
|
+
checked++;
|
|
26
|
+
const breach = applySpan(record, rule);
|
|
27
|
+
if (breach) breaches.push(breach);
|
|
28
|
+
}
|
|
19
29
|
}
|
|
20
30
|
}
|
|
21
31
|
return { pass: breaches.length === 0, checked, breaches };
|
|
@@ -64,12 +74,62 @@ function apply(record, metric, rule, records) {
|
|
|
64
74
|
const baseValue = metricOf(baseline, metric);
|
|
65
75
|
const limit = baseValue === void 0 ? void 0 : baseValue * ratio;
|
|
66
76
|
if (limit !== void 0 && value <= limit) return void 0;
|
|
67
|
-
|
|
77
|
+
const grew = spansThatGrew(record, baseline);
|
|
78
|
+
const breach = {
|
|
68
79
|
...base,
|
|
69
80
|
budget: `max ${ratio} \xD7 ${baseline.renderer} (${baseValue ?? "none"})${limit === void 0 ? "" : ` = ${round(limit)}`}`,
|
|
70
81
|
baseline: { renderer: baseline.renderer, value: baseValue },
|
|
71
|
-
message: limit === void 0 ? `${where}: ${baseline.renderer}'s record has no ${metric}` : `${where}: ${value} is over ${ratio} \xD7 ${baseline.renderer}'s ${baseValue} (${round(limit)})`
|
|
82
|
+
message: limit === void 0 ? `${where}: ${baseline.renderer}'s record has no ${metric}` : `${where}: ${value} is over ${ratio} \xD7 ${baseline.renderer}'s ${baseValue} (${round(limit)})${grew.length > 0 ? `; grew most: ${grew.map((g) => `${g.span} ${g.baselineP95} \u2192 ${g.p95} ms`).join(", ")}` : ""}`
|
|
83
|
+
};
|
|
84
|
+
if (grew.length > 0) breach.grew = grew;
|
|
85
|
+
return breach;
|
|
86
|
+
}
|
|
87
|
+
function spansOf(record) {
|
|
88
|
+
return record.breakdown ? [...record.breakdown.cpu, ...record.breakdown.gpu] : [];
|
|
89
|
+
}
|
|
90
|
+
function spansThatGrew(record, baseline, limit = 3) {
|
|
91
|
+
if (!record.breakdown || !baseline.breakdown) return [];
|
|
92
|
+
const before = new Map(spansOf(baseline).map((s) => [s.span, s.p95]));
|
|
93
|
+
return spansOf(record).map((s) => ({ span: s.span, p95: s.p95, baselineP95: before.get(s.span) ?? 0 })).filter((g) => g.p95 > g.baselineP95).sort((a, b) => b.p95 - b.baselineP95 - (a.p95 - a.baselineP95) || (a.span < b.span ? -1 : 1)).slice(0, limit);
|
|
94
|
+
}
|
|
95
|
+
function applySpan(record, rule) {
|
|
96
|
+
const stats = spansOf(record).find((s) => s.span === rule.span);
|
|
97
|
+
const where = `${record.scenario} span ${rule.span} (${record.renderer})`;
|
|
98
|
+
const base = {
|
|
99
|
+
scenario: record.scenario,
|
|
100
|
+
renderer: record.renderer,
|
|
101
|
+
fixture: record.fixture,
|
|
102
|
+
span: rule.span
|
|
72
103
|
};
|
|
104
|
+
if (!stats) {
|
|
105
|
+
const metric = `span:${rule.span}`;
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
metric,
|
|
109
|
+
value: void 0,
|
|
110
|
+
budget: describeSpan(rule),
|
|
111
|
+
message: record.breakdown ? `${where}: the record's breakdown has no ${rule.span}` : `${where}: the record has no breakdown (version 1)`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
for (const key of ["p50", "p95", "max"]) {
|
|
115
|
+
const limit = rule[key];
|
|
116
|
+
if (limit === void 0 || stats[key] <= limit) continue;
|
|
117
|
+
return {
|
|
118
|
+
...base,
|
|
119
|
+
metric: `span:${rule.span}.${key}`,
|
|
120
|
+
value: stats[key],
|
|
121
|
+
budget: `${key} ${limit}`,
|
|
122
|
+
message: `${where}: ${key} ${stats[key]} ms is over ${limit} ms`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
function describeSpan(rule) {
|
|
128
|
+
const parts = [];
|
|
129
|
+
for (const key of ["p50", "p95", "max"]) {
|
|
130
|
+
if (rule[key] !== void 0) parts.push(`${key} ${rule[key]}`);
|
|
131
|
+
}
|
|
132
|
+
return parts.join(", ");
|
|
73
133
|
}
|
|
74
134
|
function findBaseline(record, renderer, records) {
|
|
75
135
|
const candidates = records.filter(
|
|
@@ -91,5 +151,6 @@ function round(value) {
|
|
|
91
151
|
return Math.round(value * 100) / 100;
|
|
92
152
|
}
|
|
93
153
|
export {
|
|
94
|
-
checkThresholds
|
|
154
|
+
checkThresholds,
|
|
155
|
+
spansThatGrew
|
|
95
156
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aethervtt/shard-verify",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@aethervtt/shard-assets": "0.1
|
|
32
|
-
"@aethervtt/shard-core": "0.1
|
|
33
|
-
"@aethervtt/shard-gpu": "0.1
|
|
34
|
-
"@aethervtt/shard-platform": "0.1
|
|
35
|
-
"@aethervtt/shard-render": "0.1
|
|
36
|
-
"@aethervtt/shard-runtime": "0.1
|
|
37
|
-
"@aethervtt/shard-texture": "0.1
|
|
31
|
+
"@aethervtt/shard-assets": "0.2.1",
|
|
32
|
+
"@aethervtt/shard-core": "0.2.1",
|
|
33
|
+
"@aethervtt/shard-gpu": "0.2.1",
|
|
34
|
+
"@aethervtt/shard-platform": "0.2.1",
|
|
35
|
+
"@aethervtt/shard-render": "0.2.1",
|
|
36
|
+
"@aethervtt/shard-runtime": "0.2.1",
|
|
37
|
+
"@aethervtt/shard-texture": "0.2.1"
|
|
38
38
|
},
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"provenance": true
|
|
47
47
|
},
|
|
48
48
|
"shard": {
|
|
49
|
-
"buildSha": "
|
|
49
|
+
"buildSha": "772eb0d2b8a5fc499105708f4ae92c58788cc9ea"
|
|
50
50
|
}
|
|
51
51
|
}
|