@openmixer/plugin-qualify 0.16.0 → 0.17.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/README.md +154 -7
- package/bin/plugin-qualify-records-package.mjs +31 -0
- package/bin/plugin-qualify-sync.mjs +6 -1
- package/bin/plugin-qualify.mjs +12 -1
- package/dist/distributed-records.d.ts +88 -0
- package/dist/distributed-records.js +210 -0
- package/dist/distributed-records.js.map +1 -0
- package/dist/host-profiles.d.ts +5 -0
- package/dist/host-profiles.js +13 -0
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-suitability.js +9 -1
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/measurement-grid.d.ts +49 -0
- package/dist/measurement-grid.js +76 -0
- package/dist/measurement-grid.js.map +1 -0
- package/dist/memory-bound.d.ts +23 -0
- package/dist/memory-bound.js +46 -0
- package/dist/memory-bound.js.map +1 -0
- package/dist/probe-judge.js +22 -1
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-merge.d.ts +30 -0
- package/dist/probe-merge.js +80 -0
- package/dist/probe-merge.js.map +1 -0
- package/dist/probe-output.d.ts +8 -0
- package/dist/probe-output.js +7 -0
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.d.ts +26 -2
- package/dist/probe-runner.js +38 -10
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +19 -1
- package/dist/qualification-record.js +30 -1
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-cli.d.ts +9 -0
- package/dist/qualify-cli.js +30 -1
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-declarations.d.ts +15 -0
- package/dist/qualify-declarations.js +15 -0
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-options.js +11 -1
- package/dist/qualify-options.js.map +1 -1
- package/dist/qualify-sync.d.ts +23 -3
- package/dist/qualify-sync.js +172 -16
- package/dist/qualify-sync.js.map +1 -1
- package/dist/records-package.d.ts +63 -0
- package/dist/records-package.js +300 -0
- package/dist/records-package.js.map +1 -0
- package/package.json +3 -2
- package/src/distributed-records.ts +251 -0
- package/src/host-profiles.ts +13 -0
- package/src/hosting-suitability.ts +11 -1
- package/src/index.ts +5 -0
- package/src/measurement-grid.ts +97 -0
- package/src/memory-bound.ts +55 -0
- package/src/probe-judge.ts +25 -1
- package/src/probe-merge.ts +89 -0
- package/src/probe-output.ts +13 -0
- package/src/probe-runner.ts +75 -13
- package/src/qualification-record.ts +44 -2
- package/src/qualify-cli.ts +32 -1
- package/src/qualify-declarations.ts +17 -0
- package/src/qualify-options.ts +11 -1
- package/src/qualify-sync.ts +175 -16
- package/src/records-package.ts +333 -0
- package/tools/{ai-trace-commit-check.sh → commit-message-check.sh} +30 -28
- package/tools/{ai-trace-patterns.json → commit-message-patterns.json} +1 -1
package/src/probe-runner.ts
CHANGED
|
@@ -25,6 +25,9 @@ import type { PluginTarget } from "./loader.js";
|
|
|
25
25
|
import { SPAWNED_ADMISSIONS, admitPlugin, admittedHostOf, type AdmissionRuns, type HostAdmission } from "./host-admission.js";
|
|
26
26
|
import { judgeProbeOutput, judgmentVerdicts, type Judgment } from "./probe-judge.js";
|
|
27
27
|
import { readProbeOutput, type ProbeOutput } from "./probe-output.js";
|
|
28
|
+
import { budgetCells, gridCells, runnerInvocations, type GridCell, type GridSpec, type RunnerInvocation } from "./measurement-grid.js";
|
|
29
|
+
import type { MemoryBound } from "./memory-bound.js";
|
|
30
|
+
import { mergeProbeRuns, ProbeMergeError } from "./probe-merge.js";
|
|
28
31
|
import type { QualifyVerdict } from "./qualify-cli.js";
|
|
29
32
|
import {
|
|
30
33
|
CLAP_QUALIFY_TIMEOUT_S,
|
|
@@ -33,8 +36,10 @@ import {
|
|
|
33
36
|
LV2_HOSTING_BLOCK_FRAMES,
|
|
34
37
|
LV2_HOSTING_TIMEOUT_S,
|
|
35
38
|
LV2_MEASURE_TOOL_NAME,
|
|
39
|
+
MEMORY_BOUND_EXIT_ENV,
|
|
36
40
|
PROBE_RUNNER_DEFAULT,
|
|
37
41
|
PROBE_RUNNER_ENV,
|
|
42
|
+
QUALIFY_QUANTA,
|
|
38
43
|
} from "./qualify-declarations.js";
|
|
39
44
|
|
|
40
45
|
export class ProbeRunnerError extends Error {
|
|
@@ -67,8 +72,14 @@ const spawnRunner: ProbeRun = (tool, args, env) => {
|
|
|
67
72
|
};
|
|
68
73
|
|
|
69
74
|
export interface ProbeTargetOptions {
|
|
70
|
-
/** The rates the plugin is measured at
|
|
75
|
+
/** The rates the plugin is measured at; with no `cells`, each at `CLAP_SWEEP_FRAMES`. */
|
|
71
76
|
readonly rates: readonly number[];
|
|
77
|
+
/** The cells of the grid this plugin is measured at (`measurement-grid.ts`); each format's runs are planned from them. */
|
|
78
|
+
readonly cells?: readonly GridCell[];
|
|
79
|
+
/** Blocks of the CLAP sweep a cell; else `CLAP_SWEEP_BLOCKS`. The LV2 pass has a fixed shape. */
|
|
80
|
+
readonly blocks?: number;
|
|
81
|
+
/** The memory bound every run is held to; none (the runner's own default) when absent. */
|
|
82
|
+
readonly memory?: MemoryBound;
|
|
72
83
|
/** Host extensions to offer beyond the prober's own (a CLAP run). */
|
|
73
84
|
readonly offer?: readonly string[];
|
|
74
85
|
/** The wall clock the runner gives one run, in seconds; else the format's declared one. */
|
|
@@ -80,7 +91,17 @@ export interface ProbeTargetOptions {
|
|
|
80
91
|
|
|
81
92
|
/** The runner's arguments for a CLAP file; `child` is the prober the runner starts. */
|
|
82
93
|
export function clapRunnerArgs(
|
|
83
|
-
options: {
|
|
94
|
+
options: {
|
|
95
|
+
rates: readonly number[];
|
|
96
|
+
quantum?: number;
|
|
97
|
+
blocks?: number;
|
|
98
|
+
dataBytes?: number;
|
|
99
|
+
offer?: readonly string[];
|
|
100
|
+
wallS?: number;
|
|
101
|
+
out: string;
|
|
102
|
+
log: string;
|
|
103
|
+
child: string;
|
|
104
|
+
},
|
|
84
105
|
binaryPath: string,
|
|
85
106
|
id: string | undefined,
|
|
86
107
|
): string[] {
|
|
@@ -91,12 +112,13 @@ export function clapRunnerArgs(
|
|
|
91
112
|
"--rates",
|
|
92
113
|
options.rates.join(","),
|
|
93
114
|
"--quantum",
|
|
94
|
-
String(CLAP_SWEEP_FRAMES),
|
|
115
|
+
String(options.quantum ?? CLAP_SWEEP_FRAMES),
|
|
95
116
|
"--blocks",
|
|
96
|
-
String(CLAP_SWEEP_BLOCKS),
|
|
117
|
+
String(options.blocks ?? CLAP_SWEEP_BLOCKS),
|
|
97
118
|
...(options.offer === undefined || options.offer.length === 0 ? [] : ["--offer", options.offer.join(",")]),
|
|
98
119
|
"--wall-s",
|
|
99
120
|
String(options.wallS ?? CLAP_QUALIFY_TIMEOUT_S),
|
|
121
|
+
...(options.dataBytes === undefined ? [] : ["--data-bytes", String(options.dataBytes)]),
|
|
100
122
|
"--out",
|
|
101
123
|
options.out,
|
|
102
124
|
"--log",
|
|
@@ -110,16 +132,20 @@ export function clapRunnerArgs(
|
|
|
110
132
|
* The runner's arguments for an LV2 plugin, named as `lv2:<uri>` and measured by `lv2_measure`: the one this run
|
|
111
133
|
* located when `child` names it, else the one the runner looks for where the packages install it.
|
|
112
134
|
*/
|
|
113
|
-
export function lv2RunnerArgs(
|
|
135
|
+
export function lv2RunnerArgs(
|
|
136
|
+
options: { rates: readonly number[]; quantum?: number; dataBytes?: number; wallS?: number; out: string; log: string; child?: string },
|
|
137
|
+
uri: string,
|
|
138
|
+
): string[] {
|
|
114
139
|
return [
|
|
115
140
|
"run",
|
|
116
141
|
...(options.child === undefined ? [] : ["--child", options.child]),
|
|
117
142
|
"--rates",
|
|
118
143
|
options.rates.join(","),
|
|
119
144
|
"--quantum",
|
|
120
|
-
String(LV2_HOSTING_BLOCK_FRAMES),
|
|
145
|
+
String(options.quantum ?? LV2_HOSTING_BLOCK_FRAMES),
|
|
121
146
|
"--wall-s",
|
|
122
147
|
String(options.wallS ?? LV2_HOSTING_TIMEOUT_S),
|
|
148
|
+
...(options.dataBytes === undefined ? [] : ["--data-bytes", String(options.dataBytes)]),
|
|
123
149
|
"--out",
|
|
124
150
|
options.out,
|
|
125
151
|
"--log",
|
|
@@ -134,7 +160,7 @@ export interface ProbedTarget {
|
|
|
134
160
|
readonly output: ProbeOutput;
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
function runOne(target: PluginTarget, options: ProbeTargetOptions): ProbedTarget {
|
|
163
|
+
function runOne(target: PluginTarget, options: ProbeTargetOptions, invocation: RunnerInvocation): ProbedTarget {
|
|
138
164
|
const env = options.env ?? process.env;
|
|
139
165
|
const tool = probeRunnerTool(env);
|
|
140
166
|
const work = mkdtempSync(join(tmpdir(), "plugin-qualify-probe-"));
|
|
@@ -145,20 +171,28 @@ function runOne(target: PluginTarget, options: ProbeTargetOptions): ProbedTarget
|
|
|
145
171
|
let runEnv: Record<string, string | undefined> = { ...env };
|
|
146
172
|
let expectedId: string;
|
|
147
173
|
const wall = options.wallS === undefined ? {} : { wallS: options.wallS };
|
|
174
|
+
const bound = options.memory === undefined ? {} : { dataBytes: options.memory.bytes };
|
|
175
|
+
const at = { rates: invocation.rates, quantum: invocation.quantum, ...bound };
|
|
148
176
|
if (target.format === "clap") {
|
|
149
177
|
if (target.binaryPath === undefined) throw new ProbeRunnerError(`${target.uri}: no .clap to load; name the file or a directory holding it`);
|
|
150
178
|
expectedId = target.uri.startsWith("urn:clap:") ? target.uri.slice("urn:clap:".length) : target.uri;
|
|
151
179
|
const { tool: child, interposer } = clapQualifyTools(env, options.exists ?? existsSync);
|
|
152
|
-
args = clapRunnerArgs(
|
|
180
|
+
args = clapRunnerArgs(
|
|
181
|
+
{ ...at, ...(options.blocks === undefined ? {} : { blocks: options.blocks }), ...(options.offer === undefined ? {} : { offer: options.offer }), ...wall, out, log, child },
|
|
182
|
+
target.binaryPath,
|
|
183
|
+
expectedId,
|
|
184
|
+
);
|
|
153
185
|
runEnv = { ...env, LD_PRELOAD: interposer };
|
|
154
186
|
} else {
|
|
155
187
|
expectedId = target.uri;
|
|
156
188
|
const child = locateNativeTool(LV2_MEASURE_TOOL_NAME, env, options.exists ?? existsSync).path;
|
|
157
189
|
args = lv2RunnerArgs(
|
|
158
|
-
{
|
|
190
|
+
{ ...at, ...wall, out, log, ...(child === undefined ? {} : { child }) },
|
|
159
191
|
target.uri,
|
|
160
192
|
);
|
|
161
193
|
}
|
|
194
|
+
// under a bound, the interposer ends the child the moment an allocation is refused, with the status that names it
|
|
195
|
+
if (options.memory !== undefined) runEnv = { ...runEnv, [MEMORY_BOUND_EXIT_ENV]: "1" };
|
|
162
196
|
const r = (options.run ?? spawnRunner)(tool, args, runEnv);
|
|
163
197
|
if (r.error !== undefined) {
|
|
164
198
|
throw new ProbeRunnerError(`cannot run the probe runner "${tool}" (${r.error.message}): install plugin-hostd or set ${PROBE_RUNNER_ENV} to plugin-hostd-probe`);
|
|
@@ -186,9 +220,23 @@ function runOne(target: PluginTarget, options: ProbeTargetOptions): ProbedTarget
|
|
|
186
220
|
}
|
|
187
221
|
}
|
|
188
222
|
|
|
189
|
-
/** The
|
|
223
|
+
/** The cells a target is measured at: the options' cells, else every rate at `CLAP_SWEEP_FRAMES`. */
|
|
224
|
+
export function targetCells(options: Pick<ProbeTargetOptions, "rates" | "cells">): readonly GridCell[] {
|
|
225
|
+
return options.cells ?? options.rates.map((rate) => ({ rate, quantum: CLAP_SWEEP_FRAMES }));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The probe output of one target over its grid, through the runner: one run per quantum (CLAP) or per cell (LV2),
|
|
230
|
+
* merged into one document (`probe-merge.ts`). Throws {@link ProbeRunnerError} when a run leaves nothing to judge.
|
|
231
|
+
*/
|
|
190
232
|
export function probeTarget(target: PluginTarget, options: ProbeTargetOptions): ProbedTarget {
|
|
191
|
-
|
|
233
|
+
const parts = runnerInvocations(target.format, targetCells(options)).map((invocation) => ({ ...runOne(target, options, invocation), invocation }));
|
|
234
|
+
try {
|
|
235
|
+
return mergeProbeRuns(parts, options.memory);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
if (e instanceof ProbeMergeError) throw new ProbeRunnerError(e.message);
|
|
238
|
+
throw e;
|
|
239
|
+
}
|
|
192
240
|
}
|
|
193
241
|
|
|
194
242
|
/** What the pass over a run's targets produced. */
|
|
@@ -210,7 +258,16 @@ export interface ProbePass {
|
|
|
210
258
|
*/
|
|
211
259
|
export function qualifyTargets(
|
|
212
260
|
targets: readonly PluginTarget[],
|
|
213
|
-
options: ProbeTargetOptions & {
|
|
261
|
+
options: ProbeTargetOptions & {
|
|
262
|
+
readonly profiles?: readonly HostProfile[];
|
|
263
|
+
readonly admissions?: AdmissionRuns;
|
|
264
|
+
/** How the grid is chosen; with it, each target's cells come from the hosts it is judged under. */
|
|
265
|
+
readonly grid?: GridSpec;
|
|
266
|
+
/** The quanta of a `full` grid; else `QUALIFY_QUANTA`. */
|
|
267
|
+
readonly quanta?: readonly number[];
|
|
268
|
+
/** At most this many cells a plugin, the first of the grid's order; 0 or absent is every cell. */
|
|
269
|
+
readonly cellsPerPluginBudget?: number;
|
|
270
|
+
},
|
|
214
271
|
): ProbePass {
|
|
215
272
|
const profiles = options.profiles ?? [OPENMIXER_CONSOLE_PROFILE];
|
|
216
273
|
const admissionRuns = options.admissions ?? SPAWNED_ADMISSIONS;
|
|
@@ -225,6 +282,11 @@ export function qualifyTargets(
|
|
|
225
282
|
.flatMap((id) => (id === undefined ? [] : [shippedHostProfile(id)]))
|
|
226
283
|
.filter((p): p is HostProfile => p !== undefined);
|
|
227
284
|
const judgedProfiles = [...profiles, ...tierProfiles.filter((t) => !profiles.some((p) => p.id === t.id))];
|
|
285
|
+
const cells =
|
|
286
|
+
options.grid === undefined
|
|
287
|
+
? options.cells
|
|
288
|
+
: gridCells(options.grid, { rates: options.rates, quanta: options.quanta ?? QUALIFY_QUANTA, hosts: judgedProfiles.map((p) => ({ rate: p.cost.rate, quantum: p.cost.quantum })) });
|
|
289
|
+
const measured = cells === undefined ? {} : { cells: budgetCells(cells, options.cellsPerPluginBudget) };
|
|
228
290
|
// an LV2 plugin is run under each measured host once, whichever probe output it is judged with
|
|
229
291
|
const admissions: Record<string, HostAdmission> = {};
|
|
230
292
|
if (target.format === "lv2") {
|
|
@@ -246,7 +308,7 @@ export function qualifyTargets(
|
|
|
246
308
|
const targetRuns: { bytes: Buffer; judgment: Judgment }[] = [];
|
|
247
309
|
try {
|
|
248
310
|
for (const { offer, profiles: forOffer } of byOffer.values()) {
|
|
249
|
-
const probed = probeTarget(target, { ...options, offer });
|
|
311
|
+
const probed = probeTarget(target, { ...options, ...measured, offer });
|
|
250
312
|
const judgment = judgeProbeOutput(probed.output, forOffer, { admissions });
|
|
251
313
|
targetRuns.push({ bytes: probed.bytes, judgment });
|
|
252
314
|
for (const v of judgmentVerdicts(judgment)) judged.set(v.host, v);
|
|
@@ -21,6 +21,8 @@ import type { Judgment, RecordReason } from "./probe-judge.js";
|
|
|
21
21
|
import { sha256Hex, PROBE_OK, type ProbeGridPoint, type ProbeOutput } from "./probe-output.js";
|
|
22
22
|
import type { VoicesHeld } from "./measurement/format.js";
|
|
23
23
|
import { writeFileWhole } from "./whole-file.js";
|
|
24
|
+
import { HOSTING_CODE } from "./hosting-codes.js";
|
|
25
|
+
import { MEASUREMENT_UNMEASURED_REASONS } from "@openmixer/omx-contract";
|
|
24
26
|
|
|
25
27
|
export const QUALIFICATION_RECORD_SCHEMA = "plugin-qualify.qualification-record/1";
|
|
26
28
|
|
|
@@ -89,6 +91,44 @@ export function unmeasuredFigures(output: ProbeOutput): readonly UnmeasuredFigur
|
|
|
89
91
|
}));
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
/** A cell's latency when the run took no figure: the contract's unmeasured word and the hosting code a reader classifies by. */
|
|
95
|
+
export interface UnmeasuredLatency {
|
|
96
|
+
readonly unmeasuredReason: string;
|
|
97
|
+
readonly code: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A grid point as the record writes it: every cell carries `latency`, the figures or why there are none (#201). */
|
|
101
|
+
export type RecordGridPoint = Omit<ProbeGridPoint, "latency"> & {
|
|
102
|
+
readonly latency: NonNullable<ProbeGridPoint["latency"]> | UnmeasuredLatency;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const UNMEASURED_WORDS: ReadonlySet<string> = new Set(Object.values(MEASUREMENT_UNMEASURED_REASONS).map((r) => r.word));
|
|
106
|
+
const word = (key: keyof typeof MEASUREMENT_UNMEASURED_REASONS): string => MEASUREMENT_UNMEASURED_REASONS[key].word;
|
|
107
|
+
|
|
108
|
+
/** The unmeasured word of a cell whose run did not end well, by its outcome code. */
|
|
109
|
+
const OUTCOME_WORD: Readonly<Record<string, string>> = {
|
|
110
|
+
"probe.crashed": word("probeCrashed"),
|
|
111
|
+
"probe.timeout": word("probeTimeout"),
|
|
112
|
+
"probe.refused-instantiation": word("refusedInstantiation"),
|
|
113
|
+
"probe.non-finite-output": word("nonFiniteOutput"),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Why a cell has no latency figure. A cell whose run failed: its outcome's word. A latency step at the cell that did not
|
|
118
|
+
* end well: the child's own reason, which the runner writes first in the step's detail, when it is a contract word,
|
|
119
|
+
* else `probe-error`. No latency step at all: the run did not take the figure (`not-attempted`), as a runner or a child
|
|
120
|
+
* older than the latency arm writes it.
|
|
121
|
+
*/
|
|
122
|
+
export function latencyOfCell(output: ProbeOutput, point: ProbeGridPoint): RecordGridPoint["latency"] {
|
|
123
|
+
if (point.latency !== undefined) return point.latency;
|
|
124
|
+
const code = HOSTING_CODE.latencyUnmeasured;
|
|
125
|
+
if (point.outcome !== PROBE_OK) return { unmeasuredReason: OUTCOME_WORD[point.outcome] ?? word("probeError"), code };
|
|
126
|
+
const step = output.steps.find((st) => st.step === "latency" && st.rate === point.rate && (st.quantum === undefined || st.quantum === point.quantum));
|
|
127
|
+
if (step === undefined) return { unmeasuredReason: word("notAttempted"), code };
|
|
128
|
+
const said = step.detail?.split(":")[0]?.trim() ?? "";
|
|
129
|
+
return { unmeasuredReason: UNMEASURED_WORDS.has(said) ? said : word("probeError"), code: step.hostingCode ?? code };
|
|
130
|
+
}
|
|
131
|
+
|
|
92
132
|
/**
|
|
93
133
|
* What the running history made of the measured verdicts, for one host class: the second verdict beside the measured one.
|
|
94
134
|
* Written by `plugin-qualify adjust --history`; the measured `verdicts` above are never changed by it. A record without the
|
|
@@ -136,7 +176,7 @@ export interface QualificationRecord {
|
|
|
136
176
|
* {declaredFrames, measuredFrames}, `cost`, `rtSafety`, `noteSweep`), and the figures the run could not take, named.
|
|
137
177
|
*/
|
|
138
178
|
readonly measurements: {
|
|
139
|
-
readonly grid: readonly
|
|
179
|
+
readonly grid: readonly RecordGridPoint[];
|
|
140
180
|
readonly behaviour?: Readonly<Record<string, unknown>>;
|
|
141
181
|
readonly unmeasured?: readonly UnmeasuredFigure[];
|
|
142
182
|
};
|
|
@@ -150,6 +190,8 @@ export interface QualificationRecord {
|
|
|
150
190
|
readonly measuredAt: string;
|
|
151
191
|
/** The second verdict, per host class of the history that was read; absent until `adjust --history` has run. */
|
|
152
192
|
readonly history?: readonly HistoryBlock[];
|
|
193
|
+
/** Where the record came from when it was not measured on this device: the distributed set's provenance, as the set's package wrote it. */
|
|
194
|
+
readonly provenance?: Readonly<Record<string, unknown>>;
|
|
153
195
|
/** The probe output this record judged. */
|
|
154
196
|
readonly probe: {
|
|
155
197
|
readonly file: string;
|
|
@@ -227,7 +269,7 @@ export function buildQualificationRecord(
|
|
|
227
269
|
tiers: tiersOf(judgment),
|
|
228
270
|
cost: judgment.hosts.map((h) => ({ host: h.verdict.host, ...h.cost })),
|
|
229
271
|
measurements: {
|
|
230
|
-
grid: o.grid,
|
|
272
|
+
grid: o.grid.map((p) => ({ ...p, latency: latencyOfCell(o, p) })),
|
|
231
273
|
...(o.behaviour === undefined ? {} : { behaviour: o.behaviour }),
|
|
232
274
|
...(unmeasuredFigures(o).length === 0 ? {} : { unmeasured: unmeasuredFigures(o) }),
|
|
233
275
|
},
|
package/src/qualify-cli.ts
CHANGED
|
@@ -20,12 +20,22 @@ import { classifyPluginHosting, type QualifiedPlugin } from "./hosting-suitabili
|
|
|
20
20
|
import type { SuitabilityRating } from "./rating.js";
|
|
21
21
|
import type { PluginLoader, PluginTarget } from "./loader.js";
|
|
22
22
|
import { EXIT_CODE, OUTPUT_FORMATS, QUALIFY_OPTIONS, type OutputFormat } from "./qualify-options.js";
|
|
23
|
+
import { GRID_HOSTS, parseGridSpec, type GridSpec } from "./measurement-grid.js";
|
|
24
|
+
import { parseMemoryLimit } from "./memory-bound.js";
|
|
23
25
|
|
|
24
26
|
export interface QualifyArgs {
|
|
25
27
|
readonly all: boolean;
|
|
26
28
|
readonly targets: readonly string[];
|
|
27
29
|
readonly rates: readonly number[];
|
|
28
30
|
readonly quanta: readonly number[];
|
|
31
|
+
/** How the grid is chosen (`--grid`, `--grid-hosts`). */
|
|
32
|
+
readonly grid: GridSpec;
|
|
33
|
+
/** `--cells-per-plugin-budget`: at most this many cells a plugin; 0 is every cell. */
|
|
34
|
+
readonly cellsPerPluginBudget: number;
|
|
35
|
+
/** `--blocks`: blocks of the CLAP sweep a cell, when given. */
|
|
36
|
+
readonly blocks?: number;
|
|
37
|
+
/** `--memory-limit`: bytes, or `off`; absent is the default, half of the RAM. */
|
|
38
|
+
readonly memoryLimit?: number | "off";
|
|
29
39
|
readonly out?: string;
|
|
30
40
|
/** `--pins`: where the pins file is written. */
|
|
31
41
|
readonly pins?: string;
|
|
@@ -79,6 +89,23 @@ export function parseQualifyArgs(argv: readonly string[]): ParseResult {
|
|
|
79
89
|
if (rates === undefined) return { ok: false, error: "--rates must be positive integers, comma-separated" };
|
|
80
90
|
const quanta = numberList((seen.get("--quanta") as string | undefined) ?? defaultOf("--quanta"));
|
|
81
91
|
if (quanta === undefined) return { ok: false, error: "--quanta must be positive integers, comma-separated" };
|
|
92
|
+
const gridText = seen.get("--grid") as string | undefined;
|
|
93
|
+
if (gridText !== undefined && seen.has("--grid-hosts") && gridText.trim() !== "hosts") return { ok: false, error: "--grid-hosts and --grid name two grids; give one" };
|
|
94
|
+
const grid = gridText === undefined ? { ok: true as const, spec: GRID_HOSTS } : parseGridSpec(gridText);
|
|
95
|
+
if (!grid.ok) return { ok: false, error: `--grid: ${grid.error}` };
|
|
96
|
+
const count = (flag: string, min: number): number | undefined => {
|
|
97
|
+
const v = seen.get(flag) as string | undefined;
|
|
98
|
+
if (v === undefined) return undefined;
|
|
99
|
+
const n = Number(v);
|
|
100
|
+
return /^\d+$/.test(v.trim()) && Number.isSafeInteger(n) && n >= min ? n : NaN;
|
|
101
|
+
};
|
|
102
|
+
const cellsPerPluginBudget = count("--cells-per-plugin-budget", 0) ?? 0;
|
|
103
|
+
if (Number.isNaN(cellsPerPluginBudget)) return { ok: false, error: "--cells-per-plugin-budget must be a whole number, 0 or more" };
|
|
104
|
+
const blocks = count("--blocks", 1);
|
|
105
|
+
if (blocks !== undefined && Number.isNaN(blocks)) return { ok: false, error: "--blocks must be a whole number, 1 or more" };
|
|
106
|
+
const memoryText = seen.get("--memory-limit") as string | undefined;
|
|
107
|
+
const memory = memoryText === undefined ? undefined : parseMemoryLimit(memoryText);
|
|
108
|
+
if (memory !== undefined && !memory.ok) return { ok: false, error: `--memory-limit: ${memory.error}` };
|
|
82
109
|
const format = (seen.get("--format") as string | undefined) ?? defaultOf("--format");
|
|
83
110
|
if (!(OUTPUT_FORMATS as readonly string[]).includes(format)) {
|
|
84
111
|
return { ok: false, error: `--format must be one of ${OUTPUT_FORMATS.join(", ")}` };
|
|
@@ -94,6 +121,10 @@ export function parseQualifyArgs(argv: readonly string[]): ParseResult {
|
|
|
94
121
|
targets,
|
|
95
122
|
rates,
|
|
96
123
|
quanta,
|
|
124
|
+
grid: grid.spec,
|
|
125
|
+
cellsPerPluginBudget,
|
|
126
|
+
...(blocks === undefined ? {} : { blocks }),
|
|
127
|
+
...(memory === undefined ? {} : { memoryLimit: memory.bytes }),
|
|
97
128
|
...(out === undefined ? {} : { out }),
|
|
98
129
|
...(pins === undefined ? {} : { pins }),
|
|
99
130
|
format: format as OutputFormat,
|
|
@@ -108,7 +139,7 @@ export function helpText(): string {
|
|
|
108
139
|
const rows = QUALIFY_OPTIONS.map((o) => {
|
|
109
140
|
const left = o.value === undefined ? o.flag : `${o.flag} <${o.value}>`;
|
|
110
141
|
const def = o.default === undefined ? "" : ` (default: ${o.default})`;
|
|
111
|
-
return ` ${left.padEnd(
|
|
142
|
+
return ` ${left.padEnd(36)}${o.describe}${def}`;
|
|
112
143
|
});
|
|
113
144
|
return [
|
|
114
145
|
"plugin-qualify — qualify audio plugins for real-time hosting",
|
|
@@ -133,6 +133,23 @@ export const NATIVE_TOOL_DIRS: readonly string[] = ["/usr/libexec/plugin-qualify
|
|
|
133
133
|
export const CLAP_SWEEP_BLOCKS = 512;
|
|
134
134
|
export const CLAP_SWEEP_FRAMES = 512;
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The measurement grid (#193). Every record written before the grid was measured at `GRID_CONTINUITY_QUANTUM`, so
|
|
138
|
+
* the hosts' grid also takes that quantum at every rate: a new record stays comparable with an old one.
|
|
139
|
+
*/
|
|
140
|
+
export const GRID_CONTINUITY_QUANTUM = CLAP_SWEEP_FRAMES;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The memory bound of a run (#193). The runner's data limit (`--data-bytes`, `RLIMIT_DATA` on the measuring child) is
|
|
144
|
+
* `MEMORY_LIMIT_RAM_FRACTION` of the machine's RAM unless `--memory-limit` says otherwise. With `MEMORY_BOUND_EXIT_ENV`
|
|
145
|
+
* set, the interposer ends the child with `MEMORY_BOUND_EXIT_STATUS` the moment an allocation is refused, so a run
|
|
146
|
+
* that outgrew the bound is told from a crash by an exit status, never by words; `native/rt-interpose.h` holds the
|
|
147
|
+
* same two (src/memory-bound.test.ts holds them together).
|
|
148
|
+
*/
|
|
149
|
+
export const MEMORY_LIMIT_RAM_FRACTION = 0.5;
|
|
150
|
+
export const MEMORY_BOUND_EXIT_ENV = "OMX_RT_OOM_EXIT";
|
|
151
|
+
export const MEMORY_BOUND_EXIT_STATUS = 121;
|
|
152
|
+
|
|
136
153
|
/** Seconds one `.clap` may take to qualify at every rate before the prober is killed. */
|
|
137
154
|
export const CLAP_QUALIFY_TIMEOUT_S = 120;
|
|
138
155
|
|
package/src/qualify-options.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The `plugin-qualify` CLI's OPTIONS, declared once. The parser, `--help` and the tests all read this table; every default is a declaration
|
|
5
5
|
* from `declarations.ts`, never a literal here.
|
|
6
6
|
*/
|
|
7
|
-
import { QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
7
|
+
import { CLAP_SWEEP_BLOCKS, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
8
8
|
import { HOST_PROFILES, OPENMIXER_CONSOLE_PROFILE } from "./host-profiles.js";
|
|
9
9
|
|
|
10
10
|
/** How the result is printed. `text` = plugin, verdict, the reason that decided it. */
|
|
@@ -35,6 +35,16 @@ export const QUALIFY_OPTIONS: readonly QualifyOption[] = [
|
|
|
35
35
|
{ flag: "--all", describe: "every LV2 plugin on the LV2 search path and every CLAP plugin on the CLAP search path; with --out, one record each" },
|
|
36
36
|
{ flag: "--rates", value: "list", describe: "sample rates to measure at, comma-separated", default: QUALIFY_RATES.join(",") },
|
|
37
37
|
{ flag: "--quanta", value: "list", describe: "quanta (frames) to rate cost at, comma-separated", default: QUALIFY_QUANTA.join(",") },
|
|
38
|
+
{
|
|
39
|
+
flag: "--grid",
|
|
40
|
+
value: "hosts|full|cells",
|
|
41
|
+
describe: "the cells measured: hosts = each judged host's rate/quantum, then 512 at every rate; full = every rate at every quantum; or <rate>/<quantum>,...",
|
|
42
|
+
default: "hosts",
|
|
43
|
+
},
|
|
44
|
+
{ flag: "--grid-hosts", describe: "the same as --grid hosts" },
|
|
45
|
+
{ flag: "--cells-per-plugin-budget", value: "n", describe: "measure at most the first n cells of the grid a plugin (0: every cell)", default: "0" },
|
|
46
|
+
{ flag: "--blocks", value: "n", describe: "blocks of the CLAP sweep a cell (the LV2 pass has a fixed shape)", default: String(CLAP_SWEEP_BLOCKS) },
|
|
47
|
+
{ flag: "--memory-limit", value: "size|off", describe: "the memory bound of every run (bytes, or with K, M, G); past it a cell is probe.oom", default: "half of the RAM" },
|
|
38
48
|
{ flag: "--out", value: "dir", describe: "write one interchange JSON document per run into this directory" },
|
|
39
49
|
{ flag: "--format", value: OUTPUT_FORMATS.join("|"), describe: "json = the verdicts as JSON; text = plugin, verdict, deciding reason", default: "text" },
|
|
40
50
|
{ flag: "--isolate", describe: "a fresh worker per plugin, so a crash or hang takes only that plugin" },
|