@openmixer/plugin-qualify 0.10.1 → 0.12.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 +52 -10
- package/bin/plugin-qualify.mjs +61 -28
- package/dist/clap-loader.d.ts +1 -1
- package/dist/clap-loader.js +3 -3
- package/dist/clap-loader.js.map +1 -1
- package/dist/clap-qualify.d.ts +60 -51
- package/dist/clap-qualify.js +143 -91
- package/dist/clap-qualify.js.map +1 -1
- package/dist/cost-fields.d.ts +26 -0
- package/dist/cost-fields.js +30 -0
- package/dist/cost-fields.js.map +1 -1
- package/dist/hosting-codes.d.ts +1 -0
- package/dist/hosting-codes.js +1 -0
- package/dist/hosting-codes.js.map +1 -1
- package/dist/hosting-suitability.d.ts +22 -3
- package/dist/hosting-suitability.js +25 -5
- 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/judge-cli.d.ts +41 -0
- package/dist/judge-cli.js +114 -0
- package/dist/judge-cli.js.map +1 -0
- package/dist/measurement/cli.d.ts +7 -0
- package/dist/measurement/cli.js +14 -0
- package/dist/measurement/cli.js.map +1 -1
- package/dist/measurement/format.d.ts +44 -4
- package/dist/measurement/format.js +46 -3
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/probe-json.js +4 -0
- package/dist/measurement/probe-json.js.map +1 -1
- package/dist/measurement/sweep-jsonl.js +2 -1
- package/dist/measurement/sweep-jsonl.js.map +1 -1
- package/dist/measurement/vocabulary.d.ts +9 -0
- package/dist/measurement/vocabulary.js +33 -0
- package/dist/measurement/vocabulary.js.map +1 -1
- package/dist/probe-judge.d.ts +65 -0
- package/dist/probe-judge.js +217 -0
- package/dist/probe-judge.js.map +1 -0
- package/dist/probe-output-fixtures.d.ts +25 -0
- package/dist/probe-output-fixtures.js +134 -0
- package/dist/probe-output-fixtures.js.map +1 -0
- package/dist/probe-output.d.ts +153 -0
- package/dist/probe-output.js +374 -0
- package/dist/probe-output.js.map +1 -0
- package/dist/probe-runner.d.ts +79 -0
- package/dist/probe-runner.js +191 -0
- package/dist/probe-runner.js.map +1 -0
- package/dist/qualification-record.d.ts +82 -0
- package/dist/qualification-record.js +85 -0
- package/dist/qualification-record.js.map +1 -0
- package/dist/qualify-cli.js +1 -1
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-declarations.d.ts +11 -2
- package/dist/qualify-declarations.js +14 -4
- package/dist/qualify-declarations.js.map +1 -1
- package/fixtures/clap-scan/fake.json +30 -6
- package/fixtures/probe-output/memory-limit-refused.json +190 -0
- package/fixtures/probe-output/plugin-crash.json +236 -0
- package/fixtures/probe-output/success-msi.json +363 -0
- package/package.json +5 -1
- package/src/clap-loader.ts +3 -2
- package/src/clap-qualify.ts +179 -138
- package/src/cost-fields.ts +39 -0
- package/src/hosting-codes.ts +1 -0
- package/src/hosting-suitability.ts +42 -7
- package/src/index.ts +5 -0
- package/src/judge-cli.ts +133 -0
- package/src/measurement/cli.ts +20 -0
- package/src/measurement/format.ts +80 -2
- package/src/measurement/probe-json.ts +4 -0
- package/src/measurement/sweep-jsonl.ts +2 -1
- package/src/measurement/vocabulary.ts +38 -0
- package/src/probe-judge.ts +272 -0
- package/src/probe-output-fixtures.ts +146 -0
- package/src/probe-output.ts +511 -0
- package/src/probe-runner.ts +244 -0
- package/src/qualification-record.ts +149 -0
- package/src/qualify-cli.ts +1 -1
- package/src/qualify-declarations.ts +16 -4
- package/tools/benchmark.py +116 -13
- package/tools/lv2-measure.mjs +2 -0
package/src/judge-cli.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* `plugin-qualify judge <probe-output>...`: judge probe outputs already written (by
|
|
5
|
+
* `plugin-hostd-probe run`, by plugin-hostd's `judge_cmd`, or by hand) and write the qualification
|
|
6
|
+
* record beside a byte copy of each. `bin/plugin-qualify.mjs` only reads argv and prints.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import type { HostProfile } from "./host-profiles.js";
|
|
13
|
+
import { judgeProbeOutput, judgmentVerdicts } from "./probe-judge.js";
|
|
14
|
+
import { readProbeOutput } from "./probe-output.js";
|
|
15
|
+
import { writeQualification } from "./qualification-record.js";
|
|
16
|
+
import type { QualifyVerdict } from "./qualify-cli.js";
|
|
17
|
+
import { EXIT_CODE, OUTPUT_FORMATS, type OutputFormat } from "./qualify-options.js";
|
|
18
|
+
import { OPENMIXER_CONSOLE_PROFILE } from "./host-profiles.js";
|
|
19
|
+
|
|
20
|
+
export interface JudgeArgs {
|
|
21
|
+
readonly files: readonly string[];
|
|
22
|
+
readonly hostProfiles: readonly string[];
|
|
23
|
+
readonly qualificationDir?: string;
|
|
24
|
+
readonly format: OutputFormat;
|
|
25
|
+
readonly help: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ParseJudgeResult = { ok: true; args: JudgeArgs } | { ok: false; error: string };
|
|
29
|
+
|
|
30
|
+
const VALUE_FLAGS = ["--host-profile", "--qualification-dir", "--format"];
|
|
31
|
+
|
|
32
|
+
export function parseJudgeArgs(argv: readonly string[]): ParseJudgeResult {
|
|
33
|
+
const files: string[] = [];
|
|
34
|
+
const specs: string[] = [];
|
|
35
|
+
let dir: string | undefined;
|
|
36
|
+
let format: string = "text";
|
|
37
|
+
let help = false;
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const a = argv[i]!;
|
|
40
|
+
if (!a.startsWith("--")) {
|
|
41
|
+
files.push(a);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (a === "--help") {
|
|
45
|
+
help = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!VALUE_FLAGS.includes(a)) return { ok: false, error: `unknown option ${a}` };
|
|
49
|
+
const v = argv[++i];
|
|
50
|
+
if (v === undefined || v.startsWith("--")) return { ok: false, error: `${a} needs a value` };
|
|
51
|
+
if (a === "--host-profile") specs.push(...v.split(",").map((t) => t.trim()).filter((t) => t !== ""));
|
|
52
|
+
else if (a === "--qualification-dir") dir = v;
|
|
53
|
+
else format = v;
|
|
54
|
+
}
|
|
55
|
+
if (!(OUTPUT_FORMATS as readonly string[]).includes(format)) return { ok: false, error: `--format must be one of ${OUTPUT_FORMATS.join(", ")}` };
|
|
56
|
+
if (!help && files.length === 0) return { ok: false, error: "name at least one probe output" };
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
args: {
|
|
60
|
+
files,
|
|
61
|
+
hostProfiles: [...new Set(specs.length > 0 ? specs : [OPENMIXER_CONSOLE_PROFILE.id])],
|
|
62
|
+
...(dir === undefined ? {} : { qualificationDir: dir }),
|
|
63
|
+
format: format as OutputFormat,
|
|
64
|
+
help,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function judgeHelpText(): string {
|
|
70
|
+
return [
|
|
71
|
+
"plugin-qualify judge — judge probe outputs and write the qualification records",
|
|
72
|
+
"",
|
|
73
|
+
" plugin-qualify judge [options] <probe-output.json>...",
|
|
74
|
+
"",
|
|
75
|
+
" --host-profile <file|id> judge against this host; repeat or comma-separate for one verdict per profile",
|
|
76
|
+
` (default: ${OPENMIXER_CONSOLE_PROFILE.id})`,
|
|
77
|
+
" --qualification-dir <dir> where the records and the copies of the probe outputs go",
|
|
78
|
+
" (default: $XDG_STATE_HOME/omx/qualification, else ~/.local/state/omx/qualification)",
|
|
79
|
+
" --format <json|text> json = the records; text = plugin, verdict, deciding reason (default: text)",
|
|
80
|
+
"",
|
|
81
|
+
`exit ${EXIT_CODE.allQualified} all qualified / ${EXIT_CODE.someNotQualified} some not / ${EXIT_CODE.couldNotMeasure} something could not be read or judged`,
|
|
82
|
+
"",
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The qualification directory when none is named: the state directory of owner spec section 6.9, per user. */
|
|
87
|
+
export function defaultQualificationDir(env: Readonly<Record<string, string | undefined>>, home: string = homedir()): string {
|
|
88
|
+
const state = env["XDG_STATE_HOME"];
|
|
89
|
+
return join(state !== undefined && state !== "" ? state : join(home, ".local", "state"), "omx", "qualification");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface JudgeFilesResult {
|
|
93
|
+
readonly verdicts: readonly QualifyVerdict[];
|
|
94
|
+
readonly records: readonly { readonly recordPath: string; readonly probePath: string }[];
|
|
95
|
+
/** Each file that was refused or could not be read, with the code or the reason. */
|
|
96
|
+
readonly failures: readonly { readonly file: string; readonly problem: string }[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Judge each file under every profile and write its record; a file that does not read is a failure, and the others go on. */
|
|
100
|
+
export function judgeFiles(
|
|
101
|
+
files: readonly string[],
|
|
102
|
+
options: { profiles: readonly HostProfile[]; dir: string; read?: (path: string) => Buffer },
|
|
103
|
+
): JudgeFilesResult {
|
|
104
|
+
const read = options.read ?? ((p: string) => readFileSync(p));
|
|
105
|
+
const verdicts: QualifyVerdict[] = [];
|
|
106
|
+
const records: { recordPath: string; probePath: string }[] = [];
|
|
107
|
+
const failures: { file: string; problem: string }[] = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
let bytes: Buffer;
|
|
110
|
+
try {
|
|
111
|
+
bytes = read(file);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
failures.push({ file, problem: `cannot read: ${e instanceof Error ? e.message : String(e)}` });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const r = readProbeOutput(bytes.toString("utf8"));
|
|
117
|
+
if (!r.ok) {
|
|
118
|
+
failures.push({ file, problem: `${r.code}: ${r.problem}` });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const judgment = judgeProbeOutput(r.output, options.profiles);
|
|
122
|
+
const w = writeQualification(options.dir, bytes, judgment);
|
|
123
|
+
records.push({ recordPath: w.recordPath, probePath: w.probePath });
|
|
124
|
+
verdicts.push(...judgmentVerdicts(judgment));
|
|
125
|
+
}
|
|
126
|
+
return { verdicts, records, failures };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 2 when a file could not be judged or a plugin was not measured, else 0 when every verdict qualifies, else 1. */
|
|
130
|
+
export function judgeExitCode(result: JudgeFilesResult): number {
|
|
131
|
+
if (result.failures.length > 0 || result.verdicts.length === 0 || result.verdicts.some((v) => !v.measured)) return EXIT_CODE.couldNotMeasure;
|
|
132
|
+
return result.verdicts.every((v) => v.qualified) ? EXIT_CODE.allQualified : EXIT_CODE.someNotQualified;
|
|
133
|
+
}
|
package/src/measurement/cli.ts
CHANGED
|
@@ -54,6 +54,9 @@ export const TOOL_NAME = "lv2-measure";
|
|
|
54
54
|
*/
|
|
55
55
|
export const TOOL_VERSION = "0.1.0";
|
|
56
56
|
|
|
57
|
+
/** The most notes a cost run holds: one per MIDI key, as the probers cap it. */
|
|
58
|
+
export const PROBE_NOTES_MAX = 128;
|
|
59
|
+
|
|
57
60
|
/** A fully resolved invocation. */
|
|
58
61
|
export interface MeasureOptions {
|
|
59
62
|
/** Where the document goes. Absent = stdout. */
|
|
@@ -70,6 +73,11 @@ export interface MeasureOptions {
|
|
|
70
73
|
readonly sweep: boolean;
|
|
71
74
|
/** Core to pin the cost pass to. Absent = let the prober choose. */
|
|
72
75
|
readonly costCpu?: number;
|
|
76
|
+
/**
|
|
77
|
+
* The notes the cost pass holds down while it times an instrument (`--probe-notes`, the console's
|
|
78
|
+
* `SYNTH_VOICE_PROBE_NOTES`), basis `probe`. Absent = an instrument is unmeasured `voices-unknown`.
|
|
79
|
+
*/
|
|
80
|
+
readonly probeNotes?: number;
|
|
73
81
|
readonly label?: string;
|
|
74
82
|
/** Directory holding `scan.py` and `benchmark.py`. */
|
|
75
83
|
readonly toolDir: string;
|
|
@@ -203,6 +211,7 @@ export function buildPipeline(options: MeasureOptions, native?: HostingTools): P
|
|
|
203
211
|
"--cost-provenance",
|
|
204
212
|
costProvenance,
|
|
205
213
|
...(options.costCpu === undefined ? [] : ["--cost-cpu", String(options.costCpu)]),
|
|
214
|
+
...(options.probeNotes === undefined ? [] : ["--probe-notes", String(options.probeNotes)]),
|
|
206
215
|
],
|
|
207
216
|
produces: cost,
|
|
208
217
|
dimension: "cost",
|
|
@@ -356,6 +365,7 @@ export function parseMeasureArgs(
|
|
|
356
365
|
let workDir = defaults.workDir;
|
|
357
366
|
let sweep = false;
|
|
358
367
|
let costCpu: number | undefined;
|
|
368
|
+
let probeNotes: number | undefined;
|
|
359
369
|
let label: string | undefined;
|
|
360
370
|
let toolDir = defaults.toolDir;
|
|
361
371
|
let python = env.PYTHON ?? "python3";
|
|
@@ -430,6 +440,15 @@ export function parseMeasureArgs(
|
|
|
430
440
|
costCpu = cpu;
|
|
431
441
|
break;
|
|
432
442
|
}
|
|
443
|
+
case "--probe-notes": {
|
|
444
|
+
const value = next();
|
|
445
|
+
const notes = value === undefined ? Number.NaN : Number(value);
|
|
446
|
+
if (!Number.isInteger(notes) || notes < 1 || notes > PROBE_NOTES_MAX) {
|
|
447
|
+
return { ok: false, error: `--probe-notes needs a count of notes, 1 to ${PROBE_NOTES_MAX}` };
|
|
448
|
+
}
|
|
449
|
+
probeNotes = notes;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
433
452
|
case "--label": {
|
|
434
453
|
const value = next();
|
|
435
454
|
if (value === undefined) return { ok: false, error: "--label needs text" };
|
|
@@ -494,6 +513,7 @@ export function parseMeasureArgs(
|
|
|
494
513
|
...(scanPath === undefined ? {} : { scanPath }),
|
|
495
514
|
...(plugins === undefined ? {} : { plugins }),
|
|
496
515
|
...(costCpu === undefined ? {} : { costCpu }),
|
|
516
|
+
...(probeNotes === undefined ? {} : { probeNotes }),
|
|
497
517
|
...(label === undefined ? {} : { label }),
|
|
498
518
|
...(featureFlags.length === 0 ? {} : { featureFlags }),
|
|
499
519
|
},
|
|
@@ -58,10 +58,12 @@ import {
|
|
|
58
58
|
type HostLatencyHandling,
|
|
59
59
|
type MeasurementDimension,
|
|
60
60
|
type VerdictRating,
|
|
61
|
+
type VoiceBasis,
|
|
61
62
|
HOST_ISOLATIONS,
|
|
62
63
|
HOST_LATENCY_HANDLING,
|
|
63
64
|
MEASUREMENT_DIMENSIONS,
|
|
64
65
|
VERDICT_RATINGS,
|
|
66
|
+
isVoiceBasis,
|
|
65
67
|
readDimension,
|
|
66
68
|
} from "./vocabulary.js";
|
|
67
69
|
|
|
@@ -74,8 +76,10 @@ export const MEASUREMENT_FORMAT_MAJOR = 1;
|
|
|
74
76
|
/** The minor this build writes. Additive-only within {@link MEASUREMENT_FORMAT_MAJOR}.
|
|
75
77
|
* `1.1` added the three hosting dimensions — {@link StabilityMeasurement},
|
|
76
78
|
* {@link RtSafetyMeasurement}, {@link FeaturesMeasurement} — which a 1.0 reader skips.
|
|
77
|
-
* `1.2` added the verdicts per host profile; `1.3` the {@link IntegrityMeasurement} block.
|
|
78
|
-
|
|
79
|
+
* `1.2` added the verdicts per host profile; `1.3` the {@link IntegrityMeasurement} block; `1.4` the notes
|
|
80
|
+
* an instrument's cost was measured with ({@link CostAtRate.voices}); `1.5` the note sweep an instrument's
|
|
81
|
+
* `run()` was counted under ({@link RtSafetyMeasurement.noteSweep}). */
|
|
82
|
+
export const MEASUREMENT_FORMAT_MINOR = 5;
|
|
79
83
|
|
|
80
84
|
/** The version string this build writes, e.g. `"1.0"`. */
|
|
81
85
|
export const MEASUREMENT_FORMAT_VERSION = `${MEASUREMENT_FORMAT_MAJOR}.${MEASUREMENT_FORMAT_MINOR}`;
|
|
@@ -270,6 +274,17 @@ export interface LatencyMeasurement {
|
|
|
270
274
|
readonly unmeasuredDetail?: string;
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The notes an instrument held down while its cost was timed (1.4): how many, where that count came
|
|
279
|
+
* from ({@link VoiceBasis}), and the voice capacity the plugin declared at that rate when it declares one.
|
|
280
|
+
* An instrument costs nothing until a voice plays, so its figure means nothing without this.
|
|
281
|
+
*/
|
|
282
|
+
export interface VoicesHeld {
|
|
283
|
+
readonly held: number;
|
|
284
|
+
readonly basis: VoiceBasis;
|
|
285
|
+
readonly capacity?: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
273
288
|
/** One rate's cost reading. Per SAMPLE, so it is independent of the block size. */
|
|
274
289
|
export interface CostAtRate {
|
|
275
290
|
readonly nsPerSampleMedian: number;
|
|
@@ -286,6 +301,8 @@ export interface CostAtRate {
|
|
|
286
301
|
/** Timed, but emitted silence under a stimulus that should have passed — the figure may
|
|
287
302
|
* describe a short-circuit path rather than the DSP. */
|
|
288
303
|
readonly silentOutput?: boolean;
|
|
304
|
+
/** 1.4: for an instrument, the notes held while it was timed. Absent for an effect. */
|
|
305
|
+
readonly voices?: VoicesHeld;
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
/** Everything one run learned about one plugin's CPU cost. */
|
|
@@ -369,6 +386,27 @@ export interface StabilityMeasurement {
|
|
|
369
386
|
readonly unmeasuredDetail?: string;
|
|
370
387
|
}
|
|
371
388
|
|
|
389
|
+
/**
|
|
390
|
+
* 1.5: an instrument played through its MIDI input while the interposer counted, apart from the control
|
|
391
|
+
* sweep — a chord, bend and a controller, pressure, the chord let go, a burst of note ons and their offs,
|
|
392
|
+
* all notes off. `applicable: false` is a plugin with no MIDI input; the counts are present only when
|
|
393
|
+
* the interposer was loaded.
|
|
394
|
+
*/
|
|
395
|
+
export interface NoteSweepMeasurement {
|
|
396
|
+
readonly applicable: boolean;
|
|
397
|
+
readonly dialect?: "clap" | "midi";
|
|
398
|
+
readonly blocks?: number;
|
|
399
|
+
readonly blockFrames?: number;
|
|
400
|
+
/** The note, expression and MIDI events the sweep sent. */
|
|
401
|
+
readonly noteEvents?: number;
|
|
402
|
+
readonly processErrors?: number;
|
|
403
|
+
readonly nonFinite?: boolean;
|
|
404
|
+
readonly interposer?: boolean;
|
|
405
|
+
readonly allocationsInRun?: number;
|
|
406
|
+
readonly locksInRun?: number;
|
|
407
|
+
readonly syscallsInRun?: number;
|
|
408
|
+
}
|
|
409
|
+
|
|
372
410
|
/**
|
|
373
411
|
* What an interposer counted INSIDE `run()`. `rate` and `blockFrames` are the pair it ran at —
|
|
374
412
|
* a count is a property of the code paths the run took there. Readable when all three counts
|
|
@@ -391,6 +429,8 @@ export interface RtSafetyMeasurement {
|
|
|
391
429
|
readonly syscallsInRun?: number;
|
|
392
430
|
/** Repeats disagreed. */
|
|
393
431
|
readonly variable?: boolean;
|
|
432
|
+
/** 1.5: the same counts taken while the plugin was played through its MIDI input. */
|
|
433
|
+
readonly noteSweep?: NoteSweepMeasurement;
|
|
394
434
|
readonly unmeasuredReason?: string;
|
|
395
435
|
readonly unmeasuredDetail?: string;
|
|
396
436
|
}
|
|
@@ -834,9 +874,20 @@ function readCostAtRate(value: unknown): CostAtRate | undefined {
|
|
|
834
874
|
...optional("nsPerSampleMax", finiteNumber(value.nsPerSampleMax)),
|
|
835
875
|
...optional("warmupNsPerSampleMedian", finiteNumber(value.warmupNsPerSampleMedian)),
|
|
836
876
|
...optional("silentOutput", booleanValue(value.silentOutput)),
|
|
877
|
+
...optional("voices", readVoicesHeld(value.voices)),
|
|
837
878
|
};
|
|
838
879
|
}
|
|
839
880
|
|
|
881
|
+
/** The notes held, or `undefined` unless the count and a known basis are there. */
|
|
882
|
+
export function readVoicesHeld(value: unknown): VoicesHeld | undefined {
|
|
883
|
+
if (!isRecord(value)) return undefined;
|
|
884
|
+
const held = positiveNumber(value.held);
|
|
885
|
+
const basis = isVoiceBasis(value.basis) ? value.basis : undefined;
|
|
886
|
+
const capacity = finiteNumber(value.capacity);
|
|
887
|
+
if (held === undefined || !Number.isInteger(held) || basis === undefined) return undefined;
|
|
888
|
+
return { held, basis, ...(capacity !== undefined && Number.isInteger(capacity) && capacity >= 0 ? { capacity } : {}) };
|
|
889
|
+
}
|
|
890
|
+
|
|
840
891
|
function readCost(value: unknown): CostMeasurement | undefined {
|
|
841
892
|
if (!isRecord(value)) return undefined;
|
|
842
893
|
return {
|
|
@@ -942,10 +993,37 @@ function readRtSafety(value: unknown): RtSafetyMeasurement | undefined {
|
|
|
942
993
|
...optional("locksInRun", finiteNumber(value.locksInRun)),
|
|
943
994
|
...optional("syscallsInRun", finiteNumber(value.syscallsInRun)),
|
|
944
995
|
...optional("variable", booleanValue(value.variable)),
|
|
996
|
+
...optional("noteSweep", readNoteSweep(value.noteSweep)),
|
|
945
997
|
...reasonOf(value),
|
|
946
998
|
};
|
|
947
999
|
}
|
|
948
1000
|
|
|
1001
|
+
/** A note sweep, or `undefined` unless it says whether it was applicable; a count it cannot vouch for is left out. */
|
|
1002
|
+
export function readNoteSweep(value: unknown): NoteSweepMeasurement | undefined {
|
|
1003
|
+
if (!isRecord(value)) return undefined;
|
|
1004
|
+
const applicable = booleanValue(value.applicable);
|
|
1005
|
+
if (applicable === undefined) return undefined;
|
|
1006
|
+
if (!applicable) return { applicable };
|
|
1007
|
+
const dialect = value.dialect === "clap" || value.dialect === "midi" ? value.dialect : undefined;
|
|
1008
|
+
const counted = (v: unknown) => {
|
|
1009
|
+
const n = finiteNumber(v);
|
|
1010
|
+
return n !== undefined && Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
1011
|
+
};
|
|
1012
|
+
return {
|
|
1013
|
+
applicable,
|
|
1014
|
+
...optional("dialect", dialect),
|
|
1015
|
+
...optional("blocks", counted(value.blocks)),
|
|
1016
|
+
...optional("blockFrames", counted(value.blockFrames)),
|
|
1017
|
+
...optional("noteEvents", counted(value.noteEvents)),
|
|
1018
|
+
...optional("processErrors", counted(value.processErrors)),
|
|
1019
|
+
...optional("nonFinite", booleanValue(value.nonFinite)),
|
|
1020
|
+
...optional("interposer", booleanValue(value.interposer)),
|
|
1021
|
+
...optional("allocationsInRun", counted(value.allocationsInRun)),
|
|
1022
|
+
...optional("locksInRun", counted(value.locksInRun)),
|
|
1023
|
+
...optional("syscallsInRun", counted(value.syscallsInRun)),
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
949
1027
|
function readFeatures(value: unknown): FeaturesMeasurement | undefined {
|
|
950
1028
|
if (!isRecord(value)) return undefined;
|
|
951
1029
|
return {
|
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
PluginTopology,
|
|
36
36
|
} from "./format.js";
|
|
37
37
|
import { booleanValue, finiteNumber, isRecord, nonEmptyString, optional, positiveNumber } from "./guards.js";
|
|
38
|
+
import { readVoicesHeld } from "./format.js";
|
|
38
39
|
import type { MeasurementDimension } from "./vocabulary.js";
|
|
39
40
|
|
|
40
41
|
/** One free-text prober failure, and the vocabulary term it means. */
|
|
@@ -55,6 +56,8 @@ export const PROBE_REASON_RULES: readonly ReasonRule[] = [
|
|
|
55
56
|
{ match: /^no-audio-in$/, code: "no-audio-in" },
|
|
56
57
|
{ match: /^no-audio-out$/, code: "no-audio-out" },
|
|
57
58
|
{ match: /^silent-output$/, code: "silent-output" },
|
|
59
|
+
{ match: /^voices-unknown$/, code: "voices-unknown" },
|
|
60
|
+
{ match: /hosting\.worker\.nonconforming/, code: "worker-nonconforming" },
|
|
58
61
|
{ match: /worker crashed|worker desync|worker unavailable/, code: "probe-crashed" },
|
|
59
62
|
{ match: /timed out|timeout/, code: "probe-timeout" },
|
|
60
63
|
{ match: /non-finite/, code: "non-finite-output" },
|
|
@@ -197,6 +200,7 @@ function readCost(value: unknown): CostMeasurement | undefined {
|
|
|
197
200
|
...optional("nsPerSampleMax", finiteNumber(reading.nsPerSampleMax)),
|
|
198
201
|
...optional("warmupNsPerSampleMedian", finiteNumber(reading.warmupNsPerSampleMedian)),
|
|
199
202
|
...optional("silentOutput", booleanValue(reading.silentOutput)),
|
|
203
|
+
...optional("voices", readVoicesHeld(reading.voices)),
|
|
200
204
|
};
|
|
201
205
|
}
|
|
202
206
|
}
|
|
@@ -29,7 +29,7 @@ import type {
|
|
|
29
29
|
SoakReading,
|
|
30
30
|
StabilityMeasurement,
|
|
31
31
|
} from "./format.js";
|
|
32
|
-
import { readIntegrity, readLifecycle, readSoakAtRate, readThreads } from "./format.js";
|
|
32
|
+
import { readIntegrity, readLifecycle, readNoteSweep, readSoakAtRate, readThreads } from "./format.js";
|
|
33
33
|
import { booleanValue, finiteNumber, isRecord, mapArray, nonEmptyString, optional, positiveNumber } from "./guards.js";
|
|
34
34
|
|
|
35
35
|
/** The sweep's three passes, each a list of parsed JSONL lines. Only `measurements` is required. */
|
|
@@ -106,6 +106,7 @@ function rtSafetyOf(row: Record<string, unknown>): RtSafetyMeasurement {
|
|
|
106
106
|
...optional("locksInRun", finiteNumber(rt.locksInRun)),
|
|
107
107
|
...optional("syscallsInRun", finiteNumber(rt.syscallsInRun)),
|
|
108
108
|
...optional("variable", booleanValue(rt.variable)),
|
|
109
|
+
...optional("noteSweep", readNoteSweep(rt.noteSweep)),
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -85,6 +85,25 @@ export const UNMEASURED_TERMS: readonly UnmeasuredTerm[] = [
|
|
|
85
85
|
"whitespace, a manifest using @base or a percent-encoded IRI, or a CLAP plugin with more parameters than the " +
|
|
86
86
|
"host accepts. The plugin stays unpinned, never partly pinned.",
|
|
87
87
|
},
|
|
88
|
+
{
|
|
89
|
+
code: "no-voices",
|
|
90
|
+
kind: "nothing-to-measure",
|
|
91
|
+
description:
|
|
92
|
+
"An instrument whose voice-info declares a voice capacity of 0: there is no voice to hold down, so its " +
|
|
93
|
+
"cost with notes held cannot be taken.",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
code: "notes-unplayable",
|
|
97
|
+
kind: "nothing-to-measure",
|
|
98
|
+
description: "An instrument whose note input reads no dialect the prober speaks (CLAP's or MIDI's): it cannot be played.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
code: "voices-unknown",
|
|
102
|
+
kind: "not-attempted",
|
|
103
|
+
description:
|
|
104
|
+
"An instrument that declares no voice capacity, measured by a run given no probe count (`--probe-notes`): " +
|
|
105
|
+
"the run did not know how many notes to hold, so it timed none.",
|
|
106
|
+
},
|
|
88
107
|
{
|
|
89
108
|
code: "refused-instantiation",
|
|
90
109
|
kind: "probe-failed",
|
|
@@ -112,6 +131,13 @@ export const UNMEASURED_TERMS: readonly UnmeasuredTerm[] = [
|
|
|
112
131
|
"The URI was not present in this machine's LV2 world. A fact about this installation, " +
|
|
113
132
|
"not about the plugin — another host may well have it.",
|
|
114
133
|
},
|
|
134
|
+
{
|
|
135
|
+
code: "worker-nonconforming",
|
|
136
|
+
kind: "probe-failed",
|
|
137
|
+
description:
|
|
138
|
+
"The plugin's LV2_Worker_Interface leaves work or work_response NULL, which the LV2 worker " +
|
|
139
|
+
"specification does not allow: it was refused before any call, not measured.",
|
|
140
|
+
},
|
|
115
141
|
{
|
|
116
142
|
code: "probe-error",
|
|
117
143
|
kind: "probe-failed",
|
|
@@ -310,3 +336,15 @@ export type HostIsolation = (typeof HOST_ISOLATIONS)[number];
|
|
|
310
336
|
export const HOST_LATENCY_HANDLING = ["compensated", "uncompensated"] as const;
|
|
311
337
|
export type HostLatencyHandling = (typeof HOST_LATENCY_HANDLING)[number];
|
|
312
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Where the number of notes an instrument's cost was measured with came from: `declared` is the voice
|
|
341
|
+
* capacity the plugin declares (CLAP's voice-info, read after activate at that rate); `probe` is a count
|
|
342
|
+
* the caller gave (`--probe-notes`, the console's `SYNTH_VOICE_PROBE_NOTES`), used when the plugin declares
|
|
343
|
+
* none or when the caller asked for that count whatever it declares.
|
|
344
|
+
*/
|
|
345
|
+
export const VOICE_BASES = ["declared", "probe"] as const;
|
|
346
|
+
export type VoiceBasis = (typeof VOICE_BASES)[number];
|
|
347
|
+
|
|
348
|
+
export function isVoiceBasis(value: unknown): value is VoiceBasis {
|
|
349
|
+
return VOICE_BASES.some((b) => b === value);
|
|
350
|
+
}
|