@camstack/addon-pipeline-orchestrator 1.2.619 → 1.2.620
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/dist/_stub.js +1 -1
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-Brtt25nW.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-C4ACA0hQ.mjs} +3 -3
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dm0FVwD0.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CBSfN92a.mjs} +1 -1
- package/dist/{hostInit-fkGgy-RP.mjs → hostInit-CndUy6j1.mjs} +3 -3
- package/dist/index.js +255 -29
- package/dist/index.mjs +255 -29
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -77565,28 +77565,33 @@ var DETECTOR_PERF = {
|
|
|
77565
77565
|
},
|
|
77566
77566
|
"amd-igpu": {
|
|
77567
77567
|
"yolov9t-320": {
|
|
77568
|
-
p50Ms:
|
|
77569
|
-
fps:
|
|
77568
|
+
p50Ms: 40,
|
|
77569
|
+
fps: 25,
|
|
77570
77570
|
measured: true
|
|
77571
77571
|
},
|
|
77572
77572
|
"yolov9t-640": {
|
|
77573
|
-
p50Ms:
|
|
77574
|
-
fps:
|
|
77573
|
+
p50Ms: 89,
|
|
77574
|
+
fps: 11,
|
|
77575
77575
|
measured: true
|
|
77576
77576
|
},
|
|
77577
77577
|
"yolov9s-320": {
|
|
77578
|
-
p50Ms:
|
|
77579
|
-
fps:
|
|
77578
|
+
p50Ms: 65,
|
|
77579
|
+
fps: 15,
|
|
77580
77580
|
measured: true
|
|
77581
77581
|
},
|
|
77582
77582
|
"yolov9s-640": {
|
|
77583
|
-
p50Ms:
|
|
77584
|
-
fps:
|
|
77583
|
+
p50Ms: 125,
|
|
77584
|
+
fps: 8,
|
|
77585
77585
|
measured: true
|
|
77586
77586
|
},
|
|
77587
77587
|
"yolov9m-320": {
|
|
77588
|
-
p50Ms:
|
|
77589
|
-
fps:
|
|
77588
|
+
p50Ms: 85,
|
|
77589
|
+
fps: 12,
|
|
77590
|
+
measured: true
|
|
77591
|
+
},
|
|
77592
|
+
"yolov9m-640": {
|
|
77593
|
+
p50Ms: 228,
|
|
77594
|
+
fps: 4,
|
|
77590
77595
|
measured: true
|
|
77591
77596
|
}
|
|
77592
77597
|
},
|
|
@@ -78113,6 +78118,55 @@ function resolveEffectiveShares(input) {
|
|
|
78113
78118
|
deviceWeights
|
|
78114
78119
|
};
|
|
78115
78120
|
}
|
|
78121
|
+
/**
|
|
78122
|
+
* Automatic shares from CAPACITY (D755). Pure.
|
|
78123
|
+
*
|
|
78124
|
+
* A node's share is its capacity in parallel sessions over the cluster's; a
|
|
78125
|
+
* device's share inside its node follows its throughput. There is no load
|
|
78126
|
+
* input: nothing here depends on how many sessions ran, only on what each
|
|
78127
|
+
* node can carry.
|
|
78128
|
+
*
|
|
78129
|
+
* Two places a capacity can come from:
|
|
78130
|
+
* - `hardware` — each enabled accelerator at its Auto tune model, from the
|
|
78131
|
+
* measured latency table: ⌊fps × (1 − headroom) / camera fps⌋ sessions;
|
|
78132
|
+
* - `benchmark` — the node's own measured max sessions per device, read
|
|
78133
|
+
* through a {@link NodeCapacitySource}. A node whose results do not cover
|
|
78134
|
+
* every enabled device it would be sized on is sized from `hardware`
|
|
78135
|
+
* instead, and the answer says so per node — never a silent mix.
|
|
78136
|
+
*/
|
|
78137
|
+
/**
|
|
78138
|
+
* The measured capacity of `deviceKey` for the model it runs NOW, or
|
|
78139
|
+
* undefined. A cell measured for another model says nothing about this one
|
|
78140
|
+
* (a failed load of a heavier build is not this build's capacity), and a cell
|
|
78141
|
+
* that names no model cannot be matched. The ONE predicate behind both the
|
|
78142
|
+
* automatic shares and the automatic benchmark's "has results".
|
|
78143
|
+
*/
|
|
78144
|
+
function capacityForModel(node, deviceKey, modelId) {
|
|
78145
|
+
if (node === void 0 || modelId === null) return void 0;
|
|
78146
|
+
return node.devices.filter((d) => d.deviceKey === deviceKey && d.modelId === modelId).toSorted((a, b) => b.measuredAtMs - a.measuredAtMs)[0];
|
|
78147
|
+
}
|
|
78148
|
+
/**
|
|
78149
|
+
* Every measured (device × model) cell that no device runs now — what
|
|
78150
|
+
* {@link capacityForModel} skips, so the caller can say so once.
|
|
78151
|
+
*/
|
|
78152
|
+
function foreignCapacityCells(recommendations, measured) {
|
|
78153
|
+
const out = [];
|
|
78154
|
+
for (const rec of recommendations) {
|
|
78155
|
+
const node = measured.get(rec.nodeId);
|
|
78156
|
+
if (node === void 0) continue;
|
|
78157
|
+
for (const cell of node.devices) {
|
|
78158
|
+
const runs = rec.devices.find((d) => d.deviceKey === cell.deviceKey)?.chosen?.modelId ?? null;
|
|
78159
|
+
if (cell.modelId === null || cell.modelId === runs) continue;
|
|
78160
|
+
out.push({
|
|
78161
|
+
nodeId: rec.nodeId,
|
|
78162
|
+
deviceKey: cell.deviceKey,
|
|
78163
|
+
measuredModelId: cell.modelId,
|
|
78164
|
+
runsModelId: runs
|
|
78165
|
+
});
|
|
78166
|
+
}
|
|
78167
|
+
}
|
|
78168
|
+
return out;
|
|
78169
|
+
}
|
|
78116
78170
|
/** Integer shares summing to 100 (largest remainder); ties broken by key. */
|
|
78117
78171
|
function toPercentShares(weights) {
|
|
78118
78172
|
const total = [...weights.values()].reduce((a, b) => a + b, 0);
|
|
@@ -78164,7 +78218,11 @@ function sizeNode(rec, input) {
|
|
|
78164
78218
|
if (input.source === "hardware") return fromHardware(null);
|
|
78165
78219
|
if (!rec.reachable) return fromHardware(null);
|
|
78166
78220
|
const results = input.measured.get(rec.nodeId);
|
|
78167
|
-
const byKey = new Map(
|
|
78221
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
78222
|
+
for (const d of rec.devices) {
|
|
78223
|
+
const row = capacityForModel(results, d.deviceKey, d.chosen?.modelId ?? null);
|
|
78224
|
+
if (row !== void 0) byKey.set(d.deviceKey, row);
|
|
78225
|
+
}
|
|
78168
78226
|
const usable = rec.devices.filter((d) => d.enabled && d.available);
|
|
78169
78227
|
const mustCover = rec.devices.filter((d) => d.enabled && d.available && d.chosen !== null);
|
|
78170
78228
|
const covered = usable.filter((d) => byKey.has(d.deviceKey));
|
|
@@ -78414,16 +78472,34 @@ function measuredDetectorCells(rows, nodeId, ctx) {
|
|
|
78414
78472
|
return out;
|
|
78415
78473
|
}
|
|
78416
78474
|
/**
|
|
78417
|
-
*
|
|
78418
|
-
*
|
|
78475
|
+
* Parallel sessions a `device`-phase cell stands for.
|
|
78476
|
+
*
|
|
78477
|
+
* Both numbers are "streams at the camera rate the hardware carried inside
|
|
78478
|
+
* the frame interval" (`kMax` for the detector alone, `maxSessions` for the
|
|
78479
|
+
* detector plus its enrichment steps) and the benchmark page already shows
|
|
78480
|
+
* `kMax` as "streams", so the unit is the same and the conversion is the
|
|
78481
|
+
* identity. It is an UPPER bound for a session that also runs enrichment —
|
|
78482
|
+
* which is why a `pipeline` cell for the same model always wins over it.
|
|
78483
|
+
*/
|
|
78484
|
+
function sessionsOfDeviceCell(kMax) {
|
|
78485
|
+
return Math.max(0, Math.floor(kMax));
|
|
78486
|
+
}
|
|
78487
|
+
/**
|
|
78488
|
+
* Per node, every (device × model) at its best fresh measurement: the newest
|
|
78489
|
+
* `pipeline` cell, else the newest `device` cell read as
|
|
78490
|
+
* {@link sessionsOfDeviceCell}. Several models of one device are kept apart —
|
|
78491
|
+
* the caller picks the one the device runs NOW (a cell for another model says
|
|
78492
|
+
* nothing about it).
|
|
78419
78493
|
*/
|
|
78420
78494
|
function measuredCapacityFromResults(rows, nodeIds, ctx) {
|
|
78421
78495
|
const byNode = /* @__PURE__ */ new Map();
|
|
78422
78496
|
for (const row of freshResults(rows, ctx)) {
|
|
78423
|
-
if (!nodeIds.includes(row.nodeId)
|
|
78497
|
+
if (!nodeIds.includes(row.nodeId)) continue;
|
|
78498
|
+
const candidate = capacityOfRow(row);
|
|
78424
78499
|
const devices = byNode.get(row.nodeId) ?? /* @__PURE__ */ new Map();
|
|
78425
|
-
const
|
|
78426
|
-
|
|
78500
|
+
const key = `${candidate.deviceKey}\u0000${candidate.modelId ?? ""}`;
|
|
78501
|
+
const held = devices.get(key);
|
|
78502
|
+
if (held === void 0 || betterCapacityCell(candidate, held)) devices.set(key, candidate);
|
|
78427
78503
|
byNode.set(row.nodeId, devices);
|
|
78428
78504
|
}
|
|
78429
78505
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -78432,16 +78508,32 @@ function measuredCapacityFromResults(rows, nodeIds, ctx) {
|
|
|
78432
78508
|
if (devices === void 0) continue;
|
|
78433
78509
|
out.set(nodeId, {
|
|
78434
78510
|
nodeId,
|
|
78435
|
-
devices: [...devices.values()].toSorted((a, b) => a.
|
|
78436
|
-
deviceKey: r.cell.deviceKey,
|
|
78437
|
-
maxSessions: r.cell.phase === "pipeline" ? r.cell.maxSessions : 0,
|
|
78438
|
-
modelId: r.cell.modelId,
|
|
78439
|
-
measuredAtMs: r.measuredAtMs
|
|
78440
|
-
}))
|
|
78511
|
+
devices: [...devices.values()].toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey) || (a.modelId ?? "").localeCompare(b.modelId ?? ""))
|
|
78441
78512
|
});
|
|
78442
78513
|
}
|
|
78443
78514
|
return out;
|
|
78444
78515
|
}
|
|
78516
|
+
function capacityOfRow(row) {
|
|
78517
|
+
const { cell } = row;
|
|
78518
|
+
return cell.phase === "pipeline" ? {
|
|
78519
|
+
deviceKey: cell.deviceKey,
|
|
78520
|
+
maxSessions: cell.maxSessions,
|
|
78521
|
+
modelId: cell.modelId,
|
|
78522
|
+
measuredAtMs: row.measuredAtMs,
|
|
78523
|
+
phase: "pipeline"
|
|
78524
|
+
} : {
|
|
78525
|
+
deviceKey: cell.deviceKey,
|
|
78526
|
+
maxSessions: sessionsOfDeviceCell(cell.kMax),
|
|
78527
|
+
modelId: cell.modelId,
|
|
78528
|
+
measuredAtMs: row.measuredAtMs,
|
|
78529
|
+
phase: "device"
|
|
78530
|
+
};
|
|
78531
|
+
}
|
|
78532
|
+
/** A pipeline cell beats a device cell; within a phase the newer one wins. */
|
|
78533
|
+
function betterCapacityCell(a, b) {
|
|
78534
|
+
if (a.phase !== b.phase) return a.phase === "pipeline";
|
|
78535
|
+
return a.measuredAtMs > b.measuredAtMs;
|
|
78536
|
+
}
|
|
78445
78537
|
var BENCH_ROOT_STEP_ID = "object-detection";
|
|
78446
78538
|
function slotAddons(schema, slot) {
|
|
78447
78539
|
return schema.slots.find((s) => s.id === slot)?.addons ?? [];
|
|
@@ -79025,6 +79117,44 @@ var NodeBenchmarkStore = class {
|
|
|
79025
79117
|
return true;
|
|
79026
79118
|
}
|
|
79027
79119
|
};
|
|
79120
|
+
//#endregion
|
|
79121
|
+
//#region src/node-benchmark/auto-benchmark.ts
|
|
79122
|
+
/**
|
|
79123
|
+
* The automatic node benchmark: an enabled, available device that has no
|
|
79124
|
+
* measured result — a new node, or a device discovered later — is measured by
|
|
79125
|
+
* a `device`-phase run limited to it, so the automatic shares read a number
|
|
79126
|
+
* instead of the table without the operator starting anything.
|
|
79127
|
+
*
|
|
79128
|
+
* - ONE auto run at a time, and never while any benchmark runs (the service
|
|
79129
|
+
* allows one run cluster-wide);
|
|
79130
|
+
* - a node that is busy is deferred: it carries more than
|
|
79131
|
+
* {@link AUTO_BENCH_MAX_CAMERAS_ON_NODE} cameras (a benchmark moves them
|
|
79132
|
+
* off and a camera with no other frame source would stay and spoil the
|
|
79133
|
+
* numbers). The hub, which usually carries cameras, waits until it is quiet
|
|
79134
|
+
* and the operator can still run it by hand;
|
|
79135
|
+
* - a failed run is retried at most {@link AUTO_BENCH_MAX_ATTEMPTS} times,
|
|
79136
|
+
* {@link AUTO_BENCH_RETRY_MS} apart. The ledger is persisted per (node,
|
|
79137
|
+
* device) and restored at boot — a respawn right after a lost run would
|
|
79138
|
+
* otherwise start the next one at once and hand the device a fresh budget;
|
|
79139
|
+
* a device that gains a usable result is forgotten;
|
|
79140
|
+
* - never for a device the operator disabled, one that is unplugged or
|
|
79141
|
+
* excluded, or one that already has a usable result: a fresh cell for the
|
|
79142
|
+
* model it runs NOW (the predicate the automatic shares read, so the two
|
|
79143
|
+
* cannot disagree). A result for another model does not count — the device
|
|
79144
|
+
* is measured again.
|
|
79145
|
+
*
|
|
79146
|
+
* The planner is pure; the scheduler injects every read and the start.
|
|
79147
|
+
*/
|
|
79148
|
+
/**
|
|
79149
|
+
* `nodeId\0deviceKey` of every device that has a usable result: a fresh
|
|
79150
|
+
* measurement for the model it runs now. The SAME predicate
|
|
79151
|
+
* ({@link capacityForModel}) the automatic shares size a node by.
|
|
79152
|
+
*/
|
|
79153
|
+
function benchmarkedDevices(runs, measured) {
|
|
79154
|
+
const out = /* @__PURE__ */ new Set();
|
|
79155
|
+
for (const run of runs) if (capacityForModel(measured.get(run.nodeId), run.deviceKey, run.modelId) !== void 0) out.add(benchKey(run.nodeId, run.deviceKey));
|
|
79156
|
+
return out;
|
|
79157
|
+
}
|
|
79028
79158
|
/** How often the scheduler looks (it is also nudged when a device set changes). */
|
|
79029
79159
|
var AUTO_BENCH_TICK_MS = 6e4;
|
|
79030
79160
|
/** Hard deadline of an auto run (a one-model device ramp takes well under this). */
|
|
@@ -79111,6 +79241,7 @@ var AutoBenchmarkScheduler = class {
|
|
|
79111
79241
|
attempts = /* @__PURE__ */ new Map();
|
|
79112
79242
|
/** The last decision logged per (node, device): a decision is logged when it CHANGES. */
|
|
79113
79243
|
lastLogged = /* @__PURE__ */ new Map();
|
|
79244
|
+
restored = null;
|
|
79114
79245
|
inFlight = null;
|
|
79115
79246
|
timer = null;
|
|
79116
79247
|
constructor(deps) {
|
|
@@ -79140,6 +79271,27 @@ var AutoBenchmarkScheduler = class {
|
|
|
79140
79271
|
});
|
|
79141
79272
|
return this.inFlight;
|
|
79142
79273
|
}
|
|
79274
|
+
/** Load the persisted ledger ONCE, before the first decision; a failed read is a fresh budget, said so. */
|
|
79275
|
+
restore() {
|
|
79276
|
+
this.restored ??= (async () => {
|
|
79277
|
+
const persistence = this.deps.persistence;
|
|
79278
|
+
if (persistence === void 0) return;
|
|
79279
|
+
try {
|
|
79280
|
+
for (const [key, attempt] of await persistence.load()) if (!this.attempts.has(key)) this.attempts.set(key, attempt);
|
|
79281
|
+
this.deps.logger.info("auto benchmark: attempts restored", { meta: { devices: this.attempts.size } });
|
|
79282
|
+
} catch (err) {
|
|
79283
|
+
this.deps.logger.warn("auto benchmark: attempt ledger unreadable — starting with none", { meta: { error: errMsg$2(err) } });
|
|
79284
|
+
}
|
|
79285
|
+
})();
|
|
79286
|
+
return this.restored;
|
|
79287
|
+
}
|
|
79288
|
+
async persist() {
|
|
79289
|
+
try {
|
|
79290
|
+
await this.deps.persistence?.save(this.attempts);
|
|
79291
|
+
} catch (err) {
|
|
79292
|
+
this.deps.logger.warn("auto benchmark: attempt ledger not saved — kept in memory", { meta: { error: errMsg$2(err) } });
|
|
79293
|
+
}
|
|
79294
|
+
}
|
|
79143
79295
|
note(decision, extra = "") {
|
|
79144
79296
|
const signature = `${decision.kind}:${"reason" in decision ? decision.reason : ""}${extra}`;
|
|
79145
79297
|
const key = benchKey(decision.nodeId, decision.deviceKey);
|
|
@@ -79156,14 +79308,20 @@ var AutoBenchmarkScheduler = class {
|
|
|
79156
79308
|
}
|
|
79157
79309
|
async run(trigger) {
|
|
79158
79310
|
const nowMs = this.deps.now?.() ?? Date.now();
|
|
79311
|
+
await this.restore();
|
|
79159
79312
|
const plan = planAutoBenchmark({
|
|
79160
79313
|
nodes: await this.deps.readNodes(),
|
|
79161
|
-
benchmarked: this.deps.benchmarked(),
|
|
79314
|
+
benchmarked: await this.deps.benchmarked(),
|
|
79162
79315
|
runActive: this.deps.runActive(),
|
|
79163
79316
|
attempts: this.attempts,
|
|
79164
79317
|
nowMs
|
|
79165
79318
|
});
|
|
79166
|
-
|
|
79319
|
+
let forgot = false;
|
|
79320
|
+
for (const decision of plan.decisions) {
|
|
79321
|
+
if (decision.kind !== "start") this.note(decision);
|
|
79322
|
+
if (decision.kind === "skip" && decision.reason === "has-results") forgot = this.attempts.delete(benchKey(decision.nodeId, decision.deviceKey)) || forgot;
|
|
79323
|
+
}
|
|
79324
|
+
if (forgot) await this.persist();
|
|
79167
79325
|
if (plan.start === null) return;
|
|
79168
79326
|
const { nodeId, deviceKeys } = plan.start;
|
|
79169
79327
|
const models = await this.deps.pickModels(nodeId, deviceKeys);
|
|
@@ -79208,6 +79366,7 @@ var AutoBenchmarkScheduler = class {
|
|
|
79208
79366
|
lastAtMs: nowMs
|
|
79209
79367
|
});
|
|
79210
79368
|
this.lastLogged.delete(key);
|
|
79369
|
+
await this.persist();
|
|
79211
79370
|
this.deps.logger.info("auto benchmark: started", {
|
|
79212
79371
|
tags: { nodeId },
|
|
79213
79372
|
meta: {
|
|
@@ -79221,6 +79380,29 @@ var AutoBenchmarkScheduler = class {
|
|
|
79221
79380
|
}
|
|
79222
79381
|
};
|
|
79223
79382
|
//#endregion
|
|
79383
|
+
//#region src/node-benchmark/auto-benchmark-attempts.ts
|
|
79384
|
+
function attemptsToBlob(attempts) {
|
|
79385
|
+
const out = {};
|
|
79386
|
+
for (const [key, attempt] of attempts) {
|
|
79387
|
+
const sep = key.indexOf("\0");
|
|
79388
|
+
if (sep < 0) continue;
|
|
79389
|
+
const nodeId = key.slice(0, sep);
|
|
79390
|
+
out[nodeId] = {
|
|
79391
|
+
...out[nodeId],
|
|
79392
|
+
[key.slice(sep + 1)]: attempt
|
|
79393
|
+
};
|
|
79394
|
+
}
|
|
79395
|
+
return out;
|
|
79396
|
+
}
|
|
79397
|
+
function attemptsFromBlob(blob, keyOf) {
|
|
79398
|
+
const out = /* @__PURE__ */ new Map();
|
|
79399
|
+
for (const [nodeId, devices] of Object.entries(blob)) for (const [deviceKey, attempt] of Object.entries(devices)) out.set(keyOf(nodeId, deviceKey), {
|
|
79400
|
+
count: attempt.count,
|
|
79401
|
+
lastAtMs: attempt.lastAtMs
|
|
79402
|
+
});
|
|
79403
|
+
return out;
|
|
79404
|
+
}
|
|
79405
|
+
//#endregion
|
|
79224
79406
|
//#region src/node-benchmark/node-benchmark-service.ts
|
|
79225
79407
|
/**
|
|
79226
79408
|
* The node benchmark lifecycle (D791) — the orchestrator owns it because it
|
|
@@ -89941,7 +90123,7 @@ var NodeStressController = class NodeStressController {
|
|
|
89941
90123
|
}
|
|
89942
90124
|
/** The configured mode, defaulting to {@link DEFAULT_NODE_STRESS_MODE}. */
|
|
89943
90125
|
mode() {
|
|
89944
|
-
return parseNodeStressMode((this.deps.readGlobalSettings() ?? {}).nodeStressMode) ?? "
|
|
90126
|
+
return parseNodeStressMode((this.deps.readGlobalSettings() ?? {}).nodeStressMode) ?? "failover";
|
|
89945
90127
|
}
|
|
89946
90128
|
/**
|
|
89947
90129
|
* Record one camera metrics snapshot. Shares the `PipelineCameraMetricsSnapshot`
|
|
@@ -91595,6 +91777,10 @@ async function redispatchAffected(deps, cleared) {
|
|
|
91595
91777
|
* schema is kept faithful to the on-disk shape rather than overly strict.
|
|
91596
91778
|
*/
|
|
91597
91779
|
var NodeBindingsSchema = record(string(), record(string(), string()));
|
|
91780
|
+
var AutoBenchmarkAttemptsSchema = record(string(), record(string(), object({
|
|
91781
|
+
count: number().int().min(0),
|
|
91782
|
+
lastAtMs: number()
|
|
91783
|
+
})));
|
|
91598
91784
|
var StoredAgentAddonConfigSchema = object({
|
|
91599
91785
|
enabled: boolean(),
|
|
91600
91786
|
modelId: string().optional(),
|
|
@@ -97097,7 +97283,7 @@ function buildGlobalSettingsSections(options) {
|
|
|
97097
97283
|
key: "nodeStressMode",
|
|
97098
97284
|
type: "select",
|
|
97099
97285
|
label: "Node stress handling",
|
|
97100
|
-
description: "
|
|
97286
|
+
description: "When a node is saturated (queue pressure, dropped frames and delivered-vs-configured fps of active cameras), Failover moves one of its cameras to an idle node. One camera at a time, a cooldown per camera, never back to the node it just left, at most 4 moves an hour across the cluster. A camera is never paused. Observe only logs the verdict; Off does nothing.",
|
|
97101
97287
|
options: [
|
|
97102
97288
|
{
|
|
97103
97289
|
value: "off",
|
|
@@ -97112,7 +97298,7 @@ function buildGlobalSettingsSections(options) {
|
|
|
97112
97298
|
label: "Failover (relocate a camera)"
|
|
97113
97299
|
}
|
|
97114
97300
|
],
|
|
97115
|
-
default: "
|
|
97301
|
+
default: "failover"
|
|
97116
97302
|
},
|
|
97117
97303
|
{
|
|
97118
97304
|
key: "fpsLowWindowMs",
|
|
@@ -98050,6 +98236,7 @@ var AutoTuneService = class {
|
|
|
98050
98236
|
} catch (err) {
|
|
98051
98237
|
this.deps.logger.warn("load balancing: benchmark results unreadable — sized from hardware", { meta: { error: errMsg(err) } });
|
|
98052
98238
|
}
|
|
98239
|
+
this.noteForeignCells(rec.recommendations, measured);
|
|
98053
98240
|
return computeCapacityShares({
|
|
98054
98241
|
recommendations: rec.recommendations,
|
|
98055
98242
|
source,
|
|
@@ -98059,6 +98246,28 @@ var AutoTuneService = class {
|
|
|
98059
98246
|
minSessionsPerDevice
|
|
98060
98247
|
});
|
|
98061
98248
|
}
|
|
98249
|
+
/** Foreign-model cells already reported, so a recompute does not repeat the line. */
|
|
98250
|
+
reportedForeign = /* @__PURE__ */ new Set();
|
|
98251
|
+
/**
|
|
98252
|
+
* A measured cell for a model the device does not run now is ignored by the
|
|
98253
|
+
* shares (D913 follow-up); say so ONCE per (node, device, models), with the
|
|
98254
|
+
* device key — silence would read as "measured and used".
|
|
98255
|
+
*/
|
|
98256
|
+
noteForeignCells(recommendations, measured) {
|
|
98257
|
+
for (const cell of foreignCapacityCells(recommendations, measured)) {
|
|
98258
|
+
const key = `${cell.nodeId}|${cell.deviceKey}|${cell.measuredModelId}|${cell.runsModelId ?? ""}`;
|
|
98259
|
+
if (this.reportedForeign.has(key)) continue;
|
|
98260
|
+
this.reportedForeign.add(key);
|
|
98261
|
+
this.deps.logger.info("load balancing: benchmark cell for another model ignored", {
|
|
98262
|
+
tags: { nodeId: cell.nodeId },
|
|
98263
|
+
meta: {
|
|
98264
|
+
deviceKey: cell.deviceKey,
|
|
98265
|
+
measuredModelId: cell.measuredModelId,
|
|
98266
|
+
runsModelId: cell.runsModelId
|
|
98267
|
+
}
|
|
98268
|
+
});
|
|
98269
|
+
}
|
|
98270
|
+
}
|
|
98062
98271
|
/**
|
|
98063
98272
|
* Gather every node and recommend. Nothing but hardware, catalog, the sizing
|
|
98064
98273
|
* and — for a plan — the custom pins goes in. `customPins` maps
|
|
@@ -100570,6 +100779,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
100570
100779
|
*/
|
|
100571
100780
|
startAutoBenchmark(store) {
|
|
100572
100781
|
this.autoBenchmark?.stop();
|
|
100782
|
+
const attempts = this.state("autoBenchmarkAttempts", AutoBenchmarkAttemptsSchema, {});
|
|
100573
100783
|
this.autoBenchmark = new AutoBenchmarkScheduler({
|
|
100574
100784
|
logger: this.ctx.logger,
|
|
100575
100785
|
readNodes: async () => {
|
|
@@ -100583,7 +100793,19 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
100583
100793
|
devices: live[nodeId] ?? []
|
|
100584
100794
|
})));
|
|
100585
100795
|
},
|
|
100586
|
-
benchmarked:
|
|
100796
|
+
benchmarked: async () => {
|
|
100797
|
+
if (store.listResults().length === 0) return /* @__PURE__ */ new Set();
|
|
100798
|
+
const hardware = await this.autoTune().suggestShares({
|
|
100799
|
+
headroom: AUTO_SHARE_HEADROOM,
|
|
100800
|
+
source: "hardware"
|
|
100801
|
+
});
|
|
100802
|
+
const measured = await this.readMeasuredCapacity(hardware.nodes.map((n) => n.nodeId));
|
|
100803
|
+
return benchmarkedDevices(hardware.nodes.flatMap((n) => n.devices.map((d) => ({
|
|
100804
|
+
nodeId: n.nodeId,
|
|
100805
|
+
deviceKey: d.deviceKey,
|
|
100806
|
+
modelId: d.modelId
|
|
100807
|
+
}))), measured);
|
|
100808
|
+
},
|
|
100587
100809
|
runActive: () => this.benchmarkService().isRunActive(),
|
|
100588
100810
|
pickModels: async (nodeId, deviceKeys) => {
|
|
100589
100811
|
const candidates = await this.getNodeBenchmarkCandidates({ nodeId });
|
|
@@ -100594,7 +100816,11 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
100594
100816
|
}
|
|
100595
100817
|
return out;
|
|
100596
100818
|
},
|
|
100597
|
-
start: (plan) => this.benchmarkService().start(plan)
|
|
100819
|
+
start: (plan) => this.benchmarkService().start(plan),
|
|
100820
|
+
persistence: {
|
|
100821
|
+
load: async () => attemptsFromBlob(await attempts.get(), benchKey),
|
|
100822
|
+
save: (map) => attempts.set(attemptsToBlob(map))
|
|
100823
|
+
}
|
|
100598
100824
|
});
|
|
100599
100825
|
this.autoBenchmark.start();
|
|
100600
100826
|
this.autoBenchmark.tick("boot");
|