@openmixer/plugin-qualify 0.17.5 → 0.18.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 +127 -13
- package/bin/plugin-qualify-sync.mjs +3 -1
- package/dist/clap-qualify.d.ts +3 -1
- package/dist/clap-qualify.js +7 -1
- package/dist/clap-qualify.js.map +1 -1
- package/dist/direct-limit.d.ts +27 -0
- package/dist/direct-limit.js +52 -0
- package/dist/direct-limit.js.map +1 -0
- package/dist/distributed-records.d.ts +4 -2
- package/dist/distributed-records.js +1 -1
- package/dist/distributed-records.js.map +1 -1
- package/dist/host-admission.d.ts +12 -2
- package/dist/host-admission.js +24 -4
- package/dist/host-admission.js.map +1 -1
- package/dist/host-profiles.d.ts +8 -0
- package/dist/host-profiles.js +16 -1
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-codes.generated.d.ts +10 -0
- package/dist/hosting-codes.generated.js +10 -0
- package/dist/hosting-codes.generated.js.map +1 -1
- package/dist/hosting-suitability.d.ts +34 -8
- package/dist/hosting-suitability.js +92 -7
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/measurement/format.d.ts +54 -8
- package/dist/measurement/format.js +54 -3
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/plan.d.ts +3 -3
- package/dist/measurement/plan.js +1 -1
- package/dist/measurement-grid.d.ts +8 -0
- package/dist/measurement-grid.js +4 -0
- package/dist/measurement-grid.js.map +1 -1
- package/dist/memory-bound.d.ts +6 -0
- package/dist/memory-bound.js +8 -0
- package/dist/memory-bound.js.map +1 -1
- package/dist/memory-budget.d.ts +39 -0
- package/dist/memory-budget.js +32 -0
- package/dist/memory-budget.js.map +1 -0
- package/dist/probe-judge.d.ts +11 -0
- package/dist/probe-judge.js +26 -10
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-merge.js +6 -5
- package/dist/probe-merge.js.map +1 -1
- package/dist/probe-output-fixtures.d.ts +6 -0
- package/dist/probe-output-fixtures.js +8 -0
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +4 -2
- package/dist/probe-output.js +2 -1
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.js +7 -3
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +116 -10
- package/dist/qualification-record.js +34 -30
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-adapter-pass.d.ts +67 -0
- package/dist/qualify-adapter-pass.js +255 -0
- package/dist/qualify-adapter-pass.js.map +1 -0
- package/dist/qualify-declarations.d.ts +53 -9
- package/dist/qualify-declarations.js +54 -2
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-rejudge.d.ts +81 -0
- package/dist/qualify-rejudge.js +421 -0
- package/dist/qualify-rejudge.js.map +1 -0
- package/dist/qualify-sync.d.ts +72 -1
- package/dist/qualify-sync.js +194 -21
- package/dist/qualify-sync.js.map +1 -1
- package/fixtures/records-0.16.1/http___example.org_raffo@unversioned-2060c09ff626.default.probe.json +46 -0
- package/fixtures/records-0.16.1/http___example.org_raffo@unversioned-2060c09ff626.default.qualification.json +185 -0
- package/fixtures/records-0.16.1/http___lv2plug.in_plugins_eg-amp@unversioned-f046a60efd45.default.probe.json +51 -0
- package/fixtures/records-0.16.1/http___lv2plug.in_plugins_eg-amp@unversioned-f046a60efd45.default.qualification.json +232 -0
- package/fixtures/records-0.16.1/https___github.com_lucianodato_noise-repellent@unversioned-c3b5e21dd9a7.default.probe.json +51 -0
- package/fixtures/records-0.16.1/https___github.com_lucianodato_noise-repellent@unversioned-c3b5e21dd9a7.default.qualification.json +245 -0
- package/fixtures/records-0.16.1/in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.probe.json +52 -0
- package/fixtures/records-0.16.1/in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.qualification.json +145 -0
- package/fixtures/records-0.16.1/in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.probe.json +52 -0
- package/fixtures/records-0.16.1/in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.qualification.json +155 -0
- package/package.json +1 -1
- package/src/clap-qualify.ts +8 -1
- package/src/direct-limit.ts +68 -0
- package/src/distributed-records.ts +1 -1
- package/src/host-admission.ts +27 -4
- package/src/host-profiles.ts +17 -1
- package/src/hosting-codes.generated.ts +10 -0
- package/src/hosting-suitability.ts +105 -11
- package/src/index.ts +3 -0
- package/src/measurement/format.ts +97 -3
- package/src/measurement/plan.ts +3 -3
- package/src/measurement-grid.ts +5 -0
- package/src/memory-bound.ts +10 -0
- package/src/memory-budget.ts +55 -0
- package/src/probe-judge.ts +29 -9
- package/src/probe-merge.ts +6 -5
- package/src/probe-output-fixtures.ts +10 -1
- package/src/probe-output.ts +5 -2
- package/src/probe-runner.ts +5 -3
- package/src/qualification-record.ts +121 -18
- package/src/qualify-adapter-pass.ts +278 -0
- package/src/qualify-declarations.ts +67 -3
- package/src/qualify-rejudge.ts +452 -0
- package/src/qualify-sync.ts +210 -20
- package/tools/_declarations.py +1 -0
- package/tools/benchmark.py +25 -2
- package/tools/gen-python-declarations.mjs +1 -0
- package/tools/lv2-measure.mjs +7 -4
- package/tools/lv2_presets.py +1 -1
- package/tools/lv2_resolve.py +49 -0
- package/tools/mod-host-admit.mjs +23 -6
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "plugin-qualify.qualification-record/1",
|
|
3
|
+
"plugin": {
|
|
4
|
+
"format": "clap",
|
|
5
|
+
"id": "in.lsp-plug.impulse_responses_mono"
|
|
6
|
+
},
|
|
7
|
+
"binary": {
|
|
8
|
+
"path": "/usr/lib/clap/lsp-plugins.clap",
|
|
9
|
+
"sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2",
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "lsp-plugins.clap",
|
|
13
|
+
"sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"layout": {
|
|
17
|
+
"scheme": "omx-layout/1",
|
|
18
|
+
"sha256": "86f71f835f0db0150b6b219b738b97ae0d9d423d2305e29ebdb172b8e20a6381"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"verdicts": [
|
|
22
|
+
{
|
|
23
|
+
"host": "omx-clap-host",
|
|
24
|
+
"measured": true,
|
|
25
|
+
"rating": "unsuitable",
|
|
26
|
+
"qualified": false,
|
|
27
|
+
"hosting": "isolated",
|
|
28
|
+
"arm": "declaration",
|
|
29
|
+
"deciding": "hosting.rt-safety.violations-in-run",
|
|
30
|
+
"reasons": [
|
|
31
|
+
{
|
|
32
|
+
"dimension": "rtSafety",
|
|
33
|
+
"rating": "unsuitable",
|
|
34
|
+
"code": "hosting.rt-safety.violations-in-run",
|
|
35
|
+
"params": {
|
|
36
|
+
"allocations": 0,
|
|
37
|
+
"syscalls": 1,
|
|
38
|
+
"locks": 0
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"tiers": {
|
|
45
|
+
"isolated": {
|
|
46
|
+
"host": "omx-clap-host",
|
|
47
|
+
"measured": true,
|
|
48
|
+
"qualified": false,
|
|
49
|
+
"rating": "unsuitable",
|
|
50
|
+
"deciding": "hosting.rt-safety.violations-in-run"
|
|
51
|
+
},
|
|
52
|
+
"offeredOn": [],
|
|
53
|
+
"dimmed": "omx-clap-host: hosting.rt-safety.violations-in-run"
|
|
54
|
+
},
|
|
55
|
+
"cost": [
|
|
56
|
+
{
|
|
57
|
+
"host": "omx-clap-host",
|
|
58
|
+
"rate": 48000,
|
|
59
|
+
"quantum": 128,
|
|
60
|
+
"reasons": [
|
|
61
|
+
{
|
|
62
|
+
"dimension": "cost",
|
|
63
|
+
"rating": "unknown",
|
|
64
|
+
"code": "hosting.cost.unmeasured-at-rate-and-quantum",
|
|
65
|
+
"params": {
|
|
66
|
+
"rate": 48000,
|
|
67
|
+
"quantum": 128
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"measurements": {
|
|
74
|
+
"grid": [
|
|
75
|
+
{
|
|
76
|
+
"rate": 48000,
|
|
77
|
+
"quantum": 512,
|
|
78
|
+
"outcome": "probe.ok",
|
|
79
|
+
"latency": {
|
|
80
|
+
"declaredFrames": 0,
|
|
81
|
+
"measuredFrames": 0
|
|
82
|
+
},
|
|
83
|
+
"cost": {
|
|
84
|
+
"median": 213.506,
|
|
85
|
+
"percentile": {
|
|
86
|
+
"p": 95,
|
|
87
|
+
"value": 230.143
|
|
88
|
+
},
|
|
89
|
+
"blocks": 512,
|
|
90
|
+
"warmupBlocks": 64
|
|
91
|
+
},
|
|
92
|
+
"rtSafety": {
|
|
93
|
+
"interposer": true,
|
|
94
|
+
"swept": true,
|
|
95
|
+
"sweepApplicable": true,
|
|
96
|
+
"blocks": 512,
|
|
97
|
+
"allocations": 0,
|
|
98
|
+
"locks": 0,
|
|
99
|
+
"syscalls": 1,
|
|
100
|
+
"threadViolations": 0,
|
|
101
|
+
"processErrors": 0,
|
|
102
|
+
"events": [
|
|
103
|
+
"nanosleep"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"noteSweep": {
|
|
107
|
+
"applicable": false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
"notMeasured": [
|
|
113
|
+
{
|
|
114
|
+
"dimension": "stability",
|
|
115
|
+
"code": "hosting.stability.unsoaked"
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"tool": {
|
|
119
|
+
"runner": "plugin-hostd-probe 0.13.1",
|
|
120
|
+
"child": "clap-qualify",
|
|
121
|
+
"interposer": "rt_interpose.so"
|
|
122
|
+
},
|
|
123
|
+
"measuredAt": "2026-10-28T20:56:58+00:00",
|
|
124
|
+
"probe": {
|
|
125
|
+
"file": "in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.probe.json",
|
|
126
|
+
"sha256": "bc818dfeff46238de3b5558ab8d54aa5677a18f6e3cad79c67bc117313d0a79d",
|
|
127
|
+
"bytes": 3894,
|
|
128
|
+
"protocol": {
|
|
129
|
+
"name": "plugin-hostd.probe-protocol",
|
|
130
|
+
"version": 1
|
|
131
|
+
},
|
|
132
|
+
"reason": "on-request",
|
|
133
|
+
"outcome": "probe.ok",
|
|
134
|
+
"complete": true,
|
|
135
|
+
"machineClass": {
|
|
136
|
+
"arch": "aarch64",
|
|
137
|
+
"family": "arm",
|
|
138
|
+
"coreBand": "3-4",
|
|
139
|
+
"clockBand": "1.5-2.5GHz",
|
|
140
|
+
"preemptRt": false,
|
|
141
|
+
"cstatesLimited": false
|
|
142
|
+
},
|
|
143
|
+
"meetsProtocol": false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "plugin-hostd.probe-output",
|
|
3
|
+
"formatVersion": "1.1",
|
|
4
|
+
"run": {
|
|
5
|
+
"id": "20261028T210152Z-321dd-bb8cc3c7",
|
|
6
|
+
"startedAt": "2026-10-28T21:01:52+00:00",
|
|
7
|
+
"elapsedMs": 332,
|
|
8
|
+
"complete": true,
|
|
9
|
+
"outcome": "probe.ok",
|
|
10
|
+
"reason": "on-request",
|
|
11
|
+
"protocol": {"name": "plugin-hostd.probe-protocol", "version": 1},
|
|
12
|
+
"tools": {"runner": "plugin-hostd-probe 0.13.1", "child": "clap-qualify", "interposer": "rt_interpose.so"},
|
|
13
|
+
"via": "runner"
|
|
14
|
+
},
|
|
15
|
+
"plugin": {"format": "clap", "id": "in.lsp-plug.trigger_midi_mono"},
|
|
16
|
+
"binary": {"path": "/usr/lib/clap/lsp-plugins.clap", "sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2", "files": [{"path": "lsp-plugins.clap", "sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2"}], "layout": {"scheme": "omx-layout/1", "sha256": "5a2b073f777379e75dcd931ff9930352d12dcfab9e1fb599d3b1d45d62fe3e1d"}},
|
|
17
|
+
"machine": {
|
|
18
|
+
"class": {"arch": "aarch64", "family": "arm", "coreBand": "3-4", "clockBand": "1.5-2.5GHz", "preemptRt": false, "cstatesLimited": false},
|
|
19
|
+
"profile": {"governor": "powersave", "turbo": false, "smt": false, "preempt": "PREEMPT",
|
|
20
|
+
"cpuidle": [],
|
|
21
|
+
"meetsProtocol": false, "unmet": ["probe.profile.governor", "probe.profile.not-pinned"]},
|
|
22
|
+
"pinnedCpus": "0-3"
|
|
23
|
+
},
|
|
24
|
+
"offer": {"id": "custom", "clapExtensions": ["clap.log", "clap.thread-check", "clap.latency", "clap.params", "clap.audio-ports", "clap.state", "clap.note-ports", "clap.note-name", "clap.voice-info", "clap.event-registry", "clap.timer-support", "clap.posix-fd-support", "clap.thread-pool", "clap.tail", "clap.surround/4", "clap.ambisonic/3", "clap.audio-ports-config", "clap.preset-load/2", "clap.track-info/1", "clap.remote-controls/2"], "realtime": "none"},
|
|
25
|
+
"limits": {"set": "default",
|
|
26
|
+
"applied": [
|
|
27
|
+
{"limit": "wall-clock", "value": 60, "unit": "s", "by": "supervisor"},
|
|
28
|
+
{"limit": "cpu-time", "value": 540, "unit": "s", "by": "rlimit"},
|
|
29
|
+
{"limit": "memory", "value": 4294967296, "unit": "bytes", "by": "rlimit"},
|
|
30
|
+
{"limit": "files", "value": 256, "unit": "count", "by": "rlimit"},
|
|
31
|
+
{"limit": "file-size", "value": 67108864, "unit": "bytes", "by": "rlimit"},
|
|
32
|
+
{"limit": "core-dump", "value": 0, "unit": "bytes", "by": "rlimit"},
|
|
33
|
+
{"limit": "rt-priority", "value": 0, "by": "rlimit"},
|
|
34
|
+
{"limit": "no-new-privs", "by": "prctl"}
|
|
35
|
+
],
|
|
36
|
+
"unavailable": [
|
|
37
|
+
{"limit": "tasks", "code": "probe.limit.not-implemented"}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"steps": [
|
|
41
|
+
{"step": "hash", "outcome": "probe.ok", "elapsedMs": 0},
|
|
42
|
+
{"step": "lifecycle", "outcome": "probe.ok", "elapsedMs": 332, "exitStatus": 0},
|
|
43
|
+
{"step": "instantiate", "outcome": "probe.ok", "elapsedMs": 0, "detail": "read from the child's line"},
|
|
44
|
+
{"step": "latency", "rate": 48000, "quantum": 512, "outcome": "probe.ok", "elapsedMs": 0, "detail": "read from the child's line"},
|
|
45
|
+
{"step": "rt-safety", "outcome": "probe.ok", "elapsedMs": 0, "detail": "counts are totals over every rate, read from the child's line"},
|
|
46
|
+
{"step": "note-sweep", "outcome": "probe.ok", "elapsedMs": 0, "detail": "counts are totals over every rate, read from the child's line"}
|
|
47
|
+
],
|
|
48
|
+
"grid": [
|
|
49
|
+
{"rate": 48000, "quantum": 512, "outcome": "probe.ok", "latency": {"declaredFrames": 0, "measuredFrames": 0, "method": "impulse-onset"}, "cost": {"unit": "ns-per-sample", "median": 47.9961, "percentile": {"p": 95, "value": 57.2365}, "min": 31.5742, "max": 114.258, "blocks": 512, "warmupBlocks": 64}, "rtSafety": {"interposer": true, "swept": true, "sweepApplicable": true, "blocks": 512, "allocations": 0, "locks": 0, "syscalls": 0, "threadViolations": 0, "processErrors": 0, "events": []}, "noteSweep": {"applicable": true, "dialect": "midi", "allocations": 0, "locks": 0, "syscalls": 1}}
|
|
50
|
+
],
|
|
51
|
+
"logs": [{"name": "stderr", "path": "/tmp/plugin-qualify-probe-0t8B87/child.stderr.log", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "bytes": 0}]
|
|
52
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "plugin-qualify.qualification-record/1",
|
|
3
|
+
"plugin": {
|
|
4
|
+
"format": "clap",
|
|
5
|
+
"id": "in.lsp-plug.trigger_midi_mono"
|
|
6
|
+
},
|
|
7
|
+
"binary": {
|
|
8
|
+
"path": "/usr/lib/clap/lsp-plugins.clap",
|
|
9
|
+
"sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2",
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "lsp-plugins.clap",
|
|
13
|
+
"sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"layout": {
|
|
17
|
+
"scheme": "omx-layout/1",
|
|
18
|
+
"sha256": "5a2b073f777379e75dcd931ff9930352d12dcfab9e1fb599d3b1d45d62fe3e1d"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"verdicts": [
|
|
22
|
+
{
|
|
23
|
+
"host": "omx-clap-host",
|
|
24
|
+
"measured": true,
|
|
25
|
+
"rating": "unsuitable",
|
|
26
|
+
"qualified": false,
|
|
27
|
+
"hosting": "isolated",
|
|
28
|
+
"arm": "declaration",
|
|
29
|
+
"deciding": "hosting.rt-safety.violations-under-notes",
|
|
30
|
+
"reasons": [
|
|
31
|
+
{
|
|
32
|
+
"dimension": "rtSafety",
|
|
33
|
+
"rating": "suitable",
|
|
34
|
+
"code": "hosting.rt-safety.clean-under-sweep",
|
|
35
|
+
"params": {
|
|
36
|
+
"blocks": 512
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"dimension": "rtSafety",
|
|
41
|
+
"rating": "unsuitable",
|
|
42
|
+
"code": "hosting.rt-safety.violations-under-notes",
|
|
43
|
+
"params": {
|
|
44
|
+
"allocations": 0,
|
|
45
|
+
"syscalls": 1,
|
|
46
|
+
"locks": 0
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"tiers": {
|
|
53
|
+
"isolated": {
|
|
54
|
+
"host": "omx-clap-host",
|
|
55
|
+
"measured": true,
|
|
56
|
+
"qualified": false,
|
|
57
|
+
"rating": "unsuitable",
|
|
58
|
+
"deciding": "hosting.rt-safety.violations-under-notes"
|
|
59
|
+
},
|
|
60
|
+
"offeredOn": [],
|
|
61
|
+
"dimmed": "omx-clap-host: hosting.rt-safety.violations-under-notes"
|
|
62
|
+
},
|
|
63
|
+
"cost": [
|
|
64
|
+
{
|
|
65
|
+
"host": "omx-clap-host",
|
|
66
|
+
"rate": 48000,
|
|
67
|
+
"quantum": 128,
|
|
68
|
+
"reasons": [
|
|
69
|
+
{
|
|
70
|
+
"dimension": "cost",
|
|
71
|
+
"rating": "unknown",
|
|
72
|
+
"code": "hosting.cost.unmeasured-at-rate-and-quantum",
|
|
73
|
+
"params": {
|
|
74
|
+
"rate": 48000,
|
|
75
|
+
"quantum": 128
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"measurements": {
|
|
82
|
+
"grid": [
|
|
83
|
+
{
|
|
84
|
+
"rate": 48000,
|
|
85
|
+
"quantum": 512,
|
|
86
|
+
"outcome": "probe.ok",
|
|
87
|
+
"latency": {
|
|
88
|
+
"declaredFrames": 0,
|
|
89
|
+
"measuredFrames": 0
|
|
90
|
+
},
|
|
91
|
+
"cost": {
|
|
92
|
+
"median": 47.9961,
|
|
93
|
+
"percentile": {
|
|
94
|
+
"p": 95,
|
|
95
|
+
"value": 57.2365
|
|
96
|
+
},
|
|
97
|
+
"blocks": 512,
|
|
98
|
+
"warmupBlocks": 64
|
|
99
|
+
},
|
|
100
|
+
"rtSafety": {
|
|
101
|
+
"interposer": true,
|
|
102
|
+
"swept": true,
|
|
103
|
+
"sweepApplicable": true,
|
|
104
|
+
"blocks": 512,
|
|
105
|
+
"allocations": 0,
|
|
106
|
+
"locks": 0,
|
|
107
|
+
"syscalls": 0,
|
|
108
|
+
"threadViolations": 0,
|
|
109
|
+
"processErrors": 0,
|
|
110
|
+
"events": []
|
|
111
|
+
},
|
|
112
|
+
"noteSweep": {
|
|
113
|
+
"applicable": true,
|
|
114
|
+
"dialect": "midi",
|
|
115
|
+
"allocations": 0,
|
|
116
|
+
"locks": 0,
|
|
117
|
+
"syscalls": 1
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"notMeasured": [
|
|
123
|
+
{
|
|
124
|
+
"dimension": "stability",
|
|
125
|
+
"code": "hosting.stability.unsoaked"
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"tool": {
|
|
129
|
+
"runner": "plugin-hostd-probe 0.13.1",
|
|
130
|
+
"child": "clap-qualify",
|
|
131
|
+
"interposer": "rt_interpose.so"
|
|
132
|
+
},
|
|
133
|
+
"measuredAt": "2026-10-28T21:01:52+00:00",
|
|
134
|
+
"probe": {
|
|
135
|
+
"file": "in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.probe.json",
|
|
136
|
+
"sha256": "17986ad33bc5087175fcce9512761cc28fb66d18beb01598ee2e72b5c5302fa6",
|
|
137
|
+
"bytes": 3978,
|
|
138
|
+
"protocol": {
|
|
139
|
+
"name": "plugin-hostd.probe-protocol",
|
|
140
|
+
"version": 1
|
|
141
|
+
},
|
|
142
|
+
"reason": "on-request",
|
|
143
|
+
"outcome": "probe.ok",
|
|
144
|
+
"complete": true,
|
|
145
|
+
"machineClass": {
|
|
146
|
+
"arch": "aarch64",
|
|
147
|
+
"family": "arm",
|
|
148
|
+
"coreBand": "3-4",
|
|
149
|
+
"clockBand": "1.5-2.5GHz",
|
|
150
|
+
"preemptRt": false,
|
|
151
|
+
"cstatesLimited": false
|
|
152
|
+
},
|
|
153
|
+
"meetsProtocol": false
|
|
154
|
+
}
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/plugin-qualify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Qualify audio plugins for real-time hosting: scan, offline latency and cost measurement, and a fail-closed rating.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
package/src/clap-qualify.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import type { QualifyVerdict } from "./qualify-cli.js";
|
|
37
37
|
import { RATING_SEVERITY, worstRating, type SuitabilityRating } from "./rating.js";
|
|
38
38
|
import { halfPopulatedRefusal, locateNativeTools } from "./tool-paths.js";
|
|
39
|
-
import { readIntegrity, type IntegrityMeasurement, type VoicesHeld } from "./measurement/format.js";
|
|
39
|
+
import { readIntegrity, readMemoryFootprint, type IntegrityMeasurement, type MemoryFootprint, type VoicesHeld } from "./measurement/format.js";
|
|
40
40
|
import { isVoiceBasis } from "./measurement/vocabulary.js";
|
|
41
41
|
import { coreFraction } from "./cost-fields.js";
|
|
42
42
|
|
|
@@ -103,6 +103,8 @@ export interface ClapCostReading {
|
|
|
103
103
|
/** The percentile `nsPerSampleP95` is taken at. */
|
|
104
104
|
readonly percentile: number;
|
|
105
105
|
readonly perRate: Readonly<Record<string, ClapCostAtRate>>;
|
|
106
|
+
/** What one instance held at each rate (`memory` on the prober's per-rate object), read apart from the timing. */
|
|
107
|
+
readonly memory?: Readonly<Record<string, MemoryFootprint>>;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
/** One plugin's line from `clap-qualify`. */
|
|
@@ -285,9 +287,12 @@ function readCost(v: unknown): ClapCostReading {
|
|
|
285
287
|
const perRateIn = v["perRate"];
|
|
286
288
|
if (!isRecord(perRateIn)) return bad("cost has no perRate map");
|
|
287
289
|
const perRate: Record<string, ClapCostAtRate> = {};
|
|
290
|
+
const memory: Record<string, MemoryFootprint> = {};
|
|
288
291
|
for (const [rate, at] of Object.entries(perRateIn)) {
|
|
289
292
|
if (!/^[1-9]\d*$/.test(rate)) return bad(`cost rate ${rate}`);
|
|
290
293
|
perRate[rate] = readCostAtRate(at, `cost at ${rate}`, stimulus === "notes-held");
|
|
294
|
+
const footprint = isRecord(at) ? readMemoryFootprint(at["memory"]) : undefined;
|
|
295
|
+
if (footprint !== undefined) memory[rate] = footprint;
|
|
291
296
|
}
|
|
292
297
|
return {
|
|
293
298
|
stimulus,
|
|
@@ -295,6 +300,7 @@ function readCost(v: unknown): ClapCostReading {
|
|
|
295
300
|
warmupBlocks: count(v, "warmupBlocks", "cost"),
|
|
296
301
|
percentile: figure(v, "percentile", "cost"),
|
|
297
302
|
perRate,
|
|
303
|
+
...(Object.keys(memory).length === 0 ? {} : { memory }),
|
|
298
304
|
};
|
|
299
305
|
}
|
|
300
306
|
|
|
@@ -338,6 +344,7 @@ export function qualifiedPluginOfClapLine(line: ClapQualifyLine, stability?: Plu
|
|
|
338
344
|
clap,
|
|
339
345
|
rtSafety: { ...rtSafety, noteSweep },
|
|
340
346
|
cpuCost: { perRate: clapCostPerRate(line.cost) },
|
|
347
|
+
...(line.cost?.memory === undefined ? {} : { memory: { perRate: line.cost.memory } }),
|
|
341
348
|
...(stability === undefined ? {} : { stability }),
|
|
342
349
|
};
|
|
343
350
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The hard limit of a process plugin-qualify spawns directly. Through plugin-hostd's runner a measured child has `memory.max`;
|
|
5
|
+
* a direct spawn has no runner, so it gets `RLIMIT_AS`, which needs no privilege. It is off for a sanitizer build, which
|
|
6
|
+
* reserves far more address space than it uses and would fail at start under any limit worth having.
|
|
7
|
+
*
|
|
8
|
+
* Hitting the limit is an outcome: the interposer ends the child with `MEMORY_BOUND_EXIT_STATUS` on the first refused
|
|
9
|
+
* allocation when `MEMORY_BOUND_EXIT_ENV` is set ({@link addressSpaceLimited} sets it), and {@link directOutcome} names it.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { totalmem } from "node:os";
|
|
13
|
+
|
|
14
|
+
import { ADDRESS_SPACE_LIMIT_RAM_FRACTION, MEMORY_BOUND_EXIT_ENV, MEMORY_BOUND_EXIT_STATUS } from "./qualify-declarations.js";
|
|
15
|
+
|
|
16
|
+
/** The outcome of a direct spawn that hit its limit. */
|
|
17
|
+
export const MEMORY_LIMIT_EXCEEDED = "memory-limit-exceeded";
|
|
18
|
+
export const ADDRESS_SPACE_MECHANISM = "rlimit-as";
|
|
19
|
+
|
|
20
|
+
const SANITIZER_ENTRY = ["__asan_init", "__tsan_init", "__msan_init"];
|
|
21
|
+
|
|
22
|
+
/** Whether the program is built with a sanitizer that reserves address space: its own init symbol is in the binary. */
|
|
23
|
+
export function isSanitizerBuild(tool: string, read: (path: string) => Buffer = (p) => readFileSync(p)): boolean {
|
|
24
|
+
let bytes: Buffer;
|
|
25
|
+
try {
|
|
26
|
+
bytes = read(tool);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return SANITIZER_ENTRY.some((s) => bytes.includes(s));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DirectSpawn {
|
|
34
|
+
readonly command: string;
|
|
35
|
+
readonly args: readonly string[];
|
|
36
|
+
readonly env: Readonly<Record<string, string>>;
|
|
37
|
+
/** The limit it runs under, in bytes; absent for a sanitizer build. */
|
|
38
|
+
readonly limitBytes?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The limit a direct spawn takes by default: `ADDRESS_SPACE_LIMIT_RAM_FRACTION` of the machine's RAM. */
|
|
42
|
+
export const defaultAddressSpaceLimit = (ramBytes: number = totalmem()): number => Math.floor(ramBytes * ADDRESS_SPACE_LIMIT_RAM_FRACTION);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* How to spawn `tool` under the limit: the shell sets it and execs the program, since node has no way to set a child's
|
|
46
|
+
* rlimit. The program's own process stays the one spawned, so a signal or a timeout reaches it as before.
|
|
47
|
+
*/
|
|
48
|
+
export function addressSpaceLimited(
|
|
49
|
+
tool: string,
|
|
50
|
+
args: readonly string[],
|
|
51
|
+
limitBytes: number = defaultAddressSpaceLimit(),
|
|
52
|
+
sanitizer: boolean = isSanitizerBuild(tool),
|
|
53
|
+
): DirectSpawn {
|
|
54
|
+
if (sanitizer) return { command: tool, args, env: {} };
|
|
55
|
+
const kb = Math.ceil(limitBytes / 2 ** 10);
|
|
56
|
+
return {
|
|
57
|
+
command: "/bin/sh",
|
|
58
|
+
args: ["-c", `ulimit -v ${String(kb)} && exec "$@"`, "sh", tool, ...args],
|
|
59
|
+
env: { [MEMORY_BOUND_EXIT_ENV]: "1" },
|
|
60
|
+
limitBytes,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What the end of a direct spawn under a limit comes to: the outcome and the limit when it hit it, else nothing. */
|
|
65
|
+
export function directOutcome(status: number | null, spawn: DirectSpawn): { readonly outcome: string; readonly memoryLimit: { readonly bytes: number; readonly mechanism: string } } | undefined {
|
|
66
|
+
if (spawn.limitBytes === undefined || status !== MEMORY_BOUND_EXIT_STATUS) return undefined;
|
|
67
|
+
return { outcome: MEMORY_LIMIT_EXCEEDED, memoryLimit: { bytes: spawn.limitBytes, mechanism: ADDRESS_SPACE_MECHANISM } };
|
|
68
|
+
}
|
|
@@ -153,7 +153,7 @@ export function distributedUri(item: SyncItem, r: DistributedRecord): string {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/** What the sync's own judgment would make of the record's verdict for the host, as `outcomeOf` reads a run. */
|
|
156
|
-
function outcomeOfVerdict(v: RecordVerdict): SyncOutcome {
|
|
156
|
+
export function outcomeOfVerdict(v: Pick<RecordVerdict, "measured" | "rating" | "deciding">): SyncOutcome {
|
|
157
157
|
if (!v.measured) return v.deciding === "probe.crashed" ? "crashed" : v.deciding === "probe.timeout" ? "hung" : "unmeasured";
|
|
158
158
|
if (v.rating === "suitable" || v.rating === "conditional") return "qualified";
|
|
159
159
|
return v.rating === "unsuitable" ? "refused" : "unmeasured";
|
package/src/host-admission.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { spawnSync } from "node:child_process";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
import type { HostProfile } from "./host-profiles.js";
|
|
15
|
+
import { ADMIT_STEP_MS_ENV } from "./qualify-declarations.js";
|
|
15
16
|
|
|
16
17
|
/** The profile ids whose verdict is measured by a run. */
|
|
17
18
|
export const ADMITTED_HOSTS = ["mod-host", "jalv"] as const;
|
|
@@ -24,11 +25,23 @@ export function admittedHostOf(profile: HostProfile): AdmittedHost | undefined {
|
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* plugin-hostd's LV2 adapter, omx-clap-host: an LV2 plugin is run under it once, as under mod-host, and the record
|
|
27
|
-
* carries the outcome as its own step
|
|
28
|
-
*
|
|
28
|
+
* carries the outcome as its own step (plugin-qualify#223). The run decides the omx-clap-host verdict of an LV2
|
|
29
|
+
* plugin, the isolated tier, as a mod-host run decides mod-host's; a CLAP plugin's stays the measurement.
|
|
29
30
|
*/
|
|
30
31
|
export const ADAPTER_HOST = "omx-clap-host";
|
|
31
32
|
|
|
33
|
+
/** The profiles whose LV2 plugins run under that adapter: omx-clap-host, and the Pi boards it runs on (`host-profiles.ts`). */
|
|
34
|
+
export const ADAPTER_RUN_HOSTS: readonly string[] = [ADAPTER_HOST, "openmixer-pi-4gb", "openmixer-pi-8gb"];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether this run decides the profile's verdict for a plugin of this format. A mod-host or jalv run always does, an
|
|
38
|
+
* unmeasured one making the verdict unknown; the adapter's only when it measured, since the omx-clap-host verdict
|
|
39
|
+
* otherwise stands on the measurement.
|
|
40
|
+
*/
|
|
41
|
+
export function runDecides(profile: HostProfile, format: string, admission: HostAdmission): boolean {
|
|
42
|
+
return admittedHostOf(profile) !== undefined || (ADAPTER_RUN_HOSTS.includes(profile.id) && format === "lv2" && admission.measured);
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
/** What running the plugin under one host came to. */
|
|
33
46
|
export interface HostAdmission {
|
|
34
47
|
readonly host: AdmittedHost | typeof ADAPTER_HOST;
|
|
@@ -62,12 +75,16 @@ export interface ModHostStep {
|
|
|
62
75
|
/** What `tools/mod-host-admit.mjs` printed. */
|
|
63
76
|
export interface ModHostRun {
|
|
64
77
|
readonly installed: boolean;
|
|
78
|
+
/** False when the host never took a connection (omx-clap-host with no JACK server): no plugin was loaded. */
|
|
79
|
+
readonly started?: boolean;
|
|
65
80
|
readonly steps: readonly ModHostStep[];
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
/** The verdict of a run of a host that speaks mod-host's protocol: the first step that did not answer `resp 0` decides. */
|
|
69
84
|
export function classifyProtocolRun(host: ProtocolHost, run: ModHostRun): HostAdmission {
|
|
70
85
|
if (!run.installed) return notInstalled(host);
|
|
86
|
+
// the host never listened, so the plugin was never loaded: nothing was learned about it
|
|
87
|
+
if (run.started === false) return { host, measured: false, admitted: false, code: `${host}.not-started`, params: {} };
|
|
71
88
|
for (const s of run.steps) {
|
|
72
89
|
if (s.died !== undefined) {
|
|
73
90
|
if (s.died.startsWith("SIG")) return refused(host, `signal-${s.died}`, { step: s.cmd });
|
|
@@ -131,7 +148,10 @@ const MOD_HOST_ADMIT = fileURLToPath(new URL("../tools/mod-host-admit.mjs", impo
|
|
|
131
148
|
/** The runs against the installed hosts. */
|
|
132
149
|
export const SPAWNED_ADMISSIONS: AdmissionRuns = {
|
|
133
150
|
adapter(uri, env) {
|
|
134
|
-
|
|
151
|
+
// two commands at the limit the pass gives each, and the tool's own start and end: it kills the host itself first
|
|
152
|
+
const stepMs = Number(env?.[ADMIT_STEP_MS_ENV] ?? 0);
|
|
153
|
+
const timeout = stepMs > 0 ? 2 * stepMs + 15000 : MOD_HOST_ADMISSION_MS;
|
|
154
|
+
const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri, ADAPTER_HOST], { encoding: "utf8", timeout, killSignal: "SIGKILL", ...(env === undefined ? {} : { env: { ...env } }) });
|
|
135
155
|
if (r.error !== undefined || r.status !== 0) {
|
|
136
156
|
throw new Error(`${ADAPTER_HOST} admission of ${uri} gave no answer (${r.error?.message ?? `exit ${String(r.status)}`}): ${(r.stderr ?? "").trim()}`);
|
|
137
157
|
}
|
|
@@ -145,7 +165,10 @@ export const SPAWNED_ADMISSIONS: AdmissionRuns = {
|
|
|
145
165
|
return JSON.parse(r.stdout) as ModHostRun;
|
|
146
166
|
},
|
|
147
167
|
jalv(uri, env) {
|
|
148
|
-
|
|
168
|
+
// SIGKILL at the deadline: jalv hosting some plugins ignores SIGTERM, and one stuck in jack_client_open never reaches
|
|
169
|
+
// the loop that reads the flag TERM sets, so a TERM left spawnSync waiting with it (#187). Alive at the deadline is
|
|
170
|
+
// all the verdict asks, so nothing is lost by not letting it exit cleanly.
|
|
171
|
+
const r = spawnSync("jalv", ["-n", "plugin-qualify", uri], { encoding: "utf8", timeout: JALV_HOSTED_MS, killSignal: "SIGKILL", input: "", ...(env === undefined ? {} : { env: { ...env } }) });
|
|
149
172
|
const code = (r.error as NodeJS.ErrnoException | undefined)?.code;
|
|
150
173
|
if (code === "ENOENT") return { installed: false, stillRunning: false, status: null, signal: null, stderr: "" };
|
|
151
174
|
return { installed: true, stillRunning: code === "ETIMEDOUT", status: r.status, signal: code === "ETIMEDOUT" ? null : r.signal, stderr: r.stderr ?? "" };
|
package/src/host-profiles.ts
CHANGED
|
@@ -164,8 +164,24 @@ export const OMX_CLAP_HOST_PROFILE: HostProfile = {
|
|
|
164
164
|
isolation: "shared-process",
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* A Raspberry Pi with 4 GB, and with 8 GB: what runs a plugin there is omx-clap-host under plugin-hostd, so the same
|
|
169
|
+
* extensions, instruments and isolation, on the rates, quanta and memory of the board. Its memory budget is provisional
|
|
170
|
+
* ({@link HOST_BUDGETS}), so a plugin under it is `conditional` until the numbers are measured on a Pi, and the reason says
|
|
171
|
+
* so. Judged at every cell the board runs.
|
|
172
|
+
*/
|
|
173
|
+
export const PI_4GB_PROFILE: HostProfile = { ...OMX_CLAP_HOST_PROFILE, id: "openmixer-pi-4gb", cost: HOST_BUDGETS["openmixer-pi-4gb"] };
|
|
174
|
+
export const PI_8GB_PROFILE: HostProfile = { ...OMX_CLAP_HOST_PROFILE, id: "openmixer-pi-8gb", cost: HOST_BUDGETS["openmixer-pi-8gb"] };
|
|
175
|
+
|
|
167
176
|
/** Every profile the tool ships, by id. */
|
|
168
|
-
export const HOST_PROFILES: readonly HostProfile[] = [
|
|
177
|
+
export const HOST_PROFILES: readonly HostProfile[] = [
|
|
178
|
+
OPENMIXER_CONSOLE_PROFILE,
|
|
179
|
+
JALV_PROFILE,
|
|
180
|
+
MOD_HOST_PROFILE,
|
|
181
|
+
OMX_CLAP_HOST_PROFILE,
|
|
182
|
+
PI_4GB_PROFILE,
|
|
183
|
+
PI_8GB_PROFILE,
|
|
184
|
+
];
|
|
169
185
|
|
|
170
186
|
/** A shipped profile by id, or `undefined`. */
|
|
171
187
|
export function shippedHostProfile(id: string): HostProfile | undefined {
|
|
@@ -32,6 +32,10 @@ export const HOSTING_CODE = Object.freeze({
|
|
|
32
32
|
"latencyParameterDependent": "hosting.latency.parameter-dependent",
|
|
33
33
|
"latencyResolved": "hosting.latency.resolved",
|
|
34
34
|
"latencyUnmeasured": "hosting.latency.unmeasured",
|
|
35
|
+
"memoryAboveCeiling": "hosting.memory.above-ceiling",
|
|
36
|
+
"memoryUnmeasured": "hosting.memory.unmeasured",
|
|
37
|
+
"memoryWithinCeiling": "hosting.memory.within-ceiling",
|
|
38
|
+
"memoryWithinProvisionalCeiling": "hosting.memory.within-provisional-ceiling",
|
|
35
39
|
"outputNonFinite": "hosting.output.non-finite",
|
|
36
40
|
"overrideIsolated": "hosting.override.curator-isolated",
|
|
37
41
|
"patchBusy": "hosting.patch.busy",
|
|
@@ -45,6 +49,9 @@ export const HOSTING_CODE = Object.freeze({
|
|
|
45
49
|
"noRealisation": "hosting.no-realisation",
|
|
46
50
|
"realisedInProcess": "hosting.realised.in-process",
|
|
47
51
|
"unjudged": "hosting.unjudged",
|
|
52
|
+
"resourceHintsOutOfRange": "hosting.resource-hints.out-of-range",
|
|
53
|
+
"resourceHintsUnknownHint": "hosting.resource-hints.unknown-hint",
|
|
54
|
+
"resourceHintsWrongUnit": "hosting.resource-hints.wrong-unit",
|
|
48
55
|
"rtSafetyClean": "hosting.rt-safety.clean-under-sweep",
|
|
49
56
|
"rtSafetyNotReproducible": "hosting.rt-safety.not-reproducible",
|
|
50
57
|
"rtSafetyNotesClean": "hosting.rt-safety.clean-under-notes",
|
|
@@ -81,4 +88,7 @@ export const HOSTING_CODE = Object.freeze({
|
|
|
81
88
|
"topologyWiderThanStrip": "hosting.topology.wider-than-strip",
|
|
82
89
|
"workerNonconforming": "hosting.worker.nonconforming",
|
|
83
90
|
"idleHoldRefused": "hosting.idle.hold-refused",
|
|
91
|
+
"windowNoDisplay": "hosting.window.no-display",
|
|
92
|
+
"windowUnsupported": "hosting.window.unsupported",
|
|
93
|
+
"windowRefused": "hosting.window.refused",
|
|
84
94
|
} as const);
|