@aethervtt/shard-verify 0.0.0-stage → 0.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/dist/stats.js ADDED
@@ -0,0 +1,56 @@
1
+ class SampleRing {
2
+ times;
3
+ values;
4
+ next = 0;
5
+ count = 0;
6
+ constructor(capacity) {
7
+ this.times = new Float64Array(capacity);
8
+ this.values = new Float64Array(capacity);
9
+ }
10
+ get size() {
11
+ return this.count;
12
+ }
13
+ push(time, value) {
14
+ this.times[this.next] = time;
15
+ this.values[this.next] = value;
16
+ this.next = (this.next + 1) % this.times.length;
17
+ if (this.count < this.times.length) this.count++;
18
+ }
19
+ clear() {
20
+ this.next = 0;
21
+ this.count = 0;
22
+ }
23
+ /** The values of samples at or after `since`, oldest first. Allocates: not for frames. */
24
+ since(since) {
25
+ const capacity = this.times.length;
26
+ const start = (this.next - this.count + capacity) % capacity;
27
+ const out = [];
28
+ for (let i = 0; i < this.count; i++) {
29
+ const at = (start + i) % capacity;
30
+ if (this.times[at] >= since) out.push(this.values[at]);
31
+ }
32
+ return Float64Array.from(out);
33
+ }
34
+ }
35
+ function percentile(values, p) {
36
+ if (values.length === 0) return 0;
37
+ values.sort();
38
+ const rank = Math.max(1, Math.ceil(p * values.length));
39
+ return values[rank - 1];
40
+ }
41
+ function distribution(values) {
42
+ return {
43
+ p50: percentile(values, 0.5),
44
+ p95: percentile(values, 0.95),
45
+ p99: percentile(values, 0.99)
46
+ };
47
+ }
48
+ function round(value) {
49
+ return Math.round(value * 100) / 100;
50
+ }
51
+ export {
52
+ SampleRing,
53
+ distribution,
54
+ percentile,
55
+ round
56
+ };
@@ -0,0 +1,31 @@
1
+ import type { Thresholds } from './plan.js';
2
+ import { type PerfRecord } from './record.js';
3
+ /** One broken budget: the metric, the value, the budget, and the baseline it came from. */
4
+ export interface Breach {
5
+ scenario: string;
6
+ metric: string;
7
+ renderer: string;
8
+ fixture: string;
9
+ /** Undefined when the record has no such metric. */
10
+ value: number | undefined;
11
+ /** The limit that was broken, e.g. `max 16.7` or `max 1.0 × three (12.1) = 12.1`. */
12
+ budget: string;
13
+ baseline?: {
14
+ renderer: string;
15
+ value: number | undefined;
16
+ };
17
+ message: string;
18
+ }
19
+ export interface PerfCheck {
20
+ pass: boolean;
21
+ /** Rules applied, over every record they applied to. */
22
+ checked: number;
23
+ breaches: Breach[];
24
+ }
25
+ /**
26
+ * Applies a plan's thresholds to the records of one run (0062). Absolute rules apply to every
27
+ * record of the scenario whose renderer isn't a ratio baseline there. A ratio rule compares with
28
+ * the baseline renderer's record of the same scenario and fixture, on the same device where the
29
+ * run has one; with no baseline record, the rule fails.
30
+ */
31
+ export declare function checkThresholds(records: readonly PerfRecord[], thresholds: Thresholds): PerfCheck;
@@ -0,0 +1,95 @@
1
+ import { valueAt } from "./expect.js";
2
+ import { rendererName } from "./record.js";
3
+ function checkThresholds(records, thresholds) {
4
+ const breaches = [];
5
+ let checked = 0;
6
+ for (const [scenario, rules] of Object.entries(thresholds)) {
7
+ const baselines = new Set(
8
+ Object.values(rules).map((r) => r.maxRatioTo).filter((r) => r !== void 0)
9
+ );
10
+ const subjects = records.filter(
11
+ (r) => r.scenario === scenario && !baselines.has(rendererName(r))
12
+ );
13
+ for (const record of subjects) {
14
+ for (const [metric, rule] of Object.entries(rules)) {
15
+ checked++;
16
+ const breach = apply(record, metric, rule, records);
17
+ if (breach) breaches.push(breach);
18
+ }
19
+ }
20
+ }
21
+ return { pass: breaches.length === 0, checked, breaches };
22
+ }
23
+ function metricOf(record, metric) {
24
+ const value = valueAt(record, metric);
25
+ return typeof value === "number" ? value : void 0;
26
+ }
27
+ function apply(record, metric, rule, records) {
28
+ const value = metricOf(record, metric);
29
+ const base = {
30
+ scenario: record.scenario,
31
+ metric,
32
+ renderer: record.renderer,
33
+ fixture: record.fixture,
34
+ value
35
+ };
36
+ const where = `${record.scenario} ${metric} (${record.renderer})`;
37
+ if (value === void 0) {
38
+ return { ...base, budget: describe(rule), message: `${where}: the record has no ${metric}` };
39
+ }
40
+ if (rule.max !== void 0 && value > rule.max) {
41
+ return {
42
+ ...base,
43
+ budget: `max ${rule.max}`,
44
+ message: `${where}: ${value} is over the max of ${rule.max}`
45
+ };
46
+ }
47
+ if (rule.min !== void 0 && value < rule.min) {
48
+ return {
49
+ ...base,
50
+ budget: `min ${rule.min}`,
51
+ message: `${where}: ${value} is under the min of ${rule.min}`
52
+ };
53
+ }
54
+ if (rule.maxRatioTo === void 0) return void 0;
55
+ const ratio = rule.ratio ?? 1;
56
+ const baseline = findBaseline(record, rule.maxRatioTo, records);
57
+ if (!baseline) {
58
+ return {
59
+ ...base,
60
+ budget: `max ${ratio} \xD7 ${rule.maxRatioTo}`,
61
+ message: `${where}: no ${rule.maxRatioTo} record of ${record.scenario} on ${record.fixture} to compare with`
62
+ };
63
+ }
64
+ const baseValue = metricOf(baseline, metric);
65
+ const limit = baseValue === void 0 ? void 0 : baseValue * ratio;
66
+ if (limit !== void 0 && value <= limit) return void 0;
67
+ return {
68
+ ...base,
69
+ budget: `max ${ratio} \xD7 ${baseline.renderer} (${baseValue ?? "none"})${limit === void 0 ? "" : ` = ${round(limit)}`}`,
70
+ 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)})`
72
+ };
73
+ }
74
+ function findBaseline(record, renderer, records) {
75
+ const candidates = records.filter(
76
+ (r) => rendererName(r) === renderer && r.scenario === record.scenario && r.fixture === record.fixture
77
+ );
78
+ const sameDevice = candidates.find(
79
+ (r) => r.device.ua === record.device.ua && r.device.dpr === record.device.dpr && r.device.viewport[0] === record.device.viewport[0] && r.device.viewport[1] === record.device.viewport[1]
80
+ );
81
+ return sameDevice ?? candidates[0];
82
+ }
83
+ function describe(rule) {
84
+ const parts = [];
85
+ if (rule.max !== void 0) parts.push(`max ${rule.max}`);
86
+ if (rule.min !== void 0) parts.push(`min ${rule.min}`);
87
+ if (rule.maxRatioTo !== void 0) parts.push(`max ${rule.ratio ?? 1} \xD7 ${rule.maxRatioTo}`);
88
+ return parts.join(", ");
89
+ }
90
+ function round(value) {
91
+ return Math.round(value * 100) / 100;
92
+ }
93
+ export {
94
+ checkThresholds
95
+ };
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-verify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./metrics": {
13
+ "types": "./dist/metrics/index.d.ts",
14
+ "default": "./dist/metrics/index.js"
15
+ },
16
+ "./page": {
17
+ "types": "./dist/page.d.ts",
18
+ "default": "./dist/page.js"
19
+ },
20
+ "./node": {
21
+ "types": "./dist/node/index.d.ts",
22
+ "default": "./dist/node/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "LICENSE",
28
+ "README.md"
29
+ ],
30
+ "dependencies": {
31
+ "@aethervtt/shard-assets": "0.1.0",
32
+ "@aethervtt/shard-core": "0.1.0",
33
+ "@aethervtt/shard-gpu": "0.1.0",
34
+ "@aethervtt/shard-platform": "0.1.0",
35
+ "@aethervtt/shard-render": "0.1.0",
36
+ "@aethervtt/shard-runtime": "0.1.0",
37
+ "@aethervtt/shard-texture": "0.1.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/aethervtt/shard.git",
42
+ "directory": "packages/verify"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "provenance": true
47
+ },
48
+ "shard": {
49
+ "buildSha": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
50
+ }
51
+ }