@camstack/system 1.2.217 → 1.2.219
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 +9 -6
- package/dist/addon-runner.mjs +9 -6
- package/dist/addon-utils.js +2 -2
- package/dist/addon-utils.mjs +2 -2
- 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 +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/gpu-probe.d.ts +18 -0
- package/dist/builtins/native-metrics/native-metrics-provider.d.ts +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +24 -8
- package/dist/builtins/native-metrics/native-metrics.addon.js +1318 -1098
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1318 -1101
- package/dist/builtins/native-metrics/proc-process-table.d.ts +25 -0
- package/dist/builtins/platform-probe/index.js +3 -3
- package/dist/builtins/platform-probe/index.mjs +3 -3
- 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-pragmas.d.ts +23 -0
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +1 -0
- 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/sqlite-storage/wal-checkpoint-policy.d.ts +115 -0
- package/dist/builtins/sqlite-storage/wal-checkpoint-worker.d.ts +1 -0
- package/dist/builtins/sqlite-storage/wal-checkpointer.d.ts +70 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -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/{custom-action-registry-F__gp_VX.mjs → custom-action-registry-B6gOtUWA.mjs} +19 -0
- package/dist/{custom-action-registry-jY0NOZK8.js → custom-action-registry-DKWhaWL1.js} +19 -0
- package/dist/{dist-C85OOASj.js → dist-B17W9ngu.js} +118 -3
- package/dist/{dist-DtDKMSeG.mjs → dist-Cyz5_i7z.mjs} +113 -4
- package/dist/index.d.ts +9 -2
- package/dist/index.js +39 -36
- package/dist/index.mjs +23 -38
- package/dist/kernel/array-buffer-census.d.ts +122 -0
- package/dist/kernel/custom-action-registry.d.ts +10 -1
- package/dist/kernel/heap-watch.d.ts +39 -1
- package/dist/kernel/moleculer/mesh-queue-report.d.ts +51 -0
- package/dist/kernel/outbound-queue-watch.d.ts +94 -0
- package/dist/kernel/socket-plane-report.d.ts +75 -4
- package/dist/kernel/transport/cap-routing-hints.d.ts +21 -0
- package/dist/kernel/transport/child-cap-protocol.d.ts +43 -3
- package/dist/kernel/transport/frame-codec.d.ts +49 -0
- package/dist/kernel/transport/local-child-client.d.ts +25 -2
- package/dist/kernel/transport/local-child-registry.d.ts +36 -1
- package/dist/kernel/transport/local-transport.d.ts +45 -2
- package/dist/kernel/transport/socket-channel.d.ts +19 -1
- package/dist/{manifest-system-deps-726RBOX7.mjs → manifest-system-deps-C7dihwWT.mjs} +1604 -788
- package/dist/{manifest-system-deps-DvY1bY_n.js → manifest-system-deps-g_nK0xsF.js} +1607 -683
- package/dist/process/proc-stat.d.ts +75 -0
- package/dist/process/resource-monitor.d.ts +9 -1
- package/dist/{resource-monitor-CdnzxBLP.js → resource-monitor-LVE1BoLs.js} +42 -23
- package/dist/{resource-monitor-BWmQ5i-o.mjs → resource-monitor-Vp0bPV3U.mjs} +42 -22
- package/dist/{retired-settings-keys-Cx-lYIbU.mjs → retired-settings-keys-BCYUrdsA.mjs} +1 -1
- package/dist/{retired-settings-keys-Dfj9LA-i.js → retired-settings-keys-CjvB7Etx.js} +1 -1
- package/dist/wal-checkpoint-policy-CAcg63o-.mjs +92 -0
- package/dist/wal-checkpoint-policy-CCJZngds.js +127 -0
- package/dist/wal-checkpoint-worker.js +138 -0
- package/dist/wal-checkpoint-worker.mjs +137 -0
- package/package.json +1 -1
- package/dist/{event-loop-stall-monitor-DM9OAzy8.js → event-loop-stall-monitor-CAsHdMwP.js} +1 -1
- package/dist/{event-loop-stall-monitor-B3CFd9tM.mjs → event-loop-stall-monitor-GPrVvx14.mjs} +1 -1
- package/dist/{tls-DOTmtLCW.mjs → tls-2xbgg78V.mjs} +2 -2
- package/dist/{tls-BxQlomxd.js → tls-BFs4PzoW.js} +2 -2
|
@@ -3,14 +3,14 @@ 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-B17W9ngu.js");
|
|
7
7
|
let node_fs_promises = require("node:fs/promises");
|
|
8
|
-
let node_child_process = require("node:child_process");
|
|
9
|
-
let node_util = require("node:util");
|
|
10
8
|
let node_fs = require("node:fs");
|
|
11
9
|
node_fs = require_chunk.__toESM(node_fs);
|
|
10
|
+
let node_child_process = require("node:child_process");
|
|
12
11
|
let node_os = require("node:os");
|
|
13
12
|
node_os = require_chunk.__toESM(node_os);
|
|
13
|
+
let node_util = require("node:util");
|
|
14
14
|
//#region src/builtins/native-metrics/cgroup-memory.ts
|
|
15
15
|
/** `readFileSync`, with every failure flattened to `null`. */
|
|
16
16
|
var nodeCgroupFileReader = { read(path) {
|
|
@@ -1092,1118 +1092,1376 @@ var ContainerMemoryStore = class {
|
|
|
1092
1092
|
}
|
|
1093
1093
|
};
|
|
1094
1094
|
//#endregion
|
|
1095
|
-
//#region src/builtins/native-metrics/
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1095
|
+
//#region src/builtins/native-metrics/load-series-store.ts
|
|
1096
|
+
/**
|
|
1097
|
+
* @durable class=audit owner=metrics-native
|
|
1098
|
+
* write="one row per process per sample, appended in ONE transaction per
|
|
1099
|
+
* sample (`settingsStore.insertMany`) by the HUB's `native-metrics`, off its
|
|
1100
|
+
* subscription to `metrics.node-processes-snapshot`. Emission is
|
|
1101
|
+
* fixed-cadence, so the write rate is deterministic: nodes x processes /
|
|
1102
|
+
* cadence, 7.6 rows/s on the live fleet at the 10 s default. Only a sample
|
|
1103
|
+
* the in-memory ring ACCEPTED is written, which is what makes the append
|
|
1104
|
+
* idempotent without a unique key."
|
|
1105
|
+
* retention="TWO bounds, swept together at most once a minute. By AGE, the
|
|
1106
|
+
* operator's `loadSeriesRetentionHours` (default 6, the intention); by ROWS,
|
|
1107
|
+
* DEFAULT_MAX_ROWS (the guarantee, which bites regardless of the age knob).
|
|
1108
|
+
* Both prune oldest-first through a bounded page — never a full scan — and
|
|
1109
|
+
* the row cap logs when it bites. Losing the table loses the cold window
|
|
1110
|
+
* only: the ring keeps serving the last hour and the charts keep drawing."
|
|
1111
|
+
*/
|
|
1112
|
+
var LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
|
|
1113
|
+
/**
|
|
1114
|
+
* Hard ceiling on the table, in ROWS.
|
|
1115
|
+
*
|
|
1116
|
+
* The guarantee behind the retention knob. Sized against the shipped default
|
|
1117
|
+
* and against the worst configuration the knob can express:
|
|
1118
|
+
*
|
|
1119
|
+
* | cadence | retention | rows (3 nodes, 76 processes) |
|
|
1120
|
+
* | --- | --- | --- |
|
|
1121
|
+
* | 10 s | 6 h (default) | 164 160 |
|
|
1122
|
+
* | 5 s | 6 h | 328 320 |
|
|
1123
|
+
* | 5 s | 72 h | 3 939 840 — capped here |
|
|
1124
|
+
*
|
|
1125
|
+
* 500 000 rows x the measured 87 B = **41.5 MiB**, roughly 3x what the shipped
|
|
1126
|
+
* default needs and far below the 2 M rows a three-day retention would reach.
|
|
1127
|
+
* It is deliberately NOT a knob: the point of a guarantee is that it is not
|
|
1128
|
+
* subject to the same hand that raised the intention. This is the discipline
|
|
1129
|
+
* the in-memory ring already adopted (`DEFAULT_MAX_TOTAL_PROCESS_ROWS`) and
|
|
1130
|
+
* the one the RTP pre-roll rings still lack — a maximum per instance and no
|
|
1131
|
+
* budget at all for the whole, which reached 898 MB across 47 rings
|
|
1132
|
+
* (`docs/architecture/load-ledger.md` #5).
|
|
1133
|
+
*/
|
|
1134
|
+
var DEFAULT_MAX_ROWS = 5e5;
|
|
1135
|
+
var LOAD_SERIES_COLUMNS = [
|
|
1136
|
+
(
|
|
1137
|
+
/** `INTEGER PRIMARY KEY` = SQLite rowid alias: the key IS the row's address,
|
|
1138
|
+
* so it costs no separate index and no stored string. */
|
|
1139
|
+
{
|
|
1140
|
+
name: "id",
|
|
1141
|
+
type: "INTEGER",
|
|
1142
|
+
primaryKey: true,
|
|
1143
|
+
notNull: true
|
|
1144
|
+
}),
|
|
1145
|
+
{
|
|
1146
|
+
name: "nodeId",
|
|
1147
|
+
type: "TEXT",
|
|
1148
|
+
notNull: true
|
|
1149
|
+
},
|
|
1150
|
+
(
|
|
1151
|
+
/** The EMITTING node's timestamp for the sample this row belongs to. Every
|
|
1152
|
+
* row of one sample shares it — that is what makes a sample reassemblable. */
|
|
1153
|
+
{
|
|
1154
|
+
name: "atMs",
|
|
1155
|
+
type: "INTEGER",
|
|
1156
|
+
notNull: true
|
|
1157
|
+
}),
|
|
1158
|
+
{
|
|
1159
|
+
name: "pid",
|
|
1160
|
+
type: "INTEGER",
|
|
1161
|
+
notNull: true
|
|
1162
|
+
},
|
|
1163
|
+
(
|
|
1164
|
+
/** `NULL` for a process no addon owns — `root` and `system` both. */
|
|
1165
|
+
{
|
|
1166
|
+
name: "addonId",
|
|
1167
|
+
type: "TEXT"
|
|
1168
|
+
}),
|
|
1169
|
+
{
|
|
1170
|
+
name: "classification",
|
|
1171
|
+
type: "TEXT",
|
|
1172
|
+
notNull: true
|
|
1173
|
+
},
|
|
1174
|
+
(
|
|
1175
|
+
/** `ps pcpu` x 10. A LIFETIME average, not a rate — see `NodeProcess`. */
|
|
1176
|
+
{
|
|
1177
|
+
name: "cpuDeci",
|
|
1178
|
+
type: "INTEGER",
|
|
1179
|
+
notNull: true
|
|
1180
|
+
}),
|
|
1181
|
+
{
|
|
1182
|
+
name: "rssMib",
|
|
1183
|
+
type: "INTEGER",
|
|
1184
|
+
notNull: true
|
|
1185
|
+
},
|
|
1186
|
+
(
|
|
1187
|
+
/** Instantaneous main-thread CPU% x 10. `NULL` = UNKNOWN, never zero. */
|
|
1188
|
+
{
|
|
1189
|
+
name: "cpuMainDeci",
|
|
1190
|
+
type: "INTEGER"
|
|
1191
|
+
}),
|
|
1192
|
+
(
|
|
1193
|
+
/** Instantaneous V8-helper-pool CPU% x 10. `NULL` = UNKNOWN, never zero. */
|
|
1194
|
+
{
|
|
1195
|
+
name: "cpuGcDeci",
|
|
1196
|
+
type: "INTEGER"
|
|
1197
|
+
})
|
|
1198
|
+
];
|
|
1199
|
+
var LOAD_SERIES_INDEXES = [(
|
|
1200
|
+
/**
|
|
1201
|
+
* The ONE index, and it serves both jobs.
|
|
1202
|
+
*
|
|
1203
|
+
* Reads are always "this node, newer than T" — `nodeId` leads so the
|
|
1204
|
+
* equality is a range scan and `atMs` supplies the order without a sort.
|
|
1205
|
+
* Prunes are always "this node, oldest first" — the same index, walked from
|
|
1206
|
+
* the other end. A second index on `atMs` alone was measured and rejected:
|
|
1207
|
+
* it cost 23 B/row (196.0 → 172.8 with the two column drops) to serve a
|
|
1208
|
+
* cross-node prune that a per-node loop over three nodes already serves.
|
|
1209
|
+
*/
|
|
1210
|
+
{
|
|
1211
|
+
name: "idx_load_samples_node_at",
|
|
1212
|
+
columns: ["nodeId", "atMs"]
|
|
1213
|
+
})];
|
|
1214
|
+
var BYTES_PER_MIB = 1048576;
|
|
1215
|
+
/** Tenths → percent. `null` stays `null`: unknown is not zero. */
|
|
1216
|
+
function fromDeci(value) {
|
|
1217
|
+
return value === null ? null : Math.round(value) / 10;
|
|
1218
|
+
}
|
|
1219
|
+
/** Percent → tenths, lossless because both producers already round to tenths. */
|
|
1220
|
+
function toDeci(value) {
|
|
1221
|
+
return value === null ? null : Math.round(value * 10);
|
|
1222
|
+
}
|
|
1223
|
+
/** Storage row → the shape a reader gets. The ONE decode. */
|
|
1224
|
+
function rowToProcess(row) {
|
|
1225
|
+
return {
|
|
1226
|
+
pid: row.pid,
|
|
1227
|
+
addonId: row.addonId,
|
|
1228
|
+
classification: row.classification,
|
|
1229
|
+
cpuPercent: fromDeci(row.cpuDeci) ?? 0,
|
|
1230
|
+
memoryRssBytes: row.rssMib * BYTES_PER_MIB,
|
|
1231
|
+
cpuMainPercent: fromDeci(row.cpuMainDeci),
|
|
1232
|
+
cpuGcPercent: fromDeci(row.cpuGcDeci)
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
/** The shape a reader gets → the storage row. The ONE encode. */
|
|
1236
|
+
function processToRow(nodeId, atMs, process) {
|
|
1237
|
+
return {
|
|
1238
|
+
nodeId,
|
|
1239
|
+
atMs,
|
|
1240
|
+
pid: process.pid,
|
|
1241
|
+
addonId: process.addonId,
|
|
1242
|
+
classification: process.classification,
|
|
1243
|
+
cpuDeci: toDeci(process.cpuPercent) ?? 0,
|
|
1244
|
+
rssMib: Math.round(process.memoryRssBytes / BYTES_PER_MIB),
|
|
1245
|
+
cpuMainDeci: toDeci(process.cpuMainPercent),
|
|
1246
|
+
cpuGcDeci: toDeci(process.cpuGcPercent)
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
/** Group flat rows into samples, oldest-first. A sample is one `atMs`. */
|
|
1250
|
+
function rowsToSamples(rows) {
|
|
1251
|
+
const byAt = /* @__PURE__ */ new Map();
|
|
1252
|
+
for (const row of rows) {
|
|
1253
|
+
const held = byAt.get(row.atMs);
|
|
1254
|
+
if (held === void 0) byAt.set(row.atMs, [rowToProcess(row)]);
|
|
1255
|
+
else held.push(rowToProcess(row));
|
|
1108
1256
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1257
|
+
return [...byAt.entries()].toSorted((a, b) => a[0] - b[0]).map(([atMs, processes]) => ({
|
|
1258
|
+
atMs,
|
|
1259
|
+
processes
|
|
1260
|
+
}));
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* The cold tier. Append-only, bounded, and never the authority on anything the
|
|
1264
|
+
* hot ring can answer.
|
|
1265
|
+
*
|
|
1266
|
+
* Every method is best-effort and says so in the log rather than throwing at
|
|
1267
|
+
* the emitter: a storage hiccup must cost a gap in a chart, never a dropped
|
|
1268
|
+
* snapshot on the bus.
|
|
1269
|
+
*/
|
|
1270
|
+
var LoadSeriesStore = class {
|
|
1271
|
+
declared = false;
|
|
1272
|
+
lastPruneAtMs = 0;
|
|
1273
|
+
store;
|
|
1274
|
+
logger;
|
|
1275
|
+
nowFn;
|
|
1276
|
+
prunePageRows;
|
|
1277
|
+
pruneIntervalMs;
|
|
1278
|
+
constructor(deps) {
|
|
1279
|
+
this.store = deps.store;
|
|
1280
|
+
this.logger = deps.logger;
|
|
1281
|
+
this.nowFn = deps.now ?? (() => Date.now());
|
|
1282
|
+
this.prunePageRows = deps.prunePageRows ?? 2e4;
|
|
1283
|
+
this.pruneIntervalMs = deps.pruneIntervalMs ?? 6e4;
|
|
1116
1284
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1285
|
+
/** Idempotently declare the collection. `false` when the store refused. */
|
|
1286
|
+
async declare() {
|
|
1287
|
+
if (this.declared) return true;
|
|
1288
|
+
try {
|
|
1289
|
+
await this.store.declareCollection.mutate({
|
|
1290
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1291
|
+
columns: [...LOAD_SERIES_COLUMNS],
|
|
1292
|
+
indexes: LOAD_SERIES_INDEXES.map((i) => ({
|
|
1293
|
+
name: i.name,
|
|
1294
|
+
columns: [...i.columns]
|
|
1295
|
+
}))
|
|
1296
|
+
});
|
|
1297
|
+
this.declared = true;
|
|
1298
|
+
return true;
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
this.logger.warn("load series declareCollection failed — nothing will be retained on disk", { meta: {
|
|
1301
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1302
|
+
error: require_dist.errMsg(err)
|
|
1303
|
+
} });
|
|
1304
|
+
return false;
|
|
1122
1305
|
}
|
|
1123
|
-
this.collectSnapshot().then((s) => {
|
|
1124
|
-
this.cachedSnapshot = s;
|
|
1125
|
-
this.consecutiveSampleErrors = 0;
|
|
1126
|
-
}).catch((err) => this.reportSampleError(err));
|
|
1127
|
-
this.samplingTimer = setInterval(() => {
|
|
1128
|
-
this.collectSnapshot().then((s) => {
|
|
1129
|
-
this.cachedSnapshot = s;
|
|
1130
|
-
this.consecutiveSampleErrors = 0;
|
|
1131
|
-
}).catch((err) => this.reportSampleError(err));
|
|
1132
|
-
}, intervalMs);
|
|
1133
|
-
if (this.samplingTimer.unref) this.samplingTimer.unref();
|
|
1134
1306
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
if (this.
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1307
|
+
/**
|
|
1308
|
+
* Append ONE sample — every process row of it — in ONE transaction.
|
|
1309
|
+
*
|
|
1310
|
+
* Never a write per row. `insertMany` exists for exactly this: at the 10 s
|
|
1311
|
+
* default the fleet produces 7.6 rows/s, and 7.6 separate commits per second
|
|
1312
|
+
* on the connection that also serves every cluster-wide configuration read
|
|
1313
|
+
* is a constant load nobody asked for.
|
|
1314
|
+
*/
|
|
1315
|
+
async append(nodeId, atMs, processes) {
|
|
1316
|
+
if (processes.length === 0) return 0;
|
|
1317
|
+
if (!await this.declare()) return 0;
|
|
1318
|
+
const records = processes.map((p) => ({ data: { ...processToRow(nodeId, atMs, p) } }));
|
|
1319
|
+
try {
|
|
1320
|
+
const { inserted } = await this.store.insertMany.mutate({
|
|
1321
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1322
|
+
records
|
|
1323
|
+
});
|
|
1324
|
+
return inserted;
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
this.logger.warn("load series sample not retained — this interval will be missing", { meta: {
|
|
1327
|
+
nodeId,
|
|
1328
|
+
atMs,
|
|
1329
|
+
rows: processes.length,
|
|
1330
|
+
error: require_dist.errMsg(err)
|
|
1331
|
+
} });
|
|
1332
|
+
return 0;
|
|
1152
1333
|
}
|
|
1153
1334
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1335
|
+
/**
|
|
1336
|
+
* Read one node's cold samples, oldest-first.
|
|
1337
|
+
*
|
|
1338
|
+
* `sinceMs` is EXCLUSIVE, matching the ring, so a caller passing the newest
|
|
1339
|
+
* timestamp it holds gets only what it is missing. `limitRows` bounds the
|
|
1340
|
+
* read in ROWS (not samples) because rows are what the query costs.
|
|
1341
|
+
*/
|
|
1342
|
+
async read(nodeId, sinceMs, limitRows) {
|
|
1343
|
+
if (!await this.declare()) return [];
|
|
1344
|
+
try {
|
|
1345
|
+
const records = await this.store.query.query({
|
|
1346
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1347
|
+
filter: {
|
|
1348
|
+
where: { nodeId },
|
|
1349
|
+
whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
|
|
1350
|
+
orderBy: {
|
|
1351
|
+
field: "atMs",
|
|
1352
|
+
direction: "asc"
|
|
1353
|
+
},
|
|
1354
|
+
limit: limitRows
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
const rows = [];
|
|
1358
|
+
for (const record of records) {
|
|
1359
|
+
const row = recordToRow(record.data);
|
|
1360
|
+
if (row !== null) rows.push(row);
|
|
1361
|
+
}
|
|
1362
|
+
return rowsToSamples(rows);
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
this.logger.warn("load series cold read failed — answering from the hot window only", { meta: {
|
|
1365
|
+
nodeId,
|
|
1366
|
+
sinceMs,
|
|
1367
|
+
error: require_dist.errMsg(err)
|
|
1368
|
+
} });
|
|
1369
|
+
return [];
|
|
1370
|
+
}
|
|
1184
1371
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1372
|
+
/**
|
|
1373
|
+
* Enforce BOTH bounds, oldest-first, through a bounded page each.
|
|
1374
|
+
*
|
|
1375
|
+
* Rate-limited to {@link pruneIntervalMs}: a bound is not a deadline, and the
|
|
1376
|
+
* append path must not pay a sweep on every sample.
|
|
1377
|
+
*/
|
|
1378
|
+
async prune(nodeIds, retention, force = false) {
|
|
1379
|
+
const now = this.nowFn();
|
|
1380
|
+
if (!force && now - this.lastPruneAtMs < this.pruneIntervalMs) return null;
|
|
1381
|
+
this.lastPruneAtMs = now;
|
|
1382
|
+
if (!await this.declare()) return null;
|
|
1383
|
+
let deletedByAge = 0;
|
|
1384
|
+
let deletedByCap = 0;
|
|
1385
|
+
let rowsExamined = 0;
|
|
1386
|
+
const ageCutoff = now - retention.retentionHours * 36e5;
|
|
1387
|
+
for (const nodeId of nodeIds) {
|
|
1388
|
+
const outcome = await this.pruneNodeToCutoff(nodeId, ageCutoff);
|
|
1389
|
+
deletedByAge += outcome.deleted;
|
|
1390
|
+
rowsExamined += outcome.examined;
|
|
1391
|
+
}
|
|
1392
|
+
const total = await this.count();
|
|
1393
|
+
const excess = total === null ? 0 : total - retention.maxRows;
|
|
1394
|
+
if (excess > 0) {
|
|
1395
|
+
const outcome = await this.pruneOldestRows(nodeIds, excess);
|
|
1396
|
+
deletedByCap = outcome.deleted;
|
|
1397
|
+
rowsExamined += outcome.examined;
|
|
1398
|
+
this.logger.warn("load series ROW CAP bit — evicting the oldest samples", { meta: {
|
|
1399
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1400
|
+
rows: total,
|
|
1401
|
+
cap: retention.maxRows,
|
|
1402
|
+
over: excess,
|
|
1403
|
+
deleted: outcome.deleted,
|
|
1404
|
+
retentionHours: retention.retentionHours,
|
|
1405
|
+
hint: "lower the retention or the sampling cadence — the cap is the guarantee, not the intention"
|
|
1406
|
+
} });
|
|
1407
|
+
}
|
|
1188
1408
|
return {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
temperature: s.cpuTemperature ?? void 0,
|
|
1194
|
-
gpuPercent: s.gpu?.utilization,
|
|
1195
|
-
gpuMemoryPercent: s.gpu && s.gpu.memoryTotalBytes > 0 ? Math.round(s.gpu.memoryUsedBytes / s.gpu.memoryTotalBytes * 1e3) / 10 : void 0
|
|
1409
|
+
deletedByAge,
|
|
1410
|
+
deletedByCap,
|
|
1411
|
+
rowsExamined,
|
|
1412
|
+
capBit: excess > 0
|
|
1196
1413
|
};
|
|
1197
1414
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
async getProcessStats(input) {
|
|
1208
|
-
const { getPidStats } = await Promise.resolve().then(() => require("../../resource-monitor-CdnzxBLP.js")).then((n) => n.resource_monitor_exports);
|
|
1209
|
-
const raw = await getPidStats(input.pids);
|
|
1210
|
-
const result = [];
|
|
1211
|
-
for (const [pid, s] of raw) result.push({
|
|
1212
|
-
pid,
|
|
1213
|
-
cpu: s.cpu,
|
|
1214
|
-
memory: s.memory,
|
|
1215
|
-
privateBytes: s.privateBytes,
|
|
1216
|
-
sharedBytes: s.sharedBytes
|
|
1217
|
-
});
|
|
1218
|
-
return result;
|
|
1415
|
+
/** Total rows, or `null` when the store could not answer. */
|
|
1416
|
+
async count() {
|
|
1417
|
+
if (!await this.declare()) return null;
|
|
1418
|
+
try {
|
|
1419
|
+
return await this.store.count.query({ collection: LOAD_SERIES_COLLECTION });
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
this.logger.warn("load series count failed — the row cap is not enforced this pass", { meta: { error: require_dist.errMsg(err) } });
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1219
1424
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1425
|
+
/**
|
|
1426
|
+
* Delete this node's rows older than `cutoff`, at most one page's worth.
|
|
1427
|
+
*
|
|
1428
|
+
* The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
|
|
1429
|
+
* own is one statement but an UNBOUNDED one — a first pass after a retention
|
|
1430
|
+
* change would delete millions of rows inside a single stalling transaction.
|
|
1431
|
+
* So the page is read first (keys only, ordered by the index), its last
|
|
1432
|
+
* `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
|
|
1433
|
+
*/
|
|
1434
|
+
async pruneNodeToCutoff(nodeId, cutoff) {
|
|
1435
|
+
try {
|
|
1436
|
+
const page = await this.store.query.query({
|
|
1437
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1438
|
+
filter: {
|
|
1439
|
+
where: { nodeId },
|
|
1440
|
+
whereBetween: { atMs: [0, cutoff] },
|
|
1441
|
+
orderBy: {
|
|
1442
|
+
field: "atMs",
|
|
1443
|
+
direction: "asc"
|
|
1444
|
+
},
|
|
1445
|
+
limit: this.prunePageRows
|
|
1446
|
+
},
|
|
1447
|
+
columns: ["atMs"]
|
|
1448
|
+
});
|
|
1449
|
+
if (page.length === 0) return {
|
|
1450
|
+
deleted: 0,
|
|
1451
|
+
examined: 0
|
|
1452
|
+
};
|
|
1453
|
+
const effectiveCutoff = Number(page.at(-1)?.data["atMs"]);
|
|
1454
|
+
if (!Number.isFinite(effectiveCutoff)) return {
|
|
1455
|
+
deleted: 0,
|
|
1456
|
+
examined: page.length
|
|
1457
|
+
};
|
|
1458
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
1459
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1460
|
+
filter: {
|
|
1461
|
+
where: { nodeId },
|
|
1462
|
+
whereBetween: { atMs: [0, effectiveCutoff] }
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
return {
|
|
1466
|
+
deleted,
|
|
1467
|
+
examined: page.length
|
|
1468
|
+
};
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
this.logger.warn("load series age prune failed — the table keeps growing this pass", { meta: {
|
|
1471
|
+
nodeId,
|
|
1472
|
+
cutoff,
|
|
1473
|
+
error: require_dist.errMsg(err)
|
|
1474
|
+
} });
|
|
1475
|
+
return {
|
|
1476
|
+
deleted: 0,
|
|
1477
|
+
examined: 0
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1240
1480
|
}
|
|
1241
|
-
/**
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1481
|
+
/**
|
|
1482
|
+
* Drop the oldest rows across the known nodes until `excess` is covered.
|
|
1483
|
+
*
|
|
1484
|
+
* Same bounded-page technique, walked per node so the one index serves it.
|
|
1485
|
+
* The node holding the oldest rows pays first, which is also the node
|
|
1486
|
+
* producing the pressure when a runaway process count is the cause.
|
|
1487
|
+
*/
|
|
1488
|
+
async pruneOldestRows(nodeIds, excess) {
|
|
1489
|
+
let remaining = Math.min(excess, this.prunePageRows);
|
|
1490
|
+
let deleted = 0;
|
|
1491
|
+
let examined = 0;
|
|
1492
|
+
for (const nodeId of nodeIds) {
|
|
1493
|
+
if (remaining <= 0) break;
|
|
1494
|
+
try {
|
|
1495
|
+
const page = await this.store.query.query({
|
|
1496
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1497
|
+
filter: {
|
|
1498
|
+
where: { nodeId },
|
|
1499
|
+
orderBy: {
|
|
1500
|
+
field: "atMs",
|
|
1501
|
+
direction: "asc"
|
|
1502
|
+
},
|
|
1503
|
+
limit: remaining
|
|
1504
|
+
},
|
|
1505
|
+
columns: ["atMs"]
|
|
1506
|
+
});
|
|
1507
|
+
examined += page.length;
|
|
1508
|
+
if (page.length === 0) continue;
|
|
1509
|
+
const cutoff = Number(page.at(-1)?.data["atMs"]);
|
|
1510
|
+
if (!Number.isFinite(cutoff)) continue;
|
|
1511
|
+
const result = await this.store.deleteWhere.mutate({
|
|
1512
|
+
collection: LOAD_SERIES_COLLECTION,
|
|
1513
|
+
filter: {
|
|
1514
|
+
where: { nodeId },
|
|
1515
|
+
whereBetween: { atMs: [0, cutoff] }
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
deleted += result.deleted;
|
|
1519
|
+
remaining -= result.deleted;
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
this.logger.warn("load series cap prune failed — the cap is not enforced this pass", { meta: {
|
|
1522
|
+
nodeId,
|
|
1523
|
+
error: require_dist.errMsg(err)
|
|
1524
|
+
} });
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1265
1527
|
return {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
usedBytes: used,
|
|
1269
|
-
availableBytes: free,
|
|
1270
|
-
swapUsedBytes,
|
|
1271
|
-
swapTotalBytes
|
|
1528
|
+
deleted,
|
|
1529
|
+
examined
|
|
1272
1530
|
};
|
|
1273
1531
|
}
|
|
1274
1532
|
};
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const output = (0, node_child_process.execFileSync)("vm_stat", {
|
|
1296
|
-
timeout: 2e3,
|
|
1297
|
-
encoding: "utf-8"
|
|
1298
|
-
});
|
|
1299
|
-
const pageSize = 16384;
|
|
1300
|
-
const parse = (label) => {
|
|
1301
|
-
const match = output.match(new RegExp(`${label}:\\s+(\\d+)`));
|
|
1302
|
-
return match ? parseInt(match[1], 10) * pageSize : 0;
|
|
1303
|
-
};
|
|
1304
|
-
const free = parse("Pages free");
|
|
1305
|
-
const inactive = parse("Pages inactive");
|
|
1306
|
-
const purgeable = parse("Pages purgeable");
|
|
1307
|
-
const speculative = parse("Pages speculative");
|
|
1308
|
-
const available = free + inactive + purgeable + speculative;
|
|
1309
|
-
const total = node_os.totalmem();
|
|
1310
|
-
const used = total - available;
|
|
1311
|
-
let swapUsedBytes = 0;
|
|
1312
|
-
let swapTotalBytes = 0;
|
|
1313
|
-
try {
|
|
1314
|
-
const swapOut = (0, node_child_process.execFileSync)("sysctl", ["-n", "vm.swapusage"], {
|
|
1315
|
-
timeout: 2e3,
|
|
1316
|
-
encoding: "utf-8"
|
|
1317
|
-
});
|
|
1318
|
-
const totalMatch = swapOut.match(/total\s*=\s*([\d.]+)M/);
|
|
1319
|
-
const usedMatch = swapOut.match(/used\s*=\s*([\d.]+)M/);
|
|
1320
|
-
if (totalMatch) swapTotalBytes = parseFloat(totalMatch[1]) * 1024 * 1024;
|
|
1321
|
-
if (usedMatch) swapUsedBytes = parseFloat(usedMatch[1]) * 1024 * 1024;
|
|
1322
|
-
} catch {}
|
|
1323
|
-
provider._cachedMacMem = {
|
|
1324
|
-
percent: Math.round(used / total * 1e3) / 10,
|
|
1325
|
-
totalBytes: total,
|
|
1326
|
-
usedBytes: used,
|
|
1327
|
-
availableBytes: available,
|
|
1328
|
-
swapUsedBytes,
|
|
1329
|
-
swapTotalBytes
|
|
1330
|
-
};
|
|
1331
|
-
} catch {
|
|
1332
|
-
provider._cachedMacMem = null;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
function getDiskSpaceInternal(dirPath) {
|
|
1336
|
-
return new Promise((resolve, reject) => {
|
|
1337
|
-
node_fs.statfs(dirPath, (err, stats) => {
|
|
1338
|
-
if (err) return reject(err);
|
|
1339
|
-
const totalBytes = stats.blocks * stats.bsize;
|
|
1340
|
-
const availableBytes = stats.bavail * stats.bsize;
|
|
1341
|
-
const usedBytes = totalBytes - stats.bfree * stats.bsize;
|
|
1342
|
-
resolve({
|
|
1343
|
-
path: dirPath,
|
|
1344
|
-
totalBytes,
|
|
1345
|
-
usedBytes,
|
|
1346
|
-
availableBytes,
|
|
1347
|
-
percent: totalBytes > 0 ? Math.round(usedBytes / totalBytes * 1e3) / 10 : 0
|
|
1348
|
-
});
|
|
1349
|
-
});
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
async function takeDiskIoSnapshot() {
|
|
1353
|
-
const now = Date.now();
|
|
1354
|
-
if (IS_LINUX) try {
|
|
1355
|
-
const data = node_fs.readFileSync("/proc/diskstats", "utf-8");
|
|
1356
|
-
let readOps = 0, readBytes = 0, writeOps = 0, writeBytes = 0;
|
|
1357
|
-
for (const line of data.split("\n")) {
|
|
1358
|
-
const parts = line.trim().split(/\s+/);
|
|
1359
|
-
if (parts.length < 14) continue;
|
|
1360
|
-
const devName = parts[2];
|
|
1361
|
-
if (/\d+$/.test(devName) && !/^nvme\d+n\d+$/.test(devName)) continue;
|
|
1362
|
-
readOps += parseInt(parts[3], 10) || 0;
|
|
1363
|
-
readBytes += (parseInt(parts[5], 10) || 0) * 512;
|
|
1364
|
-
writeOps += parseInt(parts[7], 10) || 0;
|
|
1365
|
-
writeBytes += (parseInt(parts[9], 10) || 0) * 512;
|
|
1366
|
-
}
|
|
1367
|
-
return {
|
|
1368
|
-
readBytes,
|
|
1369
|
-
writeBytes,
|
|
1370
|
-
readOps,
|
|
1371
|
-
writeOps,
|
|
1372
|
-
timestampMs: now
|
|
1373
|
-
};
|
|
1374
|
-
} catch {}
|
|
1375
|
-
return {
|
|
1376
|
-
readBytes: 0,
|
|
1377
|
-
writeBytes: 0,
|
|
1378
|
-
readOps: 0,
|
|
1379
|
-
writeOps: 0,
|
|
1380
|
-
timestampMs: now
|
|
1533
|
+
/** Structural validation on read. A malformed row is skipped, never charted. */
|
|
1534
|
+
function recordToRow(data) {
|
|
1535
|
+
const nodeId = data["nodeId"];
|
|
1536
|
+
const classification = data["classification"];
|
|
1537
|
+
if (typeof nodeId !== "string" || typeof classification !== "string") return null;
|
|
1538
|
+
const atMs = Number(data["atMs"]);
|
|
1539
|
+
const pid = Number(data["pid"]);
|
|
1540
|
+
const cpuDeci = Number(data["cpuDeci"]);
|
|
1541
|
+
const rssMib = Number(data["rssMib"]);
|
|
1542
|
+
if (![
|
|
1543
|
+
atMs,
|
|
1544
|
+
pid,
|
|
1545
|
+
cpuDeci,
|
|
1546
|
+
rssMib
|
|
1547
|
+
].every((n) => Number.isFinite(n))) return null;
|
|
1548
|
+
const rawAddon = data["addonId"];
|
|
1549
|
+
const optional = (raw) => {
|
|
1550
|
+
if (raw === null || raw === void 0) return null;
|
|
1551
|
+
const n = Number(raw);
|
|
1552
|
+
return Number.isFinite(n) ? n : null;
|
|
1381
1553
|
};
|
|
1382
|
-
}
|
|
1383
|
-
async function takeNetworkIoSnapshot() {
|
|
1384
|
-
const now = Date.now();
|
|
1385
|
-
if (IS_LINUX) try {
|
|
1386
|
-
const data = node_fs.readFileSync("/proc/net/dev", "utf-8");
|
|
1387
|
-
let rxBytes = 0, txBytes = 0, rxPackets = 0, txPackets = 0, rxErrors = 0, txErrors = 0;
|
|
1388
|
-
for (const line of data.split("\n")) {
|
|
1389
|
-
const match = line.match(/^\s*(\w+):\s+(.+)$/);
|
|
1390
|
-
if (!match) continue;
|
|
1391
|
-
if (match[1] === "lo") continue;
|
|
1392
|
-
const cols = match[2].trim().split(/\s+/);
|
|
1393
|
-
rxBytes += parseInt(cols[0], 10) || 0;
|
|
1394
|
-
rxPackets += parseInt(cols[1], 10) || 0;
|
|
1395
|
-
rxErrors += parseInt(cols[2], 10) || 0;
|
|
1396
|
-
txBytes += parseInt(cols[8], 10) || 0;
|
|
1397
|
-
txPackets += parseInt(cols[9], 10) || 0;
|
|
1398
|
-
txErrors += parseInt(cols[10], 10) || 0;
|
|
1399
|
-
}
|
|
1400
|
-
return {
|
|
1401
|
-
rxBytes,
|
|
1402
|
-
txBytes,
|
|
1403
|
-
rxPackets,
|
|
1404
|
-
txPackets,
|
|
1405
|
-
rxErrors,
|
|
1406
|
-
txErrors,
|
|
1407
|
-
timestampMs: now
|
|
1408
|
-
};
|
|
1409
|
-
} catch {}
|
|
1410
|
-
if (IS_DARWIN) try {
|
|
1411
|
-
const stdout = await execAsync("netstat", ["-ib"]);
|
|
1412
|
-
let rxBytes = 0, txBytes = 0, rxPackets = 0, txPackets = 0, rxErrors = 0, txErrors = 0;
|
|
1413
|
-
for (const line of stdout.split("\n")) {
|
|
1414
|
-
const parts = line.trim().split(/\s+/);
|
|
1415
|
-
if (parts.length < 11) continue;
|
|
1416
|
-
if (parts[0] === "lo0" || parts[0] === "Name") continue;
|
|
1417
|
-
if (!parts[2]?.startsWith("Link#")) continue;
|
|
1418
|
-
rxPackets += parseInt(parts[4], 10) || 0;
|
|
1419
|
-
rxErrors += parseInt(parts[5], 10) || 0;
|
|
1420
|
-
rxBytes += parseInt(parts[6], 10) || 0;
|
|
1421
|
-
txPackets += parseInt(parts[7], 10) || 0;
|
|
1422
|
-
txErrors += parseInt(parts[8], 10) || 0;
|
|
1423
|
-
txBytes += parseInt(parts[9], 10) || 0;
|
|
1424
|
-
}
|
|
1425
|
-
return {
|
|
1426
|
-
rxBytes,
|
|
1427
|
-
txBytes,
|
|
1428
|
-
rxPackets,
|
|
1429
|
-
txPackets,
|
|
1430
|
-
rxErrors,
|
|
1431
|
-
txErrors,
|
|
1432
|
-
timestampMs: now
|
|
1433
|
-
};
|
|
1434
|
-
} catch {}
|
|
1435
1554
|
return {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1555
|
+
nodeId,
|
|
1556
|
+
atMs,
|
|
1557
|
+
pid,
|
|
1558
|
+
addonId: typeof rawAddon === "string" ? rawAddon : null,
|
|
1559
|
+
classification,
|
|
1560
|
+
cpuDeci,
|
|
1561
|
+
rssMib,
|
|
1562
|
+
cpuMainDeci: optional(data["cpuMainDeci"]),
|
|
1563
|
+
cpuGcDeci: optional(data["cpuGcDeci"])
|
|
1443
1564
|
};
|
|
1444
1565
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
try {
|
|
1504
|
-
openFds = node_fs.readdirSync(`/proc/${process.pid}/fd`).length;
|
|
1505
|
-
} catch {}
|
|
1506
|
-
try {
|
|
1507
|
-
const match = node_fs.readFileSync(`/proc/${process.pid}/status`, "utf-8").match(/Threads:\s+(\d+)/);
|
|
1508
|
-
if (match) threadCount = parseInt(match[1], 10);
|
|
1509
|
-
} catch {}
|
|
1566
|
+
//#endregion
|
|
1567
|
+
//#region src/builtins/native-metrics/load-series-config.ts
|
|
1568
|
+
/**
|
|
1569
|
+
* The two knobs of the durable load series, and the arithmetic that makes them
|
|
1570
|
+
* honest.
|
|
1571
|
+
*
|
|
1572
|
+
* ## Where they live, and why there
|
|
1573
|
+
*
|
|
1574
|
+
* In `native-metrics`'s OWN global settings document — the one that already
|
|
1575
|
+
* exists and already carries `samplingIntervalMs`. Fields on a document, never
|
|
1576
|
+
* a method per knob: `system.getLoggingSettings` / `setLoggingSettings` set
|
|
1577
|
+
* that precedent and it holds here.
|
|
1578
|
+
*
|
|
1579
|
+
* They are NOT in the logging document, and that is a decision rather than an
|
|
1580
|
+
* omission. That document is about levels and diagnostic windows; a metrics
|
|
1581
|
+
* cadence and a table's retention are neither. Two documents that both claim a
|
|
1582
|
+
* knob is how this repo has already shipped a switch nobody read.
|
|
1583
|
+
*
|
|
1584
|
+
* They are cluster-wide, not per-node. Every node emits on the same cadence or
|
|
1585
|
+
* the fleet's series cannot be laid over each other, and the table is single
|
|
1586
|
+
* and hub-resident, so a per-node retention would be a promise nothing could
|
|
1587
|
+
* keep.
|
|
1588
|
+
*
|
|
1589
|
+
* ## Refused, not clamped
|
|
1590
|
+
*
|
|
1591
|
+
* A cadence outside 5-60 s is REFUSED. Clamping would silently store 10 when
|
|
1592
|
+
* the operator typed 2 and then report 10 back as though it had been accepted
|
|
1593
|
+
* — the failure mode where a knob and its readback agree on a value nobody
|
|
1594
|
+
* chose. The schema advertises the bounds; this refuses anything else.
|
|
1595
|
+
*/
|
|
1596
|
+
/** Why a value was refused — the message an operator is shown verbatim. */
|
|
1597
|
+
var LoadSeriesConfigError = class extends Error {
|
|
1598
|
+
constructor(message) {
|
|
1599
|
+
super(message);
|
|
1600
|
+
this.name = "LoadSeriesConfigError";
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
function requireInteger(value, field) {
|
|
1604
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1605
|
+
if (!Number.isFinite(n)) throw new LoadSeriesConfigError(`${field} must be a number, received ${String(value)}`);
|
|
1606
|
+
return Math.round(n);
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Resolve the configuration, REFUSING anything out of range.
|
|
1610
|
+
*
|
|
1611
|
+
* An ABSENT field takes its default — that is not a refusal, it is a document
|
|
1612
|
+
* that has never been written. A PRESENT field outside its bounds throws.
|
|
1613
|
+
*/
|
|
1614
|
+
function resolveLoadSeriesConfig(raw) {
|
|
1615
|
+
let cadenceSec = 10;
|
|
1616
|
+
if (raw.loadSeriesCadenceSec !== void 0 && raw.loadSeriesCadenceSec !== null) {
|
|
1617
|
+
cadenceSec = requireInteger(raw.loadSeriesCadenceSec, "loadSeriesCadenceSec");
|
|
1618
|
+
if (cadenceSec < 5 || cadenceSec > 60) throw new LoadSeriesConfigError(`load series cadence must be between 5 and 60 seconds — refused ${cadenceSec}`);
|
|
1619
|
+
}
|
|
1620
|
+
let retentionHours = 6;
|
|
1621
|
+
if (raw.loadSeriesRetentionHours !== void 0 && raw.loadSeriesRetentionHours !== null) {
|
|
1622
|
+
retentionHours = requireInteger(raw.loadSeriesRetentionHours, "loadSeriesRetentionHours");
|
|
1623
|
+
if (retentionHours < 1 || retentionHours > 72) throw new LoadSeriesConfigError(`load series retention must be between 1 and 72 hours — refused ${retentionHours}`);
|
|
1510
1624
|
}
|
|
1511
|
-
const nodeProcess = process;
|
|
1512
1625
|
return {
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
activeRequests: (nodeProcess._getActiveRequests?.() ?? []).length
|
|
1626
|
+
cadenceSec,
|
|
1627
|
+
retentionHours,
|
|
1628
|
+
maxRows: DEFAULT_MAX_ROWS
|
|
1517
1629
|
};
|
|
1518
1630
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
} catch {
|
|
1539
|
-
return null;
|
|
1540
|
-
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Project the cost of a configuration from the numbers the cluster is actually
|
|
1633
|
+
* reporting — nodes and processes OBSERVED, never a constant.
|
|
1634
|
+
*
|
|
1635
|
+
* This is what makes the knob honest: an operator raising retention to three
|
|
1636
|
+
* days sees the row count and the megabytes BEFORE applying it, rather than
|
|
1637
|
+
* discovering them afterwards. `MEASURED_BYTES_PER_ROW` is measured, not
|
|
1638
|
+
* guessed — see `load-series-store.ts`.
|
|
1639
|
+
*/
|
|
1640
|
+
function projectLoadSeriesCost(input) {
|
|
1641
|
+
const samplesPerNodeWindow = input.config.retentionHours * 3600 / input.config.cadenceSec;
|
|
1642
|
+
const intendedRows = Math.round(samplesPerNodeWindow * Math.max(input.observedProcessRows, 0));
|
|
1643
|
+
const boundedRows = Math.min(intendedRows, input.config.maxRows);
|
|
1644
|
+
return {
|
|
1645
|
+
intendedRows,
|
|
1646
|
+
boundedRows,
|
|
1647
|
+
estimatedBytes: boundedRows * 87,
|
|
1648
|
+
capBites: intendedRows > input.config.maxRows
|
|
1649
|
+
};
|
|
1541
1650
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
})
|
|
1651
|
+
/** Human-readable one-liner for the settings form. */
|
|
1652
|
+
function describeLoadSeriesCost(projection) {
|
|
1653
|
+
const mib = (projection.estimatedBytes / 1048576).toFixed(1);
|
|
1654
|
+
const rows = projection.intendedRows.toLocaleString("en-US");
|
|
1655
|
+
const capped = projection.boundedRows.toLocaleString("en-US");
|
|
1656
|
+
if (!projection.capBites) return `This configuration retains ~${rows} rows ≈ ${mib} MiB on the hub database (NVMe cache), measured at 87 bytes per row.`;
|
|
1657
|
+
return `This configuration WANTS ~${rows} rows, above the ${projection.boundedRows.toLocaleString("en-US")}-row hard cap. The table will hold ~${capped} rows ≈ ${mib} MiB and the OLDEST samples will be evicted, so the effective window is shorter than the retention you asked for.`;
|
|
1549
1658
|
}
|
|
1550
1659
|
//#endregion
|
|
1551
|
-
//#region src/builtins/native-metrics/load-series-
|
|
1552
|
-
/**
|
|
1553
|
-
* @durable class=audit owner=metrics-native
|
|
1554
|
-
* write="one row per process per sample, appended in ONE transaction per
|
|
1555
|
-
* sample (`settingsStore.insertMany`) by the HUB's `native-metrics`, off its
|
|
1556
|
-
* subscription to `metrics.node-processes-snapshot`. Emission is
|
|
1557
|
-
* fixed-cadence, so the write rate is deterministic: nodes x processes /
|
|
1558
|
-
* cadence, 7.6 rows/s on the live fleet at the 10 s default. Only a sample
|
|
1559
|
-
* the in-memory ring ACCEPTED is written, which is what makes the append
|
|
1560
|
-
* idempotent without a unique key."
|
|
1561
|
-
* retention="TWO bounds, swept together at most once a minute. By AGE, the
|
|
1562
|
-
* operator's `loadSeriesRetentionHours` (default 6, the intention); by ROWS,
|
|
1563
|
-
* DEFAULT_MAX_ROWS (the guarantee, which bites regardless of the age knob).
|
|
1564
|
-
* Both prune oldest-first through a bounded page — never a full scan — and
|
|
1565
|
-
* the row cap logs when it bites. Losing the table loses the cold window
|
|
1566
|
-
* only: the ring keeps serving the last hour and the charts keep drawing."
|
|
1567
|
-
*/
|
|
1568
|
-
var LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
|
|
1660
|
+
//#region src/builtins/native-metrics/load-series-reader.ts
|
|
1569
1661
|
/**
|
|
1570
|
-
*
|
|
1662
|
+
* The ONE reader over both retention tiers.
|
|
1571
1663
|
*
|
|
1572
|
-
* The
|
|
1573
|
-
*
|
|
1664
|
+
* The hot window is the in-memory ring; the cold one is
|
|
1665
|
+
* `metrics:node-load-samples`. A caller never learns which tier a point came
|
|
1666
|
+
* from, and that is a hard requirement rather than a nicety: a second reader
|
|
1667
|
+
* is how two charts start disagreeing about the same node, and it is how "the
|
|
1668
|
+
* ring says X, the table says Y" becomes a question nobody can answer.
|
|
1574
1669
|
*
|
|
1575
|
-
*
|
|
1576
|
-
* | --- | --- | --- |
|
|
1577
|
-
* | 10 s | 6 h (default) | 164 160 |
|
|
1578
|
-
* | 5 s | 6 h | 328 320 |
|
|
1579
|
-
* | 5 s | 72 h | 3 939 840 — capped here |
|
|
1670
|
+
* Three things happen here, in this order, and each one is load-bearing:
|
|
1580
1671
|
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1672
|
+
* 1. **Merge and dedupe on `atMs`.** The two tiers OVERLAP by construction —
|
|
1673
|
+
* the ring holds the last 180 snapshots and the table holds every snapshot
|
|
1674
|
+
* the ring accepted, including those same 180. A snapshot present in both
|
|
1675
|
+
* must contribute exactly once. Identity is `atMs`, because one node emits
|
|
1676
|
+
* one snapshot per tick and a timestamp already held IS that snapshot. The
|
|
1677
|
+
* HOT copy wins on a tie: it is the payload as it arrived, while the cold
|
|
1678
|
+
* one has been through the storage encode.
|
|
1679
|
+
* 2. **Fold per function.** The chart draws one series per addon, so the fold
|
|
1680
|
+
* happens here rather than in the browser.
|
|
1681
|
+
* 3. **Reduce, preserving the extremes.** See `load-series-fold.ts` for why
|
|
1682
|
+
* the representative of a bucket is its MAX and never its mean.
|
|
1683
|
+
*
|
|
1684
|
+
* A GAP stays a gap through all three. Emission is fixed-cadence, so a missing
|
|
1685
|
+
* interval means one thing — nobody reported — and nothing here fills it in.
|
|
1589
1686
|
*/
|
|
1590
|
-
var DEFAULT_MAX_ROWS = 5e5;
|
|
1591
|
-
var LOAD_SERIES_COLUMNS = [
|
|
1592
|
-
(
|
|
1593
|
-
/** `INTEGER PRIMARY KEY` = SQLite rowid alias: the key IS the row's address,
|
|
1594
|
-
* so it costs no separate index and no stored string. */
|
|
1595
|
-
{
|
|
1596
|
-
name: "id",
|
|
1597
|
-
type: "INTEGER",
|
|
1598
|
-
primaryKey: true,
|
|
1599
|
-
notNull: true
|
|
1600
|
-
}),
|
|
1601
|
-
{
|
|
1602
|
-
name: "nodeId",
|
|
1603
|
-
type: "TEXT",
|
|
1604
|
-
notNull: true
|
|
1605
|
-
},
|
|
1606
|
-
(
|
|
1607
|
-
/** The EMITTING node's timestamp for the sample this row belongs to. Every
|
|
1608
|
-
* row of one sample shares it — that is what makes a sample reassemblable. */
|
|
1609
|
-
{
|
|
1610
|
-
name: "atMs",
|
|
1611
|
-
type: "INTEGER",
|
|
1612
|
-
notNull: true
|
|
1613
|
-
}),
|
|
1614
|
-
{
|
|
1615
|
-
name: "pid",
|
|
1616
|
-
type: "INTEGER",
|
|
1617
|
-
notNull: true
|
|
1618
|
-
},
|
|
1619
|
-
(
|
|
1620
|
-
/** `NULL` for a process no addon owns — `root` and `system` both. */
|
|
1621
|
-
{
|
|
1622
|
-
name: "addonId",
|
|
1623
|
-
type: "TEXT"
|
|
1624
|
-
}),
|
|
1625
|
-
{
|
|
1626
|
-
name: "classification",
|
|
1627
|
-
type: "TEXT",
|
|
1628
|
-
notNull: true
|
|
1629
|
-
},
|
|
1630
|
-
(
|
|
1631
|
-
/** `ps pcpu` x 10. A LIFETIME average, not a rate — see `NodeProcess`. */
|
|
1632
|
-
{
|
|
1633
|
-
name: "cpuDeci",
|
|
1634
|
-
type: "INTEGER",
|
|
1635
|
-
notNull: true
|
|
1636
|
-
}),
|
|
1637
|
-
{
|
|
1638
|
-
name: "rssMib",
|
|
1639
|
-
type: "INTEGER",
|
|
1640
|
-
notNull: true
|
|
1641
|
-
},
|
|
1642
|
-
(
|
|
1643
|
-
/** Instantaneous main-thread CPU% x 10. `NULL` = UNKNOWN, never zero. */
|
|
1644
|
-
{
|
|
1645
|
-
name: "cpuMainDeci",
|
|
1646
|
-
type: "INTEGER"
|
|
1647
|
-
}),
|
|
1648
|
-
(
|
|
1649
|
-
/** Instantaneous V8-helper-pool CPU% x 10. `NULL` = UNKNOWN, never zero. */
|
|
1650
|
-
{
|
|
1651
|
-
name: "cpuGcDeci",
|
|
1652
|
-
type: "INTEGER"
|
|
1653
|
-
})
|
|
1654
|
-
];
|
|
1655
|
-
var LOAD_SERIES_INDEXES = [(
|
|
1656
1687
|
/**
|
|
1657
|
-
*
|
|
1688
|
+
* Merge the two tiers into one deduped, oldest-first list of samples.
|
|
1658
1689
|
*
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1662
|
-
* the other end. A second index on `atMs` alone was measured and rejected:
|
|
1663
|
-
* it cost 23 B/row (196.0 → 172.8 with the two column drops) to serve a
|
|
1664
|
-
* cross-node prune that a per-node loop over three nodes already serves.
|
|
1690
|
+
* Exported because the dedup is the property worth testing on its own: a
|
|
1691
|
+
* doubled point is invisible in a total and obvious only as a chart that reads
|
|
1692
|
+
* twice as loud as the machine.
|
|
1665
1693
|
*/
|
|
1666
|
-
{
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
/** Tenths → percent. `null` stays `null`: unknown is not zero. */
|
|
1672
|
-
function fromDeci(value) {
|
|
1673
|
-
return value === null ? null : Math.round(value) / 10;
|
|
1694
|
+
function mergeSamples(cold, hot) {
|
|
1695
|
+
const byAt = /* @__PURE__ */ new Map();
|
|
1696
|
+
for (const sample of cold) byAt.set(sample.atMs, sample);
|
|
1697
|
+
for (const sample of hot) byAt.set(sample.atMs, sample);
|
|
1698
|
+
return [...byAt.values()].toSorted((a, b) => a.atMs - b.atMs);
|
|
1674
1699
|
}
|
|
1675
|
-
/**
|
|
1676
|
-
function
|
|
1677
|
-
return
|
|
1700
|
+
/** `RetainedProcess` is already the fold's row shape — named, not re-derived. */
|
|
1701
|
+
function toFoldRows(processes) {
|
|
1702
|
+
return processes;
|
|
1678
1703
|
}
|
|
1679
|
-
/**
|
|
1680
|
-
function
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1704
|
+
/**
|
|
1705
|
+
* Merge, fold and reduce. The whole read path in one pure function, so every
|
|
1706
|
+
* property that matters — dedup, peak survival, an empty bucket staying empty
|
|
1707
|
+
* — is testable without a database or a ring.
|
|
1708
|
+
*/
|
|
1709
|
+
function mergeLoadSeries(input) {
|
|
1710
|
+
const samples = mergeSamples(input.cold, input.hot);
|
|
1711
|
+
if (samples.length === 0) return {
|
|
1712
|
+
series: [],
|
|
1713
|
+
bucketMs: input.cadenceMs,
|
|
1714
|
+
retainedSamples: 0,
|
|
1715
|
+
oldestAtMs: null
|
|
1716
|
+
};
|
|
1717
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1718
|
+
for (const sample of samples) for (const bucket of require_dist.foldSnapshotByFunction(toFoldRows(sample.processes), sample.atMs)) {
|
|
1719
|
+
const held = byKey.get(bucket.key);
|
|
1720
|
+
if (held === void 0) byKey.set(bucket.key, {
|
|
1721
|
+
kind: bucket.kind,
|
|
1722
|
+
points: [bucket.point]
|
|
1723
|
+
});
|
|
1724
|
+
else held.points.push(bucket.point);
|
|
1725
|
+
}
|
|
1726
|
+
const oldestAtMs = samples[0]?.atMs ?? null;
|
|
1727
|
+
const newestAtMs = samples.at(-1)?.atMs ?? oldestAtMs;
|
|
1728
|
+
const spanMs = oldestAtMs === null || newestAtMs === null ? 0 : newestAtMs - oldestAtMs + input.cadenceMs;
|
|
1729
|
+
const bucketMs = input.maxPoints === void 0 ? input.cadenceMs : require_dist.resolveBucketMs(spanMs, input.cadenceMs, input.maxPoints);
|
|
1730
|
+
const origin = oldestAtMs ?? 0;
|
|
1731
|
+
const series = [...byKey.entries()].map(([key, held]) => ({
|
|
1732
|
+
key,
|
|
1733
|
+
kind: held.kind,
|
|
1734
|
+
points: bucketMs > input.cadenceMs ? require_dist.reducePoints(held.points, bucketMs, origin) : held.points
|
|
1735
|
+
}));
|
|
1736
|
+
const weight = (s) => {
|
|
1737
|
+
const last = s.points.at(-1);
|
|
1738
|
+
if (last === void 0) return -1;
|
|
1739
|
+
if (last.cpuMainPercent !== null && last.cpuGcPercent !== null) return last.cpuMainPercent + last.cpuGcPercent;
|
|
1740
|
+
return last.cpuLifetimePercent;
|
|
1689
1741
|
};
|
|
1690
|
-
}
|
|
1691
|
-
/** The shape a reader gets → the storage row. The ONE encode. */
|
|
1692
|
-
function processToRow(nodeId, atMs, process) {
|
|
1693
1742
|
return {
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
cpuMainDeci: toDeci(process.cpuMainPercent),
|
|
1702
|
-
cpuGcDeci: toDeci(process.cpuGcPercent)
|
|
1743
|
+
series: series.toSorted((a, b) => {
|
|
1744
|
+
const d = weight(b) - weight(a);
|
|
1745
|
+
return d !== 0 ? d : a.key.localeCompare(b.key);
|
|
1746
|
+
}),
|
|
1747
|
+
bucketMs,
|
|
1748
|
+
retainedSamples: samples.length,
|
|
1749
|
+
oldestAtMs
|
|
1703
1750
|
};
|
|
1704
1751
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1752
|
+
//#endregion
|
|
1753
|
+
//#region src/builtins/native-metrics/gpu-probe.ts
|
|
1754
|
+
/**
|
|
1755
|
+
* The Linux GPU probe, forking only when there is something to fork.
|
|
1756
|
+
*
|
|
1757
|
+
* `nvidia-smi` was spawned on every sample — twelve times a minute from
|
|
1758
|
+
* hub-main — on a host with an Intel i915 and no NVIDIA driver. Each spawn
|
|
1759
|
+
* was a synchronous `fork()` of the largest process on the host (D441: p50
|
|
1760
|
+
* 91 ms at 5.8 GB RSS), and each one failed at `exec` and yielded `null`.
|
|
1761
|
+
* Twelve forks a minute bought nothing (D452).
|
|
1762
|
+
*
|
|
1763
|
+
* Whether the binary exists is a question `access(2)` answers on the libuv
|
|
1764
|
+
* pool without a fork. It is asked on EVERY call rather than latched: a handful
|
|
1765
|
+
* of `stat`s every five seconds costs nothing, and a latch is a second fact
|
|
1766
|
+
* about the host that can go stale — the same shape as the caches D224
|
|
1767
|
+
* forbids.
|
|
1768
|
+
*/
|
|
1769
|
+
var nodeAccessCheck = (path) => (0, node_fs_promises.access)(path, node_fs.constants.X_OK);
|
|
1770
|
+
/**
|
|
1771
|
+
* The first `PATH` entry under which `binary` is executable, or `null`.
|
|
1772
|
+
* An empty or missing `PATH` asks nothing.
|
|
1773
|
+
*/
|
|
1774
|
+
async function findOnPath(binary, pathEnv, check = nodeAccessCheck) {
|
|
1775
|
+
const dirs = (pathEnv ?? "").split(":").filter((dir) => dir.length > 0);
|
|
1776
|
+
for (const dir of dirs) {
|
|
1777
|
+
const candidate = `${dir}/${binary}`;
|
|
1778
|
+
try {
|
|
1779
|
+
await check(candidate);
|
|
1780
|
+
return candidate;
|
|
1781
|
+
} catch {}
|
|
1712
1782
|
}
|
|
1713
|
-
return
|
|
1714
|
-
atMs,
|
|
1715
|
-
processes
|
|
1716
|
-
}));
|
|
1783
|
+
return null;
|
|
1717
1784
|
}
|
|
1785
|
+
var NVIDIA_SMI_ARGS = ["--query-gpu=utilization.gpu,name,memory.used,memory.total,temperature.gpu", "--format=csv,noheader,nounits"];
|
|
1718
1786
|
/**
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
1722
|
-
* Every method is best-effort and says so in the log rather than throwing at
|
|
1723
|
-
* the emitter: a storage hiccup must cost a gap in a chart, never a dropped
|
|
1724
|
-
* snapshot on the bus.
|
|
1787
|
+
* `nvidia-smi`'s one-line CSV as a `MetricsGpuInfo`, or `null` when the
|
|
1788
|
+
* binary is absent, fails, or prints something else. Never throws.
|
|
1725
1789
|
*/
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1790
|
+
async function readNvidiaGpu(deps) {
|
|
1791
|
+
const binary = await deps.findOnPath("nvidia-smi");
|
|
1792
|
+
if (binary === null) return null;
|
|
1793
|
+
let stdout;
|
|
1794
|
+
try {
|
|
1795
|
+
stdout = await deps.exec(binary, NVIDIA_SMI_ARGS);
|
|
1796
|
+
} catch {
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
const parts = stdout.trim().split(",").map((s) => s.trim());
|
|
1800
|
+
if (parts.length < 5) return null;
|
|
1801
|
+
return {
|
|
1802
|
+
utilization: parseInt(parts[0], 10) || 0,
|
|
1803
|
+
model: parts[1] ?? "Unknown GPU",
|
|
1804
|
+
memoryUsedBytes: (parseInt(parts[2], 10) || 0) * 1024 * 1024,
|
|
1805
|
+
memoryTotalBytes: (parseInt(parts[3], 10) || 0) * 1024 * 1024,
|
|
1806
|
+
temperature: parseInt(parts[4], 10) || null
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
//#endregion
|
|
1810
|
+
//#region src/builtins/native-metrics/native-metrics-provider.ts
|
|
1811
|
+
var IS_DARWIN = node_os.platform() === "darwin";
|
|
1812
|
+
var IS_LINUX = node_os.platform() === "linux";
|
|
1813
|
+
var NativeMetricsProvider = class {
|
|
1814
|
+
id = "native";
|
|
1815
|
+
cachedSnapshot = null;
|
|
1816
|
+
samplingTimer = null;
|
|
1817
|
+
macMemTimer = null;
|
|
1818
|
+
prevCpu;
|
|
1819
|
+
diagnostics = null;
|
|
1820
|
+
consecutiveSampleErrors = 0;
|
|
1821
|
+
constructor() {
|
|
1822
|
+
this.prevCpu = aggregateCpuTimes();
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Optional diagnostics sink for sampling errors. The first failure and
|
|
1826
|
+
* every Nth failure thereafter are reported so a permanently-broken
|
|
1827
|
+
* collector doesn't spam logs but is still observable.
|
|
1828
|
+
*/
|
|
1829
|
+
setDiagnostics(diag) {
|
|
1830
|
+
this.diagnostics = diag;
|
|
1831
|
+
}
|
|
1832
|
+
startSampling(intervalMs) {
|
|
1833
|
+
if (IS_DARWIN) {
|
|
1834
|
+
sampleMacMemory(this);
|
|
1835
|
+
this.macMemTimer = setInterval(() => sampleMacMemory(this), 5e3);
|
|
1836
|
+
if (this.macMemTimer.unref) this.macMemTimer.unref();
|
|
1837
|
+
}
|
|
1838
|
+
this.collectSnapshot().then((s) => {
|
|
1839
|
+
this.cachedSnapshot = s;
|
|
1840
|
+
this.consecutiveSampleErrors = 0;
|
|
1841
|
+
}).catch((err) => this.reportSampleError(err));
|
|
1842
|
+
this.samplingTimer = setInterval(() => {
|
|
1843
|
+
this.collectSnapshot().then((s) => {
|
|
1844
|
+
this.cachedSnapshot = s;
|
|
1845
|
+
this.consecutiveSampleErrors = 0;
|
|
1846
|
+
}).catch((err) => this.reportSampleError(err));
|
|
1847
|
+
}, intervalMs);
|
|
1848
|
+
if (this.samplingTimer.unref) this.samplingTimer.unref();
|
|
1849
|
+
}
|
|
1850
|
+
reportSampleError(err) {
|
|
1851
|
+
this.consecutiveSampleErrors++;
|
|
1852
|
+
if (!this.diagnostics) return;
|
|
1853
|
+
if (!(this.consecutiveSampleErrors === 1 || this.consecutiveSampleErrors % 60 === 0)) return;
|
|
1854
|
+
this.diagnostics.warn("metrics-provider sample failed", {
|
|
1855
|
+
consecutiveErrors: this.consecutiveSampleErrors,
|
|
1856
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
stopSampling() {
|
|
1860
|
+
if (this.samplingTimer) {
|
|
1861
|
+
clearInterval(this.samplingTimer);
|
|
1862
|
+
this.samplingTimer = null;
|
|
1863
|
+
}
|
|
1864
|
+
if (this.macMemTimer) {
|
|
1865
|
+
clearInterval(this.macMemTimer);
|
|
1866
|
+
this.macMemTimer = null;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async collectSnapshot() {
|
|
1870
|
+
const cpu = this.sampleCpu();
|
|
1871
|
+
const memory = this.getMemoryInfo();
|
|
1872
|
+
const processInfo = getProcessResources();
|
|
1873
|
+
const [gpu, network, disk, cpuTemp] = await Promise.all([
|
|
1874
|
+
getGpuInfoInternal().catch(() => null),
|
|
1875
|
+
takeNetworkIoSnapshot(),
|
|
1876
|
+
takeDiskIoSnapshot(),
|
|
1877
|
+
getCpuTemperatureInternal().catch(() => null)
|
|
1878
|
+
]);
|
|
1879
|
+
const snapshot = {
|
|
1880
|
+
cpu,
|
|
1881
|
+
memory,
|
|
1882
|
+
gpu,
|
|
1883
|
+
network,
|
|
1884
|
+
disk,
|
|
1885
|
+
pressure: {
|
|
1886
|
+
cpu: getPressure("cpu"),
|
|
1887
|
+
memory: getPressure("memory"),
|
|
1888
|
+
io: getPressure("io")
|
|
1889
|
+
},
|
|
1890
|
+
process: processInfo,
|
|
1891
|
+
cpuTemperature: cpuTemp,
|
|
1892
|
+
timestampMs: Date.now()
|
|
1893
|
+
};
|
|
1894
|
+
this.cachedSnapshot = snapshot;
|
|
1895
|
+
return snapshot;
|
|
1896
|
+
}
|
|
1897
|
+
async getCached() {
|
|
1898
|
+
return this.cachedSnapshot;
|
|
1899
|
+
}
|
|
1900
|
+
async getCurrent() {
|
|
1901
|
+
const s = this.cachedSnapshot ?? await this.collectSnapshot();
|
|
1902
|
+
const mb = (bytes) => Math.round(bytes / 1024 / 1024);
|
|
1903
|
+
return {
|
|
1904
|
+
cpuPercent: s.cpu.total,
|
|
1905
|
+
memoryPercent: s.memory.percent,
|
|
1906
|
+
memoryUsedMB: mb(s.memory.usedBytes),
|
|
1907
|
+
memoryTotalMB: mb(s.memory.totalBytes),
|
|
1908
|
+
temperature: s.cpuTemperature ?? void 0,
|
|
1909
|
+
gpuPercent: s.gpu?.utilization,
|
|
1910
|
+
gpuMemoryPercent: s.gpu && s.gpu.memoryTotalBytes > 0 ? Math.round(s.gpu.memoryUsedBytes / s.gpu.memoryTotalBytes * 1e3) / 10 : void 0
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
async getDiskSpace(input) {
|
|
1914
|
+
return getDiskSpaceInternal(input.dirPath);
|
|
1915
|
+
}
|
|
1916
|
+
async getGpuInfo() {
|
|
1917
|
+
return getGpuInfoInternal();
|
|
1918
|
+
}
|
|
1919
|
+
async getCpuTemperature() {
|
|
1920
|
+
return getCpuTemperatureInternal();
|
|
1921
|
+
}
|
|
1922
|
+
async getProcessStats(input) {
|
|
1923
|
+
const { getPidStats } = await Promise.resolve().then(() => require("../../resource-monitor-LVE1BoLs.js")).then((n) => n.resource_monitor_exports);
|
|
1924
|
+
const raw = await getPidStats(input.pids);
|
|
1925
|
+
const result = [];
|
|
1926
|
+
for (const [pid, s] of raw) result.push({
|
|
1927
|
+
pid,
|
|
1928
|
+
cpu: s.cpu,
|
|
1929
|
+
memory: s.memory,
|
|
1930
|
+
privateBytes: s.privateBytes,
|
|
1931
|
+
sharedBytes: s.sharedBytes
|
|
1932
|
+
});
|
|
1933
|
+
return result;
|
|
1934
|
+
}
|
|
1935
|
+
sampleCpu() {
|
|
1936
|
+
const curr = aggregateCpuTimes();
|
|
1937
|
+
const prev = this.prevCpu;
|
|
1938
|
+
this.prevCpu = curr;
|
|
1939
|
+
const dUser = curr.user - prev.user;
|
|
1940
|
+
const dNice = curr.nice - prev.nice;
|
|
1941
|
+
const dSys = curr.sys - prev.sys;
|
|
1942
|
+
const dIrq = curr.irq - prev.irq;
|
|
1943
|
+
const dIdle = curr.idle - prev.idle;
|
|
1944
|
+
const dTotal = dUser + dNice + dSys + dIrq + dIdle;
|
|
1945
|
+
const pct = (v) => dTotal === 0 ? 0 : Math.round(v / dTotal * 1e3) / 10;
|
|
1946
|
+
return {
|
|
1947
|
+
total: pct(dUser + dNice + dSys + dIrq),
|
|
1948
|
+
user: pct(dUser),
|
|
1949
|
+
system: pct(dSys),
|
|
1950
|
+
irq: pct(dIrq),
|
|
1951
|
+
nice: pct(dNice),
|
|
1952
|
+
loadAvg: node_os.loadavg(),
|
|
1953
|
+
cores: node_os.cpus().length
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
/** Cached macOS memory (set by sampleMacMemory timer). */
|
|
1957
|
+
_cachedMacMem = null;
|
|
1958
|
+
getMemoryInfo() {
|
|
1959
|
+
const total = node_os.totalmem();
|
|
1960
|
+
if (total === 0) return {
|
|
1961
|
+
percent: 0,
|
|
1962
|
+
totalBytes: 0,
|
|
1963
|
+
usedBytes: 0,
|
|
1964
|
+
availableBytes: 0,
|
|
1965
|
+
swapUsedBytes: 0,
|
|
1966
|
+
swapTotalBytes: 0
|
|
1967
|
+
};
|
|
1968
|
+
if (IS_DARWIN && this._cachedMacMem !== null) return this._cachedMacMem;
|
|
1969
|
+
const free = node_os.freemem();
|
|
1970
|
+
const used = total - free;
|
|
1971
|
+
let swapUsedBytes = 0;
|
|
1972
|
+
let swapTotalBytes = 0;
|
|
1973
|
+
if (IS_LINUX) try {
|
|
1974
|
+
const meminfo = node_fs.readFileSync("/proc/meminfo", "utf-8");
|
|
1975
|
+
const swapTotal = parseInt(meminfo.match(/SwapTotal:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
|
|
1976
|
+
const swapFree = parseInt(meminfo.match(/SwapFree:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
|
|
1977
|
+
swapTotalBytes = swapTotal;
|
|
1978
|
+
swapUsedBytes = swapTotal - swapFree;
|
|
1979
|
+
} catch {}
|
|
1980
|
+
return {
|
|
1981
|
+
percent: Math.round((total - free) / total * 1e3) / 10,
|
|
1982
|
+
totalBytes: total,
|
|
1983
|
+
usedBytes: used,
|
|
1984
|
+
availableBytes: free,
|
|
1985
|
+
swapUsedBytes,
|
|
1986
|
+
swapTotalBytes
|
|
1987
|
+
};
|
|
1740
1988
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
}))
|
|
1752
|
-
});
|
|
1753
|
-
this.declared = true;
|
|
1754
|
-
return true;
|
|
1755
|
-
} catch (err) {
|
|
1756
|
-
this.logger.warn("load series declareCollection failed — nothing will be retained on disk", { meta: {
|
|
1757
|
-
collection: LOAD_SERIES_COLLECTION,
|
|
1758
|
-
error: require_dist.errMsg(err)
|
|
1759
|
-
} });
|
|
1760
|
-
return false;
|
|
1761
|
-
}
|
|
1989
|
+
};
|
|
1990
|
+
function aggregateCpuTimes() {
|
|
1991
|
+
const cpus = node_os.cpus();
|
|
1992
|
+
let user = 0, nice = 0, sys = 0, irq = 0, idle = 0;
|
|
1993
|
+
for (const cpu of cpus) {
|
|
1994
|
+
user += cpu.times.user;
|
|
1995
|
+
nice += cpu.times.nice;
|
|
1996
|
+
sys += cpu.times.sys;
|
|
1997
|
+
irq += cpu.times.irq;
|
|
1998
|
+
idle += cpu.times.idle;
|
|
1762
1999
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2000
|
+
return {
|
|
2001
|
+
user,
|
|
2002
|
+
nice,
|
|
2003
|
+
sys,
|
|
2004
|
+
irq,
|
|
2005
|
+
idle
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
function sampleMacMemory(provider) {
|
|
2009
|
+
try {
|
|
2010
|
+
const output = (0, node_child_process.execFileSync)("vm_stat", {
|
|
2011
|
+
timeout: 2e3,
|
|
2012
|
+
encoding: "utf-8"
|
|
2013
|
+
});
|
|
2014
|
+
const pageSize = 16384;
|
|
2015
|
+
const parse = (label) => {
|
|
2016
|
+
const match = output.match(new RegExp(`${label}:\\s+(\\d+)`));
|
|
2017
|
+
return match ? parseInt(match[1], 10) * pageSize : 0;
|
|
2018
|
+
};
|
|
2019
|
+
const free = parse("Pages free");
|
|
2020
|
+
const inactive = parse("Pages inactive");
|
|
2021
|
+
const purgeable = parse("Pages purgeable");
|
|
2022
|
+
const speculative = parse("Pages speculative");
|
|
2023
|
+
const available = free + inactive + purgeable + speculative;
|
|
2024
|
+
const total = node_os.totalmem();
|
|
2025
|
+
const used = total - available;
|
|
2026
|
+
let swapUsedBytes = 0;
|
|
2027
|
+
let swapTotalBytes = 0;
|
|
1775
2028
|
try {
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
|
|
2029
|
+
const swapOut = (0, node_child_process.execFileSync)("sysctl", ["-n", "vm.swapusage"], {
|
|
2030
|
+
timeout: 2e3,
|
|
2031
|
+
encoding: "utf-8"
|
|
1779
2032
|
});
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
2033
|
+
const totalMatch = swapOut.match(/total\s*=\s*([\d.]+)M/);
|
|
2034
|
+
const usedMatch = swapOut.match(/used\s*=\s*([\d.]+)M/);
|
|
2035
|
+
if (totalMatch) swapTotalBytes = parseFloat(totalMatch[1]) * 1024 * 1024;
|
|
2036
|
+
if (usedMatch) swapUsedBytes = parseFloat(usedMatch[1]) * 1024 * 1024;
|
|
2037
|
+
} catch {}
|
|
2038
|
+
provider._cachedMacMem = {
|
|
2039
|
+
percent: Math.round(used / total * 1e3) / 10,
|
|
2040
|
+
totalBytes: total,
|
|
2041
|
+
usedBytes: used,
|
|
2042
|
+
availableBytes: available,
|
|
2043
|
+
swapUsedBytes,
|
|
2044
|
+
swapTotalBytes
|
|
2045
|
+
};
|
|
2046
|
+
} catch {
|
|
2047
|
+
provider._cachedMacMem = null;
|
|
1790
2048
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
|
|
1806
|
-
orderBy: {
|
|
1807
|
-
field: "atMs",
|
|
1808
|
-
direction: "asc"
|
|
1809
|
-
},
|
|
1810
|
-
limit: limitRows
|
|
1811
|
-
}
|
|
2049
|
+
}
|
|
2050
|
+
function getDiskSpaceInternal(dirPath) {
|
|
2051
|
+
return new Promise((resolve, reject) => {
|
|
2052
|
+
node_fs.statfs(dirPath, (err, stats) => {
|
|
2053
|
+
if (err) return reject(err);
|
|
2054
|
+
const totalBytes = stats.blocks * stats.bsize;
|
|
2055
|
+
const availableBytes = stats.bavail * stats.bsize;
|
|
2056
|
+
const usedBytes = totalBytes - stats.bfree * stats.bsize;
|
|
2057
|
+
resolve({
|
|
2058
|
+
path: dirPath,
|
|
2059
|
+
totalBytes,
|
|
2060
|
+
usedBytes,
|
|
2061
|
+
availableBytes,
|
|
2062
|
+
percent: totalBytes > 0 ? Math.round(usedBytes / totalBytes * 1e3) / 10 : 0
|
|
1812
2063
|
});
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2064
|
+
});
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
async function takeDiskIoSnapshot() {
|
|
2068
|
+
const now = Date.now();
|
|
2069
|
+
if (IS_LINUX) try {
|
|
2070
|
+
const data = node_fs.readFileSync("/proc/diskstats", "utf-8");
|
|
2071
|
+
let readOps = 0, readBytes = 0, writeOps = 0, writeBytes = 0;
|
|
2072
|
+
for (const line of data.split("\n")) {
|
|
2073
|
+
const parts = line.trim().split(/\s+/);
|
|
2074
|
+
if (parts.length < 14) continue;
|
|
2075
|
+
const devName = parts[2];
|
|
2076
|
+
if (/\d+$/.test(devName) && !/^nvme\d+n\d+$/.test(devName)) continue;
|
|
2077
|
+
readOps += parseInt(parts[3], 10) || 0;
|
|
2078
|
+
readBytes += (parseInt(parts[5], 10) || 0) * 512;
|
|
2079
|
+
writeOps += parseInt(parts[7], 10) || 0;
|
|
2080
|
+
writeBytes += (parseInt(parts[9], 10) || 0) * 512;
|
|
1826
2081
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2082
|
+
return {
|
|
2083
|
+
readBytes,
|
|
2084
|
+
writeBytes,
|
|
2085
|
+
readOps,
|
|
2086
|
+
writeOps,
|
|
2087
|
+
timestampMs: now
|
|
2088
|
+
};
|
|
2089
|
+
} catch {}
|
|
2090
|
+
return {
|
|
2091
|
+
readBytes: 0,
|
|
2092
|
+
writeBytes: 0,
|
|
2093
|
+
readOps: 0,
|
|
2094
|
+
writeOps: 0,
|
|
2095
|
+
timestampMs: now
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
async function takeNetworkIoSnapshot() {
|
|
2099
|
+
const now = Date.now();
|
|
2100
|
+
if (IS_LINUX) try {
|
|
2101
|
+
const data = node_fs.readFileSync("/proc/net/dev", "utf-8");
|
|
2102
|
+
let rxBytes = 0, txBytes = 0, rxPackets = 0, txPackets = 0, rxErrors = 0, txErrors = 0;
|
|
2103
|
+
for (const line of data.split("\n")) {
|
|
2104
|
+
const match = line.match(/^\s*(\w+):\s+(.+)$/);
|
|
2105
|
+
if (!match) continue;
|
|
2106
|
+
if (match[1] === "lo") continue;
|
|
2107
|
+
const cols = match[2].trim().split(/\s+/);
|
|
2108
|
+
rxBytes += parseInt(cols[0], 10) || 0;
|
|
2109
|
+
rxPackets += parseInt(cols[1], 10) || 0;
|
|
2110
|
+
rxErrors += parseInt(cols[2], 10) || 0;
|
|
2111
|
+
txBytes += parseInt(cols[8], 10) || 0;
|
|
2112
|
+
txPackets += parseInt(cols[9], 10) || 0;
|
|
2113
|
+
txErrors += parseInt(cols[10], 10) || 0;
|
|
1847
2114
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
2115
|
+
return {
|
|
2116
|
+
rxBytes,
|
|
2117
|
+
txBytes,
|
|
2118
|
+
rxPackets,
|
|
2119
|
+
txPackets,
|
|
2120
|
+
rxErrors,
|
|
2121
|
+
txErrors,
|
|
2122
|
+
timestampMs: now
|
|
2123
|
+
};
|
|
2124
|
+
} catch {}
|
|
2125
|
+
if (IS_DARWIN) try {
|
|
2126
|
+
const stdout = await execAsync("netstat", ["-ib"]);
|
|
2127
|
+
let rxBytes = 0, txBytes = 0, rxPackets = 0, txPackets = 0, rxErrors = 0, txErrors = 0;
|
|
2128
|
+
for (const line of stdout.split("\n")) {
|
|
2129
|
+
const parts = line.trim().split(/\s+/);
|
|
2130
|
+
if (parts.length < 11) continue;
|
|
2131
|
+
if (parts[0] === "lo0" || parts[0] === "Name") continue;
|
|
2132
|
+
if (!parts[2]?.startsWith("Link#")) continue;
|
|
2133
|
+
rxPackets += parseInt(parts[4], 10) || 0;
|
|
2134
|
+
rxErrors += parseInt(parts[5], 10) || 0;
|
|
2135
|
+
rxBytes += parseInt(parts[6], 10) || 0;
|
|
2136
|
+
txPackets += parseInt(parts[7], 10) || 0;
|
|
2137
|
+
txErrors += parseInt(parts[8], 10) || 0;
|
|
2138
|
+
txBytes += parseInt(parts[9], 10) || 0;
|
|
1863
2139
|
}
|
|
1864
2140
|
return {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2141
|
+
rxBytes,
|
|
2142
|
+
txBytes,
|
|
2143
|
+
rxPackets,
|
|
2144
|
+
txPackets,
|
|
2145
|
+
rxErrors,
|
|
2146
|
+
txErrors,
|
|
2147
|
+
timestampMs: now
|
|
1869
2148
|
};
|
|
2149
|
+
} catch {}
|
|
2150
|
+
return {
|
|
2151
|
+
rxBytes: 0,
|
|
2152
|
+
txBytes: 0,
|
|
2153
|
+
rxPackets: 0,
|
|
2154
|
+
txPackets: 0,
|
|
2155
|
+
rxErrors: 0,
|
|
2156
|
+
txErrors: 0,
|
|
2157
|
+
timestampMs: now
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
async function getCpuTemperatureInternal() {
|
|
2161
|
+
if (IS_LINUX) for (const p of ["/sys/class/thermal/thermal_zone0/temp", "/sys/class/hwmon/hwmon0/temp1_input"]) try {
|
|
2162
|
+
const raw = node_fs.readFileSync(p, "utf-8").trim();
|
|
2163
|
+
const millideg = parseInt(raw, 10);
|
|
2164
|
+
if (!isNaN(millideg)) return Math.round(millideg / 100) / 10;
|
|
2165
|
+
} catch {
|
|
2166
|
+
continue;
|
|
1870
2167
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2168
|
+
if (IS_DARWIN) try {
|
|
2169
|
+
const match = (await execAsync("sudo", [
|
|
2170
|
+
"-n",
|
|
2171
|
+
"powermetrics",
|
|
2172
|
+
"--samplers",
|
|
2173
|
+
"smc",
|
|
2174
|
+
"-i",
|
|
2175
|
+
"1",
|
|
2176
|
+
"-n",
|
|
2177
|
+
"1"
|
|
2178
|
+
], 3e3)).match(/CPU die temperature:\s+([\d.]+)\s*C/);
|
|
2179
|
+
if (match) return Math.round(parseFloat(match[1]) * 10) / 10;
|
|
2180
|
+
} catch {}
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
async function getGpuInfoInternal() {
|
|
2184
|
+
if (IS_DARWIN) try {
|
|
2185
|
+
const utilMatch = (await execAsync("ioreg", [
|
|
2186
|
+
"-r",
|
|
2187
|
+
"-d",
|
|
2188
|
+
"1",
|
|
2189
|
+
"-c",
|
|
2190
|
+
"IOAccelerator"
|
|
2191
|
+
])).match(/"GPU Core Utilization"\s*=\s*(\d+)/);
|
|
2192
|
+
const util = utilMatch ? parseInt(utilMatch[1], 10) : 0;
|
|
2193
|
+
const utilPct = util > 1e3 ? Math.round(util / 1e7) : util;
|
|
2194
|
+
return {
|
|
2195
|
+
utilization: Math.min(100, utilPct),
|
|
2196
|
+
model: "Apple Silicon GPU",
|
|
2197
|
+
memoryUsedBytes: 0,
|
|
2198
|
+
memoryTotalBytes: 0,
|
|
2199
|
+
temperature: null
|
|
2200
|
+
};
|
|
2201
|
+
} catch {}
|
|
2202
|
+
if (IS_LINUX) return readNvidiaGpu({
|
|
2203
|
+
findOnPath: (binary) => findOnPath(binary, process.env["PATH"]),
|
|
2204
|
+
exec: (command, args) => execAsync(command, [...args])
|
|
2205
|
+
});
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
function getProcessResources() {
|
|
2209
|
+
let openFds = 0;
|
|
2210
|
+
let threadCount = 0;
|
|
2211
|
+
if (IS_LINUX) {
|
|
1874
2212
|
try {
|
|
1875
|
-
|
|
1876
|
-
} catch
|
|
1877
|
-
this.logger.warn("load series count failed — the row cap is not enforced this pass", { meta: { error: require_dist.errMsg(err) } });
|
|
1878
|
-
return null;
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
/**
|
|
1882
|
-
* Delete this node's rows older than `cutoff`, at most one page's worth.
|
|
1883
|
-
*
|
|
1884
|
-
* The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
|
|
1885
|
-
* own is one statement but an UNBOUNDED one — a first pass after a retention
|
|
1886
|
-
* change would delete millions of rows inside a single stalling transaction.
|
|
1887
|
-
* So the page is read first (keys only, ordered by the index), its last
|
|
1888
|
-
* `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
|
|
1889
|
-
*/
|
|
1890
|
-
async pruneNodeToCutoff(nodeId, cutoff) {
|
|
2213
|
+
openFds = node_fs.readdirSync(`/proc/${process.pid}/fd`).length;
|
|
2214
|
+
} catch {}
|
|
1891
2215
|
try {
|
|
1892
|
-
const
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
const
|
|
1910
|
-
if (!
|
|
1911
|
-
deleted: 0,
|
|
1912
|
-
examined: page.length
|
|
1913
|
-
};
|
|
1914
|
-
const { deleted } = await this.store.deleteWhere.mutate({
|
|
1915
|
-
collection: LOAD_SERIES_COLLECTION,
|
|
1916
|
-
filter: {
|
|
1917
|
-
where: { nodeId },
|
|
1918
|
-
whereBetween: { atMs: [0, effectiveCutoff] }
|
|
1919
|
-
}
|
|
1920
|
-
});
|
|
1921
|
-
return {
|
|
1922
|
-
deleted,
|
|
1923
|
-
examined: page.length
|
|
1924
|
-
};
|
|
1925
|
-
} catch (err) {
|
|
1926
|
-
this.logger.warn("load series age prune failed — the table keeps growing this pass", { meta: {
|
|
1927
|
-
nodeId,
|
|
1928
|
-
cutoff,
|
|
1929
|
-
error: require_dist.errMsg(err)
|
|
1930
|
-
} });
|
|
2216
|
+
const match = node_fs.readFileSync(`/proc/${process.pid}/status`, "utf-8").match(/Threads:\s+(\d+)/);
|
|
2217
|
+
if (match) threadCount = parseInt(match[1], 10);
|
|
2218
|
+
} catch {}
|
|
2219
|
+
}
|
|
2220
|
+
const nodeProcess = process;
|
|
2221
|
+
return {
|
|
2222
|
+
openFds,
|
|
2223
|
+
threadCount,
|
|
2224
|
+
activeHandles: (nodeProcess._getActiveHandles?.() ?? []).length,
|
|
2225
|
+
activeRequests: (nodeProcess._getActiveRequests?.() ?? []).length
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function getPressure(resource) {
|
|
2229
|
+
if (!IS_LINUX) return null;
|
|
2230
|
+
try {
|
|
2231
|
+
const data = node_fs.readFileSync(`/proc/pressure/${resource}`, "utf-8");
|
|
2232
|
+
const parseLine = (prefix) => {
|
|
2233
|
+
const match = data.match(new RegExp(`${prefix} avg10=(\\d+\\.\\d+) avg60=(\\d+\\.\\d+) avg300=(\\d+\\.\\d+)`));
|
|
2234
|
+
if (!match) return null;
|
|
1931
2235
|
return {
|
|
1932
|
-
|
|
1933
|
-
|
|
2236
|
+
avg10: parseFloat(match[1]),
|
|
2237
|
+
avg60: parseFloat(match[2]),
|
|
2238
|
+
avg300: parseFloat(match[3])
|
|
1934
2239
|
};
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
* Drop the oldest rows across the known nodes until `excess` is covered.
|
|
1939
|
-
*
|
|
1940
|
-
* Same bounded-page technique, walked per node so the one index serves it.
|
|
1941
|
-
* The node holding the oldest rows pays first, which is also the node
|
|
1942
|
-
* producing the pressure when a runaway process count is the cause.
|
|
1943
|
-
*/
|
|
1944
|
-
async pruneOldestRows(nodeIds, excess) {
|
|
1945
|
-
let remaining = Math.min(excess, this.prunePageRows);
|
|
1946
|
-
let deleted = 0;
|
|
1947
|
-
let examined = 0;
|
|
1948
|
-
for (const nodeId of nodeIds) {
|
|
1949
|
-
if (remaining <= 0) break;
|
|
1950
|
-
try {
|
|
1951
|
-
const page = await this.store.query.query({
|
|
1952
|
-
collection: LOAD_SERIES_COLLECTION,
|
|
1953
|
-
filter: {
|
|
1954
|
-
where: { nodeId },
|
|
1955
|
-
orderBy: {
|
|
1956
|
-
field: "atMs",
|
|
1957
|
-
direction: "asc"
|
|
1958
|
-
},
|
|
1959
|
-
limit: remaining
|
|
1960
|
-
},
|
|
1961
|
-
columns: ["atMs"]
|
|
1962
|
-
});
|
|
1963
|
-
examined += page.length;
|
|
1964
|
-
if (page.length === 0) continue;
|
|
1965
|
-
const cutoff = Number(page.at(-1)?.data["atMs"]);
|
|
1966
|
-
if (!Number.isFinite(cutoff)) continue;
|
|
1967
|
-
const result = await this.store.deleteWhere.mutate({
|
|
1968
|
-
collection: LOAD_SERIES_COLLECTION,
|
|
1969
|
-
filter: {
|
|
1970
|
-
where: { nodeId },
|
|
1971
|
-
whereBetween: { atMs: [0, cutoff] }
|
|
1972
|
-
}
|
|
1973
|
-
});
|
|
1974
|
-
deleted += result.deleted;
|
|
1975
|
-
remaining -= result.deleted;
|
|
1976
|
-
} catch (err) {
|
|
1977
|
-
this.logger.warn("load series cap prune failed — the cap is not enforced this pass", { meta: {
|
|
1978
|
-
nodeId,
|
|
1979
|
-
error: require_dist.errMsg(err)
|
|
1980
|
-
} });
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
2240
|
+
};
|
|
2241
|
+
const some = parseLine("some");
|
|
2242
|
+
if (!some) return null;
|
|
1983
2243
|
return {
|
|
1984
|
-
|
|
1985
|
-
|
|
2244
|
+
some,
|
|
2245
|
+
full: parseLine("full")
|
|
1986
2246
|
};
|
|
2247
|
+
} catch {
|
|
2248
|
+
return null;
|
|
1987
2249
|
}
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2250
|
+
}
|
|
2251
|
+
function execAsync(cmd, args, timeoutMs = 5e3) {
|
|
2252
|
+
return new Promise((resolve, reject) => {
|
|
2253
|
+
(0, node_child_process.execFile)(cmd, args, { timeout: timeoutMs }, (err, stdout) => {
|
|
2254
|
+
if (err) return reject(err);
|
|
2255
|
+
resolve(stdout);
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Parse one `/proc/<pid>/stat` line. `null` for a line that does not parse —
|
|
2261
|
+
* a process that exited between the readdir and the read leaves a truncated
|
|
2262
|
+
* or empty file, and that is normal, not an error.
|
|
2263
|
+
*/
|
|
2264
|
+
function parseProcStat(line) {
|
|
2265
|
+
const close = line.lastIndexOf(")");
|
|
2266
|
+
const open = line.indexOf("(");
|
|
2267
|
+
if (close < 0 || open < 0 || close < open) return null;
|
|
2268
|
+
const pid = Number(line.slice(0, open).trim());
|
|
2269
|
+
const comm = line.slice(open + 1, close);
|
|
2270
|
+
const rest = line.slice(close + 1).trim().split(/\s+/);
|
|
2271
|
+
const state = rest[0];
|
|
2272
|
+
const ppid = Number(rest[1]);
|
|
2273
|
+
const pgid = Number(rest[2]);
|
|
2274
|
+
const utime = Number(rest[11]);
|
|
2275
|
+
const stime = Number(rest[12]);
|
|
2276
|
+
const startTicks = Number(rest[19]);
|
|
2277
|
+
const rssPages = Number(rest[21]);
|
|
2278
|
+
if (state === void 0 || state === "") return null;
|
|
1998
2279
|
if (![
|
|
1999
|
-
atMs,
|
|
2000
2280
|
pid,
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
return Number.isFinite(n) ? n : null;
|
|
2009
|
-
};
|
|
2281
|
+
ppid,
|
|
2282
|
+
pgid,
|
|
2283
|
+
utime,
|
|
2284
|
+
stime,
|
|
2285
|
+
startTicks,
|
|
2286
|
+
rssPages
|
|
2287
|
+
].every(Number.isFinite)) return null;
|
|
2010
2288
|
return {
|
|
2011
|
-
nodeId,
|
|
2012
|
-
atMs,
|
|
2013
2289
|
pid,
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2290
|
+
comm,
|
|
2291
|
+
state,
|
|
2292
|
+
ppid,
|
|
2293
|
+
pgid,
|
|
2294
|
+
ticks: utime + stime,
|
|
2295
|
+
startTicks,
|
|
2296
|
+
rssPages
|
|
2020
2297
|
};
|
|
2021
2298
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2299
|
+
function statusKb(status, key) {
|
|
2300
|
+
const match = status.match(new RegExp(`^${key}:\\s+(\\d+)`, "m"));
|
|
2301
|
+
return match ? parseInt(match[1], 10) * 1024 : void 0;
|
|
2302
|
+
}
|
|
2303
|
+
/** Read `VmRSS` / `RssAnon` / `RssFile` (kB in the file) out of `/proc/<pid>/status`. */
|
|
2304
|
+
function parseProcStatusRss(status) {
|
|
2305
|
+
return {
|
|
2306
|
+
rssBytes: statusKb(status, "VmRSS"),
|
|
2307
|
+
privateBytes: statusKb(status, "RssAnon"),
|
|
2308
|
+
sharedBytes: statusKb(status, "RssFile")
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
/** Seconds the process has been alive — `ps etime`, whole seconds. */
|
|
2312
|
+
function elapsedSecSince(input) {
|
|
2313
|
+
return Math.max(0, Math.floor(input.uptimeSec - input.startTicks / 100));
|
|
2314
|
+
}
|
|
2024
2315
|
/**
|
|
2025
|
-
*
|
|
2026
|
-
* honest.
|
|
2027
|
-
*
|
|
2028
|
-
* ## Where they live, and why there
|
|
2029
|
-
*
|
|
2030
|
-
* In `native-metrics`'s OWN global settings document — the one that already
|
|
2031
|
-
* exists and already carries `samplingIntervalMs`. Fields on a document, never
|
|
2032
|
-
* a method per knob: `system.getLoggingSettings` / `setLoggingSettings` set
|
|
2033
|
-
* that precedent and it holds here.
|
|
2034
|
-
*
|
|
2035
|
-
* They are NOT in the logging document, and that is a decision rather than an
|
|
2036
|
-
* omission. That document is about levels and diagnostic windows; a metrics
|
|
2037
|
-
* cadence and a table's retention are neither. Two documents that both claim a
|
|
2038
|
-
* knob is how this repo has already shipped a switch nobody read.
|
|
2039
|
-
*
|
|
2040
|
-
* They are cluster-wide, not per-node. Every node emits on the same cadence or
|
|
2041
|
-
* the fleet's series cannot be laid over each other, and the table is single
|
|
2042
|
-
* and hub-resident, so a per-node retention would be a promise nothing could
|
|
2043
|
-
* keep.
|
|
2044
|
-
*
|
|
2045
|
-
* ## Refused, not clamped
|
|
2316
|
+
* `ps pcpu`: CPU time over elapsed time, as a percentage with one decimal.
|
|
2046
2317
|
*
|
|
2047
|
-
*
|
|
2048
|
-
* the
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2318
|
+
* This is deliberately the LIFETIME average `ps` prints, not an instantaneous
|
|
2319
|
+
* rate: the columns fed from it have always shown that quantity, and the
|
|
2320
|
+
* instantaneous split is a different number with a different owner
|
|
2321
|
+
* (`cpuMainPercent` / `cpuGcPercent` from `thread-cpu-sampler.ts`). A process
|
|
2322
|
+
* with no elapsed window yet reports 0, never a division by zero.
|
|
2051
2323
|
*/
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2324
|
+
function lifetimeCpuPercent(input) {
|
|
2325
|
+
const elapsedSec = input.uptimeSec - input.startTicks / 100;
|
|
2326
|
+
if (!(elapsedSec > 0)) return 0;
|
|
2327
|
+
const cpuSec = input.ticks / 100;
|
|
2328
|
+
return Math.round(cpuSec / elapsedSec * 1e3) / 10;
|
|
2329
|
+
}
|
|
2330
|
+
var PID_RE = /^\d+$/;
|
|
2331
|
+
var nodeProcReader = {
|
|
2332
|
+
listPids: async () => (await (0, node_fs_promises.readdir)("/proc")).filter((entry) => PID_RE.test(entry)).map((entry) => Number(entry)),
|
|
2333
|
+
readStat: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/stat`, "latin1"),
|
|
2334
|
+
readStatus: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/status`, "latin1"),
|
|
2335
|
+
readCmdline: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/cmdline`, "latin1"),
|
|
2336
|
+
readUptimeSec: async () => {
|
|
2337
|
+
const [first] = (await (0, node_fs_promises.readFile)("/proc/uptime", "latin1")).trim().split(/\s+/);
|
|
2338
|
+
const seconds = Number(first);
|
|
2339
|
+
if (!Number.isFinite(seconds)) throw new Error(`/proc/uptime unreadable: ${String(first)}`);
|
|
2340
|
+
return seconds;
|
|
2057
2341
|
}
|
|
2058
2342
|
};
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
return
|
|
2343
|
+
/** `ps command=`: argv joined by spaces, or `[comm]` when there is no argv. */
|
|
2344
|
+
function commandFromCmdline(cmdline, comm) {
|
|
2345
|
+
const joined = cmdline.split("\0").filter((part) => part.length > 0).join(" ");
|
|
2346
|
+
return joined.length > 0 ? joined : `[${comm}]`;
|
|
2063
2347
|
}
|
|
2348
|
+
//#endregion
|
|
2349
|
+
//#region src/builtins/native-metrics/proc-process-table.ts
|
|
2064
2350
|
/**
|
|
2065
|
-
*
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2068
|
-
*
|
|
2351
|
+
* The node's process table — every row `ps -eo pid,ppid,pgid,pcpu,rss,etime,
|
|
2352
|
+
* command` printed — read from `/proc` on Linux, with no fork.
|
|
2353
|
+
*
|
|
2354
|
+
* Why the fork mattered, and the numbers, are in `process/proc-stat.ts`. What
|
|
2355
|
+
* this file adds is the WHOLE-TABLE read: one `readdir` of `/proc`, then per
|
|
2356
|
+
* pid `stat`, `status` and `cmdline`, all through `fs/promises` so the reads
|
|
2357
|
+
* land on the libuv pool and the main thread pays only the parse. Measured in
|
|
2358
|
+
* the hub container (70 processes, D452): 2–4 ms wall for the walk, against
|
|
2359
|
+
* 3–4 ms for `ps` itself — the fork that USED to precede it was the cost.
|
|
2360
|
+
*
|
|
2361
|
+
* A pid that vanishes between the listing and its reads is omitted whole.
|
|
2362
|
+
* Never a partial row: a row with a command and no memory would render as a
|
|
2363
|
+
* process using nothing, which is a different claim from "gone".
|
|
2069
2364
|
*/
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2365
|
+
var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
2366
|
+
async function readProcRow(pid, uptimeSec, reader) {
|
|
2367
|
+
let stat;
|
|
2368
|
+
let cmdline;
|
|
2369
|
+
let status;
|
|
2370
|
+
try {
|
|
2371
|
+
[stat, cmdline, status] = await Promise.all([
|
|
2372
|
+
reader.readStat(pid),
|
|
2373
|
+
reader.readCmdline(pid),
|
|
2374
|
+
reader.readStatus(pid)
|
|
2375
|
+
]);
|
|
2376
|
+
} catch {
|
|
2377
|
+
return null;
|
|
2080
2378
|
}
|
|
2379
|
+
const parsed = parseProcStat(stat);
|
|
2380
|
+
if (parsed === null) return null;
|
|
2381
|
+
const rss = parseProcStatusRss(status);
|
|
2081
2382
|
return {
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
function projectLoadSeriesCost(input) {
|
|
2097
|
-
const samplesPerNodeWindow = input.config.retentionHours * 3600 / input.config.cadenceSec;
|
|
2098
|
-
const intendedRows = Math.round(samplesPerNodeWindow * Math.max(input.observedProcessRows, 0));
|
|
2099
|
-
const boundedRows = Math.min(intendedRows, input.config.maxRows);
|
|
2100
|
-
return {
|
|
2101
|
-
intendedRows,
|
|
2102
|
-
boundedRows,
|
|
2103
|
-
estimatedBytes: boundedRows * 87,
|
|
2104
|
-
capBites: intendedRows > input.config.maxRows
|
|
2383
|
+
pid: parsed.pid,
|
|
2384
|
+
ppid: parsed.ppid,
|
|
2385
|
+
pgid: parsed.pgid,
|
|
2386
|
+
cpuPercent: lifetimeCpuPercent({
|
|
2387
|
+
ticks: parsed.ticks,
|
|
2388
|
+
startTicks: parsed.startTicks,
|
|
2389
|
+
uptimeSec
|
|
2390
|
+
}),
|
|
2391
|
+
memoryRssBytes: rss.rssBytes ?? 0,
|
|
2392
|
+
uptimeSec: elapsedSecSince({
|
|
2393
|
+
startTicks: parsed.startTicks,
|
|
2394
|
+
uptimeSec
|
|
2395
|
+
}),
|
|
2396
|
+
command: commandFromCmdline(cmdline, parsed.comm)
|
|
2105
2397
|
};
|
|
2106
2398
|
}
|
|
2107
|
-
/**
|
|
2108
|
-
function
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2111
|
-
const capped = projection.boundedRows.toLocaleString("en-US");
|
|
2112
|
-
if (!projection.capBites) return `This configuration retains ~${rows} rows ≈ ${mib} MiB on the hub database (NVMe cache), measured at 87 bytes per row.`;
|
|
2113
|
-
return `This configuration WANTS ~${rows} rows, above the ${projection.boundedRows.toLocaleString("en-US")}-row hard cap. The table will hold ~${capped} rows ≈ ${mib} MiB and the OLDEST samples will be evicted, so the effective window is shorter than the retention you asked for.`;
|
|
2399
|
+
/** Every process in `/proc`, as the rows `ps -eo …` would have printed. */
|
|
2400
|
+
async function readProcProcessTable(reader = nodeProcReader) {
|
|
2401
|
+
const [pids, uptimeSec] = await Promise.all([reader.listPids(), reader.readUptimeSec()]);
|
|
2402
|
+
return (await Promise.all(pids.map((pid) => readProcRow(pid, uptimeSec, reader)))).filter((row) => row !== null);
|
|
2114
2403
|
}
|
|
2115
|
-
//#endregion
|
|
2116
|
-
//#region src/builtins/native-metrics/load-series-reader.ts
|
|
2117
|
-
/**
|
|
2118
|
-
* The ONE reader over both retention tiers.
|
|
2119
|
-
*
|
|
2120
|
-
* The hot window is the in-memory ring; the cold one is
|
|
2121
|
-
* `metrics:node-load-samples`. A caller never learns which tier a point came
|
|
2122
|
-
* from, and that is a hard requirement rather than a nicety: a second reader
|
|
2123
|
-
* is how two charts start disagreeing about the same node, and it is how "the
|
|
2124
|
-
* ring says X, the table says Y" becomes a question nobody can answer.
|
|
2125
|
-
*
|
|
2126
|
-
* Three things happen here, in this order, and each one is load-bearing:
|
|
2127
|
-
*
|
|
2128
|
-
* 1. **Merge and dedupe on `atMs`.** The two tiers OVERLAP by construction —
|
|
2129
|
-
* the ring holds the last 180 snapshots and the table holds every snapshot
|
|
2130
|
-
* the ring accepted, including those same 180. A snapshot present in both
|
|
2131
|
-
* must contribute exactly once. Identity is `atMs`, because one node emits
|
|
2132
|
-
* one snapshot per tick and a timestamp already held IS that snapshot. The
|
|
2133
|
-
* HOT copy wins on a tie: it is the payload as it arrived, while the cold
|
|
2134
|
-
* one has been through the storage encode.
|
|
2135
|
-
* 2. **Fold per function.** The chart draws one series per addon, so the fold
|
|
2136
|
-
* happens here rather than in the browser.
|
|
2137
|
-
* 3. **Reduce, preserving the extremes.** See `load-series-fold.ts` for why
|
|
2138
|
-
* the representative of a bucket is its MAX and never its mean.
|
|
2139
|
-
*
|
|
2140
|
-
* A GAP stays a gap through all three. Emission is fixed-cadence, so a missing
|
|
2141
|
-
* interval means one thing — nobody reported — and nothing here fills it in.
|
|
2142
|
-
*/
|
|
2143
2404
|
/**
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
* Exported because the dedup is the property worth testing on its own: a
|
|
2147
|
-
* doubled point is invisible in a total and obvious only as a chart that reads
|
|
2148
|
-
* twice as loud as the machine.
|
|
2405
|
+
* Parse `ps etime` → seconds. `MM:SS`, `HH:MM:SS`, `DD-HH:MM:SS`.
|
|
2406
|
+
* Only the `ps` path needs this; `/proc` gives the number directly.
|
|
2149
2407
|
*/
|
|
2150
|
-
function
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2408
|
+
function parseEtime(etime) {
|
|
2409
|
+
const dashParts = etime.split("-");
|
|
2410
|
+
let days = 0;
|
|
2411
|
+
let hms = etime;
|
|
2412
|
+
if (dashParts.length === 2) {
|
|
2413
|
+
days = parseInt(dashParts[0], 10) || 0;
|
|
2414
|
+
hms = dashParts[1];
|
|
2415
|
+
}
|
|
2416
|
+
const parts = hms.split(":").map((p) => parseInt(p, 10) || 0);
|
|
2417
|
+
let hours = 0;
|
|
2418
|
+
let minutes = 0;
|
|
2419
|
+
let seconds = 0;
|
|
2420
|
+
if (parts.length === 3) [hours, minutes, seconds] = parts;
|
|
2421
|
+
else if (parts.length === 2) [minutes, seconds] = parts;
|
|
2422
|
+
else if (parts.length === 1) [seconds] = parts;
|
|
2423
|
+
return days * 86400 + hours * 3600 + minutes * 60 + seconds;
|
|
2155
2424
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2425
|
+
var PS_ROW_RE = /^(\d+)\s+(\d+)\s+(\d+)\s+([\d.]+)\s+(\d+)\s+(\S+)\s+(.+)$/;
|
|
2426
|
+
/** Parse the output of `ps -eo pid=,ppid=,pgid=,pcpu=,rss=,etime=,command=`. */
|
|
2427
|
+
function parsePsTable(stdout) {
|
|
2428
|
+
const rows = [];
|
|
2429
|
+
for (const line of stdout.split("\n")) {
|
|
2430
|
+
const match = line.trim().match(PS_ROW_RE);
|
|
2431
|
+
if (!match) continue;
|
|
2432
|
+
const [, pidS, ppidS, pgidS, cpuS, rssS, etimeS, command] = match;
|
|
2433
|
+
rows.push({
|
|
2434
|
+
pid: parseInt(pidS, 10),
|
|
2435
|
+
ppid: parseInt(ppidS, 10),
|
|
2436
|
+
pgid: parseInt(pgidS, 10),
|
|
2437
|
+
cpuPercent: Math.round(parseFloat(cpuS) * 10) / 10,
|
|
2438
|
+
memoryRssBytes: parseInt(rssS, 10) * 1024,
|
|
2439
|
+
uptimeSec: parseEtime(etimeS),
|
|
2440
|
+
command
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
return rows;
|
|
2444
|
+
}
|
|
2445
|
+
/** The fork. Only reached on a platform without `/proc`. */
|
|
2446
|
+
async function spawnPsTable() {
|
|
2447
|
+
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,pgid=,pcpu=,rss=,etime=,command="], {
|
|
2448
|
+
timeout: 5e3,
|
|
2449
|
+
maxBuffer: 8 * 1024 * 1024
|
|
2450
|
+
});
|
|
2451
|
+
return parsePsTable(stdout);
|
|
2159
2452
|
}
|
|
2453
|
+
var defaultDeps = {
|
|
2454
|
+
platform: (0, node_os.platform)(),
|
|
2455
|
+
reader: nodeProcReader,
|
|
2456
|
+
spawnPs: spawnPsTable
|
|
2457
|
+
};
|
|
2160
2458
|
/**
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
2163
|
-
*
|
|
2459
|
+
* The process table, from `/proc` where it exists and from `ps` elsewhere.
|
|
2460
|
+
* Throws only when the chosen source cannot answer at all; the caller logs it
|
|
2461
|
+
* as a skipped interval.
|
|
2164
2462
|
*/
|
|
2165
|
-
function
|
|
2166
|
-
|
|
2167
|
-
if (samples.length === 0) return {
|
|
2168
|
-
series: [],
|
|
2169
|
-
bucketMs: input.cadenceMs,
|
|
2170
|
-
retainedSamples: 0,
|
|
2171
|
-
oldestAtMs: null
|
|
2172
|
-
};
|
|
2173
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
2174
|
-
for (const sample of samples) for (const bucket of require_dist.foldSnapshotByFunction(toFoldRows(sample.processes), sample.atMs)) {
|
|
2175
|
-
const held = byKey.get(bucket.key);
|
|
2176
|
-
if (held === void 0) byKey.set(bucket.key, {
|
|
2177
|
-
kind: bucket.kind,
|
|
2178
|
-
points: [bucket.point]
|
|
2179
|
-
});
|
|
2180
|
-
else held.points.push(bucket.point);
|
|
2181
|
-
}
|
|
2182
|
-
const oldestAtMs = samples[0]?.atMs ?? null;
|
|
2183
|
-
const newestAtMs = samples.at(-1)?.atMs ?? oldestAtMs;
|
|
2184
|
-
const spanMs = oldestAtMs === null || newestAtMs === null ? 0 : newestAtMs - oldestAtMs + input.cadenceMs;
|
|
2185
|
-
const bucketMs = input.maxPoints === void 0 ? input.cadenceMs : require_dist.resolveBucketMs(spanMs, input.cadenceMs, input.maxPoints);
|
|
2186
|
-
const origin = oldestAtMs ?? 0;
|
|
2187
|
-
const series = [...byKey.entries()].map(([key, held]) => ({
|
|
2188
|
-
key,
|
|
2189
|
-
kind: held.kind,
|
|
2190
|
-
points: bucketMs > input.cadenceMs ? require_dist.reducePoints(held.points, bucketMs, origin) : held.points
|
|
2191
|
-
}));
|
|
2192
|
-
const weight = (s) => {
|
|
2193
|
-
const last = s.points.at(-1);
|
|
2194
|
-
if (last === void 0) return -1;
|
|
2195
|
-
if (last.cpuMainPercent !== null && last.cpuGcPercent !== null) return last.cpuMainPercent + last.cpuGcPercent;
|
|
2196
|
-
return last.cpuLifetimePercent;
|
|
2197
|
-
};
|
|
2198
|
-
return {
|
|
2199
|
-
series: series.toSorted((a, b) => {
|
|
2200
|
-
const d = weight(b) - weight(a);
|
|
2201
|
-
return d !== 0 ? d : a.key.localeCompare(b.key);
|
|
2202
|
-
}),
|
|
2203
|
-
bucketMs,
|
|
2204
|
-
retainedSamples: samples.length,
|
|
2205
|
-
oldestAtMs
|
|
2206
|
-
};
|
|
2463
|
+
function readProcessTable(deps = defaultDeps) {
|
|
2464
|
+
return deps.platform === "linux" ? readProcProcessTable(deps.reader) : deps.spawnPs();
|
|
2207
2465
|
}
|
|
2208
2466
|
//#endregion
|
|
2209
2467
|
//#region src/builtins/native-metrics/process-classification.ts
|
|
@@ -2454,7 +2712,6 @@ var ThreadCpuTracker = class {
|
|
|
2454
2712
|
};
|
|
2455
2713
|
//#endregion
|
|
2456
2714
|
//#region src/builtins/native-metrics/native-metrics.addon.ts
|
|
2457
|
-
var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
2458
2715
|
/**
|
|
2459
2716
|
* Cadence for the per-node metric snapshot bus events. ~5 s matches
|
|
2460
2717
|
* the existing UI polling cadence (ProcessesTab, ProcessResources)
|
|
@@ -2463,25 +2720,13 @@ var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
|
2463
2720
|
*/
|
|
2464
2721
|
var METRICS_SNAPSHOT_INTERVAL_MS = 5e3;
|
|
2465
2722
|
/**
|
|
2466
|
-
*
|
|
2467
|
-
* operator's `loadSeriesCadenceSec` (5-60 s, 10 by default).
|
|
2468
|
-
*
|
|
2469
|
-
* It used to emit only ON A CHANGE, with a 60 s fallback heartbeat, and the
|
|
2470
|
-
* dedup is gone rather than tuned. A conditional emit makes a hole in the
|
|
2471
|
-
* series mean two different things — "nothing changed" and "nobody reported"
|
|
2472
|
-
* — and telling those apart is the entire reason an operator opens this chart
|
|
2473
|
-
* after a spike. Now a gap means exactly one thing.
|
|
2474
|
-
*
|
|
2475
|
-
* It is not free: the emit rate is deterministic, so the durable table's row
|
|
2476
|
-
* count is too (nodes x processes / cadence, no discount for a quiet fleet).
|
|
2477
|
-
* The measured cost of that is in `docs/architecture/load-ledger.md`, and it
|
|
2478
|
-
* is why the retention default is six hours rather than three days.
|
|
2479
|
-
*/
|
|
2480
|
-
/**
|
|
2481
|
-
* Force a metrics-snapshot emit at least every 60s even when the
|
|
2723
|
+
* Force a RESOURCES-snapshot emit at least every 60s even when the
|
|
2482
2724
|
* coarsened payload looks unchanged. Without this an idle node
|
|
2483
2725
|
* (steady CPU within ±5%, no process churn) goes silent on the
|
|
2484
2726
|
* bus and the UI's "node alive" chip can stale out.
|
|
2727
|
+
*
|
|
2728
|
+
* The PROCESS-tree snapshot has no such heartbeat and no dedup: it is
|
|
2729
|
+
* fixed-cadence — see `startProcessSnapshotTimer`.
|
|
2485
2730
|
*/
|
|
2486
2731
|
var METRICS_SNAPSHOT_HEARTBEAT_MS = 6e4;
|
|
2487
2732
|
/**
|
|
@@ -2796,7 +3041,23 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
|
|
|
2796
3041
|
} });
|
|
2797
3042
|
}
|
|
2798
3043
|
}
|
|
2799
|
-
/**
|
|
3044
|
+
/**
|
|
3045
|
+
* (Re)arm the process-tree timer at the configured interval.
|
|
3046
|
+
*
|
|
3047
|
+
* The process-tree snapshot is FIXED-CADENCE, and its cadence is the
|
|
3048
|
+
* operator's `loadSeriesCadenceSec` (5-60 s, 10 by default).
|
|
3049
|
+
*
|
|
3050
|
+
* It used to emit only ON A CHANGE, with a 60 s fallback heartbeat, and the
|
|
3051
|
+
* dedup is gone rather than tuned. A conditional emit makes a hole in the
|
|
3052
|
+
* series mean two different things — "nothing changed" and "nobody reported"
|
|
3053
|
+
* — and telling those apart is the entire reason an operator opens this chart
|
|
3054
|
+
* after a spike. Now a gap means exactly one thing.
|
|
3055
|
+
*
|
|
3056
|
+
* It is not free: the emit rate is deterministic, so the durable table's row
|
|
3057
|
+
* count is too (nodes x processes / cadence, no discount for a quiet fleet).
|
|
3058
|
+
* The measured cost of that is in `docs/architecture/load-ledger.md`, and it
|
|
3059
|
+
* is why the retention default is six hours rather than three days.
|
|
3060
|
+
*/
|
|
2800
3061
|
startProcessSnapshotTimer() {
|
|
2801
3062
|
if (this.processSnapshotTimer) clearInterval(this.processSnapshotTimer);
|
|
2802
3063
|
this.processSnapshotTimer = setInterval(() => this.emitProcessesSnapshot(), this.loadConfig.cadenceSec * 1e3);
|
|
@@ -2858,7 +3119,8 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
|
|
|
2858
3119
|
/**
|
|
2859
3120
|
* Emit one `metrics.node-processes-snapshot` for this node.
|
|
2860
3121
|
*
|
|
2861
|
-
* Heavy — a full
|
|
3122
|
+
* Heavy — a full process-table read (`/proc` on Linux, `ps -eo` elsewhere;
|
|
3123
|
+
* `proc-process-table.ts`) plus a `$process.list` broker call — and
|
|
2862
3124
|
* UNCONDITIONAL. The change-detection that used to guard it is gone: with a
|
|
2863
3125
|
* fixed cadence a missing interval means exactly one thing, and that is the
|
|
2864
3126
|
* property an operator investigating a spike is actually looking for.
|
|
@@ -2963,40 +3225,17 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
|
|
|
2963
3225
|
});
|
|
2964
3226
|
}
|
|
2965
3227
|
/**
|
|
2966
|
-
*
|
|
2967
|
-
*
|
|
2968
|
-
*
|
|
2969
|
-
*
|
|
2970
|
-
*
|
|
3228
|
+
* Every pid + command + resource stats on this node.
|
|
3229
|
+
*
|
|
3230
|
+
* From `/proc` on Linux — no fork. This used to be a `ps -eo` child on every
|
|
3231
|
+
* tick, i.e. a synchronous `fork()` of hub-main at p50 91 ms (D441, D452).
|
|
3232
|
+
* `ps` remains the source only on a platform without `/proc`.
|
|
2971
3233
|
*/
|
|
2972
|
-
/** Raw `ps` scan returning every pid + command + resource stats. */
|
|
2973
3234
|
async runPs() {
|
|
2974
3235
|
try {
|
|
2975
|
-
|
|
2976
|
-
timeout: 5e3,
|
|
2977
|
-
maxBuffer: 8 * 1024 * 1024
|
|
2978
|
-
});
|
|
2979
|
-
const lines = stdout.split("\n");
|
|
2980
|
-
const rows = [];
|
|
2981
|
-
for (const line of lines) {
|
|
2982
|
-
const trimmed = line.trim();
|
|
2983
|
-
if (!trimmed) continue;
|
|
2984
|
-
const match = trimmed.match(/^(\d+)\s+(\d+)\s+(\d+)\s+([\d.]+)\s+(\d+)\s+(\S+)\s+(.+)$/);
|
|
2985
|
-
if (!match) continue;
|
|
2986
|
-
const [, pidS, ppidS, pgidS, cpuS, rssS, etimeS, command] = match;
|
|
2987
|
-
rows.push({
|
|
2988
|
-
pid: parseInt(pidS, 10),
|
|
2989
|
-
ppid: parseInt(ppidS, 10),
|
|
2990
|
-
pgid: parseInt(pgidS, 10),
|
|
2991
|
-
cpuPercent: Math.round(parseFloat(cpuS) * 10) / 10,
|
|
2992
|
-
memoryRssBytes: parseInt(rssS, 10) * 1024,
|
|
2993
|
-
uptimeSec: parseEtime(etimeS),
|
|
2994
|
-
command
|
|
2995
|
-
});
|
|
2996
|
-
}
|
|
2997
|
-
return rows;
|
|
3236
|
+
return await readProcessTable();
|
|
2998
3237
|
} catch (err) {
|
|
2999
|
-
this.ctx.logger.warn("
|
|
3238
|
+
this.ctx.logger.warn("process table read failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
3000
3239
|
return [];
|
|
3001
3240
|
}
|
|
3002
3241
|
}
|
|
@@ -3081,29 +3320,10 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
|
|
|
3081
3320
|
}] });
|
|
3082
3321
|
}
|
|
3083
3322
|
};
|
|
3084
|
-
/**
|
|
3085
|
-
* Parse `ps etime` format → seconds.
|
|
3086
|
-
*
|
|
3087
|
-
* Formats we handle:
|
|
3088
|
-
* `MM:SS` (< 1h)
|
|
3089
|
-
* `HH:MM:SS` (< 1d)
|
|
3090
|
-
* `DD-HH:MM:SS` (>= 1d)
|
|
3091
|
-
*/
|
|
3092
|
-
function parseEtime(etime) {
|
|
3093
|
-
const dashParts = etime.split("-");
|
|
3094
|
-
let days = 0;
|
|
3095
|
-
let hms = etime;
|
|
3096
|
-
if (dashParts.length === 2) {
|
|
3097
|
-
days = parseInt(dashParts[0], 10) || 0;
|
|
3098
|
-
hms = dashParts[1];
|
|
3099
|
-
}
|
|
3100
|
-
const parts = hms.split(":").map((p) => parseInt(p, 10) || 0);
|
|
3101
|
-
let hours = 0, minutes = 0, seconds = 0;
|
|
3102
|
-
if (parts.length === 3) [hours, minutes, seconds] = parts;
|
|
3103
|
-
else if (parts.length === 2) [minutes, seconds] = parts;
|
|
3104
|
-
else if (parts.length === 1) [seconds] = parts;
|
|
3105
|
-
return days * 86400 + hours * 3600 + minutes * 60 + seconds;
|
|
3106
|
-
}
|
|
3107
3323
|
//#endregion
|
|
3108
3324
|
exports.NativeMetricsProvider = NativeMetricsProvider;
|
|
3109
3325
|
exports.default = NativeMetricsAddon;
|
|
3326
|
+
exports.lifetimeCpuPercent = lifetimeCpuPercent;
|
|
3327
|
+
exports.nodeProcReader = nodeProcReader;
|
|
3328
|
+
exports.parseProcStat = parseProcStat;
|
|
3329
|
+
exports.parseProcStatusRss = parseProcStatusRss;
|