@openmixer/plugin-qualify 0.17.5 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -13
- package/dist/clap-qualify.d.ts +3 -1
- package/dist/clap-qualify.js +7 -1
- package/dist/clap-qualify.js.map +1 -1
- package/dist/direct-limit.d.ts +27 -0
- package/dist/direct-limit.js +52 -0
- package/dist/direct-limit.js.map +1 -0
- package/dist/host-admission.d.ts +10 -2
- package/dist/host-admission.js +13 -2
- package/dist/host-admission.js.map +1 -1
- package/dist/host-profiles.d.ts +8 -0
- package/dist/host-profiles.js +16 -1
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-codes.generated.d.ts +10 -0
- package/dist/hosting-codes.generated.js +10 -0
- package/dist/hosting-codes.generated.js.map +1 -1
- package/dist/hosting-suitability.d.ts +34 -8
- package/dist/hosting-suitability.js +92 -7
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/measurement/format.d.ts +54 -8
- package/dist/measurement/format.js +54 -3
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/plan.d.ts +3 -3
- package/dist/measurement/plan.js +1 -1
- package/dist/measurement-grid.d.ts +8 -0
- package/dist/measurement-grid.js +4 -0
- package/dist/measurement-grid.js.map +1 -1
- package/dist/memory-bound.d.ts +6 -0
- package/dist/memory-bound.js +8 -0
- package/dist/memory-bound.js.map +1 -1
- package/dist/memory-budget.d.ts +39 -0
- package/dist/memory-budget.js +32 -0
- package/dist/memory-budget.js.map +1 -0
- package/dist/probe-judge.js +22 -9
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-merge.js +6 -5
- package/dist/probe-merge.js.map +1 -1
- package/dist/probe-output-fixtures.d.ts +6 -0
- package/dist/probe-output-fixtures.js +8 -0
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +4 -2
- package/dist/probe-output.js +2 -1
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.js +6 -3
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +26 -2
- package/dist/qualification-record.js +17 -18
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-declarations.d.ts +30 -11
- package/dist/qualify-declarations.js +38 -2
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-sync.js +18 -4
- package/dist/qualify-sync.js.map +1 -1
- package/package.json +1 -1
- package/src/clap-qualify.ts +8 -1
- package/src/direct-limit.ts +68 -0
- package/src/host-admission.ts +15 -2
- package/src/host-profiles.ts +17 -1
- package/src/hosting-codes.generated.ts +10 -0
- package/src/hosting-suitability.ts +105 -11
- package/src/index.ts +1 -0
- package/src/measurement/format.ts +97 -3
- package/src/measurement/plan.ts +3 -3
- package/src/measurement-grid.ts +5 -0
- package/src/memory-bound.ts +10 -0
- package/src/memory-budget.ts +55 -0
- package/src/probe-judge.ts +21 -8
- package/src/probe-merge.ts +6 -5
- package/src/probe-output-fixtures.ts +10 -1
- package/src/probe-output.ts +5 -2
- package/src/probe-runner.ts +5 -3
- package/src/qualification-record.ts +22 -4
- package/src/qualify-declarations.ts +49 -3
- package/src/qualify-sync.ts +18 -3
- package/tools/_declarations.py +1 -0
- package/tools/benchmark.py +25 -2
- package/tools/gen-python-declarations.mjs +1 -0
- package/tools/lv2-measure.mjs +7 -4
- package/tools/lv2_presets.py +1 -1
- package/tools/mod-host-admit.mjs +2 -1
|
@@ -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);
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
SoakReading,
|
|
31
31
|
ThreadReading,
|
|
32
32
|
} from "./measurement/format.js";
|
|
33
|
+
import { memoryWithinBudget, type MemoryReadings } from "./memory-budget.js";
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* What a reason may interpolate: wire-serialisable scalars only. Structurally the same record as
|
|
@@ -71,6 +72,8 @@ export interface QualifiedPlugin {
|
|
|
71
72
|
readonly lv2Features?: PluginLv2Features;
|
|
72
73
|
/** The plugin's binary format, the scan's declared fact (CLAP spec §6); absent reads `lv2`. */
|
|
73
74
|
readonly format?: "lv2" | "clap";
|
|
75
|
+
/** What one instance holds: per rate, or per rate AND quantum as {@link cpuCost} has it. A missing cell is unmeasured. */
|
|
76
|
+
readonly memory?: MemoryReadings;
|
|
74
77
|
/** What the CLAP prober (`native/clap-qualify`) measured, for a `clap` plugin (CLAP spec §3). */
|
|
75
78
|
readonly clap?: PluginClapMeasurement;
|
|
76
79
|
}
|
|
@@ -78,14 +81,15 @@ export interface QualifiedPlugin {
|
|
|
78
81
|
/** Where a plugin's audio is actually processed. */
|
|
79
82
|
export type HostingPath = "in-process" | "isolated";
|
|
80
83
|
|
|
81
|
-
/** The dimensions that decide a host
|
|
84
|
+
/** The dimensions that decide a host, folded worst-wins. */
|
|
82
85
|
export type HostingDimension =
|
|
83
86
|
| "stability"
|
|
84
87
|
| "rtSafety"
|
|
85
88
|
| "features"
|
|
86
89
|
| "cost"
|
|
87
90
|
| "latency"
|
|
88
|
-
| "topology"
|
|
91
|
+
| "topology"
|
|
92
|
+
| "memory";
|
|
89
93
|
|
|
90
94
|
// The in-process feature list and the CLAP host extensions are the `openmixer-console` host
|
|
91
95
|
// profile's; they live with the profiles (`host-profiles.ts`) and are re-exported here, where the
|
|
@@ -446,13 +450,15 @@ export function deadAgainstBest(levelDbfs: number, bestDbfs: number, policy: Hos
|
|
|
446
450
|
}
|
|
447
451
|
|
|
448
452
|
/** The classes of soak failure, as the strings `failures` is built from. */
|
|
449
|
-
export type SoakFailureKind = "died" | "non-finite" | "dead" | "mostly-silent" | "startup-blast" | "timed-out";
|
|
453
|
+
export type SoakFailureKind = "died" | "non-finite" | "dead" | "mostly-silent" | "startup-blast" | "timed-out" | "leaks";
|
|
450
454
|
|
|
451
455
|
export interface SoakFailure {
|
|
452
456
|
readonly rate: number;
|
|
453
457
|
readonly kind: SoakFailureKind;
|
|
454
458
|
/** Extra words for the string form, e.g. `20dBFS`. */
|
|
455
459
|
readonly detail?: string;
|
|
460
|
+
/** For `leaks`: what the growth was and the ceiling it passed, which the reason carries. */
|
|
461
|
+
readonly params?: CodedRefusalParams;
|
|
456
462
|
}
|
|
457
463
|
|
|
458
464
|
/** A soak's reading: what voided it, if anything, and what it proved when nothing did. */
|
|
@@ -465,7 +471,7 @@ export interface SoakFindings {
|
|
|
465
471
|
* cut, a fact about the harness: it voids the soak but is never a failure of the plugin. */
|
|
466
472
|
readonly failed: readonly SoakFailure[];
|
|
467
473
|
readonly ratesTested: number;
|
|
468
|
-
/** The largest RSS growth across rates, for the record;
|
|
474
|
+
/** The largest RSS growth across rates, warm-up included, for the record; `leaks` is what is judged. */
|
|
469
475
|
readonly rssGrowthKb: number;
|
|
470
476
|
}
|
|
471
477
|
|
|
@@ -473,6 +479,17 @@ export interface SoakFindings {
|
|
|
473
479
|
* settled above it cannot be the reference, or every sane rate reads dead against it. */
|
|
474
480
|
const BLAST_DBFS = 6;
|
|
475
481
|
|
|
482
|
+
/**
|
|
483
|
+
* The growth a rate's soak is still showing, in KB per minute of audio: from halfway past warm-up to its highest reading,
|
|
484
|
+
* so that a library that finishes loading a little late is not a leak and a leak of any size is. `undefined` when the soak
|
|
485
|
+
* did not reach those readings: not read as flat.
|
|
486
|
+
*/
|
|
487
|
+
export function soakGrowthKbPerMin(v: SoakAtRate): number | undefined {
|
|
488
|
+
const { rssMidKb, midSeconds, rssMaxKb, seconds } = v;
|
|
489
|
+
if (rssMidKb === undefined || midSeconds === undefined || rssMaxKb === undefined || seconds === undefined || seconds <= midSeconds) return undefined;
|
|
490
|
+
return (Math.max(rssMaxKb - rssMidKb, 0) * 60) / (seconds - midSeconds);
|
|
491
|
+
}
|
|
492
|
+
|
|
476
493
|
/**
|
|
477
494
|
* Read a soak, rate by rate. A rate voids it if the plugin did not instantiate or died there,
|
|
478
495
|
* emitted a non-finite window, settled dead against its own best rate ({@link deadAgainstBest};
|
|
@@ -516,7 +533,23 @@ export function soakFindings(soak: SoakReading | undefined, policy: HostingPolic
|
|
|
516
533
|
continue;
|
|
517
534
|
}
|
|
518
535
|
const first = v.firstRmsDbfs ?? -99;
|
|
519
|
-
if (first > BLAST_DBFS)
|
|
536
|
+
if (first > BLAST_DBFS) {
|
|
537
|
+
failed.push({ rate, kind: "startup-blast", detail: `${first.toFixed(0)}dBFS` });
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const growth = soakGrowthKbPerMin(v);
|
|
541
|
+
if (growth !== undefined && growth > policy.memoryGrowthKbPerMinCeiling) {
|
|
542
|
+
failed.push({
|
|
543
|
+
rate,
|
|
544
|
+
kind: "leaks",
|
|
545
|
+
detail: `${growth.toFixed(0)}KB/min`,
|
|
546
|
+
params: {
|
|
547
|
+
growthKbPerMin: Math.round(growth),
|
|
548
|
+
ceilingKbPerMin: policy.memoryGrowthKbPerMinCeiling,
|
|
549
|
+
...(policy.memoryGrowthCeilingProvisional ? { provisional: "yes" as const } : {}),
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
}
|
|
520
553
|
}
|
|
521
554
|
const failures = failed.map((f) => `${f.kind}@${f.rate}${f.detail === undefined ? "" : `(${f.detail})`}`);
|
|
522
555
|
return {
|
|
@@ -592,6 +625,7 @@ export function stabilityReasons(
|
|
|
592
625
|
reason("stability", "objective", "unsuitable", HOSTING_CODE.stabilitySoakFailedAtRate, {
|
|
593
626
|
failure: f.kind,
|
|
594
627
|
rate: f.rate,
|
|
628
|
+
...f.params,
|
|
595
629
|
}),
|
|
596
630
|
);
|
|
597
631
|
}
|
|
@@ -650,9 +684,11 @@ export function rtSafetyReasons(
|
|
|
650
684
|
if (m.variable === true) {
|
|
651
685
|
return [reason("rtSafety", "objective", "unknown", HOSTING_CODE.rtSafetyNotReproducible)];
|
|
652
686
|
}
|
|
687
|
+
// RT-unsafe is a hosting mode, never a refusal (#196): the plugin loads and runs in a worker of its own, never
|
|
688
|
+
// grouped with RT-safe plugins; conditional is isolated in the pin. Crashes, hangs and load failures stay unsuitable.
|
|
653
689
|
if (rtSafetyViolating(m)) {
|
|
654
690
|
return [
|
|
655
|
-
reason("rtSafety", "objective", "
|
|
691
|
+
reason("rtSafety", "objective", "conditional", HOSTING_CODE.rtSafetyViolations, {
|
|
656
692
|
allocations: m.allocationsInRun ?? 0,
|
|
657
693
|
syscalls: m.syscallsInRun ?? 0,
|
|
658
694
|
locks: m.locksInRun ?? 0,
|
|
@@ -777,6 +813,60 @@ export function hostingCostReasons(
|
|
|
777
813
|
return out;
|
|
778
814
|
}
|
|
779
815
|
|
|
816
|
+
/**
|
|
817
|
+
* What a host's memory budget makes of a plugin. A host with no budget declared reads nothing here. Every cell the host
|
|
818
|
+
* runs is read; the worst stands, so a plugin is never read as fitting on the cell that happened to be measured. A missing
|
|
819
|
+
* figure is `unknown`, never a fit. Under a provisional budget a plugin below the ceiling is `conditional` with a code of its
|
|
820
|
+
* own, since the number it was held to was not measured on that kind of host; one above it is `unsuitable` and says the
|
|
821
|
+
* ceiling is provisional.
|
|
822
|
+
*/
|
|
823
|
+
export function hostingMemoryReasons(
|
|
824
|
+
memory: MemoryReadings | undefined,
|
|
825
|
+
profile: HostProfile,
|
|
826
|
+
/** The live rate and quantum, for a host whose budget names no cells of its own; absent, the cell its cost is read at. */
|
|
827
|
+
live?: { readonly rate: number; readonly quantum?: number },
|
|
828
|
+
): readonly HostingReason[] {
|
|
829
|
+
const budget = profile.cost.memory;
|
|
830
|
+
if (budget === undefined) return [];
|
|
831
|
+
const provisional: CodedRefusalParams = budget.provisional === true ? { provisional: "yes" } : {};
|
|
832
|
+
const cells = budget.cells ?? [{ rate: live?.rate ?? profile.cost.rate, quantum: live?.quantum ?? profile.cost.quantum }];
|
|
833
|
+
const reads = cells.map((c) => ({ ...c, read: memoryWithinBudget(memoryAt(memory, c.rate, c.quantum), budget) }));
|
|
834
|
+
const above = reads.find((r) => r.read.outcome === "above");
|
|
835
|
+
if (above !== undefined && above.read.outcome === "above") {
|
|
836
|
+
return [
|
|
837
|
+
reason("memory", "objective", "unsuitable", HOSTING_CODE.memoryAboveCeiling, {
|
|
838
|
+
rate: above.rate,
|
|
839
|
+
quantum: above.quantum,
|
|
840
|
+
figure: above.read.figure,
|
|
841
|
+
kb: above.read.kb,
|
|
842
|
+
ceilingKb: above.read.ceilingKb,
|
|
843
|
+
...provisional,
|
|
844
|
+
}),
|
|
845
|
+
];
|
|
846
|
+
}
|
|
847
|
+
const unmeasured = reads.find((r) => r.read.outcome === "unmeasured");
|
|
848
|
+
if (unmeasured !== undefined && unmeasured.read.outcome === "unmeasured") {
|
|
849
|
+
return [
|
|
850
|
+
reason("memory", "objective", "unknown", HOSTING_CODE.memoryUnmeasured, {
|
|
851
|
+
rate: unmeasured.rate,
|
|
852
|
+
quantum: unmeasured.quantum,
|
|
853
|
+
...(unmeasured.read.unmeasuredReason === undefined ? {} : { unmeasuredReason: unmeasured.read.unmeasuredReason }),
|
|
854
|
+
}),
|
|
855
|
+
];
|
|
856
|
+
}
|
|
857
|
+
const first = reads[0]!.read;
|
|
858
|
+
const params: CodedRefusalParams = first.outcome === "within" ? { perInstanceKb: first.perInstanceKb, firstInstanceKb: first.firstInstanceKb, ceilingKb: budget.perInstanceKb, consoleCeilingKb: budget.perConsoleKb } : {};
|
|
859
|
+
return budget.provisional === true
|
|
860
|
+
? [reason("memory", "objective", "conditional", HOSTING_CODE.memoryWithinProvisionalCeiling, { ...params, ...provisional })]
|
|
861
|
+
: [reason("memory", "objective", "suitable", HOSTING_CODE.memoryWithinCeiling, params)];
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/** The footprint at a rate and quantum: a grid's cell when the plugin has a grid, else its rate's. */
|
|
865
|
+
function memoryAt(memory: MemoryReadings | undefined, rate: number, quantum: number) {
|
|
866
|
+
if (memory?.perRateQuantum !== undefined) return memory.perRateQuantum[costPointKey(rate, quantum)];
|
|
867
|
+
return memory?.perRate?.[String(rate)];
|
|
868
|
+
}
|
|
869
|
+
|
|
780
870
|
export function hostingLatencyReasons(
|
|
781
871
|
descriptor: Pick<QualifiedPlugin, "latency">,
|
|
782
872
|
handling: HostProfile["latency"] = "compensated",
|
|
@@ -924,7 +1014,8 @@ export function clapFeatureReasons(
|
|
|
924
1014
|
/**
|
|
925
1015
|
* The CLAP additions to `rtSafety` (CLAP spec §3): the interposer's counts are read by
|
|
926
1016
|
* `rtSafetyReasons` exactly as for an LV2; these are the two violations only a CLAP can commit
|
|
927
|
-
* — a strict thread-check violation and a `CLAP_PROCESS_ERROR`
|
|
1017
|
+
* — a strict thread-check violation (`conditional`: the plugin runs isolated, #196) and a `CLAP_PROCESS_ERROR`
|
|
1018
|
+
* (`unsuitable`). A sweep
|
|
928
1019
|
* that made no `process()` call measured nothing.
|
|
929
1020
|
*/
|
|
930
1021
|
export function clapRtReasons(clap: PluginClapMeasurement | undefined): readonly HostingReason[] {
|
|
@@ -933,7 +1024,7 @@ export function clapRtReasons(clap: PluginClapMeasurement | undefined): readonly
|
|
|
933
1024
|
}
|
|
934
1025
|
const out: HostingReason[] = [];
|
|
935
1026
|
if (clap.threadViolations > 0) {
|
|
936
|
-
out.push(reason("rtSafety", "objective", "
|
|
1027
|
+
out.push(reason("rtSafety", "objective", "conditional", HOSTING_CODE.clapThreadViolation, { violations: clap.threadViolations }));
|
|
937
1028
|
}
|
|
938
1029
|
if (clap.processErrors > 0) {
|
|
939
1030
|
out.push(reason("rtSafety", "objective", "unsuitable", HOSTING_CODE.clapProcessError, { errors: clap.processErrors }));
|
|
@@ -945,8 +1036,9 @@ export function clapRtReasons(clap: PluginClapMeasurement | undefined): readonly
|
|
|
945
1036
|
* The rtSafety reasons of the note sweep, for a plugin with `noteInputs` note inputs: none for a
|
|
946
1037
|
* plugin that takes no notes; `unknown` when it takes notes and was not played under the
|
|
947
1038
|
* interposer (an instrument is cheap and clean at rest, so the parameter sweep alone says nothing
|
|
948
|
-
* about a voice); `
|
|
949
|
-
*
|
|
1039
|
+
* about a voice); `conditional` (isolated in the pin: its own worker, never refused) on any
|
|
1040
|
+
* allocation, lock or syscall while it was played; `unsuitable` on a process error only (a crash);
|
|
1041
|
+
* `suitable` on a clean count.
|
|
950
1042
|
*/
|
|
951
1043
|
export function noteSweepReasons(sweep: PluginNoteSweepMeasurement | undefined, noteInputs: number): readonly HostingReason[] {
|
|
952
1044
|
if (noteInputs === 0) return [];
|
|
@@ -957,7 +1049,7 @@ export function noteSweepReasons(sweep: PluginNoteSweepMeasurement | undefined,
|
|
|
957
1049
|
const out: HostingReason[] = [];
|
|
958
1050
|
if (sweep.allocationsInRun + sweep.locksInRun + sweep.syscallsInRun > 0) {
|
|
959
1051
|
out.push(
|
|
960
|
-
reason("rtSafety", "objective", "
|
|
1052
|
+
reason("rtSafety", "objective", "conditional", HOSTING_CODE.rtSafetyNotesViolations, {
|
|
961
1053
|
allocations: sweep.allocationsInRun,
|
|
962
1054
|
syscalls: sweep.syscallsInRun,
|
|
963
1055
|
locks: sweep.locksInRun,
|
|
@@ -1002,6 +1094,7 @@ export function classifyPluginHosting(
|
|
|
1002
1094
|
...(profile.instruments ? noteSweepReasons(descriptor.rtSafety?.noteSweep, descriptor.clap?.noteInputs ?? (descriptor.hasMidiIn ? 1 : 0)) : []),
|
|
1003
1095
|
...clapFeatureReasons(descriptor.clap, profile, descriptor.hasMidiIn),
|
|
1004
1096
|
...hostingCostReasons(descriptor, options, policy),
|
|
1097
|
+
...hostingMemoryReasons(descriptor.memory, profile, options),
|
|
1005
1098
|
...clapLatencyReasons(descriptor.clap),
|
|
1006
1099
|
...clapTopologyReasons(descriptor.clap, profile.instruments),
|
|
1007
1100
|
]
|
|
@@ -1011,6 +1104,7 @@ export function classifyPluginHosting(
|
|
|
1011
1104
|
...(profile.instruments ? noteSweepReasons(descriptor.rtSafety?.noteSweep, descriptor.hasMidiIn ? 1 : 0) : []),
|
|
1012
1105
|
...featureReasons(descriptor.lv2Features, descriptor, profile.lv2Features, options.quantum !== undefined, profile.instruments),
|
|
1013
1106
|
...hostingCostReasons(descriptor, options, policy),
|
|
1107
|
+
...hostingMemoryReasons(descriptor.memory, profile, options),
|
|
1014
1108
|
...hostingLatencyReasons(descriptor, profile.latency),
|
|
1015
1109
|
...hostingTopologyReasons(descriptor, profile.instruments),
|
|
1016
1110
|
];
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./tool-paths.js";
|
|
|
10
10
|
export * from "./rating.js";
|
|
11
11
|
export * from "./host-profiles.js";
|
|
12
12
|
export * from "./hosting-suitability.js";
|
|
13
|
+
export * from "./memory-budget.js";
|
|
13
14
|
export * from "./hosting-codes.js";
|
|
14
15
|
export * from "./loader.js";
|
|
15
16
|
export * from "./lv2-loader.js";
|
|
@@ -78,8 +78,10 @@ export const MEASUREMENT_FORMAT_MAJOR = 1;
|
|
|
78
78
|
* {@link RtSafetyMeasurement}, {@link FeaturesMeasurement} — which a 1.0 reader skips.
|
|
79
79
|
* `1.2` added the verdicts per host profile; `1.3` the {@link IntegrityMeasurement} block; `1.4` the notes
|
|
80
80
|
* an instrument's cost was measured with ({@link CostAtRate.voices}); `1.5` the note sweep an instrument's
|
|
81
|
-
* `run()` was counted under ({@link RtSafetyMeasurement.noteSweep}).
|
|
82
|
-
|
|
81
|
+
* `run()` was counted under ({@link RtSafetyMeasurement.noteSweep}); `1.6` an instance's memory footprint
|
|
82
|
+
* beside the cost reading ({@link CostAtRate.memory}), the soak's readings past warm-up
|
|
83
|
+
* ({@link SoakAtRate.rssAfterWarmupKb}), and a host profile's memory budget ({@link HostBudget.memory}). */
|
|
84
|
+
export const MEASUREMENT_FORMAT_MINOR = 6;
|
|
83
85
|
|
|
84
86
|
/** The version string this build writes, e.g. `"1.0"`. */
|
|
85
87
|
export const MEASUREMENT_FORMAT_VERSION = `${MEASUREMENT_FORMAT_MAJOR}.${MEASUREMENT_FORMAT_MINOR}`;
|
|
@@ -285,6 +287,28 @@ export interface VoicesHeld {
|
|
|
285
287
|
readonly capacity?: number;
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
/**
|
|
291
|
+
* 1.6: what one instance holds at this rate: how far the resident set of a process of its own grew from before the
|
|
292
|
+
* plugin loaded, in KB, at each step of its life. Raw readings, never a conclusion: `memory-budget.ts` reads them against
|
|
293
|
+
* a budget. `secondInstanceKb` is what a second instance added once it had done the same, so what one instance costs is
|
|
294
|
+
* told from what every instance shares (a sample library, a wavetable, an IR). `notesHeldKb` and its `basis` are an
|
|
295
|
+
* instrument's, at the notes the cost arm holds.
|
|
296
|
+
*
|
|
297
|
+
* A figure not taken is absent, never 0; `unmeasuredReason` (an {@link ../vocabulary.UNMEASURED_TERMS} code) says why
|
|
298
|
+
* one that should be there is not, and may stand beside the figures that were taken.
|
|
299
|
+
*/
|
|
300
|
+
export interface MemoryFootprint {
|
|
301
|
+
readonly afterInstantiateKb?: number;
|
|
302
|
+
readonly afterActivateKb?: number;
|
|
303
|
+
/** Activated, after processing: what a long-running host carries. */
|
|
304
|
+
readonly atRestKb?: number;
|
|
305
|
+
readonly notesHeldKb?: number;
|
|
306
|
+
readonly secondInstanceKb?: number;
|
|
307
|
+
readonly basis?: VoiceBasis;
|
|
308
|
+
readonly unmeasuredReason?: string;
|
|
309
|
+
readonly unmeasuredDetail?: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
288
312
|
/** One rate's cost reading. Per SAMPLE, so it is independent of the block size. */
|
|
289
313
|
export interface CostAtRate {
|
|
290
314
|
readonly nsPerSampleMedian: number;
|
|
@@ -303,6 +327,8 @@ export interface CostAtRate {
|
|
|
303
327
|
readonly silentOutput?: boolean;
|
|
304
328
|
/** 1.4: for an instrument, the notes held while it was timed. Absent for an effect. */
|
|
305
329
|
readonly voices?: VoicesHeld;
|
|
330
|
+
/** 1.6: this instance's memory footprint at the same rate. */
|
|
331
|
+
readonly memory?: MemoryFootprint;
|
|
306
332
|
}
|
|
307
333
|
|
|
308
334
|
/** Everything one run learned about one plugin's CPU cost. */
|
|
@@ -368,6 +394,12 @@ export interface SoakAtRate {
|
|
|
368
394
|
readonly rssStartKb?: number;
|
|
369
395
|
readonly rssMaxKb?: number;
|
|
370
396
|
readonly rssGrowthKb?: number;
|
|
397
|
+
/** 1.6: the resident set past the settling `firstRmsDbfs` skips, and the audio-seconds to reach it. */
|
|
398
|
+
readonly rssAfterWarmupKb?: number;
|
|
399
|
+
readonly warmupSeconds?: number;
|
|
400
|
+
/** 1.6: the resident set halfway from there to the end, so that growth which stopped is told from growth still climbing. */
|
|
401
|
+
readonly rssMidKb?: number;
|
|
402
|
+
readonly midSeconds?: number;
|
|
371
403
|
}
|
|
372
404
|
|
|
373
405
|
/** A continuous soak, per rate, with whether the controls were swept while it ran. */
|
|
@@ -501,6 +533,22 @@ export interface HostBudget {
|
|
|
501
533
|
readonly coreFractionCeiling: number;
|
|
502
534
|
readonly rate: number;
|
|
503
535
|
readonly quantum: number;
|
|
536
|
+
/** 1.6: what this host gives its plugins' memory; absent is a host with no budget declared, never a budget of 0. */
|
|
537
|
+
readonly memory?: HostMemoryBudget;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 1.6: a host's memory budget, and where it is read. `perInstanceKb` bounds what one more instance costs;
|
|
542
|
+
* `perConsoleKb` bounds all the plugins of the host, so the most one plugin may take with what its instances share.
|
|
543
|
+
* `provisional` says the numbers were not measured on that kind of host: a verdict that rests on them says so and does
|
|
544
|
+
* not say the plugin fits. `cells` are the rates and quanta the host runs, each judged, where it runs more than the one
|
|
545
|
+
* its cost is read at.
|
|
546
|
+
*/
|
|
547
|
+
export interface HostMemoryBudget {
|
|
548
|
+
readonly perInstanceKb: number;
|
|
549
|
+
readonly perConsoleKb: number;
|
|
550
|
+
readonly provisional?: boolean;
|
|
551
|
+
readonly cells?: readonly { readonly rate: number; readonly quantum: number }[];
|
|
504
552
|
}
|
|
505
553
|
|
|
506
554
|
/**
|
|
@@ -554,6 +602,24 @@ const isPositive = (v: unknown): v is number => typeof v === "number" && Number.
|
|
|
554
602
|
* typed; a missing or mistyped one is refused by name, and a host the reader was not told about
|
|
555
603
|
* is never filled in from a default.
|
|
556
604
|
*/
|
|
605
|
+
/** A budget, `undefined` for none declared, or the words of what is wrong with it. */
|
|
606
|
+
function readHostMemoryBudget(value: unknown): HostMemoryBudget | undefined | string {
|
|
607
|
+
if (value === undefined) return undefined;
|
|
608
|
+
if (!isRecord(value)) return "cost.memory: an object, or absent";
|
|
609
|
+
if (!isPositive(value.perInstanceKb) || !isPositive(value.perConsoleKb)) return "cost.memory: perInstanceKb and perConsoleKb are positive numbers";
|
|
610
|
+
if (value.provisional !== undefined && typeof value.provisional !== "boolean") return "cost.memory.provisional: a boolean, or absent";
|
|
611
|
+
const cells = value.cells;
|
|
612
|
+
if (cells !== undefined && (!Array.isArray(cells) || cells.length === 0 || !cells.every((c) => isRecord(c) && Number.isInteger(c.rate) && Number.isInteger(c.quantum) && isPositive(c.rate) && isPositive(c.quantum)))) {
|
|
613
|
+
return "cost.memory.cells: rates and quanta as positive integers, or absent";
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
perInstanceKb: value.perInstanceKb,
|
|
617
|
+
perConsoleKb: value.perConsoleKb,
|
|
618
|
+
...optional("provisional", value.provisional as boolean | undefined),
|
|
619
|
+
...optional("cells", (cells as readonly Record<string, number>[] | undefined)?.map((c) => ({ rate: c.rate!, quantum: c.quantum! }))),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
557
623
|
export function readHostProfile(value: unknown): HostProfileRead {
|
|
558
624
|
if (!isRecord(value)) return { ok: false, problem: "a host profile is a JSON object" };
|
|
559
625
|
const { id, lv2Features, clapExtensions, instruments, cost, latency, isolation } = value;
|
|
@@ -569,6 +635,8 @@ export function readHostProfile(value: unknown): HostProfileRead {
|
|
|
569
635
|
if (!Number.isInteger(cost.rate) || !Number.isInteger(cost.quantum)) {
|
|
570
636
|
return { ok: false, problem: "cost: rate and quantum are integers" };
|
|
571
637
|
}
|
|
638
|
+
const memory = readHostMemoryBudget(cost.memory);
|
|
639
|
+
if (typeof memory === "string") return { ok: false, problem: memory };
|
|
572
640
|
if (!(HOST_LATENCY_HANDLING as readonly unknown[]).includes(latency)) {
|
|
573
641
|
return { ok: false, problem: `latency: one of ${HOST_LATENCY_HANDLING.join(", ")}` };
|
|
574
642
|
}
|
|
@@ -582,7 +650,7 @@ export function readHostProfile(value: unknown): HostProfileRead {
|
|
|
582
650
|
lv2Features,
|
|
583
651
|
clapExtensions,
|
|
584
652
|
instruments,
|
|
585
|
-
cost: { coreFractionCeiling: cost.coreFractionCeiling, rate: cost.rate, quantum: cost.quantum },
|
|
653
|
+
cost: { coreFractionCeiling: cost.coreFractionCeiling, rate: cost.rate, quantum: cost.quantum, ...optional("memory", memory) },
|
|
586
654
|
latency: latency as HostLatencyHandling,
|
|
587
655
|
isolation: isolation as HostIsolation,
|
|
588
656
|
},
|
|
@@ -851,6 +919,27 @@ function readLatency(value: unknown): LatencyMeasurement | undefined {
|
|
|
851
919
|
};
|
|
852
920
|
}
|
|
853
921
|
|
|
922
|
+
/** A KB figure: finite and never negative. */
|
|
923
|
+
function kilobytes(value: unknown): number | undefined {
|
|
924
|
+
const n = finiteNumber(value);
|
|
925
|
+
return n !== undefined && n >= 0 ? n : undefined;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** A memory footprint, or `undefined` unless it carries at least one figure or a reason. */
|
|
929
|
+
export function readMemoryFootprint(value: unknown): MemoryFootprint | undefined {
|
|
930
|
+
if (!isRecord(value)) return undefined;
|
|
931
|
+
const footprint: MemoryFootprint = {
|
|
932
|
+
...optional("afterInstantiateKb", kilobytes(value.afterInstantiateKb)),
|
|
933
|
+
...optional("afterActivateKb", kilobytes(value.afterActivateKb)),
|
|
934
|
+
...optional("atRestKb", kilobytes(value.atRestKb)),
|
|
935
|
+
...optional("notesHeldKb", kilobytes(value.notesHeldKb)),
|
|
936
|
+
...optional("secondInstanceKb", kilobytes(value.secondInstanceKb)),
|
|
937
|
+
...optional("basis", isVoiceBasis(value.basis) ? value.basis : undefined),
|
|
938
|
+
...reasonOf(value),
|
|
939
|
+
};
|
|
940
|
+
return Object.keys(footprint).length > 0 ? footprint : undefined;
|
|
941
|
+
}
|
|
942
|
+
|
|
854
943
|
function readCostAtRate(value: unknown): CostAtRate | undefined {
|
|
855
944
|
if (!isRecord(value)) return undefined;
|
|
856
945
|
const nsPerSampleMedian = finiteNumber(value.nsPerSampleMedian);
|
|
@@ -875,6 +964,7 @@ function readCostAtRate(value: unknown): CostAtRate | undefined {
|
|
|
875
964
|
...optional("warmupNsPerSampleMedian", finiteNumber(value.warmupNsPerSampleMedian)),
|
|
876
965
|
...optional("silentOutput", booleanValue(value.silentOutput)),
|
|
877
966
|
...optional("voices", readVoicesHeld(value.voices)),
|
|
967
|
+
...optional("memory", readMemoryFootprint(value.memory)),
|
|
878
968
|
};
|
|
879
969
|
}
|
|
880
970
|
|
|
@@ -957,6 +1047,10 @@ export function readSoakAtRate(value: unknown): SoakAtRate | undefined {
|
|
|
957
1047
|
...optional("rssStartKb", finiteNumber(value.rssStartKb)),
|
|
958
1048
|
...optional("rssMaxKb", finiteNumber(value.rssMaxKb)),
|
|
959
1049
|
...optional("rssGrowthKb", finiteNumber(value.rssGrowthKb)),
|
|
1050
|
+
...optional("rssAfterWarmupKb", kilobytes(value.rssAfterWarmupKb)),
|
|
1051
|
+
...optional("warmupSeconds", finiteNumber(value.warmupSeconds)),
|
|
1052
|
+
...optional("rssMidKb", kilobytes(value.rssMidKb)),
|
|
1053
|
+
...optional("midSeconds", finiteNumber(value.midSeconds)),
|
|
960
1054
|
};
|
|
961
1055
|
}
|
|
962
1056
|
|
package/src/measurement/plan.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface InstallationRates {
|
|
|
38
38
|
readonly liveRate?: number;
|
|
39
39
|
/** Rates the active device actually offers. Empty/absent = the console has not probed. */
|
|
40
40
|
readonly supportedRates?: readonly number[];
|
|
41
|
-
/** Rates the
|
|
41
|
+
/** Rates the caller asked for. Absent = "whatever this installation can use". */
|
|
42
42
|
readonly requestedRates?: readonly number[];
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -47,7 +47,7 @@ export interface RatePlan {
|
|
|
47
47
|
/** Rates to measure, ascending. Empty when nothing could be resolved. */
|
|
48
48
|
readonly rates: readonly number[];
|
|
49
49
|
/**
|
|
50
|
-
* Rates the
|
|
50
|
+
* Rates the caller asked for that the installation does not offer. Reported, not
|
|
51
51
|
* measured — a figure at a rate the rig cannot run is a figure nobody will ever use, and
|
|
52
52
|
* quietly measuring it would make the run longer for nothing.
|
|
53
53
|
*/
|
|
@@ -63,7 +63,7 @@ export interface RatePlan {
|
|
|
63
63
|
* Resolve the rates a local run should measure at.
|
|
64
64
|
*
|
|
65
65
|
* The live rate is always included when known — a console must have figures for the rate it
|
|
66
|
-
* is running at, whatever else the
|
|
66
|
+
* is running at, whatever else the caller asked for. Beyond that: the requested rates
|
|
67
67
|
* intersected with what the device offers, or, absent a request, the whole supported list.
|
|
68
68
|
*/
|
|
69
69
|
export function ratesForInstallation(input: InstallationRates): RatePlan {
|
package/src/measurement-grid.ts
CHANGED
|
@@ -72,6 +72,11 @@ export function gridCells(
|
|
|
72
72
|
return unique([...lead.filter((c) => inProduct.has(cellKey(c))), ...product]);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** The cells a host is judged at: the one its cost is read at, then the others its memory budget says it runs. */
|
|
76
|
+
export function hostCells(profile: { readonly cost: GridCell & { readonly memory?: { readonly cells?: readonly GridCell[] } } }): GridCell[] {
|
|
77
|
+
return unique([{ rate: profile.cost.rate, quantum: profile.cost.quantum }, ...(profile.cost.memory?.cells ?? [])]);
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
/** The first `budget` cells, or every one when the budget is absent or 0. */
|
|
76
81
|
export function budgetCells(cells: readonly GridCell[], budget: number | undefined): GridCell[] {
|
|
77
82
|
return budget === undefined || budget === 0 ? [...cells] : cells.slice(0, budget);
|
package/src/memory-bound.ts
CHANGED
|
@@ -17,9 +17,18 @@ import { MEMORY_BOUND_EXIT_STATUS, MEMORY_LIMIT_RAM_FRACTION } from "./qualify-d
|
|
|
17
17
|
/** How the bound is applied: the runner's data limit on the child. */
|
|
18
18
|
export const MEMORY_BOUND_MECHANISM = "rlimit-data";
|
|
19
19
|
|
|
20
|
+
/** How the bound was applied when the runner set it as its cgroup's `memory.max`, which its document says. */
|
|
21
|
+
export const MEMORY_CGROUP_MECHANISM = "cgroup-memory-max";
|
|
22
|
+
|
|
20
23
|
/** The outcome of a run, and of each of its cells, that outgrew the bound. */
|
|
21
24
|
export const PROBE_OOM = "probe.oom";
|
|
22
25
|
|
|
26
|
+
/** The runner's own outcome for a child the kernel killed at its cgroup's `memory.max`. */
|
|
27
|
+
export const PROBE_MEMORY_LIMIT = "probe.memory-limit";
|
|
28
|
+
|
|
29
|
+
/** Whether an outcome says a run outgrew its memory limit, by either bound. */
|
|
30
|
+
export const isOutOfMemory = (outcome: string): boolean => outcome === PROBE_OOM || outcome === PROBE_MEMORY_LIMIT;
|
|
31
|
+
|
|
23
32
|
export interface MemoryBound {
|
|
24
33
|
readonly bytes: number;
|
|
25
34
|
readonly mechanism: typeof MEMORY_BOUND_MECHANISM;
|
|
@@ -50,6 +59,7 @@ export function memoryBoundOf(setting: number | "off" | undefined, ramBytes?: nu
|
|
|
50
59
|
|
|
51
60
|
/** Whether a run under `bound` ended out of memory: the interposer's exit status, or a SIGKILL that was no deadline's. */
|
|
52
61
|
export function outOfMemory(output: ProbeOutput, bound: MemoryBound | undefined): boolean {
|
|
62
|
+
if (output.run.outcome === PROBE_MEMORY_LIMIT || output.steps.some((s) => s.outcome === PROBE_MEMORY_LIMIT)) return true;
|
|
53
63
|
if (bound === undefined) return false;
|
|
54
64
|
return output.steps.some((s) => s.exitStatus === MEMORY_BOUND_EXIT_STATUS || (s.signal === "SIGKILL" && s.outcome === "probe.crashed"));
|
|
55
65
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* What a measured footprint comes to, and whether it fits a host's budget. Pure: the readings come from the measurers, the
|
|
5
|
+
* budget from the host profile, and the verdict's reasons from `hosting-suitability.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Two figures come out of a footprint. `firstInstanceKb` is the most one instance held at any step, with everything its
|
|
8
|
+
* instances share. `perInstanceKb` is what one more instance costs: the second instance's own part and its voices. A host's
|
|
9
|
+
* `perInstanceKb` bounds the second and its `perConsoleKb` the first, so a sampler with a large library shared between
|
|
10
|
+
* instances can fit a small host once and still be read correctly for two.
|
|
11
|
+
*
|
|
12
|
+
* Nothing absent reads as fitting: no figure, or a reason beside the figures, is `unmeasured`.
|
|
13
|
+
*/
|
|
14
|
+
import type { HostMemoryBudget, MemoryFootprint } from "./measurement/format.js";
|
|
15
|
+
|
|
16
|
+
/** A plugin's footprints as a verdict reads them: per rate, or per rate and quantum (a probe output's grid). */
|
|
17
|
+
export interface MemoryReadings {
|
|
18
|
+
readonly perRate?: Readonly<Record<string, MemoryFootprint>>;
|
|
19
|
+
readonly perRateQuantum?: Readonly<Record<string, MemoryFootprint>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The most one instance held at any step, with everything its instances share. `undefined` for no figure. */
|
|
23
|
+
export function firstInstanceKb(f: MemoryFootprint): number | undefined {
|
|
24
|
+
const figures = [f.afterInstantiateKb, f.afterActivateKb, f.atRestKb, f.notesHeldKb].filter((v): v is number => v !== undefined);
|
|
25
|
+
return figures.length === 0 ? undefined : Math.max(...figures);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What one more instance costs: the second instance's own part and the voices of this one. With no second instance
|
|
30
|
+
* measured, the whole of the first, since nothing separates what it shares.
|
|
31
|
+
*/
|
|
32
|
+
export function perInstanceKb(f: MemoryFootprint): number | undefined {
|
|
33
|
+
const first = firstInstanceKb(f);
|
|
34
|
+
if (first === undefined) return undefined;
|
|
35
|
+
if (f.secondInstanceKb === undefined) return first;
|
|
36
|
+
const voices = f.notesHeldKb !== undefined && f.atRestKb !== undefined ? Math.max(f.notesHeldKb - f.atRestKb, 0) : 0;
|
|
37
|
+
return f.secondInstanceKb + voices;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type MemoryBudgetRead =
|
|
41
|
+
| { readonly outcome: "within"; readonly perInstanceKb: number; readonly firstInstanceKb: number }
|
|
42
|
+
| { readonly outcome: "above"; readonly figure: "perInstance" | "firstInstance"; readonly kb: number; readonly ceilingKb: number }
|
|
43
|
+
| { readonly outcome: "unmeasured"; readonly unmeasuredReason?: string };
|
|
44
|
+
|
|
45
|
+
/** A footprint against a budget. A missing figure is `"unmeasured"`, never a fit; a figure above a ceiling is above it whatever else is missing. */
|
|
46
|
+
export function memoryWithinBudget(footprint: MemoryFootprint | undefined, budget: HostMemoryBudget): MemoryBudgetRead {
|
|
47
|
+
const first = footprint === undefined ? undefined : firstInstanceKb(footprint);
|
|
48
|
+
const per = footprint === undefined ? undefined : perInstanceKb(footprint);
|
|
49
|
+
if (first !== undefined && first > budget.perConsoleKb) return { outcome: "above", figure: "firstInstance", kb: first, ceilingKb: budget.perConsoleKb };
|
|
50
|
+
if (per !== undefined && per > budget.perInstanceKb) return { outcome: "above", figure: "perInstance", kb: per, ceilingKb: budget.perInstanceKb };
|
|
51
|
+
if (first === undefined || per === undefined || footprint?.unmeasuredReason !== undefined) {
|
|
52
|
+
return { outcome: "unmeasured", ...(footprint?.unmeasuredReason === undefined ? {} : { unmeasuredReason: footprint.unmeasuredReason }) };
|
|
53
|
+
}
|
|
54
|
+
return { outcome: "within", perInstanceKb: per, firstInstanceKb: first };
|
|
55
|
+
}
|
package/src/probe-judge.ts
CHANGED
|
@@ -24,21 +24,23 @@ import {
|
|
|
24
24
|
type ClapRtSafetyReading,
|
|
25
25
|
type RtSafetyDocument,
|
|
26
26
|
} from "./clap-qualify.js";
|
|
27
|
-
import { gridCostReadings } from "./cost-fields.js";
|
|
27
|
+
import { costPointKey, gridCostReadings } from "./cost-fields.js";
|
|
28
28
|
import {
|
|
29
29
|
hostingCostReasons,
|
|
30
|
+
hostingMemoryReasons,
|
|
30
31
|
noteSweepReasons,
|
|
31
32
|
rtSafetyReasons,
|
|
32
33
|
contributedHostingRating,
|
|
33
34
|
type HostingReason,
|
|
34
35
|
} from "./hosting-suitability.js";
|
|
35
36
|
import type { HostProfile } from "./host-profiles.js";
|
|
36
|
-
import {
|
|
37
|
+
import { runDecides, type HostAdmission } from "./host-admission.js";
|
|
37
38
|
import { PROBE_NOT_ATTEMPTED, PROBE_OK, type ProbeGridPoint, type ProbeOutput } from "./probe-output.js";
|
|
38
39
|
import type { QualifyVerdict } from "./qualify-cli.js";
|
|
39
40
|
import { RATING_SEVERITY, worstRating, type SuitabilityRating } from "./rating.js";
|
|
40
41
|
import type { IntegrityMeasurement, VoicesHeld } from "./measurement/format.js";
|
|
41
|
-
import {
|
|
42
|
+
import { isOutOfMemory } from "./memory-bound.js";
|
|
43
|
+
import type { MemoryFootprint } from "./measurement/format.js";
|
|
42
44
|
|
|
43
45
|
/** Facts of the descriptor, which the probe output repeats none of. */
|
|
44
46
|
export interface JudgeFacts {
|
|
@@ -202,6 +204,13 @@ function decidingOf(reasons: readonly RecordReason[], rating: SuitabilityRating)
|
|
|
202
204
|
return [...reasons].sort((a, b) => RATING_SEVERITY[b.rating] - RATING_SEVERITY[a.rating]).find((r) => r.rating === rating);
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
/** The footprints of the cells that ran to their end, as the verdict reads them. */
|
|
208
|
+
function gridMemoryReadings(output: ProbeOutput): Record<string, MemoryFootprint> {
|
|
209
|
+
const out: Record<string, MemoryFootprint> = {};
|
|
210
|
+
for (const p of output.grid) if (p.outcome === PROBE_OK && p.memory !== undefined) out[costPointKey(p.rate, p.quantum)] = p.memory;
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
205
214
|
/** The grid point at a host's own cell, whatever its outcome; `undefined` when the grid does not hold that cell. */
|
|
206
215
|
function cellOf(output: ProbeOutput, profile: HostProfile): ProbeGridPoint | undefined {
|
|
207
216
|
return output.grid.find((p) => p.rate === profile.cost.rate && p.quantum === profile.cost.quantum);
|
|
@@ -238,7 +247,7 @@ function costOf(output: ProbeOutput, profile: HostProfile): HostJudgment["cost"]
|
|
|
238
247
|
function failedOutcome(output: ProbeOutput): string | undefined {
|
|
239
248
|
if (output.run.outcome !== PROBE_OK) return output.run.outcome;
|
|
240
249
|
// a cell that outgrew the memory bound is a recorded outcome of that cell, judged per host below, not the run's end
|
|
241
|
-
return output.grid.find((p) => p.outcome !== PROBE_OK && p.outcome !== PROBE_NOT_ATTEMPTED && p.outcome
|
|
250
|
+
return output.grid.find((p) => p.outcome !== PROBE_OK && p.outcome !== PROBE_NOT_ATTEMPTED && !isOutOfMemory(p.outcome))?.outcome;
|
|
242
251
|
}
|
|
243
252
|
|
|
244
253
|
/**
|
|
@@ -282,11 +291,15 @@ export function judgeProbeOutput(output: ProbeOutput, profiles: readonly HostPro
|
|
|
282
291
|
}
|
|
283
292
|
// the plugin did not run within the memory bound at this host's own rate and quantum: not hostable there as measured
|
|
284
293
|
const cell = cellOf(output, profile);
|
|
285
|
-
if (cell
|
|
286
|
-
reasons = [...reasons, { dimension: "memory", rating: "unknown", code:
|
|
294
|
+
if (cell !== undefined && isOutOfMemory(cell.outcome)) {
|
|
295
|
+
reasons = [...reasons, { dimension: "memory", rating: "unknown", code: cell.outcome, params: { rate: cell.rate, quantum: cell.quantum, ...memoryParams(cell) } }];
|
|
296
|
+
} else {
|
|
297
|
+
// what this host's budget makes of the footprint, every cell it runs: part of the verdict, unlike the cost beside it
|
|
298
|
+
reasons = [...reasons, ...hostingMemoryReasons({ perRateQuantum: gridMemoryReadings(output) }, profile).map(asRecordReason)];
|
|
287
299
|
}
|
|
288
300
|
const admission = facts.admissions?.[profile.id];
|
|
289
|
-
|
|
301
|
+
const ran = admission !== undefined && runDecides(profile, output.plugin.format, admission);
|
|
302
|
+
if (ran && admission !== undefined) {
|
|
290
303
|
if (!admission.measured || !admission.admitted) {
|
|
291
304
|
// the host could not be run, or the plugin did not survive it: the run decides, whatever the declaration says
|
|
292
305
|
const code = admission.code ?? `${admission.host}.not-admitted`;
|
|
@@ -312,7 +325,7 @@ export function judgeProbeOutput(output: ProbeOutput, profiles: readonly HostPro
|
|
|
312
325
|
...(deciding === undefined ? {} : { deciding: deciding.code }),
|
|
313
326
|
},
|
|
314
327
|
hosting: rating === "suitable" && profile.isolation === "in-process" ? "in-process" : "isolated",
|
|
315
|
-
arm:
|
|
328
|
+
arm: ran ? "run" : "declaration",
|
|
316
329
|
reasons,
|
|
317
330
|
cost: costOf(output, profile),
|
|
318
331
|
};
|