@openmixer/plugin-qualify 0.18.5 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -730
- package/bin/plugin-qualify-sync.mjs +1 -1
- package/bin/plugin-qualify.mjs +7 -0
- package/dist/clap-qualify.d.ts +1 -1
- package/dist/compare.d.ts +311 -0
- package/dist/compare.js +502 -0
- package/dist/compare.js.map +1 -0
- package/dist/distributed-records.d.ts +0 -2
- package/dist/distributed-records.js +0 -2
- package/dist/distributed-records.js.map +1 -1
- package/dist/host-admission.d.ts +2 -2
- package/dist/host-admission.js +7 -0
- package/dist/host-admission.js.map +1 -1
- package/dist/hosting-codes.generated.d.ts +1 -0
- package/dist/hosting-codes.generated.js +1 -0
- package/dist/hosting-codes.generated.js.map +1 -1
- package/dist/hosting-suitability.d.ts +3 -1
- package/dist/hosting-suitability.js +9 -0
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lv2-loader.js +6 -5
- package/dist/lv2-loader.js.map +1 -1
- package/dist/measurement/format.d.ts +15 -0
- package/dist/measurement/format.js +19 -0
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/sweep-jsonl.js +5 -1
- package/dist/measurement/sweep-jsonl.js.map +1 -1
- package/dist/measurement-grid.js +1 -1
- package/dist/pins-file.d.ts +22 -2
- package/dist/pins-file.js +118 -17
- package/dist/pins-file.js.map +1 -1
- package/dist/qualification-record.d.ts +26 -0
- package/dist/qualification-record.js +21 -0
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-declarations.d.ts +6 -0
- package/dist/qualify-declarations.js +8 -0
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-isolated.d.ts +16 -3
- package/dist/qualify-isolated.js +31 -3
- package/dist/qualify-isolated.js.map +1 -1
- package/dist/qualify-pool.d.ts +129 -0
- package/dist/qualify-pool.js +290 -0
- package/dist/qualify-pool.js.map +1 -0
- package/dist/qualify-rejudge.js +7 -0
- package/dist/qualify-rejudge.js.map +1 -1
- package/dist/qualify-sync.d.ts +36 -1
- package/dist/qualify-sync.js +144 -20
- package/dist/qualify-sync.js.map +1 -1
- package/dist/records-package.d.ts +5 -0
- package/dist/records-package.js +25 -8
- package/dist/records-package.js.map +1 -1
- package/fixtures/clap-scan/fake.json +8 -2
- package/fixtures/probe-output/success-msi.json +2 -2
- package/package.json +3 -3
- package/src/clap-qualify.ts +1 -1
- package/src/compare.ts +699 -0
- package/src/distributed-records.ts +0 -3
- package/src/host-admission.ts +9 -2
- package/src/hosting-codes.generated.ts +1 -0
- package/src/hosting-suitability.ts +14 -0
- package/src/index.ts +1 -0
- package/src/lv2-loader.ts +5 -4
- package/src/measurement/format.ts +32 -0
- package/src/measurement/sweep-jsonl.ts +5 -1
- package/src/measurement-grid.ts +1 -1
- package/src/pins-file.ts +109 -16
- package/src/qualification-record.ts +38 -0
- package/src/qualify-declarations.ts +8 -0
- package/src/qualify-isolated.ts +31 -5
- package/src/qualify-pool.ts +318 -0
- package/src/qualify-rejudge.ts +8 -0
- package/src/qualify-sync.ts +140 -19
- package/src/records-package.ts +25 -8
- package/tools/benchmark.py +9 -3
- package/tools/compare-host.mjs +432 -0
- package/tools/compare-run.mjs +355 -0
- package/tools/descriptors.py +27 -0
- package/tools/isolated-step.mjs +3 -1
- package/tools/lilv_load.py +25 -0
- package/tools/lv2_resolve.py +3 -1
- package/tools/pool-step.mjs +186 -0
- package/tools/scan.py +16 -20
- package/tools/audit-overlap.sh +0 -250
- package/tools/commit-message-check.sh +0 -128
- package/tools/commit-message-patterns.json +0 -41
- package/tools/private-refs-check.sh +0 -90
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The pool step: the one place that grants a plugin the CLAP thread pool. omx-clap-host sizes a pool of helper threads
|
|
5
|
+
* from its worker's cpu set, and plugin-hostd starts a worker with it only for a plugin whose pin says `pool=cores`
|
|
6
|
+
* (plugin-db's `PLUGIN_DB_PIN_POOLS`: "measured real-time safe and faster at the quantum"). That word is the plugin's
|
|
7
|
+
* qualification to give, so nothing writes it but this step.
|
|
8
|
+
*
|
|
9
|
+
* A plugin that does not offer `clap.thread-pool` is never asked: its record says `not-asked` and its pin has no pool
|
|
10
|
+
* word, so it runs serial. A plugin that does is first run by the prober with real helper threads under the interposer
|
|
11
|
+
* (`clap-qualify --pool`, which counts what the plugin's tasks do on a thread that is not the audio thread); one that
|
|
12
|
+
* allocates, locks or makes a system call there is `off`. Then it is run twice in a worker of its own on a private JACK
|
|
13
|
+
* server at the host's rate and quantum, with the same notes held, once under `pool=off` and once under `pool=cores`
|
|
14
|
+
* (`tools/pool-step.mjs`), each mode twice in turn and judged by its best window, because what else runs on the machine can
|
|
15
|
+
* only make a window worse. The pool is granted when, with it on,
|
|
16
|
+
*
|
|
17
|
+
* - its tasks, run on the helpers under the interposer, allocated nothing, took no lock and made no system call,
|
|
18
|
+
* - the worker really started with the pool (a daemon that does not apply the word decides nothing),
|
|
19
|
+
* - the output is finite and not silent, and the worker lived through the window,
|
|
20
|
+
* - the window took no more xruns than the same window without the pool, and
|
|
21
|
+
* - the server's DSP load, which is the share of the cycle the plugin's block takes, fell to at most
|
|
22
|
+
* {@link POOL_GAIN} of the load without the pool and by at least {@link POOL_MIN_GAIN} points of the cycle (a plugin
|
|
23
|
+
* with next to nothing to do has too little to run in parallel for a ratio to mean anything).
|
|
24
|
+
*
|
|
25
|
+
* A plugin that is measured and fails any of these gets `pool=off`: the word says it was looked at. A machine that could
|
|
26
|
+
* not run the step decides nothing, and the sync tries again.
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from "node:child_process";
|
|
29
|
+
import { existsSync } from "node:fs";
|
|
30
|
+
import { availableParallelism } from "node:os";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
|
|
33
|
+
import type { GridCell } from "./measurement-grid.js";
|
|
34
|
+
import type { PinEntry, PinPool } from "./pins-file.js";
|
|
35
|
+
import type { PoolFigures, QualificationRecord, RunBlock, RunStep } from "./qualification-record.js";
|
|
36
|
+
import { ADMIT_STEP_MS_ENV, CLAP_QUALIFY_TOOL_NAME, ISOLATED_SIGNAL_TOOL_ENV, ISOLATED_SIGNAL_TOOL_NAME, RT_INTERPOSE_NAME } from "./qualify-declarations.js";
|
|
37
|
+
import { ISOLATED_RESULT, RUN_STEP, baseSteps, pinFromRecord, runIdOf } from "./qualify-isolated.js";
|
|
38
|
+
import { startPrivateJack, type JackStarter } from "./qualify-adapter-pass.js";
|
|
39
|
+
import { locateNativeTool, toolsDir } from "./tool-paths.js";
|
|
40
|
+
import { writeFileWhole } from "./whole-file.js";
|
|
41
|
+
|
|
42
|
+
/** The name of the step in a record's run. It is not one of `RUN_STEP`: a record is complete without it (a plugin that does not ask for the pool, or a machine that cannot run the step, has none). */
|
|
43
|
+
export const POOL_STEP = "pool";
|
|
44
|
+
|
|
45
|
+
/** The server's DSP load with the pool may be at most this fraction of the load without it. */
|
|
46
|
+
export const POOL_GAIN = 0.85;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The load must also fall by at least this many points of the cycle. The server's load has a floor of its own (the signal
|
|
50
|
+
* client, the ports, the worker's routing), so a plugin with next to nothing to do can fall by a good ratio of a small
|
|
51
|
+
* difference, and a pool for it would cost the cores it holds.
|
|
52
|
+
*/
|
|
53
|
+
export const POOL_MIN_GAIN = 3;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The pool's helpers must have been on a cpu this many nanoseconds in the busiest window with the pool (20 ms of a 3 s
|
|
57
|
+
* window, across all of them): a worker that holds the pool and whose plugin asks for tasks every block keeps them
|
|
58
|
+
* busy for a good part of it, and a few microseconds is a start-up, not a pool at work.
|
|
59
|
+
*/
|
|
60
|
+
export const POOL_HELPERS_IDLE_NS = 20_000_000;
|
|
61
|
+
|
|
62
|
+
export const POOL_RESULT = { cores: "cores", off: "off", notAsked: "not-asked", notRun: "not-run" } as const;
|
|
63
|
+
|
|
64
|
+
/** Why the step decided what it did, or why it could not run. */
|
|
65
|
+
export const POOL_CODE = {
|
|
66
|
+
noHost: "pool.no-host",
|
|
67
|
+
noServer: "pool.no-private-server",
|
|
68
|
+
notAdded: "pool.not-added",
|
|
69
|
+
windowFailed: "pool.window-failed",
|
|
70
|
+
unwatched: "pool.not-watched",
|
|
71
|
+
notRtSafe: "pool.not-rt-safe",
|
|
72
|
+
notApplied: "pool.not-applied",
|
|
73
|
+
crashed: "pool.crashed-with-pool",
|
|
74
|
+
unsafeOutput: "pool.unsafe-output",
|
|
75
|
+
xruns: "pool.more-xruns",
|
|
76
|
+
notFaster: "pool.not-faster",
|
|
77
|
+
helpersIdle: "pool.helpers-idle",
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
/** What one window saw, with the pool as the word set it. */
|
|
81
|
+
export interface PoolWindowFacts {
|
|
82
|
+
readonly problem?: string;
|
|
83
|
+
readonly alive?: boolean;
|
|
84
|
+
readonly workers?: number;
|
|
85
|
+
/** hostd's `worker_list` field for the worker: `on` for a worker started with the pool. */
|
|
86
|
+
readonly poolOfWorker?: string;
|
|
87
|
+
readonly xruns?: number;
|
|
88
|
+
readonly finite?: boolean;
|
|
89
|
+
readonly silent?: boolean;
|
|
90
|
+
readonly dspLoad?: number;
|
|
91
|
+
/** Nanoseconds the worker's pool helpers were on a cpu in the window (the busiest of the windows taken): little means the worker held the pool and its helpers ran next to nothing. */
|
|
92
|
+
readonly helperBusyNs?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** What the plugin did on the helper threads of a real pool under the interposer. */
|
|
96
|
+
export interface PoolRtFacts {
|
|
97
|
+
readonly helpers: number;
|
|
98
|
+
/** The tasks the helpers ran; none means the pool was not exercised, and nothing was seen of it. */
|
|
99
|
+
readonly tasksOnHelpers: number;
|
|
100
|
+
readonly allocations: number;
|
|
101
|
+
readonly locks: number;
|
|
102
|
+
readonly syscalls: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** What one run of the step saw. A run that never reached the plugin says `started: false`, and `problem`. */
|
|
106
|
+
export interface PoolRunFacts {
|
|
107
|
+
readonly started: boolean;
|
|
108
|
+
readonly problem?: string;
|
|
109
|
+
/** Whether the plugin offers clap.thread-pool. */
|
|
110
|
+
readonly asks?: boolean;
|
|
111
|
+
/** The plugin's tasks on helper threads, counted by the interposer (the prober's `--pool`); absent when it could not be watched. */
|
|
112
|
+
readonly rt?: PoolRtFacts;
|
|
113
|
+
readonly added?: boolean;
|
|
114
|
+
readonly off?: PoolWindowFacts;
|
|
115
|
+
readonly on?: PoolWindowFacts;
|
|
116
|
+
/** The worker under `pool=cores` was started with the pool and the one under `pool=off` was not. */
|
|
117
|
+
readonly poolApplied?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type PoolRunner = (uri: string, cell: GridCell, env: Readonly<Record<string, string | undefined>>) => PoolRunFacts;
|
|
121
|
+
|
|
122
|
+
function figuresOf(run: PoolRunFacts): PoolFigures | undefined {
|
|
123
|
+
const { off, on, rt } = run;
|
|
124
|
+
const watched = rt === undefined ? {} : { allocationsOn: rt.allocations, locksOn: rt.locks, syscallsOn: rt.syscalls, tasksOnHelpers: rt.tasksOnHelpers };
|
|
125
|
+
const timed = off?.dspLoad === undefined || on?.dspLoad === undefined ? {} : { dspLoadOff: off.dspLoad, dspLoadOn: on.dspLoad, xrunsOff: off.xruns ?? 0, xrunsOn: on.xruns ?? 0 };
|
|
126
|
+
return Object.keys(watched).length === 0 && Object.keys(timed).length === 0 ? undefined : { ...watched, ...timed };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The decision table of the step, with the figures it took. Pure. */
|
|
130
|
+
export function judgePoolRun(run: PoolRunFacts): RunStep {
|
|
131
|
+
const step = (result: string, code?: string, detail?: string): RunStep => {
|
|
132
|
+
const pool = figuresOf(run);
|
|
133
|
+
return { step: POOL_STEP, result, ...(code === undefined ? {} : { code }), ...(detail === undefined ? {} : { detail }), ...(pool === undefined ? {} : { pool }) };
|
|
134
|
+
};
|
|
135
|
+
if (!run.started) return step(POOL_RESULT.notRun, POOL_CODE.noHost, run.problem);
|
|
136
|
+
if (run.asks === false) return step(POOL_RESULT.notAsked);
|
|
137
|
+
// the plugin on helper threads comes first: a plugin that allocates or locks there is refused the pool whatever it gains,
|
|
138
|
+
// and one nobody could watch there decides nothing
|
|
139
|
+
const { rt } = run;
|
|
140
|
+
if (rt === undefined || rt.tasksOnHelpers < 1) return step(POOL_RESULT.notRun, POOL_CODE.unwatched, run.problem ?? "the plugin's tasks were not seen on helper threads under the interposer");
|
|
141
|
+
if (rt.allocations + rt.locks + rt.syscalls > 0) {
|
|
142
|
+
return step(POOL_RESULT.off, POOL_CODE.notRtSafe, `${String(rt.allocations)} allocations, ${String(rt.locks)} locks and ${String(rt.syscalls)} system calls while its tasks ran on ${String(rt.helpers)} helper threads`);
|
|
143
|
+
}
|
|
144
|
+
if (run.added !== true) return step(POOL_RESULT.notRun, POOL_CODE.notAdded, run.problem);
|
|
145
|
+
const { off, on } = run;
|
|
146
|
+
// the window without the pool is the reference: a plugin that does not even run alone says nothing about the pool
|
|
147
|
+
if (off === undefined || off.problem !== undefined || off.alive !== true || off.finite !== true) return step(POOL_RESULT.notRun, POOL_CODE.windowFailed, off?.problem ?? "the window without the pool did not run");
|
|
148
|
+
if (on === undefined) return step(POOL_RESULT.notRun, POOL_CODE.windowFailed, "the window with the pool did not run");
|
|
149
|
+
if (run.poolApplied !== true) return step(POOL_RESULT.notRun, POOL_CODE.notApplied, "the daemon did not start the worker with the thread pool");
|
|
150
|
+
if (on.alive !== true) return step(POOL_RESULT.off, POOL_CODE.crashed, on.problem);
|
|
151
|
+
if (on.problem !== undefined) return step(POOL_RESULT.notRun, POOL_CODE.windowFailed, on.problem);
|
|
152
|
+
if (on.finite !== true || on.silent !== false) return step(POOL_RESULT.off, POOL_CODE.unsafeOutput);
|
|
153
|
+
// the worker held the pool and its helpers ran nothing: the plugin's requests never reached them, so the window with the
|
|
154
|
+
// pool measured the window without it again, and no gain was seen because there was nothing to see
|
|
155
|
+
if (on.helperBusyNs !== undefined && on.helperBusyNs < POOL_HELPERS_IDLE_NS) return step(POOL_RESULT.off, POOL_CODE.helpersIdle, "the worker's pool helpers ran no task in the window with the pool");
|
|
156
|
+
if ((on.xruns ?? 0) > (off.xruns ?? 0)) return step(POOL_RESULT.off, POOL_CODE.xruns);
|
|
157
|
+
if (off.dspLoad === undefined || on.dspLoad === undefined) return step(POOL_RESULT.notRun, POOL_CODE.windowFailed, "the server's load was not read");
|
|
158
|
+
if (on.dspLoad > off.dspLoad * POOL_GAIN || off.dspLoad - on.dspLoad < POOL_MIN_GAIN) return step(POOL_RESULT.off, POOL_CODE.notFaster);
|
|
159
|
+
return step(POOL_RESULT.cores);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The pool word a step grants, or `undefined` for a step that decided nothing or granted none. */
|
|
163
|
+
export function poolWordOf(step: RunStep | undefined): PinPool | undefined {
|
|
164
|
+
if (step?.step !== POOL_STEP) return undefined;
|
|
165
|
+
return step.result === POOL_RESULT.cores ? "cores" : step.result === POOL_RESULT.off ? "off" : undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The pool step of a run, if it has one. */
|
|
169
|
+
export function poolStepOf(run: QualificationRecord["run"]): RunStep | undefined {
|
|
170
|
+
return run?.steps.find((s) => s.step === POOL_STEP);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The pin once the step has spoken: its pool word follows the step, and a step that decided nothing leaves none. A refused pin carries none. Pure. */
|
|
174
|
+
export function pinAfterPool(pin: PinEntry | undefined, step: RunStep | undefined): PinEntry | undefined {
|
|
175
|
+
if (pin === undefined) return undefined;
|
|
176
|
+
const { pool: _had, ...rest } = pin;
|
|
177
|
+
const word = poolWordOf(step);
|
|
178
|
+
return word === undefined || pin.verdict === "refused" ? rest : { ...rest, pool: word };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Whether a step ended with a decision: `not-run` is the machine's, and the sync asks again. */
|
|
182
|
+
export const poolDecided = (s: RunStep): boolean => s.step === POOL_STEP && s.result !== POOL_RESULT.notRun;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Whether a record owes the pool step: a CLAP plugin judged in a pass with steps, not refused, with no decided pool step.
|
|
186
|
+
* A record from before the steps owes the isolated step first, which gives it a run; the pool is for a plugin that runs,
|
|
187
|
+
* and a refused one never is.
|
|
188
|
+
*/
|
|
189
|
+
export function owesPool(record: QualificationRecord): boolean {
|
|
190
|
+
if (record.plugin.format !== "clap" || record.run === undefined) return false;
|
|
191
|
+
const steps = record.run.steps;
|
|
192
|
+
if (steps.some(poolDecided)) return false;
|
|
193
|
+
if (steps.some((s) => s.step === RUN_STEP.isolated && s.result === ISOLATED_RESULT.refused)) return false;
|
|
194
|
+
return record.verdicts.some((v) => v.measured && v.rating !== "unsuitable");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** How long each window plays, and what the step may take beyond the two: the host's start and its answers. */
|
|
198
|
+
const POOL_WINDOW_MS = 3000;
|
|
199
|
+
const POOL_ROUNDS = 2;
|
|
200
|
+
const POOL_STEP_SLACK_MS = 120000;
|
|
201
|
+
|
|
202
|
+
/** Whether the plugin of a `clap:<path>#<id>` uri offers clap.thread-pool, by the prober's own `--asks-thread-pool`; `undefined` when it could not be asked. */
|
|
203
|
+
export function asksForPool(prober: string, uri: string, env: Readonly<Record<string, string | undefined>>): boolean | undefined {
|
|
204
|
+
const m = /^clap:(.*)#([^#]*)$/s.exec(uri);
|
|
205
|
+
if (m === null) return undefined;
|
|
206
|
+
const r = spawnSync(prober, ["--asks-thread-pool", m[1]!, m[2]!], { encoding: "utf8", timeout: 60000, killSignal: "SIGKILL", env: { ...env } });
|
|
207
|
+
if (r.status !== 0) return undefined;
|
|
208
|
+
try {
|
|
209
|
+
return (JSON.parse(r.stdout) as { threadPool?: unknown }).threadPool === true;
|
|
210
|
+
} catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The most helper threads the prober's pool makes (`POOL_HELPERS_MAX` in native/clap-qualify.c). */
|
|
216
|
+
const POOL_HELPERS_MAX = 7;
|
|
217
|
+
|
|
218
|
+
/** How many blocks the prober's sweeps run: the sync's own, so a plugin that needs a few blocks to settle is seen settled. */
|
|
219
|
+
const WATCH_BLOCKS = 512;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* What the plugin does on helper threads: the prober run with a real pool of one helper per core of this process's set but
|
|
223
|
+
* one (at most seven), under the interposer, which counts what any thread does while a sweep runs. The pool must have
|
|
224
|
+
* been used: a run whose tasks all ran on the calling thread saw nothing of the helpers.
|
|
225
|
+
*/
|
|
226
|
+
export function watchOnHelpers(prober: string, interposer: string, uri: string, cell: GridCell, env: Readonly<Record<string, string | undefined>>): { readonly rt: PoolRtFacts } | { readonly problem: string } {
|
|
227
|
+
const m = /^clap:(.*)#([^#]*)$/s.exec(uri);
|
|
228
|
+
if (m === null) return { problem: `${uri} is no clap uri` };
|
|
229
|
+
const helpers = Math.min(POOL_HELPERS_MAX, availableParallelism() - 1);
|
|
230
|
+
if (helpers < 1) return { problem: "this machine has one core, and a pool of one core is no pool" };
|
|
231
|
+
const preload = [interposer, env["LD_PRELOAD"]].filter((x) => x !== undefined && x !== "").join(" ");
|
|
232
|
+
const r = spawnSync(prober, ["--rates", String(cell.rate), "--block", String(cell.quantum), "--blocks", String(WATCH_BLOCKS), "--pool", String(helpers), m[1]!, m[2]!], {
|
|
233
|
+
encoding: "utf8",
|
|
234
|
+
timeout: 120000,
|
|
235
|
+
killSignal: "SIGKILL",
|
|
236
|
+
env: { ...env, LD_PRELOAD: preload },
|
|
237
|
+
});
|
|
238
|
+
if (r.error !== undefined || r.status !== 0) return { problem: `${CLAP_QUALIFY_TOOL_NAME} --pool: ${r.error?.message ?? `exit ${String(r.status)}`}` };
|
|
239
|
+
const line = r.stdout.split("\n").filter((l) => l.startsWith("{")).pop();
|
|
240
|
+
if (line === undefined) return { problem: `${CLAP_QUALIFY_TOOL_NAME} --pool printed no line` };
|
|
241
|
+
try {
|
|
242
|
+
const doc = JSON.parse(line) as { rtSafety?: { interposer?: boolean; allocationsInRun?: number; locksInRun?: number; syscallsInRun?: number }; noteSweep?: { interposer?: boolean; allocationsInRun?: number; locksInRun?: number; syscallsInRun?: number }; threadPool?: { tasksOnHelpers?: number } };
|
|
243
|
+
const sweeps = [doc.rtSafety, doc.noteSweep].filter((x) => x?.interposer === true);
|
|
244
|
+
if (doc.rtSafety?.interposer !== true) return { problem: "the interposer did not load in the prober" };
|
|
245
|
+
const sum = (key: "allocationsInRun" | "locksInRun" | "syscallsInRun"): number => sweeps.reduce((n, x) => n + (x?.[key] ?? 0), 0);
|
|
246
|
+
return { rt: { helpers, tasksOnHelpers: doc.threadPool?.tasksOnHelpers ?? 0, allocations: sum("allocationsInRun"), locks: sum("locksInRun"), syscalls: sum("syscallsInRun") } };
|
|
247
|
+
} catch {
|
|
248
|
+
return { problem: `${CLAP_QUALIFY_TOOL_NAME} --pool printed no JSON` };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The step as it runs on a device: a private server at the cell, and `tools/pool-step.mjs` under it. */
|
|
253
|
+
export function spawnedPoolRunner(jack: JackStarter = startPrivateJack): PoolRunner {
|
|
254
|
+
return (uri, cell, env) => {
|
|
255
|
+
const prober = locateNativeTool(CLAP_QUALIFY_TOOL_NAME, env, existsSync).path;
|
|
256
|
+
if (prober === undefined) return { started: false, problem: `no ${CLAP_QUALIFY_TOOL_NAME}` };
|
|
257
|
+
// a plugin that does not offer the pool is not worth a JACK server and a daemon: ask the plugin first
|
|
258
|
+
const asks = asksForPool(prober, uri, env);
|
|
259
|
+
if (asks === undefined) return { started: false, problem: `${uri} could not be asked for clap.thread-pool` };
|
|
260
|
+
if (!asks) return { started: true, asks: false };
|
|
261
|
+
// then what it does on helper threads, which needs no server: a plugin that allocates there is refused without one
|
|
262
|
+
const interposer = locateNativeTool(RT_INTERPOSE_NAME, env, existsSync).path;
|
|
263
|
+
if (interposer === undefined) return { started: true, asks: true, problem: `no ${RT_INTERPOSE_NAME}` };
|
|
264
|
+
const watched = watchOnHelpers(prober, interposer, uri, cell, env);
|
|
265
|
+
if (!("rt" in watched)) return { started: true, asks: true, problem: watched.problem };
|
|
266
|
+
if (watched.rt.tasksOnHelpers < 1 || watched.rt.allocations + watched.rt.locks + watched.rt.syscalls > 0) return { started: true, asks: true, rt: watched.rt };
|
|
267
|
+
const signal = locateNativeTool(ISOLATED_SIGNAL_TOOL_NAME, env, existsSync).path;
|
|
268
|
+
if (signal === undefined) return { started: false, problem: `no ${ISOLATED_SIGNAL_TOOL_NAME}` };
|
|
269
|
+
const server = jack(cell);
|
|
270
|
+
if (server === undefined) return { started: false, problem: `${POOL_CODE.noServer} at ${String(cell.rate)}/${String(cell.quantum)}` };
|
|
271
|
+
try {
|
|
272
|
+
const r = spawnSync(process.execPath, [join(toolsDir(), "pool-step.mjs"), uri, String(POOL_WINDOW_MS)], {
|
|
273
|
+
encoding: "utf8",
|
|
274
|
+
timeout: 2 * POOL_ROUNDS * POOL_WINDOW_MS + POOL_STEP_SLACK_MS,
|
|
275
|
+
killSignal: "SIGKILL",
|
|
276
|
+
env: { ...env, JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
|
|
277
|
+
});
|
|
278
|
+
if (r.error !== undefined || r.status !== 0) return { started: false, problem: `pool-step: ${r.error?.message ?? `exit ${String(r.status)}`}: ${(r.stderr ?? "").trim()}` };
|
|
279
|
+
return { ...(JSON.parse(r.stdout) as PoolRunFacts), asks: true, rt: watched.rt };
|
|
280
|
+
} finally {
|
|
281
|
+
server.stop();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The step's environment: each of its commands has the sync's budget. */
|
|
287
|
+
export const poolEnv = (env: Readonly<Record<string, string | undefined>>, budgetS: number): Readonly<Record<string, string | undefined>> => ({ ...env, [ADMIT_STEP_MS_ENV]: String(budgetS * 1000) });
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The steps of a run with the pool step added or replaced: a step that could not run takes the place of an earlier one
|
|
291
|
+
* that could not, and a decided step is never replaced here.
|
|
292
|
+
*/
|
|
293
|
+
export function withPoolStep(steps: readonly RunStep[], step: RunStep): RunStep[] {
|
|
294
|
+
return [...steps.filter((s) => s.step !== POOL_STEP), step];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The pin a record can give back to a plugin whose state lost it: what {@link pinFromRecord} gives, with the pool word
|
|
299
|
+
* of the record's pool step.
|
|
300
|
+
*/
|
|
301
|
+
export function pinFromRecordWithPool(record: QualificationRecord): PinEntry | undefined {
|
|
302
|
+
return pinAfterPool(pinFromRecord(record), poolStepOf(record.run));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The missing pool step of a record already judged: the run keeps its id and gains the step, and nothing else is
|
|
307
|
+
* measured. `undefined` when it owes none, or has no run id to extend. Writes the record whole.
|
|
308
|
+
*/
|
|
309
|
+
export function completePool(path: string, record: QualificationRecord, uri: string, cell: GridCell, runner: PoolRunner, env: Readonly<Record<string, string | undefined>>): { readonly record: QualificationRecord; readonly step: RunStep } | undefined {
|
|
310
|
+
if (!owesPool(record)) return undefined;
|
|
311
|
+
const id = runIdOf(record, join(path, ".."));
|
|
312
|
+
if (id === undefined) return undefined;
|
|
313
|
+
const step = judgePoolRun(runner(uri, cell, env));
|
|
314
|
+
const run: RunBlock = { id, steps: withPoolStep(record.run?.steps ?? baseSteps(record), step) };
|
|
315
|
+
const next = { ...record, run };
|
|
316
|
+
writeFileWhole(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
317
|
+
return { record: next, step };
|
|
318
|
+
}
|
package/src/qualify-rejudge.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
type RunBlock,
|
|
50
50
|
} from "./qualification-record.js";
|
|
51
51
|
import { ISOLATED_RESULT, RUN_STEP, pinAfterStep, provenAlone } from "./qualify-isolated.js";
|
|
52
|
+
import { pinAfterPool, poolStepOf } from "./qualify-pool.js";
|
|
52
53
|
import { PI_CELLS, PROBE_MEMORY_ONLY_ENV, PROBE_NOTES_ENV } from "./qualify-declarations.js";
|
|
53
54
|
import { SYNC_STATE_SCHEMA, digestFiles, recordStem, type SyncContext, type SyncEntry, type SyncPlugin, type SyncState } from "./qualify-sync.js";
|
|
54
55
|
import type { SuitabilityRating } from "./rating.js";
|
|
@@ -270,6 +271,13 @@ const nameOf = (record: QualificationRecord): string => record.plugin.id;
|
|
|
270
271
|
|
|
271
272
|
/** A state plugin with the verdict its record now holds for the host: the pin's files stay as they are, only its word follows. */
|
|
272
273
|
function pluginWithVerdict(plugin: SyncPlugin, v: RecordVerdict, run?: RunBlock): SyncPlugin {
|
|
274
|
+
// the pool word is a figure of the record's pool step, not a rating: a re-judge keeps it
|
|
275
|
+
const judged = pluginWithVerdictOnly(plugin, v, run);
|
|
276
|
+
const step = poolStepOf(run);
|
|
277
|
+
return judged.pin === undefined || step === undefined ? judged : { ...judged, pin: pinAfterPool(judged.pin, step)! };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function pluginWithVerdictOnly(plugin: SyncPlugin, v: RecordVerdict, run?: RunBlock): SyncPlugin {
|
|
273
281
|
const { pin, code: _code, ...rest } = plugin;
|
|
274
282
|
const word = v.measured ? pinVerdictOf(v.rating as SuitabilityRating) : undefined;
|
|
275
283
|
// a rating does not make a plugin isolated: a run seen alone does, and a re-judge measures nothing. A record from before
|