@camstack/addon-pipeline-orchestrator 1.2.292 → 1.2.293
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 +21 -21
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets--PHyrJOr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CNnJE0gg.mjs} +3 -3
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CXr4GmWP.mjs +26 -0
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Cu7Q-ifj.mjs +26 -0
- package/dist/{hostInit-D7n994Lm.mjs → hostInit-S3TibCek.mjs} +3 -3
- package/dist/index.js +636 -627
- package/dist/index.mjs +636 -627
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-tJspAxws.mjs +0 -26
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CcezrkWV.mjs +0 -26
package/dist/index.js
CHANGED
|
@@ -5410,7 +5410,7 @@ var ZodIssueCode = {
|
|
|
5410
5410
|
var ZodFirstPartyTypeKind;
|
|
5411
5411
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5412
5412
|
//#endregion
|
|
5413
|
-
//#region ../types/dist/sleep-
|
|
5413
|
+
//#region ../types/dist/sleep-DAJ9Er57.mjs
|
|
5414
5414
|
/**
|
|
5415
5415
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5416
5416
|
* window to float samples (D455).
|
|
@@ -25033,170 +25033,17 @@ var pipelineExecutorCapability = {
|
|
|
25033
25033
|
}
|
|
25034
25034
|
};
|
|
25035
25035
|
/**
|
|
25036
|
-
* Wire shapes of the Load balancing surface (D748): the session placement
|
|
25037
|
-
* policy, the atomic share write, the observed placement statistics, the
|
|
25038
|
-
* hardware share suggestion and the stale device pins.
|
|
25039
|
-
*/
|
|
25040
|
-
/**
|
|
25041
|
-
* How a new detection session picks its node and device.
|
|
25042
|
-
* - `least-loaded` — the historical rule: lowest `attached / weight`, a
|
|
25043
|
-
* constant node order at a tie (D37). An idle node always ties at 0, so a
|
|
25044
|
-
* weight only bites once a node already holds a session.
|
|
25045
|
-
* - `share` — the share of PARALLEL sessions is held to the weights, and a
|
|
25046
|
-
* full or drowning node yields to a free one (D748).
|
|
25047
|
-
*/
|
|
25048
|
-
var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
|
|
25049
|
-
/**
|
|
25050
|
-
* `share` since D750: the percentages are held by default. `least-loaded` stays
|
|
25051
|
-
* selectable.
|
|
25052
|
-
*/
|
|
25053
|
-
var DEFAULT_SESSION_PLACEMENT_POLICY = "share";
|
|
25054
|
-
/** The global-settings key the policy is stored under (orchestrator `load` section). */
|
|
25055
|
-
var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
|
|
25056
|
-
/** Parse a stored value; anything unknown is the default. */
|
|
25057
|
-
function parseSessionPlacementPolicy(value) {
|
|
25058
|
-
const parsed = SessionPlacementPolicySchema.safeParse(value);
|
|
25059
|
-
return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
|
|
25060
|
-
}
|
|
25061
|
-
var LoadBalancingSharesInputSchema = object({
|
|
25062
|
-
/** nodeId → share (> 0). A node left out keeps its weight. */
|
|
25063
|
-
nodes: record(string(), number().positive()).default({}),
|
|
25064
|
-
/** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
|
|
25065
|
-
devices: record(string(), record(string(), number().positive())).default({})
|
|
25066
|
-
});
|
|
25067
|
-
var PlacementStatsInputSchema = object({
|
|
25068
|
-
/** Window the observed shares are averaged over (default 1 h, max 24 h). */
|
|
25069
|
-
windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
|
|
25070
|
-
var PlacementDeviceStatsSchema = object({
|
|
25071
|
-
deviceKey: string(),
|
|
25072
|
-
/** Sessions running on this device now. */
|
|
25073
|
-
sessionsNow: number(),
|
|
25074
|
-
/** Sessions started on this device inside the window. */
|
|
25075
|
-
sessionsStarted: number(),
|
|
25076
|
-
/** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
|
|
25077
|
-
observedShare: number().nullable()
|
|
25078
|
-
});
|
|
25079
|
-
var PlacementNodeStatsSchema = object({
|
|
25080
|
-
nodeId: string(),
|
|
25081
|
-
sessionsNow: number(),
|
|
25082
|
-
sessionsStarted: number(),
|
|
25083
|
-
/** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
|
|
25084
|
-
observedShare: number().nullable(),
|
|
25085
|
-
/** Most sessions this node ran at once inside the window. */
|
|
25086
|
-
peakSessions: number(),
|
|
25087
|
-
/** Sessions a share election handed elsewhere because this node was full or drowning. */
|
|
25088
|
-
yieldsAway: number(),
|
|
25089
|
-
devices: array(PlacementDeviceStatsSchema).readonly()
|
|
25090
|
-
});
|
|
25091
|
-
/** A camera's device pin that the session path cannot honour. */
|
|
25092
|
-
var StaleDevicePinSchema = object({
|
|
25093
|
-
deviceId: number(),
|
|
25094
|
-
deviceKey: string(),
|
|
25095
|
-
/** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
|
|
25096
|
-
reason: _enum(["disabled", "absent"])
|
|
25097
|
-
});
|
|
25098
|
-
var PlacementStatsSchema = object({
|
|
25099
|
-
policy: SessionPlacementPolicySchema,
|
|
25100
|
-
windowMs: number(),
|
|
25101
|
-
/** How much of the window was actually observed (the hub keeps it in memory). */
|
|
25102
|
-
observedForMs: number(),
|
|
25103
|
-
sessionsNow: number(),
|
|
25104
|
-
/** Most parallel sessions across the cluster inside the window. */
|
|
25105
|
-
clusterPeakSessions: number(),
|
|
25106
|
-
nodes: array(PlacementNodeStatsSchema).readonly(),
|
|
25107
|
-
stalePins: array(StaleDevicePinSchema).readonly()
|
|
25108
|
-
});
|
|
25109
|
-
var SuggestSharesInputSchema = object({ headroom: number().min(.3).max(.5).default(.4) });
|
|
25110
|
-
var SuggestedDeviceShareSchema = object({
|
|
25111
|
-
deviceKey: string(),
|
|
25112
|
-
/** Share of the node (0–100, rounded). */
|
|
25113
|
-
share: number(),
|
|
25114
|
-
modelId: string().nullable(),
|
|
25115
|
-
/** Inferences per second at that model, from the measured table. */
|
|
25116
|
-
fps: number().nullable(),
|
|
25117
|
-
measured: boolean(),
|
|
25118
|
-
/** Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋. Never enforced. */
|
|
25119
|
-
suggestedMaxSessions: number().nullable()
|
|
25120
|
-
});
|
|
25121
|
-
var SuggestedNodeShareSchema = object({
|
|
25122
|
-
nodeId: string(),
|
|
25123
|
-
/** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
|
|
25124
|
-
share: number().nullable(),
|
|
25125
|
-
/** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
|
|
25126
|
-
capacitySessions: number(),
|
|
25127
|
-
devices: array(SuggestedDeviceShareSchema).readonly(),
|
|
25128
|
-
skippedReason: string().nullable()
|
|
25129
|
-
});
|
|
25130
|
-
var SuggestSharesResultSchema = object({
|
|
25131
|
-
headroom: number(),
|
|
25132
|
-
/** Camera detection rate the capacity is counted in (the fastest camera's fps). */
|
|
25133
|
-
cameraFps: number(),
|
|
25134
|
-
/** How many share → model → capacity rounds ran before the shares stopped moving (≤ 3). */
|
|
25135
|
-
iterations: number(),
|
|
25136
|
-
nodes: array(SuggestedNodeShareSchema).readonly()
|
|
25137
|
-
});
|
|
25138
|
-
/**
|
|
25139
|
-
* Where the node and device shares come from (D750).
|
|
25140
|
-
* - `auto` — computed by the system from the cluster's measured capacity at
|
|
25141
|
-
* the Auto tune models, and recomputed whenever a node or device joins,
|
|
25142
|
-
* leaves, is switched on or off, or changes model.
|
|
25143
|
-
* - `manual` — the operator's stored sliders. A node or device that cannot
|
|
25144
|
-
* take sessions gives its share to the others in proportion; the stored
|
|
25145
|
-
* values are never rewritten for that.
|
|
25146
|
-
*/
|
|
25147
|
-
var ShareSourceSchema = _enum(["auto", "manual"]);
|
|
25148
|
-
var DEFAULT_SHARE_SOURCE = "auto";
|
|
25149
|
-
/** Headroom the automatic shares are computed at (Auto tune's default). */
|
|
25150
|
-
var AUTO_SHARE_HEADROOM = .4;
|
|
25151
|
-
/** The global-settings key the share source is stored under (orchestrator `load` section). */
|
|
25152
|
-
var SHARE_SOURCE_SETTING_KEY = "shareSource";
|
|
25153
|
-
/** Parse a stored value; anything unknown is the default. */
|
|
25154
|
-
function parseShareSource(value) {
|
|
25155
|
-
const parsed = ShareSourceSchema.safeParse(value);
|
|
25156
|
-
return parsed.success ? parsed.data : DEFAULT_SHARE_SOURCE;
|
|
25157
|
-
}
|
|
25158
|
-
var SetShareSourceInputSchema = object({ source: ShareSourceSchema });
|
|
25159
|
-
object({
|
|
25160
|
-
nodes: record(string(), number()),
|
|
25161
|
-
devices: record(string(), record(string(), number()))
|
|
25162
|
-
});
|
|
25163
|
-
var EffectiveDeviceShareSchema = object({
|
|
25164
|
-
deviceKey: string(),
|
|
25165
|
-
/** Can take sessions now (enabled, available, can run the root). */
|
|
25166
|
-
eligible: boolean(),
|
|
25167
|
-
/** The share the source assigns, as a percentage of the node's devices. */
|
|
25168
|
-
configuredPct: number(),
|
|
25169
|
-
/** The share actually used, after an ineligible device's share went to the others. */
|
|
25170
|
-
effectivePct: number()
|
|
25171
|
-
});
|
|
25172
|
-
var EffectiveNodeShareSchema = object({
|
|
25173
|
-
nodeId: string(),
|
|
25174
|
-
eligible: boolean(),
|
|
25175
|
-
configuredPct: number(),
|
|
25176
|
-
effectivePct: number(),
|
|
25177
|
-
devices: array(EffectiveDeviceShareSchema).readonly()
|
|
25178
|
-
});
|
|
25179
|
-
var EffectiveSharesSchema = object({
|
|
25180
|
-
source: ShareSourceSchema,
|
|
25181
|
-
policy: SessionPlacementPolicySchema,
|
|
25182
|
-
/** `auto` with no computed answer yet: the stored sliders are used meanwhile. */
|
|
25183
|
-
autoPending: boolean(),
|
|
25184
|
-
/** When the automatic shares were last computed (ms epoch); null = never. */
|
|
25185
|
-
computedAt: number().nullable(),
|
|
25186
|
-
/** What made the system recompute them last. */
|
|
25187
|
-
trigger: string().nullable(),
|
|
25188
|
-
nodes: array(EffectiveNodeShareSchema).readonly(),
|
|
25189
|
-
/** The relative weights the election reads (before eligibility). */
|
|
25190
|
-
nodeWeights: record(string(), number()),
|
|
25191
|
-
deviceWeights: record(string(), record(string(), number()))
|
|
25192
|
-
});
|
|
25193
|
-
/**
|
|
25194
25036
|
* The wire shapes of the Auto tune plan and apply (D745), used by the
|
|
25195
25037
|
* `pipeline-orchestrator` cap. The recommender's own types
|
|
25196
|
-
* (`recommend.ts
|
|
25038
|
+
* (`recommend.ts`) are the TS face of the same data; these
|
|
25197
25039
|
* schemas are what crosses the cap boundary.
|
|
25198
25040
|
*/
|
|
25199
25041
|
var ModelFormatSchema$1 = _enum(MODEL_FORMATS);
|
|
25042
|
+
/** The operator's range for it. */
|
|
25043
|
+
var MIN_SESSIONS_PER_DEVICE_RANGE = {
|
|
25044
|
+
min: 1,
|
|
25045
|
+
max: 8
|
|
25046
|
+
};
|
|
25200
25047
|
var HardwareClassIdSchema = _enum([
|
|
25201
25048
|
"intel-core-ultra",
|
|
25202
25049
|
"intel-n-series",
|
|
@@ -25214,8 +25061,9 @@ var AcceleratorKindSchema = _enum([
|
|
|
25214
25061
|
]);
|
|
25215
25062
|
var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
25216
25063
|
object({
|
|
25217
|
-
kind: literal("
|
|
25218
|
-
|
|
25064
|
+
kind: literal("capacity"),
|
|
25065
|
+
sessions: number(),
|
|
25066
|
+
minSessions: number()
|
|
25219
25067
|
}),
|
|
25220
25068
|
object({
|
|
25221
25069
|
kind: literal("tie-faster"),
|
|
@@ -25230,16 +25078,13 @@ var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
|
25230
25078
|
}),
|
|
25231
25079
|
object({
|
|
25232
25080
|
kind: literal("no-fit-lightest"),
|
|
25233
|
-
|
|
25081
|
+
sessions: number(),
|
|
25082
|
+
minSessions: number()
|
|
25234
25083
|
}),
|
|
25235
25084
|
object({
|
|
25236
25085
|
kind: literal("derived-latency"),
|
|
25237
25086
|
modelId: string()
|
|
25238
25087
|
}),
|
|
25239
|
-
object({
|
|
25240
|
-
kind: literal("estimated-load"),
|
|
25241
|
-
observedForMs: number().nullable()
|
|
25242
|
-
}),
|
|
25243
25088
|
object({ kind: literal("cpu-device") }),
|
|
25244
25089
|
object({ kind: literal("coral-untouched") }),
|
|
25245
25090
|
object({ kind: literal("device-disabled") }),
|
|
@@ -25255,28 +25100,13 @@ var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
|
25255
25100
|
format: ModelFormatSchema$1
|
|
25256
25101
|
})
|
|
25257
25102
|
]);
|
|
25258
|
-
var DeviceDemandSchema = object({
|
|
25259
|
-
nodeId: string(),
|
|
25260
|
-
deviceKey: string(),
|
|
25261
|
-
demandFps: number(),
|
|
25262
|
-
maxCameraFps: number(),
|
|
25263
|
-
alwaysOnCameras: number(),
|
|
25264
|
-
onMotionSessions: number(),
|
|
25265
|
-
onMotionSource: _enum([
|
|
25266
|
-
"observed-24h",
|
|
25267
|
-
"estimate",
|
|
25268
|
-
"none"
|
|
25269
|
-
]),
|
|
25270
|
-
observedForMs: number().nullable(),
|
|
25271
|
-
/** How on-motion sessions were charged: per-device observation, or split by share (D748). */
|
|
25272
|
-
placement: SessionPlacementPolicySchema
|
|
25273
|
-
});
|
|
25274
25103
|
var DeviceModelChoiceSchema = object({
|
|
25275
25104
|
modelId: string(),
|
|
25276
25105
|
p50Ms: number(),
|
|
25277
25106
|
fps: number(),
|
|
25278
25107
|
measured: boolean(),
|
|
25279
|
-
|
|
25108
|
+
/** Parallel sessions at the camera rate with the headroom kept free: ⌊fps × (1 − headroom) / camera fps⌋. */
|
|
25109
|
+
capacitySessions: number(),
|
|
25280
25110
|
personRecall: number()
|
|
25281
25111
|
});
|
|
25282
25112
|
var DeviceRecommendationSchema = object({
|
|
@@ -25284,7 +25114,6 @@ var DeviceRecommendationSchema = object({
|
|
|
25284
25114
|
accelerator: AcceleratorKindSchema.nullable(),
|
|
25285
25115
|
enabled: boolean(),
|
|
25286
25116
|
available: boolean(),
|
|
25287
|
-
demand: DeviceDemandSchema.nullable(),
|
|
25288
25117
|
chosen: DeviceModelChoiceSchema.nullable(),
|
|
25289
25118
|
reasons: array(RecommendationReasonSchema).readonly()
|
|
25290
25119
|
});
|
|
@@ -25345,20 +25174,20 @@ var AutoTuneOptionsSchema = object({
|
|
|
25345
25174
|
/** Share of each accelerator's throughput kept free (0.3–0.5). */
|
|
25346
25175
|
headroom: number().min(.3).max(.5).default(.4),
|
|
25347
25176
|
/**
|
|
25348
|
-
*
|
|
25349
|
-
*
|
|
25350
|
-
*
|
|
25351
|
-
* is stored.
|
|
25177
|
+
* The least number of parallel sessions every device must sustain at the
|
|
25178
|
+
* camera rate with the headroom kept free (D755). The most accurate model
|
|
25179
|
+
* that reaches it wins.
|
|
25352
25180
|
*/
|
|
25353
|
-
|
|
25181
|
+
minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
|
|
25354
25182
|
});
|
|
25355
25183
|
var AutoTunePlanSchema = object({
|
|
25356
25184
|
/** Hash of everything the apply will write — the apply refuses a different one. */
|
|
25357
25185
|
planHash: string(),
|
|
25358
25186
|
family: literal("yolov9"),
|
|
25359
25187
|
headroom: number(),
|
|
25360
|
-
|
|
25361
|
-
|
|
25188
|
+
minSessionsPerDevice: number(),
|
|
25189
|
+
/** The detection rate a session is counted at (the fastest camera running the pipeline root). */
|
|
25190
|
+
cameraFps: number(),
|
|
25362
25191
|
generatedAt: number(),
|
|
25363
25192
|
nodes: array(NodeRecommendationSchema).readonly(),
|
|
25364
25193
|
nodeChanges: array(NodeDeviceChangeSchema).readonly(),
|
|
@@ -25368,7 +25197,8 @@ var AutoTunePlanSchema = object({
|
|
|
25368
25197
|
});
|
|
25369
25198
|
var AutoTuneApplyInputSchema = object({
|
|
25370
25199
|
planHash: string(),
|
|
25371
|
-
headroom: number().min(.3).max(.5).default(.4)
|
|
25200
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
25201
|
+
minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
|
|
25372
25202
|
});
|
|
25373
25203
|
var AutoTunePhaseResultSchema = object({
|
|
25374
25204
|
phase: _enum([
|
|
@@ -25400,6 +25230,206 @@ var AutoTuneApplyResultSchema = discriminatedUnion("kind", [
|
|
|
25400
25230
|
reason: _enum(["apply-in-progress"])
|
|
25401
25231
|
})
|
|
25402
25232
|
]);
|
|
25233
|
+
/**
|
|
25234
|
+
* Wire shapes of the Load balancing surface (D748): the session placement
|
|
25235
|
+
* policy, the atomic share write, the observed placement statistics, the
|
|
25236
|
+
* hardware share suggestion and the stale device pins.
|
|
25237
|
+
*/
|
|
25238
|
+
/**
|
|
25239
|
+
* How a new detection session picks its node and device.
|
|
25240
|
+
* - `least-loaded` — the historical rule: lowest `attached / weight`, a
|
|
25241
|
+
* constant node order at a tie (D37). An idle node always ties at 0, so a
|
|
25242
|
+
* weight only bites once a node already holds a session.
|
|
25243
|
+
* - `share` — the share of PARALLEL sessions is held to the weights, and a
|
|
25244
|
+
* full or drowning node yields to a free one (D748).
|
|
25245
|
+
*/
|
|
25246
|
+
var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
|
|
25247
|
+
/**
|
|
25248
|
+
* `share` since D750: the percentages are held by default. `least-loaded` stays
|
|
25249
|
+
* selectable.
|
|
25250
|
+
*/
|
|
25251
|
+
var DEFAULT_SESSION_PLACEMENT_POLICY = "share";
|
|
25252
|
+
/** The global-settings key the policy is stored under (orchestrator `load` section). */
|
|
25253
|
+
var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
|
|
25254
|
+
/** Parse a stored value; anything unknown is the default. */
|
|
25255
|
+
function parseSessionPlacementPolicy(value) {
|
|
25256
|
+
const parsed = SessionPlacementPolicySchema.safeParse(value);
|
|
25257
|
+
return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
|
|
25258
|
+
}
|
|
25259
|
+
var LoadBalancingSharesInputSchema = object({
|
|
25260
|
+
/** nodeId → share (> 0). A node left out keeps its weight. */
|
|
25261
|
+
nodes: record(string(), number().positive()).default({}),
|
|
25262
|
+
/** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
|
|
25263
|
+
devices: record(string(), record(string(), number().positive())).default({})
|
|
25264
|
+
});
|
|
25265
|
+
var PlacementStatsInputSchema = object({
|
|
25266
|
+
/** Window the observed shares are averaged over (default 1 h, max 24 h). */
|
|
25267
|
+
windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
|
|
25268
|
+
var PlacementDeviceStatsSchema = object({
|
|
25269
|
+
deviceKey: string(),
|
|
25270
|
+
/** Sessions running on this device now. */
|
|
25271
|
+
sessionsNow: number(),
|
|
25272
|
+
/** Sessions started on this device inside the window. */
|
|
25273
|
+
sessionsStarted: number(),
|
|
25274
|
+
/** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
|
|
25275
|
+
observedShare: number().nullable()
|
|
25276
|
+
});
|
|
25277
|
+
var PlacementNodeStatsSchema = object({
|
|
25278
|
+
nodeId: string(),
|
|
25279
|
+
sessionsNow: number(),
|
|
25280
|
+
sessionsStarted: number(),
|
|
25281
|
+
/** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
|
|
25282
|
+
observedShare: number().nullable(),
|
|
25283
|
+
/** Most sessions this node ran at once inside the window. */
|
|
25284
|
+
peakSessions: number(),
|
|
25285
|
+
/** Sessions a share election handed elsewhere because this node was full or drowning. */
|
|
25286
|
+
yieldsAway: number(),
|
|
25287
|
+
devices: array(PlacementDeviceStatsSchema).readonly()
|
|
25288
|
+
});
|
|
25289
|
+
/** A camera's device pin that the session path cannot honour. */
|
|
25290
|
+
var StaleDevicePinSchema = object({
|
|
25291
|
+
deviceId: number(),
|
|
25292
|
+
deviceKey: string(),
|
|
25293
|
+
/** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
|
|
25294
|
+
reason: _enum(["disabled", "absent"])
|
|
25295
|
+
});
|
|
25296
|
+
var PlacementStatsSchema = object({
|
|
25297
|
+
policy: SessionPlacementPolicySchema,
|
|
25298
|
+
windowMs: number(),
|
|
25299
|
+
/** How much of the window was actually observed (the hub keeps it in memory). */
|
|
25300
|
+
observedForMs: number(),
|
|
25301
|
+
sessionsNow: number(),
|
|
25302
|
+
/** Most parallel sessions across the cluster inside the window. */
|
|
25303
|
+
clusterPeakSessions: number(),
|
|
25304
|
+
nodes: array(PlacementNodeStatsSchema).readonly(),
|
|
25305
|
+
stalePins: array(StaleDevicePinSchema).readonly()
|
|
25306
|
+
});
|
|
25307
|
+
/**
|
|
25308
|
+
* Where an automatic share's capacity comes from (D755).
|
|
25309
|
+
* - `hardware` — the static capacity of each device at its Auto tune model:
|
|
25310
|
+
* the measured latency table for the node's hardware class.
|
|
25311
|
+
* - `benchmark` — the node's own measured capacity (node benchmark results).
|
|
25312
|
+
* A node with no results falls back to `hardware`, and says so.
|
|
25313
|
+
*/
|
|
25314
|
+
var CapacitySourceSchema = _enum(["hardware", "benchmark"]);
|
|
25315
|
+
/** Why a node under the `benchmark` source was sized from hardware instead. */
|
|
25316
|
+
var CapacityFallbackSchema = _enum(["no-benchmark-results", "partial-benchmark-results"]);
|
|
25317
|
+
var SuggestSharesInputSchema = object({
|
|
25318
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
25319
|
+
/** Default `hardware`. */
|
|
25320
|
+
source: CapacitySourceSchema.default("hardware")
|
|
25321
|
+
});
|
|
25322
|
+
var SuggestedDeviceShareSchema = object({
|
|
25323
|
+
deviceKey: string(),
|
|
25324
|
+
/** Share of the node (0–100, rounded). */
|
|
25325
|
+
share: number(),
|
|
25326
|
+
modelId: string().nullable(),
|
|
25327
|
+
/** Inferences per second at that model, from the measured table; null when sized by a benchmark. */
|
|
25328
|
+
fps: number().nullable(),
|
|
25329
|
+
/** False for a table cell derived from another chip; true for a measured cell or a benchmark. */
|
|
25330
|
+
measured: boolean(),
|
|
25331
|
+
/**
|
|
25332
|
+
* Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋ from the table,
|
|
25333
|
+
* or the benchmark's measured max sessions. Never enforced.
|
|
25334
|
+
*/
|
|
25335
|
+
suggestedMaxSessions: number().nullable()
|
|
25336
|
+
});
|
|
25337
|
+
var SuggestedNodeShareSchema = object({
|
|
25338
|
+
nodeId: string(),
|
|
25339
|
+
/** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
|
|
25340
|
+
share: number().nullable(),
|
|
25341
|
+
/** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
|
|
25342
|
+
capacitySessions: number(),
|
|
25343
|
+
devices: array(SuggestedDeviceShareSchema).readonly(),
|
|
25344
|
+
skippedReason: string().nullable(),
|
|
25345
|
+
/** What this node's capacity was read from; null when the node is skipped. */
|
|
25346
|
+
capacitySource: CapacitySourceSchema.nullable(),
|
|
25347
|
+
/** Set when the `benchmark` source was asked for and this node used `hardware`. */
|
|
25348
|
+
capacityFallback: CapacityFallbackSchema.nullable()
|
|
25349
|
+
});
|
|
25350
|
+
var SuggestSharesResultSchema = object({
|
|
25351
|
+
headroom: number(),
|
|
25352
|
+
/** The source asked for; each node says what it actually used. */
|
|
25353
|
+
source: CapacitySourceSchema,
|
|
25354
|
+
/** Camera detection rate the capacity is counted in (the fastest camera's fps). */
|
|
25355
|
+
cameraFps: number(),
|
|
25356
|
+
/** The Auto tune capacity floor the models were picked at. */
|
|
25357
|
+
minSessionsPerDevice: number(),
|
|
25358
|
+
nodes: array(SuggestedNodeShareSchema).readonly()
|
|
25359
|
+
});
|
|
25360
|
+
/**
|
|
25361
|
+
* Where the node and device shares come from (D750, D755).
|
|
25362
|
+
* - `hardware` (default) — computed by the system from each node's static
|
|
25363
|
+
* capacity (its accelerators at their Auto tune models, from the measured
|
|
25364
|
+
* latency table), recomputed whenever a node or device joins, leaves, is
|
|
25365
|
+
* switched on or off, or changes model. No load input.
|
|
25366
|
+
* - `benchmark` — the same, from each node's measured capacity (node
|
|
25367
|
+
* benchmark results); a node without results falls back to `hardware`.
|
|
25368
|
+
* - `manual` — the operator's stored sliders.
|
|
25369
|
+
* Either way a node or device that cannot take sessions gives its share to
|
|
25370
|
+
* the others in proportion; the stored values are never rewritten for that.
|
|
25371
|
+
*/
|
|
25372
|
+
var ShareSourceSchema = _enum([
|
|
25373
|
+
"hardware",
|
|
25374
|
+
"benchmark",
|
|
25375
|
+
"manual"
|
|
25376
|
+
]);
|
|
25377
|
+
var DEFAULT_SHARE_SOURCE = "hardware";
|
|
25378
|
+
/** `hardware` or `benchmark`: the system computes the shares. */
|
|
25379
|
+
function isAutomaticShareSource(source) {
|
|
25380
|
+
return source !== "manual";
|
|
25381
|
+
}
|
|
25382
|
+
/** Headroom the automatic shares are computed at (Auto tune's default). */
|
|
25383
|
+
var AUTO_SHARE_HEADROOM = .4;
|
|
25384
|
+
/** The global-settings key the share source is stored under (orchestrator `load` section). */
|
|
25385
|
+
var SHARE_SOURCE_SETTING_KEY = "shareSource";
|
|
25386
|
+
/**
|
|
25387
|
+
* Parse a stored value; anything unknown is the default. `auto` (D750, before
|
|
25388
|
+
* D755 split it) reads as `hardware` — that is what it computed.
|
|
25389
|
+
*/
|
|
25390
|
+
function parseShareSource(value) {
|
|
25391
|
+
const parsed = ShareSourceSchema.safeParse(value);
|
|
25392
|
+
return parsed.success ? parsed.data : DEFAULT_SHARE_SOURCE;
|
|
25393
|
+
}
|
|
25394
|
+
var SetShareSourceInputSchema = object({ source: ShareSourceSchema });
|
|
25395
|
+
object({
|
|
25396
|
+
nodes: record(string(), number()),
|
|
25397
|
+
devices: record(string(), record(string(), number()))
|
|
25398
|
+
});
|
|
25399
|
+
var EffectiveDeviceShareSchema = object({
|
|
25400
|
+
deviceKey: string(),
|
|
25401
|
+
/** Can take sessions now (enabled, available, can run the root). */
|
|
25402
|
+
eligible: boolean(),
|
|
25403
|
+
/** The share the source assigns, as a percentage of the node's devices. */
|
|
25404
|
+
configuredPct: number(),
|
|
25405
|
+
/** The share actually used, after an ineligible device's share went to the others. */
|
|
25406
|
+
effectivePct: number()
|
|
25407
|
+
});
|
|
25408
|
+
var EffectiveNodeShareSchema = object({
|
|
25409
|
+
nodeId: string(),
|
|
25410
|
+
eligible: boolean(),
|
|
25411
|
+
configuredPct: number(),
|
|
25412
|
+
effectivePct: number(),
|
|
25413
|
+
/** Automatic sources: what this node's share was computed from; null under `manual` or before the first answer. */
|
|
25414
|
+
capacitySource: CapacitySourceSchema.nullable(),
|
|
25415
|
+
/** Automatic `benchmark`: why this node used `hardware` instead. */
|
|
25416
|
+
capacityFallback: CapacityFallbackSchema.nullable(),
|
|
25417
|
+
devices: array(EffectiveDeviceShareSchema).readonly()
|
|
25418
|
+
});
|
|
25419
|
+
var EffectiveSharesSchema = object({
|
|
25420
|
+
source: ShareSourceSchema,
|
|
25421
|
+
policy: SessionPlacementPolicySchema,
|
|
25422
|
+
/** An automatic source with no computed answer yet: the stored sliders are used meanwhile. */
|
|
25423
|
+
autoPending: boolean(),
|
|
25424
|
+
/** When the automatic shares were last computed (ms epoch); null = never. */
|
|
25425
|
+
computedAt: number().nullable(),
|
|
25426
|
+
/** What made the system recompute them last. */
|
|
25427
|
+
trigger: string().nullable(),
|
|
25428
|
+
nodes: array(EffectiveNodeShareSchema).readonly(),
|
|
25429
|
+
/** The relative weights the election reads (before eligibility). */
|
|
25430
|
+
nodeWeights: record(string(), number()),
|
|
25431
|
+
deviceWeights: record(string(), record(string(), number()))
|
|
25432
|
+
});
|
|
25403
25433
|
object({
|
|
25404
25434
|
activeCameras: number(),
|
|
25405
25435
|
throttledCameras: number(),
|
|
@@ -26807,8 +26837,10 @@ var CameraStepOverridePatchSchema = object({
|
|
|
26807
26837
|
modelId: string().optional(),
|
|
26808
26838
|
settings: record(string(), unknown()).readonly().optional()
|
|
26809
26839
|
});
|
|
26840
|
+
var CameraStepSettingsSchema = record(string(), record(string(), unknown()).readonly());
|
|
26810
26841
|
var CameraPipelineSettingsSchema = object({
|
|
26811
26842
|
stepToggles: record(string(), boolean()).optional(),
|
|
26843
|
+
stepSettings: CameraStepSettingsSchema.optional(),
|
|
26812
26844
|
stepOverridesByDevice: record(string(), record(string(), record(string(), CameraStepOverridePatchSchema))).optional(),
|
|
26813
26845
|
pipelineByAgent: record(string(), CameraPipelineForAgentSchema).optional()
|
|
26814
26846
|
});
|
|
@@ -27574,9 +27606,11 @@ var pipelineOrchestratorCapability = {
|
|
|
27574
27606
|
auth: "admin"
|
|
27575
27607
|
}),
|
|
27576
27608
|
/**
|
|
27577
|
-
* Auto tune (D745) — DRY RUN. Recommends ONE detector family
|
|
27578
|
-
* for the whole cluster from each node's static hardware class
|
|
27579
|
-
* measured latency table
|
|
27609
|
+
* Auto tune (D745, D755) — DRY RUN. Recommends ONE detector family
|
|
27610
|
+
* (YOLOv9) for the whole cluster from each node's static hardware class
|
|
27611
|
+
* and the measured latency table: per device, the most accurate model
|
|
27612
|
+
* that sustains at least `minSessionsPerDevice` parallel sessions at the
|
|
27613
|
+
* camera rate with the headroom kept free (no load input), and lists every write
|
|
27580
27614
|
* the apply would make: the node model pins set, the enrichment pins and
|
|
27581
27615
|
* model-bound thresholds removed (node level and per camera), and the
|
|
27582
27616
|
* per-camera detector thresholds cleared. Writes nothing. Step
|
|
@@ -27615,10 +27649,12 @@ var pipelineOrchestratorCapability = {
|
|
|
27615
27649
|
*/
|
|
27616
27650
|
getPlacementStats: method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }),
|
|
27617
27651
|
/**
|
|
27618
|
-
* Load balancing (D748) — shares proportional to each node's
|
|
27619
|
-
* capacity
|
|
27620
|
-
*
|
|
27621
|
-
*
|
|
27652
|
+
* Load balancing (D748, D755) — shares proportional to each node's
|
|
27653
|
+
* capacity in parallel sessions: from the latency table at the models
|
|
27654
|
+
* Auto tune picks (`hardware`), or from node benchmark results
|
|
27655
|
+
* (`benchmark`, each node without results falling back to `hardware` and
|
|
27656
|
+
* saying so). The automatic shares are exactly this answer. Per device,
|
|
27657
|
+
* an ADVISORY session cap. Writes nothing.
|
|
27622
27658
|
*/
|
|
27623
27659
|
suggestShares: method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }),
|
|
27624
27660
|
/**
|
|
@@ -27629,9 +27665,11 @@ var pipelineOrchestratorCapability = {
|
|
|
27629
27665
|
*/
|
|
27630
27666
|
getEffectiveShares: method(_void(), EffectiveSharesSchema, { auth: "admin" }),
|
|
27631
27667
|
/**
|
|
27632
|
-
* Load balancing (D750) — switch
|
|
27633
|
-
*
|
|
27634
|
-
*
|
|
27668
|
+
* Load balancing (D750, D755) — switch the share source: `hardware`
|
|
27669
|
+
* (automatic, from static capacity), `benchmark` (automatic, from node
|
|
27670
|
+
* benchmark results) or `manual`. Going manual seeds the sliders with the
|
|
27671
|
+
* current automatic shares (one atomic write), so nothing jumps; going
|
|
27672
|
+
* to an automatic source recomputes at once.
|
|
27635
27673
|
*/
|
|
27636
27674
|
setShareSource: method(SetShareSourceInputSchema, object({ success: literal(true) }), {
|
|
27637
27675
|
kind: "mutation",
|
|
@@ -27666,6 +27704,22 @@ var pipelineOrchestratorCapability = {
|
|
|
27666
27704
|
kind: "mutation",
|
|
27667
27705
|
auth: "admin"
|
|
27668
27706
|
}),
|
|
27707
|
+
/**
|
|
27708
|
+
* Write or clear one step's CAMERA-WIDE settings (D754) — the fields this
|
|
27709
|
+
* camera overrides on every accelerator it runs on. `settings` is the
|
|
27710
|
+
* whole field-level bag for that step (only the overridden keys; a
|
|
27711
|
+
* model-bound field travels with its binding key); `null` or `{}` clears
|
|
27712
|
+
* it. Merged at dispatch between the node base and the per-(node,device)
|
|
27713
|
+
* override, key by key.
|
|
27714
|
+
*/
|
|
27715
|
+
setCameraStepSettings: method(object({
|
|
27716
|
+
deviceId: number(),
|
|
27717
|
+
addonId: string(),
|
|
27718
|
+
settings: record(string(), unknown()).nullable()
|
|
27719
|
+
}), object({ success: literal(true) }), {
|
|
27720
|
+
kind: "mutation",
|
|
27721
|
+
auth: "admin"
|
|
27722
|
+
}),
|
|
27669
27723
|
/** Write or clear the wholesale per-agent pipeline override. Pass `pipeline: null` to drop the override. */
|
|
27670
27724
|
setCameraPipelineForAgent: method(object({
|
|
27671
27725
|
deviceId: number(),
|
|
@@ -43954,6 +44008,12 @@ Object.freeze({
|
|
|
43954
44008
|
addonId: null,
|
|
43955
44009
|
access: "create"
|
|
43956
44010
|
},
|
|
44011
|
+
"pipelineOrchestrator.setCameraStepSettings": {
|
|
44012
|
+
capName: "pipeline-orchestrator",
|
|
44013
|
+
capScope: "system",
|
|
44014
|
+
addonId: null,
|
|
44015
|
+
access: "create"
|
|
44016
|
+
},
|
|
43957
44017
|
"pipelineOrchestrator.setCameraStepToggle": {
|
|
43958
44018
|
capName: "pipeline-orchestrator",
|
|
43959
44019
|
capScope: "system",
|
|
@@ -49430,6 +49490,11 @@ Object.freeze({
|
|
|
49430
49490
|
form: "single",
|
|
49431
49491
|
optional: false
|
|
49432
49492
|
}],
|
|
49493
|
+
"pipelineOrchestrator.setCameraStepSettings": [{
|
|
49494
|
+
name: "deviceId",
|
|
49495
|
+
form: "single",
|
|
49496
|
+
optional: false
|
|
49497
|
+
}],
|
|
49433
49498
|
"pipelineOrchestrator.setCameraStepToggle": [{
|
|
49434
49499
|
name: "deviceId",
|
|
49435
49500
|
form: "single",
|
|
@@ -51720,137 +51785,22 @@ var DETECTOR_ACCURACY = {
|
|
|
51720
51785
|
personRecall: .646
|
|
51721
51786
|
}
|
|
51722
51787
|
};
|
|
51723
|
-
var DEFAULT_DEMAND_OPTIONS = {
|
|
51724
|
-
onMotionConcurrency: .25,
|
|
51725
|
-
onMotionMinConcurrent: 2
|
|
51726
|
-
};
|
|
51727
|
-
function deviceId(nodeId, deviceKey) {
|
|
51728
|
-
return `${nodeId}\u0000${deviceKey}`;
|
|
51729
|
-
}
|
|
51730
|
-
function charges(load) {
|
|
51731
|
-
return load.mode !== "disabled" && load.runsPipelineRoot && load.fps > 0;
|
|
51732
|
-
}
|
|
51733
|
-
/** The highest `count` rates, summed. */
|
|
51734
|
-
function topRatesSum(rates, count) {
|
|
51735
|
-
return rates.toSorted((a, b) => b - a).slice(0, Math.max(0, count)).reduce((sum, r) => sum + r, 0);
|
|
51736
|
-
}
|
|
51737
|
-
/** Concurrent on-motion sessions assumed cluster-wide without an observation. */
|
|
51738
|
-
function estimatedConcurrentSessions(cameraCount, options) {
|
|
51739
|
-
if (cameraCount <= 0) return 0;
|
|
51740
|
-
const share = Math.ceil(options.onMotionConcurrency * cameraCount);
|
|
51741
|
-
return Math.min(cameraCount, Math.max(share, Math.min(cameraCount, options.onMotionMinConcurrent)));
|
|
51742
|
-
}
|
|
51743
|
-
/**
|
|
51744
|
-
* Demand on every eligible device of the cluster.
|
|
51745
|
-
*/
|
|
51746
|
-
function estimateDeviceDemand(input) {
|
|
51747
|
-
const options = input.options ?? DEFAULT_DEMAND_OPTIONS;
|
|
51748
|
-
const placement = input.shares ? "share" : "least-loaded";
|
|
51749
|
-
const eligibleById = new Map(input.eligible.map((d) => [deviceId(d.nodeId, d.deviceKey), d]));
|
|
51750
|
-
const peakById = new Map(input.peaks.map((p) => [deviceId(p.nodeId, p.deviceKey), p]));
|
|
51751
|
-
const weightByNode = /* @__PURE__ */ new Map();
|
|
51752
|
-
const eligibleCountByNode = /* @__PURE__ */ new Map();
|
|
51753
|
-
for (const d of input.eligible) {
|
|
51754
|
-
weightByNode.set(d.nodeId, (weightByNode.get(d.nodeId) ?? 0) + Math.max(d.weight, 0));
|
|
51755
|
-
eligibleCountByNode.set(d.nodeId, (eligibleCountByNode.get(d.nodeId) ?? 0) + 1);
|
|
51756
|
-
}
|
|
51757
|
-
const pinnedFps = /* @__PURE__ */ new Map();
|
|
51758
|
-
const alwaysOnFps = /* @__PURE__ */ new Map();
|
|
51759
|
-
const alwaysOnCount = /* @__PURE__ */ new Map();
|
|
51760
|
-
const maxFps = /* @__PURE__ */ new Map();
|
|
51761
|
-
const onMotionRates = [];
|
|
51762
|
-
const bump = (map, key, by) => {
|
|
51763
|
-
map.set(key, (map.get(key) ?? 0) + by);
|
|
51764
|
-
};
|
|
51765
|
-
const noteMax = (key, fps) => {
|
|
51766
|
-
maxFps.set(key, Math.max(maxFps.get(key) ?? 0, fps));
|
|
51767
|
-
};
|
|
51768
|
-
const pinNodeOf = (pin) => {
|
|
51769
|
-
if (pin.nodeId !== null) return pin.nodeId;
|
|
51770
|
-
const holders = input.eligible.filter((d) => d.deviceKey === pin.deviceKey);
|
|
51771
|
-
return holders.length === 1 ? holders[0].nodeId : null;
|
|
51772
|
-
};
|
|
51773
|
-
let alwaysOnTotal = 0;
|
|
51774
|
-
for (const cam of input.cameras) {
|
|
51775
|
-
if (!charges(cam)) continue;
|
|
51776
|
-
const pinNode = cam.devicePin !== null ? pinNodeOf(cam.devicePin) : null;
|
|
51777
|
-
if (cam.devicePin !== null && pinNode !== null) {
|
|
51778
|
-
const key = deviceId(pinNode, cam.devicePin.deviceKey);
|
|
51779
|
-
if (!eligibleById.has(key)) continue;
|
|
51780
|
-
bump(pinnedFps, key, cam.fps);
|
|
51781
|
-
noteMax(key, cam.fps);
|
|
51782
|
-
continue;
|
|
51783
|
-
}
|
|
51784
|
-
const standing = cam.mode === "always-on" && cam.standingNodeId !== null ? cam.standingNodeId : cam.nodePinId;
|
|
51785
|
-
if (standing !== null) {
|
|
51786
|
-
if (cam.mode === "always-on") alwaysOnTotal += 1;
|
|
51787
|
-
const nodeWeight = weightByNode.get(standing) ?? 0;
|
|
51788
|
-
for (const d of input.eligible) {
|
|
51789
|
-
if (d.nodeId !== standing || nodeWeight <= 0) continue;
|
|
51790
|
-
const key = deviceId(d.nodeId, d.deviceKey);
|
|
51791
|
-
const share = Math.max(d.weight, 0) / nodeWeight;
|
|
51792
|
-
bump(alwaysOnFps, key, cam.fps * share);
|
|
51793
|
-
bump(alwaysOnCount, key, share);
|
|
51794
|
-
noteMax(key, cam.fps);
|
|
51795
|
-
}
|
|
51796
|
-
continue;
|
|
51797
|
-
}
|
|
51798
|
-
onMotionRates.push(cam.fps);
|
|
51799
|
-
}
|
|
51800
|
-
const fastestOnMotion = onMotionRates.reduce((m, r) => Math.max(m, r), 0);
|
|
51801
|
-
const nodeCount = weightByNode.size;
|
|
51802
|
-
const estimatedTotal = estimatedConcurrentSessions(onMotionRates.length, options);
|
|
51803
|
-
const clusterPeak = input.shares?.clusterPeak ?? null;
|
|
51804
|
-
const clusterObserved = clusterPeak !== null && clusterPeak.observedForMs >= 864e5;
|
|
51805
|
-
const clusterSessions = clusterObserved ? Math.max(0, clusterPeak.peakSessions - alwaysOnTotal) : estimatedTotal;
|
|
51806
|
-
const shareSum = [...weightByNode.keys()].reduce((sum, nodeId) => sum + Math.max(input.shares?.nodes[nodeId] ?? 0, 0), 0);
|
|
51807
|
-
return input.eligible.map((d) => {
|
|
51808
|
-
const key = deviceId(d.nodeId, d.deviceKey);
|
|
51809
|
-
const peak = peakById.get(key) ?? (eligibleCountByNode.get(d.nodeId) === 1 ? peakById.get(deviceId(d.nodeId, "")) : void 0);
|
|
51810
|
-
const alwaysOn = alwaysOnCount.get(key) ?? 0;
|
|
51811
|
-
let onMotionSessions = 0;
|
|
51812
|
-
let onMotionSource = "none";
|
|
51813
|
-
if (onMotionRates.length > 0 && input.shares !== void 0) {
|
|
51814
|
-
const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
|
|
51815
|
-
const nodeShare = shareSum > 0 ? Math.max(input.shares.nodes[d.nodeId] ?? 0, 0) / shareSum : 0;
|
|
51816
|
-
const deviceShare = nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight : 0;
|
|
51817
|
-
onMotionSessions = Math.max(0, Math.ceil(clusterSessions * nodeShare * deviceShare - 1e-9));
|
|
51818
|
-
onMotionSource = clusterObserved ? "observed-24h" : "estimate";
|
|
51819
|
-
} else if (onMotionRates.length > 0) if (peak !== void 0 && peak.observedForMs >= 864e5) {
|
|
51820
|
-
onMotionSessions = Math.max(0, Math.round(peak.peakSessions - alwaysOn));
|
|
51821
|
-
onMotionSource = "observed-24h";
|
|
51822
|
-
} else {
|
|
51823
|
-
const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
|
|
51824
|
-
const share = nodeCount > 0 && nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight / nodeCount : 0;
|
|
51825
|
-
onMotionSessions = Math.ceil(estimatedTotal * share);
|
|
51826
|
-
onMotionSource = "estimate";
|
|
51827
|
-
}
|
|
51828
|
-
const demandFps = (pinnedFps.get(key) ?? 0) + (alwaysOnFps.get(key) ?? 0) + topRatesSum(onMotionRates, onMotionSessions);
|
|
51829
|
-
return {
|
|
51830
|
-
nodeId: d.nodeId,
|
|
51831
|
-
deviceKey: d.deviceKey,
|
|
51832
|
-
demandFps: Math.round(demandFps * 100) / 100,
|
|
51833
|
-
maxCameraFps: Math.max(maxFps.get(key) ?? 0, fastestOnMotion),
|
|
51834
|
-
alwaysOnCameras: Math.round(alwaysOn * 100) / 100,
|
|
51835
|
-
onMotionSessions,
|
|
51836
|
-
onMotionSource,
|
|
51837
|
-
observedForMs: input.shares !== void 0 ? clusterPeak?.observedForMs ?? null : peak?.observedForMs ?? null,
|
|
51838
|
-
placement
|
|
51839
|
-
};
|
|
51840
|
-
}).toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
|
|
51841
|
-
}
|
|
51842
51788
|
/** The family the cluster runs. */
|
|
51843
51789
|
var AUTO_TUNE_FAMILY = "yolov9";
|
|
51790
|
+
/** Parallel sessions one device sustains at `fps` with `headroom` kept free. */
|
|
51791
|
+
function capacitySessionsOf(fps, headroom, cameraFps) {
|
|
51792
|
+
if (!(fps > 0) || !(cameraFps > 0)) return 0;
|
|
51793
|
+
return Math.max(0, Math.floor(fps * (1 - headroom) / cameraFps + 1e-9));
|
|
51794
|
+
}
|
|
51844
51795
|
function round(value, digits = 3) {
|
|
51845
51796
|
const f = 10 ** digits;
|
|
51846
51797
|
return Math.round(value * f) / f;
|
|
51847
51798
|
}
|
|
51848
|
-
function
|
|
51849
|
-
return cell
|
|
51799
|
+
function sessionsOn(cell, sizing) {
|
|
51800
|
+
return cell === void 0 ? 0 : capacitySessionsOf(cell.fps, sizing.headroom, sizing.cameraFps);
|
|
51850
51801
|
}
|
|
51851
|
-
function fitsOn(cell,
|
|
51852
|
-
|
|
51853
|
-
return budget >= (demand?.demandFps ?? 0) && budget >= (demand?.maxCameraFps ?? 0);
|
|
51802
|
+
function fitsOn(cell, sizing) {
|
|
51803
|
+
return sessionsOn(cell, sizing) >= sizing.minSessions;
|
|
51854
51804
|
}
|
|
51855
51805
|
function candidatesFor(contexts, formatsByModel) {
|
|
51856
51806
|
return Object.keys(DETECTOR_ACCURACY).filter((id) => id.startsWith(AUTO_TUNE_FAMILY)).filter((id) => contexts.every((c) => c.column[id] !== void 0 && (formatsByModel[id] ?? []).includes(c.device.format))).map((id) => ({
|
|
@@ -51861,28 +51811,24 @@ function candidatesFor(contexts, formatsByModel) {
|
|
|
51861
51811
|
function totalLatency(contexts, modelId) {
|
|
51862
51812
|
return contexts.reduce((sum, c) => sum + (c.column[modelId]?.p50Ms ?? Number.POSITIVE_INFINITY), 0);
|
|
51863
51813
|
}
|
|
51864
|
-
|
|
51865
|
-
|
|
51866
|
-
|
|
51867
|
-
return Math.max(worst, cell ? utilisationOf(cell, c.demand) : Number.POSITIVE_INFINITY);
|
|
51868
|
-
}, 0);
|
|
51814
|
+
/** The fewest sessions any device of the group sustains with this model. */
|
|
51815
|
+
function worstSessions(contexts, modelId, sizing) {
|
|
51816
|
+
return contexts.reduce((worst, c) => Math.min(worst, sessionsOn(c.column[modelId], sizing)), Number.POSITIVE_INFINITY);
|
|
51869
51817
|
}
|
|
51870
51818
|
/** One model for a group of devices that must agree (step 2–4 of the header). */
|
|
51871
|
-
function pickForGroup(contexts, formatsByModel,
|
|
51819
|
+
function pickForGroup(contexts, formatsByModel, sizing) {
|
|
51872
51820
|
const candidates = candidatesFor(contexts, formatsByModel);
|
|
51873
51821
|
if (candidates.length === 0) return null;
|
|
51874
|
-
const fitting = candidates.filter((c) => contexts.every((ctx) =>
|
|
51875
|
-
const cell = ctx.column[c.modelId];
|
|
51876
|
-
return cell !== void 0 && fitsOn(cell, ctx.demand, headroom);
|
|
51877
|
-
}));
|
|
51822
|
+
const fitting = candidates.filter((c) => contexts.every((ctx) => fitsOn(ctx.column[c.modelId], sizing)));
|
|
51878
51823
|
if (fitting.length === 0) {
|
|
51879
|
-
const lightest = candidates.toSorted((a, b) =>
|
|
51824
|
+
const lightest = candidates.toSorted((a, b) => worstSessions(contexts, b.modelId, sizing) - worstSessions(contexts, a.modelId, sizing) || totalLatency(contexts, a.modelId) - totalLatency(contexts, b.modelId))[0];
|
|
51880
51825
|
if (lightest === void 0) return null;
|
|
51881
51826
|
return {
|
|
51882
51827
|
modelId: lightest.modelId,
|
|
51883
51828
|
reasons: [{
|
|
51884
51829
|
kind: "no-fit-lightest",
|
|
51885
|
-
|
|
51830
|
+
sessions: worstSessions(contexts, lightest.modelId, sizing),
|
|
51831
|
+
minSessions: sizing.minSessions
|
|
51886
51832
|
}]
|
|
51887
51833
|
};
|
|
51888
51834
|
}
|
|
@@ -51892,10 +51838,7 @@ function pickForGroup(contexts, formatsByModel, headroom) {
|
|
|
51892
51838
|
const reasons = [];
|
|
51893
51839
|
const mostAccurate = candidates[0];
|
|
51894
51840
|
if (mostAccurate !== void 0 && mostAccurate.modelId !== best.modelId) {
|
|
51895
|
-
const limitedBy = contexts.find((ctx) =>
|
|
51896
|
-
const cell = ctx.column[mostAccurate.modelId];
|
|
51897
|
-
return cell === void 0 || !fitsOn(cell, ctx.demand, headroom);
|
|
51898
|
-
})?.device.key ?? "";
|
|
51841
|
+
const limitedBy = contexts.find((ctx) => !fitsOn(ctx.column[mostAccurate.modelId], sizing))?.device.key ?? "";
|
|
51899
51842
|
reasons.push({
|
|
51900
51843
|
kind: "stepped-down",
|
|
51901
51844
|
from: mostAccurate.modelId,
|
|
@@ -51913,7 +51856,7 @@ function pickForGroup(contexts, formatsByModel, headroom) {
|
|
|
51913
51856
|
reasons
|
|
51914
51857
|
};
|
|
51915
51858
|
}
|
|
51916
|
-
function choiceOn(ctx, modelId) {
|
|
51859
|
+
function choiceOn(ctx, modelId, sizing) {
|
|
51917
51860
|
const cell = ctx.column[modelId];
|
|
51918
51861
|
if (cell === void 0) return null;
|
|
51919
51862
|
return {
|
|
@@ -51921,23 +51864,20 @@ function choiceOn(ctx, modelId) {
|
|
|
51921
51864
|
p50Ms: cell.p50Ms,
|
|
51922
51865
|
fps: cell.fps,
|
|
51923
51866
|
measured: cell.measured,
|
|
51924
|
-
|
|
51867
|
+
capacitySessions: sessionsOn(cell, sizing),
|
|
51925
51868
|
personRecall: DETECTOR_ACCURACY[modelId]?.personRecall ?? 0
|
|
51926
51869
|
};
|
|
51927
51870
|
}
|
|
51928
|
-
function deviceReasonsFor(
|
|
51871
|
+
function deviceReasonsFor(choice, sizing) {
|
|
51929
51872
|
const out = [{
|
|
51930
|
-
kind: "
|
|
51931
|
-
|
|
51873
|
+
kind: "capacity",
|
|
51874
|
+
sessions: choice.capacitySessions,
|
|
51875
|
+
minSessions: sizing.minSessions
|
|
51932
51876
|
}];
|
|
51933
51877
|
if (!choice.measured) out.push({
|
|
51934
51878
|
kind: "derived-latency",
|
|
51935
51879
|
modelId: choice.modelId
|
|
51936
51880
|
});
|
|
51937
|
-
if (ctx.demand?.onMotionSource === "estimate") out.push({
|
|
51938
|
-
kind: "estimated-load",
|
|
51939
|
-
observedForMs: ctx.demand.observedForMs
|
|
51940
|
-
});
|
|
51941
51881
|
return out;
|
|
51942
51882
|
}
|
|
51943
51883
|
function skippedNode(node, reason, hardwareClass) {
|
|
@@ -51953,7 +51893,6 @@ function skippedNode(node, reason, hardwareClass) {
|
|
|
51953
51893
|
accelerator: node.hardware ? acceleratorKindOf(d.key, node.hardware) : null,
|
|
51954
51894
|
enabled: d.enabled,
|
|
51955
51895
|
available: d.available,
|
|
51956
|
-
demand: null,
|
|
51957
51896
|
chosen: null,
|
|
51958
51897
|
reasons: []
|
|
51959
51898
|
})),
|
|
@@ -51963,7 +51902,11 @@ function skippedNode(node, reason, hardwareClass) {
|
|
|
51963
51902
|
/** Recommend one node. See the module header. */
|
|
51964
51903
|
function recommendNode(input) {
|
|
51965
51904
|
const { node } = input;
|
|
51966
|
-
const
|
|
51905
|
+
const sizing = {
|
|
51906
|
+
headroom: input.headroom ?? .4,
|
|
51907
|
+
cameraFps: input.cameraFps ?? 10,
|
|
51908
|
+
minSessions: input.minSessionsPerDevice ?? 2
|
|
51909
|
+
};
|
|
51967
51910
|
const perf = input.perf ?? DETECTOR_PERF;
|
|
51968
51911
|
if (!node.reachable) return skippedNode(node, { kind: "node-offline" }, null);
|
|
51969
51912
|
if (node.hardware === null) return skippedNode(node, { kind: "no-hardware-facts" }, null);
|
|
@@ -51974,7 +51917,6 @@ function recommendNode(input) {
|
|
|
51974
51917
|
kind: "no-measured-data",
|
|
51975
51918
|
hardwareClass
|
|
51976
51919
|
}, hardwareClass);
|
|
51977
|
-
const demandByKey = new Map(input.demand.filter((d) => d.nodeId === node.nodeId).map((d) => [d.deviceKey, d]));
|
|
51978
51920
|
const devices = node.devices.toSorted((a, b) => a.key.localeCompare(b.key));
|
|
51979
51921
|
const contexts = /* @__PURE__ */ new Map();
|
|
51980
51922
|
const early = /* @__PURE__ */ new Map();
|
|
@@ -52003,8 +51945,7 @@ function recommendNode(input) {
|
|
|
52003
51945
|
contexts.set(device.key, {
|
|
52004
51946
|
device,
|
|
52005
51947
|
accelerator,
|
|
52006
|
-
column
|
|
52007
|
-
demand: demandByKey.get(device.key) ?? null
|
|
51948
|
+
column
|
|
52008
51949
|
});
|
|
52009
51950
|
}
|
|
52010
51951
|
const isCpu = (a) => a === "x86-cpu" || a === "arm-cpu";
|
|
@@ -52014,23 +51955,23 @@ function recommendNode(input) {
|
|
|
52014
51955
|
const reasonsByKey = /* @__PURE__ */ new Map();
|
|
52015
51956
|
const nodeReasons = [];
|
|
52016
51957
|
let acceleratorModelId = null;
|
|
52017
|
-
const groupPick = accelerators.length > 0 ? pickForGroup(accelerators, node.formatsByModel,
|
|
51958
|
+
const groupPick = accelerators.length > 0 ? pickForGroup(accelerators, node.formatsByModel, sizing) : null;
|
|
52018
51959
|
if (groupPick !== null) {
|
|
52019
51960
|
acceleratorModelId = groupPick.modelId;
|
|
52020
51961
|
nodeReasons.push(...groupPick.reasons);
|
|
52021
51962
|
for (const ctx of accelerators) {
|
|
52022
|
-
const choice = choiceOn(ctx, groupPick.modelId);
|
|
51963
|
+
const choice = choiceOn(ctx, groupPick.modelId, sizing);
|
|
52023
51964
|
if (choice === null) continue;
|
|
52024
51965
|
chosenByKey.set(ctx.device.key, choice);
|
|
52025
|
-
reasonsByKey.set(ctx.device.key, deviceReasonsFor(
|
|
51966
|
+
reasonsByKey.set(ctx.device.key, deviceReasonsFor(choice, sizing));
|
|
52026
51967
|
}
|
|
52027
51968
|
} else for (const ctx of accelerators) reasonsByKey.set(ctx.device.key, [{
|
|
52028
51969
|
kind: "no-family-build",
|
|
52029
51970
|
format: ctx.device.format
|
|
52030
51971
|
}]);
|
|
52031
51972
|
for (const ctx of cpus) {
|
|
52032
|
-
const pick = pickForGroup([ctx], node.formatsByModel,
|
|
52033
|
-
const choice = pick === null ? null : choiceOn(ctx, pick.modelId);
|
|
51973
|
+
const pick = pickForGroup([ctx], node.formatsByModel, sizing);
|
|
51974
|
+
const choice = pick === null ? null : choiceOn(ctx, pick.modelId, sizing);
|
|
52034
51975
|
if (pick === null || choice === null) {
|
|
52035
51976
|
reasonsByKey.set(ctx.device.key, [{
|
|
52036
51977
|
kind: "no-family-build",
|
|
@@ -52042,7 +51983,7 @@ function recommendNode(input) {
|
|
|
52042
51983
|
reasonsByKey.set(ctx.device.key, [
|
|
52043
51984
|
{ kind: "cpu-device" },
|
|
52044
51985
|
...pick.reasons,
|
|
52045
|
-
...deviceReasonsFor(
|
|
51986
|
+
...deviceReasonsFor(choice, sizing)
|
|
52046
51987
|
]);
|
|
52047
51988
|
}
|
|
52048
51989
|
return {
|
|
@@ -52057,7 +51998,6 @@ function recommendNode(input) {
|
|
|
52057
51998
|
accelerator: acceleratorKindOf(device.key, hardware),
|
|
52058
51999
|
enabled: device.enabled,
|
|
52059
52000
|
available: device.available,
|
|
52060
|
-
demand: demandByKey.get(device.key) ?? null,
|
|
52061
52001
|
chosen: chosenByKey.get(device.key) ?? null,
|
|
52062
52002
|
reasons: early.get(device.key) ?? reasonsByKey.get(device.key) ?? []
|
|
52063
52003
|
})),
|
|
@@ -52104,6 +52044,18 @@ function electByShare(candidates) {
|
|
|
52104
52044
|
reason: "share"
|
|
52105
52045
|
};
|
|
52106
52046
|
}
|
|
52047
|
+
/**
|
|
52048
|
+
* The shares a session election actually reads, and the view the Load
|
|
52049
|
+
* balancing tab shows (D750). Pure.
|
|
52050
|
+
*
|
|
52051
|
+
* The SOURCE decides the configured weights: the automatic capacity shares
|
|
52052
|
+
* (`hardware` or `benchmark`, D755), or the operator's stored sliders. Eligibility decides the effective ones: a
|
|
52053
|
+
* node or device that cannot take a session now (offline, switched off, no
|
|
52054
|
+
* detect capability, unable to run the root) contributes nothing, and the
|
|
52055
|
+
* others split its share in proportion to their own. That redistribution is
|
|
52056
|
+
* exactly what `electByShare` does over its candidate set, so the view and
|
|
52057
|
+
* the election cannot disagree; the stored values are never rewritten for it.
|
|
52058
|
+
*/
|
|
52107
52059
|
/** Weight a node or device has when its source names none (the balancer's own default). */
|
|
52108
52060
|
var DEFAULT_WEIGHT = 1;
|
|
52109
52061
|
function positive(value) {
|
|
@@ -52114,7 +52066,8 @@ function percentages(keys, weightOf, counts) {
|
|
|
52114
52066
|
return new Map(keys.map((k) => [k, total > 0 && counts(k) ? Math.round(weightOf(k) / total * 1e3) / 10 : 0]));
|
|
52115
52067
|
}
|
|
52116
52068
|
function resolveEffectiveShares(input) {
|
|
52117
|
-
const
|
|
52069
|
+
const useAuto = isAutomaticShareSource(input.source) && input.auto !== null;
|
|
52070
|
+
const weights = useAuto && input.auto !== null ? input.auto : input.stored;
|
|
52118
52071
|
const nodeWeights = {};
|
|
52119
52072
|
for (const nodeId of input.nodeIds) nodeWeights[nodeId] = positive(weights.nodes[nodeId]);
|
|
52120
52073
|
const eligibleNodes = new Set(input.eligibleNodes);
|
|
@@ -52136,18 +52089,21 @@ function resolveEffectiveShares(input) {
|
|
|
52136
52089
|
configuredPct: devConfigured.get(deviceKey) ?? 0,
|
|
52137
52090
|
effectivePct: devEffective.get(deviceKey) ?? 0
|
|
52138
52091
|
}));
|
|
52092
|
+
const capacity = useAuto ? input.autoCapacity?.[nodeId] : void 0;
|
|
52139
52093
|
return {
|
|
52140
52094
|
nodeId,
|
|
52141
52095
|
eligible: eligibleNodes.has(nodeId),
|
|
52142
52096
|
configuredPct: configured.get(nodeId) ?? 0,
|
|
52143
52097
|
effectivePct: effective.get(nodeId) ?? 0,
|
|
52098
|
+
capacitySource: capacity?.source ?? null,
|
|
52099
|
+
capacityFallback: capacity?.fallback ?? null,
|
|
52144
52100
|
devices
|
|
52145
52101
|
};
|
|
52146
52102
|
});
|
|
52147
52103
|
return {
|
|
52148
52104
|
source: input.source,
|
|
52149
52105
|
policy: input.policy ?? "share",
|
|
52150
|
-
autoPending: input.source
|
|
52106
|
+
autoPending: isAutomaticShareSource(input.source) && input.auto === null,
|
|
52151
52107
|
computedAt: input.computedAt ?? null,
|
|
52152
52108
|
trigger: input.trigger ?? null,
|
|
52153
52109
|
nodes,
|
|
@@ -52156,6 +52112,137 @@ function resolveEffectiveShares(input) {
|
|
|
52156
52112
|
};
|
|
52157
52113
|
}
|
|
52158
52114
|
/**
|
|
52115
|
+
* Automatic shares from CAPACITY (D755). Pure.
|
|
52116
|
+
*
|
|
52117
|
+
* A node's share is its capacity in parallel sessions over the cluster's; a
|
|
52118
|
+
* device's share inside its node follows its throughput. There is no load
|
|
52119
|
+
* input: nothing here depends on how many sessions ran, only on what each
|
|
52120
|
+
* node can carry.
|
|
52121
|
+
*
|
|
52122
|
+
* Two places a capacity can come from:
|
|
52123
|
+
* - `hardware` — each enabled accelerator at its Auto tune model, from the
|
|
52124
|
+
* measured latency table: ⌊fps × (1 − headroom) / camera fps⌋ sessions;
|
|
52125
|
+
* - `benchmark` — the node's own measured max sessions per device, read
|
|
52126
|
+
* through a {@link NodeCapacitySource}. A node whose results do not cover
|
|
52127
|
+
* every enabled device it would be sized on is sized from `hardware`
|
|
52128
|
+
* instead, and the answer says so per node — never a silent mix.
|
|
52129
|
+
*/
|
|
52130
|
+
/** A source that has no results — the benchmark store before it exists. */
|
|
52131
|
+
var NO_MEASURED_CAPACITY = { readMeasuredCapacity: async () => /* @__PURE__ */ new Map() };
|
|
52132
|
+
/** Integer shares summing to 100 (largest remainder); ties broken by key. */
|
|
52133
|
+
function toPercentShares(weights) {
|
|
52134
|
+
const total = [...weights.values()].reduce((a, b) => a + b, 0);
|
|
52135
|
+
const out = /* @__PURE__ */ new Map();
|
|
52136
|
+
if (total <= 0) return out;
|
|
52137
|
+
const raw = [...weights].map(([k, w]) => ({
|
|
52138
|
+
k,
|
|
52139
|
+
exact: w / total * 100
|
|
52140
|
+
}));
|
|
52141
|
+
let assigned = 0;
|
|
52142
|
+
for (const r of raw) {
|
|
52143
|
+
out.set(r.k, Math.floor(r.exact));
|
|
52144
|
+
assigned += Math.floor(r.exact);
|
|
52145
|
+
}
|
|
52146
|
+
const remainder = (exact) => Math.round((exact - Math.floor(exact)) * 1e6);
|
|
52147
|
+
const byRemainder = raw.toSorted((a, b) => remainder(b.exact) - remainder(a.exact) || a.k.localeCompare(b.k));
|
|
52148
|
+
for (let i = 0; i < 100 - assigned; i++) {
|
|
52149
|
+
const r = byRemainder[i % byRemainder.length];
|
|
52150
|
+
if (r === void 0) break;
|
|
52151
|
+
out.set(r.k, (out.get(r.k) ?? 0) + 1);
|
|
52152
|
+
}
|
|
52153
|
+
return out;
|
|
52154
|
+
}
|
|
52155
|
+
function hardwareDevices(rec, headroom, cameraFps) {
|
|
52156
|
+
return rec.devices.filter((d) => d.enabled && d.available && d.chosen !== null).map((d) => {
|
|
52157
|
+
const fps = d.chosen?.fps ?? 0;
|
|
52158
|
+
return {
|
|
52159
|
+
deviceKey: d.deviceKey,
|
|
52160
|
+
sessions: capacitySessionsOf(fps, headroom, cameraFps),
|
|
52161
|
+
weight: Math.max(fps, 0),
|
|
52162
|
+
modelId: d.chosen?.modelId ?? null,
|
|
52163
|
+
fps: d.chosen?.fps ?? null,
|
|
52164
|
+
measured: d.chosen?.measured ?? false
|
|
52165
|
+
};
|
|
52166
|
+
});
|
|
52167
|
+
}
|
|
52168
|
+
function sizeNode(rec, input) {
|
|
52169
|
+
const fromHardware = (fallback) => {
|
|
52170
|
+
const devices = rec.skipped ? [] : hardwareDevices(rec, input.headroom, input.cameraFps);
|
|
52171
|
+
const capacity = devices.reduce((sum, d) => sum + d.sessions, 0);
|
|
52172
|
+
return {
|
|
52173
|
+
rec,
|
|
52174
|
+
devices,
|
|
52175
|
+
capacitySource: rec.skipped ? null : "hardware",
|
|
52176
|
+
capacityFallback: rec.skipped ? null : fallback,
|
|
52177
|
+
skippedReason: rec.skipped ? rec.reasons[0]?.kind ?? "skipped" : capacity > 0 ? null : "no-capacity"
|
|
52178
|
+
};
|
|
52179
|
+
};
|
|
52180
|
+
if (input.source === "hardware") return fromHardware(null);
|
|
52181
|
+
if (!rec.reachable) return fromHardware(null);
|
|
52182
|
+
const results = input.measured.get(rec.nodeId);
|
|
52183
|
+
const byKey = new Map((results?.devices ?? []).map((d) => [d.deviceKey, d]));
|
|
52184
|
+
const usable = rec.devices.filter((d) => d.enabled && d.available);
|
|
52185
|
+
const mustCover = rec.devices.filter((d) => d.enabled && d.available && d.chosen !== null);
|
|
52186
|
+
const covered = usable.filter((d) => byKey.has(d.deviceKey));
|
|
52187
|
+
if (covered.length === 0) return fromHardware("no-benchmark-results");
|
|
52188
|
+
if (mustCover.some((d) => !byKey.has(d.deviceKey))) return fromHardware("partial-benchmark-results");
|
|
52189
|
+
const devices = covered.map((d) => {
|
|
52190
|
+
const row = byKey.get(d.deviceKey);
|
|
52191
|
+
const sessions = Math.max(0, Math.floor(row?.maxSessions ?? 0));
|
|
52192
|
+
return {
|
|
52193
|
+
deviceKey: d.deviceKey,
|
|
52194
|
+
sessions,
|
|
52195
|
+
weight: sessions,
|
|
52196
|
+
modelId: row?.modelId ?? null,
|
|
52197
|
+
fps: null,
|
|
52198
|
+
measured: true
|
|
52199
|
+
};
|
|
52200
|
+
});
|
|
52201
|
+
return {
|
|
52202
|
+
rec,
|
|
52203
|
+
devices,
|
|
52204
|
+
capacitySource: "benchmark",
|
|
52205
|
+
capacityFallback: null,
|
|
52206
|
+
skippedReason: devices.reduce((sum, d) => sum + d.sessions, 0) > 0 ? null : "no-capacity"
|
|
52207
|
+
};
|
|
52208
|
+
}
|
|
52209
|
+
/** Shares proportional to capacity — the automatic shares and the suggestion are this. */
|
|
52210
|
+
function computeCapacityShares(input) {
|
|
52211
|
+
const sized = input.recommendations.map((rec) => sizeNode(rec, input));
|
|
52212
|
+
const capacityByNode = /* @__PURE__ */ new Map();
|
|
52213
|
+
for (const node of sized) {
|
|
52214
|
+
const capacity = node.devices.reduce((sum, d) => sum + d.sessions, 0);
|
|
52215
|
+
if (node.skippedReason === null && capacity > 0) capacityByNode.set(node.rec.nodeId, capacity);
|
|
52216
|
+
}
|
|
52217
|
+
const nodePct = toPercentShares(capacityByNode);
|
|
52218
|
+
const nodes = sized.map((node) => {
|
|
52219
|
+
const devicePct = toPercentShares(new Map(node.devices.map((d) => [d.deviceKey, d.weight])));
|
|
52220
|
+
return {
|
|
52221
|
+
nodeId: node.rec.nodeId,
|
|
52222
|
+
share: nodePct.get(node.rec.nodeId) ?? null,
|
|
52223
|
+
capacitySessions: node.devices.reduce((sum, d) => sum + d.sessions, 0),
|
|
52224
|
+
skippedReason: node.skippedReason,
|
|
52225
|
+
capacitySource: node.capacitySource,
|
|
52226
|
+
capacityFallback: node.capacityFallback,
|
|
52227
|
+
devices: node.devices.map((d) => ({
|
|
52228
|
+
deviceKey: d.deviceKey,
|
|
52229
|
+
share: devicePct.get(d.deviceKey) ?? 0,
|
|
52230
|
+
modelId: d.modelId,
|
|
52231
|
+
fps: d.fps,
|
|
52232
|
+
measured: d.measured,
|
|
52233
|
+
suggestedMaxSessions: d.sessions
|
|
52234
|
+
}))
|
|
52235
|
+
};
|
|
52236
|
+
});
|
|
52237
|
+
return {
|
|
52238
|
+
headroom: input.headroom,
|
|
52239
|
+
source: input.source,
|
|
52240
|
+
cameraFps: input.cameraFps,
|
|
52241
|
+
minSessionsPerDevice: input.minSessionsPerDevice,
|
|
52242
|
+
nodes
|
|
52243
|
+
};
|
|
52244
|
+
}
|
|
52245
|
+
/**
|
|
52159
52246
|
* THE detail-crop convention — the single derivation of the rectangle a
|
|
52160
52247
|
* detail/enrichment step (clip-embedding, face-detection, plate-detection…)
|
|
52161
52248
|
* is fed.
|
|
@@ -64627,6 +64714,7 @@ var StoredCameraPipelineForAgentSchema = object({
|
|
|
64627
64714
|
});
|
|
64628
64715
|
var StoredCameraPipelineSettingsSchema = object({
|
|
64629
64716
|
stepToggles: record(string(), boolean()).optional(),
|
|
64717
|
+
stepSettings: record(string(), record(string(), unknown()).readonly()).optional(),
|
|
64630
64718
|
stepOverridesByDevice: record(string(), record(string(), record(string(), StoredCameraStepOverridePatchSchema))).optional(),
|
|
64631
64719
|
stepOverridesByAgent: record(string(), record(string(), LegacyCameraStepOverridePatchSchema)).optional(),
|
|
64632
64720
|
pipelineByAgent: record(string(), StoredCameraPipelineForAgentSchema).optional(),
|
|
@@ -64946,6 +65034,30 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
64946
65034
|
};
|
|
64947
65035
|
});
|
|
64948
65036
|
}
|
|
65037
|
+
/**
|
|
65038
|
+
* Write or clear one camera's CAMERA-WIDE settings for `addonId` (D754): the
|
|
65039
|
+
* field-level bag merged between the node base and the per-(node,device)
|
|
65040
|
+
* override on every accelerator. `null` or an empty bag deletes the step's
|
|
65041
|
+
* entry; an empty layer is pruned. Read-patch-write, immutable.
|
|
65042
|
+
*/
|
|
65043
|
+
async writeCameraStepSettings(deviceId, addonId, settings) {
|
|
65044
|
+
await this.cameraSettingsState.update((prev) => {
|
|
65045
|
+
const key = String(deviceId);
|
|
65046
|
+
const camera = prev[key] ?? {};
|
|
65047
|
+
const nextLayer = { ...camera.stepSettings };
|
|
65048
|
+
if (settings === null || Object.keys(settings).length === 0) delete nextLayer[addonId];
|
|
65049
|
+
else nextLayer[addonId] = { ...settings };
|
|
65050
|
+
const { stepSettings: _drop, ...rest } = camera;
|
|
65051
|
+
const nextCamera = {
|
|
65052
|
+
...rest,
|
|
65053
|
+
...Object.keys(nextLayer).length > 0 ? { stepSettings: nextLayer } : {}
|
|
65054
|
+
};
|
|
65055
|
+
return {
|
|
65056
|
+
...prev,
|
|
65057
|
+
[key]: nextCamera
|
|
65058
|
+
};
|
|
65059
|
+
});
|
|
65060
|
+
}
|
|
64949
65061
|
/** Purge one device's entry through the durable handle (whole-blob round-trip). */
|
|
64950
65062
|
async deleteCameraSettings(deviceId) {
|
|
64951
65063
|
await this.cameraSettingsState.update((prev) => {
|
|
@@ -65512,13 +65624,14 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
65512
65624
|
async readDeviceProvisioning(deviceId, nodeId, deviceKeys) {
|
|
65513
65625
|
const [agentBlob, cameraMap] = await Promise.all([this.agentSettingsState.get(), this.readCameraSettingsMap()]);
|
|
65514
65626
|
const devices = agentBlob[nodeId]?.inferenceDevices;
|
|
65515
|
-
const
|
|
65627
|
+
const camera = cameraMap[String(deviceId)];
|
|
65628
|
+
const byDevice = camera?.stepOverridesByDevice?.[nodeId];
|
|
65516
65629
|
const out = /* @__PURE__ */ new Map();
|
|
65517
65630
|
for (const deviceKey of deviceKeys) {
|
|
65518
|
-
const
|
|
65631
|
+
const perDevice = deviceKey === "" ? void 0 : byDevice?.[deviceKey];
|
|
65519
65632
|
out.set(deviceKey, {
|
|
65520
65633
|
base: foldNodeStepSettings(devices, deviceKey),
|
|
65521
|
-
override:
|
|
65634
|
+
override: layerCameraStepSettings(camera?.stepSettings, perDevice)
|
|
65522
65635
|
});
|
|
65523
65636
|
}
|
|
65524
65637
|
return out;
|
|
@@ -65582,6 +65695,30 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
65582
65695
|
return `tpl_${Date.now().toString(36).slice(-6)}${rand}`;
|
|
65583
65696
|
}
|
|
65584
65697
|
};
|
|
65698
|
+
/**
|
|
65699
|
+
* The camera's override for one accelerator, with its CAMERA-WIDE settings
|
|
65700
|
+
* (D754) folded underneath: per step, `settings = { ...camera, ...perDevice }`
|
|
65701
|
+
* (a per-accelerator key wins), and the per-accelerator `modelId` kept as is —
|
|
65702
|
+
* the camera layer carries no model. The result is what
|
|
65703
|
+
* `applyDeviceProvisioning` merges over the node base, so the full order is
|
|
65704
|
+
* node ⊕ camera ⊕ camera-per-accelerator. Fresh objects; a camera with neither
|
|
65705
|
+
* layer yields `{}`.
|
|
65706
|
+
*/
|
|
65707
|
+
function layerCameraStepSettings(cameraWide, perDevice) {
|
|
65708
|
+
const out = { ...perDevice };
|
|
65709
|
+
for (const [stepId, settings] of Object.entries(cameraWide ?? {})) {
|
|
65710
|
+
if (Object.keys(settings).length === 0) continue;
|
|
65711
|
+
const own = out[stepId];
|
|
65712
|
+
out[stepId] = {
|
|
65713
|
+
...own,
|
|
65714
|
+
settings: {
|
|
65715
|
+
...settings,
|
|
65716
|
+
...own?.settings
|
|
65717
|
+
}
|
|
65718
|
+
};
|
|
65719
|
+
}
|
|
65720
|
+
return out;
|
|
65721
|
+
}
|
|
65585
65722
|
//#endregion
|
|
65586
65723
|
//#region src/pipeline-watchdog.ts
|
|
65587
65724
|
var DEFAULT_WATCHDOG_THRESHOLDS = {
|
|
@@ -69204,7 +69341,6 @@ async function buildOrchestratorControllers(deps) {
|
|
|
69204
69341
|
readGlobalSettings: () => globalSettings,
|
|
69205
69342
|
onCensus: (entries) => {
|
|
69206
69343
|
motionSliceGate?.noteCensus(entries);
|
|
69207
|
-
deps.onRunnerCensus(entries.flatMap((e) => e.load === null ? [] : [e.load]));
|
|
69208
69344
|
providerSliceGate?.refreshIfStale("runner-census");
|
|
69209
69345
|
}
|
|
69210
69346
|
});
|
|
@@ -70804,11 +70940,13 @@ function applyCameraAutoTune(blob, input) {
|
|
|
70804
70940
|
//#endregion
|
|
70805
70941
|
//#region src/auto-tune/auto-tune-service.ts
|
|
70806
70942
|
/**
|
|
70807
|
-
* Auto tune (D745): gather the inputs, compute the plan, and apply
|
|
70808
|
-
* the plan the operator confirmed.
|
|
70943
|
+
* Auto tune (D745, D755): gather the inputs, compute the plan, and apply
|
|
70944
|
+
* exactly the plan the operator confirmed.
|
|
70809
70945
|
*
|
|
70810
70946
|
* The plan is the recommender (`@camstack/types` auto-tune) over every
|
|
70811
|
-
* runner node
|
|
70947
|
+
* runner node — sized by a capacity floor (`minSessionsPerDevice` parallel
|
|
70948
|
+
* sessions per device at the camera rate), never by observed or estimated
|
|
70949
|
+
* load — plus the diff of the writes (`plan-diff.ts`) and the
|
|
70812
70950
|
* per-camera detector thresholds the hub's executor would clear. Its
|
|
70813
70951
|
* `planHash` names everything the apply writes; the apply recomputes the plan
|
|
70814
70952
|
* and refuses a different hash, so a configuration change between the two is
|
|
@@ -70825,6 +70963,9 @@ function applyCameraAutoTune(blob, input) {
|
|
|
70825
70963
|
* A failure after phase 2 leaves "overrides cleared, previous node model",
|
|
70826
70964
|
* which is a valid configuration (the defaults run); re-running converges.
|
|
70827
70965
|
*/
|
|
70966
|
+
function minSessionsOf(input) {
|
|
70967
|
+
return input.minSessionsPerDevice ?? 2;
|
|
70968
|
+
}
|
|
70828
70969
|
function errMsg(err) {
|
|
70829
70970
|
return err instanceof Error ? err.message : String(err);
|
|
70830
70971
|
}
|
|
@@ -70853,29 +70994,6 @@ function boundKeysOf(catalogs) {
|
|
|
70853
70994
|
}
|
|
70854
70995
|
return Object.fromEntries([...byStep].map(([step, set]) => [step, [...set].toSorted()]));
|
|
70855
70996
|
}
|
|
70856
|
-
/** Bounded fixed point of the hardware share suggestion (D748). */
|
|
70857
|
-
var SUGGEST_MAX_ROUNDS = 3;
|
|
70858
|
-
/** Integer shares summing to 100 (largest remainder). */
|
|
70859
|
-
function toPercent(weights) {
|
|
70860
|
-
const total = [...weights.values()].reduce((a, b) => a + b, 0);
|
|
70861
|
-
const out = /* @__PURE__ */ new Map();
|
|
70862
|
-
if (total <= 0) return out;
|
|
70863
|
-
const raw = [...weights].map(([k, w]) => ({
|
|
70864
|
-
k,
|
|
70865
|
-
exact: w / total * 100
|
|
70866
|
-
}));
|
|
70867
|
-
let assigned = 0;
|
|
70868
|
-
for (const r of raw) {
|
|
70869
|
-
out.set(r.k, Math.floor(r.exact));
|
|
70870
|
-
assigned += Math.floor(r.exact);
|
|
70871
|
-
}
|
|
70872
|
-
const byRemainder = raw.toSorted((a, b) => b.exact - Math.floor(b.exact) - (a.exact - Math.floor(a.exact)) || a.k.localeCompare(b.k));
|
|
70873
|
-
for (let i = 0; i < 100 - assigned; i++) {
|
|
70874
|
-
const r = byRemainder[i % byRemainder.length];
|
|
70875
|
-
out.set(r.k, (out.get(r.k) ?? 0) + 1);
|
|
70876
|
-
}
|
|
70877
|
-
return out;
|
|
70878
|
-
}
|
|
70879
70997
|
var AutoTuneService = class {
|
|
70880
70998
|
deps;
|
|
70881
70999
|
applying = false;
|
|
@@ -70883,87 +71001,38 @@ var AutoTuneService = class {
|
|
|
70883
71001
|
this.deps = deps;
|
|
70884
71002
|
}
|
|
70885
71003
|
async plan(input) {
|
|
70886
|
-
return (await this.computePlan(input.headroom, input
|
|
71004
|
+
return (await this.computePlan(input.headroom, minSessionsOf(input))).plan;
|
|
70887
71005
|
}
|
|
70888
71006
|
/**
|
|
70889
|
-
* Shares proportional to each node's capacity
|
|
70890
|
-
*
|
|
70891
|
-
*
|
|
70892
|
-
*
|
|
70893
|
-
*
|
|
71007
|
+
* Shares proportional to each node's capacity in parallel sessions (D755):
|
|
71008
|
+
* from the latency table at the models Auto tune picks at its defaults
|
|
71009
|
+
* (`hardware`), or from node benchmark results (`benchmark`), a node
|
|
71010
|
+
* without results falling back to `hardware` and saying so. ONE pass, and
|
|
71011
|
+
* nothing but capacity goes in — the automatic shares are exactly this.
|
|
71012
|
+
* Per device it reports an ADVISORY session cap — never written, never
|
|
71013
|
+
* enforced.
|
|
70894
71014
|
*/
|
|
70895
71015
|
async suggestShares(input) {
|
|
70896
|
-
|
|
70897
|
-
|
|
70898
|
-
|
|
70899
|
-
|
|
70900
|
-
|
|
70901
|
-
|
|
70902
|
-
|
|
70903
|
-
|
|
70904
|
-
rounds = round;
|
|
70905
|
-
const rec = await this.recommend(input.headroom, shares);
|
|
70906
|
-
cameraFps = rec.cameraFps;
|
|
70907
|
-
result = this.sharesFrom(rec, input.headroom);
|
|
70908
|
-
const next = {
|
|
70909
|
-
nodes: Object.fromEntries(result.flatMap((n) => n.share !== null && n.share > 0 ? [[n.nodeId, n.share]] : [])),
|
|
70910
|
-
devices: Object.fromEntries(result.map((n) => [n.nodeId, Object.fromEntries(n.devices.flatMap((d) => d.share > 0 ? [[d.deviceKey, d.share]] : []))]))
|
|
70911
|
-
};
|
|
70912
|
-
const settled = JSON.stringify(next) === JSON.stringify(shares);
|
|
70913
|
-
shares = next;
|
|
70914
|
-
if (settled) break;
|
|
71016
|
+
const source = input.source ?? "hardware";
|
|
71017
|
+
const minSessionsPerDevice = 2;
|
|
71018
|
+
const rec = await this.recommend(input.headroom, minSessionsPerDevice);
|
|
71019
|
+
let measured = /* @__PURE__ */ new Map();
|
|
71020
|
+
if (source === "benchmark") try {
|
|
71021
|
+
measured = await this.deps.measuredCapacity.readMeasuredCapacity(rec.recommendations.map((r) => r.nodeId));
|
|
71022
|
+
} catch (err) {
|
|
71023
|
+
this.deps.logger.warn("load balancing: benchmark results unreadable — sized from hardware", { meta: { error: errMsg(err) } });
|
|
70915
71024
|
}
|
|
70916
|
-
return {
|
|
71025
|
+
return computeCapacityShares({
|
|
71026
|
+
recommendations: rec.recommendations,
|
|
71027
|
+
source,
|
|
71028
|
+
measured,
|
|
70917
71029
|
headroom: input.headroom,
|
|
70918
|
-
cameraFps,
|
|
70919
|
-
|
|
70920
|
-
nodes: [...result]
|
|
70921
|
-
};
|
|
70922
|
-
}
|
|
70923
|
-
sharesFrom(rec, headroom) {
|
|
70924
|
-
const capacityByNode = /* @__PURE__ */ new Map();
|
|
70925
|
-
const perNode = rec.recommendations.map((r) => {
|
|
70926
|
-
const devices = r.devices.filter((d) => d.enabled && d.available && d.chosen !== null).map((d) => {
|
|
70927
|
-
const fps = d.chosen?.fps ?? 0;
|
|
70928
|
-
return {
|
|
70929
|
-
d,
|
|
70930
|
-
fps,
|
|
70931
|
-
sessions: rec.cameraFps > 0 ? Math.floor(fps * (1 - headroom) / rec.cameraFps) : 0
|
|
70932
|
-
};
|
|
70933
|
-
});
|
|
70934
|
-
const capacity = devices.reduce((sum, x) => sum + x.sessions, 0);
|
|
70935
|
-
if (!r.skipped && capacity > 0) capacityByNode.set(r.nodeId, capacity);
|
|
70936
|
-
return {
|
|
70937
|
-
r,
|
|
70938
|
-
devices,
|
|
70939
|
-
capacity
|
|
70940
|
-
};
|
|
70941
|
-
});
|
|
70942
|
-
const nodePct = toPercent(capacityByNode);
|
|
70943
|
-
return perNode.map(({ r, devices, capacity }) => {
|
|
70944
|
-
const devicePct = toPercent(new Map(devices.map((x) => [x.d.deviceKey, Math.max(x.fps, 0)])));
|
|
70945
|
-
return {
|
|
70946
|
-
nodeId: r.nodeId,
|
|
70947
|
-
share: nodePct.get(r.nodeId) ?? null,
|
|
70948
|
-
capacitySessions: capacity,
|
|
70949
|
-
skippedReason: r.skipped ? r.reasons[0]?.kind ?? "skipped" : capacity > 0 ? null : "no-capacity",
|
|
70950
|
-
devices: devices.map((x) => ({
|
|
70951
|
-
deviceKey: x.d.deviceKey,
|
|
70952
|
-
share: devicePct.get(x.d.deviceKey) ?? 0,
|
|
70953
|
-
modelId: x.d.chosen?.modelId ?? null,
|
|
70954
|
-
fps: x.d.chosen?.fps ?? null,
|
|
70955
|
-
measured: x.d.chosen?.measured ?? false,
|
|
70956
|
-
suggestedMaxSessions: x.sessions
|
|
70957
|
-
}))
|
|
70958
|
-
};
|
|
71030
|
+
cameraFps: rec.cameraFps,
|
|
71031
|
+
minSessionsPerDevice
|
|
70959
71032
|
});
|
|
70960
71033
|
}
|
|
70961
|
-
/**
|
|
70962
|
-
|
|
70963
|
-
* round) forces share placement with those shares overlaid on the stored
|
|
70964
|
-
* ones; absent, the STORED policy decides how on-motion load is charged.
|
|
70965
|
-
*/
|
|
70966
|
-
async recommend(headroom, shares) {
|
|
71034
|
+
/** Gather every node and recommend. Nothing but hardware, catalog and the sizing goes in. */
|
|
71035
|
+
async recommend(headroom, minSessionsPerDevice) {
|
|
70967
71036
|
const nodeIds = [...new Set(["hub", ...this.deps.runnerNodeIds()])].toSorted();
|
|
70968
71037
|
const gathered = await Promise.all(nodeIds.map(async (nodeId) => {
|
|
70969
71038
|
const [devices, hardware, catalog] = await Promise.all([
|
|
@@ -70978,8 +71047,6 @@ var AutoTuneService = class {
|
|
|
70978
71047
|
catalog
|
|
70979
71048
|
};
|
|
70980
71049
|
}));
|
|
70981
|
-
const effective = await this.deps.readEffectiveWeights();
|
|
70982
|
-
const deviceShare = (nodeId, key, stored) => shares?.devices[nodeId]?.[key] ?? effective.devices[nodeId]?.[key] ?? stored;
|
|
70983
71050
|
const nodes = gathered.map((g) => ({
|
|
70984
71051
|
nodeId: g.nodeId,
|
|
70985
71052
|
reachable: g.devices.reachable && g.catalog !== null,
|
|
@@ -70992,40 +71059,22 @@ var AutoTuneService = class {
|
|
|
70992
71059
|
})),
|
|
70993
71060
|
formatsByModel: formatsByModelOf(g.catalog)
|
|
70994
71061
|
}));
|
|
70995
|
-
const
|
|
70996
|
-
|
|
70997
|
-
deviceKey: d.key,
|
|
70998
|
-
weight: deviceShare(g.nodeId, d.key, d.weight)
|
|
70999
|
-
})) : []);
|
|
71000
|
-
const cameras = await this.deps.cameraLoads();
|
|
71001
|
-
const policy = shares ? "share" : this.deps.sessionPlacement();
|
|
71002
|
-
const demand = estimateDeviceDemand({
|
|
71003
|
-
cameras,
|
|
71004
|
-
eligible,
|
|
71005
|
-
peaks: this.deps.peaks(),
|
|
71006
|
-
...policy === "share" ? { shares: {
|
|
71007
|
-
nodes: {
|
|
71008
|
-
...effective.nodes,
|
|
71009
|
-
...shares?.nodes
|
|
71010
|
-
},
|
|
71011
|
-
clusterPeak: this.deps.clusterPeak()
|
|
71012
|
-
} } : {}
|
|
71013
|
-
});
|
|
71014
|
-
const recommendations = nodes.map((node) => recommendNode({
|
|
71015
|
-
node,
|
|
71016
|
-
demand,
|
|
71017
|
-
headroom
|
|
71018
|
-
}));
|
|
71019
|
-
const cameraFps = cameras.reduce((m, c) => c.mode !== "disabled" && c.runsPipelineRoot ? Math.max(m, c.fps) : m, 0);
|
|
71062
|
+
const observedFps = await this.deps.cameraFps();
|
|
71063
|
+
const cameraFps = observedFps !== null && observedFps > 0 ? observedFps : 10;
|
|
71020
71064
|
return {
|
|
71021
71065
|
gathered,
|
|
71022
71066
|
nodes,
|
|
71023
|
-
recommendations
|
|
71024
|
-
|
|
71067
|
+
recommendations: nodes.map((node) => recommendNode({
|
|
71068
|
+
node,
|
|
71069
|
+
headroom,
|
|
71070
|
+
minSessionsPerDevice,
|
|
71071
|
+
cameraFps
|
|
71072
|
+
})),
|
|
71073
|
+
cameraFps
|
|
71025
71074
|
};
|
|
71026
71075
|
}
|
|
71027
|
-
async computePlan(headroom,
|
|
71028
|
-
const { gathered, nodes, recommendations } = await this.recommend(headroom,
|
|
71076
|
+
async computePlan(headroom, minSessionsPerDevice) {
|
|
71077
|
+
const { gathered, nodes, recommendations, cameraFps } = await this.recommend(headroom, minSessionsPerDevice);
|
|
71029
71078
|
const touchedNodes = new Set(recommendations.filter((r) => !r.skipped).map((r) => r.nodeId));
|
|
71030
71079
|
const targets = /* @__PURE__ */ new Map();
|
|
71031
71080
|
const formats = /* @__PURE__ */ new Map();
|
|
@@ -71085,9 +71134,10 @@ var AutoTuneService = class {
|
|
|
71085
71134
|
return {
|
|
71086
71135
|
plan: {
|
|
71087
71136
|
planHash: (0, node_crypto.createHash)("sha256").update(canonical({
|
|
71088
|
-
version:
|
|
71137
|
+
version: 2,
|
|
71089
71138
|
headroom,
|
|
71090
|
-
|
|
71139
|
+
minSessionsPerDevice,
|
|
71140
|
+
cameraFps,
|
|
71091
71141
|
picks: recommendations.map((r) => ({
|
|
71092
71142
|
nodeId: r.nodeId,
|
|
71093
71143
|
skipped: r.skipped,
|
|
@@ -71099,7 +71149,8 @@ var AutoTuneService = class {
|
|
|
71099
71149
|
})).digest("hex"),
|
|
71100
71150
|
family: "yolov9",
|
|
71101
71151
|
headroom,
|
|
71102
|
-
|
|
71152
|
+
minSessionsPerDevice,
|
|
71153
|
+
cameraFps,
|
|
71103
71154
|
generatedAt: this.deps.now(),
|
|
71104
71155
|
nodes: recommendations,
|
|
71105
71156
|
nodeChanges: [...nodeChanges],
|
|
@@ -71124,7 +71175,7 @@ var AutoTuneService = class {
|
|
|
71124
71175
|
}
|
|
71125
71176
|
}
|
|
71126
71177
|
async applyLocked(input) {
|
|
71127
|
-
const { plan, diffInput, formats } = await this.computePlan(input.headroom);
|
|
71178
|
+
const { plan, diffInput, formats } = await this.computePlan(input.headroom, minSessionsOf(input));
|
|
71128
71179
|
if (plan.planHash !== input.planHash) {
|
|
71129
71180
|
this.deps.logger.warn("auto tune: stale plan refused", { meta: {
|
|
71130
71181
|
confirmed: input.planHash,
|
|
@@ -71267,83 +71318,6 @@ var AutoTuneService = class {
|
|
|
71267
71318
|
}
|
|
71268
71319
|
};
|
|
71269
71320
|
//#endregion
|
|
71270
|
-
//#region src/auto-tune/device-peak-tracker.ts
|
|
71271
|
-
var BUCKET_MS = 3600 * 1e3;
|
|
71272
|
-
var DevicePeakTracker = class DevicePeakTracker {
|
|
71273
|
-
now;
|
|
71274
|
-
series = /* @__PURE__ */ new Map();
|
|
71275
|
-
cluster = null;
|
|
71276
|
-
constructor(now = () => Date.now()) {
|
|
71277
|
-
this.now = now;
|
|
71278
|
-
}
|
|
71279
|
-
static key(nodeId, deviceKey) {
|
|
71280
|
-
return `${nodeId}\u0000${deviceKey}`;
|
|
71281
|
-
}
|
|
71282
|
-
/** Fold one runner census in. A runner that did not answer is simply absent. */
|
|
71283
|
-
noteCensus(loads) {
|
|
71284
|
-
const now = this.now();
|
|
71285
|
-
const bucket = now - now % BUCKET_MS;
|
|
71286
|
-
let clusterTotal = 0;
|
|
71287
|
-
for (const load of loads) {
|
|
71288
|
-
const rows = load.devices.length > 0 ? load.devices : [{
|
|
71289
|
-
deviceKey: "",
|
|
71290
|
-
attachedCameras: load.attachedCameras
|
|
71291
|
-
}];
|
|
71292
|
-
for (const device of rows) {
|
|
71293
|
-
clusterTotal += device.attachedCameras;
|
|
71294
|
-
const key = DevicePeakTracker.key(load.nodeId, device.deviceKey);
|
|
71295
|
-
let s = this.series.get(key);
|
|
71296
|
-
if (s === void 0) {
|
|
71297
|
-
s = {
|
|
71298
|
-
firstSeenAt: now,
|
|
71299
|
-
buckets: /* @__PURE__ */ new Map()
|
|
71300
|
-
};
|
|
71301
|
-
this.series.set(key, s);
|
|
71302
|
-
}
|
|
71303
|
-
s.buckets.set(bucket, Math.max(s.buckets.get(bucket) ?? 0, device.attachedCameras));
|
|
71304
|
-
for (const start of s.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) s.buckets.delete(start);
|
|
71305
|
-
}
|
|
71306
|
-
}
|
|
71307
|
-
if (loads.length === 0) return;
|
|
71308
|
-
const c = this.cluster ?? {
|
|
71309
|
-
firstSeenAt: now,
|
|
71310
|
-
buckets: /* @__PURE__ */ new Map()
|
|
71311
|
-
};
|
|
71312
|
-
this.cluster = c;
|
|
71313
|
-
c.buckets.set(bucket, Math.max(c.buckets.get(bucket) ?? 0, clusterTotal));
|
|
71314
|
-
for (const start of c.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) c.buckets.delete(start);
|
|
71315
|
-
}
|
|
71316
|
-
/** The cluster-wide parallel peak over the window, or null before any census. */
|
|
71317
|
-
clusterPeak() {
|
|
71318
|
-
const c = this.cluster;
|
|
71319
|
-
if (c === null) return null;
|
|
71320
|
-
const now = this.now();
|
|
71321
|
-
let peak = 0;
|
|
71322
|
-
for (const [start, value] of c.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
|
|
71323
|
-
return {
|
|
71324
|
-
peakSessions: peak,
|
|
71325
|
-
observedForMs: now - c.firstSeenAt
|
|
71326
|
-
};
|
|
71327
|
-
}
|
|
71328
|
-
/** Every observed device's peak over the window and how long it has been watched. */
|
|
71329
|
-
peaks() {
|
|
71330
|
-
const now = this.now();
|
|
71331
|
-
const out = [];
|
|
71332
|
-
for (const [key, s] of this.series) {
|
|
71333
|
-
const [nodeId = "", deviceKey = ""] = key.split("\0");
|
|
71334
|
-
let peak = 0;
|
|
71335
|
-
for (const [start, value] of s.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
|
|
71336
|
-
out.push({
|
|
71337
|
-
nodeId,
|
|
71338
|
-
deviceKey,
|
|
71339
|
-
peakSessions: peak,
|
|
71340
|
-
observedForMs: now - s.firstSeenAt
|
|
71341
|
-
});
|
|
71342
|
-
}
|
|
71343
|
-
return out.toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
|
|
71344
|
-
}
|
|
71345
|
-
};
|
|
71346
|
-
//#endregion
|
|
71347
71321
|
//#region src/session-placement-stats.ts
|
|
71348
71322
|
var RETAIN_MS = 1440 * 60 * 1e3;
|
|
71349
71323
|
function keyOf(nodeId, deviceKey) {
|
|
@@ -71510,6 +71484,16 @@ function applyLoadBalancingShares(prev, write) {
|
|
|
71510
71484
|
}
|
|
71511
71485
|
//#endregion
|
|
71512
71486
|
//#region src/load-balancing/effective-shares-service.ts
|
|
71487
|
+
/** Skips that mean "not readable yet", never "cannot take sessions". */
|
|
71488
|
+
var NOT_READY_REASONS = new Set(["node-offline", "no-hardware-facts"]);
|
|
71489
|
+
function capacityFrom(result) {
|
|
71490
|
+
const out = {};
|
|
71491
|
+
for (const node of result.nodes) if (node.capacitySource !== null) out[node.nodeId] = {
|
|
71492
|
+
source: node.capacitySource,
|
|
71493
|
+
fallback: node.capacityFallback
|
|
71494
|
+
};
|
|
71495
|
+
return out;
|
|
71496
|
+
}
|
|
71513
71497
|
function weightsFrom(result) {
|
|
71514
71498
|
const nodes = {};
|
|
71515
71499
|
const devices = {};
|
|
@@ -71540,19 +71524,27 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71540
71524
|
now() {
|
|
71541
71525
|
return this.deps.now?.() ?? Date.now();
|
|
71542
71526
|
}
|
|
71543
|
-
/** The last automatic answer; null =
|
|
71527
|
+
/** The last automatic answer for the CURRENT source; null = none yet. */
|
|
71544
71528
|
current() {
|
|
71545
|
-
return this.
|
|
71529
|
+
return this.usable();
|
|
71530
|
+
}
|
|
71531
|
+
/** The answer, when the source is automatic and the answer was computed for it. */
|
|
71532
|
+
usable() {
|
|
71533
|
+
const source = this.deps.readSource();
|
|
71534
|
+
if (!isAutomaticShareSource(source)) return null;
|
|
71535
|
+
return this.answer !== null && this.answer.source === source ? this.answer : null;
|
|
71546
71536
|
}
|
|
71547
71537
|
/** Node weights the election reads. */
|
|
71548
71538
|
async nodeWeights() {
|
|
71549
|
-
|
|
71539
|
+
const answer = this.usable();
|
|
71540
|
+
if (answer !== null) return { ...answer.weights.nodes };
|
|
71550
71541
|
return { ...(await this.deps.readStored()).nodes };
|
|
71551
71542
|
}
|
|
71552
71543
|
/** The weights of a node's ELIGIBLE devices (`eligible` carries their stored weights). */
|
|
71553
71544
|
deviceWeights(nodeId, eligible) {
|
|
71554
|
-
|
|
71555
|
-
|
|
71545
|
+
const answer = this.usable();
|
|
71546
|
+
if (answer === null) return { ...eligible };
|
|
71547
|
+
const auto = answer.weights.devices[nodeId] ?? {};
|
|
71556
71548
|
const out = {};
|
|
71557
71549
|
for (const [key, stored] of Object.entries(eligible)) {
|
|
71558
71550
|
const share = auto[key];
|
|
@@ -71562,15 +71554,14 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71562
71554
|
}
|
|
71563
71555
|
/** Both maps, as the Auto tune recommender charges load by them. */
|
|
71564
71556
|
async effectiveWeights() {
|
|
71565
|
-
|
|
71566
|
-
return this.deps.readStored();
|
|
71557
|
+
return this.usable()?.weights ?? this.deps.readStored();
|
|
71567
71558
|
}
|
|
71568
71559
|
/**
|
|
71569
|
-
* Recompute now (
|
|
71570
|
-
* trailing run picks up whatever was reported meanwhile.
|
|
71560
|
+
* Recompute now (automatic sources only). Concurrent calls coalesce: one
|
|
71561
|
+
* runs, and one trailing run picks up whatever was reported meanwhile.
|
|
71571
71562
|
*/
|
|
71572
71563
|
async recompute(trigger) {
|
|
71573
|
-
if (this.deps.readSource()
|
|
71564
|
+
if (!isAutomaticShareSource(this.deps.readSource())) return;
|
|
71574
71565
|
if (this.inFlight !== null) {
|
|
71575
71566
|
this.pendingTrigger = trigger;
|
|
71576
71567
|
return this.inFlight;
|
|
@@ -71586,11 +71577,13 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71586
71577
|
}
|
|
71587
71578
|
}
|
|
71588
71579
|
async run(trigger) {
|
|
71580
|
+
const source = this.deps.readSource();
|
|
71581
|
+
if (!isAutomaticShareSource(source)) return;
|
|
71589
71582
|
let signature;
|
|
71590
71583
|
let result;
|
|
71591
71584
|
try {
|
|
71592
71585
|
signature = await this.deps.topologySignature();
|
|
71593
|
-
result = await this.deps.suggest();
|
|
71586
|
+
result = await this.deps.suggest(source);
|
|
71594
71587
|
} catch (err) {
|
|
71595
71588
|
this.deps.logger.warn("load balancing: automatic shares not recomputed — last answer kept", { meta: {
|
|
71596
71589
|
trigger,
|
|
@@ -71601,17 +71594,27 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71601
71594
|
}
|
|
71602
71595
|
const before = this.answer?.weights ?? null;
|
|
71603
71596
|
const after = weightsFrom(result);
|
|
71597
|
+
const notReady = result.nodes.filter((n) => n.share === null && NOT_READY_REASONS.has(n.skippedReason ?? "")).map((n) => n.nodeId).toSorted();
|
|
71598
|
+
const capacity = capacityFrom(result);
|
|
71604
71599
|
this.answer = {
|
|
71605
71600
|
weights: after,
|
|
71601
|
+
capacity,
|
|
71602
|
+
source,
|
|
71606
71603
|
computedAt: this.now(),
|
|
71607
71604
|
trigger,
|
|
71608
|
-
signature
|
|
71605
|
+
signature,
|
|
71606
|
+
notReady
|
|
71609
71607
|
};
|
|
71610
71608
|
const nodeIds = [...new Set([...Object.keys(before?.nodes ?? {}), ...result.nodes.map((n) => n.nodeId)])];
|
|
71611
|
-
|
|
71609
|
+
const log = before !== null && JSON.stringify(before) === JSON.stringify(after) && trigger.startsWith("reconcile:not-ready") ? this.deps.logger.debug.bind(this.deps.logger) : this.deps.logger.info.bind(this.deps.logger);
|
|
71610
|
+
for (const nodeId of nodeIds.toSorted()) log("load balancing: automatic shares recomputed", {
|
|
71612
71611
|
tags: { nodeId },
|
|
71613
71612
|
meta: {
|
|
71614
71613
|
trigger,
|
|
71614
|
+
source,
|
|
71615
|
+
capacitySource: capacity[nodeId]?.source ?? null,
|
|
71616
|
+
capacityFallback: capacity[nodeId]?.fallback ?? null,
|
|
71617
|
+
notReady: notReady.includes(nodeId),
|
|
71615
71618
|
before: before?.nodes[nodeId] ?? null,
|
|
71616
71619
|
after: after.nodes[nodeId] ?? null,
|
|
71617
71620
|
devicesBefore: JSON.stringify(before?.devices[nodeId] ?? {}),
|
|
@@ -71619,10 +71622,17 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71619
71622
|
}
|
|
71620
71623
|
});
|
|
71621
71624
|
}
|
|
71622
|
-
/**
|
|
71625
|
+
/**
|
|
71626
|
+
* Recompute if there is no answer for the current source, a node was not
|
|
71627
|
+
* readable yet, the topology changed or the answer has grown old
|
|
71628
|
+
* (automatic sources only).
|
|
71629
|
+
*/
|
|
71623
71630
|
async reconcile() {
|
|
71624
|
-
|
|
71631
|
+
const source = this.deps.readSource();
|
|
71632
|
+
if (!isAutomaticShareSource(source)) return;
|
|
71625
71633
|
if (this.answer === null) return this.recompute("reconcile:first");
|
|
71634
|
+
if (this.answer.source !== source) return this.recompute(`reconcile:source:${source}`);
|
|
71635
|
+
if (this.answer.notReady.length > 0) return this.recompute(`reconcile:not-ready:${this.answer.notReady.join(",")}`);
|
|
71626
71636
|
if (this.now() - this.answer.computedAt > EffectiveSharesService.MAX_AGE_MS) return this.recompute("reconcile:age");
|
|
71627
71637
|
let signature;
|
|
71628
71638
|
try {
|
|
@@ -71762,7 +71772,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
71762
71772
|
*/
|
|
71763
71773
|
clusterModelChoiceReady = false;
|
|
71764
71774
|
/** The per-device 24 h peak of concurrent sessions, fed by every runner census (D745). */
|
|
71765
|
-
devicePeaks = new DevicePeakTracker();
|
|
71766
71775
|
/** What session placement did (D748): the Load balancing tab's "actual" column. */
|
|
71767
71776
|
placementStats = new SessionPlacementStats();
|
|
71768
71777
|
/** The stored session placement policy (D748, default `share` since D750). */
|
|
@@ -72048,7 +72057,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
72048
72057
|
rebalance: (opts) => this.rebalance(opts),
|
|
72049
72058
|
setCameraPipelineForAgent: (input) => this.setCameraPipelineForAgent(input),
|
|
72050
72059
|
maintenancePaused: () => this.storageMigrationLeaseId !== null,
|
|
72051
|
-
onRunnerCensus: (loads) => this.devicePeaks.noteCensus(loads),
|
|
72052
72060
|
placementStats: this.placementStats,
|
|
72053
72061
|
setTopology: (topology) => {
|
|
72054
72062
|
this.topology = topology;
|
|
@@ -73171,11 +73179,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73171
73179
|
readCameraSettings: () => store.readCameraSettingsMap(),
|
|
73172
73180
|
rewriteAgentSettings: (transform) => store.rewriteAgentSettingsMap(transform),
|
|
73173
73181
|
rewriteCameraSettings: (transform) => store.rewriteCameraSettingsMap(transform),
|
|
73174
|
-
|
|
73175
|
-
|
|
73176
|
-
clusterPeak: () => this.devicePeaks.clusterPeak(),
|
|
73177
|
-
sessionPlacement: () => this.readSessionPlacement(),
|
|
73178
|
-
readEffectiveWeights: () => this.effectiveShares().effectiveWeights(),
|
|
73182
|
+
cameraFps: async () => this.autoTuneCameraFps(),
|
|
73183
|
+
measuredCapacity: NO_MEASURED_CAPACITY,
|
|
73179
73184
|
detectorStore: async (input) => {
|
|
73180
73185
|
const api = this.ctx.api;
|
|
73181
73186
|
if (!api) throw new Error("ctx.api unavailable");
|
|
@@ -73200,28 +73205,18 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73200
73205
|
});
|
|
73201
73206
|
return this.autoTuneService;
|
|
73202
73207
|
}
|
|
73203
|
-
/**
|
|
73204
|
-
|
|
73205
|
-
|
|
73206
|
-
|
|
73207
|
-
|
|
73208
|
-
|
|
73209
|
-
|
|
73210
|
-
|
|
73211
|
-
|
|
73212
|
-
|
|
73213
|
-
mode: config.detectionMode,
|
|
73214
|
-
fps: config.detectionFps,
|
|
73215
|
-
runsPipelineRoot: (config.detectionSources ?? ["pipeline"]).includes("pipeline"),
|
|
73216
|
-
standingNodeId: standingNode,
|
|
73217
|
-
nodePinId: nodePin,
|
|
73218
|
-
devicePin: pinnedKey !== null ? {
|
|
73219
|
-
nodeId: nodePin ?? standingNode,
|
|
73220
|
-
deviceKey: pinnedKey
|
|
73221
|
-
} : null
|
|
73222
|
-
});
|
|
73208
|
+
/**
|
|
73209
|
+
* The rate one detection session is counted at (D755): the fastest camera
|
|
73210
|
+
* with detection started that runs the pipeline root; null when none does.
|
|
73211
|
+
*/
|
|
73212
|
+
autoTuneCameraFps() {
|
|
73213
|
+
let fps = 0;
|
|
73214
|
+
for (const config of this.detectionWiring.activeDetections.values()) {
|
|
73215
|
+
if (config.detectionMode === "disabled") continue;
|
|
73216
|
+
if (!(config.detectionSources ?? ["pipeline"]).includes("pipeline")) continue;
|
|
73217
|
+
fps = Math.max(fps, config.detectionFps);
|
|
73223
73218
|
}
|
|
73224
|
-
return
|
|
73219
|
+
return fps > 0 ? fps : null;
|
|
73225
73220
|
}
|
|
73226
73221
|
async planAutoTune(input) {
|
|
73227
73222
|
return this.autoTune().plan(input);
|
|
@@ -73293,7 +73288,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73293
73288
|
this.effectiveSharesService = new EffectiveSharesService({
|
|
73294
73289
|
readSource: () => this.readShareSource(),
|
|
73295
73290
|
readStored: async () => storedShareWeights(await store.readAgentSettingsMap()),
|
|
73296
|
-
suggest: () => this.autoTune().suggestShares({
|
|
73291
|
+
suggest: (source) => this.autoTune().suggestShares({
|
|
73292
|
+
headroom: AUTO_SHARE_HEADROOM,
|
|
73293
|
+
source
|
|
73294
|
+
}),
|
|
73297
73295
|
topologySignature: async () => shareTopologySignature([...new Set(["hub", ...this.topology.collectRunnerNodeIds()])], await store.readAgentSettingsMap()),
|
|
73298
73296
|
logger: this.ctx.logger
|
|
73299
73297
|
});
|
|
@@ -73330,6 +73328,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73330
73328
|
nodeIds,
|
|
73331
73329
|
stored: storedShareWeights(await this.settingsStore.readAgentSettingsMap()),
|
|
73332
73330
|
auto: auto?.weights ?? null,
|
|
73331
|
+
...auto !== null ? { autoCapacity: auto.capacity } : {},
|
|
73333
73332
|
eligibleNodes,
|
|
73334
73333
|
eligibleDevices,
|
|
73335
73334
|
computedAt: auto?.computedAt ?? null,
|
|
@@ -73338,7 +73337,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73338
73337
|
}
|
|
73339
73338
|
async setShareSource(input) {
|
|
73340
73339
|
const previous = this.readShareSource();
|
|
73341
|
-
if (input.source === "manual" && previous
|
|
73340
|
+
if (input.source === "manual" && isAutomaticShareSource(previous)) {
|
|
73342
73341
|
const auto = this.effectiveShares().current();
|
|
73343
73342
|
if (auto !== null) await this.setLoadBalancingShares(auto.weights);
|
|
73344
73343
|
}
|
|
@@ -73347,7 +73346,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73347
73346
|
from: previous,
|
|
73348
73347
|
to: input.source
|
|
73349
73348
|
} });
|
|
73350
|
-
if (input.source
|
|
73349
|
+
if (isAutomaticShareSource(input.source)) this.noteShareChange(`source:${input.source}`);
|
|
73351
73350
|
return { success: true };
|
|
73352
73351
|
}
|
|
73353
73352
|
async getCameraSettings(input) {
|
|
@@ -73379,6 +73378,16 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73379
73378
|
await this.emitResolvedCameraUpdated(input.deviceId);
|
|
73380
73379
|
return { success: true };
|
|
73381
73380
|
}
|
|
73381
|
+
/**
|
|
73382
|
+
* D754 — write or clear one step's CAMERA-WIDE settings (field-level bag,
|
|
73383
|
+
* merged under the per-(node,device) override on every accelerator). No
|
|
73384
|
+
* deviceKey to validate: the layer is node-independent by construction.
|
|
73385
|
+
*/
|
|
73386
|
+
async setCameraStepSettings(input) {
|
|
73387
|
+
await this.settingsStore.writeCameraStepSettings(input.deviceId, input.addonId, input.settings);
|
|
73388
|
+
await this.emitResolvedCameraUpdated(input.deviceId);
|
|
73389
|
+
return { success: true };
|
|
73390
|
+
}
|
|
73382
73391
|
async setCameraPipelineForAgent(input) {
|
|
73383
73392
|
const existing = (await this.settingsStore.readCameraSettingsMap())[String(input.deviceId)] ?? {};
|
|
73384
73393
|
const nextByAgent = { ...existing.pipelineByAgent };
|