@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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/compare.d.ts +45 -0
- package/dist/compare.js +213 -0
- package/dist/expect.d.ts +14 -0
- package/dist/expect.js +70 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +42 -0
- package/dist/metrics/index.d.ts +1 -0
- package/dist/metrics/index.js +12 -0
- package/dist/metrics/metrics.d.ts +83 -0
- package/dist/metrics/metrics.js +244 -0
- package/dist/node/capture.d.ts +36 -0
- package/dist/node/capture.js +253 -0
- package/dist/node/files.d.ts +40 -0
- package/dist/node/files.js +159 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.js +28 -0
- package/dist/node/png.d.ts +7 -0
- package/dist/node/png.js +50 -0
- package/dist/page-api.d.ts +32 -0
- package/dist/page-api.js +0 -0
- package/dist/page.d.ts +17 -0
- package/dist/page.js +104 -0
- package/dist/plan.d.ts +117 -0
- package/dist/plan.js +233 -0
- package/dist/record.d.ts +84 -0
- package/dist/record.js +202 -0
- package/dist/report.d.ts +20 -0
- package/dist/report.js +43 -0
- package/dist/run.d.ts +56 -0
- package/dist/run.js +19 -0
- package/dist/stats.d.ts +26 -0
- package/dist/stats.js +56 -0
- package/dist/thresholds.d.ts +31 -0
- package/dist/thresholds.js +95 -0
- package/package.json +49 -4
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|