@a11ign/screenreader-fleet 0.1.0 → 0.3.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/capture-client.d.mts +0 -1
- package/dist/capture-client.mjs +144 -306
- package/dist/check-worker-code.d.mts +0 -1
- package/dist/check-worker-code.mjs +42 -141
- package/dist/cli-flags.d.mts +0 -1
- package/dist/cli-flags.mjs +33 -179
- package/dist/code-drift.d.mts +0 -1
- package/dist/command-line-census.d.mts +0 -1
- package/dist/compare-workers.d.mts +0 -1
- package/dist/compare-workers.mjs +383 -255
- package/dist/control-plane-isolation.d.mts +0 -1
- package/dist/deploy-worker.d.mts +0 -1
- package/dist/deploy-worker.mjs +101 -242
- package/dist/doctor.d.mts +0 -1
- package/dist/doctor.mjs +361 -809
- package/dist/fleet-consistency.d.mts +0 -1
- package/dist/fleet-consistency.mjs +155 -379
- package/dist/fleet-env.d.mts +0 -1
- package/dist/fleet-env.mjs +148 -438
- package/dist/fleet-scripts.d.mts +0 -1
- package/dist/git-safe-env.d.mts +0 -1
- package/dist/guest-run.d.mts +0 -1
- package/dist/host-address.d.mts +0 -1
- package/dist/host-address.mjs +19 -90
- package/dist/host-capacity.d.mts +0 -1
- package/dist/host-capacity.mjs +22 -136
- package/dist/host-metrics.d.mts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +231 -0
- package/dist/local-vm.d.ts +0 -1
- package/dist/measure-guard.d.mts +0 -1
- package/dist/normalise-fleet.d.mts +0 -1
- package/dist/npm-cli-executable.d.mts +0 -1
- package/dist/probe-outcome.d.mts +0 -1
- package/dist/probe-outcome.mjs +50 -96
- package/dist/protocol-guard.d.mts +0 -1
- package/dist/source-walk.d.mts +0 -1
- package/dist/src_fleet-scripts_mjs.mjs +16 -0
- package/dist/src_git-safe-env_mjs.mjs +9 -0
- package/dist/src_utm-deprecated_mjs.mjs +4 -0
- package/dist/transient-fault.d.mts +0 -1
- package/dist/transient-fault.mjs +21 -81
- package/dist/utm-deprecated.d.mts +0 -1
- package/dist/worker-code-check.d.mts +0 -1
- package/dist/worker-code-check.mjs +127 -76
- package/dist/worker-health.d.mts +0 -1
- package/dist/worker-health.mjs +16 -61
- package/dist/worker-http.d.mts +0 -1
- package/dist/worker-http.mjs +42 -234
- package/dist/worker-stats.d.mts +0 -1
- package/package.json +12 -5
- package/dist/capture-client.d.mts.map +0 -1
- package/dist/capture-client.mjs.map +0 -1
- package/dist/check-worker-code.d.mts.map +0 -1
- package/dist/check-worker-code.mjs.map +0 -1
- package/dist/cli-flags.d.mts.map +0 -1
- package/dist/cli-flags.mjs.map +0 -1
- package/dist/code-drift.d.mts.map +0 -1
- package/dist/code-drift.mjs +0 -284
- package/dist/code-drift.mjs.map +0 -1
- package/dist/command-line-census.d.mts.map +0 -1
- package/dist/command-line-census.mjs +0 -96
- package/dist/command-line-census.mjs.map +0 -1
- package/dist/compare-workers.d.mts.map +0 -1
- package/dist/compare-workers.mjs.map +0 -1
- package/dist/control-plane-isolation.d.mts.map +0 -1
- package/dist/control-plane-isolation.mjs +0 -67
- package/dist/control-plane-isolation.mjs.map +0 -1
- package/dist/deploy-worker.d.mts.map +0 -1
- package/dist/deploy-worker.mjs.map +0 -1
- package/dist/doctor.d.mts.map +0 -1
- package/dist/doctor.mjs.map +0 -1
- package/dist/fleet-consistency.d.mts.map +0 -1
- package/dist/fleet-consistency.mjs.map +0 -1
- package/dist/fleet-env.d.mts.map +0 -1
- package/dist/fleet-env.mjs.map +0 -1
- package/dist/fleet-scripts.d.mts.map +0 -1
- package/dist/fleet-scripts.mjs +0 -41
- package/dist/fleet-scripts.mjs.map +0 -1
- package/dist/git-safe-env.d.mts.map +0 -1
- package/dist/git-safe-env.mjs +0 -44
- package/dist/git-safe-env.mjs.map +0 -1
- package/dist/guest-run.d.mts.map +0 -1
- package/dist/guest-run.mjs +0 -164
- package/dist/guest-run.mjs.map +0 -1
- package/dist/host-address.d.mts.map +0 -1
- package/dist/host-address.mjs.map +0 -1
- package/dist/host-capacity.d.mts.map +0 -1
- package/dist/host-capacity.mjs.map +0 -1
- package/dist/host-metrics.d.mts.map +0 -1
- package/dist/host-metrics.mjs +0 -201
- package/dist/host-metrics.mjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -25
- package/dist/index.js.map +0 -1
- package/dist/local-vm.d.ts.map +0 -1
- package/dist/local-vm.js +0 -360
- package/dist/local-vm.js.map +0 -1
- package/dist/measure-guard.d.mts.map +0 -1
- package/dist/measure-guard.mjs +0 -73
- package/dist/measure-guard.mjs.map +0 -1
- package/dist/normalise-fleet.d.mts.map +0 -1
- package/dist/normalise-fleet.mjs +0 -76
- package/dist/normalise-fleet.mjs.map +0 -1
- package/dist/npm-cli-executable.d.mts.map +0 -1
- package/dist/npm-cli-executable.mjs +0 -159
- package/dist/npm-cli-executable.mjs.map +0 -1
- package/dist/probe-outcome.d.mts.map +0 -1
- package/dist/probe-outcome.mjs.map +0 -1
- package/dist/protocol-guard.d.mts.map +0 -1
- package/dist/protocol-guard.mjs +0 -121
- package/dist/protocol-guard.mjs.map +0 -1
- package/dist/source-walk.d.mts.map +0 -1
- package/dist/source-walk.mjs +0 -56
- package/dist/source-walk.mjs.map +0 -1
- package/dist/transient-fault.d.mts.map +0 -1
- package/dist/transient-fault.mjs.map +0 -1
- package/dist/utm-deprecated.d.mts.map +0 -1
- package/dist/utm-deprecated.mjs +0 -23
- package/dist/utm-deprecated.mjs.map +0 -1
- package/dist/worker-code-check.d.mts.map +0 -1
- package/dist/worker-code-check.mjs.map +0 -1
- package/dist/worker-health.d.mts.map +0 -1
- package/dist/worker-health.mjs.map +0 -1
- package/dist/worker-http.d.mts.map +0 -1
- package/dist/worker-http.mjs.map +0 -1
- package/dist/worker-stats.d.mts.map +0 -1
- package/dist/worker-stats.mjs +0 -143
- package/dist/worker-stats.mjs.map +0 -1
package/dist/compare-workers.mjs
CHANGED
|
@@ -1,123 +1,285 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
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";
|
|
2
|
+
import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
34
5
|
import { resolve } from "node:path";
|
|
6
|
+
import { describeProbe, probeHealth, WORKER_PROBE_TIMEOUT_MS } from "./probe-outcome.mjs";
|
|
7
|
+
import { CAPTURE_CLIENT_TIMEOUT_MS, requestJson } from "./worker-http.mjs";
|
|
35
8
|
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
9
|
+
async function refuseIfBusy(workers, { what, timeoutMs = WORKER_PROBE_TIMEOUT_MS, request }) {
|
|
10
|
+
const states = await Promise.all(workers.map(async (worker)=>{
|
|
11
|
+
const probe = await probeHealth(worker, {
|
|
12
|
+
timeoutMs,
|
|
13
|
+
request
|
|
14
|
+
});
|
|
15
|
+
if ("ready" === probe.outcome) return {
|
|
16
|
+
worker,
|
|
17
|
+
free: true,
|
|
18
|
+
why: ""
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
worker,
|
|
22
|
+
free: false,
|
|
23
|
+
why: describeProbe(probe, {
|
|
24
|
+
worker,
|
|
25
|
+
timeoutMs
|
|
26
|
+
})
|
|
27
|
+
};
|
|
28
|
+
}));
|
|
29
|
+
const busy = states.filter((s)=>!s.free);
|
|
30
|
+
if (!busy.length) return;
|
|
31
|
+
throw new Error(`REFUSING to measure ${what}: ${busy.length} of ${workers.length} worker(s) are not free — ${busy.map((b)=>b.why).join(" ")} A measurement taken against a busy box samples its 429s, not its behaviour, and returns a number that looks real. Wait, or pass a worker that is free.`);
|
|
32
|
+
}
|
|
33
|
+
const VITALS_TIMEOUT_MS = 20000;
|
|
34
|
+
async function sampleVitals(worker, { timeoutMs = VITALS_TIMEOUT_MS, request, warn = ()=>{} } = {}) {
|
|
35
|
+
const probe = await probeHealth(worker, {
|
|
36
|
+
timeoutMs,
|
|
37
|
+
request
|
|
38
|
+
});
|
|
39
|
+
if ("refused" === probe.outcome || "no-answer" === probe.outcome) {
|
|
40
|
+
warn(` no vitals sampled: ${describeProbe(probe, {
|
|
41
|
+
worker,
|
|
42
|
+
timeoutMs
|
|
43
|
+
})}\n`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return probe.health?.vitals ?? null;
|
|
47
|
+
}
|
|
48
|
+
const MIN_ROUNDS_FOR_A_VERDICT = 5;
|
|
49
|
+
const sorted = (values)=>[
|
|
50
|
+
...values
|
|
51
|
+
].sort((a, b)=>a - b);
|
|
52
|
+
function quantile(values, q) {
|
|
53
|
+
if (!values.length) return null;
|
|
54
|
+
const s = sorted(values);
|
|
55
|
+
const position = (s.length - 1) * q;
|
|
56
|
+
const low = Math.floor(position), high = Math.ceil(position);
|
|
57
|
+
return low === high ? s[low] : s[low] + (s[high] - s[low]) * (position - low);
|
|
58
|
+
}
|
|
59
|
+
function describe(values) {
|
|
60
|
+
if (!values.length) return null;
|
|
61
|
+
const q1 = quantile(values, 0.25);
|
|
62
|
+
const q3 = quantile(values, 0.75);
|
|
63
|
+
return {
|
|
64
|
+
n: values.length,
|
|
65
|
+
median: quantile(values, 0.5),
|
|
66
|
+
q1,
|
|
67
|
+
q3,
|
|
68
|
+
iqr: q3 - q1,
|
|
69
|
+
min: Math.min(...values),
|
|
70
|
+
max: Math.max(...values)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function overlaps(a, b) {
|
|
74
|
+
return a.q1 <= b.q3 && b.q1 <= a.q3;
|
|
75
|
+
}
|
|
76
|
+
function compareWorkers(samplesByWorker) {
|
|
77
|
+
const stats = {};
|
|
78
|
+
for (const [worker, values] of Object.entries(samplesByWorker)){
|
|
79
|
+
const description = describe(values);
|
|
80
|
+
if (description) stats[worker] = description;
|
|
81
|
+
}
|
|
82
|
+
const names = Object.keys(stats);
|
|
83
|
+
if (names.length < 2) return {
|
|
84
|
+
stats,
|
|
85
|
+
slowest: null,
|
|
86
|
+
fastest: null,
|
|
87
|
+
distinguishable: false,
|
|
88
|
+
verdict: "need at least two workers with samples"
|
|
89
|
+
};
|
|
90
|
+
const rounds = Math.min(...names.map((n)=>stats[n].n));
|
|
91
|
+
const byMedian = [
|
|
92
|
+
...names
|
|
93
|
+
].sort((a, b)=>stats[a].median - stats[b].median);
|
|
94
|
+
const fastest = byMedian[0], slowest = byMedian[byMedian.length - 1];
|
|
95
|
+
const gap = stats[slowest].median - stats[fastest].median;
|
|
96
|
+
const widest = Math.max(stats[fastest].iqr, stats[slowest].iqr);
|
|
97
|
+
if (rounds < MIN_ROUNDS_FOR_A_VERDICT) return {
|
|
98
|
+
stats,
|
|
99
|
+
slowest,
|
|
100
|
+
fastest,
|
|
101
|
+
distinguishable: false,
|
|
102
|
+
verdict: `only ${rounds} round(s) — too few to claim anything. Run at least ${MIN_ROUNDS_FOR_A_VERDICT}.`
|
|
103
|
+
};
|
|
104
|
+
if (gap <= widest || overlaps(stats[fastest], stats[slowest])) return {
|
|
105
|
+
stats,
|
|
106
|
+
slowest,
|
|
107
|
+
fastest,
|
|
108
|
+
distinguishable: false,
|
|
109
|
+
verdict: `NOT DISTINGUISHABLE: medians differ by ${gap.toFixed(1)} but the spread is ${widest.toFixed(1)} and the interquartile ranges overlap. These samples do not support a difference between the workers.`
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
stats,
|
|
113
|
+
slowest,
|
|
114
|
+
fastest,
|
|
115
|
+
distinguishable: true,
|
|
116
|
+
verdict: `${slowest} is slower than ${fastest} by ${gap.toFixed(1)} (medians), with non-overlapping interquartile ranges over ${rounds} interleaved rounds.`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function recoveryRates(deltas) {
|
|
120
|
+
const rates = {};
|
|
121
|
+
for (const [worker, d] of Object.entries(deltas))rates[worker] = d.captures > 0 ? d.recoveries / d.captures : null;
|
|
122
|
+
return rates;
|
|
123
|
+
}
|
|
124
|
+
const BYTES_PER_MB = 1048576;
|
|
125
|
+
function parseVmStat(output) {
|
|
126
|
+
const pageSize = Number(/page size of (\d+) bytes/.exec(output)?.[1]);
|
|
127
|
+
if (!Number.isFinite(pageSize)) return null;
|
|
128
|
+
const pages = (label)=>Number(new RegExp(`${label}:\\s+(\\d+)`).exec(output)?.[1] ?? 0);
|
|
129
|
+
const mb = (label)=>Math.round(pages(label) * pageSize / BYTES_PER_MB);
|
|
130
|
+
return {
|
|
131
|
+
freeMb: mb("Pages free"),
|
|
132
|
+
activeMb: mb("Pages active"),
|
|
133
|
+
inactiveMb: mb("Pages inactive"),
|
|
134
|
+
wiredMb: mb("Pages wired down"),
|
|
135
|
+
compressorMb: mb("Pages occupied by compressor"),
|
|
136
|
+
pageins: pages("Pageins"),
|
|
137
|
+
pageouts: pages("Pageouts")
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function parseIostat(output) {
|
|
141
|
+
const lines = String(output).trim().split(/\r?\n/);
|
|
142
|
+
const header = lines.findIndex((l)=>/^\s*disk/.test(l));
|
|
143
|
+
if (-1 === header) return [];
|
|
144
|
+
const devices = lines[header].trim().split(/\s+/);
|
|
145
|
+
const samples = lines.slice(header + 2).filter((l)=>/\d/.test(l));
|
|
146
|
+
const last = samples.at(-1);
|
|
147
|
+
if (!last) return [];
|
|
148
|
+
const numbers = last.trim().split(/\s+/).map(Number);
|
|
149
|
+
return devices.map((device, i)=>({
|
|
150
|
+
device,
|
|
151
|
+
kbPerTransfer: numbers[3 * i] ?? 0,
|
|
152
|
+
transfersPerSecond: numbers[3 * i + 1] ?? 0,
|
|
153
|
+
mbPerSecond: numbers[3 * i + 2] ?? 0
|
|
154
|
+
})).filter((d)=>Number.isFinite(d.mbPerSecond));
|
|
155
|
+
}
|
|
156
|
+
function parseLoadAverage(output) {
|
|
157
|
+
const m = /([\d.]+)\s+([\d.]+)\s+([\d.]+)/.exec(String(output));
|
|
158
|
+
return m ? {
|
|
159
|
+
one: Number(m[1]),
|
|
160
|
+
five: Number(m[2]),
|
|
161
|
+
fifteen: Number(m[3])
|
|
162
|
+
} : null;
|
|
163
|
+
}
|
|
164
|
+
function parseProcessMemory(psOutput, match) {
|
|
165
|
+
return String(psOutput).trim().split(/\r?\n/).flatMap((line)=>{
|
|
166
|
+
const fields = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
167
|
+
if (!fields || !fields[3].includes(match)) return [];
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
pid: Number(fields[1]),
|
|
171
|
+
residentMb: Math.round(Number(fields[2]) / 1024),
|
|
172
|
+
command: fields[3]
|
|
173
|
+
}
|
|
174
|
+
];
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const run = (cmd, args)=>{
|
|
178
|
+
try {
|
|
179
|
+
return execFileSync(cmd, args, {
|
|
180
|
+
encoding: "utf8",
|
|
181
|
+
timeout: 15000
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function sampleHost({ processMatch = "QEMU" } = {}) {
|
|
188
|
+
const memory = parseVmStat(run("vm_stat", []));
|
|
189
|
+
return {
|
|
190
|
+
at: new Date().toISOString(),
|
|
191
|
+
memory,
|
|
192
|
+
disk: parseIostat(run("iostat", [
|
|
193
|
+
"-d",
|
|
194
|
+
"-c",
|
|
195
|
+
"2",
|
|
196
|
+
"-w",
|
|
197
|
+
"1"
|
|
198
|
+
])),
|
|
199
|
+
load: parseLoadAverage(run("sysctl", [
|
|
200
|
+
"-n",
|
|
201
|
+
"vm.loadavg"
|
|
202
|
+
])),
|
|
203
|
+
processes: parseProcessMemory(run("ps", [
|
|
204
|
+
"-Ao",
|
|
205
|
+
"pid=,rss=,comm="
|
|
206
|
+
]), processMatch)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const reading = (snapshot, field, fallback)=>snapshot?.memory?.[field] ?? fallback;
|
|
210
|
+
function diffHost(before, after) {
|
|
211
|
+
const pageouts = (reading(after, "pageouts", 0) ?? 0) - (reading(before, "pageouts", 0) ?? 0);
|
|
212
|
+
const pageins = (reading(after, "pageins", 0) ?? 0) - (reading(before, "pageins", 0) ?? 0);
|
|
213
|
+
return {
|
|
214
|
+
pageoutsDelta: pageouts,
|
|
215
|
+
pageinsDelta: pageins,
|
|
216
|
+
swappingDuringRun: pageouts > 0,
|
|
217
|
+
compressorMb: reading(after, "compressorMb", null),
|
|
218
|
+
freeMb: reading(after, "freeMb", null),
|
|
219
|
+
residentMbTotal: (after?.processes ?? []).reduce((sum, p)=>sum + p.residentMb, 0)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
refuseUnknownFlags([
|
|
223
|
+
"--rounds=",
|
|
224
|
+
"--runs="
|
|
225
|
+
], {
|
|
226
|
+
entry: import.meta.url,
|
|
227
|
+
command: "npm run worker:compare"
|
|
228
|
+
});
|
|
48
229
|
const OUT = resolve(fileURLToPath(new URL("../../../", import.meta.url)), "runs/worker-compare");
|
|
49
230
|
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
231
|
function refuseIfRunsReadonly() {
|
|
59
|
-
if (process.env.A11Y_RUNS_READONLY
|
|
60
|
-
return;
|
|
232
|
+
if ("1" !== process.env.A11Y_RUNS_READONLY) return;
|
|
61
233
|
console.error("REFUSING to write runs/worker-compare/compare.json — A11Y_RUNS_READONLY=1 is set.");
|
|
62
234
|
console.error(" Unset A11Y_RUNS_READONLY to run this for real.");
|
|
63
235
|
process.exit(3);
|
|
64
236
|
}
|
|
65
|
-
const
|
|
66
|
-
const runs = Number(
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
async function capture(/** @type {any} */ worker) {
|
|
237
|
+
const compare_workers_args = process.argv.slice(2);
|
|
238
|
+
const runs = Number(compare_workers_args.find((a)=>a.startsWith("--rounds="))?.slice(9) ?? compare_workers_args.find((a)=>a.startsWith("--runs="))?.slice(7) ?? 6);
|
|
239
|
+
const [page, ...compare_workers_workers] = compare_workers_args.filter((a)=>!a.startsWith("--"));
|
|
240
|
+
const compare_workers_short = (worker)=>new URL(worker).hostname.split(".").pop();
|
|
241
|
+
async function capture(worker) {
|
|
71
242
|
const response = await requestJson(`${worker.replace(/\/$/, "")}/capture`, {
|
|
72
243
|
method: "POST",
|
|
73
|
-
body: {
|
|
74
|
-
|
|
244
|
+
body: {
|
|
245
|
+
url: page,
|
|
246
|
+
probeForms: true
|
|
247
|
+
},
|
|
248
|
+
timeoutMs: CAPTURE_CLIENT_TIMEOUT_MS
|
|
75
249
|
});
|
|
76
250
|
const body = response.json ?? {};
|
|
77
|
-
if (!response.ok)
|
|
78
|
-
throw new Error(`HTTP ${response.status}: ${JSON.stringify(body).slice(0, 160)}`);
|
|
251
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${JSON.stringify(body).slice(0, 160)}`);
|
|
79
252
|
return body;
|
|
80
253
|
}
|
|
81
|
-
async function diagnostics(
|
|
254
|
+
async function diagnostics(worker) {
|
|
82
255
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
256
|
+
const response = await requestJson(`${worker.replace(/\/$/, "")}/diagnostics`, {
|
|
257
|
+
timeoutMs: 180000
|
|
258
|
+
});
|
|
88
259
|
return response.ok ? response.json ?? null : null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return null; // an older worker has no /diagnostics; the phase comparison still works
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
92
262
|
}
|
|
93
263
|
}
|
|
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>} */
|
|
264
|
+
function phaseCosts(entries) {
|
|
103
265
|
const costs = {};
|
|
104
266
|
let previous = 0;
|
|
105
|
-
for (const entry of entries ?? []) {
|
|
106
|
-
if (typeof entry.atMs !== "number")
|
|
107
|
-
continue;
|
|
267
|
+
for (const entry of entries ?? [])if ("number" == typeof entry.atMs) {
|
|
108
268
|
costs[entry.event] = (costs[entry.event] ?? 0) + (entry.atMs - previous);
|
|
109
269
|
previous = entry.atMs;
|
|
110
270
|
}
|
|
111
271
|
return costs;
|
|
112
272
|
}
|
|
113
|
-
|
|
114
|
-
function sweepDetail(/** @type {any} */ entries) {
|
|
115
|
-
/** @type {Record<string, any>} */
|
|
273
|
+
function sweepDetail(entries) {
|
|
116
274
|
const byType = {};
|
|
117
|
-
for (const entry of entries ?? [])
|
|
118
|
-
if (entry.event
|
|
119
|
-
|
|
120
|
-
|
|
275
|
+
for (const entry of entries ?? []){
|
|
276
|
+
if ("sweep" !== entry.event || !entry.type) continue;
|
|
277
|
+
const to = byType[entry.type] ??= {
|
|
278
|
+
found: 0,
|
|
279
|
+
ms: 0,
|
|
280
|
+
trips: 0,
|
|
281
|
+
n: 0
|
|
282
|
+
};
|
|
121
283
|
to.found += entry.found ?? 0;
|
|
122
284
|
to.ms += (entry.prevMs ?? 0) + (entry.nextMs ?? 0);
|
|
123
285
|
to.trips += (entry.prevTrips ?? 0) + (entry.nextTrips ?? 0);
|
|
@@ -125,208 +287,174 @@ function sweepDetail(/** @type {any} */ entries) {
|
|
|
125
287
|
}
|
|
126
288
|
return byType;
|
|
127
289
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
290
|
+
async function vitals(worker) {
|
|
291
|
+
return sampleVitals(worker, {
|
|
292
|
+
warn: (line)=>process.stderr.write(line)
|
|
293
|
+
});
|
|
131
294
|
}
|
|
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
295
|
async function main() {
|
|
141
|
-
if (!page ||
|
|
296
|
+
if (!page || compare_workers_workers.length < 2) {
|
|
142
297
|
process.stderr.write("usage: npm run worker:compare -- <page-url> <worker> <worker> [--rounds=6]\n");
|
|
143
298
|
process.exit(2);
|
|
144
299
|
}
|
|
145
300
|
refuseIfRunsReadonly();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
301
|
+
await refuseIfBusy(compare_workers_workers, {
|
|
302
|
+
what: `worker:compare against ${page}`
|
|
303
|
+
});
|
|
304
|
+
const results = Object.fromEntries(compare_workers_workers.map((w)=>[
|
|
305
|
+
w,
|
|
306
|
+
{
|
|
307
|
+
phases: [],
|
|
308
|
+
sweeps: [],
|
|
309
|
+
walls: [],
|
|
310
|
+
diagnostics: null
|
|
311
|
+
}
|
|
312
|
+
]));
|
|
158
313
|
const vitalsBefore = {};
|
|
159
|
-
/** @type {Record<string, any>} */
|
|
160
314
|
const vitalsAfter = {};
|
|
161
|
-
for (const worker of
|
|
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.
|
|
315
|
+
for (const worker of compare_workers_workers)vitalsBefore[worker] = await vitals(worker);
|
|
166
316
|
const hostBefore = sampleHost();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
}
|
|
317
|
+
process.stdout.write(`\nInterleaving ${runs} round(s) across ${compare_workers_workers.length} worker(s)\n`);
|
|
318
|
+
for(let round = 1; round <= runs; round++)for (const worker of compare_workers_workers)try {
|
|
319
|
+
const started = Date.now();
|
|
320
|
+
const result = await capture(worker);
|
|
321
|
+
const wall = Date.now() - started;
|
|
322
|
+
results[worker].walls.push(wall);
|
|
323
|
+
results[worker].phases.push(phaseCosts(result.diagnostics));
|
|
324
|
+
results[worker].sweeps.push(sweepDetail(result.diagnostics));
|
|
325
|
+
process.stdout.write(` round ${round} ${compare_workers_short(worker)}: ${(wall / MS_PER_S).toFixed(1)}s ${result.transcript?.length ?? 0} phrases\n`);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
process.stdout.write(` round ${round} ${compare_workers_short(worker)}: FAILED ${error.message}\n`);
|
|
187
328
|
}
|
|
188
|
-
for (const worker of
|
|
329
|
+
for (const worker of compare_workers_workers){
|
|
189
330
|
vitalsAfter[worker] = await vitals(worker);
|
|
190
331
|
results[worker].diagnostics = await diagnostics(worker);
|
|
191
332
|
}
|
|
192
|
-
report({
|
|
333
|
+
report({
|
|
334
|
+
results,
|
|
335
|
+
vitalsBefore,
|
|
336
|
+
vitalsAfter,
|
|
337
|
+
hostBefore
|
|
338
|
+
});
|
|
193
339
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
340
|
+
function report({ results, vitalsBefore, vitalsAfter, hostBefore }) {
|
|
341
|
+
const allPhases = [
|
|
342
|
+
...new Set(Object.values(results).flatMap((r)=>r.phases.flatMap(Object.keys)))
|
|
343
|
+
];
|
|
344
|
+
const phaseSamples = (worker, phase)=>results[worker].phases.map((p)=>(p[phase] ?? 0) / MS_PER_S);
|
|
345
|
+
const { verdict, deltas } = reportWallTime({
|
|
346
|
+
results,
|
|
347
|
+
vitalsBefore,
|
|
348
|
+
vitalsAfter
|
|
349
|
+
});
|
|
350
|
+
reportPhases({
|
|
351
|
+
results,
|
|
352
|
+
allPhases,
|
|
353
|
+
phaseSamples
|
|
354
|
+
});
|
|
355
|
+
const { foundations, hostAfter } = reportFoundations({
|
|
356
|
+
hostBefore
|
|
357
|
+
});
|
|
358
|
+
mkdirSync(OUT, {
|
|
359
|
+
recursive: true
|
|
360
|
+
});
|
|
361
|
+
writeFileSync(resolve(OUT, "compare.json"), JSON.stringify({
|
|
362
|
+
page: page,
|
|
363
|
+
rounds: runs,
|
|
364
|
+
results,
|
|
365
|
+
verdict,
|
|
366
|
+
recoveryDeltas: deltas,
|
|
367
|
+
foundations,
|
|
368
|
+
hostBefore,
|
|
369
|
+
hostAfter
|
|
370
|
+
}, null, 2) + "\n", "utf8");
|
|
210
371
|
process.stdout.write(`\nReport: ${resolve(OUT, "compare.json")}\n`);
|
|
211
372
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
373
|
+
function reportWallTime({ results, vitalsBefore, vitalsAfter }) {
|
|
374
|
+
const wallSeconds = Object.fromEntries(compare_workers_workers.map((w)=>[
|
|
375
|
+
compare_workers_short(w),
|
|
376
|
+
results[w].walls.map((v)=>v / MS_PER_S)
|
|
377
|
+
]));
|
|
216
378
|
const verdict = compareWorkers(wallSeconds);
|
|
217
379
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
380
|
+
process.stdout.write(` ${"worker".padEnd(10)}${"n".padStart(4)}${"median".padStart(9)}${"IQR".padStart(9)}${"min".padStart(8)}${"max".padStart(8)} recoveries\n`);
|
|
381
|
+
const deltas = Object.fromEntries(compare_workers_workers.map((w)=>[
|
|
382
|
+
compare_workers_short(w),
|
|
383
|
+
{
|
|
384
|
+
recoveries: (vitalsAfter[w]?.recoveries ?? 0) - (vitalsBefore[w]?.recoveries ?? 0),
|
|
385
|
+
captures: (vitalsAfter[w]?.captures ?? 0) - (vitalsBefore[w]?.captures ?? 0)
|
|
386
|
+
}
|
|
387
|
+
]));
|
|
388
|
+
const rates = recoveryRates(deltas);
|
|
389
|
+
for (const w of compare_workers_workers){
|
|
390
|
+
const name = String(compare_workers_short(w)), d = describe(wallSeconds[name]);
|
|
391
|
+
if (!d) continue;
|
|
392
|
+
const rate = null === rates[name] ? "n/a" : `${deltas[name].recoveries}/${deltas[name].captures}`;
|
|
393
|
+
process.stdout.write(` ${name.padEnd(10)}${String(d.n).padStart(4)}${d.median.toFixed(1).padStart(9)}${d.iqr.toFixed(1).padStart(9)}${d.min.toFixed(1).padStart(8)}${d.max.toFixed(1).padStart(8)} ${rate}\n`);
|
|
232
394
|
}
|
|
233
395
|
process.stdout.write(`\n ${verdict.verdict}\n`);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
396
|
+
return {
|
|
397
|
+
verdict,
|
|
398
|
+
deltas
|
|
399
|
+
};
|
|
237
400
|
}
|
|
238
|
-
|
|
239
|
-
function reportPhases(/** @type {any} */ { results, allPhases, phaseSamples }) {
|
|
240
|
-
// Per-phase medians, so a real difference can be located rather than guessed at.
|
|
401
|
+
function reportPhases({ results, allPhases, phaseSamples }) {
|
|
241
402
|
process.stdout.write(`\nPHASE MEDIANS (seconds)\n`);
|
|
242
|
-
process.stdout.write(` ${"phase".padEnd(20)}${
|
|
243
|
-
const rows = allPhases
|
|
244
|
-
.map((
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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)))];
|
|
403
|
+
process.stdout.write(` ${"phase".padEnd(20)}${compare_workers_workers.map((w)=>String(compare_workers_short(w)).padStart(9)).join("")} spread\n`);
|
|
404
|
+
const rows = allPhases.map((phase)=>{
|
|
405
|
+
const medians = compare_workers_workers.map((w)=>describe(phaseSamples(w, phase))?.median ?? 0);
|
|
406
|
+
return {
|
|
407
|
+
phase,
|
|
408
|
+
medians,
|
|
409
|
+
spread: Math.max(...medians) - Math.min(...medians)
|
|
410
|
+
};
|
|
411
|
+
}).filter((row)=>row.medians.some((v)=>v >= 0.05)).sort((a, b)=>b.spread - a.spread);
|
|
412
|
+
for (const { phase, medians, spread } of rows)process.stdout.write(` ${phase.padEnd(20)}${medians.map((v)=>v.toFixed(1).padStart(9)).join("")}${spread.toFixed(1).padStart(10)}${spread >= 1 ? " <-- diverges" : ""}\n`);
|
|
413
|
+
const sweepTypes = [
|
|
414
|
+
...new Set(Object.values(results).flatMap((r)=>r.sweeps.flatMap(Object.keys)))
|
|
415
|
+
];
|
|
257
416
|
if (sweepTypes.length) {
|
|
258
417
|
process.stdout.write("\nSWEEP DETAIL (mean ms / round trips / elements found)\n");
|
|
259
|
-
for (const type of sweepTypes)
|
|
418
|
+
for (const type of sweepTypes){
|
|
260
419
|
process.stdout.write(` ${type.padEnd(14)}`);
|
|
261
|
-
for (const w of
|
|
262
|
-
const runsWith = results[w].sweeps.map((
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
process.stdout.write(`${short(w)}: ${ms}ms/${trips}t/${found}f `);
|
|
420
|
+
for (const w of compare_workers_workers){
|
|
421
|
+
const runsWith = results[w].sweeps.map((s)=>s[type]).filter(Boolean);
|
|
422
|
+
const ms = (describe(runsWith.map((x)=>x.ms))?.median ?? 0).toFixed(0);
|
|
423
|
+
const trips = (describe(runsWith.map((x)=>x.trips))?.median ?? 0).toFixed(1);
|
|
424
|
+
const found = (describe(runsWith.map((x)=>x.found))?.median ?? 0).toFixed(1);
|
|
425
|
+
process.stdout.write(`${compare_workers_short(w)}: ${ms}ms/${trips}t/${found}f `);
|
|
268
426
|
}
|
|
269
427
|
process.stdout.write("\n");
|
|
270
428
|
}
|
|
271
429
|
process.stdout.write("\n ms up but trips flat => each round trip is slower (the guest or NVDA is the cause).\n");
|
|
272
430
|
process.stdout.write(" trips up => the sweep is genuinely walking more (the page or dedupe differs).\n");
|
|
273
431
|
}
|
|
274
|
-
for (const w of
|
|
432
|
+
for (const w of compare_workers_workers){
|
|
275
433
|
const d = results[w].diagnostics;
|
|
276
|
-
if (
|
|
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`);
|
|
434
|
+
if (d) process.stdout.write(`\n ${compare_workers_short(w)} guest: profile=${d.edgeProfile?.megabytes}MB policy=${JSON.stringify(d.edgePolicy)} procs=${JSON.stringify(d.processes)}\n`);
|
|
280
435
|
}
|
|
281
436
|
}
|
|
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 }) {
|
|
437
|
+
function reportFoundations({ hostBefore }) {
|
|
287
438
|
const hostAfter = sampleHost();
|
|
288
|
-
const foundations = diffHost(
|
|
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.
|
|
439
|
+
const foundations = diffHost(hostBefore, hostAfter);
|
|
297
440
|
const localGuests = hostAfter.processes.length;
|
|
298
|
-
process.stdout.write(localGuests
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
process.stdout.write(`
|
|
305
|
-
|
|
306
|
-
process.stdout.write(`
|
|
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`);
|
|
441
|
+
process.stdout.write(localGuests ? "\nFOUNDATIONS (the host, during this run)\n" : "\nFOUNDATIONS (this CONTROL host only — no local guests, so none of this describes the workers)\n");
|
|
442
|
+
process.stdout.write(` load ${hostAfter.load?.one ?? "?"} (1m), ${hostAfter.load?.five ?? "?"} (5m)\n`);
|
|
443
|
+
const busiest = [
|
|
444
|
+
...hostAfter.disk ?? []
|
|
445
|
+
].sort((a, b)=>b.mbPerSecond - a.mbPerSecond)[0];
|
|
446
|
+
process.stdout.write(` disk ${busiest ? `${busiest.device} ${busiest.mbPerSecond} MB/s, ${busiest.transfersPerSecond} tps` : "unavailable"}\n`);
|
|
447
|
+
process.stdout.write(` guest resident ${foundations.residentMbTotal} MB across ${hostAfter.processes.length} process(es) <- RSS, not phys_footprint\n`);
|
|
448
|
+
process.stdout.write(` free / compressed ${foundations.freeMb} MB free, ${foundations.compressorMb} MB compressed\n`);
|
|
449
|
+
process.stdout.write(` paging ${foundations.pageoutsDelta} pageouts during the run${foundations.swappingDuringRun ? localGuests ? " <-- THE HOST WAS SWAPPING; these timings describe a constrained machine" : " (on this control host; the workers are elsewhere, so this did not affect them)" : " (none)"}\n`);
|
|
319
450
|
if (!localGuests) {
|
|
320
451
|
process.stdout.write("\n No local guest processes were found, so these workers are REMOTE machines and nothing\n");
|
|
321
452
|
process.stdout.write(" above describes them. For a bare-metal fleet, each worker's own /diagnostics is the\n");
|
|
322
453
|
process.stdout.write(" equivalent: curl http://<worker>:8765/diagnostics | jq .\n");
|
|
323
454
|
}
|
|
324
|
-
return {
|
|
455
|
+
return {
|
|
456
|
+
foundations,
|
|
457
|
+
hostAfter
|
|
458
|
+
};
|
|
325
459
|
}
|
|
326
|
-
|
|
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
|
|
460
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href) await main();
|