@openmixer/plugin-qualify 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +203 -2
- package/bin/plugin-qualify.mjs +77 -0
- package/dist/clap-loader.d.ts +147 -0
- package/dist/clap-loader.js +252 -0
- package/dist/clap-loader.js.map +1 -0
- package/dist/clap-qualify.d.ts +126 -0
- package/dist/clap-qualify.js +272 -0
- package/dist/clap-qualify.js.map +1 -0
- package/dist/host-profiles.d.ts +42 -0
- package/dist/host-profiles.js +106 -0
- package/dist/host-profiles.js.map +1 -0
- package/dist/hosting-codes.d.ts +62 -0
- package/dist/hosting-codes.js +72 -0
- package/dist/hosting-codes.js.map +1 -0
- package/dist/hosting-suitability.d.ts +354 -0
- package/dist/hosting-suitability.js +654 -0
- package/dist/hosting-suitability.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +26 -0
- package/dist/loader.js +9 -0
- package/dist/loader.js.map +1 -0
- package/dist/lv2-loader.d.ts +7 -0
- package/dist/lv2-loader.js +50 -0
- package/dist/lv2-loader.js.map +1 -0
- package/dist/measurement/cli.d.ts +168 -0
- package/dist/measurement/cli.js +353 -0
- package/dist/measurement/cli.js.map +1 -0
- package/dist/measurement/fixtures.d.ts +23 -0
- package/dist/measurement/fixtures.js +129 -0
- package/dist/measurement/fixtures.js.map +1 -0
- package/dist/measurement/format.d.ts +483 -0
- package/dist/measurement/format.js +627 -0
- package/dist/measurement/format.js.map +1 -0
- package/dist/measurement/guards.d.ts +33 -0
- package/dist/measurement/guards.js +61 -0
- package/dist/measurement/guards.js.map +1 -0
- package/dist/measurement/index.d.ts +36 -0
- package/dist/measurement/index.js +39 -0
- package/dist/measurement/index.js.map +1 -0
- package/dist/measurement/merge.d.ts +143 -0
- package/dist/measurement/merge.js +315 -0
- package/dist/measurement/merge.js.map +1 -0
- package/dist/measurement/plan.d.ts +127 -0
- package/dist/measurement/plan.js +141 -0
- package/dist/measurement/plan.js.map +1 -0
- package/dist/measurement/probe-json.d.ts +61 -0
- package/dist/measurement/probe-json.js +253 -0
- package/dist/measurement/probe-json.js.map +1 -0
- package/dist/measurement/provenance.d.ts +91 -0
- package/dist/measurement/provenance.js +173 -0
- package/dist/measurement/provenance.js.map +1 -0
- package/dist/measurement/sweep-jsonl.d.ts +40 -0
- package/dist/measurement/sweep-jsonl.js +154 -0
- package/dist/measurement/sweep-jsonl.js.map +1 -0
- package/dist/measurement/vocabulary.d.ts +125 -0
- package/dist/measurement/vocabulary.js +217 -0
- package/dist/measurement/vocabulary.js.map +1 -0
- package/dist/qualify-cli.d.ts +83 -0
- package/dist/qualify-cli.js +224 -0
- package/dist/qualify-cli.js.map +1 -0
- package/dist/qualify-declarations.d.ts +98 -0
- package/dist/qualify-declarations.js +76 -0
- package/dist/qualify-declarations.js.map +1 -0
- package/dist/qualify-options.d.ts +22 -0
- package/dist/qualify-options.js +36 -0
- package/dist/qualify-options.js.map +1 -0
- package/dist/rating.d.ts +32 -0
- package/dist/rating.js +30 -0
- package/dist/rating.js.map +1 -0
- package/dist/tool-paths.d.ts +12 -0
- package/dist/tool-paths.js +29 -0
- package/dist/tool-paths.js.map +1 -0
- package/fixtures/clap-scan/controls.json +8 -0
- package/fixtures/clap-scan/fake.json +16 -0
- package/fixtures/clap-scan/mixed.json +18 -0
- package/fixtures/zero-path.lv2/manifest.ttl +3 -0
- package/fixtures/zero-path.lv2/zero-path.c +85 -0
- package/fixtures/zero-path.lv2/zero-path.ttl +21 -0
- package/package.json +91 -3
- package/src/clap-loader.ts +393 -0
- package/src/clap-qualify.ts +387 -0
- package/src/host-profiles.ts +122 -0
- package/src/hosting-codes.ts +71 -0
- package/src/hosting-suitability.ts +982 -0
- package/src/index.ts +19 -0
- package/src/loader.ts +30 -0
- package/src/lv2-loader.ts +50 -0
- package/src/measurement/cli.ts +458 -0
- package/src/measurement/fixtures.ts +141 -0
- package/src/measurement/format.ts +1064 -0
- package/src/measurement/guards.ts +64 -0
- package/src/measurement/index.ts +39 -0
- package/src/measurement/merge.ts +434 -0
- package/src/measurement/plan.ts +219 -0
- package/src/measurement/probe-json.ts +271 -0
- package/src/measurement/provenance.ts +220 -0
- package/src/measurement/sweep-jsonl.ts +173 -0
- package/src/measurement/vocabulary.ts +298 -0
- package/src/qualify-cli.ts +277 -0
- package/src/qualify-declarations.ts +117 -0
- package/src/qualify-options.ts +49 -0
- package/src/rating.ts +53 -0
- package/src/tool-paths.ts +32 -0
- package/tools/_declarations.py +21 -0
- package/tools/benchmark.py +2462 -0
- package/tools/gen-python-declarations.d.mts +4 -0
- package/tools/gen-python-declarations.mjs +92 -0
- package/tools/hosting-sweep.mjs +51 -0
- package/tools/lv2-measure.mjs +204 -0
- package/tools/lv2_presets.py +132 -0
- package/tools/scan.py +432 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run is going to do, decided before it starts.
|
|
3
|
+
*
|
|
4
|
+
* Two questions, answered in the same breath: *"Measure at least the rates this
|
|
5
|
+
* installation can actually use … but it must not silently fall back to a rate it does not
|
|
6
|
+
* use"*, and *"show progress"* — which needs a duration, which nobody can honestly produce
|
|
7
|
+
* for a machine they have never seen.
|
|
8
|
+
*
|
|
9
|
+
* ## Rates are data, not a constant
|
|
10
|
+
*
|
|
11
|
+
* There is no default rate list in this module and there must never be one. A console that
|
|
12
|
+
* runs 48 k and 96 k does not need 192 k figures; a console whose driver only offers 44.1 k
|
|
13
|
+
* must not be handed a 48 k measurement wearing its label. The plan is derived from what the
|
|
14
|
+
* installation reports — its live rate and its device's supported list — and a request for a
|
|
15
|
+
* rate the hardware does not offer is DROPPED and reported, never honoured.
|
|
16
|
+
*
|
|
17
|
+
* A plan with no rates is a legitimate outcome: it means nobody has told us what this
|
|
18
|
+
* installation runs at, and the correct behaviour is to refuse and say so rather than pick
|
|
19
|
+
* 48 000 because it is a common number.
|
|
20
|
+
*
|
|
21
|
+
* ## Duration is an estimate or it is nothing
|
|
22
|
+
*
|
|
23
|
+
* {@link estimateRunSeconds} scales a REFERENCE run's measured wall time by the work the new
|
|
24
|
+
* plan asks for. With no reference run — or a reference that never recorded its own duration
|
|
25
|
+
* — it returns `undefined`. There is no fallback constant: "about 15 minutes" measured on a
|
|
26
|
+
* Core Ultra 9 is not an estimate for a NUC, and a wrong progress estimate is how an operator
|
|
27
|
+
* ends up cancelling a run that was nearly done.
|
|
28
|
+
*/
|
|
29
|
+
import { type MeasurementDimension } from "./vocabulary.js";
|
|
30
|
+
import type { MeasurementDocument, MeasurementRun } from "./format.js";
|
|
31
|
+
/** What the installation knows about its own clock, as far as the plan is concerned. */
|
|
32
|
+
export interface InstallationRates {
|
|
33
|
+
/** The rate the graph is running at right now, when the console knows it. */
|
|
34
|
+
readonly liveRate?: number;
|
|
35
|
+
/** Rates the active device actually offers. Empty/absent = the console has not probed. */
|
|
36
|
+
readonly supportedRates?: readonly number[];
|
|
37
|
+
/** Rates the operator asked for. Absent = "whatever this installation can use". */
|
|
38
|
+
readonly requestedRates?: readonly number[];
|
|
39
|
+
}
|
|
40
|
+
/** The rates a run will measure at, and what was left out on the way there. */
|
|
41
|
+
export interface RatePlan {
|
|
42
|
+
/** Rates to measure, ascending. Empty when nothing could be resolved. */
|
|
43
|
+
readonly rates: readonly number[];
|
|
44
|
+
/**
|
|
45
|
+
* Rates the operator asked for that the installation does not offer. Reported, not
|
|
46
|
+
* measured — a figure at a rate the rig cannot run is a figure nobody will ever use, and
|
|
47
|
+
* quietly measuring it would make the run longer for nothing.
|
|
48
|
+
*/
|
|
49
|
+
readonly dropped: readonly number[];
|
|
50
|
+
/**
|
|
51
|
+
* Set when {@link rates} is empty: `no-rates-known`. The console could name neither a
|
|
52
|
+
* live rate nor a supported list, so there is nothing to measure and no safe guess.
|
|
53
|
+
*/
|
|
54
|
+
readonly problem?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the rates a local run should measure at.
|
|
58
|
+
*
|
|
59
|
+
* The live rate is always included when known — a console must have figures for the rate it
|
|
60
|
+
* is running at, whatever else the operator asked for. Beyond that: the requested rates
|
|
61
|
+
* intersected with what the device offers, or, absent a request, the whole supported list.
|
|
62
|
+
*/
|
|
63
|
+
export declare function ratesForInstallation(input: InstallationRates): RatePlan;
|
|
64
|
+
/** A run, fully specified, before anything is probed. */
|
|
65
|
+
export interface MeasurementPlan {
|
|
66
|
+
readonly rates: readonly number[];
|
|
67
|
+
readonly dimensions: readonly MeasurementDimension[];
|
|
68
|
+
/** Rates asked for and dropped as unusable here — carried through so the UI can say so. */
|
|
69
|
+
readonly droppedRates: readonly number[];
|
|
70
|
+
/** How many plugins the run will visit, when a scan has already told us. */
|
|
71
|
+
readonly pluginCount?: number;
|
|
72
|
+
/** Set when the plan cannot run: `no-rates-known` / `no-dimensions`. */
|
|
73
|
+
readonly problem?: string;
|
|
74
|
+
}
|
|
75
|
+
/** Build a plan from resolved rates and the dimensions the operator chose. */
|
|
76
|
+
export declare function measurementPlan(ratePlan: RatePlan, dimensions: readonly MeasurementDimension[], pluginCount?: number): MeasurementPlan;
|
|
77
|
+
/** True when a plan has enough to run: at least one rate and at least one dimension. */
|
|
78
|
+
export declare function isRunnablePlan(plan: MeasurementPlan): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The unit of work a plan represents: plugins × rates × dimensions.
|
|
81
|
+
*
|
|
82
|
+
* Crude on purpose. A convolution reverb costs more than a gain, but the prober caps each
|
|
83
|
+
* plugin's wall time, so over a whole catalogue the product is close enough to scale a
|
|
84
|
+
* measured duration by — and a more elaborate model would imply a precision the estimate
|
|
85
|
+
* does not have.
|
|
86
|
+
*/
|
|
87
|
+
export declare function planWorkUnits(plan: MeasurementPlan): number | undefined;
|
|
88
|
+
/** A duration estimate, with the run it was extrapolated from named. */
|
|
89
|
+
export interface RunEstimate {
|
|
90
|
+
readonly seconds: number;
|
|
91
|
+
/** The reference run's id, host and duration — everything needed to render "on <host>,
|
|
92
|
+
* <n> plugins took <t>", so the operator can judge the extrapolation themselves. */
|
|
93
|
+
readonly basis: {
|
|
94
|
+
readonly runId: string;
|
|
95
|
+
readonly host: string;
|
|
96
|
+
readonly seconds: number;
|
|
97
|
+
readonly workUnits: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* How long this plan is likely to take, extrapolated from a run that actually happened.
|
|
102
|
+
*
|
|
103
|
+
* `undefined` whenever it cannot be derived — no reference document, the reference never
|
|
104
|
+
* recorded `elapsedSeconds`, or the plan does not yet know its plugin count. An unknown
|
|
105
|
+
* duration is reported as unknown; there is no constant to fall back on and inventing one
|
|
106
|
+
* would be the exact dishonesty the provenance discipline exists to prevent.
|
|
107
|
+
*
|
|
108
|
+
* When the reference ran on ANOTHER machine the estimate is still only an order of
|
|
109
|
+
* magnitude — which is why {@link RunEstimate.basis} names the host rather than hiding it.
|
|
110
|
+
*/
|
|
111
|
+
export declare function estimateRunSeconds(plan: MeasurementPlan, reference: MeasurementDocument | undefined): RunEstimate | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Rates a completed run covers that the installation no longer cares about, and rates the
|
|
114
|
+
* installation now uses that the run never covered.
|
|
115
|
+
*
|
|
116
|
+
* The second list is the rate-change case: the operator moved the rig to 96 kHz, so the
|
|
117
|
+
* figures for 96 kHz are MISSING rather than wrong, and the console should offer to measure
|
|
118
|
+
* them instead of interpolating across a boundary 19 nonconforming plugins prove cannot be
|
|
119
|
+
* interpolated across.
|
|
120
|
+
*/
|
|
121
|
+
export interface RateCoverage {
|
|
122
|
+
readonly covered: readonly number[];
|
|
123
|
+
readonly missing: readonly number[];
|
|
124
|
+
readonly extra: readonly number[];
|
|
125
|
+
}
|
|
126
|
+
/** Compare a run's rates against the rates an installation now uses. */
|
|
127
|
+
export declare function rateCoverage(run: MeasurementRun | undefined, needed: readonly number[]): RateCoverage;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* What a run is going to do, decided before it starts.
|
|
5
|
+
*
|
|
6
|
+
* Two questions, answered in the same breath: *"Measure at least the rates this
|
|
7
|
+
* installation can actually use … but it must not silently fall back to a rate it does not
|
|
8
|
+
* use"*, and *"show progress"* — which needs a duration, which nobody can honestly produce
|
|
9
|
+
* for a machine they have never seen.
|
|
10
|
+
*
|
|
11
|
+
* ## Rates are data, not a constant
|
|
12
|
+
*
|
|
13
|
+
* There is no default rate list in this module and there must never be one. A console that
|
|
14
|
+
* runs 48 k and 96 k does not need 192 k figures; a console whose driver only offers 44.1 k
|
|
15
|
+
* must not be handed a 48 k measurement wearing its label. The plan is derived from what the
|
|
16
|
+
* installation reports — its live rate and its device's supported list — and a request for a
|
|
17
|
+
* rate the hardware does not offer is DROPPED and reported, never honoured.
|
|
18
|
+
*
|
|
19
|
+
* A plan with no rates is a legitimate outcome: it means nobody has told us what this
|
|
20
|
+
* installation runs at, and the correct behaviour is to refuse and say so rather than pick
|
|
21
|
+
* 48 000 because it is a common number.
|
|
22
|
+
*
|
|
23
|
+
* ## Duration is an estimate or it is nothing
|
|
24
|
+
*
|
|
25
|
+
* {@link estimateRunSeconds} scales a REFERENCE run's measured wall time by the work the new
|
|
26
|
+
* plan asks for. With no reference run — or a reference that never recorded its own duration
|
|
27
|
+
* — it returns `undefined`. There is no fallback constant: "about 15 minutes" measured on a
|
|
28
|
+
* Core Ultra 9 is not an estimate for a NUC, and a wrong progress estimate is how an operator
|
|
29
|
+
* ends up cancelling a run that was nearly done.
|
|
30
|
+
*/
|
|
31
|
+
import { MEASUREMENT_DIMENSIONS } from "./vocabulary.js";
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the rates a local run should measure at.
|
|
34
|
+
*
|
|
35
|
+
* The live rate is always included when known — a console must have figures for the rate it
|
|
36
|
+
* is running at, whatever else the operator asked for. Beyond that: the requested rates
|
|
37
|
+
* intersected with what the device offers, or, absent a request, the whole supported list.
|
|
38
|
+
*/
|
|
39
|
+
export function ratesForInstallation(input) {
|
|
40
|
+
const supported = [...new Set((input.supportedRates ?? []).filter((r) => Number.isFinite(r) && r > 0))];
|
|
41
|
+
const live = input.liveRate !== undefined && Number.isFinite(input.liveRate) && input.liveRate > 0
|
|
42
|
+
? input.liveRate
|
|
43
|
+
: undefined;
|
|
44
|
+
// What this installation can actually use: the device list, plus the rate it is demonstrably
|
|
45
|
+
// running at (a live rate absent from the probed list is the device telling us the list is
|
|
46
|
+
// incomplete, not the other way round).
|
|
47
|
+
const usable = new Set(supported);
|
|
48
|
+
if (live !== undefined)
|
|
49
|
+
usable.add(live);
|
|
50
|
+
const requested = input.requestedRates?.filter((r) => Number.isFinite(r) && r > 0);
|
|
51
|
+
if (requested === undefined || requested.length === 0) {
|
|
52
|
+
const rates = [...usable].sort((a, b) => a - b);
|
|
53
|
+
return rates.length > 0
|
|
54
|
+
? { rates, dropped: [] }
|
|
55
|
+
: { rates: [], dropped: [], problem: "no-rates-known" };
|
|
56
|
+
}
|
|
57
|
+
const wanted = new Set(requested);
|
|
58
|
+
if (live !== undefined)
|
|
59
|
+
wanted.add(live);
|
|
60
|
+
const rates = [...wanted].filter((r) => usable.has(r)).sort((a, b) => a - b);
|
|
61
|
+
const dropped = [...wanted].filter((r) => !usable.has(r)).sort((a, b) => a - b);
|
|
62
|
+
return rates.length > 0
|
|
63
|
+
? { rates, dropped }
|
|
64
|
+
: { rates: [], dropped, problem: "no-rates-known" };
|
|
65
|
+
}
|
|
66
|
+
/** Build a plan from resolved rates and the dimensions the operator chose. */
|
|
67
|
+
export function measurementPlan(ratePlan, dimensions, pluginCount) {
|
|
68
|
+
const wanted = MEASUREMENT_DIMENSIONS.filter((d) => dimensions.includes(d));
|
|
69
|
+
const problem = ratePlan.problem ?? (wanted.length === 0 ? "no-dimensions" : undefined);
|
|
70
|
+
return {
|
|
71
|
+
rates: ratePlan.rates,
|
|
72
|
+
dimensions: wanted,
|
|
73
|
+
droppedRates: ratePlan.dropped,
|
|
74
|
+
...(pluginCount === undefined ? {} : { pluginCount }),
|
|
75
|
+
...(problem === undefined ? {} : { problem }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** True when a plan has enough to run: at least one rate and at least one dimension. */
|
|
79
|
+
export function isRunnablePlan(plan) {
|
|
80
|
+
return plan.problem === undefined && plan.rates.length > 0 && plan.dimensions.length > 0;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The unit of work a plan represents: plugins × rates × dimensions.
|
|
84
|
+
*
|
|
85
|
+
* Crude on purpose. A convolution reverb costs more than a gain, but the prober caps each
|
|
86
|
+
* plugin's wall time, so over a whole catalogue the product is close enough to scale a
|
|
87
|
+
* measured duration by — and a more elaborate model would imply a precision the estimate
|
|
88
|
+
* does not have.
|
|
89
|
+
*/
|
|
90
|
+
export function planWorkUnits(plan) {
|
|
91
|
+
if (plan.pluginCount === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
return plan.pluginCount * plan.rates.length * plan.dimensions.length;
|
|
94
|
+
}
|
|
95
|
+
/** The work a completed run represents, from its own document. */
|
|
96
|
+
function runWorkUnits(document) {
|
|
97
|
+
const units = document.plugins.length * document.run.rates.length * document.run.dimensions.length;
|
|
98
|
+
return units > 0 ? units : undefined;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* How long this plan is likely to take, extrapolated from a run that actually happened.
|
|
102
|
+
*
|
|
103
|
+
* `undefined` whenever it cannot be derived — no reference document, the reference never
|
|
104
|
+
* recorded `elapsedSeconds`, or the plan does not yet know its plugin count. An unknown
|
|
105
|
+
* duration is reported as unknown; there is no constant to fall back on and inventing one
|
|
106
|
+
* would be the exact dishonesty the provenance discipline exists to prevent.
|
|
107
|
+
*
|
|
108
|
+
* When the reference ran on ANOTHER machine the estimate is still only an order of
|
|
109
|
+
* magnitude — which is why {@link RunEstimate.basis} names the host rather than hiding it.
|
|
110
|
+
*/
|
|
111
|
+
export function estimateRunSeconds(plan, reference) {
|
|
112
|
+
if (reference === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
const elapsed = reference.run.elapsedSeconds;
|
|
115
|
+
if (elapsed === undefined || !(elapsed > 0))
|
|
116
|
+
return undefined;
|
|
117
|
+
const referenceUnits = runWorkUnits(reference);
|
|
118
|
+
const planUnits = planWorkUnits(plan);
|
|
119
|
+
if (referenceUnits === undefined || planUnits === undefined || planUnits === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
return {
|
|
122
|
+
seconds: (elapsed * planUnits) / referenceUnits,
|
|
123
|
+
basis: {
|
|
124
|
+
runId: reference.run.id,
|
|
125
|
+
host: reference.run.host.hostname,
|
|
126
|
+
seconds: elapsed,
|
|
127
|
+
workUnits: referenceUnits,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Compare a run's rates against the rates an installation now uses. */
|
|
132
|
+
export function rateCoverage(run, needed) {
|
|
133
|
+
const have = new Set(run?.rates ?? []);
|
|
134
|
+
const want = [...new Set(needed)].sort((a, b) => a - b);
|
|
135
|
+
return {
|
|
136
|
+
covered: want.filter((rate) => have.has(rate)),
|
|
137
|
+
missing: want.filter((rate) => !have.has(rate)),
|
|
138
|
+
extra: [...have].filter((rate) => !want.includes(rate)).sort((a, b) => a - b),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/measurement/plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prober's JSON → the interchange format.
|
|
3
|
+
*
|
|
4
|
+
* `tools/scan.py` and `tools/benchmark.py` write a flat array of annotated descriptors —
|
|
5
|
+
* their own working shape, which predates this format and carries fields (curation tiers,
|
|
6
|
+
* parameter lists, RPM ownership) that have nothing to do with a measurement. This module
|
|
7
|
+
* is the one place that knows both shapes, and it is where three deliberate conversions
|
|
8
|
+
* happen:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Free text becomes vocabulary.** The prober says `"worker crashed (…)"`,
|
|
11
|
+
* `"probe timed out"`, `"non-finite output"` — useful to a human, unmergeable between
|
|
12
|
+
* projects. Each is mapped to a term from {@link ./vocabulary.ts}, and the prober's own
|
|
13
|
+
* sentence is preserved verbatim next to it so the coarser code never destroys the
|
|
14
|
+
* detail.
|
|
15
|
+
* 2. **Derived figures are dropped.** `coreFractionP95` and `instancesPerCoreP95` are
|
|
16
|
+
* `nsPerSample × rate` and its reciprocal. Storing them invites the two to disagree
|
|
17
|
+
* after an edit; the format keeps the measured primitive and lets consumers derive.
|
|
18
|
+
* 3. **`nsPerSampleP95` becomes `nsPerSamplePercentile`.** The percentile is a property of
|
|
19
|
+
* the RUN, declared once in {@link ../format.CostMethod}. Baking `95` into every field
|
|
20
|
+
* name would make a p99 run unrepresentable.
|
|
21
|
+
*
|
|
22
|
+
* The reader is structural: it validates the fields it needs and ignores everything else,
|
|
23
|
+
* so it survives the prober growing new ones.
|
|
24
|
+
*/
|
|
25
|
+
import type { MeasuredPlugin } from "./format.js";
|
|
26
|
+
import type { MeasurementDimension } from "./vocabulary.js";
|
|
27
|
+
/** One free-text prober failure, and the vocabulary term it means. */
|
|
28
|
+
interface ReasonRule {
|
|
29
|
+
/** Matched against the prober's lowercased text. */
|
|
30
|
+
readonly match: RegExp;
|
|
31
|
+
readonly code: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The prober's words → the controlled vocabulary. Ordered: the first match wins, so the
|
|
35
|
+
* specific patterns come before the general ones.
|
|
36
|
+
*
|
|
37
|
+
* A table rather than a switch precisely so the next prober — someone else's — can be
|
|
38
|
+
* supported by adding rows, which is the whole ambition of publishing a format.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PROBE_REASON_RULES: readonly ReasonRule[];
|
|
41
|
+
/**
|
|
42
|
+
* The vocabulary code for a prober reason.
|
|
43
|
+
*
|
|
44
|
+
* Anything unmatched becomes `probe-error` — never a guessed structural reason. A reason we
|
|
45
|
+
* cannot classify must not be allowed to claim the plugin has nothing to measure, because
|
|
46
|
+
* that claim would override a good figure from another machine ({@link ./merge.ts}).
|
|
47
|
+
*/
|
|
48
|
+
export declare function probeReasonCode(text: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* One prober descriptor as a {@link MeasuredPlugin}, or `undefined` when it is not one.
|
|
51
|
+
*
|
|
52
|
+
* `dimensions` says which dimensions the run covered: a dimension NOT covered yields no
|
|
53
|
+
* block at all (the run never asked), while a covered dimension always yields one, carrying
|
|
54
|
+
* either readings or a reason. That distinction is what makes a partial run readable. The
|
|
55
|
+
* prober's JSON carries only `latency` and `cost`; a hosting dimension listed here yields no
|
|
56
|
+
* block, because this reader has nothing to read it from (`sweep-jsonl.ts` does).
|
|
57
|
+
*/
|
|
58
|
+
export declare function measuredPluginFromProbeEntry(value: unknown, dimensions: readonly MeasurementDimension[]): MeasuredPlugin | undefined;
|
|
59
|
+
/** Every readable descriptor in a prober output array, as measured plugins. */
|
|
60
|
+
export declare function measuredPluginsFromProbeOutput(value: unknown, dimensions: readonly MeasurementDimension[]): MeasuredPlugin[];
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The prober's JSON → the interchange format.
|
|
5
|
+
*
|
|
6
|
+
* `tools/scan.py` and `tools/benchmark.py` write a flat array of annotated descriptors —
|
|
7
|
+
* their own working shape, which predates this format and carries fields (curation tiers,
|
|
8
|
+
* parameter lists, RPM ownership) that have nothing to do with a measurement. This module
|
|
9
|
+
* is the one place that knows both shapes, and it is where three deliberate conversions
|
|
10
|
+
* happen:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Free text becomes vocabulary.** The prober says `"worker crashed (…)"`,
|
|
13
|
+
* `"probe timed out"`, `"non-finite output"` — useful to a human, unmergeable between
|
|
14
|
+
* projects. Each is mapped to a term from {@link ./vocabulary.ts}, and the prober's own
|
|
15
|
+
* sentence is preserved verbatim next to it so the coarser code never destroys the
|
|
16
|
+
* detail.
|
|
17
|
+
* 2. **Derived figures are dropped.** `coreFractionP95` and `instancesPerCoreP95` are
|
|
18
|
+
* `nsPerSample × rate` and its reciprocal. Storing them invites the two to disagree
|
|
19
|
+
* after an edit; the format keeps the measured primitive and lets consumers derive.
|
|
20
|
+
* 3. **`nsPerSampleP95` becomes `nsPerSamplePercentile`.** The percentile is a property of
|
|
21
|
+
* the RUN, declared once in {@link ../format.CostMethod}. Baking `95` into every field
|
|
22
|
+
* name would make a p99 run unrepresentable.
|
|
23
|
+
*
|
|
24
|
+
* The reader is structural: it validates the fields it needs and ignores everything else,
|
|
25
|
+
* so it survives the prober growing new ones.
|
|
26
|
+
*/
|
|
27
|
+
import { booleanValue, finiteNumber, isRecord, nonEmptyString, optional, positiveNumber } from "./guards.js";
|
|
28
|
+
/**
|
|
29
|
+
* The prober's words → the controlled vocabulary. Ordered: the first match wins, so the
|
|
30
|
+
* specific patterns come before the general ones.
|
|
31
|
+
*
|
|
32
|
+
* A table rather than a switch precisely so the next prober — someone else's — can be
|
|
33
|
+
* supported by adding rows, which is the whole ambition of publishing a format.
|
|
34
|
+
*/
|
|
35
|
+
export const PROBE_REASON_RULES = [
|
|
36
|
+
{ match: /^no-audio-in$/, code: "no-audio-in" },
|
|
37
|
+
{ match: /^no-audio-out$/, code: "no-audio-out" },
|
|
38
|
+
{ match: /^silent-output$/, code: "silent-output" },
|
|
39
|
+
{ match: /worker crashed|worker desync|worker unavailable/, code: "probe-crashed" },
|
|
40
|
+
{ match: /timed out|timeout/, code: "probe-timeout" },
|
|
41
|
+
{ match: /non-finite/, code: "non-finite-output" },
|
|
42
|
+
{ match: /not installed/, code: "not-installed" },
|
|
43
|
+
{ match: /refus|instantiat/, code: "refused-instantiation" },
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* The vocabulary code for a prober reason.
|
|
47
|
+
*
|
|
48
|
+
* Anything unmatched becomes `probe-error` — never a guessed structural reason. A reason we
|
|
49
|
+
* cannot classify must not be allowed to claim the plugin has nothing to measure, because
|
|
50
|
+
* that claim would override a good figure from another machine ({@link ./merge.ts}).
|
|
51
|
+
*/
|
|
52
|
+
export function probeReasonCode(text) {
|
|
53
|
+
const lower = text.toLowerCase();
|
|
54
|
+
return PROBE_REASON_RULES.find((rule) => rule.match.test(lower))?.code ?? "probe-error";
|
|
55
|
+
}
|
|
56
|
+
function reasonFields(text) {
|
|
57
|
+
if (text === undefined)
|
|
58
|
+
return {};
|
|
59
|
+
const code = probeReasonCode(text);
|
|
60
|
+
return {
|
|
61
|
+
unmeasuredReason: code,
|
|
62
|
+
// Keep the prober's sentence only when it says more than the code already does.
|
|
63
|
+
...(text === code ? {} : { unmeasuredDetail: text }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function readTopology(entry) {
|
|
67
|
+
const audioInputs = finiteNumber(entry.audioInputs);
|
|
68
|
+
const audioOutputs = finiteNumber(entry.audioOutputs);
|
|
69
|
+
if (audioInputs === undefined || audioOutputs === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
const midiInputs = entry.hasMidiIn === true ? 1 : entry.hasMidiIn === false ? 0 : undefined;
|
|
72
|
+
const controlInputs = Array.isArray(entry.params)
|
|
73
|
+
? entry.params.filter((p) => isRecord(p) && p.kind === "control").length
|
|
74
|
+
: undefined;
|
|
75
|
+
return {
|
|
76
|
+
audioInputs,
|
|
77
|
+
audioOutputs,
|
|
78
|
+
...optional("midiInputs", midiInputs),
|
|
79
|
+
...optional("controlInputs", controlInputs),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function readSweep(value) {
|
|
83
|
+
if (!isRecord(value))
|
|
84
|
+
return undefined;
|
|
85
|
+
const rate = positiveNumber(value.rate);
|
|
86
|
+
const defaultFrames = finiteNumber(value.defaultFrames);
|
|
87
|
+
const minFrames = finiteNumber(value.minFrames);
|
|
88
|
+
const maxFrames = finiteNumber(value.maxFrames);
|
|
89
|
+
if (rate === undefined || defaultFrames === undefined || minFrames === undefined || maxFrames === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const controls = {};
|
|
93
|
+
if (isRecord(value.controls)) {
|
|
94
|
+
for (const [symbol, span] of Object.entries(value.controls)) {
|
|
95
|
+
if (!isRecord(span))
|
|
96
|
+
continue;
|
|
97
|
+
const lo = finiteNumber(span.minFrames);
|
|
98
|
+
const hi = finiteNumber(span.maxFrames);
|
|
99
|
+
if (lo === undefined || hi === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
controls[symbol] = { minFrames: lo, maxFrames: hi };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { rate, defaultFrames, minFrames, maxFrames, controls };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Rates the prober condemned, as `rate → reason`. In the catalog shape these live in a
|
|
108
|
+
* parallel `unreliableRates` array; the format puts the exclusion ON the reading it
|
|
109
|
+
* disqualifies, where a consumer cannot fail to notice it.
|
|
110
|
+
*/
|
|
111
|
+
function excludedRates(value) {
|
|
112
|
+
const out = new Map();
|
|
113
|
+
if (!Array.isArray(value))
|
|
114
|
+
return out;
|
|
115
|
+
for (const item of value) {
|
|
116
|
+
if (!isRecord(item))
|
|
117
|
+
continue;
|
|
118
|
+
const rate = positiveNumber(item.rate);
|
|
119
|
+
const reason = nonEmptyString(item.reason);
|
|
120
|
+
if (rate === undefined || reason === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
out.set(String(rate), reason);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function readLatency(value) {
|
|
127
|
+
if (!isRecord(value))
|
|
128
|
+
return undefined;
|
|
129
|
+
const excluded = excludedRates(value.unreliableRates);
|
|
130
|
+
const perRate = {};
|
|
131
|
+
if (isRecord(value.perRate)) {
|
|
132
|
+
for (const [key, reading] of Object.entries(value.perRate)) {
|
|
133
|
+
const rate = positiveNumber(Number(key));
|
|
134
|
+
if (rate === undefined || !isRecord(reading))
|
|
135
|
+
continue;
|
|
136
|
+
const frames = finiteNumber(reading.frames);
|
|
137
|
+
const ms = finiteNumber(reading.ms);
|
|
138
|
+
if (frames === undefined || ms === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
perRate[String(rate)] = {
|
|
141
|
+
frames,
|
|
142
|
+
ms,
|
|
143
|
+
...optional("stimulus", nonEmptyString(value.stimulus)),
|
|
144
|
+
...optional("excludedReason", excluded.get(String(rate))),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const unmeasurable = nonEmptyString(value.unmeasurable);
|
|
149
|
+
const measurement = {
|
|
150
|
+
perRate,
|
|
151
|
+
...optional("scalingClass", nonEmptyString(value.scalingClass)),
|
|
152
|
+
...optional("scalingNote", nonEmptyString(value.scalingNote)),
|
|
153
|
+
...optional("declaredPortSymbol", nonEmptyString(value.portSymbol)),
|
|
154
|
+
...optional("declaredFrames", finiteNumber(value.reportedFrames)),
|
|
155
|
+
...optional("declaredMismatch", booleanValue(value.declaredMismatch)),
|
|
156
|
+
...optional("paramSweep", readSweep(value.paramSweep)),
|
|
157
|
+
// A reason only when there is no reading: a figure and a reason are mutually exclusive.
|
|
158
|
+
...(Object.keys(perRate).length === 0 ? reasonFields(unmeasurable) : {}),
|
|
159
|
+
};
|
|
160
|
+
return measurement;
|
|
161
|
+
}
|
|
162
|
+
function readCost(value) {
|
|
163
|
+
if (!isRecord(value))
|
|
164
|
+
return undefined;
|
|
165
|
+
const perRate = {};
|
|
166
|
+
if (isRecord(value.perRate)) {
|
|
167
|
+
for (const [key, reading] of Object.entries(value.perRate)) {
|
|
168
|
+
const rate = positiveNumber(Number(key));
|
|
169
|
+
if (rate === undefined || !isRecord(reading))
|
|
170
|
+
continue;
|
|
171
|
+
const median = finiteNumber(reading.nsPerSampleMedian);
|
|
172
|
+
const high = finiteNumber(reading.nsPerSampleP95);
|
|
173
|
+
const blocks = finiteNumber(reading.blocks);
|
|
174
|
+
const blockFrames = positiveNumber(reading.blockFrames);
|
|
175
|
+
if (median === undefined || high === undefined || blocks === undefined || blockFrames === undefined) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
perRate[String(rate)] = {
|
|
179
|
+
nsPerSampleMedian: median,
|
|
180
|
+
nsPerSamplePercentile: high,
|
|
181
|
+
blocks,
|
|
182
|
+
blockFrames,
|
|
183
|
+
...optional("nsPerSampleMin", finiteNumber(reading.nsPerSampleMin)),
|
|
184
|
+
...optional("nsPerSampleMax", finiteNumber(reading.nsPerSampleMax)),
|
|
185
|
+
...optional("warmupNsPerSampleMedian", finiteNumber(reading.warmupNsPerSampleMedian)),
|
|
186
|
+
...optional("silentOutput", booleanValue(reading.silentOutput)),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const reason = nonEmptyString(value.unmeasurable) ?? nonEmptyString(value.failed);
|
|
191
|
+
return {
|
|
192
|
+
perRate,
|
|
193
|
+
...(Object.keys(perRate).length === 0 ? reasonFields(reason) : {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The plugin's own version, `"<minor>.<micro>"`, when the scan recorded one.
|
|
198
|
+
*
|
|
199
|
+
* Absent rather than `"0.0"` when it did not: a version of zero is a real LV2 answer
|
|
200
|
+
* (unstable/development), so it must not double as "we do not know".
|
|
201
|
+
*/
|
|
202
|
+
function readVersion(entry) {
|
|
203
|
+
const minor = finiteNumber(entry.minorVersion);
|
|
204
|
+
const micro = finiteNumber(entry.microVersion);
|
|
205
|
+
if (minor === undefined || micro === undefined)
|
|
206
|
+
return nonEmptyString(entry.version);
|
|
207
|
+
return `${minor}.${micro}`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* One prober descriptor as a {@link MeasuredPlugin}, or `undefined` when it is not one.
|
|
211
|
+
*
|
|
212
|
+
* `dimensions` says which dimensions the run covered: a dimension NOT covered yields no
|
|
213
|
+
* block at all (the run never asked), while a covered dimension always yields one, carrying
|
|
214
|
+
* either readings or a reason. That distinction is what makes a partial run readable. The
|
|
215
|
+
* prober's JSON carries only `latency` and `cost`; a hosting dimension listed here yields no
|
|
216
|
+
* block, because this reader has nothing to read it from (`sweep-jsonl.ts` does).
|
|
217
|
+
*/
|
|
218
|
+
export function measuredPluginFromProbeEntry(value, dimensions) {
|
|
219
|
+
if (!isRecord(value))
|
|
220
|
+
return undefined;
|
|
221
|
+
const uri = nonEmptyString(value.uri);
|
|
222
|
+
if (uri === undefined)
|
|
223
|
+
return undefined;
|
|
224
|
+
const topology = readTopology(value);
|
|
225
|
+
if (topology === undefined)
|
|
226
|
+
return undefined;
|
|
227
|
+
const latency = dimensions.includes("latency")
|
|
228
|
+
? readLatency(value.latency) ?? { perRate: {}, unmeasuredReason: "not-attempted" }
|
|
229
|
+
: undefined;
|
|
230
|
+
const cost = dimensions.includes("cost")
|
|
231
|
+
? readCost(value.cpuCost) ?? { perRate: {}, unmeasuredReason: "not-attempted" }
|
|
232
|
+
: undefined;
|
|
233
|
+
return {
|
|
234
|
+
uri,
|
|
235
|
+
topology,
|
|
236
|
+
...optional("name", nonEmptyString(value.name)),
|
|
237
|
+
...optional("version", readVersion(value)),
|
|
238
|
+
...optional("bundle", nonEmptyString(value.bundlePath)),
|
|
239
|
+
...optional("lv2Class", nonEmptyString(value.lv2Class)),
|
|
240
|
+
...optional("latency", latency),
|
|
241
|
+
...optional("cost", cost),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Every readable descriptor in a prober output array, as measured plugins. */
|
|
245
|
+
export function measuredPluginsFromProbeOutput(value, dimensions) {
|
|
246
|
+
if (!Array.isArray(value))
|
|
247
|
+
return [];
|
|
248
|
+
return value.flatMap((entry) => {
|
|
249
|
+
const plugin = measuredPluginFromProbeEntry(entry, dimensions);
|
|
250
|
+
return plugin === undefined ? [] : [plugin];
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=probe-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe-json.js","sourceRoot":"","sources":["../../src/measurement/probe-json.ts"],"names":[],"mappings":"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"}
|