@openmixer/plugin-qualify 0.18.4 → 0.19.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 +19 -730
- package/bin/plugin-qualify-sync.mjs +4 -2
- package/bin/plugin-qualify.mjs +7 -0
- package/dist/clap-qualify.d.ts +1 -1
- package/dist/compare.d.ts +311 -0
- package/dist/compare.js +502 -0
- package/dist/compare.js.map +1 -0
- package/dist/distributed-records.d.ts +0 -2
- package/dist/distributed-records.js +0 -2
- package/dist/distributed-records.js.map +1 -1
- package/dist/host-admission.d.ts +2 -2
- package/dist/host-admission.js +7 -0
- package/dist/host-admission.js.map +1 -1
- package/dist/hosting-codes.generated.d.ts +1 -0
- package/dist/hosting-codes.generated.js +1 -0
- package/dist/hosting-codes.generated.js.map +1 -1
- package/dist/hosting-suitability.d.ts +3 -1
- package/dist/hosting-suitability.js +9 -0
- 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/jack-registry.d.ts +6 -0
- package/dist/jack-registry.js +52 -0
- package/dist/jack-registry.js.map +1 -0
- package/dist/lv2-loader.js +6 -5
- package/dist/lv2-loader.js.map +1 -1
- package/dist/measurement/format.d.ts +15 -0
- package/dist/measurement/format.js +19 -0
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/sweep-jsonl.js +5 -1
- package/dist/measurement/sweep-jsonl.js.map +1 -1
- package/dist/measurement-grid.js +1 -1
- package/dist/pins-file.d.ts +22 -2
- package/dist/pins-file.js +118 -17
- package/dist/pins-file.js.map +1 -1
- 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 +45 -0
- package/dist/qualification-record.js +36 -0
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-adapter-pass.js +20 -5
- package/dist/qualify-adapter-pass.js.map +1 -1
- package/dist/qualify-declarations.d.ts +6 -0
- package/dist/qualify-declarations.js +8 -0
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-isolated.d.ts +16 -3
- package/dist/qualify-isolated.js +31 -3
- package/dist/qualify-isolated.js.map +1 -1
- package/dist/qualify-pool.d.ts +129 -0
- package/dist/qualify-pool.js +290 -0
- package/dist/qualify-pool.js.map +1 -0
- package/dist/qualify-rejudge.js +7 -0
- package/dist/qualify-rejudge.js.map +1 -1
- package/dist/qualify-sync.d.ts +46 -1
- package/dist/qualify-sync.js +184 -20
- package/dist/qualify-sync.js.map +1 -1
- package/dist/records-package.d.ts +5 -0
- package/dist/records-package.js +25 -8
- package/dist/records-package.js.map +1 -1
- package/fixtures/clap-scan/fake.json +8 -2
- package/fixtures/probe-output/success-msi.json +2 -2
- package/package.json +3 -3
- package/src/clap-qualify.ts +1 -1
- package/src/compare.ts +699 -0
- package/src/distributed-records.ts +0 -3
- package/src/host-admission.ts +9 -2
- package/src/hosting-codes.generated.ts +1 -0
- package/src/hosting-suitability.ts +14 -0
- package/src/index.ts +1 -0
- package/src/jack-registry.ts +49 -0
- package/src/lv2-loader.ts +5 -4
- package/src/measurement/format.ts +32 -0
- package/src/measurement/sweep-jsonl.ts +5 -1
- package/src/measurement-grid.ts +1 -1
- package/src/pins-file.ts +109 -16
- package/src/probe-judge.ts +2 -0
- package/src/probe-runner.ts +18 -4
- package/src/qualification-record.ts +69 -0
- package/src/qualify-adapter-pass.ts +18 -5
- package/src/qualify-declarations.ts +8 -0
- package/src/qualify-isolated.ts +31 -5
- package/src/qualify-pool.ts +318 -0
- package/src/qualify-rejudge.ts +8 -0
- package/src/qualify-sync.ts +187 -19
- package/src/records-package.ts +25 -8
- package/tools/benchmark.py +9 -3
- package/tools/compare-host.mjs +432 -0
- package/tools/compare-run.mjs +355 -0
- package/tools/descriptors.py +27 -0
- package/tools/isolated-step.mjs +3 -1
- package/tools/lilv_load.py +25 -0
- package/tools/lv2_resolve.py +3 -1
- package/tools/pool-step.mjs +186 -0
- package/tools/scan.py +16 -20
- package/tools/audit-overlap.sh +0 -250
- package/tools/commit-message-check.sh +0 -128
- package/tools/commit-message-patterns.json +0 -41
- package/tools/private-refs-check.sh +0 -90
package/README.md
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
# plugin-qualify
|
|
2
2
|
|
|
3
3
|
Can this audio plugin be hosted in a real-time audio engine, and at what cost? `plugin-qualify`
|
|
4
|
-
scans
|
|
5
|
-
under a real host
|
|
6
|
-
|
|
4
|
+
scans an LV2 or CLAP plugin, measures it offline (latency and CPU cost per rate and quantum),
|
|
5
|
+
runs it under a real host, and gives one verdict. `plugin-qualify-sync` keeps a record for every
|
|
6
|
+
plugin installed on a machine, rescanning when a plugin directory changes.
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
and for Debian bookworm and trixie, including Raspberry Pi OS (amd64, arm64).
|
|
12
|
-
|
|
13
|
-
Fedora:
|
|
10
|
+
Fedora 44 (x86_64, aarch64):
|
|
14
11
|
|
|
15
12
|
```
|
|
16
13
|
sudo dnf install https://www.openmixer.org/rpm/openmixer-release.noarch.rpm
|
|
17
14
|
sudo dnf install plugin-qualify
|
|
18
15
|
```
|
|
19
16
|
|
|
20
|
-
Debian and Raspberry Pi OS:
|
|
17
|
+
Debian bookworm and trixie, including Raspberry Pi OS (amd64, arm64):
|
|
21
18
|
|
|
22
19
|
```
|
|
23
20
|
curl -fsSLO https://www.openmixer.org/deb/openmixer-release.deb && sudo apt install ./openmixer-release.deb
|
|
@@ -25,737 +22,29 @@ sudo apt update
|
|
|
25
22
|
sudo apt install plugin-qualify
|
|
26
23
|
```
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
pnpm add @openmixer/plugin-qualify
|
|
32
|
-
# or, to install the command globally:
|
|
33
|
-
npm install -g @openmixer/plugin-qualify
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The npm package ships the TypeScript command only, not the native tools. The CLAP prober, the LV2 measurer and
|
|
37
|
-
the interposer come with the RPM or DEB, or from `make -C native` in a checkout; with the npm package alone,
|
|
38
|
-
point `OMX_CLAP_QUALIFY`, `OMX_LV2_MEASURE` and `OMX_RT_INTERPOSE` at them (the probe runner is plugin-hostd's, `OMX_PROBE_RUNNER`), or CLAP qualification and the LV2
|
|
39
|
-
hosting dimensions refuse with "no CLAP prober at ...".
|
|
25
|
+
The npm package (`npm install -g @openmixer/plugin-qualify`) carries the TypeScript command only;
|
|
26
|
+
the native CLAP prober and LV2 measurer come with the RPM or DEB.
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
checkout instead (Node.js 20 or newer and pnpm 10, see `BUILDING.md`):
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
git clone https://github.com/FreeMixer/plugin-qualify.git
|
|
46
|
-
cd plugin-qualify
|
|
47
|
-
pnpm install
|
|
48
|
-
pnpm exec plugin-qualify --help
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
It needs Node.js 18 or newer to run. The measuring tools also want Python 3 with the lilv bindings
|
|
52
|
-
(`python3-lilv`) and, for the hosting sweep, `mod-host`. The `Containerfile` builds an image with all of
|
|
53
|
-
them. The commands below are written as `plugin-qualify`; from a checkout run them with `pnpm exec`.
|
|
54
|
-
|
|
55
|
-
## Run it on your own plugin
|
|
28
|
+
## Start
|
|
56
29
|
|
|
57
30
|
```
|
|
58
31
|
plugin-qualify urn:example:my-plugin # one plugin, by URI
|
|
59
32
|
plugin-qualify ./my-plugin.lv2 # every plugin in a bundle
|
|
60
|
-
plugin-qualify --all --out ./qualification # every LV2
|
|
33
|
+
plugin-qualify --all --out ./qualification # every installed LV2 and CLAP plugin, one record each
|
|
34
|
+
plugin-qualify compare --all --out ./compare # every plugin under omx-clap-host, plugin-hostd, mod-host and jalv
|
|
61
35
|
plugin-qualify --help # every option and its default
|
|
62
36
|
```
|
|
63
37
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
with lilv, mod-host and the ASan mod-host, if you do not want to install them yourself.
|
|
67
|
-
|
|
68
|
-
A named LV2 plugin is measured the same way as a CLAP one: through the probe runner, then judged from the probe
|
|
69
|
-
output it writes (`plugin-qualify judge` below). Exit code: `0` every plugin qualified, `1` some did not, `2`
|
|
70
|
-
something could not be measured.
|
|
71
|
-
|
|
72
|
-
## The measurement grid
|
|
73
|
-
|
|
74
|
-
A plugin is measured in cells, each one sample rate and one quantum (block size in frames). The verdict a host
|
|
75
|
-
gets reads the cell of that host's rate and quantum, so the grid has to hold it.
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
plugin-qualify --all --out ./q # the full grid (the default): 4 rates x 4 block sizes
|
|
79
|
-
plugin-qualify --all --grid-hosts --out ./q # the hosts' grid, the fast option (also --grid hosts)
|
|
80
|
-
plugin-qualify --grid 48000/128,96000/1024 urn:my:fx # exactly these cells
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
- `--grid hosts` (the fast option; `--grid-hosts` says the same): the rate and quantum of every host the run judges
|
|
84
|
-
(each `--host-profile`, and the console's two tiers, `openmixer-console` and the format's isolated host), then
|
|
85
|
-
quantum 512 at every `--rates` rate, so that a record stays comparable with the ones written before the grid.
|
|
86
|
-
With the four shipped hosts that is seven cells: 96000/1024 (`openmixer-console`), 48000/256 (`jalv`),
|
|
87
|
-
48000/128 (`mod-host`, `omx-clap-host`), and 512 at 44100, 48000, 96000 and 192000.
|
|
88
|
-
- `--grid full` (the default; every plugin is measured under the same conditions, so any host's verdict can be read at any cell and the figures compare): every `--rates` rate at every `--quanta` quantum, 16 cells with the defaults
|
|
89
|
-
(44100, 48000, 96000, 192000 by 128, 256, 512, 1024).
|
|
90
|
-
- `--grid <rate>/<quantum>,...`: exactly the cells named.
|
|
91
|
-
|
|
92
|
-
The cells are taken in that order: the hosts' own cells first, in the order the hosts are named, then the
|
|
93
|
-
512 cells, then the rest by rate and quantum. `--cells-per-plugin-budget N` keeps the first N, so a first pass
|
|
94
|
-
over a large machine can measure the hosts' cells alone (`--cells-per-plugin-budget 3` with the four shipped hosts).
|
|
95
|
-
`--blocks N` shortens the CLAP sweep of each cell from 512 blocks to N; the LV2 pass has a fixed shape (30
|
|
96
|
-
lifecycles and 3 swept runs per cell) and is not shortened by it.
|
|
97
|
-
|
|
98
|
-
An LV2 plugin is measured on the same grid as a CLAP one. plugin-hostd's runner measures an LV2 plugin at one
|
|
99
|
-
rate per run (the highest it is given), which is why records written before the grid hold LV2 figures at 192000
|
|
100
|
-
only; plugin-qualify now runs it once per cell. A CLAP plugin is run once per quantum, at every rate of the grid
|
|
101
|
-
at that quantum. The runs of one plugin and one offer make one probe output: the grid points of every run, the
|
|
102
|
-
steps of every run, and the run's outcome. The record and its byte copy hold that document.
|
|
103
|
-
|
|
104
|
-
A run that ended badly leaves a grid point at each of its cells with the run's outcome in place of figures, so a
|
|
105
|
-
cell is never silently absent. The cost a host is judged by is read at its cell: a measured cell is compared with
|
|
106
|
-
the host's budget; a cell whose run failed reads `unknown` with that run's outcome (`probe.oom`, say); only a
|
|
107
|
-
cell the grid does not hold reads `hosting.cost.unmeasured-at-rate-and-quantum`.
|
|
108
|
-
|
|
109
|
-
### The memory bound
|
|
110
|
-
|
|
111
|
-
A sampler loading its library can take all the memory of a small machine, and the kernel then kills it, or
|
|
112
|
-
something else. Every run is bounded: `--memory-limit <bytes>` (`512M`, `2G`, or `off`), by default half of
|
|
113
|
-
the machine's RAM. The bound is the runner's data limit (`--data-bytes`, `RLIMIT_DATA` on the measuring child:
|
|
114
|
-
its heap and private writable mappings), and the interposer the child runs under turns an allocation refused
|
|
115
|
-
under that limit into an immediate exit with status 121. A run that ends that way, or by a `SIGKILL` the
|
|
116
|
-
runner did not send (the kernel's out-of-memory killer), is recorded as `probe.oom`: its cells carry the
|
|
117
|
-
outcome and `memoryLimit { bytes, mechanism }`, the plugin's record is still written, and the other cells are
|
|
118
|
-
judged as usual. A plugin whose every run ended `probe.oom` is `unknown` under every host, deciding `probe.oom`.
|
|
119
|
-
A host whose own cell ended `probe.oom` is `unknown` there as well (`memory`, `probe.oom`): the plugin did not
|
|
120
|
-
run within the bound at that host's rate and quantum.
|
|
121
|
-
|
|
122
|
-
Where the machine delegates the memory controller, the runner sets the bound as `memory.max` of a cgroup instead: a run the
|
|
123
|
-
kernel kills at it is `probe.memory-limit`, read the same way, and the document's `limits` says which it was. A process
|
|
124
|
-
plugin-qualify spawns itself, with no runner (`tools/lv2-measure.mjs`, `tools/benchmark.py`), runs under `RLIMIT_AS` at the
|
|
125
|
-
machine's RAM, off for a sanitizer build; hitting it is `memory-limit-exceeded`, with the limit.
|
|
126
|
-
|
|
127
|
-
### Memory
|
|
128
|
-
|
|
129
|
-
Each cell carries a footprint: how far the resident set of a process of its own grew, in KB, after instantiate, after
|
|
130
|
-
activate, at rest after processing, with notes held for an instrument, and what a second instance added. A record adds what
|
|
131
|
-
they come to: `firstInstanceKb`, the most one instance held with everything its instances share, and `perInstanceKb`, what
|
|
132
|
-
one more costs.
|
|
133
|
-
|
|
134
|
-
A host with a memory budget reads them, at every cell it runs, and the verdict follows:
|
|
135
|
-
`hosting.memory.within-ceiling`, `hosting.memory.above-ceiling` (`unsuitable`), or `hosting.memory.unmeasured` (`unknown`,
|
|
136
|
-
since a missing figure never reads as fitting). `openmixer-console` has one, sized for the desk, and the Raspberry Pi hosts
|
|
137
|
-
`openmixer-pi-4gb` and `openmixer-pi-8gb`, at 48000/128 and 48000/256, have one each. The Pi budgets are not measured on a Pi
|
|
138
|
-
yet, so a plugin under them is `conditional`, `hosting.memory.within-provisional-ceiling`: that gives the figure and the
|
|
139
|
-
ceiling and does not say the plugin fits. The other hosts have no budget, and read nothing.
|
|
140
|
-
|
|
141
|
-
A soak (LV2; the CLAP prober has no continuous render yet) also reads the growth: past 10 MB per minute of audio over its
|
|
142
|
-
second half, provisional, the rate fails as `leaks`.
|
|
143
|
-
|
|
144
|
-
### How long a run takes
|
|
145
|
-
|
|
146
|
-
The default grid is the full one, so the expected time of a run with no grid option is the `full` row: about 65 hours on a Raspberry Pi 5 for the vangelis set (1823 records). `--grid-hosts` is the fast option, about 30 hours there.
|
|
147
|
-
|
|
148
|
-
Times from one deep run on a Raspberry Pi 5 (4 cores, 4 GB), 1823 records (494 CLAP, two offers of each of 247,
|
|
149
|
-
and 1329 LV2), six hours in all with the grid of before (CLAP at four rates in one run at quantum 512, LV2 at
|
|
150
|
-
192000/512 alone):
|
|
151
|
-
|
|
152
|
-
| | runs | mean a run | total |
|
|
153
|
-
| --- | --- | --- | --- |
|
|
154
|
-
| CLAP, one run per offer (4 cells) | 494 | 2.8 s | 0.38 h |
|
|
155
|
-
| LV2, one run (1 cell) | 1329 | 10.5 s | 3.86 h |
|
|
156
|
-
| host admissions (jalv, mod-host) and the scan | | | about 1.8 h |
|
|
157
|
-
|
|
158
|
-
A CLAP plugin takes one run per quantum of the grid, an LV2 plugin one run per cell; the admissions do not
|
|
159
|
-
grow with the grid. On that machine, then, about:
|
|
160
|
-
|
|
161
|
-
| grid | CLAP runs a plugin | LV2 runs a plugin | expected |
|
|
162
|
-
| --- | --- | --- | --- |
|
|
163
|
-
| hosts (7 cells) | 4 | 7 | 1.5 h + 27 h + 1.8 h, about 30 h |
|
|
164
|
-
| full (16 cells) | 4 | 16 | 1.5 h + 62 h + 1.8 h, about 65 h |
|
|
165
|
-
| hosts, `--cells-per-plugin-budget 3` | 3 | 3 | 1.1 h + 11.6 h + 1.8 h, about 15 h |
|
|
166
|
-
|
|
167
|
-
These are ceilings: the LV2 time a run was taken at 192000, the dearest rate. `--blocks 128` takes a quarter of
|
|
168
|
-
the CLAP sweep a cell. A first pass with `--cells-per-plugin-budget 3`, then `--grid full` over the plugins worth
|
|
169
|
-
it, keeps a small machine's night bounded.
|
|
170
|
-
|
|
171
|
-
## CLAP plugins: the RT-safety verdict
|
|
172
|
-
|
|
173
|
-
A `.clap` file, or a directory holding one, is scanned by `omx-clap-scan` (from omx-clap-core; set
|
|
174
|
-
`OMX_CLAP_SCAN` to its path when it is not on `PATH`) and each plugin in it is then qualified by the
|
|
175
|
-
CLAP prober the package installs (from a checkout, the one built from `native/`, see `BUILDING.md`):
|
|
176
|
-
|
|
177
|
-
```
|
|
178
|
-
plugin-qualify ./my-fx.clap # every plugin in the file
|
|
179
|
-
plugin-qualify --out ./out ./my-fx.clap # and its RT-safety documents
|
|
180
|
-
CLAP_PATH=~/my-clap plugin-qualify --all # every CLAP on the search path
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Each plugin is measured by plugin-hostd's probe runner, `plugin-hostd-probe` (from the `plugin-hostd` package, 0.4.0 or later, which
|
|
184
|
-
the RPM and DEB require;
|
|
185
|
-
`OMX_PROBE_RUNNER` names it when it is not on `PATH`). The runner starts the prober under limits (a wall clock
|
|
186
|
-
that kills the whole process tree, CPU time, memory, files), and plugin-qualify waits for the runner and reads
|
|
187
|
-
the document it writes, the probe output. Nothing in plugin-qualify times a prober out on its own any more.
|
|
188
|
-
|
|
189
|
-
The files a pin holds are plugin-hostd's: the sync writes each pin with the files `plugin-hostd --pin-files <uri>`
|
|
190
|
-
prints for the device's LV2 directories (plugin-hostd 0.15.0 or later; `PLUGIN_HOSTD` names the daemon when it is not
|
|
191
|
-
on `PATH`), the list an add checks, and hashes exactly those. When plugin-hostd cannot name them, or is older, the
|
|
192
|
-
plugin gets no pin, the log says why, and the daemon pins it on its first add (`pin_mode first-use`).
|
|
193
|
-
|
|
194
|
-
`--all` walks the standard CLAP search path through the same scanner: each directory `CLAP_PATH`
|
|
195
|
-
names (colon-separated), then `~/.clap`, then `/usr/lib/clap`, which is the system directory on every
|
|
196
|
-
distribution and architecture. A plugin id found twice keeps its first place on that path. LV2 plugins
|
|
197
|
-
are measured by `tools/lv2-measure.mjs` (below); this entry point does not drive that pass yet, so
|
|
198
|
-
`--all` refuses with exit 2 when any `.lv2` bundle is on the LV2 path (`LV2_PATH`, else `~/.lv2` and the
|
|
199
|
-
system lv2 directories) rather than qualify only the CLAP side. A directory on the CLAP path that cannot
|
|
200
|
-
be read (permissions, a symlink loop) also refuses with exit 2, naming it.
|
|
201
|
-
|
|
202
|
-
The prober runs each plugin headless at every cell of the grid (see "The measurement grid"), under an interposer that counts the
|
|
203
|
-
allocations, locks and syscalls its `process()` makes while every parameter is swept, and prints
|
|
204
|
-
the file's SHA-256 beside what it measured. The verdict is the real-time-safety one: **suitable**
|
|
205
|
-
only on a clean count, **conditional** (isolated in the pin: it runs in a worker of its own, never refused) on any
|
|
206
|
-
allocation, lock, syscall or main-thread host call from `process()`, **unsuitable** on `CLAP_PROCESS_ERROR`, **unknown** when it could not be counted. An instrument is
|
|
207
|
-
quiet until it is played, so a plugin with a note input is also played, at every rate and under a
|
|
208
|
-
count of its own: a chord, the pitch bent and a controller moved, pressure, the chord let go, a
|
|
209
|
-
burst of 32 note ons in one block and their offs, all notes off, in the dialect its note input
|
|
210
|
-
reads (CLAP's, else MIDI's). An allocation, lock or syscall while it plays is **conditional**, isolated in the pin like the same
|
|
211
|
-
violation in the run (`hosting.rt-safety.violations-under-notes`); an instrument that could not be played under the
|
|
212
|
-
interposer is **unknown** (`hosting.rt-safety.notes-unmeasured`). `--out` writes the qualification records and the probe outputs they judged (see
|
|
213
|
-
"Judging a probe output" below), one pair per plugin binary and offered host extensions. Each plugin's pin is
|
|
214
|
-
an `integrity` block in the probe output: the `.clap`'s SHA-256 by its file name and the parameter-layout
|
|
215
|
-
fingerprint `omx-layout/1:<sha256>`, both computed through plugin-hostd's `pin.h`, which is what a console
|
|
216
|
-
hands plugin-hostd as `pin_set` before it adds the plugin. `OMX_CLAP_QUALIFY` and `OMX_RT_INTERPOSE`
|
|
217
|
-
name the prober and the interposer the runner starts when they are not the installed ones or the checkout's
|
|
218
|
-
own build.
|
|
219
|
-
|
|
220
|
-
## Judging a probe output
|
|
221
|
-
|
|
222
|
-
The probe output is the document `plugin-hostd-probe run` writes: one binary and one offer, the steps the
|
|
223
|
-
prober reached and how each ended, and the figures per rate and quantum, with no verdict in it. plugin-qualify
|
|
224
|
-
reads it with `readProbeOutput`, which refuses a document of a major version it does not know
|
|
225
|
-
(`probe.format.major-unknown`), a document that carries a verdict, and a document whose derived fields disagree
|
|
226
|
-
with the fields they are computed from (`probe.format.derived-mismatch`): `machine.class.cstatesLimited` and
|
|
227
|
-
`machine.profile.meetsProtocol` are recomputed from `cpuidle[]`, the governor and `unmet[]`, never trusted.
|
|
228
|
-
|
|
229
|
-
```
|
|
230
|
-
plugin-hostd-probe run --out out/plate.probe.json ./example-plate.clap org.example.plate
|
|
231
|
-
plugin-qualify judge --host-profile omx-clap-host --qualification-dir ./qualification out/plate.probe.json
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
`judge` writes, into the qualification directory (`--qualification-dir`, else `$XDG_STATE_HOME/omx/qualification`,
|
|
235
|
-
else `~/.local/state/omx/qualification`), the qualification record `<id>@<version>-<sha256 first 12>.<offer>.qualification.json`
|
|
236
|
-
and, beside it, a byte copy of the probe output it judged, `<same name>.probe.json`, because the probe directory
|
|
237
|
-
may be wiped. The record names that copy and its SHA-256 in `probe`, with the run's protocol, reason and machine
|
|
238
|
-
class, so a reference figure and a local one are told apart. The record holds, for each host profile, the verdict
|
|
239
|
-
(`rating`, `hosting`, the deciding reason code and every reason) and the cost reasons at that profile's rate and
|
|
240
|
-
quantum, and the figures as measured. Exit `0` every plugin qualified, `1` some did not, `2` something could not
|
|
241
|
-
be read or judged.
|
|
242
|
-
|
|
243
|
-
The verdict is the same one `plugin-qualify` gives CLAP plugins, the real-time-safety one, now for LV2 as
|
|
244
|
-
well; cost is reported beside it, not folded into it. Cost is read at the profile's rate AND quantum: a pair the
|
|
245
|
-
document does not hold reads `hosting.cost.unmeasured-at-rate-and-quantum`, and two quanta of one plugin that cost
|
|
246
|
-
differently give two different cost reasons. Only a measurement of the old format, which keeps one figure per
|
|
247
|
-
rate, is read per rate. A run that did not end `probe.ok` is `unknown`, with the run's outcome code as the reason.
|
|
248
|
-
|
|
249
|
-
An instrument costs nothing until a voice plays, so its cost is taken with notes held down, at velocity 127,
|
|
250
|
-
spread across the keyboard, while every parameter is swept: as many notes as the voice capacity the plugin
|
|
251
|
-
declares (CLAP `voice-info`, basis `declared`), or, when it declares none, the count `--probe-notes` gives
|
|
252
|
-
(basis `probe`; LV2 declares none). Each rate's figure says how many notes it held and why (`voices: { held,
|
|
253
|
-
basis, capacity }`). An instrument with no count to hold is not timed (`voices-unknown`).
|
|
254
|
-
|
|
255
|
-
An LV2 plugin's record holds the same two figures, under the same field names, in `measurements.grid[]`:
|
|
256
|
-
`latency { declaredFrames, measuredFrames }` and `cost`. `lv2_measure` takes them at the run's rate and block, the
|
|
257
|
-
way the CLAP prober does: the declared figure is what the plugin reports on its latency port
|
|
258
|
-
(`lv2:designation lv2:latency`, or the older `lv2:reportsLatency`; 0 when it has none), the measured one is where an
|
|
259
|
-
impulse at frame 0 lands (the first frame at or above 1e-3 of the response's own peak), or, for an instrument, where a
|
|
260
|
-
struck A4 sounds. A plugin that has no latency port is measured, never read as 0. An instrument's cost holds
|
|
261
|
-
`OMX_PROBE_NOTES` keys down (the LV2 counterpart of `--probe-notes`). A figure the run could not take (a response that
|
|
262
|
-
stays silent, an instrument with no count to hold) is left out of the grid point and named in
|
|
263
|
-
`measurements.unmeasured` with its hosting code (`hosting.latency.unmeasured`,
|
|
264
|
-
`hosting.cost.unmeasured-at-rate-and-quantum`).
|
|
265
|
-
|
|
266
|
-
Every cell of a record carries `latency`: the two figures when the run took them, else
|
|
267
|
-
`{ unmeasuredReason, code }`, the contract's word for why and the hosting code a reader classifies by. A latency
|
|
268
|
-
step that did not end well gives the child's reason (`silent-output`, say); a cell whose run failed, its outcome's
|
|
269
|
-
word (`probe-crashed`, or `probe-error` for `probe.oom`); a run that took no latency at all (a runner or a child
|
|
270
|
-
older than the latency arm), `not-attempted`. The declared figure alone, for a plugin whose response could not be
|
|
271
|
-
measured, is not in the probe output: plugin-hostd's runner writes a cell's latency only with both figures.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
The prober also scans: `OMX_CLAP_SCAN=native/build/clap-qualify` makes it the scanner, and its scan
|
|
275
|
-
keeps what a console needs to draw a third-party plugin's controls: an enum parameter's labels (as
|
|
276
|
-
the plugin's `value_to_text` renders them), each parameter's module, and the plugin's
|
|
277
|
-
remote-control pages.
|
|
278
|
-
|
|
279
|
-
## Adjusting a verdict by what the plugin did on this machine
|
|
280
|
-
|
|
281
|
-
```
|
|
282
|
-
plugin-qualify adjust --history ~/.local/state/plugin-hostd/history --qualification-dir ./qualification --host-profile omx-clap-host
|
|
283
|
-
plugin-qualify adjust --history history --qualification-dir ./qualification --host-class aarch64/4/2400 --pins pins.txt
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
plugin-hostd keeps a running history of every plugin binary on every host class it ran on (windows run alone and in a
|
|
287
|
-
group, CPU, xrun windows, crashes, restarts, quarantines). `adjust` reads that file directly, so it runs without a daemon,
|
|
288
|
-
and writes a second verdict beside the measured one in each qualification record: a `history` block per host class of the
|
|
289
|
-
file, holding the line it looked for (the plugin's plugin-hostd uri and the digest of the first file of its pin), whether
|
|
290
|
-
the history has it, the windows it counts, and for each `--host-profile` the measured pin verdict, the adjusted one, the
|
|
291
|
-
move (`demote`, `promote` or `none`) and a reason code. The measured `verdicts` and the measurement are never changed: the
|
|
292
|
-
same plugin on another host class has another history and another adjusted verdict.
|
|
293
|
-
|
|
294
|
-
| move | when | reason |
|
|
295
|
-
|---|---|---|
|
|
296
|
-
| `in-process` to `isolated` | a crash was counted within 30 days, on this host class | `history.crashed` |
|
|
297
|
-
| `in-process` to `isolated` | from 30 grouped windows, the share of xrun windows grouped exceeds the share alone by 5 points | `history.xruns-grouped` |
|
|
298
|
-
| `isolated` to `refused` | three or more solo crashes, the latest within 30 days | `history.crashed-alone` |
|
|
299
|
-
| `isolated` to `in-process` | every reason against the plugin is a `hosting.cost.*` one, 360 windows run alone (six hours), no crash or restart ever counted, no xruns raised when grouped, `cpu_p95_solo` under the host profile's ceiling | `history.steady` |
|
|
300
|
-
| none | no line for this binary on this class | `history.no-record` |
|
|
301
|
-
| none | a line, and no rule moves the verdict | `history.no-change` |
|
|
302
|
-
| none | the line would promote, but the verdict is a fact about the plugin: a real-time-safety, feature, stability, latency or topology reason, an `unknown`, or a `refused` | `history.not-liftable`, with `blockedBy` |
|
|
303
|
-
|
|
304
|
-
History never lifts a real-time-safety, feature or unknown verdict, and never promotes a refused one; a demotion is one
|
|
305
|
-
step. The thresholds are one table, `HISTORY_POLICY` in `src/history-adjust.ts`. `--pins <file>` writes the pins with the
|
|
306
|
-
adjusted verdict of the first `--host-profile` instead of the measured one; it needs exactly one host class, so name one
|
|
307
|
-
with `--host-class` when the history holds several. `--now <seconds>` fixes the time the history is read at. A line of the
|
|
308
|
-
history outside its grammar is not read and is named on stderr; a history of a version this tool does not know is refused.
|
|
309
|
-
|
|
310
|
-
## LV2 plugins: the hosting measurement
|
|
311
|
-
|
|
312
|
-
`tools/lv2-measure.mjs` measures the hosting dimensions (`stability`, `rtSafety`, `features`) through
|
|
313
|
-
the LV2 measurer the package installs, `lv2_measure`: each plugin is loaded through lilv in its own
|
|
314
|
-
process, instantiated and destroyed 30 times, and run under the interposer while every control is
|
|
315
|
-
swept. `tools/lv2-measure.mjs` measures at the highest `--rates` rate; `plugin-qualify` runs the measurer
|
|
316
|
-
once per cell of its grid (see "The measurement grid"):
|
|
317
|
-
|
|
318
|
-
```
|
|
319
|
-
node tools/lv2-measure.mjs --rates 48000 --dimensions stability,rtSafety,features \
|
|
320
|
-
--plugins urn:my:plugin --out measurements.json --run
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Without `--plugins` it measures every plugin the scan finds. Each plugin's entry carries its pin, an
|
|
324
|
-
`integrity` block: the SHA-256 of the files the host loads and the parameter-layout fingerprint,
|
|
325
|
-
through plugin-hostd's `pin.h`, as for CLAP. `OMX_LV2_MEASURE` and `OMX_RT_INTERPOSE` name the
|
|
326
|
-
measurer and the interposer when they are not the installed ones or the checkout's own build.
|
|
327
|
-
|
|
328
|
-
The features the measurer offers a plugin are the console's own: omx-clap-core's LV2 feature
|
|
329
|
-
provider, the code its LV2 host builds every instance's features with. Each of the provider's
|
|
330
|
-
options is a flag of `tools/lv2-measure.mjs` and of `lv2_measure` itself, and each plugin's record
|
|
331
|
-
names the options it ran under, `featureOptions`:
|
|
332
|
-
|
|
333
|
-
| flag | what it sets | default |
|
|
334
|
-
| --- | --- | --- |
|
|
335
|
-
| `--features LIST` | the kinds offered: `map`, `unmap`, `options`, `bounded`, `worker`, `default-state`, `log`, `rtmempool` (empty: none) | all but `default-state` |
|
|
336
|
-
| `--strict` / `--no-strict` | refuse a plugin requiring a feature not offered (the record's `refusedFeature` names it), or hand it to its own instantiate without it | `--no-strict` |
|
|
337
|
-
| `--block-declares LIST` | the buf-size declarations made when `bounded` is offered: `bounded`, `fixed`, `pow2` (empty: none) | `bounded` |
|
|
338
|
-
| `--block-min N` | `bufsz:minBlockLength`; 0 is the run's block | 1 |
|
|
339
|
-
| `--block-max N` | `bufsz:maxBlockLength`; 0 is the run's block | 0 |
|
|
340
|
-
| `--block-nominal N` | `bufsz:nominalBlockLength`; 0 is the maximum | 0 |
|
|
341
|
-
| `--log MODE` | `log:log` lines: `silent`, `route` (to stderr) or `capture` (into the record's `featureOptions.log.lines`) | `silent` |
|
|
342
|
-
| `--log-capture-bytes N` | the capture buffer, its NUL included | 4096 |
|
|
343
|
-
|
|
344
|
-
The defaults are the measurement as it has always been, so a verdict reached under them does not
|
|
345
|
-
move. `lv2_measure --help` lists the same flags.
|
|
346
|
-
|
|
347
|
-
## Scanned descriptors
|
|
348
|
-
|
|
349
|
-
Both scanners also write what each plugin states about itself as a scanned descriptor: one JSON file per
|
|
350
|
-
plugin in omx-contract's descriptor words (identity, controls, ports, what it needs from a host, whether it
|
|
351
|
-
reports latency), held to omx-contract's descriptor schema before it is written. A descriptor holds no
|
|
352
|
-
measurement and no verdict; those are the qualification record's.
|
|
353
|
-
|
|
354
|
-
The files go into the descriptor directory, the key `descriptor_dir`:
|
|
355
|
-
|
|
356
|
-
| | where |
|
|
357
|
-
| --- | --- |
|
|
358
|
-
| named | `--descriptor-dir <dir>` |
|
|
359
|
-
| a user (the default) | `$XDG_STATE_HOME/omx/descriptors`, else `~/.local/state/omx/descriptors` |
|
|
360
|
-
| a system service | `/var/lib/omx/descriptors` (`--system`) |
|
|
361
|
-
|
|
362
|
-
Each file is named `<id>@<version>-<sha256 first 12>.json`: the CLAP id or the LV2 URI, the version the
|
|
363
|
-
plugin states (`unversioned` when it states none) and the SHA-256 of its binary (the `.clap`, or the LV2
|
|
364
|
-
bundle's `lv2:binary`), with every character other than letters, digits, `.`, `_` and `-` written `_`. The hash
|
|
365
|
-
keys which file describes which binary: a changed binary is a new file, and the old one stays as history.
|
|
366
|
-
|
|
367
|
-
```
|
|
368
|
-
tools/scan.py --out catalog.json # LV2: the catalog, and the descriptors beside it
|
|
369
|
-
tools/scan.py --descriptor-dir ./descriptors urn:my:plugin # one plugin, into a directory of your own
|
|
370
|
-
native/build/clap-qualify --json ./my-fx.clap | tools/descriptors.py clap --descriptor-dir ./descriptors
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
`scan.py --no-descriptors` writes the catalog only. A descriptor the schema refuses is not written and is
|
|
374
|
-
reported as `contract.file.invalid`, omx-contract's code; a plugin whose binary cannot be read is
|
|
375
|
-
`qualify.descriptor.binary-unhashed`; a directory that cannot be written is `qualify.descriptor.dir-unwritable`;
|
|
376
|
-
no schema found is `qualify.descriptor.schema-missing`. Any of them makes the run exit `2`, and the other
|
|
377
|
-
plugins are still written. The schema is the omx-contract package plugin-qualify depends on, else
|
|
378
|
-
`/usr/share/omx-contract/omx-contract.schema.json`; `OMX_CONTRACT_SCHEMA` names another one, for a test. The
|
|
379
|
-
writer needs Python's `jsonschema` (`python3-jsonschema`).
|
|
380
|
-
|
|
381
|
-
## What the verdicts mean
|
|
382
|
-
|
|
383
|
-
- **suitable**: every dimension was measured and passed. Safe to run inside the engine's own
|
|
384
|
-
process. This is the only verdict that counts as qualified.
|
|
385
|
-
- **conditional**: it works, with a caution (for example a high cost at small quanta, or an
|
|
386
|
-
allocation, lock or syscall in `run()`). It is run isolated, and the deciding reason names the caution.
|
|
387
|
-
- **unknown**: something was not measured. An unmeasured plugin is never a pass.
|
|
388
|
-
- **unsuitable**: a measured failure: a crash, a memory error in the plugin's own binary, or a
|
|
389
|
-
required feature the host does not provide. An allocation, lock or syscall in `run()` is not
|
|
390
|
-
one: that plugin is **conditional**, and runs isolated.
|
|
391
|
-
|
|
392
|
-
The worst dimension decides. The text output prints the plugin, its verdict and the reason that
|
|
393
|
-
decided it.
|
|
394
|
-
|
|
395
|
-
## Reproducing a failure
|
|
396
|
-
|
|
397
|
-
1. Run the plugin alone: `plugin-qualify --format json --out ./out <uri>`.
|
|
398
|
-
2. The document in `./out` names the dimension that failed and, for a crash, the sanitizer class
|
|
399
|
-
and the object of the faulting frame.
|
|
400
|
-
3. Run the ASan mod-host (`mod-host-asan` in the image) with just that plugin to get the full
|
|
401
|
-
sanitizer report.
|
|
402
|
-
|
|
403
|
-
`fixtures/zero-path.lv2` is a known-bad plugin (a zero-size `atom:Path` overread). It should
|
|
404
|
-
rate **unsuitable**; if it does not, the sweep is not catching memory errors.
|
|
405
|
-
|
|
406
|
-
## For other projects (zynthian, MOD, packagers)
|
|
407
|
-
|
|
408
|
-
The measurements describe the plugin and never the host: latency and cost per rate, whether it
|
|
409
|
-
survives a thousand lifecycles and a three-hour soak, whether `run()` allocates, locks or makes
|
|
410
|
-
syscalls, and which LV2 features it requires. They are taken once. The **verdict** is a separate
|
|
411
|
-
step: the same measurements judged against a **host profile**, the facts that differ between
|
|
412
|
-
hosts. Two projects share the measurements and differ only in the profile they choose.
|
|
413
|
-
|
|
414
|
-
### Choosing a host profile
|
|
415
|
-
|
|
416
|
-
```
|
|
417
|
-
plugin-qualify --host-profile jalv ./my-synth.lv2 # zynthian: one jalv process per plugin
|
|
418
|
-
plugin-qualify --host-profile mod-host ./my-fx.lv2 # MOD: one shared mod-host process
|
|
419
|
-
plugin-qualify --host-profile omx-clap-host ./my-synth.clap # omx-clap-host, alone or under plugin-hostd
|
|
420
|
-
plugin-qualify --host-profile jalv,mod-host,openmixer-console <uri> # one verdict per profile
|
|
421
|
-
plugin-qualify --host-profile ./my-host.json <uri> # your own host
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
| profile | isolation | verdict | instruments | LV2 features read from | cost budget (P95 of a core, rate / quantum) |
|
|
425
|
-
|---|---|---|---|---|---|
|
|
426
|
-
| `openmixer-console` (default) | in-process (the console's RT thread) | judged from a declaration | no | the console's in-process table | 0.05 at 96000 / 1024 |
|
|
427
|
-
| `jalv` | per-process | **measured by a run** (`jalv -n <name> <uri>`, headless) | yes | jalv 1.6.8 `src/jalv.c` (zynthian's fork passes the same list) | 0.25 at 48000 / 256 |
|
|
428
|
-
| `mod-host` | shared-process | **measured by a run** (`add`, `activate`, `remove` on mod-host's socket) | yes | mod-host f14a230 `src/effects.c` | 0.05 at 48000 / 128 |
|
|
429
|
-
| `omx-clap-host` | shared-process | judged from a declaration | yes | the console's in-process table (the same LV2 feature provider) | 0.05 at 48000 / 128 |
|
|
430
|
-
|
|
431
|
-
**Measured or declared.** A *declared* verdict is what the profile's feature list and cost budget make of
|
|
432
|
-
the one probe output. A *measured* verdict (`jalv`, `mod-host`, LV2 plugins) also runs the plugin under that
|
|
433
|
-
host, one admission per plugin, and its record entry carries `arm: "run"` and `measured: true`. A refusal
|
|
434
|
-
(`resp -102`), a crash (the signal), a host that dies on `add`, or a port type the host does not know makes the
|
|
435
|
-
plugin `unsuitable` for that host, with the code as the deciding reason (`mod-host.resp-102`,
|
|
436
|
-
`mod-host.signal-SIGABRT`, `mod-host.died-on-add`, `jalv.unknown-port-type`, `jalv.signal-SIGABRT`). A host
|
|
437
|
-
that is not installed leaves the verdict unmeasured (`unknown`, deciding `<host>.not-installed`). After any
|
|
438
|
-
death on mod-host the next admission starts a fresh host. `plugin-qualify judge` reads a probe output
|
|
439
|
-
that holds no run, so its `jalv` and `mod-host` entries stay `arm: "declaration"`.
|
|
440
|
-
|
|
441
|
-
**The console's two tiers.** Every record carries `tiers`: the *in-process* verdict (`openmixer-console`) and the
|
|
442
|
-
*isolated* verdict (`omx-clap-host`, an LV2 plugin through plugin-hostd's LV2 adapter), `offeredOn` (`in-process` when its
|
|
443
|
-
verdict qualified, `isolated` when its verdict qualified or is `conditional`) and, when neither, `dimmed` with the reason. A
|
|
444
|
-
`conditional` plugin, RT-unsafe for instance, is offered on the isolated tier with `runsAlone: true`: it runs in a worker of
|
|
445
|
-
its own, never grouped, and its pin says `isolated`. An LV2 plugin is run once under the adapter, and
|
|
446
|
-
that run decides its `omx-clap-host` verdict as a mod-host run decides mod-host's. An `lv2:InstrumentPlugin` is also played one
|
|
447
|
-
held note there, its output read at its JACK ports (`jack_peak`): at or under -120 dBFS it reads `unknown`
|
|
448
|
-
(`omx-clap-host.silent-under-note`), not qualified and not refused, since a sampler with nothing loaded is silent too. A level
|
|
449
|
-
that could not be read reads the same way (`omx-clap-host.level-unmeasured`). A plugin that kills the isolated
|
|
450
|
-
host and qualifies in-process is offered in-process only. The tiers are judged for every plugin whether or not
|
|
451
|
-
`--host-profile` names them; mod-host and jalv are run only when `--host-profile` asks for them.
|
|
452
|
-
|
|
453
|
-
`omx-clap-host` offers a CLAP plugin the console's six host extensions and three more,
|
|
454
|
-
`clap.preset-load/2`, `clap.track-info/1` and `clap.remote-controls/2`. A CLAP plugin judged
|
|
455
|
-
under it is probed again with those three offered, so its verdict holds for the host it runs in.
|
|
456
|
-
|
|
457
|
-
The `jalv`, `mod-host` and `omx-clap-host` budgets are defaults, not a measurement of your board. State your own
|
|
458
|
-
by writing a profile file. Every field is required; a missing or mistyped field is refused and
|
|
459
|
-
never filled in with a default:
|
|
460
|
-
|
|
461
|
-
```json
|
|
462
|
-
{
|
|
463
|
-
"id": "zynthian-v5",
|
|
464
|
-
"lv2Features": ["http://lv2plug.in/ns/ext/urid#map", "http://lv2plug.in/ns/ext/urid#unmap",
|
|
465
|
-
"http://lv2plug.in/ns/ext/worker#schedule", "http://lv2plug.in/ns/ext/log#log",
|
|
466
|
-
"http://lv2plug.in/ns/ext/options#options",
|
|
467
|
-
"http://lv2plug.in/ns/ext/state#loadDefaultState",
|
|
468
|
-
"http://lv2plug.in/ns/ext/buf-size#powerOf2BlockLength",
|
|
469
|
-
"http://lv2plug.in/ns/ext/buf-size#fixedBlockLength",
|
|
470
|
-
"http://lv2plug.in/ns/ext/buf-size#boundedBlockLength"],
|
|
471
|
-
"clapExtensions": null,
|
|
472
|
-
"instruments": true,
|
|
473
|
-
"cost": { "coreFractionCeiling": 0.3, "rate": 48000, "quantum": 256 },
|
|
474
|
-
"latency": "uncompensated",
|
|
475
|
-
"isolation": "per-process"
|
|
476
|
-
}
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
What each field changes. `lv2Features` is what the host passes to `instantiate`: a plugin that
|
|
480
|
-
requires anything else is **unsuitable** on that host. `clapExtensions` is `null` for a host with
|
|
481
|
-
no CLAP path. `instruments: true` means a MIDI input is fed, and a plugin with no audio input but
|
|
482
|
-
a MIDI input and an audio output rates as an instrument. `cost` is the per-instance ceiling, and
|
|
483
|
-
the rate and quantum the verdict is judged at. `latency: "compensated"` means the host aligns on
|
|
484
|
-
the plugin's latency, so a latency that moves with a control is a caution. `isolation` decides
|
|
485
|
-
whether spawned threads and a linked GUI toolkit count as cautions: they do only when the plugin
|
|
486
|
-
shares its process. An unmeasured dimension is **unknown** on every host.
|
|
487
|
-
|
|
488
|
-
With several profiles, the exit code is `0` only if every plugin qualifies under every profile.
|
|
489
|
-
|
|
490
|
-
### The JSON document
|
|
491
|
-
|
|
492
|
-
`--out <dir>` writes one interchange document per run (`lv2-plugin-measurements`, format 1.6,
|
|
493
|
-
the same format the OpenMixer console reads). The
|
|
494
|
-
`plugins` array holds the raw, host-free measurements. Beside it, `hostProfiles` holds each
|
|
495
|
-
profile the run judged against, in full, and `verdicts` holds one entry per plugin per profile:
|
|
496
|
-
|
|
497
|
-
```json
|
|
498
|
-
{ "uri": "urn:example:verb", "host": "jalv", "rating": "unknown", "qualified": false,
|
|
499
|
-
"deciding": "hosting.stability.unsoaked" }
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
`host` is the `id` of an entry in `hostProfiles`. `deciding` is a stable code, absent when the
|
|
503
|
-
plugin is `suitable`. If you have your own host rules, re-derive them from `plugins` and do not
|
|
504
|
-
trust another host's verdict.
|
|
505
|
-
|
|
506
|
-
### As a library
|
|
507
|
-
|
|
508
|
-
`classifyPluginHosting(plugin, { rate, quantum, profile })` gives one verdict, and
|
|
509
|
-
`qualifyPlugins(measured, { profiles })` gives one per plugin per profile. `readHostProfile(json)`
|
|
510
|
-
validates a profile file, and `judgedDocument(run, plugins, profiles, verdicts)` builds the
|
|
511
|
-
document. All of them are exported from the package root. The package depends on no other
|
|
512
|
-
OpenMixer package.
|
|
513
|
-
|
|
514
|
-
### Pinning what was qualified
|
|
515
|
-
|
|
516
|
-
```
|
|
517
|
-
plugin-qualify --pins pins.txt ./my-synth.clap # a pin and a verdict per plugin, in a file plugin-hostd loads
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
`--pins <file>` writes one line per qualified plugin, the same line as plugin-hostd's `pin_set` verb with the verdict as
|
|
521
|
-
a last word: the SHA-256 of every file the host loads, the parameter-layout fingerprint, and `verdict=in-process`
|
|
522
|
-
(suitable), `isolated` (conditional) or `refused` (unsuitable). A plugin with no verdict is pinned without one. Put the
|
|
523
|
-
file in plugin-hostd's settings as `pin_file` and an `add` of that plugin needs no `pin_set`; a changed file is refused
|
|
524
|
-
with -509. The verdict is the first `--host-profile`'s. A run that took no pin leaves an existing file alone.
|
|
525
|
-
|
|
526
|
-
### Qualifying every plugin on a machine
|
|
527
|
-
|
|
528
|
-
`plugin-qualify-sync` qualifies every plugin installed on the machine, and again each one whose files
|
|
529
|
-
change. With its pin file in plugin-hostd's settings, the daemon runs in its default mode (`require_pins 1`)
|
|
530
|
-
and chains any plugin that qualified, the image's own and any installed later:
|
|
531
|
-
|
|
532
|
-
```
|
|
533
|
-
pin_file /var/lib/omx/qualified.pins # in plugin-hostd.conf
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
The package ships it as a service that runs two minutes after boot and whenever a plugin directory
|
|
537
|
-
changes, once the directory has been quiet for a minute. A run finds every `.clap` under the CLAP
|
|
538
|
-
directories and every plugin of every bundle under the LV2 ones, and hashes each one's files as
|
|
539
|
-
plugin-hostd does for a pin. A plugin whose files hash as recorded is skipped. A new or changed one gets the
|
|
540
|
-
quick qualification through the probe runner, on the grid `grid` names (see "The measurement grid"): its pin, the RT-safety sweep, cost and latency, its
|
|
541
|
-
qualification record and its descriptor. The soak is not part of it, and every record it writes says so in
|
|
542
|
-
`notMeasured` (`hosting.stability.unsoaked`).
|
|
543
|
-
|
|
544
|
-
Each run of the runner (one cell of an LV2 plugin, one quantum of a CLAP one) has `budget_s` seconds, under `memory_limit`. One that crashes or hangs is recorded as `crashed` or `hung`, one that
|
|
545
|
-
fails the sweep is pinned `verdict=refused`, and the run goes on to the next. The pin file and the state are
|
|
546
|
-
written whole after every plugin, so a run cut short keeps what it finished. The last line counts the run:
|
|
547
|
-
|
|
548
|
-
```
|
|
549
|
-
qualified 2, skipped 0, refused 0, crashed 1, hung 1, unmeasured 0, distributed 0, 4.6 s
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
A plugin that is no longer found, or whose bytes changed, leaves nothing behind: its pin, its qualification
|
|
553
|
-
record (with the probe output copied beside it) and its descriptor are removed. Records the sync did not write
|
|
554
|
-
stay, and nothing is removed in a run that could not read a bundle, so a half-written install is no uninstall.
|
|
555
|
-
A run looks only where its settings point: a plugin outside every directory of its format that the run scans is
|
|
556
|
-
kept as it is (a run scoped to LV2 keeps every CLAP pin), and only one missing from a directory it scans is gone.
|
|
557
|
-
A running plugin-hostd is told with `pin_clear`.
|
|
558
|
-
|
|
559
|
-
When plugin-hostd answers on `hostd_port`, the changed pins are also sent to it with `pin_set` and apply at
|
|
560
|
-
once. plugin-hostd serves one controller at a time, so while one is connected the run says so and the pins
|
|
561
|
-
apply at the daemon's next start.
|
|
562
|
-
|
|
563
|
-
The settings are `/etc/omx/qualify.conf`, one `key value` per line, and each is also a flag
|
|
564
|
-
(`--budget-s 30`):
|
|
565
|
-
|
|
566
|
-
| key | default |
|
|
567
|
-
| --- | --- |
|
|
568
|
-
| `lv2_path` | `$LV2_PATH`, else plugin-hostd's: `/usr/lib64/lv2:/usr/lib/lv2:/usr/local/lib/lv2` |
|
|
569
|
-
| `lv2_extra` | `/zynthian/zynthian-plugins/lv2:/zynthian/zynthian-my-data/plugins/lv2` |
|
|
570
|
-
| `clap_path`, `clap_extra` | `$CLAP_PATH`, then `~/.clap`, then `/usr/lib/clap`; none |
|
|
571
|
-
| `pin_file` | `/var/lib/omx/qualified.pins` |
|
|
572
|
-
| `qualification_dir`, `descriptor_dir` | `/var/lib/omx/qualification`, `/var/lib/omx/descriptors` |
|
|
573
|
-
| `state_file` | `/var/lib/omx/qualify-sync.json` |
|
|
574
|
-
| `budget_s` | `60` |
|
|
575
|
-
| `host_profile` | `omx-clap-host` |
|
|
576
|
-
| `grid` | `full`: every rate at every block size, 16 cells (`hosts`, or `--grid-hosts`: the cells of `host_profile` and of the console's two tiers, then 512 at every rate; or `<rate>/<quantum>,...`) |
|
|
577
|
-
| `cells_per_plugin_budget` | `0`, every cell of the grid |
|
|
578
|
-
| `memory_limit` | half of the machine's RAM (`off` for none) |
|
|
579
|
-
| `memory_notes` | `16`: the notes an instrument with no declared capacity holds in a `--memory-only` run |
|
|
580
|
-
| `hostd_port`, `hostd_feedback_port` | `5555`, the next port |
|
|
581
|
-
| `host_class` | the board's own, from the device tree (`rpi5`); none elsewhere |
|
|
582
|
-
| `distributed_dir` | `/usr/share/omx/qualification` |
|
|
583
|
-
|
|
584
|
-
Those are the service's (`--system`); run by a user, the files go under `$XDG_STATE_HOME/omx`. An LV2
|
|
585
|
-
directory added here belongs in plugin-hostd's `lv2_path` too, or the daemon cannot find the bundle it
|
|
586
|
-
pinned. `--force` qualifies everything again, and `--no-distributed` reads no distributed record. A second `plugin-qualify-sync` waits for the run in progress (a lock on `<state_file>.lock`); `--no-wait` exits 0 at once and leaves a marker, and the holder runs once more at its end over what changed meanwhile.
|
|
38
|
+
Exit code: `0` every plugin qualified, `1` some did not, `2` something could not be measured.
|
|
39
|
+
`man plugin-qualify` and `man plugin-qualify-sync` describe the commands.
|
|
587
40
|
|
|
588
|
-
|
|
589
|
-
written before 0.17.3 lacks the preset files an LV2's manifest names, which plugin-hostd 0.15.0 also checks, and its add
|
|
590
|
-
answers -509 until then. The new list replaces the old one when no file both lists hold changed its hash, so the verdict
|
|
591
|
-
stands; a changed file is measured again by the run. `--refresh-pins` does only that, writes the pin file and sends the
|
|
592
|
-
changed pins to a running plugin-hostd; the package's upgrade runs it (plugin-qualify#239).
|
|
41
|
+
## Documentation
|
|
593
42
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
The path unit watches a folder, not the bundles inside it: a new bundle appearing in a watched folder is
|
|
600
|
-
seen at once, but a package's own files being replaced in place (an upgrade) is not a change of the folder
|
|
601
|
-
itself. A packaged plugin is still caught at once, by the distribution's install triggers over the standard
|
|
602
|
-
folders. An unpackaged bundle replaced in place in an extra folder is caught at the next run of
|
|
603
|
-
plugin-qualify-sync.timer, at most about 15 minutes after the change.
|
|
604
|
-
|
|
605
|
-
### Keeping the records through an upgrade
|
|
606
|
-
|
|
607
|
-
An upgrade of plugin-qualify, of the plugins' libraries or of the judge's rules does not call for measuring every plugin
|
|
608
|
-
again. The figures in the records are carried across and judged again:
|
|
609
|
-
|
|
610
|
-
```bash
|
|
611
|
-
plugin-qualify-sync --system --rejudge
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
`--rejudge` measures nothing and needs no measuring tool. It reads every record in `qualification_dir` with the probe
|
|
615
|
-
output it names, and passes them through the same judge a measurement goes through, under the rules of the running
|
|
616
|
-
version: a real-time violation under `run()` or under notes is `conditional` and pinned `isolated`, each host's budget is
|
|
617
|
-
read, and a plugin with no footprint is `unknown` on a host with a memory budget, never read as fitting. It also judges
|
|
618
|
-
each record for `omx-clap-host`, `openmixer-console` and the Pi boards `openmixer-pi-4gb` and `openmixer-pi-8gb`. The
|
|
619
|
-
verdicts, the tiers and the plugin's pin word are replaced; the figures, the probe output and who took them stay as they
|
|
620
|
-
were. The record gains a `rejudged` block with `at`, `by` (the version that judged) and `figures` (`tool`, `measuredAt`
|
|
621
|
-
and `runId`, as measured). A run that finds nothing to change writes nothing.
|
|
622
|
-
|
|
623
|
-
A verdict never says more than the figures do. An LV2 plugin's `omx-clap-host` verdict is decided by its run under the LV2
|
|
624
|
-
adapter (the record's `adapter` block). A record with no measured adapter run reads `unknown`
|
|
625
|
-
(`qualify.sync.adapter-run-owed`), is not offered on the isolated tier, and its pin has no verdict word: plugin-hostd loads
|
|
626
|
-
such a plugin without grouping it. The same goes for a `mod-host` or `jalv` verdict with no run under that host. A record
|
|
627
|
-
whose plugin's bytes changed, or whose probe output is not the copy it names, is left as it is. Everything left owing
|
|
628
|
-
is listed under `owes` in the record and in `qualify-owed.json` beside the state file (`--rejudge` prints each line too),
|
|
629
|
-
with the measurement it needs:
|
|
630
|
-
|
|
631
|
-
| `measurement` | code | what pays it |
|
|
632
|
-
|---|---|---|
|
|
633
|
-
| `adapter-run` | `qualify.sync.adapter-run-owed` | `--adapter-only`, then `--rejudge` |
|
|
634
|
-
| `host-run` | `qualify.sync.host-run-owed` | a run under that host; today, qualifying the plugin again with `--host-profile` |
|
|
635
|
-
| `qualification` | `qualify.sync.bytes-changed`, `qualify.sync.figures-unreadable`, `qualify.sync.no-figures` | qualifying the plugin again; the next run does so for changed bytes |
|
|
636
|
-
|
|
637
|
-
A probe that crashed took no figures: its record stays `unknown` with `probe.crashed`, no pin word and the state's
|
|
638
|
-
outcome `crashed`. A crash under a host's run stays `unsuitable` and `refused`.
|
|
639
|
-
|
|
640
|
-
Footprints older records lack are taken apart from everything else:
|
|
641
|
-
|
|
642
|
-
```bash
|
|
643
|
-
plugin-qualify-sync --system --memory-only # the footprints, at the cells a Pi runs
|
|
644
|
-
plugin-qualify-sync --system --rejudge # the verdicts that read them
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
`--memory-only` takes the footprint of each plugin the state holds (after instantiate, after activate, at rest, a second
|
|
648
|
-
instance, and notes held for an instrument) at 48 kHz in blocks of 128 and 256 frames, and nothing else: no sweep, no
|
|
649
|
-
cost, no latency, no lifecycle, no soak. It goes through the probe runner under the limits of an ordinary run (`budget_s`,
|
|
650
|
-
`memory_limit`), each plugin in processes of its own, an LV2 plugin in a world of its own bundle. An instrument that
|
|
651
|
-
declares its voice capacity holds that many notes; one that does not holds `memory_notes`, with basis `probe`. Only the
|
|
652
|
-
`memory` of those cells changes in the record, a cell it lacked joins with no other figure, and a `memoryRun` block says
|
|
653
|
-
which runner took them and when. A cell whose run failed holds a reason and no figure. A plugin whose footprints are
|
|
654
|
-
already merged is skipped, so a pass cut short can be run again; `--force` takes them again. The pass judges nothing:
|
|
655
|
-
`--rejudge` afterwards does.
|
|
656
|
-
|
|
657
|
-
The adapter runs a record owes are taken apart as well:
|
|
658
|
-
|
|
659
|
-
```bash
|
|
660
|
-
plugin-qualify-sync --system --adapter-only # the runs, at the cells a Pi runs
|
|
661
|
-
plugin-qualify-sync --system --rejudge # the verdicts they decide
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
`--isolated-only` runs the isolated step a record still owes: a plugin rated conditional, or one that crashed or hung in
|
|
665
|
-
the probe, added alone under a plugin-hostd of its own on a private JACK server, with a sine through it for 3 seconds. The
|
|
666
|
-
record's `run` keeps its id and gains the step, and the pin follows it; nothing else is measured. A step the machine could
|
|
667
|
-
not run stays owed.
|
|
668
|
-
|
|
669
|
-
`--adapter-only` starts each LV2 plugin the state holds under a fresh omx-clap-host, `add` then `remove` through the LV2
|
|
670
|
-
adapter, once per cell: 48 kHz in blocks of 128 and 256 frames. Each cell has a private JACK server (`jackd -d dummy`, so
|
|
671
|
-
`jackd` must be installed), so the device's live server is never touched. A command has `budget_s` seconds, the host's
|
|
672
|
-
address space is limited as a direct spawn's is, and it is killed with whatever it started. Only the record's `adapter`
|
|
673
|
-
block changes: `versions` (omx-clap-host and plugin-hostd, read from `--version`), `measuredAt`, `cells`, and at its top
|
|
674
|
-
what the judge reads. A crash or a command that gets no answer in time is the plugin's recorded outcome, at the cell it
|
|
675
|
-
happened, and reads `unsuitable` at the next `--rejudge`. A host that could not be run decides nothing: the block says
|
|
676
|
-
`measured: false`, and the run stays owed and is tried again. The pass refuses to start, writing nothing, when a host does
|
|
677
|
-
not say its version or there is no `jackd`. A plugin whose block was taken at these host versions is skipped, so a pass
|
|
678
|
-
cut short can be run again; a host upgrade or `--force` takes them again. A plugin whose probe failed is skipped: it has
|
|
679
|
-
no verdict a run could decide.
|
|
680
|
-
|
|
681
|
-
**A leak is not measured.** Growth over time takes a soak, which this pass does not run: every record it writes names
|
|
682
|
-
`memoryGrowth` under `notMeasured`, and the last line says `leaks not measured`.
|
|
683
|
-
|
|
684
|
-
On a Raspberry Pi 5 a plugin takes under 0.6 s at the median, 5 s at the 90th percentile and 22 s at the 99th; about an
|
|
685
|
-
hour at most for the 1,542 plugins of the Zynthian set. Those are upper bounds: twice the time the 2026-10-28 records say a
|
|
686
|
-
whole pass at one cell took, which also ran 30 lifecycles, the sweep, the latency and the cost. A plugin that hangs
|
|
687
|
-
takes `budget_s` a cell. The test fixtures take 0.1 s each, both cells, on an x86-64 desktop.
|
|
688
|
-
|
|
689
|
-
### Distributed records
|
|
690
|
-
|
|
691
|
-
A device that ships with a known plugin set need not measure it again at first start. The qualification records
|
|
692
|
-
another device of the same machine class wrote for that set are installed read-only, and the sync takes from them
|
|
693
|
-
every plugin whose files are the bytes that were measured:
|
|
694
|
-
|
|
695
|
-
```
|
|
696
|
-
/usr/share/omx/qualification/<host class>/<set>/ # system (--system)
|
|
697
|
-
$XDG_DATA_HOME/omx/qualification/<host class>/<set>/ # a user run, then each $XDG_DATA_DIRS/omx/qualification
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
Each `<set>` directory holds the records (`*.qualification.json`, each with its probe output `*.probe.json`) and a
|
|
701
|
-
`provenance.json` that says where they come from: the set, the host class, the image, the package versions of the
|
|
702
|
-
measuring device, the date and the package that installed them. The host class is a directory name: `host_class`
|
|
703
|
-
in the settings, or, when it is unset, the board's own name read from the device tree (`rpi5` on a Raspberry Pi 5,
|
|
704
|
-
`rpi4` on a Raspberry Pi 4). Where neither gives one, the layer is not read and the run says so.
|
|
705
|
-
|
|
706
|
-
Before a plugin is measured, the sync looks for a distributed record of it: a CLAP record whose binary is the same
|
|
707
|
-
`.clap` path, or an LV2 record of the same plugin uri. The name only finds the record; a record applies when, and
|
|
708
|
-
only when:
|
|
709
|
-
|
|
710
|
-
- every file of its `binary.files` is installed at that path relative to the plugin's root, with that SHA-256,
|
|
711
|
-
computed on the device;
|
|
712
|
-
- its schema is the one this version writes and its probe output is one this version's reader accepts, the copy
|
|
713
|
-
intact (its SHA-256 the one the record names): the same acceptance the sync gives a record of its own, which it
|
|
714
|
-
never measures again for a newer tool;
|
|
715
|
-
- it holds a verdict for the sync's `host_profile`, and that verdict ended `qualified`, `refused`, `crashed` or
|
|
716
|
-
`hung`. One that could not be measured at the source (`unmeasured`) is measured here.
|
|
717
|
-
|
|
718
|
-
A plugin whose distributed records apply is not measured: the records are copied into `qualification_dir` with the
|
|
719
|
-
set's `provenance` object added, its pin is taken from the probe output the record judged, its descriptor is
|
|
720
|
-
written, and the run says
|
|
721
|
-
|
|
722
|
-
```
|
|
723
|
-
distributed: http://distrho.sf.net/plugins/3BandEQ: applied (zynthian-vangelis)
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
A plugin with a distributed record whose files differ is measured as before, and the run says
|
|
727
|
-
`distributed: <uri>: binary differs`; one with no distributed record is measured without a word. The summary counts
|
|
728
|
-
the plugins taken from the layer, `distributed N`, apart from the ones measured and skipped. A later run treats an
|
|
729
|
-
applied plugin like one it measured: skipped while its files are unchanged, measured when they change. `--force`
|
|
730
|
-
measures everything and reads no distributed record; `--no-distributed` keeps the layer out of a run.
|
|
731
|
-
|
|
732
|
-
The package that installs a set is built from a device's `qualification_dir` by `plugin-qualify-records-package`:
|
|
733
|
-
|
|
734
|
-
```
|
|
735
|
-
plugin-qualify-records-package /var/lib/omx/qualification --host-class rpi5 --set zynthian-vangelis \
|
|
736
|
-
--provenance provenance.json --out .
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
It writes `omx-qualification-<host class>-<set>_<version>_all.deb`, the version the build date
|
|
740
|
-
(`2026.10.29`) unless `--version` names one. The package installs the records under
|
|
741
|
-
`/usr/share/omx/qualification/<host class>/<set>/` with `provenance.json`, depends on the plugin-qualify that reads
|
|
742
|
-
the layer, and is `Architecture: all`. `--provenance` names a JSON file (or holds the JSON itself) with what the tool
|
|
743
|
-
cannot know: the image, the package versions and the source revisions of the device. The tool adds the set, the host
|
|
744
|
-
class, the package and the date. A record whose probe output is missing or does not match its SHA-256 is left out
|
|
745
|
-
and named; a record of a plugin that could not be measured (`*.unmeasured.json`) is left out, since it never
|
|
746
|
-
applies. The tool builds the Debian package only; an RPM of a set is not built yet.
|
|
747
|
-
|
|
748
|
-
### The container image
|
|
749
|
-
|
|
750
|
-
```
|
|
751
|
-
podman build -t plugin-qualify -f Containerfile .
|
|
752
|
-
podman run --rm -v "$PWD/my-plugins:/plugins:ro" -e LV2_PATH=/plugins -v "$PWD/out:/out" \
|
|
753
|
-
plugin-qualify --host-profile jalv --all --out /out
|
|
754
|
-
```
|
|
43
|
+
<https://www.openmixer.org/projects/plugin-qualify>, and in this repository the full guide
|
|
44
|
+
([docs/overview.md](docs/overview.md)): the measurement grid, the verdicts and what they mean,
|
|
45
|
+
host profiles, the JSON document, qualifying every plugin on a machine and keeping the records.
|
|
46
|
+
Building from a checkout: [BUILDING.md](BUILDING.md).
|
|
755
47
|
|
|
756
|
-
|
|
48
|
+
## Licence
|
|
757
49
|
|
|
758
|
-
|
|
759
|
-
verdict, the document and the exit code. The command-line entry point does not yet drive the
|
|
760
|
-
measuring tools (`tools/lv2-measure.mjs`, the mod-host sweep). A run that would measure exits
|
|
761
|
-
`2` and names the missing step, rather than rate a plugin on less than was asked.
|
|
50
|
+
GPL-3.0-or-later. See [LICENSE](LICENSE).
|