@openmixer/plugin-qualify 0.20.0 → 0.20.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/bin/plugin-qualify-records-package.mjs +9 -7
- package/bin/plugin-qualify.mjs +34 -0
- package/dist/distributed-records.d.ts +16 -3
- package/dist/distributed-records.js +38 -4
- package/dist/distributed-records.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/judged-layer.d.ts +91 -0
- package/dist/judged-layer.js +144 -0
- package/dist/judged-layer.js.map +1 -0
- package/dist/judgement-line.d.ts +6 -0
- package/dist/judgement-line.js +20 -0
- package/dist/judgement-line.js.map +1 -0
- package/dist/measurement/cli.d.ts +2 -0
- package/dist/measurement/cli.js +25 -2
- package/dist/measurement/cli.js.map +1 -1
- package/dist/measurement/format.d.ts +2 -0
- package/dist/measurement/format.js +5 -0
- package/dist/measurement/format.js.map +1 -1
- package/dist/qualification-record.d.ts +8 -0
- package/dist/qualification-record.js +12 -2
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-isolated.d.ts +5 -3
- package/dist/qualify-isolated.js +6 -4
- package/dist/qualify-isolated.js.map +1 -1
- package/dist/qualify-sync.d.ts +7 -2
- package/dist/qualify-sync.js +21 -11
- package/dist/qualify-sync.js.map +1 -1
- package/dist/records-package.d.ts +31 -4
- package/dist/records-package.js +149 -24
- package/dist/records-package.js.map +1 -1
- package/fixtures/records-rpi5-0.19.1/http___drumkv1.sourceforge.net_lv2@0.2-7d179bdcf9e5.default.probe.json +58 -0
- package/fixtures/records-rpi5-0.19.1/http___drumkv1.sourceforge.net_lv2@0.2-7d179bdcf9e5.default.qualification.json +305 -0
- package/fixtures/records-rpi5-0.19.1/http___samplv1.sourceforge.net_lv2@0.2-c9cd8fb3102f.default.probe.json +58 -0
- package/fixtures/records-rpi5-0.19.1/http___samplv1.sourceforge.net_lv2@0.2-c9cd8fb3102f.default.qualification.json +301 -0
- package/package.json +2 -2
- package/src/distributed-records.ts +41 -4
- package/src/index.ts +2 -0
- package/src/judged-layer.ts +197 -0
- package/src/judgement-line.ts +22 -0
- package/src/measurement/cli.ts +26 -2
- package/src/measurement/format.ts +8 -0
- package/src/qualification-record.ts +19 -2
- package/src/qualify-isolated.ts +6 -4
- package/src/qualify-sync.ts +22 -10
- package/src/records-package.ts +156 -25
- package/tools/lv2-measure.mjs +21 -2
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
* treats the plugin as one it measured.
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync, readdirSync } from "node:fs";
|
|
15
|
+
import { machine } from "node:os";
|
|
15
16
|
import { join, resolve } from "node:path";
|
|
16
17
|
|
|
18
|
+
import { cellKey, type GridCell } from "./measurement-grid.js";
|
|
17
19
|
import { clapPinUri, pinVerdictOf, type PinEntry } from "./pins-file.js";
|
|
18
20
|
import { integrityOfProbeOutput } from "./probe-judge.js";
|
|
19
21
|
import { readProbeOutput, sha256Hex } from "./probe-output.js";
|
|
@@ -60,6 +62,20 @@ export function hostClassOfBoard(model: string | undefined): string | undefined
|
|
|
60
62
|
return m === null ? undefined : `rpi${m[1]!}`;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/** The architecture `uname -m` names, or `undefined` when it cannot be read. */
|
|
66
|
+
export function readMachine(): string | undefined {
|
|
67
|
+
try {
|
|
68
|
+
return machine();
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The host class of a machine with no board name, by `uname -m`: `x86-64` for an x86_64 one; none for an architecture without a class. */
|
|
75
|
+
export function hostClassOfMachine(arch: string | undefined): string | undefined {
|
|
76
|
+
return arch?.trim() === "x86_64" ? "x86-64" : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
/** One distributed record, read: where it is, what it says, and the set it came with. */
|
|
64
80
|
export interface DistributedRecord {
|
|
65
81
|
readonly path: string;
|
|
@@ -156,26 +172,38 @@ export function outcomeOfVerdict(v: Pick<RecordVerdict, "measured" | "rating" |
|
|
|
156
172
|
return v.rating === "unsuitable" ? "refused" : "unmeasured";
|
|
157
173
|
}
|
|
158
174
|
|
|
175
|
+
/** The cells a record's grid holds, by rate then quantum: where the set it came in was measured. */
|
|
176
|
+
export function recordCells(record: Pick<QualificationRecord, "measurements">): GridCell[] {
|
|
177
|
+
const seen = new Map<string, GridCell>();
|
|
178
|
+
for (const p of record.measurements?.grid ?? []) seen.set(cellKey(p), { rate: p.rate, quantum: p.quantum });
|
|
179
|
+
return [...seen.values()].sort((a, b) => a.rate - b.rate || a.quantum - b.quantum);
|
|
180
|
+
}
|
|
181
|
+
|
|
159
182
|
/** What the layer makes of one item. */
|
|
160
183
|
export type DistributedMatch =
|
|
161
184
|
/** No distributed record names the plugin: it is measured, and nothing is said. */
|
|
162
185
|
| { readonly status: "none" }
|
|
163
186
|
/** Every reason a record found was not taken, one line each; the plugin is measured. */
|
|
164
187
|
| { readonly status: "ignored"; readonly lines: readonly string[] }
|
|
165
|
-
|
|
188
|
+
/** A set applies; `lines` names every set tried before it and why it was passed over. */
|
|
189
|
+
| { readonly status: "applies"; readonly set: string; readonly records: readonly DistributedRecord[]; readonly files: readonly string[]; readonly lines: readonly string[] };
|
|
166
190
|
|
|
167
191
|
/**
|
|
168
192
|
* Whether the item is taken from the layer. The records that find it are tried one set at a time, in the order the
|
|
169
193
|
* layer read them. Of a set, only the records whose every file is installed with the SHA-256 it recorded (computed
|
|
170
194
|
* here by `digest`) count: a record of other bytes is another build of the plugin and is passed over. The set applies
|
|
171
195
|
* when at least one record of these bytes holds a verdict for `host`, and none of those verdicts is one the source
|
|
172
|
-
* could not measure; a set with no record of these bytes is a binary that differs.
|
|
196
|
+
* could not measure; a set with no record of these bytes is a binary that differs. A set is per machine class, rate
|
|
197
|
+
* and quantum: of these records, only the ones whose grid holds every cell in `cells` (the cells the sync runs at)
|
|
198
|
+
* count, so a set measured at another cell is passed over, naming the cells it was measured at, and never answers a
|
|
199
|
+
* sync at this one by the order its name sorts in (#317). No cells given asks nothing of the grid.
|
|
173
200
|
*/
|
|
174
201
|
export function matchDistributed(
|
|
175
202
|
layer: DistributedLayer,
|
|
176
203
|
item: SyncItem,
|
|
177
204
|
host: string,
|
|
178
205
|
digest: (files: readonly string[]) => readonly FileDigest[] | undefined,
|
|
206
|
+
cells: readonly GridCell[] = [],
|
|
179
207
|
): DistributedMatch {
|
|
180
208
|
const found = item.format === "clap" ? layer.clap.get(resolve(join(item.root, item.files[0]!))) : layer.lv2.get(item.uri ?? "");
|
|
181
209
|
if (found === undefined || found.length === 0) return { status: "none" };
|
|
@@ -195,7 +223,16 @@ export function matchDistributed(
|
|
|
195
223
|
say(`distributed: ${distributedUri(item, found.find((r) => r.set === set)!)}: binary differs`);
|
|
196
224
|
continue;
|
|
197
225
|
}
|
|
198
|
-
const
|
|
226
|
+
const atCell = records.filter((r) => {
|
|
227
|
+
const held = new Set(recordCells(r.record).map(cellKey));
|
|
228
|
+
return cells.every((c) => held.has(cellKey(c)));
|
|
229
|
+
});
|
|
230
|
+
if (atCell.length === 0) {
|
|
231
|
+
const measuredAt = [...new Set(records.flatMap((r) => recordCells(r.record).map(cellKey)))];
|
|
232
|
+
say(`distributed: ${distributedUri(item, records[0]!)}: set ${set} measured at ${measuredAt.length === 0 ? "no cell" : measuredAt.join(", ")}`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const judged = atCell.filter((r) => r.record.verdicts.some((v) => v.host === host));
|
|
199
236
|
if (judged.length === 0) {
|
|
200
237
|
say(`distributed: ${distributedUri(item, records[0]!)}: no verdict for ${host} in ${set}`);
|
|
201
238
|
continue;
|
|
@@ -205,7 +242,7 @@ export function matchDistributed(
|
|
|
205
242
|
say(`distributed: ${distributedUri(item, unmeasured)}: unmeasured in ${set}, measured here`);
|
|
206
243
|
continue;
|
|
207
244
|
}
|
|
208
|
-
return { status: "applies", set, records, files: [...new Set(
|
|
245
|
+
return { status: "applies", set, records: atCell, files: [...new Set(atCell.flatMap((r) => r.record.binary.files.map((f) => f.path)))], lines };
|
|
209
246
|
}
|
|
210
247
|
return { status: "ignored", lines };
|
|
211
248
|
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ export * from "./cost-fields.js";
|
|
|
24
24
|
export * from "./probe-output.js";
|
|
25
25
|
export * from "./probe-judge.js";
|
|
26
26
|
export * from "./qualification-record.js";
|
|
27
|
+
export * from "./judgement-line.js";
|
|
28
|
+
export * from "./judged-layer.js";
|
|
27
29
|
export * from "./probe-runner.js";
|
|
28
30
|
export * from "./measurement-grid.js";
|
|
29
31
|
export * from "./memory-bound.js";
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The judged layer of a plugin-db entry, from a qualification record: plugin-qualify is that layer's one writer
|
|
5
|
+
* (plugin-db's records design note, FreeMixer/plugin-db#10). The reader never parses a record; it reads the layer this writes, so every field
|
|
6
|
+
* the state function reads is set here, from the record and from what plugin-qualify declares once:
|
|
7
|
+
*
|
|
8
|
+
* - the latency line (`LATENCY_LINE_MS`), written beside each cell's figure (`latencyMs`), so a surface shows the
|
|
9
|
+
* figure within or over the line and keeps no tier word (openmixer plugin-system design 7.15);
|
|
10
|
+
* - the rules the record was judged by (`judgementRules`), the minor of the plugin-qualify that judged it: a rule
|
|
11
|
+
* changes only in a minor, so a patch release leaves every record current;
|
|
12
|
+
* - the hosting epoch of the host the verdict is for (`HOSTING_EPOCH`).
|
|
13
|
+
*
|
|
14
|
+
* One layer is one verdict under one host: the host plugin-hostd runs a plugin with. Its verdict is the one the pins
|
|
15
|
+
* file gives the same bytes (`pinVerdictOf` of the isolated tier's host, omx-clap-host, which is plugin-hostd's
|
|
16
|
+
* worker). A record whose verdict for that host was not measured has no judged layer: the reader then answers
|
|
17
|
+
* unqualified, with its reason.
|
|
18
|
+
*/
|
|
19
|
+
import { judgementRules, latencyMsOf, LATENCY_LINE_MS } from "./judgement-line.js";
|
|
20
|
+
import { pinVerdictOf, type PinVerdict } from "./pins-file.js";
|
|
21
|
+
import { QUALIFICATION_RECORD_SCHEMA, ISOLATED_TIER_HOST, type QualificationRecord } from "./qualification-record.js";
|
|
22
|
+
import type { SuitabilityRating } from "./rating.js";
|
|
23
|
+
|
|
24
|
+
/** The host a judged layer is written for: the one that runs plugins, in workers of its own (PLUGIN_DB_ISOLATION_HOSTS). */
|
|
25
|
+
export const JUDGED_HOST = "plugin-hostd";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The hosting epoch of each host a layer is written for. The epoch is the host owner's to move, when a release of it
|
|
29
|
+
* changes how it hosts; plugin-hostd has declared no hosting change since records were written, so its epoch is 0.
|
|
30
|
+
*/
|
|
31
|
+
export const HOSTING_EPOCH = { [JUDGED_HOST]: 0 } as const satisfies Readonly<Record<string, number>>;
|
|
32
|
+
|
|
33
|
+
/** The plugin-db envelope version this writes (plugin-db data/store.schema.json). */
|
|
34
|
+
export const PLUGIN_DB_ENVELOPE = 1;
|
|
35
|
+
|
|
36
|
+
/** The pin a layer is written for, as plugin-db's pinned layer holds it. */
|
|
37
|
+
export interface LayerPin {
|
|
38
|
+
/** The `omx-files/1` SHA-256 of the plugin's files. */
|
|
39
|
+
readonly files: string;
|
|
40
|
+
/** `<scheme>:<sha256>` of the parameter layout, or null when the pin holds none. */
|
|
41
|
+
readonly layout: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface JudgedCell {
|
|
45
|
+
readonly rate: number;
|
|
46
|
+
readonly quantum: number;
|
|
47
|
+
readonly latencyMs?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface JudgedBody {
|
|
51
|
+
readonly verdict: PinVerdict;
|
|
52
|
+
readonly machineClass: string;
|
|
53
|
+
readonly host: { readonly name: string; readonly version: string; readonly hostingEpoch: number };
|
|
54
|
+
readonly rules: string;
|
|
55
|
+
readonly recordSchema: string;
|
|
56
|
+
readonly cells: readonly JudgedCell[];
|
|
57
|
+
readonly conditions: Readonly<Record<string, string>>;
|
|
58
|
+
readonly latencyLineMs: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface JudgedLayer {
|
|
62
|
+
readonly envelope: typeof PLUGIN_DB_ENVELOPE;
|
|
63
|
+
readonly layer: "judged";
|
|
64
|
+
readonly address: string;
|
|
65
|
+
readonly pin: LayerPin;
|
|
66
|
+
readonly writer: "plugin-qualify";
|
|
67
|
+
readonly body: JudgedBody;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** What the converter is given beside the record: the class word, the pin, and the rules when the record states none. */
|
|
71
|
+
export interface JudgedLayerInput {
|
|
72
|
+
/** The machine class word the record was measured on (`rpi5`, `x86-64`): the set's host class. */
|
|
73
|
+
readonly machineClass: string;
|
|
74
|
+
readonly pin: LayerPin;
|
|
75
|
+
/** plugin-db's address; by default `<format>:<id>`. */
|
|
76
|
+
readonly address?: string;
|
|
77
|
+
/** The rules of a record that states none: the plugin-qualify version that judged its set (the set's provenance). */
|
|
78
|
+
readonly judgedBy?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Why a record has no judged layer, when it has none. */
|
|
82
|
+
export type NoLayer = { readonly layer: null; readonly reason: string };
|
|
83
|
+
|
|
84
|
+
/** The version of a tool named `<name> <version>` in the record's `tool`. */
|
|
85
|
+
function versionOf(record: QualificationRecord, name: string): string | undefined {
|
|
86
|
+
for (const v of Object.values(record.tool)) {
|
|
87
|
+
const m = new RegExp(`^${name.replace(/[-.]/g, "\\$&")} (\\S+)$`).exec(v);
|
|
88
|
+
if (m) return m[1];
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The cells of the record: each measured rate and quantum, with its latency in milliseconds when one was measured. */
|
|
94
|
+
export function judgedCells(record: QualificationRecord): JudgedCell[] {
|
|
95
|
+
return record.measurements.grid
|
|
96
|
+
.filter((p) => p.outcome === "probe.ok")
|
|
97
|
+
.map((p) => {
|
|
98
|
+
const frames = (p.latency as { measuredFrames?: number }).measuredFrames;
|
|
99
|
+
return { rate: p.rate, quantum: p.quantum, ...(frames === undefined ? {} : { latencyMs: latencyMsOf(frames, p.rate) }) };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The judged layer of a record, or why there is none. The verdict is the isolated tier host's, as the pins file gives
|
|
105
|
+
* it; the host is plugin-hostd at the probe runner's version, which is plugin-hostd's.
|
|
106
|
+
*/
|
|
107
|
+
export function judgedLayerOf(record: QualificationRecord, input: JudgedLayerInput): JudgedLayer | NoLayer {
|
|
108
|
+
if (record.schema !== QUALIFICATION_RECORD_SCHEMA) return { layer: null, reason: `record schema ${JSON.stringify(record.schema)} is not ${QUALIFICATION_RECORD_SCHEMA}` };
|
|
109
|
+
const tierHost = ISOLATED_TIER_HOST[record.plugin.format];
|
|
110
|
+
const v = record.verdicts.find((r) => r.host === tierHost);
|
|
111
|
+
if (v === undefined) return { layer: null, reason: `no verdict under ${tierHost ?? `a host for ${record.plugin.format}`}` };
|
|
112
|
+
const verdict = v.measured ? pinVerdictOf(v.rating as SuitabilityRating) : undefined;
|
|
113
|
+
if (verdict === undefined) return { layer: null, reason: `${tierHost}: not measured (${v.deciding ?? v.rating})` };
|
|
114
|
+
const hostVersion = versionOf(record, "plugin-hostd-probe");
|
|
115
|
+
if (hostVersion === undefined) return { layer: null, reason: "the record names no plugin-hostd-probe version" };
|
|
116
|
+
const rules = record.rules ?? (input.judgedBy === undefined ? undefined : judgementRules(input.judgedBy));
|
|
117
|
+
if (rules === undefined) return { layer: null, reason: "the record states no rules, and no judgedBy was given" };
|
|
118
|
+
return {
|
|
119
|
+
envelope: PLUGIN_DB_ENVELOPE,
|
|
120
|
+
layer: "judged",
|
|
121
|
+
address: input.address ?? `${record.plugin.format}:${record.plugin.id}`,
|
|
122
|
+
pin: input.pin,
|
|
123
|
+
writer: "plugin-qualify",
|
|
124
|
+
body: {
|
|
125
|
+
verdict,
|
|
126
|
+
machineClass: input.machineClass,
|
|
127
|
+
host: { name: JUDGED_HOST, version: hostVersion, hostingEpoch: HOSTING_EPOCH[JUDGED_HOST] },
|
|
128
|
+
rules,
|
|
129
|
+
recordSchema: record.schema,
|
|
130
|
+
cells: judgedCells(record),
|
|
131
|
+
conditions: {},
|
|
132
|
+
latencyLineMs: record.latencyLineMs ?? LATENCY_LINE_MS,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface JudgedLayerArgs {
|
|
138
|
+
readonly record: string;
|
|
139
|
+
readonly machineClass: string;
|
|
140
|
+
readonly files: string;
|
|
141
|
+
readonly layout: string | null;
|
|
142
|
+
readonly judgedBy?: string;
|
|
143
|
+
readonly address?: string;
|
|
144
|
+
/** Print the body only, as `plugin-db put <address> judged --writer plugin-qualify` reads it from stdin. */
|
|
145
|
+
readonly body: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function judgedLayerHelpText(): string {
|
|
149
|
+
return [
|
|
150
|
+
"usage: plugin-qualify judged-layer <record.qualification.json> --machine-class <word> --files <sha256>",
|
|
151
|
+
" [--layout <scheme>:<sha256>] [--judged-by <version>] [--address <address>] [--body]",
|
|
152
|
+
"",
|
|
153
|
+
"Print the plugin-db judged layer of a qualification record, for the pin plugin-db holds (its files digest and",
|
|
154
|
+
"layout). --judged-by names the plugin-qualify that judged a record stating no rules (the set's provenance). --body",
|
|
155
|
+
"prints the body only, for `plugin-db put <address> judged --writer plugin-qualify`. A record whose verdict under",
|
|
156
|
+
"plugin-hostd's worker was not measured has no layer: the reason is printed and the exit status is 1.",
|
|
157
|
+
"",
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type ParsedJudgedLayerArgs = { ok: true; args: JudgedLayerArgs } | { ok: true; help: true } | { ok: false; error: string };
|
|
162
|
+
|
|
163
|
+
export function parseJudgedLayerArgs(argv: readonly string[]): ParsedJudgedLayerArgs {
|
|
164
|
+
const opts: Record<string, string> = {};
|
|
165
|
+
const files: string[] = [];
|
|
166
|
+
let body = false;
|
|
167
|
+
for (let i = 0; i < argv.length; i++) {
|
|
168
|
+
const a = argv[i]!;
|
|
169
|
+
if (a === "--help" || a === "-h") return { ok: true, help: true };
|
|
170
|
+
if (a === "--body") body = true;
|
|
171
|
+
else if (["--machine-class", "--files", "--layout", "--judged-by", "--address"].includes(a)) {
|
|
172
|
+
const v = argv[++i];
|
|
173
|
+
if (v === undefined || v.startsWith("--")) return { ok: false, error: `${a} needs a value` };
|
|
174
|
+
opts[a] = v;
|
|
175
|
+
} else if (a.startsWith("--")) return { ok: false, error: `unknown option ${a}` };
|
|
176
|
+
else files.push(a);
|
|
177
|
+
}
|
|
178
|
+
if (files.length !== 1) return { ok: false, error: "name one record" };
|
|
179
|
+
const machineClass = opts["--machine-class"];
|
|
180
|
+
const pinFiles = opts["--files"];
|
|
181
|
+
if (machineClass === undefined) return { ok: false, error: "--machine-class is required" };
|
|
182
|
+
if (pinFiles === undefined || !/^[0-9a-f]{64}$/.test(pinFiles)) return { ok: false, error: "--files must be the 64 hex digits of the files pin" };
|
|
183
|
+
const layout = opts["--layout"] ?? null;
|
|
184
|
+
if (layout !== null && !/^[^:]+:[0-9a-f]{64}$/.test(layout)) return { ok: false, error: "--layout must be <scheme>:<sha256>" };
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
args: {
|
|
188
|
+
record: files[0]!,
|
|
189
|
+
machineClass,
|
|
190
|
+
files: pinFiles,
|
|
191
|
+
layout,
|
|
192
|
+
body,
|
|
193
|
+
...(opts["--judged-by"] === undefined ? {} : { judgedBy: opts["--judged-by"] }),
|
|
194
|
+
...(opts["--address"] === undefined ? {} : { address: opts["--address"] }),
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* What plugin-qualify's judgement declares once and writes into every record (and so into every judged layer): the
|
|
5
|
+
* latency line and the rules a record was judged by (openmixer plugin-system design 7.15, plugin-db records.md).
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
/** The latency line, in milliseconds: declared once, here, and written into every record and judged layer. */
|
|
10
|
+
export const LATENCY_LINE_MS = 5;
|
|
11
|
+
|
|
12
|
+
/** A measured latency in milliseconds at its rate, rounded to the microsecond. */
|
|
13
|
+
export const latencyMsOf = (frames: number, rate: number): number => Math.round((frames * 1e9) / rate) / 1e6;
|
|
14
|
+
|
|
15
|
+
const VERSION: string = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
16
|
+
|
|
17
|
+
/** The rules a plugin-qualify of this version judges by: `plugin-qualify-rules/<major>.<minor>`. */
|
|
18
|
+
export function judgementRules(version: string = VERSION): string {
|
|
19
|
+
const m = /^(\d+)\.(\d+)\.\d+/.exec(version);
|
|
20
|
+
if (m === null) throw new Error(`judgementRules: ${JSON.stringify(version)} is no version`);
|
|
21
|
+
return `plugin-qualify-rules/${m[1]}.${m[2]}`;
|
|
22
|
+
}
|
package/src/measurement/cli.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
|
|
37
37
|
import type { MeasurementDocument, MeasurementRun, MeasuredPlugin } from "./format.js";
|
|
38
|
-
import { buildMeasurementDocument } from "./format.js";
|
|
38
|
+
import { buildMeasurementDocument, readIntegrity } from "./format.js";
|
|
39
39
|
import { measuredPluginsFromProbeOutput } from "./probe-json.js";
|
|
40
40
|
import { buildRun, costMethodFromProbeProvenance, hostFromProbeProvenance } from "./provenance.js";
|
|
41
41
|
import type { MeasurementHost, LatencyMethod } from "./format.js";
|
|
@@ -237,10 +237,17 @@ export function buildPipeline(options: MeasureOptions, native?: HostingTools): P
|
|
|
237
237
|
});
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// a run of the prober's dimensions alone takes the pin too: a plugin measured with no integrity block cannot be pinned
|
|
241
|
+
// from its figures (plugin-qualify#316), and the measurer names the files without instantiating anything
|
|
242
|
+
const pinned = !hosting && probed && native !== undefined && (options.plugins !== undefined || current !== undefined);
|
|
243
|
+
if (pinned && native !== undefined) {
|
|
244
|
+
steps.push({ id: "pin", command: native.tool, args: ["--pin"], perPlugin: true, produces: join(options.workDir, "pins.jsonl") });
|
|
245
|
+
}
|
|
246
|
+
|
|
240
247
|
return {
|
|
241
248
|
steps,
|
|
242
249
|
...(probed && current !== undefined ? { finalCatalog: current } : {}),
|
|
243
|
-
...(hosting && options.plugins === undefined && current !== undefined ? { hostingScan: current } : {}),
|
|
250
|
+
...((hosting || pinned) && options.plugins === undefined && current !== undefined ? { hostingScan: current } : {}),
|
|
244
251
|
...(costProvenance === undefined ? {} : { costProvenance }),
|
|
245
252
|
};
|
|
246
253
|
}
|
|
@@ -599,6 +606,8 @@ export interface AssemblyInput {
|
|
|
599
606
|
readonly catalog?: unknown;
|
|
600
607
|
/** The hosting pass's lines ({@link hostingLine}), one per plugin. Absent when it did not run. */
|
|
601
608
|
readonly hosting?: readonly unknown[];
|
|
609
|
+
/** The measurer's `--pin` lines ({@link hostingLine}), one per plugin, for a run with no hosting pass. Absent when the pin step did not run. */
|
|
610
|
+
readonly pins?: readonly unknown[];
|
|
602
611
|
/** The cost pass's provenance JSON, already parsed. Absent on a latency-only run. */
|
|
603
612
|
readonly costProvenance?: unknown;
|
|
604
613
|
/** Host facts, for a run whose provenance the cost pass did not write. */
|
|
@@ -636,6 +645,7 @@ export function assembleDocument(input: AssemblyInput): MeasurementDocument {
|
|
|
636
645
|
const plugins: MeasuredPlugin[] =
|
|
637
646
|
probed.length > 0 && input.catalog !== undefined ? measuredPluginsFromProbeOutput(input.catalog, probed) : [];
|
|
638
647
|
if (input.hosting !== undefined) mergeHosting(plugins, measuredPluginsFromSweep({ measurements: input.hosting }), input.options.dimensions);
|
|
648
|
+
if (input.pins !== undefined) mergePins(plugins, input.pins);
|
|
639
649
|
const costMethod = costMethodFromProbeProvenance(input.costProvenance);
|
|
640
650
|
const host = hostFromProbeProvenance(input.costProvenance) ?? input.host;
|
|
641
651
|
const run: MeasurementRun = buildRun({
|
|
@@ -655,6 +665,20 @@ export function assembleDocument(input: AssemblyInput): MeasurementDocument {
|
|
|
655
665
|
return buildMeasurementDocument(run, plugins);
|
|
656
666
|
}
|
|
657
667
|
|
|
668
|
+
/** The pin lines folded into the prober's plugins, by URI: a plugin without one keeps none, and a line for a plugin not measured adds none. */
|
|
669
|
+
function mergePins(plugins: MeasuredPlugin[], rows: readonly unknown[]): void {
|
|
670
|
+
for (const row of rows) {
|
|
671
|
+
if (typeof row !== "object" || row === null) continue;
|
|
672
|
+
const r = row as Record<string, unknown>;
|
|
673
|
+
const integrity = readIntegrity(r["integrity"]);
|
|
674
|
+
const at = plugins.findIndex((p) => p.uri === r["uri"]);
|
|
675
|
+
if (integrity === undefined || at < 0) continue;
|
|
676
|
+
const p = plugins[at]!;
|
|
677
|
+
const version = typeof r["version"] === "string" && r["version"] !== "" ? r["version"] : undefined;
|
|
678
|
+
plugins[at] = { ...p, ...(p.integrity === undefined ? { integrity } : {}), ...(p.version === undefined && version !== undefined ? { version } : {}) };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
658
682
|
/**
|
|
659
683
|
* The hosting pass's plugins folded into the prober's, by URI: each keeps the hosting dimensions the
|
|
660
684
|
* run asked for and the pin, and a plugin only the hosting pass measured joins with its own topology.
|
|
@@ -303,6 +303,8 @@ export interface MemoryFootprint {
|
|
|
303
303
|
/** Activated, after processing: what a long-running host carries. */
|
|
304
304
|
readonly atRestKb?: number;
|
|
305
305
|
readonly notesHeldKb?: number;
|
|
306
|
+
/** The notes held (voices sounding) when `notesHeldKb` was read, the count `basis` says where it came from. */
|
|
307
|
+
readonly notesHeld?: number;
|
|
306
308
|
readonly secondInstanceKb?: number;
|
|
307
309
|
readonly basis?: VoiceBasis;
|
|
308
310
|
readonly unmeasuredReason?: string;
|
|
@@ -950,6 +952,11 @@ function readLatency(value: unknown): LatencyMeasurement | undefined {
|
|
|
950
952
|
}
|
|
951
953
|
|
|
952
954
|
/** A KB figure: finite and never negative. */
|
|
955
|
+
function notesCount(value: unknown): number | undefined {
|
|
956
|
+
const n = positiveNumber(value);
|
|
957
|
+
return n !== undefined && Number.isInteger(n) ? n : undefined;
|
|
958
|
+
}
|
|
959
|
+
|
|
953
960
|
function kilobytes(value: unknown): number | undefined {
|
|
954
961
|
const n = finiteNumber(value);
|
|
955
962
|
return n !== undefined && n >= 0 ? n : undefined;
|
|
@@ -963,6 +970,7 @@ export function readMemoryFootprint(value: unknown): MemoryFootprint | undefined
|
|
|
963
970
|
...optional("afterActivateKb", kilobytes(value.afterActivateKb)),
|
|
964
971
|
...optional("atRestKb", kilobytes(value.atRestKb)),
|
|
965
972
|
...optional("notesHeldKb", kilobytes(value.notesHeldKb)),
|
|
973
|
+
...optional("notesHeld", notesCount(value.notesHeld)),
|
|
966
974
|
...optional("secondInstanceKb", kilobytes(value.secondInstanceKb)),
|
|
967
975
|
...optional("basis", isVoiceBasis(value.basis) ? value.basis : undefined),
|
|
968
976
|
...reasonOf(value),
|
|
@@ -20,11 +20,12 @@ import { join } from "node:path";
|
|
|
20
20
|
|
|
21
21
|
import type { Adjustment } from "./history-adjust.js";
|
|
22
22
|
import type { Judgment, RecordReason } from "./probe-judge.js";
|
|
23
|
-
import { sha256Hex, PROBE_OK, type ProbeGridPoint, type ProbeOutput } from "./probe-output.js";
|
|
23
|
+
import { sha256Hex, PROBE_NOT_ATTEMPTED, PROBE_OK, type ProbeGridPoint, type ProbeOutput } from "./probe-output.js";
|
|
24
24
|
import type { VoicesHeld } from "./measurement/format.js";
|
|
25
25
|
import { writeFileWhole } from "./whole-file.js";
|
|
26
26
|
import { HOSTING_CODE } from "./hosting-codes.js";
|
|
27
27
|
import { MEASUREMENT_UNMEASURED_REASONS } from "@openmixer/omx-contract";
|
|
28
|
+
import { judgementRules, latencyMsOf, LATENCY_LINE_MS } from "./judgement-line.js";
|
|
28
29
|
|
|
29
30
|
export const QUALIFICATION_RECORD_SCHEMA = "plugin-qualify.qualification-record/1";
|
|
30
31
|
|
|
@@ -201,6 +202,8 @@ export type RecordGridPoint = Omit<ProbeGridPoint, "latency" | "memory"> & {
|
|
|
201
202
|
/** The footprint as measured, with what it comes to: what a client adding the plugin reads (`memory-budget.ts`). */
|
|
202
203
|
readonly memory?: MemoryFootprint & { readonly firstInstanceKb?: number; readonly perInstanceKb?: number };
|
|
203
204
|
readonly latency: (Omit<NonNullable<ProbeGridPoint["latency"]>, "measuredFrames"> & { readonly measuredFrames: number }) | UnmeasuredLatency;
|
|
205
|
+
/** The measured latency in milliseconds at the cell's rate, beside the record's `latencyLineMs`; absent when none was measured. */
|
|
206
|
+
readonly latencyMs?: number;
|
|
204
207
|
};
|
|
205
208
|
|
|
206
209
|
const UNMEASURED_WORDS: ReadonlySet<string> = new Set(Object.values(MEASUREMENT_UNMEASURED_REASONS).map((r) => r.word));
|
|
@@ -220,12 +223,20 @@ export function memoryOfCell(p: ProbeGridPoint): Pick<RecordGridPoint, "memory">
|
|
|
220
223
|
return { memory: { ...p.memory, ...(first === undefined ? {} : { firstInstanceKb: first }), ...(per === undefined ? {} : { perInstanceKb: per }) } };
|
|
221
224
|
}
|
|
222
225
|
|
|
226
|
+
/** A grid point as the record writes it: its footprint, its latency or why there is none, and the latency in milliseconds. */
|
|
227
|
+
export function gridPointOf(output: ProbeOutput, p: ProbeGridPoint): RecordGridPoint {
|
|
228
|
+
const latency = latencyOfCell(output, p);
|
|
229
|
+
const frames = (latency as { measuredFrames?: number }).measuredFrames;
|
|
230
|
+
return { ...p, ...memoryOfCell(p), latency, ...(frames === undefined ? {} : { latencyMs: latencyMsOf(frames, p.rate) }) };
|
|
231
|
+
}
|
|
232
|
+
|
|
223
233
|
/** The unmeasured word of a cell whose run did not end well, by its outcome code. */
|
|
224
234
|
const OUTCOME_WORD: Readonly<Record<string, string>> = {
|
|
225
235
|
"probe.crashed": word("probeCrashed"),
|
|
226
236
|
"probe.timeout": word("probeTimeout"),
|
|
227
237
|
"probe.refused-instantiation": word("refusedInstantiation"),
|
|
228
238
|
"probe.non-finite-output": word("nonFiniteOutput"),
|
|
239
|
+
[PROBE_NOT_ATTEMPTED]: word("notAttempted"),
|
|
229
240
|
};
|
|
230
241
|
|
|
231
242
|
/**
|
|
@@ -361,6 +372,10 @@ export interface QualificationRecord {
|
|
|
361
372
|
/** Who measured and when. */
|
|
362
373
|
readonly tool: Readonly<Record<string, string>>;
|
|
363
374
|
readonly measuredAt: string;
|
|
375
|
+
/** The latency line the cells' `latencyMs` stand against (`LATENCY_LINE_MS`); absent on a record written before it was declared. */
|
|
376
|
+
readonly latencyLineMs?: number;
|
|
377
|
+
/** The rules the verdicts were judged by (`judgementRules`); absent on a record written before they were stated. */
|
|
378
|
+
readonly rules?: string;
|
|
364
379
|
/** The second verdict, per host class of the history that was read; absent until `adjust --history` has run. */
|
|
365
380
|
readonly history?: readonly HistoryBlock[];
|
|
366
381
|
/** Where the record came from when it was not measured on this device: the distributed set's provenance, as the set's package wrote it. */
|
|
@@ -483,13 +498,15 @@ export function buildQualificationRecord(
|
|
|
483
498
|
tiers: tiersOf(judgment),
|
|
484
499
|
cost: judgment.hosts.map((h) => ({ host: h.verdict.host, ...h.cost })),
|
|
485
500
|
measurements: {
|
|
486
|
-
grid: o.grid.map((p) => (
|
|
501
|
+
grid: o.grid.map((p) => gridPointOf(o, p)),
|
|
487
502
|
...(o.behaviour === undefined ? {} : { behaviour: o.behaviour }),
|
|
488
503
|
...(unmeasuredFigures(o).length === 0 ? {} : { unmeasured: unmeasuredFigures(o) }),
|
|
489
504
|
},
|
|
490
505
|
...(notMeasured.length === 0 ? {} : { notMeasured }),
|
|
491
506
|
tool: o.run.tools,
|
|
492
507
|
measuredAt: o.run.startedAt,
|
|
508
|
+
latencyLineMs: LATENCY_LINE_MS,
|
|
509
|
+
rules: judgementRules(),
|
|
493
510
|
probe: {
|
|
494
511
|
file: copy.file,
|
|
495
512
|
sha256: copy.sha256,
|
package/src/qualify-isolated.ts
CHANGED
|
@@ -100,16 +100,18 @@ function figuresOf(run: IsolatedRunFacts): IsolatedFigures | undefined {
|
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* A verdict whose only gap is memory nobody measured: it reads `unknown` and every reason that is not `suitable` is
|
|
103
|
-
* `hosting.memory.unmeasured
|
|
104
|
-
*
|
|
103
|
+
* `hosting.memory.unmeasured`, or a `conditional` one the isolated step answers (an RT violation seen in the run). Nothing
|
|
104
|
+
* says the plugin is unfit, so it is hosted in a worker of its own until its footprint is measured, and the isolated step
|
|
105
|
+
* is what shows it running alone. A conditional reason beside the memory gap owes the step as a conditional verdict does:
|
|
106
|
+
* whether one run saw a violation and another did not must not decide whether the step is run (#319).
|
|
105
107
|
*/
|
|
106
108
|
export function memoryGapOnly(v: Pick<RecordVerdict, "measured" | "rating" | "reasons">): boolean {
|
|
107
109
|
if (!v.measured || v.rating !== "unknown") return false;
|
|
108
110
|
const open = v.reasons.filter((r) => r.rating !== "suitable");
|
|
109
|
-
return open.
|
|
111
|
+
return open.some((r) => r.code === HOSTING_CODE.memoryUnmeasured) && open.every((r) => r.code === HOSTING_CODE.memoryUnmeasured || r.rating === "conditional");
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
/** Whether a record's outcome calls for the isolated step: rated `conditional` by some host, a memory gap and nothing else, or a probe that crashed or hung. */
|
|
114
|
+
/** Whether a record's outcome calls for the isolated step: rated `conditional` by some host, a memory gap and nothing else unanswered, or a probe that crashed or hung. */
|
|
113
115
|
export function owesIsolated(record: QualificationRecord): boolean {
|
|
114
116
|
return PROBE_FAILED_HARD.has(record.probe.outcome) || record.verdicts.some((v) => v.measured && (v.rating === "conditional" || memoryGapOnly(v)));
|
|
115
117
|
}
|
package/src/qualify-sync.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
24
24
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
25
25
|
|
|
26
26
|
import { clapQualifyTools } from "./clap-qualify.js";
|
|
27
|
-
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, outcomeOfVerdict, matchDistributed, readBoardModel, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
|
|
27
|
+
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, hostClassOfMachine, outcomeOfVerdict, matchDistributed, readBoardModel, readMachine, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
|
|
28
28
|
import { clapSearchPath, parseClapScan } from "./clap-loader.js";
|
|
29
29
|
import { HOSTING_CODE } from "./hosting-codes.js";
|
|
30
30
|
import { shippedHostProfile, type HostProfile } from "./host-profiles.js";
|
|
@@ -34,7 +34,7 @@ import { POOL_RESULT, completePool, judgePoolRun, pinAfterPool, pinFromRecordWit
|
|
|
34
34
|
import { hostdPinFiles, type PinFilesRun } from "./hostd-pin-files.js";
|
|
35
35
|
import type { Judgment } from "./probe-judge.js";
|
|
36
36
|
import { qualifyTargets, type ProbeRun } from "./probe-runner.js";
|
|
37
|
-
import { GRID_FULL, parseGridSpec, type GridSpec } from "./measurement-grid.js";
|
|
37
|
+
import { GRID_FULL, hostCells, parseGridSpec, type GridCell, type GridSpec } from "./measurement-grid.js";
|
|
38
38
|
import { adapterPass, type AdapterPassOptions, type AdapterPassResult } from "./qualify-adapter-pass.js";
|
|
39
39
|
import { ISOLATED_RESULT, baseSteps, completeIsolated, isolatedCell, missingSteps, judgeIsolatedRun, owesIsolated, pinAfterStep, spawnedIsolatedRunner, type IsolatedRunner } from "./qualify-isolated.js";
|
|
40
40
|
import { lv2World, rejudgeRecord, rejudgeState, rejudgeToolName, memoryPass, type MemoryPassResult, type RejudgeResult } from "./qualify-rejudge.js";
|
|
@@ -228,6 +228,7 @@ export function resolveSyncSettings(
|
|
|
228
228
|
env: Readonly<Record<string, string | undefined>>,
|
|
229
229
|
home: string = homedir(),
|
|
230
230
|
boardModel: () => string | undefined = readBoardModel,
|
|
231
|
+
machineArch: () => string | undefined = readMachine,
|
|
231
232
|
): { ok: true; settings: SyncSettings } | { ok: false; error: string } {
|
|
232
233
|
const get = (key: string): string | undefined => args.values.get(key) ?? file.get(key);
|
|
233
234
|
const state = env["XDG_STATE_HOME"] !== undefined && env["XDG_STATE_HOME"] !== "" ? env["XDG_STATE_HOME"] : join(home, ".local", "state");
|
|
@@ -276,7 +277,7 @@ export function resolveSyncSettings(
|
|
|
276
277
|
? [DISTRIBUTED_DIR_SYSTEM]
|
|
277
278
|
: [dataHome, ...dataDirs].map((d) => join(d, "omx", "qualification"));
|
|
278
279
|
const classNamed = get("host_class");
|
|
279
|
-
const hostClass = classNamed !== undefined && classNamed !== "" ? classNamed : hostClassOfBoard(boardModel());
|
|
280
|
+
const hostClass = classNamed !== undefined && classNamed !== "" ? classNamed : hostClassOfBoard(boardModel()) ?? hostClassOfMachine(machineArch());
|
|
280
281
|
if (hostClass !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(hostClass)) return { ok: false, error: `host_class ${hostClass} is not a directory name` };
|
|
281
282
|
return {
|
|
282
283
|
ok: true,
|
|
@@ -681,7 +682,10 @@ export function refreshPins(ctx: SyncContext, state: SyncState): { state: SyncSt
|
|
|
681
682
|
const pin = plugin.pin;
|
|
682
683
|
if (pin === undefined || pin.integrity.files === undefined) return plugin;
|
|
683
684
|
const r = hostdPinFiles(pin.uri, ctx.settings.lv2Dirs, ctx.env, ...(ctx.pinFilesRun === undefined ? [] : [ctx.pinFilesRun]));
|
|
684
|
-
if (!r.ok)
|
|
685
|
+
if (!r.ok) {
|
|
686
|
+
ctx.log(`${pin.uri}: pin files not refreshed: ${r.detail}`);
|
|
687
|
+
return plugin;
|
|
688
|
+
}
|
|
685
689
|
const old = new Map(pin.integrity.files.map((f) => [f.path, f.sha256]));
|
|
686
690
|
const changed = r.files.filter((f) => old.has(f.path) && old.get(f.path) !== f.sha256).map((f) => f.path);
|
|
687
691
|
const same = r.files.length === old.size && r.files.every((f) => old.get(f.path) === f.sha256);
|
|
@@ -931,7 +935,9 @@ export function isolatedPass(ctx: SyncContext, state: SyncState): { readonly sta
|
|
|
931
935
|
return rest.outcome === "qualified" && rebuilt.verdict === "refused" ? { ...rest, outcome: "refused" as const, pin: rebuilt } : { ...rest, pin: rebuilt };
|
|
932
936
|
}
|
|
933
937
|
ctx.log(`${p.uri}: isolated step ${done.step.result}${done.step.code === undefined ? "" : ` (${done.step.code})`}`);
|
|
934
|
-
|
|
938
|
+
// a plugin the state holds no pin for is pinned from the record the step now completes: the bytes it names and the word
|
|
939
|
+
// the step earned (plugin-qualify#316: 38 plugins read hosting.memory.unmeasured and had none)
|
|
940
|
+
const pin = p.pin !== undefined ? pinAfterStep(p.pin, done.step) : withHostdFiles(ctx, pinFromRecordWithPool(done.record));
|
|
935
941
|
const { pin: _pin, code: _code, ...rest } = p;
|
|
936
942
|
if (done.step.result === ISOLATED_RESULT.isolated) {
|
|
937
943
|
count.isolated++;
|
|
@@ -985,6 +991,14 @@ export function poolPass(ctx: SyncContext, state: SyncState): { readonly state:
|
|
|
985
991
|
|
|
986
992
|
// --- the distributed records ----------------------------------------------------------------------------------------
|
|
987
993
|
|
|
994
|
+
/**
|
|
995
|
+
* The cells a sync runs at, which a distributed set must hold to apply (#317): the grid's own cells when it names them
|
|
996
|
+
* (`grid 48000/512`), else the cells the host profile judges cost and memory at.
|
|
997
|
+
*/
|
|
998
|
+
export function syncCells(settings: Pick<SyncSettings, "grid" | "hostProfile">): GridCell[] {
|
|
999
|
+
return settings.grid.kind === "cells" ? [...settings.grid.cells] : hostCells(settings.hostProfile);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
988
1002
|
/** The layer a run reads: none under `--force` or `--no-distributed`, or without a host class. */
|
|
989
1003
|
function distributedLayerOf(settings: SyncSettings, force: boolean, log: (line: string) => void): DistributedLayer {
|
|
990
1004
|
if (force || !settings.distributed) return EMPTY_LAYER;
|
|
@@ -1007,12 +1021,10 @@ function takeDistributed(ctx: SyncContext, layer: DistributedLayer, item: SyncIt
|
|
|
1007
1021
|
if (layer.records === 0) return undefined;
|
|
1008
1022
|
const started = Date.now();
|
|
1009
1023
|
const host = ctx.settings.hostProfile.id;
|
|
1010
|
-
const match = matchDistributed(layer, item, host, (paths) => digestFiles(item.root, paths, files));
|
|
1024
|
+
const match = matchDistributed(layer, item, host, (paths) => digestFiles(item.root, paths, files), syncCells(ctx.settings));
|
|
1011
1025
|
if (match.status === "none") return undefined;
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
return undefined;
|
|
1015
|
-
}
|
|
1026
|
+
for (const line of match.lines) ctx.log(line);
|
|
1027
|
+
if (match.status === "ignored") return undefined;
|
|
1016
1028
|
for (const r of match.records) {
|
|
1017
1029
|
const missing = missingSteps(r.record);
|
|
1018
1030
|
if (missing.length > 0) {
|