@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.mjs
CHANGED
|
@@ -5382,7 +5382,7 @@ var ZodIssueCode = {
|
|
|
5382
5382
|
var ZodFirstPartyTypeKind;
|
|
5383
5383
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5384
5384
|
//#endregion
|
|
5385
|
-
//#region ../types/dist/sleep-
|
|
5385
|
+
//#region ../types/dist/sleep-DAJ9Er57.mjs
|
|
5386
5386
|
/**
|
|
5387
5387
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5388
5388
|
* window to float samples (D455).
|
|
@@ -25005,170 +25005,17 @@ var pipelineExecutorCapability = {
|
|
|
25005
25005
|
}
|
|
25006
25006
|
};
|
|
25007
25007
|
/**
|
|
25008
|
-
* Wire shapes of the Load balancing surface (D748): the session placement
|
|
25009
|
-
* policy, the atomic share write, the observed placement statistics, the
|
|
25010
|
-
* hardware share suggestion and the stale device pins.
|
|
25011
|
-
*/
|
|
25012
|
-
/**
|
|
25013
|
-
* How a new detection session picks its node and device.
|
|
25014
|
-
* - `least-loaded` — the historical rule: lowest `attached / weight`, a
|
|
25015
|
-
* constant node order at a tie (D37). An idle node always ties at 0, so a
|
|
25016
|
-
* weight only bites once a node already holds a session.
|
|
25017
|
-
* - `share` — the share of PARALLEL sessions is held to the weights, and a
|
|
25018
|
-
* full or drowning node yields to a free one (D748).
|
|
25019
|
-
*/
|
|
25020
|
-
var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
|
|
25021
|
-
/**
|
|
25022
|
-
* `share` since D750: the percentages are held by default. `least-loaded` stays
|
|
25023
|
-
* selectable.
|
|
25024
|
-
*/
|
|
25025
|
-
var DEFAULT_SESSION_PLACEMENT_POLICY = "share";
|
|
25026
|
-
/** The global-settings key the policy is stored under (orchestrator `load` section). */
|
|
25027
|
-
var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
|
|
25028
|
-
/** Parse a stored value; anything unknown is the default. */
|
|
25029
|
-
function parseSessionPlacementPolicy(value) {
|
|
25030
|
-
const parsed = SessionPlacementPolicySchema.safeParse(value);
|
|
25031
|
-
return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
|
|
25032
|
-
}
|
|
25033
|
-
var LoadBalancingSharesInputSchema = object({
|
|
25034
|
-
/** nodeId → share (> 0). A node left out keeps its weight. */
|
|
25035
|
-
nodes: record(string(), number().positive()).default({}),
|
|
25036
|
-
/** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
|
|
25037
|
-
devices: record(string(), record(string(), number().positive())).default({})
|
|
25038
|
-
});
|
|
25039
|
-
var PlacementStatsInputSchema = object({
|
|
25040
|
-
/** Window the observed shares are averaged over (default 1 h, max 24 h). */
|
|
25041
|
-
windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
|
|
25042
|
-
var PlacementDeviceStatsSchema = object({
|
|
25043
|
-
deviceKey: string(),
|
|
25044
|
-
/** Sessions running on this device now. */
|
|
25045
|
-
sessionsNow: number(),
|
|
25046
|
-
/** Sessions started on this device inside the window. */
|
|
25047
|
-
sessionsStarted: number(),
|
|
25048
|
-
/** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
|
|
25049
|
-
observedShare: number().nullable()
|
|
25050
|
-
});
|
|
25051
|
-
var PlacementNodeStatsSchema = object({
|
|
25052
|
-
nodeId: string(),
|
|
25053
|
-
sessionsNow: number(),
|
|
25054
|
-
sessionsStarted: number(),
|
|
25055
|
-
/** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
|
|
25056
|
-
observedShare: number().nullable(),
|
|
25057
|
-
/** Most sessions this node ran at once inside the window. */
|
|
25058
|
-
peakSessions: number(),
|
|
25059
|
-
/** Sessions a share election handed elsewhere because this node was full or drowning. */
|
|
25060
|
-
yieldsAway: number(),
|
|
25061
|
-
devices: array(PlacementDeviceStatsSchema).readonly()
|
|
25062
|
-
});
|
|
25063
|
-
/** A camera's device pin that the session path cannot honour. */
|
|
25064
|
-
var StaleDevicePinSchema = object({
|
|
25065
|
-
deviceId: number(),
|
|
25066
|
-
deviceKey: string(),
|
|
25067
|
-
/** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
|
|
25068
|
-
reason: _enum(["disabled", "absent"])
|
|
25069
|
-
});
|
|
25070
|
-
var PlacementStatsSchema = object({
|
|
25071
|
-
policy: SessionPlacementPolicySchema,
|
|
25072
|
-
windowMs: number(),
|
|
25073
|
-
/** How much of the window was actually observed (the hub keeps it in memory). */
|
|
25074
|
-
observedForMs: number(),
|
|
25075
|
-
sessionsNow: number(),
|
|
25076
|
-
/** Most parallel sessions across the cluster inside the window. */
|
|
25077
|
-
clusterPeakSessions: number(),
|
|
25078
|
-
nodes: array(PlacementNodeStatsSchema).readonly(),
|
|
25079
|
-
stalePins: array(StaleDevicePinSchema).readonly()
|
|
25080
|
-
});
|
|
25081
|
-
var SuggestSharesInputSchema = object({ headroom: number().min(.3).max(.5).default(.4) });
|
|
25082
|
-
var SuggestedDeviceShareSchema = object({
|
|
25083
|
-
deviceKey: string(),
|
|
25084
|
-
/** Share of the node (0–100, rounded). */
|
|
25085
|
-
share: number(),
|
|
25086
|
-
modelId: string().nullable(),
|
|
25087
|
-
/** Inferences per second at that model, from the measured table. */
|
|
25088
|
-
fps: number().nullable(),
|
|
25089
|
-
measured: boolean(),
|
|
25090
|
-
/** Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋. Never enforced. */
|
|
25091
|
-
suggestedMaxSessions: number().nullable()
|
|
25092
|
-
});
|
|
25093
|
-
var SuggestedNodeShareSchema = object({
|
|
25094
|
-
nodeId: string(),
|
|
25095
|
-
/** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
|
|
25096
|
-
share: number().nullable(),
|
|
25097
|
-
/** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
|
|
25098
|
-
capacitySessions: number(),
|
|
25099
|
-
devices: array(SuggestedDeviceShareSchema).readonly(),
|
|
25100
|
-
skippedReason: string().nullable()
|
|
25101
|
-
});
|
|
25102
|
-
var SuggestSharesResultSchema = object({
|
|
25103
|
-
headroom: number(),
|
|
25104
|
-
/** Camera detection rate the capacity is counted in (the fastest camera's fps). */
|
|
25105
|
-
cameraFps: number(),
|
|
25106
|
-
/** How many share → model → capacity rounds ran before the shares stopped moving (≤ 3). */
|
|
25107
|
-
iterations: number(),
|
|
25108
|
-
nodes: array(SuggestedNodeShareSchema).readonly()
|
|
25109
|
-
});
|
|
25110
|
-
/**
|
|
25111
|
-
* Where the node and device shares come from (D750).
|
|
25112
|
-
* - `auto` — computed by the system from the cluster's measured capacity at
|
|
25113
|
-
* the Auto tune models, and recomputed whenever a node or device joins,
|
|
25114
|
-
* leaves, is switched on or off, or changes model.
|
|
25115
|
-
* - `manual` — the operator's stored sliders. A node or device that cannot
|
|
25116
|
-
* take sessions gives its share to the others in proportion; the stored
|
|
25117
|
-
* values are never rewritten for that.
|
|
25118
|
-
*/
|
|
25119
|
-
var ShareSourceSchema = _enum(["auto", "manual"]);
|
|
25120
|
-
var DEFAULT_SHARE_SOURCE = "auto";
|
|
25121
|
-
/** Headroom the automatic shares are computed at (Auto tune's default). */
|
|
25122
|
-
var AUTO_SHARE_HEADROOM = .4;
|
|
25123
|
-
/** The global-settings key the share source is stored under (orchestrator `load` section). */
|
|
25124
|
-
var SHARE_SOURCE_SETTING_KEY = "shareSource";
|
|
25125
|
-
/** Parse a stored value; anything unknown is the default. */
|
|
25126
|
-
function parseShareSource(value) {
|
|
25127
|
-
const parsed = ShareSourceSchema.safeParse(value);
|
|
25128
|
-
return parsed.success ? parsed.data : DEFAULT_SHARE_SOURCE;
|
|
25129
|
-
}
|
|
25130
|
-
var SetShareSourceInputSchema = object({ source: ShareSourceSchema });
|
|
25131
|
-
object({
|
|
25132
|
-
nodes: record(string(), number()),
|
|
25133
|
-
devices: record(string(), record(string(), number()))
|
|
25134
|
-
});
|
|
25135
|
-
var EffectiveDeviceShareSchema = object({
|
|
25136
|
-
deviceKey: string(),
|
|
25137
|
-
/** Can take sessions now (enabled, available, can run the root). */
|
|
25138
|
-
eligible: boolean(),
|
|
25139
|
-
/** The share the source assigns, as a percentage of the node's devices. */
|
|
25140
|
-
configuredPct: number(),
|
|
25141
|
-
/** The share actually used, after an ineligible device's share went to the others. */
|
|
25142
|
-
effectivePct: number()
|
|
25143
|
-
});
|
|
25144
|
-
var EffectiveNodeShareSchema = object({
|
|
25145
|
-
nodeId: string(),
|
|
25146
|
-
eligible: boolean(),
|
|
25147
|
-
configuredPct: number(),
|
|
25148
|
-
effectivePct: number(),
|
|
25149
|
-
devices: array(EffectiveDeviceShareSchema).readonly()
|
|
25150
|
-
});
|
|
25151
|
-
var EffectiveSharesSchema = object({
|
|
25152
|
-
source: ShareSourceSchema,
|
|
25153
|
-
policy: SessionPlacementPolicySchema,
|
|
25154
|
-
/** `auto` with no computed answer yet: the stored sliders are used meanwhile. */
|
|
25155
|
-
autoPending: boolean(),
|
|
25156
|
-
/** When the automatic shares were last computed (ms epoch); null = never. */
|
|
25157
|
-
computedAt: number().nullable(),
|
|
25158
|
-
/** What made the system recompute them last. */
|
|
25159
|
-
trigger: string().nullable(),
|
|
25160
|
-
nodes: array(EffectiveNodeShareSchema).readonly(),
|
|
25161
|
-
/** The relative weights the election reads (before eligibility). */
|
|
25162
|
-
nodeWeights: record(string(), number()),
|
|
25163
|
-
deviceWeights: record(string(), record(string(), number()))
|
|
25164
|
-
});
|
|
25165
|
-
/**
|
|
25166
25008
|
* The wire shapes of the Auto tune plan and apply (D745), used by the
|
|
25167
25009
|
* `pipeline-orchestrator` cap. The recommender's own types
|
|
25168
|
-
* (`recommend.ts
|
|
25010
|
+
* (`recommend.ts`) are the TS face of the same data; these
|
|
25169
25011
|
* schemas are what crosses the cap boundary.
|
|
25170
25012
|
*/
|
|
25171
25013
|
var ModelFormatSchema$1 = _enum(MODEL_FORMATS);
|
|
25014
|
+
/** The operator's range for it. */
|
|
25015
|
+
var MIN_SESSIONS_PER_DEVICE_RANGE = {
|
|
25016
|
+
min: 1,
|
|
25017
|
+
max: 8
|
|
25018
|
+
};
|
|
25172
25019
|
var HardwareClassIdSchema = _enum([
|
|
25173
25020
|
"intel-core-ultra",
|
|
25174
25021
|
"intel-n-series",
|
|
@@ -25186,8 +25033,9 @@ var AcceleratorKindSchema = _enum([
|
|
|
25186
25033
|
]);
|
|
25187
25034
|
var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
25188
25035
|
object({
|
|
25189
|
-
kind: literal("
|
|
25190
|
-
|
|
25036
|
+
kind: literal("capacity"),
|
|
25037
|
+
sessions: number(),
|
|
25038
|
+
minSessions: number()
|
|
25191
25039
|
}),
|
|
25192
25040
|
object({
|
|
25193
25041
|
kind: literal("tie-faster"),
|
|
@@ -25202,16 +25050,13 @@ var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
|
25202
25050
|
}),
|
|
25203
25051
|
object({
|
|
25204
25052
|
kind: literal("no-fit-lightest"),
|
|
25205
|
-
|
|
25053
|
+
sessions: number(),
|
|
25054
|
+
minSessions: number()
|
|
25206
25055
|
}),
|
|
25207
25056
|
object({
|
|
25208
25057
|
kind: literal("derived-latency"),
|
|
25209
25058
|
modelId: string()
|
|
25210
25059
|
}),
|
|
25211
|
-
object({
|
|
25212
|
-
kind: literal("estimated-load"),
|
|
25213
|
-
observedForMs: number().nullable()
|
|
25214
|
-
}),
|
|
25215
25060
|
object({ kind: literal("cpu-device") }),
|
|
25216
25061
|
object({ kind: literal("coral-untouched") }),
|
|
25217
25062
|
object({ kind: literal("device-disabled") }),
|
|
@@ -25227,28 +25072,13 @@ var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
|
25227
25072
|
format: ModelFormatSchema$1
|
|
25228
25073
|
})
|
|
25229
25074
|
]);
|
|
25230
|
-
var DeviceDemandSchema = object({
|
|
25231
|
-
nodeId: string(),
|
|
25232
|
-
deviceKey: string(),
|
|
25233
|
-
demandFps: number(),
|
|
25234
|
-
maxCameraFps: number(),
|
|
25235
|
-
alwaysOnCameras: number(),
|
|
25236
|
-
onMotionSessions: number(),
|
|
25237
|
-
onMotionSource: _enum([
|
|
25238
|
-
"observed-24h",
|
|
25239
|
-
"estimate",
|
|
25240
|
-
"none"
|
|
25241
|
-
]),
|
|
25242
|
-
observedForMs: number().nullable(),
|
|
25243
|
-
/** How on-motion sessions were charged: per-device observation, or split by share (D748). */
|
|
25244
|
-
placement: SessionPlacementPolicySchema
|
|
25245
|
-
});
|
|
25246
25075
|
var DeviceModelChoiceSchema = object({
|
|
25247
25076
|
modelId: string(),
|
|
25248
25077
|
p50Ms: number(),
|
|
25249
25078
|
fps: number(),
|
|
25250
25079
|
measured: boolean(),
|
|
25251
|
-
|
|
25080
|
+
/** Parallel sessions at the camera rate with the headroom kept free: ⌊fps × (1 − headroom) / camera fps⌋. */
|
|
25081
|
+
capacitySessions: number(),
|
|
25252
25082
|
personRecall: number()
|
|
25253
25083
|
});
|
|
25254
25084
|
var DeviceRecommendationSchema = object({
|
|
@@ -25256,7 +25086,6 @@ var DeviceRecommendationSchema = object({
|
|
|
25256
25086
|
accelerator: AcceleratorKindSchema.nullable(),
|
|
25257
25087
|
enabled: boolean(),
|
|
25258
25088
|
available: boolean(),
|
|
25259
|
-
demand: DeviceDemandSchema.nullable(),
|
|
25260
25089
|
chosen: DeviceModelChoiceSchema.nullable(),
|
|
25261
25090
|
reasons: array(RecommendationReasonSchema).readonly()
|
|
25262
25091
|
});
|
|
@@ -25317,20 +25146,20 @@ var AutoTuneOptionsSchema = object({
|
|
|
25317
25146
|
/** Share of each accelerator's throughput kept free (0.3–0.5). */
|
|
25318
25147
|
headroom: number().min(.3).max(.5).default(.4),
|
|
25319
25148
|
/**
|
|
25320
|
-
*
|
|
25321
|
-
*
|
|
25322
|
-
*
|
|
25323
|
-
* is stored.
|
|
25149
|
+
* The least number of parallel sessions every device must sustain at the
|
|
25150
|
+
* camera rate with the headroom kept free (D755). The most accurate model
|
|
25151
|
+
* that reaches it wins.
|
|
25324
25152
|
*/
|
|
25325
|
-
|
|
25153
|
+
minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
|
|
25326
25154
|
});
|
|
25327
25155
|
var AutoTunePlanSchema = object({
|
|
25328
25156
|
/** Hash of everything the apply will write — the apply refuses a different one. */
|
|
25329
25157
|
planHash: string(),
|
|
25330
25158
|
family: literal("yolov9"),
|
|
25331
25159
|
headroom: number(),
|
|
25332
|
-
|
|
25333
|
-
|
|
25160
|
+
minSessionsPerDevice: number(),
|
|
25161
|
+
/** The detection rate a session is counted at (the fastest camera running the pipeline root). */
|
|
25162
|
+
cameraFps: number(),
|
|
25334
25163
|
generatedAt: number(),
|
|
25335
25164
|
nodes: array(NodeRecommendationSchema).readonly(),
|
|
25336
25165
|
nodeChanges: array(NodeDeviceChangeSchema).readonly(),
|
|
@@ -25340,7 +25169,8 @@ var AutoTunePlanSchema = object({
|
|
|
25340
25169
|
});
|
|
25341
25170
|
var AutoTuneApplyInputSchema = object({
|
|
25342
25171
|
planHash: string(),
|
|
25343
|
-
headroom: number().min(.3).max(.5).default(.4)
|
|
25172
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
25173
|
+
minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
|
|
25344
25174
|
});
|
|
25345
25175
|
var AutoTunePhaseResultSchema = object({
|
|
25346
25176
|
phase: _enum([
|
|
@@ -25372,6 +25202,206 @@ var AutoTuneApplyResultSchema = discriminatedUnion("kind", [
|
|
|
25372
25202
|
reason: _enum(["apply-in-progress"])
|
|
25373
25203
|
})
|
|
25374
25204
|
]);
|
|
25205
|
+
/**
|
|
25206
|
+
* Wire shapes of the Load balancing surface (D748): the session placement
|
|
25207
|
+
* policy, the atomic share write, the observed placement statistics, the
|
|
25208
|
+
* hardware share suggestion and the stale device pins.
|
|
25209
|
+
*/
|
|
25210
|
+
/**
|
|
25211
|
+
* How a new detection session picks its node and device.
|
|
25212
|
+
* - `least-loaded` — the historical rule: lowest `attached / weight`, a
|
|
25213
|
+
* constant node order at a tie (D37). An idle node always ties at 0, so a
|
|
25214
|
+
* weight only bites once a node already holds a session.
|
|
25215
|
+
* - `share` — the share of PARALLEL sessions is held to the weights, and a
|
|
25216
|
+
* full or drowning node yields to a free one (D748).
|
|
25217
|
+
*/
|
|
25218
|
+
var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
|
|
25219
|
+
/**
|
|
25220
|
+
* `share` since D750: the percentages are held by default. `least-loaded` stays
|
|
25221
|
+
* selectable.
|
|
25222
|
+
*/
|
|
25223
|
+
var DEFAULT_SESSION_PLACEMENT_POLICY = "share";
|
|
25224
|
+
/** The global-settings key the policy is stored under (orchestrator `load` section). */
|
|
25225
|
+
var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
|
|
25226
|
+
/** Parse a stored value; anything unknown is the default. */
|
|
25227
|
+
function parseSessionPlacementPolicy(value) {
|
|
25228
|
+
const parsed = SessionPlacementPolicySchema.safeParse(value);
|
|
25229
|
+
return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
|
|
25230
|
+
}
|
|
25231
|
+
var LoadBalancingSharesInputSchema = object({
|
|
25232
|
+
/** nodeId → share (> 0). A node left out keeps its weight. */
|
|
25233
|
+
nodes: record(string(), number().positive()).default({}),
|
|
25234
|
+
/** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
|
|
25235
|
+
devices: record(string(), record(string(), number().positive())).default({})
|
|
25236
|
+
});
|
|
25237
|
+
var PlacementStatsInputSchema = object({
|
|
25238
|
+
/** Window the observed shares are averaged over (default 1 h, max 24 h). */
|
|
25239
|
+
windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
|
|
25240
|
+
var PlacementDeviceStatsSchema = object({
|
|
25241
|
+
deviceKey: string(),
|
|
25242
|
+
/** Sessions running on this device now. */
|
|
25243
|
+
sessionsNow: number(),
|
|
25244
|
+
/** Sessions started on this device inside the window. */
|
|
25245
|
+
sessionsStarted: number(),
|
|
25246
|
+
/** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
|
|
25247
|
+
observedShare: number().nullable()
|
|
25248
|
+
});
|
|
25249
|
+
var PlacementNodeStatsSchema = object({
|
|
25250
|
+
nodeId: string(),
|
|
25251
|
+
sessionsNow: number(),
|
|
25252
|
+
sessionsStarted: number(),
|
|
25253
|
+
/** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
|
|
25254
|
+
observedShare: number().nullable(),
|
|
25255
|
+
/** Most sessions this node ran at once inside the window. */
|
|
25256
|
+
peakSessions: number(),
|
|
25257
|
+
/** Sessions a share election handed elsewhere because this node was full or drowning. */
|
|
25258
|
+
yieldsAway: number(),
|
|
25259
|
+
devices: array(PlacementDeviceStatsSchema).readonly()
|
|
25260
|
+
});
|
|
25261
|
+
/** A camera's device pin that the session path cannot honour. */
|
|
25262
|
+
var StaleDevicePinSchema = object({
|
|
25263
|
+
deviceId: number(),
|
|
25264
|
+
deviceKey: string(),
|
|
25265
|
+
/** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
|
|
25266
|
+
reason: _enum(["disabled", "absent"])
|
|
25267
|
+
});
|
|
25268
|
+
var PlacementStatsSchema = object({
|
|
25269
|
+
policy: SessionPlacementPolicySchema,
|
|
25270
|
+
windowMs: number(),
|
|
25271
|
+
/** How much of the window was actually observed (the hub keeps it in memory). */
|
|
25272
|
+
observedForMs: number(),
|
|
25273
|
+
sessionsNow: number(),
|
|
25274
|
+
/** Most parallel sessions across the cluster inside the window. */
|
|
25275
|
+
clusterPeakSessions: number(),
|
|
25276
|
+
nodes: array(PlacementNodeStatsSchema).readonly(),
|
|
25277
|
+
stalePins: array(StaleDevicePinSchema).readonly()
|
|
25278
|
+
});
|
|
25279
|
+
/**
|
|
25280
|
+
* Where an automatic share's capacity comes from (D755).
|
|
25281
|
+
* - `hardware` — the static capacity of each device at its Auto tune model:
|
|
25282
|
+
* the measured latency table for the node's hardware class.
|
|
25283
|
+
* - `benchmark` — the node's own measured capacity (node benchmark results).
|
|
25284
|
+
* A node with no results falls back to `hardware`, and says so.
|
|
25285
|
+
*/
|
|
25286
|
+
var CapacitySourceSchema = _enum(["hardware", "benchmark"]);
|
|
25287
|
+
/** Why a node under the `benchmark` source was sized from hardware instead. */
|
|
25288
|
+
var CapacityFallbackSchema = _enum(["no-benchmark-results", "partial-benchmark-results"]);
|
|
25289
|
+
var SuggestSharesInputSchema = object({
|
|
25290
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
25291
|
+
/** Default `hardware`. */
|
|
25292
|
+
source: CapacitySourceSchema.default("hardware")
|
|
25293
|
+
});
|
|
25294
|
+
var SuggestedDeviceShareSchema = object({
|
|
25295
|
+
deviceKey: string(),
|
|
25296
|
+
/** Share of the node (0–100, rounded). */
|
|
25297
|
+
share: number(),
|
|
25298
|
+
modelId: string().nullable(),
|
|
25299
|
+
/** Inferences per second at that model, from the measured table; null when sized by a benchmark. */
|
|
25300
|
+
fps: number().nullable(),
|
|
25301
|
+
/** False for a table cell derived from another chip; true for a measured cell or a benchmark. */
|
|
25302
|
+
measured: boolean(),
|
|
25303
|
+
/**
|
|
25304
|
+
* Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋ from the table,
|
|
25305
|
+
* or the benchmark's measured max sessions. Never enforced.
|
|
25306
|
+
*/
|
|
25307
|
+
suggestedMaxSessions: number().nullable()
|
|
25308
|
+
});
|
|
25309
|
+
var SuggestedNodeShareSchema = object({
|
|
25310
|
+
nodeId: string(),
|
|
25311
|
+
/** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
|
|
25312
|
+
share: number().nullable(),
|
|
25313
|
+
/** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
|
|
25314
|
+
capacitySessions: number(),
|
|
25315
|
+
devices: array(SuggestedDeviceShareSchema).readonly(),
|
|
25316
|
+
skippedReason: string().nullable(),
|
|
25317
|
+
/** What this node's capacity was read from; null when the node is skipped. */
|
|
25318
|
+
capacitySource: CapacitySourceSchema.nullable(),
|
|
25319
|
+
/** Set when the `benchmark` source was asked for and this node used `hardware`. */
|
|
25320
|
+
capacityFallback: CapacityFallbackSchema.nullable()
|
|
25321
|
+
});
|
|
25322
|
+
var SuggestSharesResultSchema = object({
|
|
25323
|
+
headroom: number(),
|
|
25324
|
+
/** The source asked for; each node says what it actually used. */
|
|
25325
|
+
source: CapacitySourceSchema,
|
|
25326
|
+
/** Camera detection rate the capacity is counted in (the fastest camera's fps). */
|
|
25327
|
+
cameraFps: number(),
|
|
25328
|
+
/** The Auto tune capacity floor the models were picked at. */
|
|
25329
|
+
minSessionsPerDevice: number(),
|
|
25330
|
+
nodes: array(SuggestedNodeShareSchema).readonly()
|
|
25331
|
+
});
|
|
25332
|
+
/**
|
|
25333
|
+
* Where the node and device shares come from (D750, D755).
|
|
25334
|
+
* - `hardware` (default) — computed by the system from each node's static
|
|
25335
|
+
* capacity (its accelerators at their Auto tune models, from the measured
|
|
25336
|
+
* latency table), recomputed whenever a node or device joins, leaves, is
|
|
25337
|
+
* switched on or off, or changes model. No load input.
|
|
25338
|
+
* - `benchmark` — the same, from each node's measured capacity (node
|
|
25339
|
+
* benchmark results); a node without results falls back to `hardware`.
|
|
25340
|
+
* - `manual` — the operator's stored sliders.
|
|
25341
|
+
* Either way a node or device that cannot take sessions gives its share to
|
|
25342
|
+
* the others in proportion; the stored values are never rewritten for that.
|
|
25343
|
+
*/
|
|
25344
|
+
var ShareSourceSchema = _enum([
|
|
25345
|
+
"hardware",
|
|
25346
|
+
"benchmark",
|
|
25347
|
+
"manual"
|
|
25348
|
+
]);
|
|
25349
|
+
var DEFAULT_SHARE_SOURCE = "hardware";
|
|
25350
|
+
/** `hardware` or `benchmark`: the system computes the shares. */
|
|
25351
|
+
function isAutomaticShareSource(source) {
|
|
25352
|
+
return source !== "manual";
|
|
25353
|
+
}
|
|
25354
|
+
/** Headroom the automatic shares are computed at (Auto tune's default). */
|
|
25355
|
+
var AUTO_SHARE_HEADROOM = .4;
|
|
25356
|
+
/** The global-settings key the share source is stored under (orchestrator `load` section). */
|
|
25357
|
+
var SHARE_SOURCE_SETTING_KEY = "shareSource";
|
|
25358
|
+
/**
|
|
25359
|
+
* Parse a stored value; anything unknown is the default. `auto` (D750, before
|
|
25360
|
+
* D755 split it) reads as `hardware` — that is what it computed.
|
|
25361
|
+
*/
|
|
25362
|
+
function parseShareSource(value) {
|
|
25363
|
+
const parsed = ShareSourceSchema.safeParse(value);
|
|
25364
|
+
return parsed.success ? parsed.data : DEFAULT_SHARE_SOURCE;
|
|
25365
|
+
}
|
|
25366
|
+
var SetShareSourceInputSchema = object({ source: ShareSourceSchema });
|
|
25367
|
+
object({
|
|
25368
|
+
nodes: record(string(), number()),
|
|
25369
|
+
devices: record(string(), record(string(), number()))
|
|
25370
|
+
});
|
|
25371
|
+
var EffectiveDeviceShareSchema = object({
|
|
25372
|
+
deviceKey: string(),
|
|
25373
|
+
/** Can take sessions now (enabled, available, can run the root). */
|
|
25374
|
+
eligible: boolean(),
|
|
25375
|
+
/** The share the source assigns, as a percentage of the node's devices. */
|
|
25376
|
+
configuredPct: number(),
|
|
25377
|
+
/** The share actually used, after an ineligible device's share went to the others. */
|
|
25378
|
+
effectivePct: number()
|
|
25379
|
+
});
|
|
25380
|
+
var EffectiveNodeShareSchema = object({
|
|
25381
|
+
nodeId: string(),
|
|
25382
|
+
eligible: boolean(),
|
|
25383
|
+
configuredPct: number(),
|
|
25384
|
+
effectivePct: number(),
|
|
25385
|
+
/** Automatic sources: what this node's share was computed from; null under `manual` or before the first answer. */
|
|
25386
|
+
capacitySource: CapacitySourceSchema.nullable(),
|
|
25387
|
+
/** Automatic `benchmark`: why this node used `hardware` instead. */
|
|
25388
|
+
capacityFallback: CapacityFallbackSchema.nullable(),
|
|
25389
|
+
devices: array(EffectiveDeviceShareSchema).readonly()
|
|
25390
|
+
});
|
|
25391
|
+
var EffectiveSharesSchema = object({
|
|
25392
|
+
source: ShareSourceSchema,
|
|
25393
|
+
policy: SessionPlacementPolicySchema,
|
|
25394
|
+
/** An automatic source with no computed answer yet: the stored sliders are used meanwhile. */
|
|
25395
|
+
autoPending: boolean(),
|
|
25396
|
+
/** When the automatic shares were last computed (ms epoch); null = never. */
|
|
25397
|
+
computedAt: number().nullable(),
|
|
25398
|
+
/** What made the system recompute them last. */
|
|
25399
|
+
trigger: string().nullable(),
|
|
25400
|
+
nodes: array(EffectiveNodeShareSchema).readonly(),
|
|
25401
|
+
/** The relative weights the election reads (before eligibility). */
|
|
25402
|
+
nodeWeights: record(string(), number()),
|
|
25403
|
+
deviceWeights: record(string(), record(string(), number()))
|
|
25404
|
+
});
|
|
25375
25405
|
object({
|
|
25376
25406
|
activeCameras: number(),
|
|
25377
25407
|
throttledCameras: number(),
|
|
@@ -26779,8 +26809,10 @@ var CameraStepOverridePatchSchema = object({
|
|
|
26779
26809
|
modelId: string().optional(),
|
|
26780
26810
|
settings: record(string(), unknown()).readonly().optional()
|
|
26781
26811
|
});
|
|
26812
|
+
var CameraStepSettingsSchema = record(string(), record(string(), unknown()).readonly());
|
|
26782
26813
|
var CameraPipelineSettingsSchema = object({
|
|
26783
26814
|
stepToggles: record(string(), boolean()).optional(),
|
|
26815
|
+
stepSettings: CameraStepSettingsSchema.optional(),
|
|
26784
26816
|
stepOverridesByDevice: record(string(), record(string(), record(string(), CameraStepOverridePatchSchema))).optional(),
|
|
26785
26817
|
pipelineByAgent: record(string(), CameraPipelineForAgentSchema).optional()
|
|
26786
26818
|
});
|
|
@@ -27546,9 +27578,11 @@ var pipelineOrchestratorCapability = {
|
|
|
27546
27578
|
auth: "admin"
|
|
27547
27579
|
}),
|
|
27548
27580
|
/**
|
|
27549
|
-
* Auto tune (D745) — DRY RUN. Recommends ONE detector family
|
|
27550
|
-
* for the whole cluster from each node's static hardware class
|
|
27551
|
-
* measured latency table
|
|
27581
|
+
* Auto tune (D745, D755) — DRY RUN. Recommends ONE detector family
|
|
27582
|
+
* (YOLOv9) for the whole cluster from each node's static hardware class
|
|
27583
|
+
* and the measured latency table: per device, the most accurate model
|
|
27584
|
+
* that sustains at least `minSessionsPerDevice` parallel sessions at the
|
|
27585
|
+
* camera rate with the headroom kept free (no load input), and lists every write
|
|
27552
27586
|
* the apply would make: the node model pins set, the enrichment pins and
|
|
27553
27587
|
* model-bound thresholds removed (node level and per camera), and the
|
|
27554
27588
|
* per-camera detector thresholds cleared. Writes nothing. Step
|
|
@@ -27587,10 +27621,12 @@ var pipelineOrchestratorCapability = {
|
|
|
27587
27621
|
*/
|
|
27588
27622
|
getPlacementStats: method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }),
|
|
27589
27623
|
/**
|
|
27590
|
-
* Load balancing (D748) — shares proportional to each node's
|
|
27591
|
-
* capacity
|
|
27592
|
-
*
|
|
27593
|
-
*
|
|
27624
|
+
* Load balancing (D748, D755) — shares proportional to each node's
|
|
27625
|
+
* capacity in parallel sessions: from the latency table at the models
|
|
27626
|
+
* Auto tune picks (`hardware`), or from node benchmark results
|
|
27627
|
+
* (`benchmark`, each node without results falling back to `hardware` and
|
|
27628
|
+
* saying so). The automatic shares are exactly this answer. Per device,
|
|
27629
|
+
* an ADVISORY session cap. Writes nothing.
|
|
27594
27630
|
*/
|
|
27595
27631
|
suggestShares: method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }),
|
|
27596
27632
|
/**
|
|
@@ -27601,9 +27637,11 @@ var pipelineOrchestratorCapability = {
|
|
|
27601
27637
|
*/
|
|
27602
27638
|
getEffectiveShares: method(_void(), EffectiveSharesSchema, { auth: "admin" }),
|
|
27603
27639
|
/**
|
|
27604
|
-
* Load balancing (D750) — switch
|
|
27605
|
-
*
|
|
27606
|
-
*
|
|
27640
|
+
* Load balancing (D750, D755) — switch the share source: `hardware`
|
|
27641
|
+
* (automatic, from static capacity), `benchmark` (automatic, from node
|
|
27642
|
+
* benchmark results) or `manual`. Going manual seeds the sliders with the
|
|
27643
|
+
* current automatic shares (one atomic write), so nothing jumps; going
|
|
27644
|
+
* to an automatic source recomputes at once.
|
|
27607
27645
|
*/
|
|
27608
27646
|
setShareSource: method(SetShareSourceInputSchema, object({ success: literal(true) }), {
|
|
27609
27647
|
kind: "mutation",
|
|
@@ -27638,6 +27676,22 @@ var pipelineOrchestratorCapability = {
|
|
|
27638
27676
|
kind: "mutation",
|
|
27639
27677
|
auth: "admin"
|
|
27640
27678
|
}),
|
|
27679
|
+
/**
|
|
27680
|
+
* Write or clear one step's CAMERA-WIDE settings (D754) — the fields this
|
|
27681
|
+
* camera overrides on every accelerator it runs on. `settings` is the
|
|
27682
|
+
* whole field-level bag for that step (only the overridden keys; a
|
|
27683
|
+
* model-bound field travels with its binding key); `null` or `{}` clears
|
|
27684
|
+
* it. Merged at dispatch between the node base and the per-(node,device)
|
|
27685
|
+
* override, key by key.
|
|
27686
|
+
*/
|
|
27687
|
+
setCameraStepSettings: method(object({
|
|
27688
|
+
deviceId: number(),
|
|
27689
|
+
addonId: string(),
|
|
27690
|
+
settings: record(string(), unknown()).nullable()
|
|
27691
|
+
}), object({ success: literal(true) }), {
|
|
27692
|
+
kind: "mutation",
|
|
27693
|
+
auth: "admin"
|
|
27694
|
+
}),
|
|
27641
27695
|
/** Write or clear the wholesale per-agent pipeline override. Pass `pipeline: null` to drop the override. */
|
|
27642
27696
|
setCameraPipelineForAgent: method(object({
|
|
27643
27697
|
deviceId: number(),
|
|
@@ -43926,6 +43980,12 @@ Object.freeze({
|
|
|
43926
43980
|
addonId: null,
|
|
43927
43981
|
access: "create"
|
|
43928
43982
|
},
|
|
43983
|
+
"pipelineOrchestrator.setCameraStepSettings": {
|
|
43984
|
+
capName: "pipeline-orchestrator",
|
|
43985
|
+
capScope: "system",
|
|
43986
|
+
addonId: null,
|
|
43987
|
+
access: "create"
|
|
43988
|
+
},
|
|
43929
43989
|
"pipelineOrchestrator.setCameraStepToggle": {
|
|
43930
43990
|
capName: "pipeline-orchestrator",
|
|
43931
43991
|
capScope: "system",
|
|
@@ -49402,6 +49462,11 @@ Object.freeze({
|
|
|
49402
49462
|
form: "single",
|
|
49403
49463
|
optional: false
|
|
49404
49464
|
}],
|
|
49465
|
+
"pipelineOrchestrator.setCameraStepSettings": [{
|
|
49466
|
+
name: "deviceId",
|
|
49467
|
+
form: "single",
|
|
49468
|
+
optional: false
|
|
49469
|
+
}],
|
|
49405
49470
|
"pipelineOrchestrator.setCameraStepToggle": [{
|
|
49406
49471
|
name: "deviceId",
|
|
49407
49472
|
form: "single",
|
|
@@ -51692,137 +51757,22 @@ var DETECTOR_ACCURACY = {
|
|
|
51692
51757
|
personRecall: .646
|
|
51693
51758
|
}
|
|
51694
51759
|
};
|
|
51695
|
-
var DEFAULT_DEMAND_OPTIONS = {
|
|
51696
|
-
onMotionConcurrency: .25,
|
|
51697
|
-
onMotionMinConcurrent: 2
|
|
51698
|
-
};
|
|
51699
|
-
function deviceId(nodeId, deviceKey) {
|
|
51700
|
-
return `${nodeId}\u0000${deviceKey}`;
|
|
51701
|
-
}
|
|
51702
|
-
function charges(load) {
|
|
51703
|
-
return load.mode !== "disabled" && load.runsPipelineRoot && load.fps > 0;
|
|
51704
|
-
}
|
|
51705
|
-
/** The highest `count` rates, summed. */
|
|
51706
|
-
function topRatesSum(rates, count) {
|
|
51707
|
-
return rates.toSorted((a, b) => b - a).slice(0, Math.max(0, count)).reduce((sum, r) => sum + r, 0);
|
|
51708
|
-
}
|
|
51709
|
-
/** Concurrent on-motion sessions assumed cluster-wide without an observation. */
|
|
51710
|
-
function estimatedConcurrentSessions(cameraCount, options) {
|
|
51711
|
-
if (cameraCount <= 0) return 0;
|
|
51712
|
-
const share = Math.ceil(options.onMotionConcurrency * cameraCount);
|
|
51713
|
-
return Math.min(cameraCount, Math.max(share, Math.min(cameraCount, options.onMotionMinConcurrent)));
|
|
51714
|
-
}
|
|
51715
|
-
/**
|
|
51716
|
-
* Demand on every eligible device of the cluster.
|
|
51717
|
-
*/
|
|
51718
|
-
function estimateDeviceDemand(input) {
|
|
51719
|
-
const options = input.options ?? DEFAULT_DEMAND_OPTIONS;
|
|
51720
|
-
const placement = input.shares ? "share" : "least-loaded";
|
|
51721
|
-
const eligibleById = new Map(input.eligible.map((d) => [deviceId(d.nodeId, d.deviceKey), d]));
|
|
51722
|
-
const peakById = new Map(input.peaks.map((p) => [deviceId(p.nodeId, p.deviceKey), p]));
|
|
51723
|
-
const weightByNode = /* @__PURE__ */ new Map();
|
|
51724
|
-
const eligibleCountByNode = /* @__PURE__ */ new Map();
|
|
51725
|
-
for (const d of input.eligible) {
|
|
51726
|
-
weightByNode.set(d.nodeId, (weightByNode.get(d.nodeId) ?? 0) + Math.max(d.weight, 0));
|
|
51727
|
-
eligibleCountByNode.set(d.nodeId, (eligibleCountByNode.get(d.nodeId) ?? 0) + 1);
|
|
51728
|
-
}
|
|
51729
|
-
const pinnedFps = /* @__PURE__ */ new Map();
|
|
51730
|
-
const alwaysOnFps = /* @__PURE__ */ new Map();
|
|
51731
|
-
const alwaysOnCount = /* @__PURE__ */ new Map();
|
|
51732
|
-
const maxFps = /* @__PURE__ */ new Map();
|
|
51733
|
-
const onMotionRates = [];
|
|
51734
|
-
const bump = (map, key, by) => {
|
|
51735
|
-
map.set(key, (map.get(key) ?? 0) + by);
|
|
51736
|
-
};
|
|
51737
|
-
const noteMax = (key, fps) => {
|
|
51738
|
-
maxFps.set(key, Math.max(maxFps.get(key) ?? 0, fps));
|
|
51739
|
-
};
|
|
51740
|
-
const pinNodeOf = (pin) => {
|
|
51741
|
-
if (pin.nodeId !== null) return pin.nodeId;
|
|
51742
|
-
const holders = input.eligible.filter((d) => d.deviceKey === pin.deviceKey);
|
|
51743
|
-
return holders.length === 1 ? holders[0].nodeId : null;
|
|
51744
|
-
};
|
|
51745
|
-
let alwaysOnTotal = 0;
|
|
51746
|
-
for (const cam of input.cameras) {
|
|
51747
|
-
if (!charges(cam)) continue;
|
|
51748
|
-
const pinNode = cam.devicePin !== null ? pinNodeOf(cam.devicePin) : null;
|
|
51749
|
-
if (cam.devicePin !== null && pinNode !== null) {
|
|
51750
|
-
const key = deviceId(pinNode, cam.devicePin.deviceKey);
|
|
51751
|
-
if (!eligibleById.has(key)) continue;
|
|
51752
|
-
bump(pinnedFps, key, cam.fps);
|
|
51753
|
-
noteMax(key, cam.fps);
|
|
51754
|
-
continue;
|
|
51755
|
-
}
|
|
51756
|
-
const standing = cam.mode === "always-on" && cam.standingNodeId !== null ? cam.standingNodeId : cam.nodePinId;
|
|
51757
|
-
if (standing !== null) {
|
|
51758
|
-
if (cam.mode === "always-on") alwaysOnTotal += 1;
|
|
51759
|
-
const nodeWeight = weightByNode.get(standing) ?? 0;
|
|
51760
|
-
for (const d of input.eligible) {
|
|
51761
|
-
if (d.nodeId !== standing || nodeWeight <= 0) continue;
|
|
51762
|
-
const key = deviceId(d.nodeId, d.deviceKey);
|
|
51763
|
-
const share = Math.max(d.weight, 0) / nodeWeight;
|
|
51764
|
-
bump(alwaysOnFps, key, cam.fps * share);
|
|
51765
|
-
bump(alwaysOnCount, key, share);
|
|
51766
|
-
noteMax(key, cam.fps);
|
|
51767
|
-
}
|
|
51768
|
-
continue;
|
|
51769
|
-
}
|
|
51770
|
-
onMotionRates.push(cam.fps);
|
|
51771
|
-
}
|
|
51772
|
-
const fastestOnMotion = onMotionRates.reduce((m, r) => Math.max(m, r), 0);
|
|
51773
|
-
const nodeCount = weightByNode.size;
|
|
51774
|
-
const estimatedTotal = estimatedConcurrentSessions(onMotionRates.length, options);
|
|
51775
|
-
const clusterPeak = input.shares?.clusterPeak ?? null;
|
|
51776
|
-
const clusterObserved = clusterPeak !== null && clusterPeak.observedForMs >= 864e5;
|
|
51777
|
-
const clusterSessions = clusterObserved ? Math.max(0, clusterPeak.peakSessions - alwaysOnTotal) : estimatedTotal;
|
|
51778
|
-
const shareSum = [...weightByNode.keys()].reduce((sum, nodeId) => sum + Math.max(input.shares?.nodes[nodeId] ?? 0, 0), 0);
|
|
51779
|
-
return input.eligible.map((d) => {
|
|
51780
|
-
const key = deviceId(d.nodeId, d.deviceKey);
|
|
51781
|
-
const peak = peakById.get(key) ?? (eligibleCountByNode.get(d.nodeId) === 1 ? peakById.get(deviceId(d.nodeId, "")) : void 0);
|
|
51782
|
-
const alwaysOn = alwaysOnCount.get(key) ?? 0;
|
|
51783
|
-
let onMotionSessions = 0;
|
|
51784
|
-
let onMotionSource = "none";
|
|
51785
|
-
if (onMotionRates.length > 0 && input.shares !== void 0) {
|
|
51786
|
-
const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
|
|
51787
|
-
const nodeShare = shareSum > 0 ? Math.max(input.shares.nodes[d.nodeId] ?? 0, 0) / shareSum : 0;
|
|
51788
|
-
const deviceShare = nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight : 0;
|
|
51789
|
-
onMotionSessions = Math.max(0, Math.ceil(clusterSessions * nodeShare * deviceShare - 1e-9));
|
|
51790
|
-
onMotionSource = clusterObserved ? "observed-24h" : "estimate";
|
|
51791
|
-
} else if (onMotionRates.length > 0) if (peak !== void 0 && peak.observedForMs >= 864e5) {
|
|
51792
|
-
onMotionSessions = Math.max(0, Math.round(peak.peakSessions - alwaysOn));
|
|
51793
|
-
onMotionSource = "observed-24h";
|
|
51794
|
-
} else {
|
|
51795
|
-
const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
|
|
51796
|
-
const share = nodeCount > 0 && nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight / nodeCount : 0;
|
|
51797
|
-
onMotionSessions = Math.ceil(estimatedTotal * share);
|
|
51798
|
-
onMotionSource = "estimate";
|
|
51799
|
-
}
|
|
51800
|
-
const demandFps = (pinnedFps.get(key) ?? 0) + (alwaysOnFps.get(key) ?? 0) + topRatesSum(onMotionRates, onMotionSessions);
|
|
51801
|
-
return {
|
|
51802
|
-
nodeId: d.nodeId,
|
|
51803
|
-
deviceKey: d.deviceKey,
|
|
51804
|
-
demandFps: Math.round(demandFps * 100) / 100,
|
|
51805
|
-
maxCameraFps: Math.max(maxFps.get(key) ?? 0, fastestOnMotion),
|
|
51806
|
-
alwaysOnCameras: Math.round(alwaysOn * 100) / 100,
|
|
51807
|
-
onMotionSessions,
|
|
51808
|
-
onMotionSource,
|
|
51809
|
-
observedForMs: input.shares !== void 0 ? clusterPeak?.observedForMs ?? null : peak?.observedForMs ?? null,
|
|
51810
|
-
placement
|
|
51811
|
-
};
|
|
51812
|
-
}).toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
|
|
51813
|
-
}
|
|
51814
51760
|
/** The family the cluster runs. */
|
|
51815
51761
|
var AUTO_TUNE_FAMILY = "yolov9";
|
|
51762
|
+
/** Parallel sessions one device sustains at `fps` with `headroom` kept free. */
|
|
51763
|
+
function capacitySessionsOf(fps, headroom, cameraFps) {
|
|
51764
|
+
if (!(fps > 0) || !(cameraFps > 0)) return 0;
|
|
51765
|
+
return Math.max(0, Math.floor(fps * (1 - headroom) / cameraFps + 1e-9));
|
|
51766
|
+
}
|
|
51816
51767
|
function round(value, digits = 3) {
|
|
51817
51768
|
const f = 10 ** digits;
|
|
51818
51769
|
return Math.round(value * f) / f;
|
|
51819
51770
|
}
|
|
51820
|
-
function
|
|
51821
|
-
return cell
|
|
51771
|
+
function sessionsOn(cell, sizing) {
|
|
51772
|
+
return cell === void 0 ? 0 : capacitySessionsOf(cell.fps, sizing.headroom, sizing.cameraFps);
|
|
51822
51773
|
}
|
|
51823
|
-
function fitsOn(cell,
|
|
51824
|
-
|
|
51825
|
-
return budget >= (demand?.demandFps ?? 0) && budget >= (demand?.maxCameraFps ?? 0);
|
|
51774
|
+
function fitsOn(cell, sizing) {
|
|
51775
|
+
return sessionsOn(cell, sizing) >= sizing.minSessions;
|
|
51826
51776
|
}
|
|
51827
51777
|
function candidatesFor(contexts, formatsByModel) {
|
|
51828
51778
|
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) => ({
|
|
@@ -51833,28 +51783,24 @@ function candidatesFor(contexts, formatsByModel) {
|
|
|
51833
51783
|
function totalLatency(contexts, modelId) {
|
|
51834
51784
|
return contexts.reduce((sum, c) => sum + (c.column[modelId]?.p50Ms ?? Number.POSITIVE_INFINITY), 0);
|
|
51835
51785
|
}
|
|
51836
|
-
|
|
51837
|
-
|
|
51838
|
-
|
|
51839
|
-
return Math.max(worst, cell ? utilisationOf(cell, c.demand) : Number.POSITIVE_INFINITY);
|
|
51840
|
-
}, 0);
|
|
51786
|
+
/** The fewest sessions any device of the group sustains with this model. */
|
|
51787
|
+
function worstSessions(contexts, modelId, sizing) {
|
|
51788
|
+
return contexts.reduce((worst, c) => Math.min(worst, sessionsOn(c.column[modelId], sizing)), Number.POSITIVE_INFINITY);
|
|
51841
51789
|
}
|
|
51842
51790
|
/** One model for a group of devices that must agree (step 2–4 of the header). */
|
|
51843
|
-
function pickForGroup(contexts, formatsByModel,
|
|
51791
|
+
function pickForGroup(contexts, formatsByModel, sizing) {
|
|
51844
51792
|
const candidates = candidatesFor(contexts, formatsByModel);
|
|
51845
51793
|
if (candidates.length === 0) return null;
|
|
51846
|
-
const fitting = candidates.filter((c) => contexts.every((ctx) =>
|
|
51847
|
-
const cell = ctx.column[c.modelId];
|
|
51848
|
-
return cell !== void 0 && fitsOn(cell, ctx.demand, headroom);
|
|
51849
|
-
}));
|
|
51794
|
+
const fitting = candidates.filter((c) => contexts.every((ctx) => fitsOn(ctx.column[c.modelId], sizing)));
|
|
51850
51795
|
if (fitting.length === 0) {
|
|
51851
|
-
const lightest = candidates.toSorted((a, b) =>
|
|
51796
|
+
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];
|
|
51852
51797
|
if (lightest === void 0) return null;
|
|
51853
51798
|
return {
|
|
51854
51799
|
modelId: lightest.modelId,
|
|
51855
51800
|
reasons: [{
|
|
51856
51801
|
kind: "no-fit-lightest",
|
|
51857
|
-
|
|
51802
|
+
sessions: worstSessions(contexts, lightest.modelId, sizing),
|
|
51803
|
+
minSessions: sizing.minSessions
|
|
51858
51804
|
}]
|
|
51859
51805
|
};
|
|
51860
51806
|
}
|
|
@@ -51864,10 +51810,7 @@ function pickForGroup(contexts, formatsByModel, headroom) {
|
|
|
51864
51810
|
const reasons = [];
|
|
51865
51811
|
const mostAccurate = candidates[0];
|
|
51866
51812
|
if (mostAccurate !== void 0 && mostAccurate.modelId !== best.modelId) {
|
|
51867
|
-
const limitedBy = contexts.find((ctx) =>
|
|
51868
|
-
const cell = ctx.column[mostAccurate.modelId];
|
|
51869
|
-
return cell === void 0 || !fitsOn(cell, ctx.demand, headroom);
|
|
51870
|
-
})?.device.key ?? "";
|
|
51813
|
+
const limitedBy = contexts.find((ctx) => !fitsOn(ctx.column[mostAccurate.modelId], sizing))?.device.key ?? "";
|
|
51871
51814
|
reasons.push({
|
|
51872
51815
|
kind: "stepped-down",
|
|
51873
51816
|
from: mostAccurate.modelId,
|
|
@@ -51885,7 +51828,7 @@ function pickForGroup(contexts, formatsByModel, headroom) {
|
|
|
51885
51828
|
reasons
|
|
51886
51829
|
};
|
|
51887
51830
|
}
|
|
51888
|
-
function choiceOn(ctx, modelId) {
|
|
51831
|
+
function choiceOn(ctx, modelId, sizing) {
|
|
51889
51832
|
const cell = ctx.column[modelId];
|
|
51890
51833
|
if (cell === void 0) return null;
|
|
51891
51834
|
return {
|
|
@@ -51893,23 +51836,20 @@ function choiceOn(ctx, modelId) {
|
|
|
51893
51836
|
p50Ms: cell.p50Ms,
|
|
51894
51837
|
fps: cell.fps,
|
|
51895
51838
|
measured: cell.measured,
|
|
51896
|
-
|
|
51839
|
+
capacitySessions: sessionsOn(cell, sizing),
|
|
51897
51840
|
personRecall: DETECTOR_ACCURACY[modelId]?.personRecall ?? 0
|
|
51898
51841
|
};
|
|
51899
51842
|
}
|
|
51900
|
-
function deviceReasonsFor(
|
|
51843
|
+
function deviceReasonsFor(choice, sizing) {
|
|
51901
51844
|
const out = [{
|
|
51902
|
-
kind: "
|
|
51903
|
-
|
|
51845
|
+
kind: "capacity",
|
|
51846
|
+
sessions: choice.capacitySessions,
|
|
51847
|
+
minSessions: sizing.minSessions
|
|
51904
51848
|
}];
|
|
51905
51849
|
if (!choice.measured) out.push({
|
|
51906
51850
|
kind: "derived-latency",
|
|
51907
51851
|
modelId: choice.modelId
|
|
51908
51852
|
});
|
|
51909
|
-
if (ctx.demand?.onMotionSource === "estimate") out.push({
|
|
51910
|
-
kind: "estimated-load",
|
|
51911
|
-
observedForMs: ctx.demand.observedForMs
|
|
51912
|
-
});
|
|
51913
51853
|
return out;
|
|
51914
51854
|
}
|
|
51915
51855
|
function skippedNode(node, reason, hardwareClass) {
|
|
@@ -51925,7 +51865,6 @@ function skippedNode(node, reason, hardwareClass) {
|
|
|
51925
51865
|
accelerator: node.hardware ? acceleratorKindOf(d.key, node.hardware) : null,
|
|
51926
51866
|
enabled: d.enabled,
|
|
51927
51867
|
available: d.available,
|
|
51928
|
-
demand: null,
|
|
51929
51868
|
chosen: null,
|
|
51930
51869
|
reasons: []
|
|
51931
51870
|
})),
|
|
@@ -51935,7 +51874,11 @@ function skippedNode(node, reason, hardwareClass) {
|
|
|
51935
51874
|
/** Recommend one node. See the module header. */
|
|
51936
51875
|
function recommendNode(input) {
|
|
51937
51876
|
const { node } = input;
|
|
51938
|
-
const
|
|
51877
|
+
const sizing = {
|
|
51878
|
+
headroom: input.headroom ?? .4,
|
|
51879
|
+
cameraFps: input.cameraFps ?? 10,
|
|
51880
|
+
minSessions: input.minSessionsPerDevice ?? 2
|
|
51881
|
+
};
|
|
51939
51882
|
const perf = input.perf ?? DETECTOR_PERF;
|
|
51940
51883
|
if (!node.reachable) return skippedNode(node, { kind: "node-offline" }, null);
|
|
51941
51884
|
if (node.hardware === null) return skippedNode(node, { kind: "no-hardware-facts" }, null);
|
|
@@ -51946,7 +51889,6 @@ function recommendNode(input) {
|
|
|
51946
51889
|
kind: "no-measured-data",
|
|
51947
51890
|
hardwareClass
|
|
51948
51891
|
}, hardwareClass);
|
|
51949
|
-
const demandByKey = new Map(input.demand.filter((d) => d.nodeId === node.nodeId).map((d) => [d.deviceKey, d]));
|
|
51950
51892
|
const devices = node.devices.toSorted((a, b) => a.key.localeCompare(b.key));
|
|
51951
51893
|
const contexts = /* @__PURE__ */ new Map();
|
|
51952
51894
|
const early = /* @__PURE__ */ new Map();
|
|
@@ -51975,8 +51917,7 @@ function recommendNode(input) {
|
|
|
51975
51917
|
contexts.set(device.key, {
|
|
51976
51918
|
device,
|
|
51977
51919
|
accelerator,
|
|
51978
|
-
column
|
|
51979
|
-
demand: demandByKey.get(device.key) ?? null
|
|
51920
|
+
column
|
|
51980
51921
|
});
|
|
51981
51922
|
}
|
|
51982
51923
|
const isCpu = (a) => a === "x86-cpu" || a === "arm-cpu";
|
|
@@ -51986,23 +51927,23 @@ function recommendNode(input) {
|
|
|
51986
51927
|
const reasonsByKey = /* @__PURE__ */ new Map();
|
|
51987
51928
|
const nodeReasons = [];
|
|
51988
51929
|
let acceleratorModelId = null;
|
|
51989
|
-
const groupPick = accelerators.length > 0 ? pickForGroup(accelerators, node.formatsByModel,
|
|
51930
|
+
const groupPick = accelerators.length > 0 ? pickForGroup(accelerators, node.formatsByModel, sizing) : null;
|
|
51990
51931
|
if (groupPick !== null) {
|
|
51991
51932
|
acceleratorModelId = groupPick.modelId;
|
|
51992
51933
|
nodeReasons.push(...groupPick.reasons);
|
|
51993
51934
|
for (const ctx of accelerators) {
|
|
51994
|
-
const choice = choiceOn(ctx, groupPick.modelId);
|
|
51935
|
+
const choice = choiceOn(ctx, groupPick.modelId, sizing);
|
|
51995
51936
|
if (choice === null) continue;
|
|
51996
51937
|
chosenByKey.set(ctx.device.key, choice);
|
|
51997
|
-
reasonsByKey.set(ctx.device.key, deviceReasonsFor(
|
|
51938
|
+
reasonsByKey.set(ctx.device.key, deviceReasonsFor(choice, sizing));
|
|
51998
51939
|
}
|
|
51999
51940
|
} else for (const ctx of accelerators) reasonsByKey.set(ctx.device.key, [{
|
|
52000
51941
|
kind: "no-family-build",
|
|
52001
51942
|
format: ctx.device.format
|
|
52002
51943
|
}]);
|
|
52003
51944
|
for (const ctx of cpus) {
|
|
52004
|
-
const pick = pickForGroup([ctx], node.formatsByModel,
|
|
52005
|
-
const choice = pick === null ? null : choiceOn(ctx, pick.modelId);
|
|
51945
|
+
const pick = pickForGroup([ctx], node.formatsByModel, sizing);
|
|
51946
|
+
const choice = pick === null ? null : choiceOn(ctx, pick.modelId, sizing);
|
|
52006
51947
|
if (pick === null || choice === null) {
|
|
52007
51948
|
reasonsByKey.set(ctx.device.key, [{
|
|
52008
51949
|
kind: "no-family-build",
|
|
@@ -52014,7 +51955,7 @@ function recommendNode(input) {
|
|
|
52014
51955
|
reasonsByKey.set(ctx.device.key, [
|
|
52015
51956
|
{ kind: "cpu-device" },
|
|
52016
51957
|
...pick.reasons,
|
|
52017
|
-
...deviceReasonsFor(
|
|
51958
|
+
...deviceReasonsFor(choice, sizing)
|
|
52018
51959
|
]);
|
|
52019
51960
|
}
|
|
52020
51961
|
return {
|
|
@@ -52029,7 +51970,6 @@ function recommendNode(input) {
|
|
|
52029
51970
|
accelerator: acceleratorKindOf(device.key, hardware),
|
|
52030
51971
|
enabled: device.enabled,
|
|
52031
51972
|
available: device.available,
|
|
52032
|
-
demand: demandByKey.get(device.key) ?? null,
|
|
52033
51973
|
chosen: chosenByKey.get(device.key) ?? null,
|
|
52034
51974
|
reasons: early.get(device.key) ?? reasonsByKey.get(device.key) ?? []
|
|
52035
51975
|
})),
|
|
@@ -52076,6 +52016,18 @@ function electByShare(candidates) {
|
|
|
52076
52016
|
reason: "share"
|
|
52077
52017
|
};
|
|
52078
52018
|
}
|
|
52019
|
+
/**
|
|
52020
|
+
* The shares a session election actually reads, and the view the Load
|
|
52021
|
+
* balancing tab shows (D750). Pure.
|
|
52022
|
+
*
|
|
52023
|
+
* The SOURCE decides the configured weights: the automatic capacity shares
|
|
52024
|
+
* (`hardware` or `benchmark`, D755), or the operator's stored sliders. Eligibility decides the effective ones: a
|
|
52025
|
+
* node or device that cannot take a session now (offline, switched off, no
|
|
52026
|
+
* detect capability, unable to run the root) contributes nothing, and the
|
|
52027
|
+
* others split its share in proportion to their own. That redistribution is
|
|
52028
|
+
* exactly what `electByShare` does over its candidate set, so the view and
|
|
52029
|
+
* the election cannot disagree; the stored values are never rewritten for it.
|
|
52030
|
+
*/
|
|
52079
52031
|
/** Weight a node or device has when its source names none (the balancer's own default). */
|
|
52080
52032
|
var DEFAULT_WEIGHT = 1;
|
|
52081
52033
|
function positive(value) {
|
|
@@ -52086,7 +52038,8 @@ function percentages(keys, weightOf, counts) {
|
|
|
52086
52038
|
return new Map(keys.map((k) => [k, total > 0 && counts(k) ? Math.round(weightOf(k) / total * 1e3) / 10 : 0]));
|
|
52087
52039
|
}
|
|
52088
52040
|
function resolveEffectiveShares(input) {
|
|
52089
|
-
const
|
|
52041
|
+
const useAuto = isAutomaticShareSource(input.source) && input.auto !== null;
|
|
52042
|
+
const weights = useAuto && input.auto !== null ? input.auto : input.stored;
|
|
52090
52043
|
const nodeWeights = {};
|
|
52091
52044
|
for (const nodeId of input.nodeIds) nodeWeights[nodeId] = positive(weights.nodes[nodeId]);
|
|
52092
52045
|
const eligibleNodes = new Set(input.eligibleNodes);
|
|
@@ -52108,18 +52061,21 @@ function resolveEffectiveShares(input) {
|
|
|
52108
52061
|
configuredPct: devConfigured.get(deviceKey) ?? 0,
|
|
52109
52062
|
effectivePct: devEffective.get(deviceKey) ?? 0
|
|
52110
52063
|
}));
|
|
52064
|
+
const capacity = useAuto ? input.autoCapacity?.[nodeId] : void 0;
|
|
52111
52065
|
return {
|
|
52112
52066
|
nodeId,
|
|
52113
52067
|
eligible: eligibleNodes.has(nodeId),
|
|
52114
52068
|
configuredPct: configured.get(nodeId) ?? 0,
|
|
52115
52069
|
effectivePct: effective.get(nodeId) ?? 0,
|
|
52070
|
+
capacitySource: capacity?.source ?? null,
|
|
52071
|
+
capacityFallback: capacity?.fallback ?? null,
|
|
52116
52072
|
devices
|
|
52117
52073
|
};
|
|
52118
52074
|
});
|
|
52119
52075
|
return {
|
|
52120
52076
|
source: input.source,
|
|
52121
52077
|
policy: input.policy ?? "share",
|
|
52122
|
-
autoPending: input.source
|
|
52078
|
+
autoPending: isAutomaticShareSource(input.source) && input.auto === null,
|
|
52123
52079
|
computedAt: input.computedAt ?? null,
|
|
52124
52080
|
trigger: input.trigger ?? null,
|
|
52125
52081
|
nodes,
|
|
@@ -52128,6 +52084,137 @@ function resolveEffectiveShares(input) {
|
|
|
52128
52084
|
};
|
|
52129
52085
|
}
|
|
52130
52086
|
/**
|
|
52087
|
+
* Automatic shares from CAPACITY (D755). Pure.
|
|
52088
|
+
*
|
|
52089
|
+
* A node's share is its capacity in parallel sessions over the cluster's; a
|
|
52090
|
+
* device's share inside its node follows its throughput. There is no load
|
|
52091
|
+
* input: nothing here depends on how many sessions ran, only on what each
|
|
52092
|
+
* node can carry.
|
|
52093
|
+
*
|
|
52094
|
+
* Two places a capacity can come from:
|
|
52095
|
+
* - `hardware` — each enabled accelerator at its Auto tune model, from the
|
|
52096
|
+
* measured latency table: ⌊fps × (1 − headroom) / camera fps⌋ sessions;
|
|
52097
|
+
* - `benchmark` — the node's own measured max sessions per device, read
|
|
52098
|
+
* through a {@link NodeCapacitySource}. A node whose results do not cover
|
|
52099
|
+
* every enabled device it would be sized on is sized from `hardware`
|
|
52100
|
+
* instead, and the answer says so per node — never a silent mix.
|
|
52101
|
+
*/
|
|
52102
|
+
/** A source that has no results — the benchmark store before it exists. */
|
|
52103
|
+
var NO_MEASURED_CAPACITY = { readMeasuredCapacity: async () => /* @__PURE__ */ new Map() };
|
|
52104
|
+
/** Integer shares summing to 100 (largest remainder); ties broken by key. */
|
|
52105
|
+
function toPercentShares(weights) {
|
|
52106
|
+
const total = [...weights.values()].reduce((a, b) => a + b, 0);
|
|
52107
|
+
const out = /* @__PURE__ */ new Map();
|
|
52108
|
+
if (total <= 0) return out;
|
|
52109
|
+
const raw = [...weights].map(([k, w]) => ({
|
|
52110
|
+
k,
|
|
52111
|
+
exact: w / total * 100
|
|
52112
|
+
}));
|
|
52113
|
+
let assigned = 0;
|
|
52114
|
+
for (const r of raw) {
|
|
52115
|
+
out.set(r.k, Math.floor(r.exact));
|
|
52116
|
+
assigned += Math.floor(r.exact);
|
|
52117
|
+
}
|
|
52118
|
+
const remainder = (exact) => Math.round((exact - Math.floor(exact)) * 1e6);
|
|
52119
|
+
const byRemainder = raw.toSorted((a, b) => remainder(b.exact) - remainder(a.exact) || a.k.localeCompare(b.k));
|
|
52120
|
+
for (let i = 0; i < 100 - assigned; i++) {
|
|
52121
|
+
const r = byRemainder[i % byRemainder.length];
|
|
52122
|
+
if (r === void 0) break;
|
|
52123
|
+
out.set(r.k, (out.get(r.k) ?? 0) + 1);
|
|
52124
|
+
}
|
|
52125
|
+
return out;
|
|
52126
|
+
}
|
|
52127
|
+
function hardwareDevices(rec, headroom, cameraFps) {
|
|
52128
|
+
return rec.devices.filter((d) => d.enabled && d.available && d.chosen !== null).map((d) => {
|
|
52129
|
+
const fps = d.chosen?.fps ?? 0;
|
|
52130
|
+
return {
|
|
52131
|
+
deviceKey: d.deviceKey,
|
|
52132
|
+
sessions: capacitySessionsOf(fps, headroom, cameraFps),
|
|
52133
|
+
weight: Math.max(fps, 0),
|
|
52134
|
+
modelId: d.chosen?.modelId ?? null,
|
|
52135
|
+
fps: d.chosen?.fps ?? null,
|
|
52136
|
+
measured: d.chosen?.measured ?? false
|
|
52137
|
+
};
|
|
52138
|
+
});
|
|
52139
|
+
}
|
|
52140
|
+
function sizeNode(rec, input) {
|
|
52141
|
+
const fromHardware = (fallback) => {
|
|
52142
|
+
const devices = rec.skipped ? [] : hardwareDevices(rec, input.headroom, input.cameraFps);
|
|
52143
|
+
const capacity = devices.reduce((sum, d) => sum + d.sessions, 0);
|
|
52144
|
+
return {
|
|
52145
|
+
rec,
|
|
52146
|
+
devices,
|
|
52147
|
+
capacitySource: rec.skipped ? null : "hardware",
|
|
52148
|
+
capacityFallback: rec.skipped ? null : fallback,
|
|
52149
|
+
skippedReason: rec.skipped ? rec.reasons[0]?.kind ?? "skipped" : capacity > 0 ? null : "no-capacity"
|
|
52150
|
+
};
|
|
52151
|
+
};
|
|
52152
|
+
if (input.source === "hardware") return fromHardware(null);
|
|
52153
|
+
if (!rec.reachable) return fromHardware(null);
|
|
52154
|
+
const results = input.measured.get(rec.nodeId);
|
|
52155
|
+
const byKey = new Map((results?.devices ?? []).map((d) => [d.deviceKey, d]));
|
|
52156
|
+
const usable = rec.devices.filter((d) => d.enabled && d.available);
|
|
52157
|
+
const mustCover = rec.devices.filter((d) => d.enabled && d.available && d.chosen !== null);
|
|
52158
|
+
const covered = usable.filter((d) => byKey.has(d.deviceKey));
|
|
52159
|
+
if (covered.length === 0) return fromHardware("no-benchmark-results");
|
|
52160
|
+
if (mustCover.some((d) => !byKey.has(d.deviceKey))) return fromHardware("partial-benchmark-results");
|
|
52161
|
+
const devices = covered.map((d) => {
|
|
52162
|
+
const row = byKey.get(d.deviceKey);
|
|
52163
|
+
const sessions = Math.max(0, Math.floor(row?.maxSessions ?? 0));
|
|
52164
|
+
return {
|
|
52165
|
+
deviceKey: d.deviceKey,
|
|
52166
|
+
sessions,
|
|
52167
|
+
weight: sessions,
|
|
52168
|
+
modelId: row?.modelId ?? null,
|
|
52169
|
+
fps: null,
|
|
52170
|
+
measured: true
|
|
52171
|
+
};
|
|
52172
|
+
});
|
|
52173
|
+
return {
|
|
52174
|
+
rec,
|
|
52175
|
+
devices,
|
|
52176
|
+
capacitySource: "benchmark",
|
|
52177
|
+
capacityFallback: null,
|
|
52178
|
+
skippedReason: devices.reduce((sum, d) => sum + d.sessions, 0) > 0 ? null : "no-capacity"
|
|
52179
|
+
};
|
|
52180
|
+
}
|
|
52181
|
+
/** Shares proportional to capacity — the automatic shares and the suggestion are this. */
|
|
52182
|
+
function computeCapacityShares(input) {
|
|
52183
|
+
const sized = input.recommendations.map((rec) => sizeNode(rec, input));
|
|
52184
|
+
const capacityByNode = /* @__PURE__ */ new Map();
|
|
52185
|
+
for (const node of sized) {
|
|
52186
|
+
const capacity = node.devices.reduce((sum, d) => sum + d.sessions, 0);
|
|
52187
|
+
if (node.skippedReason === null && capacity > 0) capacityByNode.set(node.rec.nodeId, capacity);
|
|
52188
|
+
}
|
|
52189
|
+
const nodePct = toPercentShares(capacityByNode);
|
|
52190
|
+
const nodes = sized.map((node) => {
|
|
52191
|
+
const devicePct = toPercentShares(new Map(node.devices.map((d) => [d.deviceKey, d.weight])));
|
|
52192
|
+
return {
|
|
52193
|
+
nodeId: node.rec.nodeId,
|
|
52194
|
+
share: nodePct.get(node.rec.nodeId) ?? null,
|
|
52195
|
+
capacitySessions: node.devices.reduce((sum, d) => sum + d.sessions, 0),
|
|
52196
|
+
skippedReason: node.skippedReason,
|
|
52197
|
+
capacitySource: node.capacitySource,
|
|
52198
|
+
capacityFallback: node.capacityFallback,
|
|
52199
|
+
devices: node.devices.map((d) => ({
|
|
52200
|
+
deviceKey: d.deviceKey,
|
|
52201
|
+
share: devicePct.get(d.deviceKey) ?? 0,
|
|
52202
|
+
modelId: d.modelId,
|
|
52203
|
+
fps: d.fps,
|
|
52204
|
+
measured: d.measured,
|
|
52205
|
+
suggestedMaxSessions: d.sessions
|
|
52206
|
+
}))
|
|
52207
|
+
};
|
|
52208
|
+
});
|
|
52209
|
+
return {
|
|
52210
|
+
headroom: input.headroom,
|
|
52211
|
+
source: input.source,
|
|
52212
|
+
cameraFps: input.cameraFps,
|
|
52213
|
+
minSessionsPerDevice: input.minSessionsPerDevice,
|
|
52214
|
+
nodes
|
|
52215
|
+
};
|
|
52216
|
+
}
|
|
52217
|
+
/**
|
|
52131
52218
|
* THE detail-crop convention — the single derivation of the rectangle a
|
|
52132
52219
|
* detail/enrichment step (clip-embedding, face-detection, plate-detection…)
|
|
52133
52220
|
* is fed.
|
|
@@ -64599,6 +64686,7 @@ var StoredCameraPipelineForAgentSchema = object({
|
|
|
64599
64686
|
});
|
|
64600
64687
|
var StoredCameraPipelineSettingsSchema = object({
|
|
64601
64688
|
stepToggles: record(string(), boolean()).optional(),
|
|
64689
|
+
stepSettings: record(string(), record(string(), unknown()).readonly()).optional(),
|
|
64602
64690
|
stepOverridesByDevice: record(string(), record(string(), record(string(), StoredCameraStepOverridePatchSchema))).optional(),
|
|
64603
64691
|
stepOverridesByAgent: record(string(), record(string(), LegacyCameraStepOverridePatchSchema)).optional(),
|
|
64604
64692
|
pipelineByAgent: record(string(), StoredCameraPipelineForAgentSchema).optional(),
|
|
@@ -64918,6 +65006,30 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
64918
65006
|
};
|
|
64919
65007
|
});
|
|
64920
65008
|
}
|
|
65009
|
+
/**
|
|
65010
|
+
* Write or clear one camera's CAMERA-WIDE settings for `addonId` (D754): the
|
|
65011
|
+
* field-level bag merged between the node base and the per-(node,device)
|
|
65012
|
+
* override on every accelerator. `null` or an empty bag deletes the step's
|
|
65013
|
+
* entry; an empty layer is pruned. Read-patch-write, immutable.
|
|
65014
|
+
*/
|
|
65015
|
+
async writeCameraStepSettings(deviceId, addonId, settings) {
|
|
65016
|
+
await this.cameraSettingsState.update((prev) => {
|
|
65017
|
+
const key = String(deviceId);
|
|
65018
|
+
const camera = prev[key] ?? {};
|
|
65019
|
+
const nextLayer = { ...camera.stepSettings };
|
|
65020
|
+
if (settings === null || Object.keys(settings).length === 0) delete nextLayer[addonId];
|
|
65021
|
+
else nextLayer[addonId] = { ...settings };
|
|
65022
|
+
const { stepSettings: _drop, ...rest } = camera;
|
|
65023
|
+
const nextCamera = {
|
|
65024
|
+
...rest,
|
|
65025
|
+
...Object.keys(nextLayer).length > 0 ? { stepSettings: nextLayer } : {}
|
|
65026
|
+
};
|
|
65027
|
+
return {
|
|
65028
|
+
...prev,
|
|
65029
|
+
[key]: nextCamera
|
|
65030
|
+
};
|
|
65031
|
+
});
|
|
65032
|
+
}
|
|
64921
65033
|
/** Purge one device's entry through the durable handle (whole-blob round-trip). */
|
|
64922
65034
|
async deleteCameraSettings(deviceId) {
|
|
64923
65035
|
await this.cameraSettingsState.update((prev) => {
|
|
@@ -65484,13 +65596,14 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
65484
65596
|
async readDeviceProvisioning(deviceId, nodeId, deviceKeys) {
|
|
65485
65597
|
const [agentBlob, cameraMap] = await Promise.all([this.agentSettingsState.get(), this.readCameraSettingsMap()]);
|
|
65486
65598
|
const devices = agentBlob[nodeId]?.inferenceDevices;
|
|
65487
|
-
const
|
|
65599
|
+
const camera = cameraMap[String(deviceId)];
|
|
65600
|
+
const byDevice = camera?.stepOverridesByDevice?.[nodeId];
|
|
65488
65601
|
const out = /* @__PURE__ */ new Map();
|
|
65489
65602
|
for (const deviceKey of deviceKeys) {
|
|
65490
|
-
const
|
|
65603
|
+
const perDevice = deviceKey === "" ? void 0 : byDevice?.[deviceKey];
|
|
65491
65604
|
out.set(deviceKey, {
|
|
65492
65605
|
base: foldNodeStepSettings(devices, deviceKey),
|
|
65493
|
-
override:
|
|
65606
|
+
override: layerCameraStepSettings(camera?.stepSettings, perDevice)
|
|
65494
65607
|
});
|
|
65495
65608
|
}
|
|
65496
65609
|
return out;
|
|
@@ -65554,6 +65667,30 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
65554
65667
|
return `tpl_${Date.now().toString(36).slice(-6)}${rand}`;
|
|
65555
65668
|
}
|
|
65556
65669
|
};
|
|
65670
|
+
/**
|
|
65671
|
+
* The camera's override for one accelerator, with its CAMERA-WIDE settings
|
|
65672
|
+
* (D754) folded underneath: per step, `settings = { ...camera, ...perDevice }`
|
|
65673
|
+
* (a per-accelerator key wins), and the per-accelerator `modelId` kept as is —
|
|
65674
|
+
* the camera layer carries no model. The result is what
|
|
65675
|
+
* `applyDeviceProvisioning` merges over the node base, so the full order is
|
|
65676
|
+
* node ⊕ camera ⊕ camera-per-accelerator. Fresh objects; a camera with neither
|
|
65677
|
+
* layer yields `{}`.
|
|
65678
|
+
*/
|
|
65679
|
+
function layerCameraStepSettings(cameraWide, perDevice) {
|
|
65680
|
+
const out = { ...perDevice };
|
|
65681
|
+
for (const [stepId, settings] of Object.entries(cameraWide ?? {})) {
|
|
65682
|
+
if (Object.keys(settings).length === 0) continue;
|
|
65683
|
+
const own = out[stepId];
|
|
65684
|
+
out[stepId] = {
|
|
65685
|
+
...own,
|
|
65686
|
+
settings: {
|
|
65687
|
+
...settings,
|
|
65688
|
+
...own?.settings
|
|
65689
|
+
}
|
|
65690
|
+
};
|
|
65691
|
+
}
|
|
65692
|
+
return out;
|
|
65693
|
+
}
|
|
65557
65694
|
//#endregion
|
|
65558
65695
|
//#region src/pipeline-watchdog.ts
|
|
65559
65696
|
var DEFAULT_WATCHDOG_THRESHOLDS = {
|
|
@@ -69176,7 +69313,6 @@ async function buildOrchestratorControllers(deps) {
|
|
|
69176
69313
|
readGlobalSettings: () => globalSettings,
|
|
69177
69314
|
onCensus: (entries) => {
|
|
69178
69315
|
motionSliceGate?.noteCensus(entries);
|
|
69179
|
-
deps.onRunnerCensus(entries.flatMap((e) => e.load === null ? [] : [e.load]));
|
|
69180
69316
|
providerSliceGate?.refreshIfStale("runner-census");
|
|
69181
69317
|
}
|
|
69182
69318
|
});
|
|
@@ -70776,11 +70912,13 @@ function applyCameraAutoTune(blob, input) {
|
|
|
70776
70912
|
//#endregion
|
|
70777
70913
|
//#region src/auto-tune/auto-tune-service.ts
|
|
70778
70914
|
/**
|
|
70779
|
-
* Auto tune (D745): gather the inputs, compute the plan, and apply
|
|
70780
|
-
* the plan the operator confirmed.
|
|
70915
|
+
* Auto tune (D745, D755): gather the inputs, compute the plan, and apply
|
|
70916
|
+
* exactly the plan the operator confirmed.
|
|
70781
70917
|
*
|
|
70782
70918
|
* The plan is the recommender (`@camstack/types` auto-tune) over every
|
|
70783
|
-
* runner node
|
|
70919
|
+
* runner node — sized by a capacity floor (`minSessionsPerDevice` parallel
|
|
70920
|
+
* sessions per device at the camera rate), never by observed or estimated
|
|
70921
|
+
* load — plus the diff of the writes (`plan-diff.ts`) and the
|
|
70784
70922
|
* per-camera detector thresholds the hub's executor would clear. Its
|
|
70785
70923
|
* `planHash` names everything the apply writes; the apply recomputes the plan
|
|
70786
70924
|
* and refuses a different hash, so a configuration change between the two is
|
|
@@ -70797,6 +70935,9 @@ function applyCameraAutoTune(blob, input) {
|
|
|
70797
70935
|
* A failure after phase 2 leaves "overrides cleared, previous node model",
|
|
70798
70936
|
* which is a valid configuration (the defaults run); re-running converges.
|
|
70799
70937
|
*/
|
|
70938
|
+
function minSessionsOf(input) {
|
|
70939
|
+
return input.minSessionsPerDevice ?? 2;
|
|
70940
|
+
}
|
|
70800
70941
|
function errMsg(err) {
|
|
70801
70942
|
return err instanceof Error ? err.message : String(err);
|
|
70802
70943
|
}
|
|
@@ -70825,29 +70966,6 @@ function boundKeysOf(catalogs) {
|
|
|
70825
70966
|
}
|
|
70826
70967
|
return Object.fromEntries([...byStep].map(([step, set]) => [step, [...set].toSorted()]));
|
|
70827
70968
|
}
|
|
70828
|
-
/** Bounded fixed point of the hardware share suggestion (D748). */
|
|
70829
|
-
var SUGGEST_MAX_ROUNDS = 3;
|
|
70830
|
-
/** Integer shares summing to 100 (largest remainder). */
|
|
70831
|
-
function toPercent(weights) {
|
|
70832
|
-
const total = [...weights.values()].reduce((a, b) => a + b, 0);
|
|
70833
|
-
const out = /* @__PURE__ */ new Map();
|
|
70834
|
-
if (total <= 0) return out;
|
|
70835
|
-
const raw = [...weights].map(([k, w]) => ({
|
|
70836
|
-
k,
|
|
70837
|
-
exact: w / total * 100
|
|
70838
|
-
}));
|
|
70839
|
-
let assigned = 0;
|
|
70840
|
-
for (const r of raw) {
|
|
70841
|
-
out.set(r.k, Math.floor(r.exact));
|
|
70842
|
-
assigned += Math.floor(r.exact);
|
|
70843
|
-
}
|
|
70844
|
-
const byRemainder = raw.toSorted((a, b) => b.exact - Math.floor(b.exact) - (a.exact - Math.floor(a.exact)) || a.k.localeCompare(b.k));
|
|
70845
|
-
for (let i = 0; i < 100 - assigned; i++) {
|
|
70846
|
-
const r = byRemainder[i % byRemainder.length];
|
|
70847
|
-
out.set(r.k, (out.get(r.k) ?? 0) + 1);
|
|
70848
|
-
}
|
|
70849
|
-
return out;
|
|
70850
|
-
}
|
|
70851
70969
|
var AutoTuneService = class {
|
|
70852
70970
|
deps;
|
|
70853
70971
|
applying = false;
|
|
@@ -70855,87 +70973,38 @@ var AutoTuneService = class {
|
|
|
70855
70973
|
this.deps = deps;
|
|
70856
70974
|
}
|
|
70857
70975
|
async plan(input) {
|
|
70858
|
-
return (await this.computePlan(input.headroom, input
|
|
70976
|
+
return (await this.computePlan(input.headroom, minSessionsOf(input))).plan;
|
|
70859
70977
|
}
|
|
70860
70978
|
/**
|
|
70861
|
-
* Shares proportional to each node's capacity
|
|
70862
|
-
*
|
|
70863
|
-
*
|
|
70864
|
-
*
|
|
70865
|
-
*
|
|
70979
|
+
* Shares proportional to each node's capacity in parallel sessions (D755):
|
|
70980
|
+
* from the latency table at the models Auto tune picks at its defaults
|
|
70981
|
+
* (`hardware`), or from node benchmark results (`benchmark`), a node
|
|
70982
|
+
* without results falling back to `hardware` and saying so. ONE pass, and
|
|
70983
|
+
* nothing but capacity goes in — the automatic shares are exactly this.
|
|
70984
|
+
* Per device it reports an ADVISORY session cap — never written, never
|
|
70985
|
+
* enforced.
|
|
70866
70986
|
*/
|
|
70867
70987
|
async suggestShares(input) {
|
|
70868
|
-
|
|
70869
|
-
|
|
70870
|
-
|
|
70871
|
-
|
|
70872
|
-
|
|
70873
|
-
|
|
70874
|
-
|
|
70875
|
-
|
|
70876
|
-
rounds = round;
|
|
70877
|
-
const rec = await this.recommend(input.headroom, shares);
|
|
70878
|
-
cameraFps = rec.cameraFps;
|
|
70879
|
-
result = this.sharesFrom(rec, input.headroom);
|
|
70880
|
-
const next = {
|
|
70881
|
-
nodes: Object.fromEntries(result.flatMap((n) => n.share !== null && n.share > 0 ? [[n.nodeId, n.share]] : [])),
|
|
70882
|
-
devices: Object.fromEntries(result.map((n) => [n.nodeId, Object.fromEntries(n.devices.flatMap((d) => d.share > 0 ? [[d.deviceKey, d.share]] : []))]))
|
|
70883
|
-
};
|
|
70884
|
-
const settled = JSON.stringify(next) === JSON.stringify(shares);
|
|
70885
|
-
shares = next;
|
|
70886
|
-
if (settled) break;
|
|
70988
|
+
const source = input.source ?? "hardware";
|
|
70989
|
+
const minSessionsPerDevice = 2;
|
|
70990
|
+
const rec = await this.recommend(input.headroom, minSessionsPerDevice);
|
|
70991
|
+
let measured = /* @__PURE__ */ new Map();
|
|
70992
|
+
if (source === "benchmark") try {
|
|
70993
|
+
measured = await this.deps.measuredCapacity.readMeasuredCapacity(rec.recommendations.map((r) => r.nodeId));
|
|
70994
|
+
} catch (err) {
|
|
70995
|
+
this.deps.logger.warn("load balancing: benchmark results unreadable — sized from hardware", { meta: { error: errMsg(err) } });
|
|
70887
70996
|
}
|
|
70888
|
-
return {
|
|
70997
|
+
return computeCapacityShares({
|
|
70998
|
+
recommendations: rec.recommendations,
|
|
70999
|
+
source,
|
|
71000
|
+
measured,
|
|
70889
71001
|
headroom: input.headroom,
|
|
70890
|
-
cameraFps,
|
|
70891
|
-
|
|
70892
|
-
nodes: [...result]
|
|
70893
|
-
};
|
|
70894
|
-
}
|
|
70895
|
-
sharesFrom(rec, headroom) {
|
|
70896
|
-
const capacityByNode = /* @__PURE__ */ new Map();
|
|
70897
|
-
const perNode = rec.recommendations.map((r) => {
|
|
70898
|
-
const devices = r.devices.filter((d) => d.enabled && d.available && d.chosen !== null).map((d) => {
|
|
70899
|
-
const fps = d.chosen?.fps ?? 0;
|
|
70900
|
-
return {
|
|
70901
|
-
d,
|
|
70902
|
-
fps,
|
|
70903
|
-
sessions: rec.cameraFps > 0 ? Math.floor(fps * (1 - headroom) / rec.cameraFps) : 0
|
|
70904
|
-
};
|
|
70905
|
-
});
|
|
70906
|
-
const capacity = devices.reduce((sum, x) => sum + x.sessions, 0);
|
|
70907
|
-
if (!r.skipped && capacity > 0) capacityByNode.set(r.nodeId, capacity);
|
|
70908
|
-
return {
|
|
70909
|
-
r,
|
|
70910
|
-
devices,
|
|
70911
|
-
capacity
|
|
70912
|
-
};
|
|
70913
|
-
});
|
|
70914
|
-
const nodePct = toPercent(capacityByNode);
|
|
70915
|
-
return perNode.map(({ r, devices, capacity }) => {
|
|
70916
|
-
const devicePct = toPercent(new Map(devices.map((x) => [x.d.deviceKey, Math.max(x.fps, 0)])));
|
|
70917
|
-
return {
|
|
70918
|
-
nodeId: r.nodeId,
|
|
70919
|
-
share: nodePct.get(r.nodeId) ?? null,
|
|
70920
|
-
capacitySessions: capacity,
|
|
70921
|
-
skippedReason: r.skipped ? r.reasons[0]?.kind ?? "skipped" : capacity > 0 ? null : "no-capacity",
|
|
70922
|
-
devices: devices.map((x) => ({
|
|
70923
|
-
deviceKey: x.d.deviceKey,
|
|
70924
|
-
share: devicePct.get(x.d.deviceKey) ?? 0,
|
|
70925
|
-
modelId: x.d.chosen?.modelId ?? null,
|
|
70926
|
-
fps: x.d.chosen?.fps ?? null,
|
|
70927
|
-
measured: x.d.chosen?.measured ?? false,
|
|
70928
|
-
suggestedMaxSessions: x.sessions
|
|
70929
|
-
}))
|
|
70930
|
-
};
|
|
71002
|
+
cameraFps: rec.cameraFps,
|
|
71003
|
+
minSessionsPerDevice
|
|
70931
71004
|
});
|
|
70932
71005
|
}
|
|
70933
|
-
/**
|
|
70934
|
-
|
|
70935
|
-
* round) forces share placement with those shares overlaid on the stored
|
|
70936
|
-
* ones; absent, the STORED policy decides how on-motion load is charged.
|
|
70937
|
-
*/
|
|
70938
|
-
async recommend(headroom, shares) {
|
|
71006
|
+
/** Gather every node and recommend. Nothing but hardware, catalog and the sizing goes in. */
|
|
71007
|
+
async recommend(headroom, minSessionsPerDevice) {
|
|
70939
71008
|
const nodeIds = [...new Set(["hub", ...this.deps.runnerNodeIds()])].toSorted();
|
|
70940
71009
|
const gathered = await Promise.all(nodeIds.map(async (nodeId) => {
|
|
70941
71010
|
const [devices, hardware, catalog] = await Promise.all([
|
|
@@ -70950,8 +71019,6 @@ var AutoTuneService = class {
|
|
|
70950
71019
|
catalog
|
|
70951
71020
|
};
|
|
70952
71021
|
}));
|
|
70953
|
-
const effective = await this.deps.readEffectiveWeights();
|
|
70954
|
-
const deviceShare = (nodeId, key, stored) => shares?.devices[nodeId]?.[key] ?? effective.devices[nodeId]?.[key] ?? stored;
|
|
70955
71022
|
const nodes = gathered.map((g) => ({
|
|
70956
71023
|
nodeId: g.nodeId,
|
|
70957
71024
|
reachable: g.devices.reachable && g.catalog !== null,
|
|
@@ -70964,40 +71031,22 @@ var AutoTuneService = class {
|
|
|
70964
71031
|
})),
|
|
70965
71032
|
formatsByModel: formatsByModelOf(g.catalog)
|
|
70966
71033
|
}));
|
|
70967
|
-
const
|
|
70968
|
-
|
|
70969
|
-
deviceKey: d.key,
|
|
70970
|
-
weight: deviceShare(g.nodeId, d.key, d.weight)
|
|
70971
|
-
})) : []);
|
|
70972
|
-
const cameras = await this.deps.cameraLoads();
|
|
70973
|
-
const policy = shares ? "share" : this.deps.sessionPlacement();
|
|
70974
|
-
const demand = estimateDeviceDemand({
|
|
70975
|
-
cameras,
|
|
70976
|
-
eligible,
|
|
70977
|
-
peaks: this.deps.peaks(),
|
|
70978
|
-
...policy === "share" ? { shares: {
|
|
70979
|
-
nodes: {
|
|
70980
|
-
...effective.nodes,
|
|
70981
|
-
...shares?.nodes
|
|
70982
|
-
},
|
|
70983
|
-
clusterPeak: this.deps.clusterPeak()
|
|
70984
|
-
} } : {}
|
|
70985
|
-
});
|
|
70986
|
-
const recommendations = nodes.map((node) => recommendNode({
|
|
70987
|
-
node,
|
|
70988
|
-
demand,
|
|
70989
|
-
headroom
|
|
70990
|
-
}));
|
|
70991
|
-
const cameraFps = cameras.reduce((m, c) => c.mode !== "disabled" && c.runsPipelineRoot ? Math.max(m, c.fps) : m, 0);
|
|
71034
|
+
const observedFps = await this.deps.cameraFps();
|
|
71035
|
+
const cameraFps = observedFps !== null && observedFps > 0 ? observedFps : 10;
|
|
70992
71036
|
return {
|
|
70993
71037
|
gathered,
|
|
70994
71038
|
nodes,
|
|
70995
|
-
recommendations
|
|
70996
|
-
|
|
71039
|
+
recommendations: nodes.map((node) => recommendNode({
|
|
71040
|
+
node,
|
|
71041
|
+
headroom,
|
|
71042
|
+
minSessionsPerDevice,
|
|
71043
|
+
cameraFps
|
|
71044
|
+
})),
|
|
71045
|
+
cameraFps
|
|
70997
71046
|
};
|
|
70998
71047
|
}
|
|
70999
|
-
async computePlan(headroom,
|
|
71000
|
-
const { gathered, nodes, recommendations } = await this.recommend(headroom,
|
|
71048
|
+
async computePlan(headroom, minSessionsPerDevice) {
|
|
71049
|
+
const { gathered, nodes, recommendations, cameraFps } = await this.recommend(headroom, minSessionsPerDevice);
|
|
71001
71050
|
const touchedNodes = new Set(recommendations.filter((r) => !r.skipped).map((r) => r.nodeId));
|
|
71002
71051
|
const targets = /* @__PURE__ */ new Map();
|
|
71003
71052
|
const formats = /* @__PURE__ */ new Map();
|
|
@@ -71057,9 +71106,10 @@ var AutoTuneService = class {
|
|
|
71057
71106
|
return {
|
|
71058
71107
|
plan: {
|
|
71059
71108
|
planHash: createHash("sha256").update(canonical({
|
|
71060
|
-
version:
|
|
71109
|
+
version: 2,
|
|
71061
71110
|
headroom,
|
|
71062
|
-
|
|
71111
|
+
minSessionsPerDevice,
|
|
71112
|
+
cameraFps,
|
|
71063
71113
|
picks: recommendations.map((r) => ({
|
|
71064
71114
|
nodeId: r.nodeId,
|
|
71065
71115
|
skipped: r.skipped,
|
|
@@ -71071,7 +71121,8 @@ var AutoTuneService = class {
|
|
|
71071
71121
|
})).digest("hex"),
|
|
71072
71122
|
family: "yolov9",
|
|
71073
71123
|
headroom,
|
|
71074
|
-
|
|
71124
|
+
minSessionsPerDevice,
|
|
71125
|
+
cameraFps,
|
|
71075
71126
|
generatedAt: this.deps.now(),
|
|
71076
71127
|
nodes: recommendations,
|
|
71077
71128
|
nodeChanges: [...nodeChanges],
|
|
@@ -71096,7 +71147,7 @@ var AutoTuneService = class {
|
|
|
71096
71147
|
}
|
|
71097
71148
|
}
|
|
71098
71149
|
async applyLocked(input) {
|
|
71099
|
-
const { plan, diffInput, formats } = await this.computePlan(input.headroom);
|
|
71150
|
+
const { plan, diffInput, formats } = await this.computePlan(input.headroom, minSessionsOf(input));
|
|
71100
71151
|
if (plan.planHash !== input.planHash) {
|
|
71101
71152
|
this.deps.logger.warn("auto tune: stale plan refused", { meta: {
|
|
71102
71153
|
confirmed: input.planHash,
|
|
@@ -71239,83 +71290,6 @@ var AutoTuneService = class {
|
|
|
71239
71290
|
}
|
|
71240
71291
|
};
|
|
71241
71292
|
//#endregion
|
|
71242
|
-
//#region src/auto-tune/device-peak-tracker.ts
|
|
71243
|
-
var BUCKET_MS = 3600 * 1e3;
|
|
71244
|
-
var DevicePeakTracker = class DevicePeakTracker {
|
|
71245
|
-
now;
|
|
71246
|
-
series = /* @__PURE__ */ new Map();
|
|
71247
|
-
cluster = null;
|
|
71248
|
-
constructor(now = () => Date.now()) {
|
|
71249
|
-
this.now = now;
|
|
71250
|
-
}
|
|
71251
|
-
static key(nodeId, deviceKey) {
|
|
71252
|
-
return `${nodeId}\u0000${deviceKey}`;
|
|
71253
|
-
}
|
|
71254
|
-
/** Fold one runner census in. A runner that did not answer is simply absent. */
|
|
71255
|
-
noteCensus(loads) {
|
|
71256
|
-
const now = this.now();
|
|
71257
|
-
const bucket = now - now % BUCKET_MS;
|
|
71258
|
-
let clusterTotal = 0;
|
|
71259
|
-
for (const load of loads) {
|
|
71260
|
-
const rows = load.devices.length > 0 ? load.devices : [{
|
|
71261
|
-
deviceKey: "",
|
|
71262
|
-
attachedCameras: load.attachedCameras
|
|
71263
|
-
}];
|
|
71264
|
-
for (const device of rows) {
|
|
71265
|
-
clusterTotal += device.attachedCameras;
|
|
71266
|
-
const key = DevicePeakTracker.key(load.nodeId, device.deviceKey);
|
|
71267
|
-
let s = this.series.get(key);
|
|
71268
|
-
if (s === void 0) {
|
|
71269
|
-
s = {
|
|
71270
|
-
firstSeenAt: now,
|
|
71271
|
-
buckets: /* @__PURE__ */ new Map()
|
|
71272
|
-
};
|
|
71273
|
-
this.series.set(key, s);
|
|
71274
|
-
}
|
|
71275
|
-
s.buckets.set(bucket, Math.max(s.buckets.get(bucket) ?? 0, device.attachedCameras));
|
|
71276
|
-
for (const start of s.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) s.buckets.delete(start);
|
|
71277
|
-
}
|
|
71278
|
-
}
|
|
71279
|
-
if (loads.length === 0) return;
|
|
71280
|
-
const c = this.cluster ?? {
|
|
71281
|
-
firstSeenAt: now,
|
|
71282
|
-
buckets: /* @__PURE__ */ new Map()
|
|
71283
|
-
};
|
|
71284
|
-
this.cluster = c;
|
|
71285
|
-
c.buckets.set(bucket, Math.max(c.buckets.get(bucket) ?? 0, clusterTotal));
|
|
71286
|
-
for (const start of c.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) c.buckets.delete(start);
|
|
71287
|
-
}
|
|
71288
|
-
/** The cluster-wide parallel peak over the window, or null before any census. */
|
|
71289
|
-
clusterPeak() {
|
|
71290
|
-
const c = this.cluster;
|
|
71291
|
-
if (c === null) return null;
|
|
71292
|
-
const now = this.now();
|
|
71293
|
-
let peak = 0;
|
|
71294
|
-
for (const [start, value] of c.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
|
|
71295
|
-
return {
|
|
71296
|
-
peakSessions: peak,
|
|
71297
|
-
observedForMs: now - c.firstSeenAt
|
|
71298
|
-
};
|
|
71299
|
-
}
|
|
71300
|
-
/** Every observed device's peak over the window and how long it has been watched. */
|
|
71301
|
-
peaks() {
|
|
71302
|
-
const now = this.now();
|
|
71303
|
-
const out = [];
|
|
71304
|
-
for (const [key, s] of this.series) {
|
|
71305
|
-
const [nodeId = "", deviceKey = ""] = key.split("\0");
|
|
71306
|
-
let peak = 0;
|
|
71307
|
-
for (const [start, value] of s.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
|
|
71308
|
-
out.push({
|
|
71309
|
-
nodeId,
|
|
71310
|
-
deviceKey,
|
|
71311
|
-
peakSessions: peak,
|
|
71312
|
-
observedForMs: now - s.firstSeenAt
|
|
71313
|
-
});
|
|
71314
|
-
}
|
|
71315
|
-
return out.toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
|
|
71316
|
-
}
|
|
71317
|
-
};
|
|
71318
|
-
//#endregion
|
|
71319
71293
|
//#region src/session-placement-stats.ts
|
|
71320
71294
|
var RETAIN_MS = 1440 * 60 * 1e3;
|
|
71321
71295
|
function keyOf(nodeId, deviceKey) {
|
|
@@ -71482,6 +71456,16 @@ function applyLoadBalancingShares(prev, write) {
|
|
|
71482
71456
|
}
|
|
71483
71457
|
//#endregion
|
|
71484
71458
|
//#region src/load-balancing/effective-shares-service.ts
|
|
71459
|
+
/** Skips that mean "not readable yet", never "cannot take sessions". */
|
|
71460
|
+
var NOT_READY_REASONS = new Set(["node-offline", "no-hardware-facts"]);
|
|
71461
|
+
function capacityFrom(result) {
|
|
71462
|
+
const out = {};
|
|
71463
|
+
for (const node of result.nodes) if (node.capacitySource !== null) out[node.nodeId] = {
|
|
71464
|
+
source: node.capacitySource,
|
|
71465
|
+
fallback: node.capacityFallback
|
|
71466
|
+
};
|
|
71467
|
+
return out;
|
|
71468
|
+
}
|
|
71485
71469
|
function weightsFrom(result) {
|
|
71486
71470
|
const nodes = {};
|
|
71487
71471
|
const devices = {};
|
|
@@ -71512,19 +71496,27 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71512
71496
|
now() {
|
|
71513
71497
|
return this.deps.now?.() ?? Date.now();
|
|
71514
71498
|
}
|
|
71515
|
-
/** The last automatic answer; null =
|
|
71499
|
+
/** The last automatic answer for the CURRENT source; null = none yet. */
|
|
71516
71500
|
current() {
|
|
71517
|
-
return this.
|
|
71501
|
+
return this.usable();
|
|
71502
|
+
}
|
|
71503
|
+
/** The answer, when the source is automatic and the answer was computed for it. */
|
|
71504
|
+
usable() {
|
|
71505
|
+
const source = this.deps.readSource();
|
|
71506
|
+
if (!isAutomaticShareSource(source)) return null;
|
|
71507
|
+
return this.answer !== null && this.answer.source === source ? this.answer : null;
|
|
71518
71508
|
}
|
|
71519
71509
|
/** Node weights the election reads. */
|
|
71520
71510
|
async nodeWeights() {
|
|
71521
|
-
|
|
71511
|
+
const answer = this.usable();
|
|
71512
|
+
if (answer !== null) return { ...answer.weights.nodes };
|
|
71522
71513
|
return { ...(await this.deps.readStored()).nodes };
|
|
71523
71514
|
}
|
|
71524
71515
|
/** The weights of a node's ELIGIBLE devices (`eligible` carries their stored weights). */
|
|
71525
71516
|
deviceWeights(nodeId, eligible) {
|
|
71526
|
-
|
|
71527
|
-
|
|
71517
|
+
const answer = this.usable();
|
|
71518
|
+
if (answer === null) return { ...eligible };
|
|
71519
|
+
const auto = answer.weights.devices[nodeId] ?? {};
|
|
71528
71520
|
const out = {};
|
|
71529
71521
|
for (const [key, stored] of Object.entries(eligible)) {
|
|
71530
71522
|
const share = auto[key];
|
|
@@ -71534,15 +71526,14 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71534
71526
|
}
|
|
71535
71527
|
/** Both maps, as the Auto tune recommender charges load by them. */
|
|
71536
71528
|
async effectiveWeights() {
|
|
71537
|
-
|
|
71538
|
-
return this.deps.readStored();
|
|
71529
|
+
return this.usable()?.weights ?? this.deps.readStored();
|
|
71539
71530
|
}
|
|
71540
71531
|
/**
|
|
71541
|
-
* Recompute now (
|
|
71542
|
-
* trailing run picks up whatever was reported meanwhile.
|
|
71532
|
+
* Recompute now (automatic sources only). Concurrent calls coalesce: one
|
|
71533
|
+
* runs, and one trailing run picks up whatever was reported meanwhile.
|
|
71543
71534
|
*/
|
|
71544
71535
|
async recompute(trigger) {
|
|
71545
|
-
if (this.deps.readSource()
|
|
71536
|
+
if (!isAutomaticShareSource(this.deps.readSource())) return;
|
|
71546
71537
|
if (this.inFlight !== null) {
|
|
71547
71538
|
this.pendingTrigger = trigger;
|
|
71548
71539
|
return this.inFlight;
|
|
@@ -71558,11 +71549,13 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71558
71549
|
}
|
|
71559
71550
|
}
|
|
71560
71551
|
async run(trigger) {
|
|
71552
|
+
const source = this.deps.readSource();
|
|
71553
|
+
if (!isAutomaticShareSource(source)) return;
|
|
71561
71554
|
let signature;
|
|
71562
71555
|
let result;
|
|
71563
71556
|
try {
|
|
71564
71557
|
signature = await this.deps.topologySignature();
|
|
71565
|
-
result = await this.deps.suggest();
|
|
71558
|
+
result = await this.deps.suggest(source);
|
|
71566
71559
|
} catch (err) {
|
|
71567
71560
|
this.deps.logger.warn("load balancing: automatic shares not recomputed — last answer kept", { meta: {
|
|
71568
71561
|
trigger,
|
|
@@ -71573,17 +71566,27 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71573
71566
|
}
|
|
71574
71567
|
const before = this.answer?.weights ?? null;
|
|
71575
71568
|
const after = weightsFrom(result);
|
|
71569
|
+
const notReady = result.nodes.filter((n) => n.share === null && NOT_READY_REASONS.has(n.skippedReason ?? "")).map((n) => n.nodeId).toSorted();
|
|
71570
|
+
const capacity = capacityFrom(result);
|
|
71576
71571
|
this.answer = {
|
|
71577
71572
|
weights: after,
|
|
71573
|
+
capacity,
|
|
71574
|
+
source,
|
|
71578
71575
|
computedAt: this.now(),
|
|
71579
71576
|
trigger,
|
|
71580
|
-
signature
|
|
71577
|
+
signature,
|
|
71578
|
+
notReady
|
|
71581
71579
|
};
|
|
71582
71580
|
const nodeIds = [...new Set([...Object.keys(before?.nodes ?? {}), ...result.nodes.map((n) => n.nodeId)])];
|
|
71583
|
-
|
|
71581
|
+
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);
|
|
71582
|
+
for (const nodeId of nodeIds.toSorted()) log("load balancing: automatic shares recomputed", {
|
|
71584
71583
|
tags: { nodeId },
|
|
71585
71584
|
meta: {
|
|
71586
71585
|
trigger,
|
|
71586
|
+
source,
|
|
71587
|
+
capacitySource: capacity[nodeId]?.source ?? null,
|
|
71588
|
+
capacityFallback: capacity[nodeId]?.fallback ?? null,
|
|
71589
|
+
notReady: notReady.includes(nodeId),
|
|
71587
71590
|
before: before?.nodes[nodeId] ?? null,
|
|
71588
71591
|
after: after.nodes[nodeId] ?? null,
|
|
71589
71592
|
devicesBefore: JSON.stringify(before?.devices[nodeId] ?? {}),
|
|
@@ -71591,10 +71594,17 @@ var EffectiveSharesService = class EffectiveSharesService {
|
|
|
71591
71594
|
}
|
|
71592
71595
|
});
|
|
71593
71596
|
}
|
|
71594
|
-
/**
|
|
71597
|
+
/**
|
|
71598
|
+
* Recompute if there is no answer for the current source, a node was not
|
|
71599
|
+
* readable yet, the topology changed or the answer has grown old
|
|
71600
|
+
* (automatic sources only).
|
|
71601
|
+
*/
|
|
71595
71602
|
async reconcile() {
|
|
71596
|
-
|
|
71603
|
+
const source = this.deps.readSource();
|
|
71604
|
+
if (!isAutomaticShareSource(source)) return;
|
|
71597
71605
|
if (this.answer === null) return this.recompute("reconcile:first");
|
|
71606
|
+
if (this.answer.source !== source) return this.recompute(`reconcile:source:${source}`);
|
|
71607
|
+
if (this.answer.notReady.length > 0) return this.recompute(`reconcile:not-ready:${this.answer.notReady.join(",")}`);
|
|
71598
71608
|
if (this.now() - this.answer.computedAt > EffectiveSharesService.MAX_AGE_MS) return this.recompute("reconcile:age");
|
|
71599
71609
|
let signature;
|
|
71600
71610
|
try {
|
|
@@ -71734,7 +71744,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
71734
71744
|
*/
|
|
71735
71745
|
clusterModelChoiceReady = false;
|
|
71736
71746
|
/** The per-device 24 h peak of concurrent sessions, fed by every runner census (D745). */
|
|
71737
|
-
devicePeaks = new DevicePeakTracker();
|
|
71738
71747
|
/** What session placement did (D748): the Load balancing tab's "actual" column. */
|
|
71739
71748
|
placementStats = new SessionPlacementStats();
|
|
71740
71749
|
/** The stored session placement policy (D748, default `share` since D750). */
|
|
@@ -72020,7 +72029,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
72020
72029
|
rebalance: (opts) => this.rebalance(opts),
|
|
72021
72030
|
setCameraPipelineForAgent: (input) => this.setCameraPipelineForAgent(input),
|
|
72022
72031
|
maintenancePaused: () => this.storageMigrationLeaseId !== null,
|
|
72023
|
-
onRunnerCensus: (loads) => this.devicePeaks.noteCensus(loads),
|
|
72024
72032
|
placementStats: this.placementStats,
|
|
72025
72033
|
setTopology: (topology) => {
|
|
72026
72034
|
this.topology = topology;
|
|
@@ -73143,11 +73151,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73143
73151
|
readCameraSettings: () => store.readCameraSettingsMap(),
|
|
73144
73152
|
rewriteAgentSettings: (transform) => store.rewriteAgentSettingsMap(transform),
|
|
73145
73153
|
rewriteCameraSettings: (transform) => store.rewriteCameraSettingsMap(transform),
|
|
73146
|
-
|
|
73147
|
-
|
|
73148
|
-
clusterPeak: () => this.devicePeaks.clusterPeak(),
|
|
73149
|
-
sessionPlacement: () => this.readSessionPlacement(),
|
|
73150
|
-
readEffectiveWeights: () => this.effectiveShares().effectiveWeights(),
|
|
73154
|
+
cameraFps: async () => this.autoTuneCameraFps(),
|
|
73155
|
+
measuredCapacity: NO_MEASURED_CAPACITY,
|
|
73151
73156
|
detectorStore: async (input) => {
|
|
73152
73157
|
const api = this.ctx.api;
|
|
73153
73158
|
if (!api) throw new Error("ctx.api unavailable");
|
|
@@ -73172,28 +73177,18 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73172
73177
|
});
|
|
73173
73178
|
return this.autoTuneService;
|
|
73174
73179
|
}
|
|
73175
|
-
/**
|
|
73176
|
-
|
|
73177
|
-
|
|
73178
|
-
|
|
73179
|
-
|
|
73180
|
-
|
|
73181
|
-
|
|
73182
|
-
|
|
73183
|
-
|
|
73184
|
-
|
|
73185
|
-
mode: config.detectionMode,
|
|
73186
|
-
fps: config.detectionFps,
|
|
73187
|
-
runsPipelineRoot: (config.detectionSources ?? ["pipeline"]).includes("pipeline"),
|
|
73188
|
-
standingNodeId: standingNode,
|
|
73189
|
-
nodePinId: nodePin,
|
|
73190
|
-
devicePin: pinnedKey !== null ? {
|
|
73191
|
-
nodeId: nodePin ?? standingNode,
|
|
73192
|
-
deviceKey: pinnedKey
|
|
73193
|
-
} : null
|
|
73194
|
-
});
|
|
73180
|
+
/**
|
|
73181
|
+
* The rate one detection session is counted at (D755): the fastest camera
|
|
73182
|
+
* with detection started that runs the pipeline root; null when none does.
|
|
73183
|
+
*/
|
|
73184
|
+
autoTuneCameraFps() {
|
|
73185
|
+
let fps = 0;
|
|
73186
|
+
for (const config of this.detectionWiring.activeDetections.values()) {
|
|
73187
|
+
if (config.detectionMode === "disabled") continue;
|
|
73188
|
+
if (!(config.detectionSources ?? ["pipeline"]).includes("pipeline")) continue;
|
|
73189
|
+
fps = Math.max(fps, config.detectionFps);
|
|
73195
73190
|
}
|
|
73196
|
-
return
|
|
73191
|
+
return fps > 0 ? fps : null;
|
|
73197
73192
|
}
|
|
73198
73193
|
async planAutoTune(input) {
|
|
73199
73194
|
return this.autoTune().plan(input);
|
|
@@ -73265,7 +73260,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73265
73260
|
this.effectiveSharesService = new EffectiveSharesService({
|
|
73266
73261
|
readSource: () => this.readShareSource(),
|
|
73267
73262
|
readStored: async () => storedShareWeights(await store.readAgentSettingsMap()),
|
|
73268
|
-
suggest: () => this.autoTune().suggestShares({
|
|
73263
|
+
suggest: (source) => this.autoTune().suggestShares({
|
|
73264
|
+
headroom: AUTO_SHARE_HEADROOM,
|
|
73265
|
+
source
|
|
73266
|
+
}),
|
|
73269
73267
|
topologySignature: async () => shareTopologySignature([...new Set(["hub", ...this.topology.collectRunnerNodeIds()])], await store.readAgentSettingsMap()),
|
|
73270
73268
|
logger: this.ctx.logger
|
|
73271
73269
|
});
|
|
@@ -73302,6 +73300,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73302
73300
|
nodeIds,
|
|
73303
73301
|
stored: storedShareWeights(await this.settingsStore.readAgentSettingsMap()),
|
|
73304
73302
|
auto: auto?.weights ?? null,
|
|
73303
|
+
...auto !== null ? { autoCapacity: auto.capacity } : {},
|
|
73305
73304
|
eligibleNodes,
|
|
73306
73305
|
eligibleDevices,
|
|
73307
73306
|
computedAt: auto?.computedAt ?? null,
|
|
@@ -73310,7 +73309,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73310
73309
|
}
|
|
73311
73310
|
async setShareSource(input) {
|
|
73312
73311
|
const previous = this.readShareSource();
|
|
73313
|
-
if (input.source === "manual" && previous
|
|
73312
|
+
if (input.source === "manual" && isAutomaticShareSource(previous)) {
|
|
73314
73313
|
const auto = this.effectiveShares().current();
|
|
73315
73314
|
if (auto !== null) await this.setLoadBalancingShares(auto.weights);
|
|
73316
73315
|
}
|
|
@@ -73319,7 +73318,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73319
73318
|
from: previous,
|
|
73320
73319
|
to: input.source
|
|
73321
73320
|
} });
|
|
73322
|
-
if (input.source
|
|
73321
|
+
if (isAutomaticShareSource(input.source)) this.noteShareChange(`source:${input.source}`);
|
|
73323
73322
|
return { success: true };
|
|
73324
73323
|
}
|
|
73325
73324
|
async getCameraSettings(input) {
|
|
@@ -73351,6 +73350,16 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
73351
73350
|
await this.emitResolvedCameraUpdated(input.deviceId);
|
|
73352
73351
|
return { success: true };
|
|
73353
73352
|
}
|
|
73353
|
+
/**
|
|
73354
|
+
* D754 — write or clear one step's CAMERA-WIDE settings (field-level bag,
|
|
73355
|
+
* merged under the per-(node,device) override on every accelerator). No
|
|
73356
|
+
* deviceKey to validate: the layer is node-independent by construction.
|
|
73357
|
+
*/
|
|
73358
|
+
async setCameraStepSettings(input) {
|
|
73359
|
+
await this.settingsStore.writeCameraStepSettings(input.deviceId, input.addonId, input.settings);
|
|
73360
|
+
await this.emitResolvedCameraUpdated(input.deviceId);
|
|
73361
|
+
return { success: true };
|
|
73362
|
+
}
|
|
73354
73363
|
async setCameraPipelineForAgent(input) {
|
|
73355
73364
|
const existing = (await this.settingsStore.readCameraSettingsMap())[String(input.deviceId)] ?? {};
|
|
73356
73365
|
const nextByAgent = { ...existing.pipelineByAgent };
|