@openmixer/plugin-qualify 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -6
- 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/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 +18 -1
- package/dist/qualify-sync.js +142 -12
- 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/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 +147 -11
- package/src/records-package.ts +333 -0
- package/tools/{ai-trace-commit-check.sh → commit-message-check.sh} +16 -16
- package/tools/{ai-trace-patterns.json → commit-message-patterns.json} +1 -1
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" },
|
package/src/qualify-sync.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
24
24
|
import { basename, dirname, join, resolve } from "node:path";
|
|
25
25
|
|
|
26
26
|
import { clapQualifyTools } from "./clap-qualify.js";
|
|
27
|
+
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, matchDistributed, readBoardModel, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
|
|
27
28
|
import { clapSearchPath, parseClapScan } from "./clap-loader.js";
|
|
28
29
|
import { HOSTING_CODE } from "./hosting-codes.js";
|
|
29
30
|
import { shippedHostProfile, type HostProfile } from "./host-profiles.js";
|
|
@@ -31,8 +32,10 @@ import { lv2BundlesOnPath, manifestPlugins } from "./lv2-loader.js";
|
|
|
31
32
|
import { judgmentPinEntries, pinsFile, type PinEntry } from "./pins-file.js";
|
|
32
33
|
import type { Judgment } from "./probe-judge.js";
|
|
33
34
|
import { qualifyTargets } from "./probe-runner.js";
|
|
35
|
+
import { GRID_HOSTS, parseGridSpec, type GridSpec } from "./measurement-grid.js";
|
|
36
|
+
import { memoryBoundOf, parseMemoryLimit, type MemoryBound } from "./memory-bound.js";
|
|
34
37
|
import { writeQualification, type NotMeasured } from "./qualification-record.js";
|
|
35
|
-
import { DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM } from "./qualify-declarations.js";
|
|
38
|
+
import { DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
36
39
|
import { toolsDir } from "./tool-paths.js";
|
|
37
40
|
import { writeFileWhole } from "./whole-file.js";
|
|
38
41
|
|
|
@@ -64,11 +67,16 @@ export const SYNC_SETTINGS: readonly SyncSetting[] = [
|
|
|
64
67
|
{ key: "qualification_dir", meaning: "where the qualification records go", default: "/var/lib/omx/qualification (system), else $XDG_STATE_HOME/omx/qualification" },
|
|
65
68
|
{ key: "descriptor_dir", meaning: "where the scanned descriptors go", default: `${DESCRIPTOR_DIR_SYSTEM} (system), else $XDG_STATE_HOME/${DESCRIPTOR_DIR_STATE_SUBDIR}` },
|
|
66
69
|
{ key: "state_file", meaning: "what was qualified, from which bytes, and how it ended", default: "/var/lib/omx/qualify-sync.json (system), else $XDG_STATE_HOME/omx/qualify-sync.json" },
|
|
67
|
-
{ key: "budget_s", meaning: "the wall clock one
|
|
70
|
+
{ key: "budget_s", meaning: "the wall clock one run gets (one cell of an LV2 plugin, one quantum of a CLAP one), in seconds; past it the plugin is recorded as hung", default: "60" },
|
|
68
71
|
{ key: "host_profile", meaning: "the host the verdict is for (a shipped profile id)", default: "omx-clap-host" },
|
|
72
|
+
{ key: "grid", meaning: "the cells measured: hosts (the host's and the console tiers' rate/quantum, then 512 at every rate), full, or <rate>/<quantum>,...", default: "hosts" },
|
|
73
|
+
{ key: "cells_per_plugin_budget", meaning: "measure at most the first n cells of the grid a plugin; 0 is every cell", default: "0" },
|
|
74
|
+
{ key: "memory_limit", meaning: "the memory bound of every run: bytes, or with K, M, G; off for none; past it a cell is probe.oom", default: "half of the RAM" },
|
|
69
75
|
{ key: "hostd_port", meaning: "plugin-hostd's command port the new pins are sent to; 0 sends none", default: "5555" },
|
|
70
76
|
{ key: "hostd_feedback_port", meaning: "plugin-hostd's feedback port, connected before the command port; 0 is the command port's next", default: "0" },
|
|
71
77
|
{ key: "settle_s", meaning: "wait until the plugin directories have been quiet this long before a run", default: "0" },
|
|
78
|
+
{ key: "host_class", meaning: "the machine class whose distributed records are read (a directory name)", default: "the board's own from the device tree (rpi5 on a Raspberry Pi 5), else none" },
|
|
79
|
+
{ key: "distributed_dir", meaning: "the directories of distributed records, colon-separated, each holding <host class>/<set>/", default: `${DISTRIBUTED_DIR_SYSTEM} (system), else $XDG_DATA_HOME/omx/qualification and each $XDG_DATA_DIRS/omx/qualification` },
|
|
72
80
|
];
|
|
73
81
|
|
|
74
82
|
export interface SyncSettings {
|
|
@@ -80,9 +88,19 @@ export interface SyncSettings {
|
|
|
80
88
|
readonly stateFile: string;
|
|
81
89
|
readonly budgetS: number;
|
|
82
90
|
readonly hostProfile: HostProfile;
|
|
91
|
+
/** The cells measured, and at most how many a plugin (0: every cell). */
|
|
92
|
+
readonly grid: GridSpec;
|
|
93
|
+
readonly cellsPerPluginBudget: number;
|
|
94
|
+
/** The memory bound of every run; `undefined` is `off`. */
|
|
95
|
+
readonly memory?: MemoryBound;
|
|
83
96
|
readonly hostdPort: number;
|
|
84
97
|
readonly hostdFeedbackPort: number;
|
|
85
98
|
readonly settleS: number;
|
|
99
|
+
/** The machine class the distributed records are read for; none, and the layer is not read. */
|
|
100
|
+
readonly hostClass?: string;
|
|
101
|
+
readonly distributedDirs: readonly string[];
|
|
102
|
+
/** False under `--no-distributed`. */
|
|
103
|
+
readonly distributed: boolean;
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
/** A settings file read: one `key value` per line, `#` starts a comment; a key alone is the empty value. An unknown key is refused by line. */
|
|
@@ -105,6 +123,7 @@ export interface SyncArgs {
|
|
|
105
123
|
readonly config?: string;
|
|
106
124
|
readonly system: boolean;
|
|
107
125
|
readonly force: boolean;
|
|
126
|
+
readonly noDistributed: boolean;
|
|
108
127
|
readonly help: boolean;
|
|
109
128
|
/** The settings given as flags, by key. */
|
|
110
129
|
readonly values: ReadonlyMap<string, string>;
|
|
@@ -118,11 +137,13 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
118
137
|
let config: string | undefined;
|
|
119
138
|
let system = false;
|
|
120
139
|
let force = false;
|
|
140
|
+
let noDistributed = false;
|
|
121
141
|
let help = false;
|
|
122
142
|
for (let i = 0; i < argv.length; i++) {
|
|
123
143
|
const a = argv[i]!;
|
|
124
144
|
if (a === "--system") system = true;
|
|
125
145
|
else if (a === "--force") force = true;
|
|
146
|
+
else if (a === "--no-distributed") noDistributed = true;
|
|
126
147
|
else if (a === "--help") help = true;
|
|
127
148
|
else if (a === "--config" || byFlag.has(a)) {
|
|
128
149
|
const v = argv[++i];
|
|
@@ -131,18 +152,19 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
131
152
|
else values.set(byFlag.get(a)!, v);
|
|
132
153
|
} else return { ok: false, error: `unknown argument ${a}` };
|
|
133
154
|
}
|
|
134
|
-
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, help, values } };
|
|
155
|
+
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, help, values } };
|
|
135
156
|
}
|
|
136
157
|
|
|
137
158
|
export function syncHelpText(): string {
|
|
138
|
-
const rows = SYNC_SETTINGS.map((s) => ` ${`${flagOf(s.key)} <value>`.padEnd(
|
|
159
|
+
const rows = SYNC_SETTINGS.map((s) => ` ${`${flagOf(s.key)} <value>`.padEnd(36)}${s.meaning}${s.default === "" ? "" : ` (default: ${s.default})`}`);
|
|
139
160
|
return [
|
|
140
161
|
"plugin-qualify-sync: qualify every plugin on the search paths once, and again when its files change",
|
|
141
162
|
"",
|
|
142
|
-
` --config <file>
|
|
143
|
-
" --system
|
|
144
|
-
" --force
|
|
145
|
-
" --
|
|
163
|
+
` ${"--config <file>".padEnd(36)}the settings file (default: ${SYNC_CONF}; a missing file is the defaults)`,
|
|
164
|
+
" --system a system service's directories, under /var/lib/omx",
|
|
165
|
+
" --force qualify every plugin again, recorded or not; no distributed record is read",
|
|
166
|
+
" --no-distributed read no distributed record: every plugin not recorded is measured",
|
|
167
|
+
" --help this text",
|
|
146
168
|
...rows,
|
|
147
169
|
"",
|
|
148
170
|
"every setting is also a line of the settings file, its key as the first word",
|
|
@@ -159,6 +181,7 @@ export function resolveSyncSettings(
|
|
|
159
181
|
args: SyncArgs,
|
|
160
182
|
env: Readonly<Record<string, string | undefined>>,
|
|
161
183
|
home: string = homedir(),
|
|
184
|
+
boardModel: () => string | undefined = readBoardModel,
|
|
162
185
|
): { ok: true; settings: SyncSettings } | { ok: false; error: string } {
|
|
163
186
|
const get = (key: string): string | undefined => args.values.get(key) ?? file.get(key);
|
|
164
187
|
const state = env["XDG_STATE_HOME"] !== undefined && env["XDG_STATE_HOME"] !== "" ? env["XDG_STATE_HOME"] : join(home, ".local", "state");
|
|
@@ -182,10 +205,31 @@ export function resolveSyncSettings(
|
|
|
182
205
|
if (hostdPort === undefined || !Number.isInteger(hostdPort) || hostdPort > 65535) return { ok: false, error: "hostd_port must be a port number, or 0" };
|
|
183
206
|
if (feedbackPort === undefined || !Number.isInteger(feedbackPort) || feedbackPort > 65535) return { ok: false, error: "hostd_feedback_port must be a port number, or 0" };
|
|
184
207
|
if (settleS === undefined) return { ok: false, error: "settle_s must be a number of seconds" };
|
|
208
|
+
const gridText = get("grid");
|
|
209
|
+
const grid = gridText === undefined || gridText === "" ? { ok: true as const, spec: GRID_HOSTS } : parseGridSpec(gridText);
|
|
210
|
+
if (!grid.ok) return { ok: false, error: `grid: ${grid.error}` };
|
|
211
|
+
const cellsPerPluginBudget = number("cells_per_plugin_budget", 0);
|
|
212
|
+
if (cellsPerPluginBudget === undefined || !Number.isInteger(cellsPerPluginBudget)) return { ok: false, error: "cells_per_plugin_budget must be a whole number, 0 or more" };
|
|
213
|
+
const memoryText = get("memory_limit");
|
|
214
|
+
const memoryLimit = memoryText === undefined || memoryText === "" ? undefined : parseMemoryLimit(memoryText);
|
|
215
|
+
if (memoryLimit !== undefined && !memoryLimit.ok) return { ok: false, error: `memory_limit: ${memoryLimit.error}` };
|
|
216
|
+
const memory = memoryBoundOf(memoryLimit?.bytes);
|
|
185
217
|
const profileId = get("host_profile") || "omx-clap-host";
|
|
186
218
|
const hostProfile = shippedHostProfile(profileId);
|
|
187
219
|
if (hostProfile === undefined) return { ok: false, error: `host_profile ${profileId} is not a shipped host profile` };
|
|
188
220
|
const once = (dirs: readonly string[]): string[] => [...new Set(dirs.map((d) => resolve(d)))];
|
|
221
|
+
const dataHome = env["XDG_DATA_HOME"] !== undefined && env["XDG_DATA_HOME"] !== "" ? env["XDG_DATA_HOME"] : join(home, ".local", "share");
|
|
222
|
+
const dataDirs = dirList(env["XDG_DATA_DIRS"] ?? "").length > 0 ? dirList(env["XDG_DATA_DIRS"]!) : ["/usr/local/share", "/usr/share"];
|
|
223
|
+
const distributedNamed = get("distributed_dir");
|
|
224
|
+
const distributedDirs =
|
|
225
|
+
distributedNamed !== undefined && distributedNamed !== ""
|
|
226
|
+
? dirList(distributedNamed)
|
|
227
|
+
: args.system
|
|
228
|
+
? [DISTRIBUTED_DIR_SYSTEM]
|
|
229
|
+
: [dataHome, ...dataDirs].map((d) => join(d, "omx", "qualification"));
|
|
230
|
+
const classNamed = get("host_class");
|
|
231
|
+
const hostClass = classNamed !== undefined && classNamed !== "" ? classNamed : hostClassOfBoard(boardModel());
|
|
232
|
+
if (hostClass !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(hostClass)) return { ok: false, error: `host_class ${hostClass} is not a directory name` };
|
|
189
233
|
return {
|
|
190
234
|
ok: true,
|
|
191
235
|
settings: {
|
|
@@ -197,9 +241,15 @@ export function resolveSyncSettings(
|
|
|
197
241
|
stateFile: place("state_file", "/var/lib/omx/qualify-sync.json", join("omx", "qualify-sync.json")),
|
|
198
242
|
budgetS,
|
|
199
243
|
hostProfile,
|
|
244
|
+
grid: grid.spec,
|
|
245
|
+
cellsPerPluginBudget,
|
|
246
|
+
...(memory === undefined ? {} : { memory }),
|
|
200
247
|
hostdPort,
|
|
201
248
|
hostdFeedbackPort: feedbackPort === 0 && hostdPort !== 0 ? hostdPort + 1 : feedbackPort,
|
|
202
249
|
settleS,
|
|
250
|
+
...(hostClass === undefined ? {} : { hostClass }),
|
|
251
|
+
distributedDirs: once(distributedDirs),
|
|
252
|
+
distributed: !args.noDistributed,
|
|
203
253
|
},
|
|
204
254
|
};
|
|
205
255
|
}
|
|
@@ -390,6 +440,8 @@ export interface SyncEntry {
|
|
|
390
440
|
readonly plugins: readonly SyncPlugin[];
|
|
391
441
|
readonly qualifiedAt: string;
|
|
392
442
|
readonly seconds: number;
|
|
443
|
+
/** The distributed set the plugins were taken from, when they were not measured here. */
|
|
444
|
+
readonly distributed?: string;
|
|
393
445
|
}
|
|
394
446
|
|
|
395
447
|
export interface SyncState {
|
|
@@ -484,7 +536,17 @@ function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgmen
|
|
|
484
536
|
}
|
|
485
537
|
|
|
486
538
|
function runPass(ctx: SyncContext, target: Parameters<typeof qualifyTargets>[0][number], env: Readonly<Record<string, string | undefined>>) {
|
|
487
|
-
|
|
539
|
+
const s = ctx.settings;
|
|
540
|
+
return qualifyTargets([target], {
|
|
541
|
+
rates: QUALIFY_RATES,
|
|
542
|
+
quanta: QUALIFY_QUANTA,
|
|
543
|
+
grid: s.grid,
|
|
544
|
+
cellsPerPluginBudget: s.cellsPerPluginBudget,
|
|
545
|
+
...(s.memory === undefined ? {} : { memory: s.memory }),
|
|
546
|
+
profiles: [s.hostProfile],
|
|
547
|
+
wallS: s.budgetS,
|
|
548
|
+
env,
|
|
549
|
+
});
|
|
488
550
|
}
|
|
489
551
|
|
|
490
552
|
/** The descriptors of a scanned `.clap`, through the one descriptor writer; a failure is named and the run goes on. */
|
|
@@ -566,6 +628,66 @@ export function qualifyItem(ctx: SyncContext, item: SyncItem, files: readonly Fi
|
|
|
566
628
|
return { format: item.format, root: item.root, files: all, plugins, qualifiedAt: new Date(started).toISOString(), seconds: (Date.now() - started) / 1000 };
|
|
567
629
|
}
|
|
568
630
|
|
|
631
|
+
// --- the distributed records ----------------------------------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
/** The layer a run reads: none under `--force` or `--no-distributed`, or without a host class. */
|
|
634
|
+
function distributedLayerOf(settings: SyncSettings, force: boolean, log: (line: string) => void): DistributedLayer {
|
|
635
|
+
if (force || !settings.distributed) return EMPTY_LAYER;
|
|
636
|
+
if (settings.hostClass === undefined) {
|
|
637
|
+
log("distributed: no host class (host_class unset, and the board names none); no distributed record is read");
|
|
638
|
+
return EMPTY_LAYER;
|
|
639
|
+
}
|
|
640
|
+
const layer = readDistributedLayer(settings.distributedDirs, settings.hostClass, log);
|
|
641
|
+
if (layer.records > 0) log(`distributed: ${String(layer.records)} records of ${layer.sets.join(", ")} for ${settings.hostClass}`);
|
|
642
|
+
return layer;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* The item's entry taken from the distributed records, or `undefined` when it is to be measured: no record finds it,
|
|
647
|
+
* or the ones that do were not taken, each reason said. Its descriptors are written as a measured item's are: a
|
|
648
|
+
* `.clap` is scanned for them (the scan lists the plugins and measures nothing), an LV2 plugin joins the run's
|
|
649
|
+
* descriptor pass.
|
|
650
|
+
*/
|
|
651
|
+
function takeDistributed(ctx: SyncContext, layer: DistributedLayer, item: SyncItem, files: readonly FileDigest[]): SyncEntry | undefined {
|
|
652
|
+
if (layer.records === 0) return undefined;
|
|
653
|
+
const started = Date.now();
|
|
654
|
+
const host = ctx.settings.hostProfile.id;
|
|
655
|
+
const match = matchDistributed(layer, item, host, (paths) => digestFiles(item.root, paths, files));
|
|
656
|
+
if (match.status === "none") return undefined;
|
|
657
|
+
if (match.status === "ignored") {
|
|
658
|
+
for (const line of match.lines) ctx.log(line);
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
let plugins: SyncPlugin[];
|
|
662
|
+
try {
|
|
663
|
+
plugins = applyDistributed(ctx.settings.qualificationDir, item, match, host);
|
|
664
|
+
} catch (e) {
|
|
665
|
+
ctx.log(`distributed: ${distributedUri(item, match.records[0]!)}: ${(e as Error).message}; measured here`);
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
if (item.format === "clap") scanClapDescriptors(ctx, item);
|
|
669
|
+
const all = digestFiles(item.root, [...item.files, ...match.files], files) ?? [...files];
|
|
670
|
+
return { format: item.format, root: item.root, files: all, plugins, qualifiedAt: new Date(started).toISOString(), seconds: (Date.now() - started) / 1000, distributed: match.set };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** The descriptors of a `.clap` taken from the layer: the scanner lists its plugins, the descriptor writer writes them. */
|
|
674
|
+
function scanClapDescriptors(ctx: SyncContext, item: SyncItem): void {
|
|
675
|
+
const file = join(item.root, item.files[0]!);
|
|
676
|
+
let tool: string;
|
|
677
|
+
try {
|
|
678
|
+
tool = clapQualifyTools(ctx.env, existsSync).tool;
|
|
679
|
+
} catch (e) {
|
|
680
|
+
ctx.log(`${file}: descriptors not written: ${(e as Error).message}`);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20 });
|
|
684
|
+
if (r.error !== undefined || r.status !== 0) {
|
|
685
|
+
ctx.log(`${file}: descriptors not written: the scan ended ${r.error?.message ?? `with exit ${String(r.status)}`}`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
writeClapDescriptors(ctx, r.stdout);
|
|
689
|
+
}
|
|
690
|
+
|
|
569
691
|
// --- the outputs ------------------------------------------------------------------------------------------------------
|
|
570
692
|
|
|
571
693
|
/** The pin file's text: every whole pin of the state, the worse verdict kept for a uri pinned twice. */
|
|
@@ -754,13 +876,15 @@ export interface SyncSummary {
|
|
|
754
876
|
readonly crashed: number;
|
|
755
877
|
readonly hung: number;
|
|
756
878
|
readonly unmeasured: number;
|
|
879
|
+
/** The plugins taken from the distributed records, not measured. */
|
|
880
|
+
readonly distributed: number;
|
|
757
881
|
readonly seconds: number;
|
|
758
882
|
/** The pins a running daemon took, when one answered. */
|
|
759
883
|
readonly sent?: number;
|
|
760
884
|
}
|
|
761
885
|
|
|
762
886
|
export function summaryLine(s: SyncSummary): string {
|
|
763
|
-
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)}, ${s.seconds.toFixed(1)} s`;
|
|
887
|
+
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`;
|
|
764
888
|
}
|
|
765
889
|
|
|
766
890
|
/** The lock of one state file: a second sync on it while one runs returns at once. A lock whose process is gone is taken over. */
|
|
@@ -814,7 +938,8 @@ export async function runSync(settings: SyncSettings, options: SyncRunOptions =
|
|
|
814
938
|
const dirs = [...settings.clapDirs, ...settings.lv2Dirs];
|
|
815
939
|
await waitQuiet(dirs, settings.settleS, log);
|
|
816
940
|
const ctx: SyncContext = { settings, env, log };
|
|
817
|
-
const count = { qualified: 0, skipped: 0, refused: 0, crashed: 0, hung: 0, unmeasured: 0 };
|
|
941
|
+
const count = { qualified: 0, skipped: 0, refused: 0, crashed: 0, hung: 0, unmeasured: 0, distributed: 0 };
|
|
942
|
+
const layer = distributedLayerOf(settings, options.force === true, log);
|
|
818
943
|
let before: string;
|
|
819
944
|
try {
|
|
820
945
|
before = readFileSync(settings.pinFile, "utf8");
|
|
@@ -846,6 +971,17 @@ export async function runSync(settings: SyncSettings, options: SyncRunOptions =
|
|
|
846
971
|
log(`${item.key}: a file of it cannot be read; skipped this run`);
|
|
847
972
|
continue;
|
|
848
973
|
}
|
|
974
|
+
const taken = takeDistributed(ctx, layer, item, files);
|
|
975
|
+
if (taken !== undefined) {
|
|
976
|
+
entries[item.key] = taken;
|
|
977
|
+
if (item.format === "lv2") lv2Pinned.push(...taken.plugins.filter((p) => p.pin !== undefined).map((p) => p.uri));
|
|
978
|
+
count.distributed += taken.plugins.length;
|
|
979
|
+
for (const p of taken.plugins) log(`distributed: ${p.uri}: applied (${taken.distributed!})`);
|
|
980
|
+
state = { schema: SYNC_STATE_SCHEMA, entries };
|
|
981
|
+
writeFileWhole(settings.stateFile, `${JSON.stringify(state, null, 1)}\n`);
|
|
982
|
+
writeFileWhole(settings.pinFile, syncPinsText(state));
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
849
985
|
const entry = qualifyItem(ctx, item, files);
|
|
850
986
|
entries[item.key] = entry;
|
|
851
987
|
if (item.format === "lv2") lv2Pinned.push(...entry.plugins.filter((p) => p.pin !== undefined).map((p) => p.uri));
|