@a11ign/screenreader-fleet 0.0.0-reserved.0 → 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 +661 -0
- package/README.md +94 -2
- package/dist/capture-client.d.mts +49 -0
- package/dist/capture-client.d.mts.map +1 -0
- package/dist/capture-client.mjs +352 -0
- package/dist/capture-client.mjs.map +1 -0
- package/dist/check-worker-code.d.mts +34 -0
- package/dist/check-worker-code.d.mts.map +1 -0
- package/dist/check-worker-code.mjs +173 -0
- package/dist/check-worker-code.mjs.map +1 -0
- package/dist/cli-flags.d.mts +71 -0
- package/dist/cli-flags.d.mts.map +1 -0
- package/dist/cli-flags.mjs +207 -0
- package/dist/cli-flags.mjs.map +1 -0
- package/dist/code-drift.d.mts +140 -0
- package/dist/code-drift.d.mts.map +1 -0
- package/dist/code-drift.mjs +284 -0
- package/dist/code-drift.mjs.map +1 -0
- package/dist/command-line-census.d.mts +33 -0
- package/dist/command-line-census.d.mts.map +1 -0
- package/dist/command-line-census.mjs +96 -0
- package/dist/command-line-census.mjs.map +1 -0
- package/dist/compare-workers.d.mts +3 -0
- package/dist/compare-workers.d.mts.map +1 -0
- package/dist/compare-workers.mjs +332 -0
- package/dist/compare-workers.mjs.map +1 -0
- package/dist/control-plane-isolation.d.mts +45 -0
- package/dist/control-plane-isolation.d.mts.map +1 -0
- package/dist/control-plane-isolation.mjs +67 -0
- package/dist/control-plane-isolation.mjs.map +1 -0
- package/dist/deploy-worker.d.mts +3 -0
- package/dist/deploy-worker.d.mts.map +1 -0
- package/dist/deploy-worker.mjs +333 -0
- package/dist/deploy-worker.mjs.map +1 -0
- package/dist/doctor.d.mts +216 -0
- package/dist/doctor.d.mts.map +1 -0
- package/dist/doctor.mjs +962 -0
- package/dist/doctor.mjs.map +1 -0
- package/dist/fleet-consistency.d.mts +235 -0
- package/dist/fleet-consistency.d.mts.map +1 -0
- package/dist/fleet-consistency.mjs +436 -0
- package/dist/fleet-consistency.mjs.map +1 -0
- package/dist/fleet-env.d.mts +228 -0
- package/dist/fleet-env.d.mts.map +1 -0
- package/dist/fleet-env.mjs +509 -0
- package/dist/fleet-env.mjs.map +1 -0
- package/dist/fleet-scripts.d.mts +11 -0
- package/dist/fleet-scripts.d.mts.map +1 -0
- package/dist/fleet-scripts.mjs +41 -0
- package/dist/fleet-scripts.mjs.map +1 -0
- package/dist/git-safe-env.d.mts +10 -0
- package/dist/git-safe-env.d.mts.map +1 -0
- package/dist/git-safe-env.mjs +44 -0
- package/dist/git-safe-env.mjs.map +1 -0
- package/dist/guest-run.d.mts +26 -0
- package/dist/guest-run.d.mts.map +1 -0
- package/dist/guest-run.mjs +164 -0
- package/dist/guest-run.mjs.map +1 -0
- package/dist/host-address.d.mts +33 -0
- package/dist/host-address.d.mts.map +1 -0
- package/dist/host-address.mjs +105 -0
- package/dist/host-address.mjs.map +1 -0
- package/dist/host-capacity.d.mts +64 -0
- package/dist/host-capacity.d.mts.map +1 -0
- package/dist/host-capacity.mjs +152 -0
- package/dist/host-capacity.mjs.map +1 -0
- package/dist/host-metrics.d.mts +116 -0
- package/dist/host-metrics.d.mts.map +1 -0
- package/dist/host-metrics.mjs +201 -0
- package/dist/host-metrics.mjs.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/local-vm.d.ts +125 -0
- package/dist/local-vm.d.ts.map +1 -0
- package/dist/local-vm.js +360 -0
- package/dist/local-vm.js.map +1 -0
- package/dist/measure-guard.d.mts +34 -0
- package/dist/measure-guard.d.mts.map +1 -0
- package/dist/measure-guard.mjs +73 -0
- package/dist/measure-guard.mjs.map +1 -0
- package/dist/normalise-fleet.d.mts +2 -0
- package/dist/normalise-fleet.d.mts.map +1 -0
- package/dist/normalise-fleet.mjs +76 -0
- package/dist/normalise-fleet.mjs.map +1 -0
- package/dist/npm-cli-executable.d.mts +42 -0
- package/dist/npm-cli-executable.d.mts.map +1 -0
- package/dist/npm-cli-executable.mjs +159 -0
- package/dist/npm-cli-executable.mjs.map +1 -0
- package/dist/probe-outcome.d.mts +89 -0
- package/dist/probe-outcome.d.mts.map +1 -0
- package/dist/probe-outcome.mjs +104 -0
- package/dist/probe-outcome.mjs.map +1 -0
- package/dist/protocol-guard.d.mts +34 -0
- package/dist/protocol-guard.d.mts.map +1 -0
- package/dist/protocol-guard.mjs +121 -0
- package/dist/protocol-guard.mjs.map +1 -0
- package/dist/source-walk.d.mts +12 -0
- package/dist/source-walk.d.mts.map +1 -0
- package/dist/source-walk.mjs +56 -0
- package/dist/source-walk.mjs.map +1 -0
- package/dist/transient-fault.d.mts +6 -0
- package/dist/transient-fault.d.mts.map +1 -0
- package/dist/transient-fault.mjs +86 -0
- package/dist/transient-fault.mjs.map +1 -0
- package/dist/utm-deprecated.d.mts +6 -0
- package/dist/utm-deprecated.d.mts.map +1 -0
- package/dist/utm-deprecated.mjs +23 -0
- package/dist/utm-deprecated.mjs.map +1 -0
- package/dist/worker-code-check.d.mts +29 -0
- package/dist/worker-code-check.d.mts.map +1 -0
- package/dist/worker-code-check.mjs +78 -0
- package/dist/worker-code-check.mjs.map +1 -0
- package/dist/worker-health.d.mts +56 -0
- package/dist/worker-health.d.mts.map +1 -0
- package/dist/worker-health.mjs +73 -0
- package/dist/worker-health.mjs.map +1 -0
- package/dist/worker-http.d.mts +103 -0
- package/dist/worker-http.d.mts.map +1 -0
- package/dist/worker-http.mjs +277 -0
- package/dist/worker-http.mjs.map +1 -0
- package/dist/worker-stats.d.mts +66 -0
- package/dist/worker-stats.d.mts.map +1 -0
- package/dist/worker-stats.mjs +143 -0
- package/dist/worker-stats.mjs.map +1 -0
- package/package.json +96 -4
- package/src/local-worker/autounattend.xml +280 -0
- package/src/local-worker/build-vm.sh +218 -0
- package/src/local-worker/clone-worker.sh +141 -0
- package/src/local-worker/create-utm-vm.sh +202 -0
- package/src/local-worker/fetch-windows-iso.sh +238 -0
- package/src/local-worker/first-boot.cmd +58 -0
- package/src/local-worker/worker-ctl.sh +442 -0
- package/src/provisioning/README.md +28 -0
- package/src/provisioning/apply-foreground-lock-timeout.ps1 +71 -0
- package/src/provisioning/bare-metal/README.md +213 -0
- package/src/provisioning/bare-metal/a11y-bootstrap.service +58 -0
- package/src/provisioning/bare-metal/autounattend.xml +428 -0
- package/src/provisioning/bare-metal/serve-bootstrap.sh +86 -0
- package/src/provisioning/bootstrap-control-plane.sh +463 -0
- package/src/provisioning/bootstrap-windows-worker.ps1 +649 -0
- package/src/provisioning/build-lean-worker-image.ps1 +275 -0
- package/src/provisioning/diagnose-nvda-worker.ps1 +174 -0
- package/src/provisioning/provision-nvda-worker.ps1 +827 -0
- package/src/provisioning/set-display-mode.ps1 +411 -0
- package/src/provisioning/stamp-provision-revision.ps1 +184 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// Why is one worker slower than another? Run the same page on each and diff every phase.
|
|
4
|
+
//
|
|
5
|
+
// npm run worker:compare -- <page-url> <worker-url> <worker-url> [...] [--rounds=6]
|
|
6
|
+
//
|
|
7
|
+
// This exists because I spent hours attributing a 2x difference between two guests to the wrong phase.
|
|
8
|
+
// `bench-capture.mjs` gives a phase table for ONE worker, so comparing two meant eyeballing two
|
|
9
|
+
// printouts, and I compared wall times instead — which said "worker 1 is faster" without saying where.
|
|
10
|
+
// The moment the phases were put side by side the answer was immediate: `windowsActivate` was identical
|
|
11
|
+
// (7.4s vs 7.3s, so Edge launch was never the difference) and the entire gap was in `sweep`.
|
|
12
|
+
//
|
|
13
|
+
// It also surfaces the per-sweep detail that capture-core already records and nothing displayed.
|
|
14
|
+
// `collectByType` marks every sweep with its type, elapsed ms and round-trip count in each direction,
|
|
15
|
+
// and the aggregate hid all of it. Six sweeps collapsed into one number cannot tell you whether a worker
|
|
16
|
+
// is doing MORE round trips or SLOWER ones — and those have different causes.
|
|
17
|
+
//
|
|
18
|
+
// Samples are INTERLEAVED round-robin and reported as medians with an interquartile range, never as
|
|
19
|
+
// means. Both choices are corrections to how this went wrong before:
|
|
20
|
+
//
|
|
21
|
+
// - Sequential sampling (all of worker A, then all of worker B) charges any drift in the host during
|
|
22
|
+
// those minutes to the difference between the workers. Round-robin makes drift common to all of them.
|
|
23
|
+
// - A single mute-recovery outlier (~86s against a normal ~12s) moves a mean by 5s and makes a healthy
|
|
24
|
+
// worker look broken. The median and IQR do not move.
|
|
25
|
+
//
|
|
26
|
+
// It also refuses to declare a difference the samples do not support -- see `./worker-stats.mjs`.
|
|
27
|
+
// "Not distinguishable" is a real answer, and it is the one that was missing.
|
|
28
|
+
import { writeFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
29
|
+
import { refuseIfBusy, sampleVitals } from "./measure-guard.mjs";
|
|
30
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
31
|
+
import { compareWorkers, describe as summarise, recoveryRates } from "./worker-stats.mjs";
|
|
32
|
+
import { sampleHost, diffHost } from "./host-metrics.mjs";
|
|
33
|
+
import { requestJson, CAPTURE_CLIENT_TIMEOUT_MS } from "./worker-http.mjs";
|
|
34
|
+
import { resolve } from "node:path";
|
|
35
|
+
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
36
|
+
/**
|
|
37
|
+
* takes a page and two workers POSITIONALLY. `--runs=` is a documented alias of `--rounds=`, so both
|
|
38
|
+
* are accepted — a guard listing only one would refuse a spelling the code deliberately supports.
|
|
39
|
+
*
|
|
40
|
+
* An unrecognised flag is otherwise IGNORED, so it runs the default and reports success.
|
|
41
|
+
*/
|
|
42
|
+
refuseUnknownFlags(["--rounds=", "--runs="], { entry: import.meta.url, command: "npm run worker:compare" });
|
|
43
|
+
// `requestJson`, not `fetch`: undici stops waiting for response HEADERS at 300 s whatever the
|
|
44
|
+
// AbortSignal says, and the worker writes its status and body together at the END of a capture.
|
|
45
|
+
// See worker-http.mjs -- this budget sits at or above that cap, so it never applied.
|
|
46
|
+
// Resolved from THIS module, never the cwd -- same reason as `doctor.mjs`'s `DATASET`: this package
|
|
47
|
+
// cannot import `@a11ign/lab`'s canonical `runs/` resolution without a dependency cycle.
|
|
48
|
+
const OUT = resolve(fileURLToPath(new URL("../../../", import.meta.url)), "runs/worker-compare");
|
|
49
|
+
const MS_PER_S = 1000;
|
|
50
|
+
/**
|
|
51
|
+
* `dataset-paths.mjs`'s `refuseIfRunsReadonly`, duplicated rather than imported -- same reason `OUT`
|
|
52
|
+
* above resolves from this module's own location instead of importing `runsRoot()`: this package cannot
|
|
53
|
+
* import `@a11ign/lab` without a dependency cycle (see `dataset-paths.test.ts`'s own EXEMPT entry
|
|
54
|
+
* for this file). `A11Y_RUNS_READONLY=1 npm run worker:compare ...` must refuse and name `OUT` exactly
|
|
55
|
+
* like every other runs/ writer, so a peer asking "is this safe to run" gets one answer regardless of
|
|
56
|
+
* which package the script happens to live in.
|
|
57
|
+
*/
|
|
58
|
+
function refuseIfRunsReadonly() {
|
|
59
|
+
if (process.env.A11Y_RUNS_READONLY !== "1")
|
|
60
|
+
return;
|
|
61
|
+
console.error("REFUSING to write runs/worker-compare/compare.json — A11Y_RUNS_READONLY=1 is set.");
|
|
62
|
+
console.error(" Unset A11Y_RUNS_READONLY to run this for real.");
|
|
63
|
+
process.exit(3);
|
|
64
|
+
}
|
|
65
|
+
const args = process.argv.slice(2);
|
|
66
|
+
const runs = Number(args.find((a) => a.startsWith("--rounds="))?.slice("--rounds=".length)
|
|
67
|
+
?? args.find((a) => a.startsWith("--runs="))?.slice("--runs=".length) ?? 6);
|
|
68
|
+
const [page, ...workers] = args.filter((a) => !a.startsWith("--"));
|
|
69
|
+
const short = (/** @type {any} */ worker) => new URL(worker).hostname.split(".").pop();
|
|
70
|
+
async function capture(/** @type {any} */ worker) {
|
|
71
|
+
const response = await requestJson(`${worker.replace(/\/$/, "")}/capture`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
body: { url: page, probeForms: true },
|
|
74
|
+
timeoutMs: CAPTURE_CLIENT_TIMEOUT_MS,
|
|
75
|
+
});
|
|
76
|
+
const body = response.json ?? {};
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
throw new Error(`HTTP ${response.status}: ${JSON.stringify(body).slice(0, 160)}`);
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
async function diagnostics(/** @type {any} */ worker) {
|
|
82
|
+
try {
|
|
83
|
+
// `requestJson`, not `fetch`: the same worker JSON client every other probe in this file uses (see
|
|
84
|
+
// `capture`, above), rather than a second hand-rolled timeout mechanism for this one endpoint.
|
|
85
|
+
const response = await requestJson(`${worker.replace(/\/$/, "")}/diagnostics`, { timeoutMs: 180_000 });
|
|
86
|
+
// `?? null`: `response.json` is `undefined` on unparseable JSON (requestJson's own contract) rather
|
|
87
|
+
// than a throw, so this still folds into the same "an older worker has no /diagnostics" outcome.
|
|
88
|
+
return response.ok ? response.json ?? null : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null; // an older worker has no /diagnostics; the phase comparison still works
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Phase costs from one capture's diagnostics.
|
|
96
|
+
*
|
|
97
|
+
* Diagnostics carry a cumulative `atMs`, so a phase's own cost is the gap from the previous mark — the
|
|
98
|
+
* same arithmetic bench-capture does. Repeated events (there are six `sweep` marks) accumulate, which is
|
|
99
|
+
* what makes `sweep` look like one large phase.
|
|
100
|
+
*/
|
|
101
|
+
function phaseCosts(/** @type {any} */ entries) {
|
|
102
|
+
/** @type {Record<string, any>} */
|
|
103
|
+
const costs = {};
|
|
104
|
+
let previous = 0;
|
|
105
|
+
for (const entry of entries ?? []) {
|
|
106
|
+
if (typeof entry.atMs !== "number")
|
|
107
|
+
continue;
|
|
108
|
+
costs[entry.event] = (costs[entry.event] ?? 0) + (entry.atMs - previous);
|
|
109
|
+
previous = entry.atMs;
|
|
110
|
+
}
|
|
111
|
+
return costs;
|
|
112
|
+
}
|
|
113
|
+
/** Per-sweep detail, keyed by sweep type, which the aggregate throws away. */
|
|
114
|
+
function sweepDetail(/** @type {any} */ entries) {
|
|
115
|
+
/** @type {Record<string, any>} */
|
|
116
|
+
const byType = {};
|
|
117
|
+
for (const entry of entries ?? []) {
|
|
118
|
+
if (entry.event !== "sweep" || !entry.type)
|
|
119
|
+
continue;
|
|
120
|
+
const to = byType[entry.type] ??= { found: 0, ms: 0, trips: 0, n: 0 };
|
|
121
|
+
to.found += entry.found ?? 0;
|
|
122
|
+
to.ms += (entry.prevMs ?? 0) + (entry.nextMs ?? 0);
|
|
123
|
+
to.trips += (entry.prevTrips ?? 0) + (entry.nextTrips ?? 0);
|
|
124
|
+
to.n += 1;
|
|
125
|
+
}
|
|
126
|
+
return byType;
|
|
127
|
+
}
|
|
128
|
+
/** `sampleVitals` says on stderr why a worker has none, so a blank column in the report has a reason. */
|
|
129
|
+
async function vitals(/** @type {string} */ worker) {
|
|
130
|
+
return sampleVitals(worker, { warn: (line) => process.stderr.write(line) });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Only when RUN, never on import.
|
|
134
|
+
*
|
|
135
|
+
* This drove REAL captures against two workers at module scope -- so importing it to check the file still
|
|
136
|
+
* loads issued `runs * workers` captures, each up to 560 s, and wrote a report under `runs/worker-compare`.
|
|
137
|
+
* The usage guard moved in with it: a missing page argument is a mistake made by a CALLER, and there is no
|
|
138
|
+
* caller when a file is merely imported.
|
|
139
|
+
*/
|
|
140
|
+
async function main() {
|
|
141
|
+
if (!page || workers.length < 2) {
|
|
142
|
+
process.stderr.write("usage: npm run worker:compare -- <page-url> <worker> <worker> [--rounds=6]\n");
|
|
143
|
+
process.exit(2);
|
|
144
|
+
}
|
|
145
|
+
refuseIfRunsReadonly();
|
|
146
|
+
// REFUSE A BUSY BOX BEFORE MEASURING IT. This whole tool exists to answer "which worker is the
|
|
147
|
+
// problem", and four separate measurement errors came from sampling a box that was doing something
|
|
148
|
+
// else — including 12 straight 429s read as timings. `refuseIfBusy` was written for exactly this and
|
|
149
|
+
// was wired to nothing, which is the `scorer:verify` shape: a guard that exists and never runs.
|
|
150
|
+
await refuseIfBusy(workers, { what: `worker:compare against ${page}` });
|
|
151
|
+
// Per worker: the three series this comparison exists to print, plus the last run's diagnostics.
|
|
152
|
+
// Declared because every array here is empty at construction and inferred `never[]`, so each push --
|
|
153
|
+
// which IS the measurement -- reads as an error while the shape that discards it reads as fine.
|
|
154
|
+
/** @type {Record<string, { phases: any[], sweeps: any[], walls: number[], diagnostics: any }>} */
|
|
155
|
+
const results = Object.fromEntries(workers.map((/** @type {string} */ w) => [w, { phases: [], sweeps: [], walls: [], diagnostics: null }]));
|
|
156
|
+
/** @type {Record<string, any>} */
|
|
157
|
+
/** @type {Record<string, any>} */
|
|
158
|
+
const vitalsBefore = {};
|
|
159
|
+
/** @type {Record<string, any>} */
|
|
160
|
+
const vitalsAfter = {};
|
|
161
|
+
for (const worker of workers)
|
|
162
|
+
vitalsBefore[worker] = await vitals(worker);
|
|
163
|
+
// The foundations, sampled around the run. Timings alone say something got slower; they never say
|
|
164
|
+
// which resource ran out. Not sampling disk I/O is why three guests contending on one SSD was
|
|
165
|
+
// misdiagnosed as a memory problem, then as a guest problem, before anyone looked at the disk.
|
|
166
|
+
const hostBefore = sampleHost();
|
|
167
|
+
// INTERLEAVED, round-robin. Measuring one worker for five minutes and then the next attributes any drift
|
|
168
|
+
// in the host during those ten minutes to the difference between the workers -- which is how a shared
|
|
169
|
+
// Mac running builds and browsers produces a confident wrong answer. Round-robin makes drift common.
|
|
170
|
+
process.stdout.write(`\nInterleaving ${runs} round(s) across ${workers.length} worker(s)\n`);
|
|
171
|
+
for (let round = 1; round <= runs; round++) {
|
|
172
|
+
for (const worker of workers) {
|
|
173
|
+
try {
|
|
174
|
+
const started = Date.now();
|
|
175
|
+
const result = await capture(worker);
|
|
176
|
+
const wall = Date.now() - started;
|
|
177
|
+
results[worker].walls.push(wall);
|
|
178
|
+
results[worker].phases.push(phaseCosts(result.diagnostics));
|
|
179
|
+
results[worker].sweeps.push(sweepDetail(result.diagnostics));
|
|
180
|
+
process.stdout.write(` round ${round} ${short(worker)}: ${(wall / MS_PER_S).toFixed(1)}s ` +
|
|
181
|
+
`${result.transcript?.length ?? 0} phrases\n`);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
process.stdout.write(` round ${round} ${short(worker)}: FAILED ${ /** @type {any} */(error).message}\n`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const worker of workers) {
|
|
189
|
+
vitalsAfter[worker] = await vitals(worker);
|
|
190
|
+
results[worker].diagnostics = await diagnostics(worker);
|
|
191
|
+
}
|
|
192
|
+
report({ results, vitalsBefore, vitalsAfter, hostBefore });
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The comparison, and the verdict. Split from the MEASUREMENT half so neither exceeds the lint gate's
|
|
196
|
+
* 70 lines and complexity 15 -- and because they are genuinely different jobs: one drives the workers,
|
|
197
|
+
* the other decides what the numbers mean. `compare-workers` exists because reading two `bench-capture`
|
|
198
|
+
* printouts side by side attributed a 2x difference to the wrong phase for hours.
|
|
199
|
+
*/
|
|
200
|
+
function report(/** @type {any} */ { results, vitalsBefore, vitalsAfter, hostBefore }) {
|
|
201
|
+
// Shared by both tables below: which phases exist at all, and how to pull one worker's samples.
|
|
202
|
+
const allPhases = [...new Set(Object.values(results).flatMap((r) => r.phases.flatMap(Object.keys)))];
|
|
203
|
+
const phaseSamples = (/** @type {any} */ worker, /** @type {any} */ phase) => results[worker].phases.map((/** @type {any} */ p) => (p[phase] ?? 0) / MS_PER_S);
|
|
204
|
+
const { verdict, deltas } = reportWallTime({ results, vitalsBefore, vitalsAfter });
|
|
205
|
+
reportPhases({ results, allPhases, phaseSamples });
|
|
206
|
+
const { foundations, hostAfter } = reportFoundations({ hostBefore });
|
|
207
|
+
mkdirSync(OUT, { recursive: true });
|
|
208
|
+
writeFileSync(resolve(OUT, "compare.json"), JSON.stringify({ page, rounds: runs, results, verdict, recoveryDeltas: deltas,
|
|
209
|
+
foundations, hostBefore, hostAfter }, null, 2) + "\n", "utf8");
|
|
210
|
+
process.stdout.write(`\nReport: ${resolve(OUT, "compare.json")}\n`);
|
|
211
|
+
}
|
|
212
|
+
/** Wall time and the verdict, first, because that is the question being asked. */
|
|
213
|
+
function reportWallTime(/** @type {any} */ { results, vitalsBefore, vitalsAfter }) {
|
|
214
|
+
// Wall time first, with the verdict, because that is the question being asked.
|
|
215
|
+
const wallSeconds = Object.fromEntries(workers.map((w) => [short(w), results[w].walls.map((/** @type {any} */ v) => v / MS_PER_S)]));
|
|
216
|
+
const verdict = compareWorkers(wallSeconds);
|
|
217
|
+
process.stdout.write(`\n\nWALL TIME (seconds), ${runs} interleaved round(s)\n`);
|
|
218
|
+
process.stdout.write(` ${"worker".padEnd(10)}${"n".padStart(4)}${"median".padStart(9)}${"IQR".padStart(9)}` +
|
|
219
|
+
`${"min".padStart(8)}${"max".padStart(8)} recoveries\n`);
|
|
220
|
+
const deltas = Object.fromEntries(workers.map((w) => [short(w), {
|
|
221
|
+
recoveries: (vitalsAfter[w]?.recoveries ?? 0) - (vitalsBefore[w]?.recoveries ?? 0),
|
|
222
|
+
captures: (vitalsAfter[w]?.captures ?? 0) - (vitalsBefore[w]?.captures ?? 0),
|
|
223
|
+
}]));
|
|
224
|
+
const rates = /** @type {Record<string, any>} */ (recoveryRates(deltas));
|
|
225
|
+
for (const w of workers) {
|
|
226
|
+
const name = String(short(w)), d = summarise(wallSeconds[name]);
|
|
227
|
+
if (!d)
|
|
228
|
+
continue;
|
|
229
|
+
const rate = rates[name] === null ? "n/a" : `${deltas[name].recoveries}/${deltas[name].captures}`;
|
|
230
|
+
process.stdout.write(` ${name.padEnd(10)}${String(d.n).padStart(4)}${d.median.toFixed(1).padStart(9)}` +
|
|
231
|
+
`${d.iqr.toFixed(1).padStart(9)}${d.min.toFixed(1).padStart(8)}${d.max.toFixed(1).padStart(8)} ${rate}\n`);
|
|
232
|
+
}
|
|
233
|
+
process.stdout.write(`\n ${verdict.verdict}\n`);
|
|
234
|
+
// Returned rather than written here: the JSON report is assembled once, by `report`, so one place
|
|
235
|
+
// decides what a comparison record contains.
|
|
236
|
+
return { verdict, deltas };
|
|
237
|
+
}
|
|
238
|
+
/** Where a real difference actually LIVES: per-phase medians, then the per-sweep detail. */
|
|
239
|
+
function reportPhases(/** @type {any} */ { results, allPhases, phaseSamples }) {
|
|
240
|
+
// Per-phase medians, so a real difference can be located rather than guessed at.
|
|
241
|
+
process.stdout.write(`\nPHASE MEDIANS (seconds)\n`);
|
|
242
|
+
process.stdout.write(` ${"phase".padEnd(20)}${workers.map((/** @type {string} */ w) => String(short(w)).padStart(9)).join("")} spread\n`);
|
|
243
|
+
const rows = allPhases
|
|
244
|
+
.map((/** @type {any} */ phase) => {
|
|
245
|
+
const medians = workers.map((w) => summarise(phaseSamples(w, phase))?.median ?? 0);
|
|
246
|
+
return { phase, medians, spread: Math.max(...medians) - Math.min(...medians) };
|
|
247
|
+
})
|
|
248
|
+
.filter((/** @type {any} */ row) => row.medians.some((/** @type {any} */ v) => v >= 0.05))
|
|
249
|
+
.sort((/** @type {any} */ a, /** @type {any} */ b) => b.spread - a.spread);
|
|
250
|
+
for (const { phase, medians, spread } of rows) {
|
|
251
|
+
process.stdout.write(` ${phase.padEnd(20)}${medians.map((/** @type {any} */ v) => v.toFixed(1).padStart(9)).join("")}` +
|
|
252
|
+
`${spread.toFixed(1).padStart(10)}${spread >= 1 ? " <-- diverges" : ""}\n`);
|
|
253
|
+
}
|
|
254
|
+
// The per-sweep view. `found` and `trips` separate "doing more work" from "doing work more slowly",
|
|
255
|
+
// which is the distinction the aggregate destroys and the one that identifies a cause.
|
|
256
|
+
const sweepTypes = [...new Set(Object.values(results).flatMap((r) => r.sweeps.flatMap(Object.keys)))];
|
|
257
|
+
if (sweepTypes.length) {
|
|
258
|
+
process.stdout.write("\nSWEEP DETAIL (mean ms / round trips / elements found)\n");
|
|
259
|
+
for (const type of sweepTypes) {
|
|
260
|
+
process.stdout.write(` ${type.padEnd(14)}`);
|
|
261
|
+
for (const w of workers) {
|
|
262
|
+
const runsWith = results[w].sweeps.map((/** @type {any} */ s) => s[type]).filter(Boolean);
|
|
263
|
+
// Medians here too: one mute recovery inside a sweep skews a mean the same way it skews wall time.
|
|
264
|
+
const ms = (summarise(runsWith.map((/** @type {any} */ x) => x.ms))?.median ?? 0).toFixed(0);
|
|
265
|
+
const trips = (summarise(runsWith.map((/** @type {any} */ x) => x.trips))?.median ?? 0).toFixed(1);
|
|
266
|
+
const found = (summarise(runsWith.map((/** @type {any} */ x) => x.found))?.median ?? 0).toFixed(1);
|
|
267
|
+
process.stdout.write(`${short(w)}: ${ms}ms/${trips}t/${found}f `);
|
|
268
|
+
}
|
|
269
|
+
process.stdout.write("\n");
|
|
270
|
+
}
|
|
271
|
+
process.stdout.write("\n ms up but trips flat => each round trip is slower (the guest or NVDA is the cause).\n");
|
|
272
|
+
process.stdout.write(" trips up => the sweep is genuinely walking more (the page or dedupe differs).\n");
|
|
273
|
+
}
|
|
274
|
+
for (const w of workers) {
|
|
275
|
+
const d = results[w].diagnostics;
|
|
276
|
+
if (!d)
|
|
277
|
+
continue;
|
|
278
|
+
process.stdout.write(`\n ${short(w)} guest: profile=${d.edgeProfile?.megabytes}MB ` +
|
|
279
|
+
`policy=${JSON.stringify(d.edgePolicy)} procs=${JSON.stringify(d.processes)}\n`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The foundations. Timings alone say something got slower; they never say which resource ran out --
|
|
284
|
+
* which is why three guests contending on one SSD was misdiagnosed as memory, then as the guests.
|
|
285
|
+
*/
|
|
286
|
+
function reportFoundations(/** @type {any} */ { hostBefore }) {
|
|
287
|
+
const hostAfter = sampleHost();
|
|
288
|
+
const foundations = diffHost(/** @type {any} */ (hostBefore), /** @type {any} */ (hostAfter));
|
|
289
|
+
// These describe THE MACHINE THIS COMMAND RAN ON, which is the workers' machine only while the workers
|
|
290
|
+
// are local UTM guests. A bare-metal fleet is four separate computers, and this host's swap state then
|
|
291
|
+
// says nothing whatever about their capture times.
|
|
292
|
+
//
|
|
293
|
+
// It printed `352 pageouts <-- THE HOST WAS SWAPPING; these timings describe a constrained machine`
|
|
294
|
+
// during a four-box bare-metal run, next to `guest resident 0 MB across 0 process(es)` — the second line
|
|
295
|
+
// already saying there were no guests here to constrain. Attributing worker timings to the wrong
|
|
296
|
+
// computer is the exact failure this tool was written to stop; it should not commit it itself.
|
|
297
|
+
const localGuests = hostAfter.processes.length;
|
|
298
|
+
process.stdout.write(localGuests
|
|
299
|
+
? "\nFOUNDATIONS (the host, during this run)\n"
|
|
300
|
+
: "\nFOUNDATIONS (this CONTROL host only — no local guests, so none of this describes the workers)\n");
|
|
301
|
+
process.stdout.write(` load ${hostAfter.load?.one ?? "?"} (1m), ` +
|
|
302
|
+
`${hostAfter.load?.five ?? "?"} (5m)\n`);
|
|
303
|
+
const busiest = [...(hostAfter.disk ?? [])].sort((a, b) => b.mbPerSecond - a.mbPerSecond)[0];
|
|
304
|
+
process.stdout.write(` disk ${busiest ? `${busiest.device} ${busiest.mbPerSecond} MB/s, ` +
|
|
305
|
+
`${busiest.transfersPerSecond} tps` : "unavailable"}\n`);
|
|
306
|
+
process.stdout.write(` guest resident ${foundations.residentMbTotal} MB across ` +
|
|
307
|
+
`${hostAfter.processes.length} process(es) <- RSS, not phys_footprint\n`);
|
|
308
|
+
process.stdout.write(` free / compressed ${foundations.freeMb} MB free, ` +
|
|
309
|
+
`${foundations.compressorMb} MB compressed\n`);
|
|
310
|
+
// The pageout DELTA is still worth printing either way — it is true about this machine. What is gated is
|
|
311
|
+
// the INFERENCE, because "these timings describe a constrained machine" is only true when the timings
|
|
312
|
+
// were produced here.
|
|
313
|
+
process.stdout.write(` paging ${foundations.pageoutsDelta} pageouts during the run` +
|
|
314
|
+
`${foundations.swappingDuringRun
|
|
315
|
+
? (localGuests
|
|
316
|
+
? " <-- THE HOST WAS SWAPPING; these timings describe a constrained machine"
|
|
317
|
+
: " (on this control host; the workers are elsewhere, so this did not affect them)")
|
|
318
|
+
: " (none)"}\n`);
|
|
319
|
+
if (!localGuests) {
|
|
320
|
+
process.stdout.write("\n No local guest processes were found, so these workers are REMOTE machines and nothing\n");
|
|
321
|
+
process.stdout.write(" above describes them. For a bare-metal fleet, each worker's own /diagnostics is the\n");
|
|
322
|
+
process.stdout.write(" equivalent: curl http://<worker>:8765/diagnostics | jq .\n");
|
|
323
|
+
}
|
|
324
|
+
return { foundations, hostAfter };
|
|
325
|
+
}
|
|
326
|
+
// REALPATH'D: `import.meta.url` is resolved through symlinks by Node's ESM loader and `process.argv[1]`
|
|
327
|
+
// is not, so a bin reached via its `.bin` symlink (which is how npm always installs one) mismatched here
|
|
328
|
+
// and this guard silently read false — the tool loaded, did nothing, and exited 0. `/var` and `/tmp` are
|
|
329
|
+
// themselves symlinks on macOS, so this fired every time. Same defect, same fix, as `cli.ts`'s `isProgram`.
|
|
330
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href)
|
|
331
|
+
await main();
|
|
332
|
+
//# sourceMappingURL=compare-workers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-workers.mjs","sourceRoot":"","sources":["../src/compare-workers.mjs"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE FLEET KEY MUST NOT SIT BESIDE npm's TRANSITIVE DEPENDENCIES — ADR 0012, enforced rather than stated.
|
|
3
|
+
*
|
|
4
|
+
* That ADR's decisive argument is not tidiness: *"the credential able to reconfigure the entire fleet would
|
|
5
|
+
* sit next to the largest supply-chain surface in the system. A compromised transitive dependency in the
|
|
6
|
+
* capture pipeline could reach the SSH key and, from there, twelve Windows boxes that auto-log-in to
|
|
7
|
+
* unlocked desktops."*
|
|
8
|
+
*
|
|
9
|
+
* MEASURED 2026-08-29, on the control plane the ADR describes:
|
|
10
|
+
*
|
|
11
|
+
* /root/a11y-witness/node_modules 56M, 121 packages
|
|
12
|
+
* /root/.ssh/a11y-witness_ed25519 the fleet key
|
|
13
|
+
*
|
|
14
|
+
* Exactly the configuration it forbids, on the machine it was written about, for as long as nobody looked.
|
|
15
|
+
* The ADR was accurate about the intent and described a system that did not exist — which is worse than no
|
|
16
|
+
* ADR, because it is read as a guarantee.
|
|
17
|
+
*
|
|
18
|
+
* NOTHING THERE NEEDED THEM, and that is checkable rather than a judgement: `code-version.mjs` — the only
|
|
19
|
+
* thing fleet management imports — has zero bare imports (`node:crypto`, `node:fs`, `node:path`,
|
|
20
|
+
* `node:url`, one sibling), and `deploy.yml` imports it BY PATH for exactly this reason. Removing 56 MB
|
|
21
|
+
* left `codeVersion()` byte-identical at `6645b75480b40003`.
|
|
22
|
+
*
|
|
23
|
+
* So this check exists because the deletion is the easy half. `npm install`, run once on that box by
|
|
24
|
+
* somebody debugging, silently restores the violation — this repo's own rule about anything relying on a
|
|
25
|
+
* human to remember, applied to a security boundary rather than to housekeeping.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Whether a control-plane host is carrying what it must not.
|
|
29
|
+
*
|
|
30
|
+
* PURE, so it can be tested without a control plane: the caller does the looking, this decides. The
|
|
31
|
+
* integration half needs a Proxmox host and root, which is precisely the kind of check that never runs.
|
|
32
|
+
*
|
|
33
|
+
* @param {{ hasNodeModules: boolean, hasFleetKey: boolean, packages?: number, isWorkspace?: boolean }} found
|
|
34
|
+
* @returns {{ violated: boolean, why: string }}
|
|
35
|
+
*/
|
|
36
|
+
export function controlPlaneIsolation({ hasNodeModules, hasFleetKey, packages, isWorkspace }: {
|
|
37
|
+
hasNodeModules: boolean;
|
|
38
|
+
hasFleetKey: boolean;
|
|
39
|
+
packages?: number;
|
|
40
|
+
isWorkspace?: boolean;
|
|
41
|
+
}): {
|
|
42
|
+
violated: boolean;
|
|
43
|
+
why: string;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=control-plane-isolation.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-plane-isolation.d.mts","sourceRoot":"","sources":["../src/control-plane-isolation.mjs"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;;;;GAQG;AACH,8FAHW;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GACzF;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CA+B9C"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* THE FLEET KEY MUST NOT SIT BESIDE npm's TRANSITIVE DEPENDENCIES — ADR 0012, enforced rather than stated.
|
|
4
|
+
*
|
|
5
|
+
* That ADR's decisive argument is not tidiness: *"the credential able to reconfigure the entire fleet would
|
|
6
|
+
* sit next to the largest supply-chain surface in the system. A compromised transitive dependency in the
|
|
7
|
+
* capture pipeline could reach the SSH key and, from there, twelve Windows boxes that auto-log-in to
|
|
8
|
+
* unlocked desktops."*
|
|
9
|
+
*
|
|
10
|
+
* MEASURED 2026-08-29, on the control plane the ADR describes:
|
|
11
|
+
*
|
|
12
|
+
* /root/a11y-witness/node_modules 56M, 121 packages
|
|
13
|
+
* /root/.ssh/a11y-witness_ed25519 the fleet key
|
|
14
|
+
*
|
|
15
|
+
* Exactly the configuration it forbids, on the machine it was written about, for as long as nobody looked.
|
|
16
|
+
* The ADR was accurate about the intent and described a system that did not exist — which is worse than no
|
|
17
|
+
* ADR, because it is read as a guarantee.
|
|
18
|
+
*
|
|
19
|
+
* NOTHING THERE NEEDED THEM, and that is checkable rather than a judgement: `code-version.mjs` — the only
|
|
20
|
+
* thing fleet management imports — has zero bare imports (`node:crypto`, `node:fs`, `node:path`,
|
|
21
|
+
* `node:url`, one sibling), and `deploy.yml` imports it BY PATH for exactly this reason. Removing 56 MB
|
|
22
|
+
* left `codeVersion()` byte-identical at `6645b75480b40003`.
|
|
23
|
+
*
|
|
24
|
+
* So this check exists because the deletion is the easy half. `npm install`, run once on that box by
|
|
25
|
+
* somebody debugging, silently restores the violation — this repo's own rule about anything relying on a
|
|
26
|
+
* human to remember, applied to a security boundary rather than to housekeeping.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Whether a control-plane host is carrying what it must not.
|
|
30
|
+
*
|
|
31
|
+
* PURE, so it can be tested without a control plane: the caller does the looking, this decides. The
|
|
32
|
+
* integration half needs a Proxmox host and root, which is precisely the kind of check that never runs.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ hasNodeModules: boolean, hasFleetKey: boolean, packages?: number, isWorkspace?: boolean }} found
|
|
35
|
+
* @returns {{ violated: boolean, why: string }}
|
|
36
|
+
*/
|
|
37
|
+
export function controlPlaneIsolation({ hasNodeModules, hasFleetKey, packages, isWorkspace = false }) {
|
|
38
|
+
// BOTH, and neither alone. `node_modules` on a box with no fleet key is a lab or a laptop, which is
|
|
39
|
+
// where they belong. A fleet key with no dependencies beside it is the intended configuration. The
|
|
40
|
+
// defect is the ADJACENCY, and reporting either half alone would fire on correct machines -- a guard
|
|
41
|
+
// that cries wolf is one people switch off.
|
|
42
|
+
if (!hasNodeModules || !hasFleetKey) {
|
|
43
|
+
return { violated: false, why: hasFleetKey
|
|
44
|
+
? "fleet key present, no node_modules beside it — ADR 0012 holds"
|
|
45
|
+
: "no fleet key here, so nothing to isolate it from" };
|
|
46
|
+
}
|
|
47
|
+
const count = packages === undefined ? "" : ` (${packages} packages)`;
|
|
48
|
+
// THE REMEDY DEPENDS ON WHICH MACHINE THIS IS, and getting that wrong makes the guard useless. On a
|
|
49
|
+
// control plane the dependencies are vestigial and go. On a DEVELOPER'S machine they are the whole point
|
|
50
|
+
// of the machine, so the thing that must leave is the KEY -- which is what makes the control plane worth
|
|
51
|
+
// having. Telling a developer to delete their node_modules would be advice nobody can take, and a guard
|
|
52
|
+
// whose advice cannot be taken is one that gets muted.
|
|
53
|
+
const remedy = isWorkspace
|
|
54
|
+
? "This is a WORKSPACE, so the dependencies belong here and the KEY does not. Dispatch fleet work "
|
|
55
|
+
+ "through the control plane instead of holding `a11y-witness_ed25519` beside 100 MB of packages "
|
|
56
|
+
+ "you did not audit — see docs/control-plane-plan.md L3."
|
|
57
|
+
: "Nothing on a control plane needs them — `code-version.mjs` has no bare imports and `deploy.yml` "
|
|
58
|
+
+ "imports it by path. Remove them: `rm -rf ~/a11y-witness/node_modules` (measured 2026-08-29: "
|
|
59
|
+
+ "codeVersion was byte-identical afterwards).";
|
|
60
|
+
return {
|
|
61
|
+
violated: true,
|
|
62
|
+
why: `ADR 0012 VIOLATED: node_modules${count} sits beside the fleet SSH key. A compromised transitive `
|
|
63
|
+
+ "dependency here can reach the key that reconfigures every Windows worker, each of which "
|
|
64
|
+
+ `auto-logs-in to an unlocked desktop. ${remedy}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=control-plane-isolation.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-plane-isolation.mjs","sourceRoot":"","sources":["../src/control-plane-isolation.mjs"],"names":[],"mappings":"AAAA,YAAY;AACZ;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,GAAG,KAAK,EAAE;IAClG,oGAAoG;IACpG,mGAAmG;IACnG,qGAAqG;IACrG,4CAA4C;IAC5C,IAAI,CAAC,cAAc,IAAI,CAAC,WAAW,EAAE,CAAC;QACpC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW;gBACxC,CAAC,CAAC,+DAA+D;gBACjE,CAAC,CAAC,kDAAkD,EAAE,CAAC;IAC3D,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,QAAQ,YAAY,CAAC;IACtE,oGAAoG;IACpG,yGAAyG;IACzG,yGAAyG;IACzG,wGAAwG;IACxG,uDAAuD;IACvD,MAAM,MAAM,GAAG,WAAW;QACxB,CAAC,CAAC,iGAAiG;cAC/F,gGAAgG;cAChG,wDAAwD;QAC5D,CAAC,CAAC,kGAAkG;cAChG,8FAA8F;cAC9F,6CAA6C,CAAC;IACpD,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,GAAG,EAAE,kCAAkC,KAAK,2DAA2D;cACnG,0FAA0F;cAC1F,wCAAwC,MAAM,EAAE;KACrD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy-worker.d.mts","sourceRoot":"","sources":["../src/deploy-worker.mjs"],"names":[],"mappings":""}
|