@camstack/system 1.2.145 → 1.2.147
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/addon-runner.js +2 -1
- package/dist/addon-runner.mjs +2 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/load-series-config.d.ts +74 -0
- package/dist/builtins/native-metrics/load-series-reader.d.ts +37 -0
- package/dist/builtins/native-metrics/load-series-store.d.ts +232 -0
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +118 -35
- package/dist/builtins/native-metrics/native-metrics.addon.js +1463 -199
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1463 -199
- package/dist/builtins/native-metrics/node-load-ring.d.ts +214 -0
- package/dist/builtins/native-metrics/process-classification.d.ts +51 -0
- package/dist/builtins/native-metrics/thread-cpu-sampler.d.ts +116 -0
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +25 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-BVU5JADq.js → dist-Dl6MFXPr.js} +496 -35
- package/dist/{dist-CG7kwIZp.mjs → dist-DrBc-7Y3.mjs} +479 -36
- package/dist/index.d.ts +2 -2
- package/dist/index.js +45 -5
- package/dist/index.mjs +37 -6
- package/dist/kernel/heap-watch.d.ts +209 -1
- package/dist/{manifest-python-deps-BRyNCnnG.mjs → manifest-python-deps-DhT0oFOZ.mjs} +200 -8
- package/dist/{manifest-python-deps-GjlyPjm0.js → manifest-python-deps-FYZBHc3v.js} +253 -7
- package/dist/{retired-settings-keys-_PLI9w0k.js → retired-settings-keys-6w_JOqBg.js} +1 -1
- package/dist/{retired-settings-keys-Dm1z4mO4.mjs → retired-settings-keys-CF5r4YKq.mjs} +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory retention for the per-node load snapshots — the shared analogue of
|
|
3
|
+
* the log ring (`packages/system/src/logging/partitioned-log-buffer.ts`), and
|
|
4
|
+
* built to the same shape for the same reason: a fixed-capacity circular
|
|
5
|
+
* partition per key, plus **declared hard ceilings** on the total.
|
|
6
|
+
*
|
|
7
|
+
* ## What this is, and what it deliberately is not
|
|
8
|
+
*
|
|
9
|
+
* `native-metrics` emits `metrics.node-processes-snapshot` every 20 s on every
|
|
10
|
+
* node whether or not anyone is watching (D250). Until now nobody kept it: the
|
|
11
|
+
* Load charts accumulated 180 points inside the browser tab, so closing the
|
|
12
|
+
* page threw the hour away and two operators looking at the same node saw two
|
|
13
|
+
* different windows.
|
|
14
|
+
*
|
|
15
|
+
* This ring RETAINS what already arrives. It adds no timer, no sampler, no
|
|
16
|
+
* probe and no new event — subscribing to a fan-out that exists is the whole
|
|
17
|
+
* mechanism. It is **not** durable retention: nothing here reaches disk, and a
|
|
18
|
+
* stored series remains its own decision with its own resolution, retention and
|
|
19
|
+
* owner (D250 § Consequences).
|
|
20
|
+
*
|
|
21
|
+
* ## It does not know what a load snapshot looks like
|
|
22
|
+
*
|
|
23
|
+
* `TProcess` is a type parameter and the payload is stored **verbatim**. The
|
|
24
|
+
* ring reads exactly two things off the envelope — which node, and when — and
|
|
25
|
+
* never a field of a process row. A field added to the snapshot (a `deviceId`
|
|
26
|
+
* next to the pid, say) flows through untouched, because there is nothing here
|
|
27
|
+
* for it to flow through.
|
|
28
|
+
*
|
|
29
|
+
* ## Measured cost — see `docs/architecture/load-ledger.md`
|
|
30
|
+
*
|
|
31
|
+
* Measured, not estimated, against the real process payloads of the live
|
|
32
|
+
* cluster (`metricsProvider.listNodeProcesses` on all three nodes: 42 + 20 + 14
|
|
33
|
+
* rows) replayed 180 deep and weighed as V8 retained heap after a forced GC:
|
|
34
|
+
*
|
|
35
|
+
* **464 B per process row**, so the live cluster's full hour —
|
|
36
|
+
* 3 nodes x 180 samples x 76 rows = 13 680 rows — is **6.06 MiB**.
|
|
37
|
+
*
|
|
38
|
+
* Linear in rows (checked at 360 samples: 463.9 B/row). Every ceiling below is
|
|
39
|
+
* expressed in ROWS for that reason: rows are what the memory is proportional
|
|
40
|
+
* to, and samples are not.
|
|
41
|
+
*/
|
|
42
|
+
/** One retained snapshot: when it arrived, and the payload exactly as it came. */
|
|
43
|
+
export interface LoadSample<TProcess> {
|
|
44
|
+
readonly atMs: number;
|
|
45
|
+
readonly processes: readonly TProcess[];
|
|
46
|
+
}
|
|
47
|
+
/** What a reader gets back for one node. */
|
|
48
|
+
export interface NodeLoadSeries<TProcess> {
|
|
49
|
+
readonly nodeId: string;
|
|
50
|
+
/** Oldest-first, and filtered by the caller's `sinceMs` when given. */
|
|
51
|
+
readonly samples: readonly LoadSample<TProcess>[];
|
|
52
|
+
/** Samples retained for this node right now — independent of `sinceMs`. */
|
|
53
|
+
readonly retainedSamples: number;
|
|
54
|
+
/** Oldest retained sample's timestamp, or `null` when nothing is retained. */
|
|
55
|
+
readonly oldestAtMs: number | null;
|
|
56
|
+
/** The per-node ceiling in force, so a caller can say what it is looking at. */
|
|
57
|
+
readonly capacity: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Per-node ceiling, in SAMPLES.
|
|
61
|
+
*
|
|
62
|
+
* 180 at the 20 s process-snapshot cadence is **one hour** — the window the
|
|
63
|
+
* operator asked to keep, and the same 180 the browser buffer used before this
|
|
64
|
+
* existed (`LOAD_SERIES_MAX_POINTS`). The cap is on points, not on age, so a
|
|
65
|
+
* node whose emits are deduped away covers a longer wall-clock window at the
|
|
66
|
+
* same cost.
|
|
67
|
+
*/
|
|
68
|
+
export declare const DEFAULT_SAMPLES_PER_NODE = 180;
|
|
69
|
+
/**
|
|
70
|
+
* Fleet ceiling, in PROCESS ROWS across every node.
|
|
71
|
+
*
|
|
72
|
+
* This is the bound the per-node ceiling cannot provide. 180 samples is a fixed
|
|
73
|
+
* number of samples, not a fixed amount of memory: a node whose process count
|
|
74
|
+
* explodes — a leaked tsx tree, an inference pool respawning, the ghost storms
|
|
75
|
+
* the Processes tab exists to surface — multiplies its own partition without
|
|
76
|
+
* ever touching the per-node cap. That is precisely the defect the RTP pre-roll
|
|
77
|
+
* rings have in `docs/architecture/load-ledger.md` (#5): a maximum per instance
|
|
78
|
+
* and no budget for the fleet, which reached 898 MB across 47 rings.
|
|
79
|
+
*
|
|
80
|
+
* 32 000 rows x the measured 464 B = **14.2 MiB**, which is deliberately the
|
|
81
|
+
* same envelope hub-main already accepted for the log ring
|
|
82
|
+
* (`DEFAULT_MAX_TOTAL_LOG_ENTRIES` 50 000 x ~306 B ~ 15 MB). It is 2.3x the
|
|
83
|
+
* live cluster's full hour (13 680 rows), so no real deployment evicts on it —
|
|
84
|
+
* it exists so a runaway node costs 14 MiB instead of everything.
|
|
85
|
+
*/
|
|
86
|
+
export declare const DEFAULT_MAX_TOTAL_PROCESS_ROWS = 32000;
|
|
87
|
+
/**
|
|
88
|
+
* Ceiling on the number of PARTITIONS.
|
|
89
|
+
*
|
|
90
|
+
* The backstop for an unbounded key space, exactly as `DEFAULT_MAX_LOG_BUCKETS`
|
|
91
|
+
* is: a partition is created on first write, and a node id that appears once —
|
|
92
|
+
* a renamed agent, a node that joined for an afternoon — must not hold a ring
|
|
93
|
+
* for the life of the process. 16 is >5x the live fleet of 3.
|
|
94
|
+
*/
|
|
95
|
+
export declare const DEFAULT_MAX_NODES = 16;
|
|
96
|
+
/**
|
|
97
|
+
* A partition whose newest sample is older than this is dropped whole.
|
|
98
|
+
*
|
|
99
|
+
* **Why age, and why this age.** The alternative — evict when the node leaves
|
|
100
|
+
* the cluster registry — was rejected twice over: it couples this ring to a
|
|
101
|
+
* registry it has no other reason to know, and a node that is merely offline is
|
|
102
|
+
* never "removed", so the case that actually happens (an agent that stops
|
|
103
|
+
* answering) would keep its partition forever. That is the failure mode being
|
|
104
|
+
* bounded.
|
|
105
|
+
*
|
|
106
|
+
* The number is not chosen, it is derived: `DEFAULT_SAMPLES_PER_NODE` x the 20 s
|
|
107
|
+
* snapshot cadence = **one hour** = the entire window this ring can represent.
|
|
108
|
+
* A node silent for that long holds nothing that overlaps the hour an operator
|
|
109
|
+
* is asking about; every point in it has aged out of the question. Below that
|
|
110
|
+
* threshold the partition is still answering a live question and is kept, even
|
|
111
|
+
* though the node is quiet — a node emits at least once a minute
|
|
112
|
+
* (`METRICS_SNAPSHOT_HEARTBEAT_MS`), so an hour of silence is not a slow node,
|
|
113
|
+
* it is a gone one.
|
|
114
|
+
*/
|
|
115
|
+
export declare const DEFAULT_NODE_IDLE_EVICTION_MS = 3600000;
|
|
116
|
+
export interface NodeLoadRingOptions {
|
|
117
|
+
readonly samplesPerNode?: number;
|
|
118
|
+
readonly maxTotalProcessRows?: number;
|
|
119
|
+
readonly maxNodes?: number;
|
|
120
|
+
readonly idleEvictionMs?: number;
|
|
121
|
+
/** Injectable clock. Tests drive eviction without sleeping for an hour. */
|
|
122
|
+
readonly now?: () => number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The partitioned ring: one {@link LoadPartition} per node id.
|
|
126
|
+
*
|
|
127
|
+
* Three bounds, doing three different jobs — the same division as the log
|
|
128
|
+
* buffer, for the same reason:
|
|
129
|
+
*
|
|
130
|
+
* - **`samplesPerNode`** is HARD and per partition. It is what the operator
|
|
131
|
+
* asked for (an hour) and it can never be exceeded.
|
|
132
|
+
* - **`maxTotalProcessRows`** is the FLEET budget, and it is denominated in the
|
|
133
|
+
* thing memory is proportional to. When it bites, the oldest sample of the
|
|
134
|
+
* HEAVIEST partition is dropped — the node creating the pressure is the node
|
|
135
|
+
* that pays, and a quiet node keeps its full hour. The node just written to is
|
|
136
|
+
* NOT protected here: if it is the heaviest, it is the culprit.
|
|
137
|
+
* - **`maxNodes`** is the backstop against an unbounded key space, and it drops
|
|
138
|
+
* whole partitions least-recently-written first, never the one being written.
|
|
139
|
+
*
|
|
140
|
+
* Plus {@link DEFAULT_NODE_IDLE_EVICTION_MS}, which is what stops a node that
|
|
141
|
+
* simply stopped emitting from living in memory forever.
|
|
142
|
+
*/
|
|
143
|
+
export declare class NodeLoadRing<TProcess> {
|
|
144
|
+
private readonly partitions;
|
|
145
|
+
/**
|
|
146
|
+
* Node id → write ordinal of its last accepted sample. A monotonic counter,
|
|
147
|
+
* not a clock: partition eviction must follow the order writes actually
|
|
148
|
+
* happened, and node clocks disagree.
|
|
149
|
+
*/
|
|
150
|
+
private readonly lastWriteSeq;
|
|
151
|
+
private writeSeq;
|
|
152
|
+
private totalRows;
|
|
153
|
+
private readonly samplesPerNode;
|
|
154
|
+
private readonly maxTotalProcessRows;
|
|
155
|
+
private readonly maxNodes;
|
|
156
|
+
private readonly idleEvictionMs;
|
|
157
|
+
private readonly now;
|
|
158
|
+
constructor(options?: NodeLoadRingOptions);
|
|
159
|
+
/**
|
|
160
|
+
* Retain one snapshot. `processes` is stored by reference — the payload is
|
|
161
|
+
* already an immutable arrival off the bus, and copying it would double the
|
|
162
|
+
* measured cost for nothing.
|
|
163
|
+
*
|
|
164
|
+
* Returns whether the sample was ACCEPTED — that is, whether it was new
|
|
165
|
+
* rather than a replay of a timestamp this node has already delivered.
|
|
166
|
+
*
|
|
167
|
+
* The return value is not diagnostics. It is the idempotence gate the
|
|
168
|
+
* DURABLE tier rides on (`load-series-store.ts`): only an accepted sample is
|
|
169
|
+
* appended to the table, which is what lets that table carry an `INTEGER`
|
|
170
|
+
* rowid key instead of a composite unique index over two million rows. The
|
|
171
|
+
* gate is one monotonic comparison in memory; the index it replaces was
|
|
172
|
+
* measured at ~40 bytes per row.
|
|
173
|
+
*/
|
|
174
|
+
record(nodeId: string, atMs: number, processes: readonly TProcess[]): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Read one node's retained series, oldest-first.
|
|
177
|
+
*
|
|
178
|
+
* `sinceMs` is EXCLUSIVE: a caller passes the newest timestamp it already
|
|
179
|
+
* holds and gets back only what it is missing. That is the whole contract
|
|
180
|
+
* that lets the admin UI seed from here and then continue live without
|
|
181
|
+
* doubling a point it already drew.
|
|
182
|
+
*
|
|
183
|
+
* A node nobody has recorded answers with an empty series, not an error —
|
|
184
|
+
* unknown is the truth about a node that has not reported.
|
|
185
|
+
*/
|
|
186
|
+
read(nodeId: string, sinceMs?: number): NodeLoadSeries<TProcess>;
|
|
187
|
+
/** Node ids with a live partition. Observability for the fleet bound. */
|
|
188
|
+
nodeIds(): readonly string[];
|
|
189
|
+
/** Process rows retained across every partition — the number that IS memory. */
|
|
190
|
+
rowCount(): number;
|
|
191
|
+
/** Samples retained across every partition. */
|
|
192
|
+
sampleCount(): number;
|
|
193
|
+
/**
|
|
194
|
+
* Drop partitions whose newest sample is older than the retention window.
|
|
195
|
+
*
|
|
196
|
+
* Lazy, on write and on read — never a timer. A timer would be a new
|
|
197
|
+
* periodic cost in a subsystem whose entire premise is that it adds none,
|
|
198
|
+
* and a ring that nobody writes to and nobody reads is not growing either.
|
|
199
|
+
*/
|
|
200
|
+
private sweepIdle;
|
|
201
|
+
/**
|
|
202
|
+
* Bring the fleet back under the row budget by dropping the OLDEST sample of
|
|
203
|
+
* the HEAVIEST partition, repeatedly.
|
|
204
|
+
*
|
|
205
|
+
* Terminates: every iteration removes one sample from a non-empty partition,
|
|
206
|
+
* and the population of samples is finite and strictly decreasing.
|
|
207
|
+
*/
|
|
208
|
+
private enforceRowBudget;
|
|
209
|
+
private heaviestPartition;
|
|
210
|
+
/** Drop `count` whole partitions, least-recently-written first. */
|
|
211
|
+
private evictPartitions;
|
|
212
|
+
/** Remove a partition and everything that indexes it. */
|
|
213
|
+
private dropPartition;
|
|
214
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { NodeProcess } from '@camstack/types';
|
|
2
|
+
import { ThreadCpuSplit } from './thread-cpu-sampler.js';
|
|
3
|
+
/** The classes a node process can carry. Mirrors `NodeProcessSchema`. */
|
|
4
|
+
export type ProcessClassification = NodeProcess['classification'];
|
|
5
|
+
/** One parsed `ps -eo pid,ppid,pgid,pcpu,rss,etime,command` row. */
|
|
6
|
+
export interface PsProcessRow {
|
|
7
|
+
readonly pid: number;
|
|
8
|
+
readonly ppid: number;
|
|
9
|
+
readonly pgid: number;
|
|
10
|
+
readonly cpuPercent: number;
|
|
11
|
+
readonly memoryRssBytes: number;
|
|
12
|
+
readonly uptimeSec: number;
|
|
13
|
+
readonly command: string;
|
|
14
|
+
}
|
|
15
|
+
/** A pid the kernel's `$process.list` attributes to an addon runner. */
|
|
16
|
+
export interface ManagedProcessEntry {
|
|
17
|
+
readonly addonId: string;
|
|
18
|
+
readonly nodeId: string;
|
|
19
|
+
}
|
|
20
|
+
export interface BuildNodeProcessesInput {
|
|
21
|
+
/** Every visible `ps` row on this node, camstack-shaped or not. */
|
|
22
|
+
readonly psRows: readonly PsProcessRow[];
|
|
23
|
+
/** This node's own pid. */
|
|
24
|
+
readonly selfPid: number;
|
|
25
|
+
/** Node id reported for the `root` row. */
|
|
26
|
+
readonly selfNodeId: string;
|
|
27
|
+
/** pid → addon attribution from `$process.list`. */
|
|
28
|
+
readonly managed: ReadonlyMap<number, ManagedProcessEntry>;
|
|
29
|
+
/** pid → thread CPU split, when the sampler had an answer this tick. */
|
|
30
|
+
readonly cpuSplits: ReadonlyMap<number, ThreadCpuSplit>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Regex matching camstack-related command lines. We surface anything with
|
|
34
|
+
* an obvious camstack fingerprint so operators can spot stray inference
|
|
35
|
+
* pools, leaked tsx watchers, and zombie node workers in the Cluster →
|
|
36
|
+
* Agent → Processes tab.
|
|
37
|
+
*
|
|
38
|
+
* Patterns mirror `scripts/hunt-zombies.sh` — keep them in sync when
|
|
39
|
+
* adding a new long-lived subprocess.
|
|
40
|
+
*/
|
|
41
|
+
export declare const CAMSTACK_CMD_RE: RegExp;
|
|
42
|
+
/**
|
|
43
|
+
* Classify one pid.
|
|
44
|
+
*
|
|
45
|
+
* Identity-driven, not ancestry-driven: a process is `root` because it IS
|
|
46
|
+
* this node, `managed` because the kernel named it, and `system` otherwise.
|
|
47
|
+
* There is no third source of truth to consult and no walk to get wrong.
|
|
48
|
+
*/
|
|
49
|
+
export declare function classifyProcess(pid: number, selfPid: number, managed: ReadonlyMap<number, ManagedProcessEntry>): ProcessClassification;
|
|
50
|
+
/** Turn a `ps` snapshot into the node's process table. */
|
|
51
|
+
export declare function buildNodeProcesses(input: BuildNodeProcessesInput): readonly NodeProcess[];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Raw tick counts for one process at one instant, already classified. */
|
|
2
|
+
export interface ThreadTickSample {
|
|
3
|
+
/** Sum of utime+stime over threads that are NOT in the V8 helper pool. */
|
|
4
|
+
readonly mainTicks: number;
|
|
5
|
+
/** Sum of utime+stime over the V8 helper-pool threads. */
|
|
6
|
+
readonly gcTicks: number;
|
|
7
|
+
/** Number of threads seen, both classes together. */
|
|
8
|
+
readonly threadCount: number;
|
|
9
|
+
/** `Date.now()` at the moment the sample was taken. */
|
|
10
|
+
readonly atMs: number;
|
|
11
|
+
/**
|
|
12
|
+
* The process's `starttime` (field 22 of `/proc/<pid>/stat`) — jiffies since
|
|
13
|
+
* boot at which THIS incarnation of the pid began. Read from the main
|
|
14
|
+
* thread, whose tid equals the pid.
|
|
15
|
+
*
|
|
16
|
+
* It is the process's identity, and it is the only thing that distinguishes
|
|
17
|
+
* "the same process, later" from "a different process that got the same pid".
|
|
18
|
+
* `null` when the main thread's stat could not be read, which is also
|
|
19
|
+
* treated as "identity unknown" — see {@link cpuSplitBetween}.
|
|
20
|
+
*/
|
|
21
|
+
readonly startTicks: number | null;
|
|
22
|
+
}
|
|
23
|
+
/** The CPU split over the window between two {@link ThreadTickSample}s. */
|
|
24
|
+
export interface ThreadCpuSplit {
|
|
25
|
+
/** CPU% of the process's own threads. 100 = one core saturated. */
|
|
26
|
+
readonly mainPercent: number;
|
|
27
|
+
/** CPU% of the V8 helper pool — see the module docblock on what this holds. */
|
|
28
|
+
readonly gcPercent: number;
|
|
29
|
+
/** Threads seen in the newer sample. */
|
|
30
|
+
readonly threadCount: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse one `/proc/<pid>/task/<tid>/stat` line.
|
|
34
|
+
*
|
|
35
|
+
* The `comm` field is field 2 and is wrapped in parentheses — and it can
|
|
36
|
+
* itself contain spaces and parentheses (`(V8Worker)`, but also
|
|
37
|
+
* `(some (weird) name)`). Splitting the line on whitespace therefore corrupts
|
|
38
|
+
* every field after it. The only correct parse is to cut at the LAST `)`, take
|
|
39
|
+
* `comm` from inside the parens, and index the remaining fields from there:
|
|
40
|
+
* after the cut, field 3 (`state`) is index 0, so `utime` (field 14) is index
|
|
41
|
+
* 11 and `stime` (field 15) is index 12.
|
|
42
|
+
*
|
|
43
|
+
* `starttime` is field 22, so index 19 by the same arithmetic. It is the
|
|
44
|
+
* thread's own start, which for the main thread (tid == pid) is the PROCESS's
|
|
45
|
+
* start — the identity {@link cpuSplitBetween} checks before it believes a
|
|
46
|
+
* window belongs to the process it belonged to last time.
|
|
47
|
+
*
|
|
48
|
+
* Returns `null` for a line that does not parse — a thread that exited between
|
|
49
|
+
* the readdir and the read leaves a truncated or empty file, and that is
|
|
50
|
+
* normal, not an error.
|
|
51
|
+
*/
|
|
52
|
+
export interface ParsedThreadStat {
|
|
53
|
+
readonly comm: string;
|
|
54
|
+
readonly ticks: number;
|
|
55
|
+
/** Field 22 (`starttime`), in jiffies since boot. */
|
|
56
|
+
readonly startTicks: number;
|
|
57
|
+
}
|
|
58
|
+
export declare function parseThreadStat(line: string): ParsedThreadStat | null;
|
|
59
|
+
/** Injectable procfs surface — the whole reason this module is testable. */
|
|
60
|
+
export interface ProcFsReader {
|
|
61
|
+
readonly listTaskIds: (pid: number) => Promise<readonly string[]>;
|
|
62
|
+
readonly readTaskStat: (pid: number, tid: string) => Promise<string>;
|
|
63
|
+
}
|
|
64
|
+
export declare const nodeProcFsReader: ProcFsReader;
|
|
65
|
+
/**
|
|
66
|
+
* Read one instant's tick counts for `pid`, or `null` when the process has no
|
|
67
|
+
* readable `/proc/<pid>/task` — it exited, we lack permission, or this is not
|
|
68
|
+
* Linux. `null` is "unknown"; it must never be turned into a zero downstream.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readThreadTicks(pid: number, reader?: ProcFsReader): Promise<ThreadTickSample | null>;
|
|
71
|
+
/**
|
|
72
|
+
* The CPU split between two samples of the SAME process.
|
|
73
|
+
*
|
|
74
|
+
* Returns `null` — never a zero — when the window cannot produce a rate:
|
|
75
|
+
* - the two samples are not separated in time (no window to divide by);
|
|
76
|
+
* - the pid's `starttime` changed, or is unknown on either side. A changed
|
|
77
|
+
* starttime means the pid was RECYCLED: this is a different process that
|
|
78
|
+
* happens to wear the same number. See below for why the tick check alone
|
|
79
|
+
* does not catch it;
|
|
80
|
+
* - either tick count went BACKWARDS, which is the same fault seen from the
|
|
81
|
+
* other side. A negative delta divided by a window would render as a
|
|
82
|
+
* plausible small number for the wrong process, and a plausible wrong
|
|
83
|
+
* number is the failure this whole surface exists to avoid.
|
|
84
|
+
*
|
|
85
|
+
* ## Why `starttime` and not just the backwards-tick check
|
|
86
|
+
*
|
|
87
|
+
* The backwards check only fires when the NEW process has burned FEWER ticks
|
|
88
|
+
* than the dead one had. A recycled pid landing on a busier process — an
|
|
89
|
+
* ffmpeg that has been transcoding for a minute inheriting the pid of an
|
|
90
|
+
* ffmpeg that idled — produces a large POSITIVE delta, which divides cleanly
|
|
91
|
+
* and renders as a spike. That is not a hypothetical rounding concern now that
|
|
92
|
+
* the process snapshot attributes each pid to a camera: it would charge one
|
|
93
|
+
* camera for another camera's whole lifetime of CPU, in a chart built
|
|
94
|
+
* specifically to answer "why is 617 worse than 615". `starttime` is the pid's
|
|
95
|
+
* incarnation number and it catches both directions.
|
|
96
|
+
*/
|
|
97
|
+
export declare function cpuSplitBetween(prev: ThreadTickSample, next: ThreadTickSample): ThreadCpuSplit | null;
|
|
98
|
+
/**
|
|
99
|
+
* Keeps the previous tick sample per pid so the next process-snapshot tick can
|
|
100
|
+
* turn it into a rate.
|
|
101
|
+
*
|
|
102
|
+
* Bounded by construction: {@link sample} replaces the whole map with only the
|
|
103
|
+
* pids it was just asked about, so a pid that stops appearing in the scan is
|
|
104
|
+
* dropped on the following tick rather than accumulating forever.
|
|
105
|
+
*/
|
|
106
|
+
export declare class ThreadCpuTracker {
|
|
107
|
+
private readonly reader;
|
|
108
|
+
private previous;
|
|
109
|
+
constructor(reader?: ProcFsReader);
|
|
110
|
+
/**
|
|
111
|
+
* Sample `pids` and return the split for each one that HAS a usable previous
|
|
112
|
+
* sample. A pid absent from the result has no answer yet — the caller must
|
|
113
|
+
* report `null`, not `0`.
|
|
114
|
+
*/
|
|
115
|
+
sample(pids: readonly number[]): Promise<ReadonlyMap<number, ThreadCpuSplit>>;
|
|
116
|
+
}
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Dl6MFXPr.js");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
8
8
|
let node_util = require("node:util");
|
|
9
9
|
let node_os = require("node:os");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Ot as errMsg, V as enumerateInferenceDevices, Wt as emitReadiness, kt as BaseAddon, tn as EventCategory, ut as platformProbeCapability, vt as scoreRuntimes } from "../../dist-DrBc-7Y3.mjs";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import * as os from "node:os";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Dl6MFXPr.js");
|
|
7
7
|
//#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
|
|
8
8
|
/**
|
|
9
9
|
* Reconcile the durable `enabledProviders` set against authoritative
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kt as BaseAddon, tn as EventCategory } from "../../dist-DrBc-7Y3.mjs";
|
|
2
2
|
//#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
|
|
3
3
|
/**
|
|
4
4
|
* Reconcile the durable `enabledProviders` set against authoritative
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Dl6MFXPr.js");
|
|
7
7
|
let zod = require("zod");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let _camstack_types_node = require("@camstack/types/node");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ft as DeviceType, I as deriveBatteryPresence, Jt as nodePin, Nt as DeviceFeature, O as bareAddonId, Ot as errMsg, bt as snapshotCapability, i as BatteryStatusSchema, k as batteryCapability, kt as BaseAddon, tn as EventCategory, wt as streamQualityLabel } from "../../dist-DrBc-7Y3.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { signExpiringUrl, verifyExpiringUrl } from "@camstack/types/node";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Dl6MFXPr.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let node_fs_promises = require("node:fs/promises");
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ct as storageProviderCapability, K as filesystemBrowseCapability, kt as BaseAddon } from "../../dist-DrBc-7Y3.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { CollectionColumn, CollectionIndex, DataStoreEngineInfo, HistogramBucket, IScopedLogger, ISettingsBackend, QueryFilter, SettingsCountInput, SettingsDeleteInput, SettingsGetInput, SettingsAggregateInput, SettingsAggregateResult, SettingsHistogramInput, SettingsInsertInput, SettingsIsEmptyInput, SettingsQueryInput, SettingsRecord, SettingsSetInput, SettingsUpdateInput } from '@camstack/types';
|
|
1
|
+
import { BulkRecord, CollectionColumn, CollectionIndex, DataStoreEngineInfo, HistogramBucket, IScopedLogger, ISettingsBackend, QueryFilter, SettingsCountInput, SettingsDeleteInput, SettingsGetInput, SettingsAggregateInput, SettingsAggregateResult, SettingsHistogramInput, SettingsInsertInput, SettingsIsEmptyInput, SettingsQueryInput, SettingsRecord, SettingsSetInput, SettingsUpdateInput } from '@camstack/types';
|
|
2
2
|
import { default as Database } from 'better-sqlite3';
|
|
3
3
|
import { MutationFilterInput } from './filter-compiler.js';
|
|
4
4
|
import { SqliteOpProfiler } from './query-profiler.js';
|
|
5
|
+
/** Input for {@link SqliteSettingsBackend.insertMany}. */
|
|
6
|
+
interface SettingsInsertManyInput {
|
|
7
|
+
readonly namespace?: string;
|
|
8
|
+
readonly collection: string;
|
|
9
|
+
readonly records: readonly BulkRecord[];
|
|
10
|
+
}
|
|
11
|
+
interface SettingsInsertManyResult {
|
|
12
|
+
readonly inserted: number;
|
|
13
|
+
}
|
|
5
14
|
/** Input for {@link SqliteSettingsBackend.deleteWhere}. */
|
|
6
15
|
interface SettingsDeleteWhereInput {
|
|
7
16
|
readonly namespace?: string;
|
|
@@ -148,6 +157,21 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
|
|
|
148
157
|
set({ namespace, collection, key, value }: SettingsSetInput): Promise<void>;
|
|
149
158
|
query<T extends object = Record<string, unknown>>({ namespace, collection, filter, columns, }: SettingsQueryInput): Promise<readonly SettingsRecord<T>[]>;
|
|
150
159
|
insert<T extends object = Record<string, unknown>>({ namespace, collection, record, }: SettingsInsertInput<T>): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Insert a batch in ONE transaction, on ONE prepared statement.
|
|
162
|
+
*
|
|
163
|
+
* The write-side twin of {@link deleteWhere}. Every row in the batch shares
|
|
164
|
+
* a single column list, which is what makes one `prepare` legal: rows are
|
|
165
|
+
* normalised to the collection's DECLARED column set, so a row that omits a
|
|
166
|
+
* column binds `null` for it rather than producing a second statement shape.
|
|
167
|
+
*
|
|
168
|
+
* All or nothing — `better-sqlite3`'s `transaction()` rolls the whole batch
|
|
169
|
+
* back on any throw. A caller writing one process sample gets one COMMIT,
|
|
170
|
+
* which is the entire point: 76 rows every 10 s is 7.6 rows/s, and 7.6
|
|
171
|
+
* separate commits per second on the connection that also serves every
|
|
172
|
+
* cluster-wide configuration read is a constant load nobody asked for.
|
|
173
|
+
*/
|
|
174
|
+
insertMany({ namespace, collection, records, }: SettingsInsertManyInput): Promise<SettingsInsertManyResult>;
|
|
151
175
|
update({ namespace, collection, id, data }: SettingsUpdateInput): Promise<void>;
|
|
152
176
|
delete({ namespace, collection, key }: SettingsDeleteInput): Promise<void>;
|
|
153
177
|
/**
|
|
Binary file
|
|
Binary file
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Dl6MFXPr.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let node_fs_promises = require("node:fs/promises");
|
|
9
9
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -663,6 +663,7 @@ function createDataStoreDispatch(getEngines) {
|
|
|
663
663
|
set: async (input) => (await engine()).set(input),
|
|
664
664
|
query: async (input) => (await engine()).query(input),
|
|
665
665
|
insert: async (input) => (await engine()).insert(input),
|
|
666
|
+
insertMany: async (input) => (await engine()).insertMany(input),
|
|
666
667
|
update: async (input) => (await engine()).update(input),
|
|
667
668
|
delete: async (input) => (await engine()).delete(input),
|
|
668
669
|
deleteWhere: async (input) => (await engine()).deleteWhere(input),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { St as storageMigrationCapability, Yt as parseJsonObject, b as StorageMigrationJobSchema, kt as BaseAddon, xt as storageCapability, y as StorageLocationTypeSchema, yt as settingsStoreCapability } from "../../dist-DrBc-7Y3.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -656,6 +656,7 @@ function createDataStoreDispatch(getEngines) {
|
|
|
656
656
|
set: async (input) => (await engine()).set(input),
|
|
657
657
|
query: async (input) => (await engine()).query(input),
|
|
658
658
|
insert: async (input) => (await engine()).insert(input),
|
|
659
|
+
insertMany: async (input) => (await engine()).insertMany(input),
|
|
659
660
|
update: async (input) => (await engine()).update(input),
|
|
660
661
|
delete: async (input) => (await engine()).delete(input),
|
|
661
662
|
deleteWhere: async (input) => (await engine()).deleteWhere(input),
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Dl6MFXPr.js");
|
|
7
7
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
8
8
|
/**
|
|
9
9
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Kt as hydrateSchema, Ot as errMsg, kt as BaseAddon } from "../../dist-DrBc-7Y3.mjs";
|
|
2
2
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
3
3
|
/**
|
|
4
4
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Dl6MFXPr.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kt as BaseAddon, rt as logDestinationCapability } from "../../dist-DrBc-7Y3.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import path from "node:path";
|