@openmixer/plugin-qualify 0.18.3 → 0.18.5
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/bin/plugin-qualify-sync.mjs +3 -1
- package/dist/host-admission.d.ts +2 -0
- package/dist/host-admission.js +1 -1
- package/dist/host-admission.js.map +1 -1
- package/dist/jack-registry.d.ts +6 -0
- package/dist/jack-registry.js +52 -0
- package/dist/jack-registry.js.map +1 -0
- package/dist/probe-judge.d.ts +5 -0
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-runner.d.ts +8 -0
- package/dist/probe-runner.js +8 -4
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +21 -0
- package/dist/qualification-record.js +15 -0
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-adapter-pass.js +24 -6
- package/dist/qualify-adapter-pass.js.map +1 -1
- package/dist/qualify-isolated.d.ts +2 -0
- package/dist/qualify-isolated.js +10 -5
- package/dist/qualify-isolated.js.map +1 -1
- package/dist/qualify-sync.d.ts +10 -0
- package/dist/qualify-sync.js +40 -0
- package/dist/qualify-sync.js.map +1 -1
- package/package.json +1 -1
- package/src/host-admission.ts +3 -2
- package/src/jack-registry.ts +49 -0
- package/src/probe-judge.ts +2 -0
- package/src/probe-runner.ts +18 -4
- package/src/qualification-record.ts +33 -0
- package/src/qualify-adapter-pass.ts +21 -6
- package/src/qualify-isolated.ts +12 -4
- package/src/qualify-sync.ts +47 -0
- package/tools/isolated-step.mjs +1 -0
- package/tools/mod-host-admit.mjs +8 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* JACK's table of its servers, /dev/shm/jack-shm-registry: a header, then eight rows of a pid and a name
|
|
5
|
+
* (`jack-<uid>:<server>:`). A server that is killed never clears its row, and once the eight are taken no server starts.
|
|
6
|
+
* The adapter pass starts and stops servers of its own, `omx-qualify-<pid>-<quantum>`; a row of one of those whose
|
|
7
|
+
* process is gone is freed (pid 0, name empty) before the next start, as jackd frees its own row when it ends well.
|
|
8
|
+
* Rows of any other server are never touched, and a file of another layout is left as it is.
|
|
9
|
+
*/
|
|
10
|
+
import { closeSync, existsSync, openSync, readSync, writeSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
export const JACK_REGISTRY = "/dev/shm/jack-shm-registry";
|
|
13
|
+
/** the header jack2 writes before the server rows, and the size of a row: a pid, then the name */
|
|
14
|
+
export const JACK_REGISTRY_HEADER = 24;
|
|
15
|
+
export const JACK_REGISTRY_ENTRY = 264;
|
|
16
|
+
const ROWS = 8;
|
|
17
|
+
const MAGIC = "KCAJ";
|
|
18
|
+
const OWN = ":omx-qualify-";
|
|
19
|
+
|
|
20
|
+
const alive = (pid: number): boolean => existsSync(`/proc/${String(pid)}`);
|
|
21
|
+
|
|
22
|
+
/** Frees the rows of the pass's own servers that are gone; the names freed, in the table's order. */
|
|
23
|
+
export function freeDeadJackSlots(path: string = process.env["OMX_JACK_REGISTRY"] ?? JACK_REGISTRY): string[] {
|
|
24
|
+
const freed: string[] = [];
|
|
25
|
+
let fd: number;
|
|
26
|
+
try {
|
|
27
|
+
fd = openSync(path, "r+");
|
|
28
|
+
} catch {
|
|
29
|
+
return freed;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const head = Buffer.alloc(JACK_REGISTRY_HEADER + ROWS * JACK_REGISTRY_ENTRY);
|
|
33
|
+
if (readSync(fd, head, 0, head.length, 0) !== head.length) return freed;
|
|
34
|
+
// the layout this code knows, or nothing is touched
|
|
35
|
+
if (head.toString("latin1", 0, 4) !== MAGIC || head.readUInt32LE(16) !== JACK_REGISTRY_HEADER + ROWS * JACK_REGISTRY_ENTRY) return freed;
|
|
36
|
+
for (let k = 0; k < ROWS; k++) {
|
|
37
|
+
const at = JACK_REGISTRY_HEADER + k * JACK_REGISTRY_ENTRY;
|
|
38
|
+
const pid = head.readInt32LE(at);
|
|
39
|
+
const name = head.toString("latin1", at + 4, at + JACK_REGISTRY_ENTRY).split("\0")[0] ?? "";
|
|
40
|
+
if (pid <= 0 || !name.includes(OWN) || alive(pid)) continue;
|
|
41
|
+
writeSync(fd, Buffer.alloc(4), 0, 4, at);
|
|
42
|
+
writeSync(fd, Buffer.alloc(1), 0, 1, at + 4);
|
|
43
|
+
freed.push(name);
|
|
44
|
+
}
|
|
45
|
+
} finally {
|
|
46
|
+
closeSync(fd);
|
|
47
|
+
}
|
|
48
|
+
return freed;
|
|
49
|
+
}
|
package/src/probe-judge.ts
CHANGED
|
@@ -197,6 +197,8 @@ export interface HostJudgment {
|
|
|
197
197
|
|
|
198
198
|
export interface Judgment {
|
|
199
199
|
readonly output: ProbeOutput;
|
|
200
|
+
/** How loaded the machine was when the probe started (probe-runner's ProbeLoad); absent when not taken. */
|
|
201
|
+
readonly load?: { readonly loadavg1: number; readonly cores: number };
|
|
200
202
|
/** An LV2 plugin's run under plugin-hostd's LV2 adapter, beside the verdicts (plugin-qualify#223). */
|
|
201
203
|
readonly adapter?: HostAdmission;
|
|
202
204
|
readonly hosts: readonly HostJudgment[];
|
package/src/probe-runner.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
15
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
16
|
+
import { availableParallelism, loadavg, tmpdir } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
19
19
|
import { ClapQualifyError, clapQualifyTools, proberOffer, type RtSafetyDocument } from "./clap-qualify.js";
|
|
@@ -155,11 +155,22 @@ export function lv2RunnerArgs(
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/** One probe output and the bytes it was read from (the bytes are what gets copied beside the record). */
|
|
158
|
+
/** How loaded the machine was when a probe started: the one-minute load average and the cores, so that a crash under a
|
|
159
|
+
* full machine can be told from one on an idle one. */
|
|
160
|
+
export interface ProbeLoad {
|
|
161
|
+
readonly loadavg1: number;
|
|
162
|
+
readonly cores: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
158
165
|
export interface ProbedTarget {
|
|
159
166
|
readonly bytes: Buffer;
|
|
160
167
|
readonly output: ProbeOutput;
|
|
168
|
+
/** The most loaded the machine was when any of the target's runs started. */
|
|
169
|
+
readonly load?: ProbeLoad;
|
|
161
170
|
}
|
|
162
171
|
|
|
172
|
+
const loadNow = (): ProbeLoad => ({ loadavg1: Math.round(loadavg()[0]! * 100) / 100, cores: availableParallelism() });
|
|
173
|
+
|
|
163
174
|
function runOne(target: PluginTarget, options: ProbeTargetOptions, invocation: RunnerInvocation): ProbedTarget {
|
|
164
175
|
const env = options.env ?? process.env;
|
|
165
176
|
const tool = probeRunnerTool(env);
|
|
@@ -193,6 +204,7 @@ function runOne(target: PluginTarget, options: ProbeTargetOptions, invocation: R
|
|
|
193
204
|
}
|
|
194
205
|
// under a bound, the interposer ends the child the moment an allocation is refused, with the status that names it
|
|
195
206
|
if (options.memory !== undefined) runEnv = { ...runEnv, [MEMORY_BOUND_EXIT_ENV]: "1" };
|
|
207
|
+
const load = loadNow();
|
|
196
208
|
const r = (options.run ?? spawnRunner)(tool, args, runEnv);
|
|
197
209
|
if (r.error !== undefined) {
|
|
198
210
|
throw new ProbeRunnerError(`cannot run the probe runner "${tool}" (${r.error.message}): install plugin-hostd or set ${PROBE_RUNNER_ENV} to plugin-hostd-probe`);
|
|
@@ -214,7 +226,7 @@ function runOne(target: PluginTarget, options: ProbeTargetOptions, invocation: R
|
|
|
214
226
|
if (read.output.plugin.id !== expectedId) {
|
|
215
227
|
throw new ProbeRunnerError(`${tool} measured ${read.output.plugin.id} when asked for ${expectedId}: not judged as the plugin asked for`);
|
|
216
228
|
}
|
|
217
|
-
return { bytes, output: read.output };
|
|
229
|
+
return { bytes, output: read.output, load };
|
|
218
230
|
} finally {
|
|
219
231
|
rmSync(work, { recursive: true, force: true });
|
|
220
232
|
}
|
|
@@ -231,8 +243,10 @@ export function targetCells(options: Pick<ProbeTargetOptions, "rates" | "cells">
|
|
|
231
243
|
*/
|
|
232
244
|
export function probeTarget(target: PluginTarget, options: ProbeTargetOptions): ProbedTarget {
|
|
233
245
|
const parts = runnerInvocations(target.format, targetCells(options)).map((invocation) => ({ ...runOne(target, options, invocation), invocation }));
|
|
246
|
+
const loads = parts.flatMap((p) => (p.load === undefined ? [] : [p.load]));
|
|
247
|
+
const load = loads.length === 0 ? undefined : loads.reduce((a, b) => (b.loadavg1 > a.loadavg1 ? b : a));
|
|
234
248
|
try {
|
|
235
|
-
return mergeProbeRuns(parts, options.memory);
|
|
249
|
+
return { ...mergeProbeRuns(parts, options.memory), ...(load === undefined ? {} : { load }) };
|
|
236
250
|
} catch (e) {
|
|
237
251
|
if (e instanceof ProbeMergeError) throw new ProbeRunnerError(e.message);
|
|
238
252
|
throw e;
|
|
@@ -321,7 +335,7 @@ export function qualifyTargets(
|
|
|
321
335
|
for (const { offer, profiles: forOffer } of byOffer.values()) {
|
|
322
336
|
const probed = probeTarget(target, { ...options, ...measured, offer });
|
|
323
337
|
const plain = judgeProbeOutput(probed.output, forOffer, { admissions });
|
|
324
|
-
const judgment = adapter === undefined ?
|
|
338
|
+
const judgment = { ...plain, ...(adapter === undefined ? {} : { adapter }), ...(probed.load === undefined ? {} : { load: probed.load }) };
|
|
325
339
|
targetRuns.push({ bytes: probed.bytes, judgment });
|
|
326
340
|
for (const v of judgmentVerdicts(judgment)) judged.set(v.host, v);
|
|
327
341
|
}
|
|
@@ -72,6 +72,8 @@ export interface IsolatedFigures {
|
|
|
72
72
|
/** The worker's CPU over the window, in percent of one core. */
|
|
73
73
|
readonly cpuPercent?: number;
|
|
74
74
|
readonly xruns?: number;
|
|
75
|
+
/** How many MIDI inputs of the plugin the step's notes were linked to. */
|
|
76
|
+
readonly midiInputsLinked?: number;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** One step of a plugin's pass and how it ended. */
|
|
@@ -341,9 +343,38 @@ export interface QualificationRecord {
|
|
|
341
343
|
readonly machineClass: Readonly<Record<string, string | boolean>>;
|
|
342
344
|
/** Whether the machine met the protocol's optimised profile: a figure that did not is not a reference figure. */
|
|
343
345
|
readonly meetsProtocol: boolean;
|
|
346
|
+
/** Each step whose child did not end well: the signal that killed it or the status it exited with, so a crash is
|
|
347
|
+
* told from the runner's own kill (a deadline or memory kill is SIGKILL). Absent when every child ended well. */
|
|
348
|
+
readonly ends?: readonly ProbeEnd[];
|
|
349
|
+
/** How loaded the machine was when the probe started; absent on a record written before it was taken. */
|
|
350
|
+
readonly load?: { readonly loadavg1: number; readonly cores: number };
|
|
344
351
|
};
|
|
345
352
|
}
|
|
346
353
|
|
|
354
|
+
/** How one step's child ended, when not well. */
|
|
355
|
+
export interface ProbeEnd {
|
|
356
|
+
readonly step: string;
|
|
357
|
+
readonly outcome: string;
|
|
358
|
+
readonly rate?: number;
|
|
359
|
+
readonly quantum?: number;
|
|
360
|
+
readonly signal?: string;
|
|
361
|
+
readonly exitStatus?: number;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The steps of a probe output whose child was killed by a signal or exited with a status other than 0. */
|
|
365
|
+
export function probeEnds(output: ProbeOutput): readonly ProbeEnd[] {
|
|
366
|
+
return output.steps
|
|
367
|
+
.filter((s) => (s.signal !== undefined && s.signal !== null && s.signal !== "") || (s.exitStatus !== undefined && s.exitStatus !== 0))
|
|
368
|
+
.map((s) => ({
|
|
369
|
+
step: s.step,
|
|
370
|
+
outcome: s.outcome,
|
|
371
|
+
...(s.rate === undefined ? {} : { rate: s.rate }),
|
|
372
|
+
...(s.quantum === undefined ? {} : { quantum: s.quantum }),
|
|
373
|
+
...(s.signal ? { signal: s.signal } : {}),
|
|
374
|
+
...(s.exitStatus === undefined || s.exitStatus === 0 ? {} : { exitStatus: s.exitStatus }),
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
347
378
|
/** One dimension a run did not measure, and the code that says so. */
|
|
348
379
|
export interface NotMeasured {
|
|
349
380
|
readonly dimension: string;
|
|
@@ -429,6 +460,8 @@ export function buildQualificationRecord(
|
|
|
429
460
|
complete: o.run.complete,
|
|
430
461
|
machineClass: o.machine.class,
|
|
431
462
|
meetsProtocol: o.machine.profile.meetsProtocol,
|
|
463
|
+
...(probeEnds(o).length === 0 ? {} : { ends: probeEnds(o) }),
|
|
464
|
+
...(judgment.load === undefined ? {} : { load: judgment.load }),
|
|
432
465
|
},
|
|
433
466
|
};
|
|
434
467
|
}
|
|
@@ -17,8 +17,9 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node
|
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { basename, join } from "node:path";
|
|
19
19
|
|
|
20
|
-
import { ADAPTER_HOST, SPAWNED_ADMISSIONS, admitUnderAdapter, type AdmissionRuns, type HostAdmission } from "./host-admission.js";
|
|
20
|
+
import { ADAPTER_HOST, ADAPTER_SILENT_CODE, SPAWNED_ADMISSIONS, admitUnderAdapter, type AdmissionRuns, type HostAdmission } from "./host-admission.js";
|
|
21
21
|
import { HOSTD_ENV } from "./hostd-pin-files.js";
|
|
22
|
+
import { freeDeadJackSlots } from "./jack-registry.js";
|
|
22
23
|
import { cellKey, type GridCell } from "./measurement-grid.js";
|
|
23
24
|
import { defaultAddressSpaceLimit } from "./direct-limit.js";
|
|
24
25
|
import { PROBE_OK } from "./probe-output.js";
|
|
@@ -36,7 +37,9 @@ export const NO_PRIVATE_SERVER = `${ADAPTER_HOST}.no-private-server`;
|
|
|
36
37
|
const RECORD_SUFFIX = ".qualification.json";
|
|
37
38
|
const SERVER_WAIT_MS = 5000;
|
|
38
39
|
const SERVER_SETTLE_MS = 300;
|
|
39
|
-
|
|
40
|
+
// a TERM is waited for this long: jackd on a Pi can take seconds to end, and a jackd that is KILLed keeps its row in
|
|
41
|
+
// JACK's server table for good (jack-registry.ts)
|
|
42
|
+
const SERVER_STOP_MS = 10000;
|
|
40
43
|
|
|
41
44
|
/** What a program answers to `--version`: its last word on the line that says `version`. */
|
|
42
45
|
export function parseVersion(text: string): string | undefined {
|
|
@@ -78,14 +81,18 @@ const sleepSync = (ms: number): void => {
|
|
|
78
81
|
/**
|
|
79
82
|
* jackd under a shell that waits for it. A pass is synchronous, so this process cannot reap a child; an unreaped jackd
|
|
80
83
|
* that had a client stays in JACK's server table as alive, and the next start under its name reads "server already active".
|
|
81
|
-
* The shell outlives jackd whatever happens: a TERM
|
|
82
|
-
* Killed with its group, the shell could not reap it.
|
|
84
|
+
* The shell outlives jackd whatever happens: a TERM to the shell is passed to jackd's own pid, and HUP is how a stop that
|
|
85
|
+
* gives up kills jackd alone. Killed with its group, the shell could not reap it. A TERM to the group does not do: a jackd
|
|
86
|
+
* that is not its group's leader keeps running after it (measured on a Raspberry Pi 5 and on CI), and the stop's KILL then
|
|
87
|
+
* left its row in JACK's registry.
|
|
83
88
|
*/
|
|
84
|
-
const REAPING_JACKD = `trap
|
|
89
|
+
const REAPING_JACKD = `trap 'kill -TERM $p 2>/dev/null' TERM; trap 'kill -KILL $p 2>/dev/null' HUP; jackd "$@" & p=$!; while kill -0 $p 2>/dev/null; do wait $p; done`;
|
|
85
90
|
|
|
86
91
|
/** `jackd` on a name of its own and the dummy driver, at the cell's rate and quantum; `undefined` when none came up. */
|
|
87
92
|
export const startPrivateJack: JackStarter = (cell) => {
|
|
88
93
|
const name = `omx-qualify-${String(process.pid)}-${String(cell.quantum)}`;
|
|
94
|
+
// the rows of servers of ours that were killed, freed before this one asks for a row
|
|
95
|
+
freeDeadJackSlots();
|
|
89
96
|
let child: ChildProcess;
|
|
90
97
|
let ended = false;
|
|
91
98
|
try {
|
|
@@ -110,7 +117,11 @@ export const startPrivateJack: JackStarter = (cell) => {
|
|
|
110
117
|
}
|
|
111
118
|
};
|
|
112
119
|
const stop = (): void => {
|
|
113
|
-
|
|
120
|
+
try {
|
|
121
|
+
if (child.pid !== undefined) process.kill(child.pid, "SIGTERM");
|
|
122
|
+
} catch {
|
|
123
|
+
// already gone
|
|
124
|
+
}
|
|
114
125
|
const until = Date.now() + SERVER_STOP_MS;
|
|
115
126
|
while (running() && Date.now() < until) sleepSync(20);
|
|
116
127
|
if (running()) {
|
|
@@ -124,6 +135,8 @@ export const startPrivateJack: JackStarter = (cell) => {
|
|
|
124
135
|
}
|
|
125
136
|
signal("SIGKILL");
|
|
126
137
|
for (const d of serverDirs(name)) rmSync(d, { recursive: true, force: true });
|
|
138
|
+
// a jackd that had to be KILLed left its row: it is freed now, not by the next start
|
|
139
|
+
freeDeadJackSlots();
|
|
127
140
|
};
|
|
128
141
|
const until = Date.now() + SERVER_WAIT_MS;
|
|
129
142
|
while (Date.now() < until) {
|
|
@@ -187,6 +200,8 @@ export const mergeAdapterRun = (record: QualificationRecord, block: AdapterBlock
|
|
|
187
200
|
export function adapterFinished(record: QualificationRecord, versions: Readonly<Record<string, string>>, cells: readonly GridCell[] = ADAPTER_PASS_CELLS): boolean {
|
|
188
201
|
const a = record.adapter;
|
|
189
202
|
if (a === undefined || !a.measured || a.versions === undefined || a.cells === undefined) return false;
|
|
203
|
+
// silence under the one key the run once played says nothing of a plugin that maps others: it is run again
|
|
204
|
+
if (a.cells.some((c) => c.code === ADAPTER_SILENT_CODE && c.params["notes"] === undefined)) return false;
|
|
190
205
|
const same = Object.keys(versions).length === Object.keys(a.versions).length && Object.entries(versions).every(([k, v]) => a.versions![k] === v);
|
|
191
206
|
return same && cells.every((c) => a.cells!.some((x) => cellKey(x) === cellKey(c)));
|
|
192
207
|
}
|
package/src/qualify-isolated.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface IsolatedRunFacts {
|
|
|
59
59
|
readonly xruns?: number;
|
|
60
60
|
readonly finite?: boolean;
|
|
61
61
|
readonly silent?: boolean;
|
|
62
|
+
/** How many of the plugin's MIDI inputs the notes were linked to; none for a plugin that has none. */
|
|
63
|
+
readonly midiInputsLinked?: number;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export type IsolatedRunner = (uri: string, cell: GridCell, env: Readonly<Record<string, string | undefined>>) => IsolatedRunFacts;
|
|
@@ -78,16 +80,20 @@ export function judgeIsolatedRun(run: IsolatedRunFacts): RunStep {
|
|
|
78
80
|
if ((run.workers ?? 0) < 1) return step(ISOLATED_RESULT.refused, ISOLATED_CODE.noWorker);
|
|
79
81
|
if (run.workers !== 1 || run.instancesInWorker !== 1) return step(ISOLATED_RESULT.refused, ISOLATED_CODE.notAlone);
|
|
80
82
|
if (run.finite !== true) return step(ISOLATED_RESULT.refused, ISOLATED_CODE.nonFinite);
|
|
81
|
-
if (run.silent !== false)
|
|
83
|
+
if (run.silent !== false) {
|
|
84
|
+
const linked = run.midiInputsLinked;
|
|
85
|
+
return step(ISOLATED_RESULT.refused, ISOLATED_CODE.silent, linked === undefined ? undefined : linked === 0 ? "no note input to link: a sine played at its audio inputs" : `notes held on ${String(linked)} MIDI input${linked === 1 ? "" : "s"} and a sine at its audio inputs`);
|
|
86
|
+
}
|
|
82
87
|
return step(ISOLATED_RESULT.isolated);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
function figuresOf(run: IsolatedRunFacts): IsolatedFigures | undefined {
|
|
86
|
-
if (run.latencyFrames === undefined && run.cpuPercent === undefined && run.xruns === undefined) return undefined;
|
|
91
|
+
if (run.latencyFrames === undefined && run.cpuPercent === undefined && run.xruns === undefined && run.midiInputsLinked === undefined) return undefined;
|
|
87
92
|
return {
|
|
88
93
|
...(run.latencyFrames === undefined ? {} : { latencyFrames: run.latencyFrames }),
|
|
89
94
|
...(run.cpuPercent === undefined ? {} : { cpuPercent: run.cpuPercent }),
|
|
90
95
|
...(run.xruns === undefined ? {} : { xruns: run.xruns }),
|
|
96
|
+
...(run.midiInputsLinked === undefined ? {} : { midiInputsLinked: run.midiInputsLinked }),
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
99
|
|
|
@@ -191,8 +197,10 @@ function runIdOf(record: QualificationRecord, dir: string): string | undefined {
|
|
|
191
197
|
*/
|
|
192
198
|
export function completeIsolated(path: string, record: QualificationRecord, uri: string, cell: GridCell, runner: IsolatedRunner, env: Readonly<Record<string, string | undefined>>): { readonly record: QualificationRecord; readonly step: RunStep } | undefined {
|
|
193
199
|
const isStep = (s: RunStep): boolean => s.step === RUN_STEP.isolated;
|
|
194
|
-
// a step that could not run decided nothing,
|
|
195
|
-
|
|
200
|
+
// a step that could not run decided nothing, and neither did silence before the notes were played where a plugin could
|
|
201
|
+
// hear them: a step of that tool does not say how many inputs it linked. A pass tries these again, and it takes their place
|
|
202
|
+
const decided = (s: RunStep): boolean => s.result !== ISOLATED_RESULT.notRun && !(s.code === ISOLATED_CODE.silent && s.figures?.midiInputsLinked === undefined);
|
|
203
|
+
if (!owesIsolated(record) || record.run?.steps.some((s) => isStep(s) && decided(s)) === true) return undefined;
|
|
196
204
|
const id = runIdOf(record, join(path, ".."));
|
|
197
205
|
if (id === undefined) return undefined;
|
|
198
206
|
const step = judgeIsolatedRun(runner(uri, cell, env));
|
package/src/qualify-sync.ts
CHANGED
|
@@ -1218,6 +1218,53 @@ export function descriptorAccessProblems(dir: string): string[] {
|
|
|
1218
1218
|
return problems;
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
|
+
/** What the records say per plugin, not per record: a CLAP plugin has a record per offer (.default, the console's
|
|
1222
|
+
* in-process tier, and .custom, omx-clap-host's isolated tier), and is offered when any of its records is. */
|
|
1223
|
+
export interface PluginCounts {
|
|
1224
|
+
readonly plugins: Readonly<Record<string, number>>;
|
|
1225
|
+
readonly offered: Readonly<Record<string, number>>;
|
|
1226
|
+
readonly notOffered: Readonly<Record<string, number>>;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
export function pluginCounts(qualificationDir: string): PluginCounts {
|
|
1230
|
+
const offeredBy = new Map<string, boolean>();
|
|
1231
|
+
let files: string[] = [];
|
|
1232
|
+
try {
|
|
1233
|
+
files = readdirSync(qualificationDir).filter((f) => f.endsWith(".qualification.json"));
|
|
1234
|
+
} catch {
|
|
1235
|
+
// no directory, no record
|
|
1236
|
+
}
|
|
1237
|
+
for (const f of files) {
|
|
1238
|
+
let r: { plugin?: { format?: string; id?: string }; tiers?: { offeredOn?: readonly string[] } };
|
|
1239
|
+
try {
|
|
1240
|
+
r = JSON.parse(readFileSync(join(qualificationDir, f), "utf8"));
|
|
1241
|
+
} catch {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (!r.plugin?.format || !r.plugin.id) continue;
|
|
1245
|
+
const key = `${r.plugin.format}\0${r.plugin.id}`;
|
|
1246
|
+
offeredBy.set(key, (offeredBy.get(key) ?? false) || (r.tiers?.offeredOn?.length ?? 0) > 0);
|
|
1247
|
+
}
|
|
1248
|
+
const plugins: Record<string, number> = {}, offered: Record<string, number> = {}, notOffered: Record<string, number> = {};
|
|
1249
|
+
for (const [key, isOffered] of offeredBy) {
|
|
1250
|
+
const format = key.split("\0")[0]!;
|
|
1251
|
+
plugins[format] = (plugins[format] ?? 0) + 1;
|
|
1252
|
+
const into = isOffered ? offered : notOffered;
|
|
1253
|
+
into[format] = (into[format] ?? 0) + 1;
|
|
1254
|
+
}
|
|
1255
|
+
return { plugins, offered, notOffered };
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** The per-plugin counts as one line: `plugins 1577 (clap 248, lv2 1329), offered 1399 (...), not offered 178 (...)`. */
|
|
1259
|
+
export function pluginCountsLine(c: PluginCounts): string {
|
|
1260
|
+
const part = (m: Readonly<Record<string, number>>): string => {
|
|
1261
|
+
const keys = Object.keys(m).sort();
|
|
1262
|
+
const total = keys.reduce((n, k) => n + m[k]!, 0);
|
|
1263
|
+
return `${String(total)}${keys.length === 0 ? "" : ` (${keys.map((k) => `${k} ${String(m[k])}`).join(", ")})`}`;
|
|
1264
|
+
};
|
|
1265
|
+
return `plugins ${part(c.plugins)}, offered ${part(c.offered)}, not offered ${part(c.notOffered)}`;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1221
1268
|
export function summaryLine(s: SyncSummary): string {
|
|
1222
1269
|
return `qualified ${String(s.qualified)}, skipped ${String(s.skipped)}, refused ${String(s.refused)}, crashed ${String(s.crashed)}, hung ${String(s.hung)}, unmeasured ${String(s.unmeasured)}, distributed ${String(s.distributed)}${s.refreshed ? `, pins refreshed ${String(s.refreshed)}` : ""}${s.rejudge === undefined ? "" : `, rejudged ${String(s.rejudge.changed)}, unchanged ${String(s.rejudge.unchanged)}, owing ${String(s.rejudge.owing)}`}${s.memoryOnly === undefined ? "" : `, memory measured ${String(s.memoryOnly.measured)}, already ${String(s.memoryOnly.already)}, failed ${String(s.memoryOnly.failed)}, leaks not measured`}${s.adapterOnly === undefined ? "" : `, adapter run ${String(s.adapterOnly.measured)}, already ${String(s.adapterOnly.already)}, failed ${String(s.adapterOnly.failed)}`}, ${s.seconds.toFixed(1)} s`;
|
|
1223
1270
|
}
|
package/tools/isolated-step.mjs
CHANGED
|
@@ -107,6 +107,7 @@ async function main() {
|
|
|
107
107
|
xruns: heard.xruns,
|
|
108
108
|
finite: heard.finite,
|
|
109
109
|
silent: heard.silent,
|
|
110
|
+
midiInputsLinked: heard.midiInputsLinked,
|
|
110
111
|
...(ticksBefore === undefined || ticksAfter === undefined ? {} : { cpuPercent: ((ticksAfter - ticksBefore) * 100000) / CLOCK_TICKS / windowMs }),
|
|
111
112
|
};
|
|
112
113
|
}
|
package/tools/mod-host-admit.mjs
CHANGED
|
@@ -18,9 +18,12 @@ const STEP_MS = Number(process.env.OMX_ADMIT_STEP_MS ?? 0) || 10000;
|
|
|
18
18
|
const ADDRESS_KB = process.env.OMX_ADMIT_ADDRESS_KB;
|
|
19
19
|
const STATE_DIR = process.env.OMX_ADMIT_STATE_DIR;
|
|
20
20
|
const INSTANCE = 0;
|
|
21
|
-
// an instrument under the adapter is played:
|
|
21
|
+
// an instrument under the adapter is played: a wide range of keys, since a drum kit answers only the ones it maps, each
|
|
22
|
+
// held, then this long; its output is read at its JACK ports from before the first note, so a short hit counts
|
|
22
23
|
const NOTE_WINDOW_MS = 1000;
|
|
23
|
-
const
|
|
24
|
+
const NOTE_FIRST = 36; // the same keys as native/isolated-signal.c plays
|
|
25
|
+
const NOTE_COUNT = 30;
|
|
26
|
+
const NOTE_VELOCITY = 100;
|
|
24
27
|
const ADAPTER = "omx-clap-host";
|
|
25
28
|
const COMMANDS = [
|
|
26
29
|
["add", `add ${uri} ${INSTANCE}`],
|
|
@@ -133,9 +136,11 @@ async function main() {
|
|
|
133
136
|
ports.length === 0
|
|
134
137
|
? { problem: "no output port" }
|
|
135
138
|
: await peakDuring(ports, async () => {
|
|
136
|
-
|
|
139
|
+
// a host that stops answering costs one budget, not one for each key
|
|
140
|
+
for (let k = 0; k < NOTE_COUNT; k++) if ((await ask(`note_on ${INSTANCE} 0 ${NOTE_FIRST + k} ${NOTE_VELOCITY}`)) === undefined) break;
|
|
137
141
|
await sleep(NOTE_WINDOW_MS);
|
|
138
142
|
});
|
|
143
|
+
if (sound.peak !== undefined) sound.notes = NOTE_COUNT;
|
|
139
144
|
}
|
|
140
145
|
buffer = "";
|
|
141
146
|
sock.write(`${line}\0`);
|