@openmixer/plugin-qualify 0.17.4 → 0.18.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 +53 -15
- package/bin/plugin-qualify-sync.mjs +1 -1
- package/dist/clap-qualify.d.ts +3 -1
- package/dist/clap-qualify.js +7 -1
- package/dist/clap-qualify.js.map +1 -1
- package/dist/direct-limit.d.ts +27 -0
- package/dist/direct-limit.js +52 -0
- package/dist/direct-limit.js.map +1 -0
- package/dist/host-admission.d.ts +10 -2
- package/dist/host-admission.js +13 -2
- package/dist/host-admission.js.map +1 -1
- package/dist/host-profiles.d.ts +8 -0
- package/dist/host-profiles.js +16 -1
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-codes.generated.d.ts +10 -0
- package/dist/hosting-codes.generated.js +10 -0
- package/dist/hosting-codes.generated.js.map +1 -1
- package/dist/hosting-suitability.d.ts +34 -8
- package/dist/hosting-suitability.js +92 -7
- 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/measurement/format.d.ts +54 -8
- package/dist/measurement/format.js +54 -3
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/plan.d.ts +3 -3
- package/dist/measurement/plan.js +1 -1
- package/dist/measurement-grid.d.ts +8 -0
- package/dist/measurement-grid.js +4 -0
- package/dist/measurement-grid.js.map +1 -1
- package/dist/memory-bound.d.ts +6 -0
- package/dist/memory-bound.js +8 -0
- package/dist/memory-bound.js.map +1 -1
- package/dist/memory-budget.d.ts +39 -0
- package/dist/memory-budget.js +32 -0
- package/dist/memory-budget.js.map +1 -0
- package/dist/probe-judge.js +22 -9
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-merge.js +6 -5
- package/dist/probe-merge.js.map +1 -1
- package/dist/probe-output-fixtures.d.ts +6 -0
- package/dist/probe-output-fixtures.js +8 -0
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +4 -2
- package/dist/probe-output.js +2 -1
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.js +6 -3
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +26 -2
- package/dist/qualification-record.js +17 -18
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-declarations.d.ts +30 -11
- package/dist/qualify-declarations.js +38 -2
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-sync.d.ts +20 -0
- package/dist/qualify-sync.js +101 -10
- package/dist/qualify-sync.js.map +1 -1
- package/package.json +1 -1
- package/src/clap-qualify.ts +8 -1
- package/src/direct-limit.ts +68 -0
- package/src/host-admission.ts +15 -2
- package/src/host-profiles.ts +17 -1
- package/src/hosting-codes.generated.ts +10 -0
- package/src/hosting-suitability.ts +105 -11
- package/src/index.ts +1 -0
- package/src/measurement/format.ts +97 -3
- package/src/measurement/plan.ts +3 -3
- package/src/measurement-grid.ts +5 -0
- package/src/memory-bound.ts +10 -0
- package/src/memory-budget.ts +55 -0
- package/src/probe-judge.ts +21 -8
- package/src/probe-merge.ts +6 -5
- package/src/probe-output-fixtures.ts +10 -1
- package/src/probe-output.ts +5 -2
- package/src/probe-runner.ts +5 -3
- package/src/qualification-record.ts +22 -4
- package/src/qualify-declarations.ts +49 -3
- package/src/qualify-sync.ts +103 -9
- package/tools/_declarations.py +1 -0
- package/tools/benchmark.py +25 -2
- package/tools/gen-python-declarations.mjs +1 -0
- package/tools/lv2-measure.mjs +7 -4
- package/tools/lv2_presets.py +1 -1
- package/tools/mod-host-admit.mjs +2 -1
package/src/qualify-sync.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { createHash } from "node:crypto";
|
|
|
21
21
|
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { createConnection } from "node:net";
|
|
23
23
|
import { homedir, tmpdir } from "node:os";
|
|
24
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
24
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
25
25
|
|
|
26
26
|
import { clapQualifyTools } from "./clap-qualify.js";
|
|
27
27
|
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, matchDistributed, readBoardModel, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
|
|
@@ -126,6 +126,8 @@ export interface SyncArgs {
|
|
|
126
126
|
readonly force: boolean;
|
|
127
127
|
readonly noDistributed: boolean;
|
|
128
128
|
readonly noWait: boolean;
|
|
129
|
+
/** `--refresh-pins`: refresh the pins' files from plugin-hostd and measure nothing (the package's upgrade runs it). */
|
|
130
|
+
readonly refreshPins: boolean;
|
|
129
131
|
readonly help: boolean;
|
|
130
132
|
/** The settings given as flags, by key. */
|
|
131
133
|
readonly values: ReadonlyMap<string, string>;
|
|
@@ -141,6 +143,7 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
141
143
|
let force = false;
|
|
142
144
|
let noDistributed = false;
|
|
143
145
|
let noWait = false;
|
|
146
|
+
let refreshPinsOnly = false;
|
|
144
147
|
let help = false;
|
|
145
148
|
for (let i = 0; i < argv.length; i++) {
|
|
146
149
|
const a = argv[i]!;
|
|
@@ -148,6 +151,7 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
148
151
|
else if (a === "--force") force = true;
|
|
149
152
|
else if (a === "--no-distributed") noDistributed = true;
|
|
150
153
|
else if (a === "--no-wait") noWait = true;
|
|
154
|
+
else if (a === "--refresh-pins") refreshPinsOnly = true;
|
|
151
155
|
else if (a === "--help") help = true;
|
|
152
156
|
else if (a === "--grid-hosts") values.set("grid", "hosts");
|
|
153
157
|
else if (a === "--config" || byFlag.has(a)) {
|
|
@@ -157,7 +161,7 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
157
161
|
else values.set(byFlag.get(a)!, v);
|
|
158
162
|
} else return { ok: false, error: `unknown argument ${a}` };
|
|
159
163
|
}
|
|
160
|
-
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, noWait, help, values } };
|
|
164
|
+
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, noWait, refreshPins: refreshPinsOnly, help, values } };
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
export function syncHelpText(): string {
|
|
@@ -170,6 +174,7 @@ export function syncHelpText(): string {
|
|
|
170
174
|
" --force qualify every plugin again, recorded or not; no distributed record is read",
|
|
171
175
|
" --no-wait a run in progress is not waited for: exit 0, and the holder runs once more at its end",
|
|
172
176
|
" --no-distributed read no distributed record: every plugin not recorded is measured",
|
|
177
|
+
" --refresh-pins only bring every pin's files up to the ones plugin-hostd names, measuring nothing",
|
|
173
178
|
" --grid-hosts the fast grid, the same as --grid hosts (7 cells with the shipped hosts)",
|
|
174
179
|
" --help this text",
|
|
175
180
|
...rows,
|
|
@@ -515,6 +520,42 @@ function withHostdFiles(ctx: SyncContext, pin: PinEntry | undefined): PinEntry |
|
|
|
515
520
|
return { ...pin, integrity: { ...pin.integrity, files: r.files } };
|
|
516
521
|
}
|
|
517
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Every pin of the state with the files plugin-hostd collects for its plugin now, measured again by nothing
|
|
525
|
+
* (plugin-qualify#239). A pin written before plugin-qualify 0.17.3 holds the probe's view of an LV2 (binary, plugin
|
|
526
|
+
* TTL, manifest) and not the preset files plugin-hostd 0.15.0 also checks, so its add answers -509 until the plugin
|
|
527
|
+
* is qualified again: hours on a device with hundreds. The new list replaces the old one when no file both lists
|
|
528
|
+
* hold changed its hash, so the verdict stands for the same bytes; a changed file is the sync's to measure again,
|
|
529
|
+
* and that pin is left as it is. A plugin plugin-hostd cannot name the files of keeps its pin.
|
|
530
|
+
*/
|
|
531
|
+
export function refreshPins(ctx: SyncContext, state: SyncState): { state: SyncState; refreshed: string[] } {
|
|
532
|
+
const refreshed: string[] = [];
|
|
533
|
+
const entries: Record<string, SyncEntry> = {};
|
|
534
|
+
for (const [key, entry] of Object.entries(state.entries)) {
|
|
535
|
+
const plugins = entry.plugins.map((plugin) => {
|
|
536
|
+
const pin = plugin.pin;
|
|
537
|
+
if (pin === undefined || pin.integrity.files === undefined) return plugin;
|
|
538
|
+
const r = hostdPinFiles(pin.uri, ctx.settings.lv2Dirs, ctx.env, ...(ctx.pinFilesRun === undefined ? [] : [ctx.pinFilesRun]));
|
|
539
|
+
if (!r.ok) return plugin;
|
|
540
|
+
const old = new Map(pin.integrity.files.map((f) => [f.path, f.sha256]));
|
|
541
|
+
const changed = r.files.filter((f) => old.has(f.path) && old.get(f.path) !== f.sha256).map((f) => f.path);
|
|
542
|
+
const same = r.files.length === old.size && r.files.every((f) => old.get(f.path) === f.sha256);
|
|
543
|
+
if (same) return plugin;
|
|
544
|
+
if (changed.length > 0) {
|
|
545
|
+
ctx.log(`${pin.uri}: ${changed.join(", ")} changed since it was qualified; its pin waits for the sync`);
|
|
546
|
+
return plugin;
|
|
547
|
+
}
|
|
548
|
+
const added = r.files.filter((f) => !old.has(f.path)).map((f) => f.path);
|
|
549
|
+
const gone = [...old.keys()].filter((path) => !r.files.some((f) => f.path === path));
|
|
550
|
+
ctx.log(`${pin.uri}: pin files refreshed${added.length ? `, ${added.join(", ")} added` : ""}${gone.length ? `, ${gone.join(", ")} gone` : ""}`);
|
|
551
|
+
refreshed.push(pin.uri);
|
|
552
|
+
return { ...plugin, pin: { ...pin, integrity: { ...pin.integrity, files: r.files } } };
|
|
553
|
+
});
|
|
554
|
+
entries[key] = refreshed.length === 0 ? entry : { ...entry, plugins };
|
|
555
|
+
}
|
|
556
|
+
return { state: refreshed.length === 0 ? state : { schema: SYNC_STATE_SCHEMA, entries }, refreshed };
|
|
557
|
+
}
|
|
558
|
+
|
|
518
559
|
/** A judged run's outcome for the host: qualified, refused, or why it was not measured. */
|
|
519
560
|
function outcomeOf(judgment: Judgment, host: string): { outcome: SyncOutcome; code?: string } {
|
|
520
561
|
const v = judgment.hosts.find((h) => h.verdict.host === host)?.verdict;
|
|
@@ -941,6 +982,8 @@ export interface SyncSummary {
|
|
|
941
982
|
readonly seconds: number;
|
|
942
983
|
/** The pins a running daemon took, when one answered. */
|
|
943
984
|
readonly sent?: number;
|
|
985
|
+
/** The pins whose files were refreshed from plugin-hostd without a measurement (plugin-qualify#239). */
|
|
986
|
+
readonly refreshed?: number;
|
|
944
987
|
}
|
|
945
988
|
|
|
946
989
|
/**
|
|
@@ -977,7 +1020,7 @@ export function descriptorAccessProblems(dir: string): string[] {
|
|
|
977
1020
|
}
|
|
978
1021
|
|
|
979
1022
|
export function summaryLine(s: SyncSummary): string {
|
|
980
|
-
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.seconds.toFixed(1)} s`;
|
|
1023
|
+
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.seconds.toFixed(1)} s`;
|
|
981
1024
|
}
|
|
982
1025
|
|
|
983
1026
|
/** The exit status `flock -n` returns when the lock is held (-E), told apart from a failure of flock itself. */
|
|
@@ -1021,6 +1064,10 @@ export const pendingMarker = (stateFile: string): string => join(dirname(stateFi
|
|
|
1021
1064
|
|
|
1022
1065
|
export interface SyncRunOptions {
|
|
1023
1066
|
readonly force?: boolean;
|
|
1067
|
+
/** `--refresh-pins`: only refresh the pins' files from plugin-hostd and send the changed ones; measure nothing. */
|
|
1068
|
+
readonly refreshOnly?: boolean;
|
|
1069
|
+
/** The run of `plugin-hostd --pin-files`, a stand-in in tests. */
|
|
1070
|
+
readonly pinFilesRun?: PinFilesRun;
|
|
1024
1071
|
/** False under `--no-wait`: a run in progress is not waited for; the holder runs once more at its end instead. */
|
|
1025
1072
|
readonly wait?: boolean;
|
|
1026
1073
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
@@ -1064,6 +1111,7 @@ export async function runSync(settings: SyncSettings, options: SyncRunOptions =
|
|
|
1064
1111
|
|
|
1065
1112
|
function addSummaries(a: SyncSummary, b: SyncSummary): SyncSummary {
|
|
1066
1113
|
const sent = (a.sent ?? 0) + (b.sent ?? 0);
|
|
1114
|
+
const refreshed = (a.refreshed ?? 0) + (b.refreshed ?? 0);
|
|
1067
1115
|
return {
|
|
1068
1116
|
qualified: a.qualified + b.qualified,
|
|
1069
1117
|
skipped: a.skipped + b.skipped,
|
|
@@ -1074,9 +1122,19 @@ function addSummaries(a: SyncSummary, b: SyncSummary): SyncSummary {
|
|
|
1074
1122
|
distributed: a.distributed + b.distributed,
|
|
1075
1123
|
seconds: a.seconds + b.seconds,
|
|
1076
1124
|
...(sent === 0 ? {} : { sent }),
|
|
1125
|
+
...(refreshed === 0 ? {} : { refreshed }),
|
|
1077
1126
|
};
|
|
1078
1127
|
}
|
|
1079
1128
|
|
|
1129
|
+
/** Whether this run's scan covers an entry: its root lies under one of the run's directories of its own format. */
|
|
1130
|
+
function scannedBy(settings: Pick<SyncSettings, "lv2Dirs" | "clapDirs">, entry: SyncEntry): boolean {
|
|
1131
|
+
const root = resolve(entry.root);
|
|
1132
|
+
return (entry.format === "clap" ? settings.clapDirs : settings.lv2Dirs).some((d) => {
|
|
1133
|
+
const dir = resolve(d);
|
|
1134
|
+
return root === dir || root.startsWith(dir.endsWith(sep) ? dir : dir + sep);
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1080
1138
|
/**
|
|
1081
1139
|
* One run: wait for quiet, qualify what is new or changed, drop what is gone, write the state, the records, the
|
|
1082
1140
|
* descriptors and the pin file, and send the changed pins to a running daemon. The state and the pin file are written
|
|
@@ -1085,12 +1143,19 @@ function addSummaries(a: SyncSummary, b: SyncSummary): SyncSummary {
|
|
|
1085
1143
|
async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promise<SyncSummary> {
|
|
1086
1144
|
const log = options.log ?? (() => {});
|
|
1087
1145
|
const env = options.env ?? process.env;
|
|
1088
|
-
log("sync run begins");
|
|
1146
|
+
log(options.refreshOnly === true ? "pin refresh begins" : "sync run begins");
|
|
1089
1147
|
{
|
|
1090
1148
|
const started = Date.now();
|
|
1091
1149
|
const dirs = [...settings.clapDirs, ...settings.lv2Dirs];
|
|
1150
|
+
const ctx: SyncContext = { settings, env, log, ...(options.pinFilesRun === undefined ? {} : { pinFilesRun: options.pinFilesRun }) };
|
|
1151
|
+
// the pins first, before the quiet wait and any measurement: an upgrade of plugin-hostd that collects more files
|
|
1152
|
+
// than the pins on disk hold refuses those plugins until their pins name them (plugin-qualify#239)
|
|
1153
|
+
const fresh = refreshPins(ctx, readSyncState(settings.stateFile, log));
|
|
1154
|
+
if (fresh.refreshed.length > 0) {
|
|
1155
|
+
writeFileWhole(settings.stateFile, `${JSON.stringify(fresh.state, null, 1)}\n`);
|
|
1156
|
+
}
|
|
1157
|
+
if (options.refreshOnly === true) return sendRefreshed(settings, fresh.state, fresh.refreshed.length, started, log);
|
|
1092
1158
|
await waitQuiet(dirs, settings.settleS, log);
|
|
1093
|
-
const ctx: SyncContext = { settings, env, log };
|
|
1094
1159
|
const count = { qualified: 0, skipped: 0, refused: 0, crashed: 0, hung: 0, unmeasured: 0, distributed: 0 };
|
|
1095
1160
|
const layer = distributedLayerOf(settings, options.force === true, log);
|
|
1096
1161
|
let before: string;
|
|
@@ -1099,7 +1164,7 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
|
|
|
1099
1164
|
} catch {
|
|
1100
1165
|
before = "";
|
|
1101
1166
|
}
|
|
1102
|
-
let state =
|
|
1167
|
+
let state = fresh.state;
|
|
1103
1168
|
const stateBefore = state;
|
|
1104
1169
|
let unreadable = false;
|
|
1105
1170
|
const lv2Pinned: string[] = [];
|
|
@@ -1111,8 +1176,14 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
|
|
|
1111
1176
|
unreadable = problems.length > 0;
|
|
1112
1177
|
const found = new Set(items.map((i) => i.key));
|
|
1113
1178
|
const entries: Record<string, SyncEntry> = {};
|
|
1114
|
-
for (const [key, entry] of Object.entries(state.entries))
|
|
1115
|
-
|
|
1179
|
+
for (const [key, entry] of Object.entries(state.entries)) {
|
|
1180
|
+
if (found.has(key)) entries[key] = entry;
|
|
1181
|
+
// a run scoped to some directories keeps what it did not scan: gone means not found where this run looked
|
|
1182
|
+
else if (!scannedBy(settings, entry)) {
|
|
1183
|
+
entries[key] = entry;
|
|
1184
|
+
if (pass === 0) log(`${key}: not under a directory this run scans; its pin is kept`);
|
|
1185
|
+
} else log(`${key}: gone, its pin is dropped`);
|
|
1186
|
+
}
|
|
1116
1187
|
for (const item of items) {
|
|
1117
1188
|
const recorded = recordedAsIs(item, entries[item.key]);
|
|
1118
1189
|
if (recorded.same && options.force !== true) {
|
|
@@ -1173,8 +1244,31 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
|
|
|
1173
1244
|
const replies = "replies" in sent ? sent.replies : [];
|
|
1174
1245
|
for (let i = 0; i < replies.length; i++) if (replies[i] !== "resp 0") log(`plugin-hostd answered ${replies[i]!} to ${changed[i]!.split(" ")[1]!}`);
|
|
1175
1246
|
const taken = replies.filter((r) => r === "resp 0").length;
|
|
1176
|
-
return {
|
|
1247
|
+
return {
|
|
1248
|
+
...count,
|
|
1249
|
+
seconds: (Date.now() - started) / 1000,
|
|
1250
|
+
...(taken === 0 ? {} : { sent: taken }),
|
|
1251
|
+
...(fresh.refreshed.length === 0 ? {} : { refreshed: fresh.refreshed.length }),
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/** The end of a `--refresh-pins` run: the pin file from the refreshed state, and its changed pins to a running daemon. */
|
|
1257
|
+
async function sendRefreshed(settings: SyncSettings, state: SyncState, refreshed: number, started: number, log: (line: string) => void): Promise<SyncSummary> {
|
|
1258
|
+
let before = "";
|
|
1259
|
+
try {
|
|
1260
|
+
before = readFileSync(settings.pinFile, "utf8");
|
|
1261
|
+
} catch {
|
|
1262
|
+
before = "";
|
|
1177
1263
|
}
|
|
1264
|
+
const after = syncPinsText(state);
|
|
1265
|
+
if (after !== before) writeFileWhole(settings.pinFile, after);
|
|
1266
|
+
const changed = changedPinLines(before, after);
|
|
1267
|
+
const sent = await sendPins(settings.hostdPort, settings.hostdFeedbackPort, changed);
|
|
1268
|
+
const taken = "replies" in sent ? sent.replies.filter((r) => r === "resp 0").length : 0;
|
|
1269
|
+
if ("unsent" in sent && changed.length > 0) log(`the ${String(changed.length)} changed pins apply at plugin-hostd's next start, from the pin file`);
|
|
1270
|
+
const zero = { qualified: 0, skipped: 0, refused: 0, crashed: 0, hung: 0, unmeasured: 0, distributed: 0 };
|
|
1271
|
+
return { ...zero, seconds: (Date.now() - started) / 1000, ...(taken === 0 ? {} : { sent: taken }), ...(refreshed === 0 ? {} : { refreshed }) };
|
|
1178
1272
|
}
|
|
1179
1273
|
|
|
1180
1274
|
/** What a failed descriptor pass says: plainly, once, when the lilv binding is what is missing (a suite without python3-lilv). */
|
package/tools/_declarations.py
CHANGED
|
@@ -17,6 +17,7 @@ OPERATIONAL_RATE_FLOOR = 48000
|
|
|
17
17
|
QUALIFY_RATES = (44100, 48000, 96000, 192000)
|
|
18
18
|
QUALIFY_QUANTA = (128, 256, 512, 1024)
|
|
19
19
|
PLUGIN_TIMEOUT_S = 15
|
|
20
|
+
ADDRESS_SPACE_LIMIT_RAM_FRACTION = 1
|
|
20
21
|
BOOT_TIMEOUT_S = 120
|
|
21
22
|
COST_TIMEOUT_S = 60
|
|
22
23
|
DESCRIPTOR_DIR_KEY = "descriptor_dir"
|
package/tools/benchmark.py
CHANGED
|
@@ -42,6 +42,7 @@ Usage:
|
|
|
42
42
|
import argparse
|
|
43
43
|
import json
|
|
44
44
|
import os
|
|
45
|
+
import resource
|
|
45
46
|
import select
|
|
46
47
|
import shlex
|
|
47
48
|
import subprocess
|
|
@@ -53,10 +54,13 @@ from collections import namedtuple
|
|
|
53
54
|
# declarations (with the rate list and timeouts), generated from `src/qualify-declarations.ts` because this script cannot import
|
|
54
55
|
# a TS module (2026-09-17 declare-derive audit, F8) — see `_declarations.py`'s own header for the
|
|
55
56
|
# regenerate/verify commands.
|
|
56
|
-
from _declarations import (BOOT_TIMEOUT_S, COST_REFERENCE_RATE, COST_TIMEOUT_S,
|
|
57
|
+
from _declarations import (ADDRESS_SPACE_LIMIT_RAM_FRACTION, BOOT_TIMEOUT_S, COST_REFERENCE_RATE, COST_TIMEOUT_S,
|
|
57
58
|
MEASURE_BASE_RATE, OPERATIONAL_RATE_FLOOR, PLUGIN_TIMEOUT_S,
|
|
58
59
|
QUALIFY_RATES, QUANTUM_FRAMES)
|
|
59
60
|
|
|
61
|
+
# the outcome of a worker past its address space, the same word as a direct spawn of plugin-qualify's (src/direct-limit.ts)
|
|
62
|
+
MEMORY_LIMIT_EXCEEDED = "memory-limit-exceeded"
|
|
63
|
+
|
|
60
64
|
try: # numpy is only needed by the worker (and as a fast path in the pure helpers)
|
|
61
65
|
import numpy as _np
|
|
62
66
|
except ImportError: # pragma: no cover - CI runs the pure tests without numpy
|
|
@@ -172,7 +176,7 @@ COST_TONE_AMP = 0.5
|
|
|
172
176
|
# whose tail is genuinely fat — the one that xruns mid-show — while being a stable
|
|
173
177
|
# statistic at this sample count.
|
|
174
178
|
COST_PERCENTILE = 95.0
|
|
175
|
-
# Cost is anchored at the rig's canonical rate: the headline figure
|
|
179
|
+
# Cost is anchored at the rig's canonical rate: the headline figure asked for is
|
|
176
180
|
# "how many of these fit in one core at the rig's rate". `COST_REFERENCE_RATE` is imported above
|
|
177
181
|
# from `_declarations.py` — the same fact `cpu-cost.ts` declares, not a second 96000 literal.
|
|
178
182
|
# A cost probe runs many more blocks than a latency probe; give it room.
|
|
@@ -1908,6 +1912,8 @@ def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
|
1908
1912
|
host, uri, declared, res["measuredFrames"], job["sweep"])
|
|
1909
1913
|
if swept:
|
|
1910
1914
|
res["sweep"] = swept
|
|
1915
|
+
except MemoryError:
|
|
1916
|
+
res = {"error": MEMORY_LIMIT_EXCEEDED}
|
|
1911
1917
|
except Exception as e: # noqa: BLE001 - report, don't die
|
|
1912
1918
|
res = {"error": f"probe error: {e}"}
|
|
1913
1919
|
if "measuredFrames" in res:
|
|
@@ -1924,6 +1930,8 @@ def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
|
1924
1930
|
symbol = arg
|
|
1925
1931
|
try:
|
|
1926
1932
|
frames, reason = probe_one(host, uri, symbol)
|
|
1933
|
+
except MemoryError:
|
|
1934
|
+
frames, reason = None, MEMORY_LIMIT_EXCEEDED
|
|
1927
1935
|
except Exception as e: # noqa: BLE001 - report, don't die, on python-level errors
|
|
1928
1936
|
frames, reason = None, f"probe error: {e}"
|
|
1929
1937
|
if frames is None:
|
|
@@ -1935,6 +1943,20 @@ def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
|
1935
1943
|
# --- the parent: batch loop with crash containment -----------------------------
|
|
1936
1944
|
|
|
1937
1945
|
|
|
1946
|
+
def address_space_limit(ram_bytes=None):
|
|
1947
|
+
"""What the worker runs under: RLIMIT_AS at ADDRESS_SPACE_LIMIT_RAM_FRACTION of the machine's RAM, since a direct
|
|
1948
|
+
spawn has no runner to give it a cgroup; set between fork and exec. A plugin past it fails its allocation, which the
|
|
1949
|
+
worker reports as `MEMORY_LIMIT_EXCEEDED` when Python sees it and which crash containment takes when the plugin dies."""
|
|
1950
|
+
if ram_bytes is None:
|
|
1951
|
+
ram_bytes = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
|
|
1952
|
+
limit = int(ram_bytes * ADDRESS_SPACE_LIMIT_RAM_FRACTION)
|
|
1953
|
+
|
|
1954
|
+
def apply():
|
|
1955
|
+
resource.setrlimit(resource.RLIMIT_AS, (limit, limit))
|
|
1956
|
+
|
|
1957
|
+
return apply
|
|
1958
|
+
|
|
1959
|
+
|
|
1938
1960
|
class _Worker:
|
|
1939
1961
|
"""Handle on one worker subprocess: spawn, job/reply exchange, kill."""
|
|
1940
1962
|
|
|
@@ -1942,6 +1964,7 @@ class _Worker:
|
|
|
1942
1964
|
self.proc = subprocess.Popen(
|
|
1943
1965
|
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
1944
1966
|
stderr=subprocess.DEVNULL, text=True, bufsize=1,
|
|
1967
|
+
preexec_fn=address_space_limit(),
|
|
1945
1968
|
)
|
|
1946
1969
|
ready = self._read_line(boot_timeout)
|
|
1947
1970
|
parsed = parse_worker_line(ready)
|
package/tools/lv2-measure.mjs
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
urisFromScan,
|
|
61
61
|
} from "../dist/measurement/index.js";
|
|
62
62
|
import { LV2_HOSTING_TIMEOUT_S, LV2_MEASURE_TOOL_NAME, RT_INTERPOSE_NAME } from "../dist/qualify-declarations.js";
|
|
63
|
+
import { addressSpaceLimited, directOutcome } from "../dist/direct-limit.js";
|
|
63
64
|
import { halfPopulatedRefusal, locateNativeTools } from "../dist/tool-paths.js";
|
|
64
65
|
|
|
65
66
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -135,9 +136,10 @@ function runHosting(step, uris, onProgress) {
|
|
|
135
136
|
const lines = [];
|
|
136
137
|
onProgress(step, { kind: "phase", plugins: uris.length, rate: Number(step.args[0]) });
|
|
137
138
|
for (const uri of uris) {
|
|
138
|
-
const
|
|
139
|
+
const limited = addressSpaceLimited(step.command, [uri, ...step.args]);
|
|
140
|
+
const r = spawnSync(limited.command, [...limited.args], {
|
|
139
141
|
encoding: "utf8",
|
|
140
|
-
env: { ...process.env, ...step.env },
|
|
142
|
+
env: { ...process.env, ...step.env, ...limited.env },
|
|
141
143
|
stdio: ["ignore", "pipe", "inherit"],
|
|
142
144
|
timeout: LV2_HOSTING_TIMEOUT_S * 1000,
|
|
143
145
|
maxBuffer: 16 << 20,
|
|
@@ -149,9 +151,10 @@ function runHosting(step, uris, onProgress) {
|
|
|
149
151
|
...(r.error?.code === "ETIMEDOUT" ? { timedOut: true } : {}),
|
|
150
152
|
stdout: r.stdout ?? "",
|
|
151
153
|
});
|
|
152
|
-
|
|
154
|
+
const limitHit = directOutcome(r.status, limited);
|
|
155
|
+
process.stderr.write(` ${uri}: exit ${line.exit}${limitHit === undefined ? "" : ` (${limitHit.outcome})`}\n`);
|
|
153
156
|
onProgress(step, { kind: "plugin" });
|
|
154
|
-
lines.push(line);
|
|
157
|
+
lines.push(limitHit === undefined ? line : { ...line, ...limitHit });
|
|
155
158
|
}
|
|
156
159
|
writeFileSync(step.produces, lines.map((l) => `${JSON.stringify(l)}\n`).join(""));
|
|
157
160
|
return lines;
|
package/tools/lv2_presets.py
CHANGED
|
@@ -16,7 +16,7 @@ body, and judges it against the plugin's own descriptor params:
|
|
|
16
16
|
listed — refuse or report, never drop.
|
|
17
17
|
|
|
18
18
|
Only the bundles lilv loads from LV2_PATH are read, which is what the scan reads: the presets a
|
|
19
|
-
plugin ships, never one another host saved in a user's home (
|
|
19
|
+
plugin ships, never one another host saved in a user's home (decision 2). The output is sorted by
|
|
20
20
|
preset URI and the values by symbol, so the same install yields the same bytes.
|
|
21
21
|
"""
|
|
22
22
|
|
package/tools/mod-host-admit.mjs
CHANGED
|
@@ -40,7 +40,8 @@ async function connect() {
|
|
|
40
40
|
async function main() {
|
|
41
41
|
if ((await Promise.race([spawnFailed, sleep(200).then(() => undefined)])) === "ENOENT") return { installed: false, steps: [] };
|
|
42
42
|
const sock = await connect();
|
|
43
|
-
|
|
43
|
+
// the host never listened (omx-clap-host with no JACK server): the plugin was never loaded
|
|
44
|
+
if (sock === undefined) return { installed: true, started: false, steps: [] };
|
|
44
45
|
let buffer = "";
|
|
45
46
|
let closed = false;
|
|
46
47
|
sock.on("data", (d) => (buffer += d.toString()));
|