@camstack/addon-pipeline-orchestrator 1.2.619 → 1.2.620

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