@camstack/system 1.2.218 → 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 +2 -2
- package/dist/addon-runner.mjs +2 -2
- 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 +26 -3
- package/dist/builtins/native-metrics/native-metrics.addon.js +1320 -1093
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1320 -1096
- 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-CsONUlhC.mjs → custom-action-registry-B6gOtUWA.mjs} +1 -1
- package/dist/{custom-action-registry-BDwM5qAK.js → custom-action-registry-DKWhaWL1.js} +1 -1
- package/dist/{dist-BOlMCOeT.js → dist-B17W9ngu.js} +2 -2
- package/dist/{dist-DigmGsQS.mjs → dist-Cyz5_i7z.mjs} +2 -2
- package/dist/index.d.ts +6 -1
- package/dist/index.js +31 -36
- package/dist/index.mjs +23 -38
- package/dist/kernel/heap-watch.d.ts +24 -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 +28 -2
- package/dist/kernel/transport/frame-codec.d.ts +49 -0
- package/dist/kernel/transport/local-child-client.d.ts +7 -0
- 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-D--wyMD9.mjs → manifest-system-deps-C7dihwWT.mjs} +1210 -682
- package/dist/{manifest-system-deps-Bo92lg5u.js → manifest-system-deps-g_nK0xsF.js} +1252 -664
- 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-vK_U-CG2.mjs → retired-settings-keys-BCYUrdsA.mjs} +1 -1
- package/dist/{retired-settings-keys-xEAlYnv7.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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel jiffies per second (`USER_HZ`). `sysconf(_SC_CLK_TCK)` is not exposed
|
|
3
|
+
* to Node, and this has been 100 on every Linux/x86-64 and arm64 kernel
|
|
4
|
+
* configuration we ship to. A wrong value would scale every CPU number and
|
|
5
|
+
* every elapsed time by one constant — visible at once, not a silent skew.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CLOCK_TICKS_PER_SEC = 100;
|
|
8
|
+
/** The fields of one `/proc/<pid>/stat` line this repository reads. */
|
|
9
|
+
export interface ProcStat {
|
|
10
|
+
readonly pid: number;
|
|
11
|
+
readonly comm: string;
|
|
12
|
+
/** Field 3: `R`, `S`, `D`, `Z`, … */
|
|
13
|
+
readonly state: string;
|
|
14
|
+
readonly ppid: number;
|
|
15
|
+
readonly pgid: number;
|
|
16
|
+
/** `utime + stime`, in jiffies, over the process's whole life. */
|
|
17
|
+
readonly ticks: number;
|
|
18
|
+
/** Field 22: jiffies since boot at which this incarnation of the pid began. */
|
|
19
|
+
readonly startTicks: number;
|
|
20
|
+
/** Field 24: resident pages. Prefer `VmRSS` from `status`, which is in kB. */
|
|
21
|
+
readonly rssPages: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse one `/proc/<pid>/stat` line. `null` for a line that does not parse —
|
|
25
|
+
* a process that exited between the readdir and the read leaves a truncated
|
|
26
|
+
* or empty file, and that is normal, not an error.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseProcStat(line: string): ProcStat | null;
|
|
29
|
+
/** Resident memory from `/proc/<pid>/status`, in bytes. Absent is `undefined`, never 0. */
|
|
30
|
+
export interface ProcStatusRss {
|
|
31
|
+
/** `VmRSS` — what `ps rss` prints. */
|
|
32
|
+
readonly rssBytes: number | undefined;
|
|
33
|
+
/** `RssAnon` — private anonymous memory, the runner's own cost. */
|
|
34
|
+
readonly privateBytes: number | undefined;
|
|
35
|
+
/** `RssFile` — file-backed pages shared with every process mapping the same files. */
|
|
36
|
+
readonly sharedBytes: number | undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Read `VmRSS` / `RssAnon` / `RssFile` (kB in the file) out of `/proc/<pid>/status`. */
|
|
39
|
+
export declare function parseProcStatusRss(status: string): ProcStatusRss;
|
|
40
|
+
/** Inputs for the two `ps`-compatible derivations below. */
|
|
41
|
+
export interface LifetimeCpuInput {
|
|
42
|
+
readonly ticks: number;
|
|
43
|
+
readonly startTicks: number;
|
|
44
|
+
/** First field of `/proc/uptime`, seconds since boot. */
|
|
45
|
+
readonly uptimeSec: number;
|
|
46
|
+
}
|
|
47
|
+
/** Seconds the process has been alive — `ps etime`, whole seconds. */
|
|
48
|
+
export declare function elapsedSecSince(input: Pick<LifetimeCpuInput, 'startTicks' | 'uptimeSec'>): number;
|
|
49
|
+
/**
|
|
50
|
+
* `ps pcpu`: CPU time over elapsed time, as a percentage with one decimal.
|
|
51
|
+
*
|
|
52
|
+
* This is deliberately the LIFETIME average `ps` prints, not an instantaneous
|
|
53
|
+
* rate: the columns fed from it have always shown that quantity, and the
|
|
54
|
+
* instantaneous split is a different number with a different owner
|
|
55
|
+
* (`cpuMainPercent` / `cpuGcPercent` from `thread-cpu-sampler.ts`). A process
|
|
56
|
+
* with no elapsed window yet reports 0, never a division by zero.
|
|
57
|
+
*/
|
|
58
|
+
export declare function lifetimeCpuPercent(input: LifetimeCpuInput): number;
|
|
59
|
+
/**
|
|
60
|
+
* The procfs surface, injectable — the whole reason the readers above are
|
|
61
|
+
* testable against fixtures rather than against a Linux kernel.
|
|
62
|
+
*/
|
|
63
|
+
export interface ProcReader {
|
|
64
|
+
/** Numeric entries of `/proc`. */
|
|
65
|
+
readonly listPids: () => Promise<readonly number[]>;
|
|
66
|
+
readonly readStat: (pid: number) => Promise<string>;
|
|
67
|
+
readonly readStatus: (pid: number) => Promise<string>;
|
|
68
|
+
/** Raw `/proc/<pid>/cmdline`: NUL-separated argv, empty for a kernel thread. */
|
|
69
|
+
readonly readCmdline: (pid: number) => Promise<string>;
|
|
70
|
+
/** First field of `/proc/uptime`. */
|
|
71
|
+
readonly readUptimeSec: () => Promise<number>;
|
|
72
|
+
}
|
|
73
|
+
export declare const nodeProcReader: ProcReader;
|
|
74
|
+
/** `ps command=`: argv joined by spaces, or `[comm]` when there is no argv. */
|
|
75
|
+
export declare function commandFromCmdline(cmdline: string, comm: string): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProcReader } from './proc-stat.js';
|
|
1
2
|
export interface PidStats {
|
|
2
3
|
cpu: number;
|
|
3
4
|
memory: number;
|
|
@@ -11,10 +12,17 @@ export interface PidStats {
|
|
|
11
12
|
/** Shared file-backed resident bytes (Linux RssFile). undefined on macOS. */
|
|
12
13
|
sharedBytes?: number;
|
|
13
14
|
}
|
|
15
|
+
/** The impure inputs, injectable so the platform branch is pinned by a test. */
|
|
16
|
+
export interface PidStatsDeps {
|
|
17
|
+
readonly platform: NodeJS.Platform;
|
|
18
|
+
readonly reader: ProcReader;
|
|
19
|
+
/** `ps -o pid=,pcpu=,rss= -p <pids>` — the fork, for a platform without `/proc`. */
|
|
20
|
+
readonly spawnPs: (pids: readonly number[]) => Promise<string>;
|
|
21
|
+
}
|
|
14
22
|
/**
|
|
15
23
|
* Get CPU% and memory RSS for one or more PIDs.
|
|
16
24
|
* Returns a map of pid → stats. Missing/dead PIDs are silently omitted.
|
|
17
25
|
*/
|
|
18
|
-
export declare function getPidStats(pids: number | readonly number[]): Promise<ReadonlyMap<number, PidStats>>;
|
|
26
|
+
export declare function getPidStats(pids: number | readonly number[], deps?: PidStatsDeps): Promise<ReadonlyMap<number, PidStats>>;
|
|
19
27
|
/** Get CPU% and memory RSS for a single PID. Returns null if dead. */
|
|
20
28
|
export declare function getSinglePidStats(pid: number): Promise<PidStats | null>;
|
|
@@ -1,29 +1,12 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
+
const require_builtins_native_metrics_native_metrics_addon = require("./builtins/native-metrics/native-metrics.addon.js");
|
|
2
3
|
let node_child_process = require("node:child_process");
|
|
3
|
-
let
|
|
4
|
-
node_fs = require_chunk.__toESM(node_fs);
|
|
4
|
+
let node_os = require("node:os");
|
|
5
5
|
//#region src/process/resource-monitor.ts
|
|
6
6
|
var resource_monitor_exports = /* @__PURE__ */ require_chunk.__exportAll({
|
|
7
7
|
getPidStats: () => getPidStats,
|
|
8
8
|
getSinglePidStats: () => getSinglePidStats
|
|
9
9
|
});
|
|
10
|
-
/**
|
|
11
|
-
* Read RssAnon / RssFile (KB) from /proc/<pid>/status. Linux-only; returns
|
|
12
|
-
* undefined fields anywhere /proc is absent. Cheap (one small file read).
|
|
13
|
-
*/
|
|
14
|
-
function readProcRss(pid) {
|
|
15
|
-
try {
|
|
16
|
-
const status = node_fs.readFileSync(`/proc/${pid}/status`, "utf8");
|
|
17
|
-
const anon = status.match(/^RssAnon:\s+(\d+)/m);
|
|
18
|
-
const file = status.match(/^RssFile:\s+(\d+)/m);
|
|
19
|
-
return {
|
|
20
|
-
privateBytes: anon ? parseInt(anon[1], 10) * 1024 : void 0,
|
|
21
|
-
sharedBytes: file ? parseInt(file[1], 10) * 1024 : void 0
|
|
22
|
-
};
|
|
23
|
-
} catch {
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
10
|
function execPsForPids(pids) {
|
|
28
11
|
return new Promise((resolve, reject) => {
|
|
29
12
|
(0, node_child_process.execFile)("ps", [
|
|
@@ -37,6 +20,11 @@ function execPsForPids(pids) {
|
|
|
37
20
|
});
|
|
38
21
|
});
|
|
39
22
|
}
|
|
23
|
+
var defaultDeps = {
|
|
24
|
+
platform: (0, node_os.platform)(),
|
|
25
|
+
reader: require_builtins_native_metrics_native_metrics_addon.nodeProcReader,
|
|
26
|
+
spawnPs: execPsForPids
|
|
27
|
+
};
|
|
40
28
|
function parsePsOutput(stdout) {
|
|
41
29
|
const result = /* @__PURE__ */ new Map();
|
|
42
30
|
for (const line of stdout.trim().split("\n")) {
|
|
@@ -50,21 +38,52 @@ function parsePsOutput(stdout) {
|
|
|
50
38
|
if (isNaN(pid) || isNaN(cpu) || isNaN(rssKb)) continue;
|
|
51
39
|
result.set(pid, {
|
|
52
40
|
cpu: Math.round(cpu * 10) / 10,
|
|
53
|
-
memory: rssKb * 1024
|
|
54
|
-
...readProcRss(pid)
|
|
41
|
+
memory: rssKb * 1024
|
|
55
42
|
});
|
|
56
43
|
}
|
|
57
44
|
return result;
|
|
58
45
|
}
|
|
46
|
+
/** One pid from `/proc`, or `null` when it is gone or unreadable. */
|
|
47
|
+
async function readProcPidStats(pid, uptimeSec, reader) {
|
|
48
|
+
let stat;
|
|
49
|
+
let status;
|
|
50
|
+
try {
|
|
51
|
+
[stat, status] = await Promise.all([reader.readStat(pid), reader.readStatus(pid)]);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const parsed = require_builtins_native_metrics_native_metrics_addon.parseProcStat(stat);
|
|
56
|
+
if (parsed === null) return null;
|
|
57
|
+
const rss = require_builtins_native_metrics_native_metrics_addon.parseProcStatusRss(status);
|
|
58
|
+
if (rss.rssBytes === void 0) return null;
|
|
59
|
+
return {
|
|
60
|
+
cpu: require_builtins_native_metrics_native_metrics_addon.lifetimeCpuPercent({
|
|
61
|
+
ticks: parsed.ticks,
|
|
62
|
+
startTicks: parsed.startTicks,
|
|
63
|
+
uptimeSec
|
|
64
|
+
}),
|
|
65
|
+
memory: rss.rssBytes,
|
|
66
|
+
privateBytes: rss.privateBytes,
|
|
67
|
+
sharedBytes: rss.sharedBytes
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function readProcPidStatsAll(pids, reader) {
|
|
71
|
+
const uptimeSec = await reader.readUptimeSec();
|
|
72
|
+
const entries = await Promise.all(pids.map(async (pid) => [pid, await readProcPidStats(pid, uptimeSec, reader)]));
|
|
73
|
+
const result = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const [pid, stats] of entries) if (stats !== null) result.set(pid, stats);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
59
77
|
/**
|
|
60
78
|
* Get CPU% and memory RSS for one or more PIDs.
|
|
61
79
|
* Returns a map of pid → stats. Missing/dead PIDs are silently omitted.
|
|
62
80
|
*/
|
|
63
|
-
async function getPidStats(pids) {
|
|
81
|
+
async function getPidStats(pids, deps = defaultDeps) {
|
|
64
82
|
const pidArray = typeof pids === "number" ? [pids] : [...pids];
|
|
65
83
|
if (pidArray.length === 0) return /* @__PURE__ */ new Map();
|
|
66
84
|
try {
|
|
67
|
-
return
|
|
85
|
+
if (deps.platform === "linux") return await readProcPidStatsAll(pidArray, deps.reader);
|
|
86
|
+
return parsePsOutput(await deps.spawnPs(pidArray));
|
|
68
87
|
} catch {
|
|
69
88
|
return /* @__PURE__ */ new Map();
|
|
70
89
|
}
|
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
import { r as __exportAll } from "./chunk-CNf5ZN-e.mjs";
|
|
2
|
+
import { i as parseProcStatusRss, n as nodeProcReader, r as parseProcStat, t as lifetimeCpuPercent } from "./builtins/native-metrics/native-metrics.addon.mjs";
|
|
2
3
|
import { execFile } from "node:child_process";
|
|
3
|
-
import
|
|
4
|
+
import { platform } from "node:os";
|
|
4
5
|
//#region src/process/resource-monitor.ts
|
|
5
6
|
var resource_monitor_exports = /* @__PURE__ */ __exportAll({
|
|
6
7
|
getPidStats: () => getPidStats,
|
|
7
8
|
getSinglePidStats: () => getSinglePidStats
|
|
8
9
|
});
|
|
9
|
-
/**
|
|
10
|
-
* Read RssAnon / RssFile (KB) from /proc/<pid>/status. Linux-only; returns
|
|
11
|
-
* undefined fields anywhere /proc is absent. Cheap (one small file read).
|
|
12
|
-
*/
|
|
13
|
-
function readProcRss(pid) {
|
|
14
|
-
try {
|
|
15
|
-
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
|
|
16
|
-
const anon = status.match(/^RssAnon:\s+(\d+)/m);
|
|
17
|
-
const file = status.match(/^RssFile:\s+(\d+)/m);
|
|
18
|
-
return {
|
|
19
|
-
privateBytes: anon ? parseInt(anon[1], 10) * 1024 : void 0,
|
|
20
|
-
sharedBytes: file ? parseInt(file[1], 10) * 1024 : void 0
|
|
21
|
-
};
|
|
22
|
-
} catch {
|
|
23
|
-
return {};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
10
|
function execPsForPids(pids) {
|
|
27
11
|
return new Promise((resolve, reject) => {
|
|
28
12
|
execFile("ps", [
|
|
@@ -36,6 +20,11 @@ function execPsForPids(pids) {
|
|
|
36
20
|
});
|
|
37
21
|
});
|
|
38
22
|
}
|
|
23
|
+
var defaultDeps = {
|
|
24
|
+
platform: platform(),
|
|
25
|
+
reader: nodeProcReader,
|
|
26
|
+
spawnPs: execPsForPids
|
|
27
|
+
};
|
|
39
28
|
function parsePsOutput(stdout) {
|
|
40
29
|
const result = /* @__PURE__ */ new Map();
|
|
41
30
|
for (const line of stdout.trim().split("\n")) {
|
|
@@ -49,21 +38,52 @@ function parsePsOutput(stdout) {
|
|
|
49
38
|
if (isNaN(pid) || isNaN(cpu) || isNaN(rssKb)) continue;
|
|
50
39
|
result.set(pid, {
|
|
51
40
|
cpu: Math.round(cpu * 10) / 10,
|
|
52
|
-
memory: rssKb * 1024
|
|
53
|
-
...readProcRss(pid)
|
|
41
|
+
memory: rssKb * 1024
|
|
54
42
|
});
|
|
55
43
|
}
|
|
56
44
|
return result;
|
|
57
45
|
}
|
|
46
|
+
/** One pid from `/proc`, or `null` when it is gone or unreadable. */
|
|
47
|
+
async function readProcPidStats(pid, uptimeSec, reader) {
|
|
48
|
+
let stat;
|
|
49
|
+
let status;
|
|
50
|
+
try {
|
|
51
|
+
[stat, status] = await Promise.all([reader.readStat(pid), reader.readStatus(pid)]);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const parsed = parseProcStat(stat);
|
|
56
|
+
if (parsed === null) return null;
|
|
57
|
+
const rss = parseProcStatusRss(status);
|
|
58
|
+
if (rss.rssBytes === void 0) return null;
|
|
59
|
+
return {
|
|
60
|
+
cpu: lifetimeCpuPercent({
|
|
61
|
+
ticks: parsed.ticks,
|
|
62
|
+
startTicks: parsed.startTicks,
|
|
63
|
+
uptimeSec
|
|
64
|
+
}),
|
|
65
|
+
memory: rss.rssBytes,
|
|
66
|
+
privateBytes: rss.privateBytes,
|
|
67
|
+
sharedBytes: rss.sharedBytes
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function readProcPidStatsAll(pids, reader) {
|
|
71
|
+
const uptimeSec = await reader.readUptimeSec();
|
|
72
|
+
const entries = await Promise.all(pids.map(async (pid) => [pid, await readProcPidStats(pid, uptimeSec, reader)]));
|
|
73
|
+
const result = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const [pid, stats] of entries) if (stats !== null) result.set(pid, stats);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
58
77
|
/**
|
|
59
78
|
* Get CPU% and memory RSS for one or more PIDs.
|
|
60
79
|
* Returns a map of pid → stats. Missing/dead PIDs are silently omitted.
|
|
61
80
|
*/
|
|
62
|
-
async function getPidStats(pids) {
|
|
81
|
+
async function getPidStats(pids, deps = defaultDeps) {
|
|
63
82
|
const pidArray = typeof pids === "number" ? [pids] : [...pids];
|
|
64
83
|
if (pidArray.length === 0) return /* @__PURE__ */ new Map();
|
|
65
84
|
try {
|
|
66
|
-
return
|
|
85
|
+
if (deps.platform === "linux") return await readProcPidStatsAll(pidArray, deps.reader);
|
|
86
|
+
return parsePsOutput(await deps.spawnPs(pidArray));
|
|
67
87
|
} catch {
|
|
68
88
|
return /* @__PURE__ */ new Map();
|
|
69
89
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Longest the worker lets pending frames sit before folding them, in ms. A
|
|
3
|
+
* quiet database (a developer machine, an agent with little to persist) still
|
|
4
|
+
* gets its WAL folded and its pages made durable within a minute.
|
|
5
|
+
*/
|
|
6
|
+
var WAL_CHECKPOINT_MAX_INTERVAL_MS = 6e4;
|
|
7
|
+
/** How often the worker looks at the wal-index, in ms. A 136-byte read. */
|
|
8
|
+
var WAL_CHECKPOINT_POLL_MS = 1e3;
|
|
9
|
+
/**
|
|
10
|
+
* A checkpoint that took this long is reported at WARN with its numbers.
|
|
11
|
+
* Matched to `SQLITE_SLOW_CALL_MS`: it is the same "the disk held us" event
|
|
12
|
+
* the profiler used to file as a slow `set`, now on the thread that paid it.
|
|
13
|
+
*/
|
|
14
|
+
var WAL_CHECKPOINT_SLOW_MS = 1e3;
|
|
15
|
+
/** Should the worker run `wal_checkpoint(PASSIVE)` now? */
|
|
16
|
+
function decideCheckpoint(input) {
|
|
17
|
+
const intervalDue = input.sinceLastRunMs >= WAL_CHECKPOINT_MAX_INTERVAL_MS;
|
|
18
|
+
if (input.mxFrame === null || input.nBackfill === null) return intervalDue ? {
|
|
19
|
+
run: true,
|
|
20
|
+
reason: "unknown-interval",
|
|
21
|
+
pendingFrames: null
|
|
22
|
+
} : {
|
|
23
|
+
run: false,
|
|
24
|
+
reason: "unknown-below-interval",
|
|
25
|
+
pendingFrames: null
|
|
26
|
+
};
|
|
27
|
+
const pendingFrames = Math.max(0, input.mxFrame - input.nBackfill);
|
|
28
|
+
if (pendingFrames === 0) return {
|
|
29
|
+
run: false,
|
|
30
|
+
reason: "nothing-pending",
|
|
31
|
+
pendingFrames
|
|
32
|
+
};
|
|
33
|
+
if (pendingFrames >= 1e3) return {
|
|
34
|
+
run: true,
|
|
35
|
+
reason: "threshold",
|
|
36
|
+
pendingFrames
|
|
37
|
+
};
|
|
38
|
+
if (intervalDue) return {
|
|
39
|
+
run: true,
|
|
40
|
+
reason: "interval",
|
|
41
|
+
pendingFrames
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
run: false,
|
|
45
|
+
reason: "below-threshold",
|
|
46
|
+
pendingFrames
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Byte offsets in the `-shm` file, from `wal.c` ("WAL-index file format"):
|
|
51
|
+
* two copies of the 48-byte `WalIndexHdr` (copy 2 at 48), then `WalCkptInfo`
|
|
52
|
+
* at 96 whose first field is `nBackfill`. `mxFrame` is at byte 16 of the
|
|
53
|
+
* header. Both are u32 in the writer's native byte order — little-endian on
|
|
54
|
+
* every x86-64 and arm64 node this ships to.
|
|
55
|
+
*/
|
|
56
|
+
var WAL_INDEX_MXFRAME_OFFSET = 16;
|
|
57
|
+
var WAL_INDEX_HDR_SIZE = 48;
|
|
58
|
+
var WAL_INDEX_NBACKFILL_OFFSET = 96;
|
|
59
|
+
/**
|
|
60
|
+
* `mxFrame` and `nBackfill` from the first {@link WAL_INDEX_HEADER_READ_BYTES}
|
|
61
|
+
* of the `-shm` file, or `null` when the buffer is short or the two header
|
|
62
|
+
* copies disagree (a writer was updating them — SQLite's own readers retry on
|
|
63
|
+
* exactly that comparison; this caller simply waits for the next poll).
|
|
64
|
+
*/
|
|
65
|
+
function parseWalIndexHeader(buffer) {
|
|
66
|
+
if (buffer.length < 100) return null;
|
|
67
|
+
const copy1 = buffer.subarray(0, WAL_INDEX_HDR_SIZE);
|
|
68
|
+
const copy2 = buffer.subarray(WAL_INDEX_HDR_SIZE, WAL_INDEX_HDR_SIZE * 2);
|
|
69
|
+
if (!copy1.equals(copy2)) return null;
|
|
70
|
+
return {
|
|
71
|
+
mxFrame: buffer.readUInt32LE(WAL_INDEX_MXFRAME_OFFSET),
|
|
72
|
+
nBackfill: buffer.readUInt32LE(WAL_INDEX_NBACKFILL_OFFSET)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function isRecord(value) {
|
|
76
|
+
return typeof value === "object" && value !== null;
|
|
77
|
+
}
|
|
78
|
+
/** Narrow a message off the worker port without a cast. */
|
|
79
|
+
function isWorkerMessage(value) {
|
|
80
|
+
if (!isRecord(value)) return false;
|
|
81
|
+
switch (value["kind"]) {
|
|
82
|
+
case "ready": return true;
|
|
83
|
+
case "failed": return typeof value["error"] === "string";
|
|
84
|
+
case "checkpointed": return typeof value["reason"] === "string" && (typeof value["pendingFrames"] === "number" || value["pendingFrames"] === null) && typeof value["walFrames"] === "number" && typeof value["checkpointedFrames"] === "number" && typeof value["busy"] === "boolean" && typeof value["ms"] === "number";
|
|
85
|
+
default: return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isWorkerStop(value) {
|
|
89
|
+
return isRecord(value) && value["kind"] === "stop";
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { isWorkerStop as a, isWorkerMessage as i, WAL_CHECKPOINT_SLOW_MS as n, parseWalIndexHeader as o, decideCheckpoint as r, WAL_CHECKPOINT_POLL_MS as t };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Longest the worker lets pending frames sit before folding them, in ms. A
|
|
3
|
+
* quiet database (a developer machine, an agent with little to persist) still
|
|
4
|
+
* gets its WAL folded and its pages made durable within a minute.
|
|
5
|
+
*/
|
|
6
|
+
var WAL_CHECKPOINT_MAX_INTERVAL_MS = 6e4;
|
|
7
|
+
/** How often the worker looks at the wal-index, in ms. A 136-byte read. */
|
|
8
|
+
var WAL_CHECKPOINT_POLL_MS = 1e3;
|
|
9
|
+
/**
|
|
10
|
+
* A checkpoint that took this long is reported at WARN with its numbers.
|
|
11
|
+
* Matched to `SQLITE_SLOW_CALL_MS`: it is the same "the disk held us" event
|
|
12
|
+
* the profiler used to file as a slow `set`, now on the thread that paid it.
|
|
13
|
+
*/
|
|
14
|
+
var WAL_CHECKPOINT_SLOW_MS = 1e3;
|
|
15
|
+
/** Should the worker run `wal_checkpoint(PASSIVE)` now? */
|
|
16
|
+
function decideCheckpoint(input) {
|
|
17
|
+
const intervalDue = input.sinceLastRunMs >= WAL_CHECKPOINT_MAX_INTERVAL_MS;
|
|
18
|
+
if (input.mxFrame === null || input.nBackfill === null) return intervalDue ? {
|
|
19
|
+
run: true,
|
|
20
|
+
reason: "unknown-interval",
|
|
21
|
+
pendingFrames: null
|
|
22
|
+
} : {
|
|
23
|
+
run: false,
|
|
24
|
+
reason: "unknown-below-interval",
|
|
25
|
+
pendingFrames: null
|
|
26
|
+
};
|
|
27
|
+
const pendingFrames = Math.max(0, input.mxFrame - input.nBackfill);
|
|
28
|
+
if (pendingFrames === 0) return {
|
|
29
|
+
run: false,
|
|
30
|
+
reason: "nothing-pending",
|
|
31
|
+
pendingFrames
|
|
32
|
+
};
|
|
33
|
+
if (pendingFrames >= 1e3) return {
|
|
34
|
+
run: true,
|
|
35
|
+
reason: "threshold",
|
|
36
|
+
pendingFrames
|
|
37
|
+
};
|
|
38
|
+
if (intervalDue) return {
|
|
39
|
+
run: true,
|
|
40
|
+
reason: "interval",
|
|
41
|
+
pendingFrames
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
run: false,
|
|
45
|
+
reason: "below-threshold",
|
|
46
|
+
pendingFrames
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Byte offsets in the `-shm` file, from `wal.c` ("WAL-index file format"):
|
|
51
|
+
* two copies of the 48-byte `WalIndexHdr` (copy 2 at 48), then `WalCkptInfo`
|
|
52
|
+
* at 96 whose first field is `nBackfill`. `mxFrame` is at byte 16 of the
|
|
53
|
+
* header. Both are u32 in the writer's native byte order — little-endian on
|
|
54
|
+
* every x86-64 and arm64 node this ships to.
|
|
55
|
+
*/
|
|
56
|
+
var WAL_INDEX_MXFRAME_OFFSET = 16;
|
|
57
|
+
var WAL_INDEX_HDR_SIZE = 48;
|
|
58
|
+
var WAL_INDEX_NBACKFILL_OFFSET = 96;
|
|
59
|
+
/**
|
|
60
|
+
* `mxFrame` and `nBackfill` from the first {@link WAL_INDEX_HEADER_READ_BYTES}
|
|
61
|
+
* of the `-shm` file, or `null` when the buffer is short or the two header
|
|
62
|
+
* copies disagree (a writer was updating them — SQLite's own readers retry on
|
|
63
|
+
* exactly that comparison; this caller simply waits for the next poll).
|
|
64
|
+
*/
|
|
65
|
+
function parseWalIndexHeader(buffer) {
|
|
66
|
+
if (buffer.length < 100) return null;
|
|
67
|
+
const copy1 = buffer.subarray(0, WAL_INDEX_HDR_SIZE);
|
|
68
|
+
const copy2 = buffer.subarray(WAL_INDEX_HDR_SIZE, WAL_INDEX_HDR_SIZE * 2);
|
|
69
|
+
if (!copy1.equals(copy2)) return null;
|
|
70
|
+
return {
|
|
71
|
+
mxFrame: buffer.readUInt32LE(WAL_INDEX_MXFRAME_OFFSET),
|
|
72
|
+
nBackfill: buffer.readUInt32LE(WAL_INDEX_NBACKFILL_OFFSET)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function isRecord(value) {
|
|
76
|
+
return typeof value === "object" && value !== null;
|
|
77
|
+
}
|
|
78
|
+
/** Narrow a message off the worker port without a cast. */
|
|
79
|
+
function isWorkerMessage(value) {
|
|
80
|
+
if (!isRecord(value)) return false;
|
|
81
|
+
switch (value["kind"]) {
|
|
82
|
+
case "ready": return true;
|
|
83
|
+
case "failed": return typeof value["error"] === "string";
|
|
84
|
+
case "checkpointed": return typeof value["reason"] === "string" && (typeof value["pendingFrames"] === "number" || value["pendingFrames"] === null) && typeof value["walFrames"] === "number" && typeof value["checkpointedFrames"] === "number" && typeof value["busy"] === "boolean" && typeof value["ms"] === "number";
|
|
85
|
+
default: return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isWorkerStop(value) {
|
|
89
|
+
return isRecord(value) && value["kind"] === "stop";
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
Object.defineProperty(exports, "WAL_CHECKPOINT_POLL_MS", {
|
|
93
|
+
enumerable: true,
|
|
94
|
+
get: function() {
|
|
95
|
+
return WAL_CHECKPOINT_POLL_MS;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
Object.defineProperty(exports, "WAL_CHECKPOINT_SLOW_MS", {
|
|
99
|
+
enumerable: true,
|
|
100
|
+
get: function() {
|
|
101
|
+
return WAL_CHECKPOINT_SLOW_MS;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(exports, "decideCheckpoint", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
get: function() {
|
|
107
|
+
return decideCheckpoint;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
Object.defineProperty(exports, "isWorkerMessage", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
get: function() {
|
|
113
|
+
return isWorkerMessage;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
Object.defineProperty(exports, "isWorkerStop", {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
get: function() {
|
|
119
|
+
return isWorkerStop;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
Object.defineProperty(exports, "parseWalIndexHeader", {
|
|
123
|
+
enumerable: true,
|
|
124
|
+
get: function() {
|
|
125
|
+
return parseWalIndexHeader;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
+
const require_wal_checkpoint_policy = require("./wal-checkpoint-policy-CCJZngds.js");
|
|
3
|
+
let node_fs = require("node:fs");
|
|
4
|
+
let better_sqlite3 = require("better-sqlite3");
|
|
5
|
+
better_sqlite3 = require_chunk.__toESM(better_sqlite3);
|
|
6
|
+
let node_worker_threads = require("node:worker_threads");
|
|
7
|
+
//#region src/builtins/sqlite-storage/wal-checkpoint-worker.ts
|
|
8
|
+
/**
|
|
9
|
+
* The checkpoint worker — a thread of `hub/sqlite-settings` whose only job is
|
|
10
|
+
* `PRAGMA wal_checkpoint(PASSIVE)` over its own connection.
|
|
11
|
+
*
|
|
12
|
+
* Why it exists, the measurement, and the rule it follows are in
|
|
13
|
+
* `wal-checkpoint-policy.ts`. What this file adds is the mechanics:
|
|
14
|
+
*
|
|
15
|
+
* - Its OWN connection to the same file. A checkpoint's fsyncs happen on the
|
|
16
|
+
* thread that issues the pragma; issuing it here is what takes them off the
|
|
17
|
+
* settings thread. The connection is sized to do nothing but checkpoint —
|
|
18
|
+
* a 2 MiB page cache and no mmap — so it is not a second copy of the
|
|
19
|
+
* engine's working set.
|
|
20
|
+
* - PASSIVE, never FULL/RESTART/TRUNCATE. PASSIVE takes no writer lock and
|
|
21
|
+
* waits for nobody: it copies what it can and returns. The settings thread's
|
|
22
|
+
* writers and readers are never blocked by this thread. (The idle-gated
|
|
23
|
+
* TRUNCATE in `wal-maintenance.ts` is a different job — reclaiming a
|
|
24
|
+
* sidecar that outgrew its limit — and stays where it is.)
|
|
25
|
+
* - The wal-index header is read as bytes (`-shm`, 100 bytes, no lock) so the
|
|
26
|
+
* poll costs nothing; when it cannot be read the policy falls back to a
|
|
27
|
+
* fixed interval, so nothing here can turn "unreadable" into "nothing to do".
|
|
28
|
+
* - Every checkpoint is reported to the parent with its numbers. The parent
|
|
29
|
+
* decides what is worth a line.
|
|
30
|
+
*
|
|
31
|
+
* A worker entry is a separate bundle (`dist/wal-checkpoint-worker.js`, wired
|
|
32
|
+
* in `vite.config.ts` next to `addon-runner`). It imports nothing from the
|
|
33
|
+
* kernel: it must load in a bare thread with `better-sqlite3` and the policy
|
|
34
|
+
* module, nothing else.
|
|
35
|
+
*/
|
|
36
|
+
function isCheckpointRow(value) {
|
|
37
|
+
if (typeof value !== "object" || value === null) return false;
|
|
38
|
+
if (!("busy" in value) || !("log" in value) || !("checkpointed" in value)) return false;
|
|
39
|
+
return typeof value.busy === "number" && typeof value.log === "number" && typeof value.checkpointed === "number";
|
|
40
|
+
}
|
|
41
|
+
function readCheckpointRow(value) {
|
|
42
|
+
if (isCheckpointRow(value)) return value;
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
const [first] = value;
|
|
45
|
+
if (isCheckpointRow(first)) return first;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function isWorkerData(value) {
|
|
50
|
+
if (typeof value !== "object" || value === null) return false;
|
|
51
|
+
if (!("dbPath" in value) || !("pollMs" in value)) return false;
|
|
52
|
+
return typeof value.dbPath === "string" && typeof value.pollMs === "number";
|
|
53
|
+
}
|
|
54
|
+
/** The two wal-index counters, or nulls when the sidecar cannot be read. */
|
|
55
|
+
function readWalIndex(shmPath) {
|
|
56
|
+
let fd = null;
|
|
57
|
+
try {
|
|
58
|
+
fd = (0, node_fs.openSync)(shmPath, "r");
|
|
59
|
+
const buffer = Buffer.alloc(100);
|
|
60
|
+
if ((0, node_fs.readSync)(fd, buffer, 0, 100, 0) < 100) return {
|
|
61
|
+
mxFrame: null,
|
|
62
|
+
nBackfill: null
|
|
63
|
+
};
|
|
64
|
+
return require_wal_checkpoint_policy.parseWalIndexHeader(buffer) ?? {
|
|
65
|
+
mxFrame: null,
|
|
66
|
+
nBackfill: null
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
return {
|
|
70
|
+
mxFrame: null,
|
|
71
|
+
nBackfill: null
|
|
72
|
+
};
|
|
73
|
+
} finally {
|
|
74
|
+
if (fd !== null) (0, node_fs.closeSync)(fd);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function post(message) {
|
|
78
|
+
node_worker_threads.parentPort?.postMessage(message);
|
|
79
|
+
}
|
|
80
|
+
function run() {
|
|
81
|
+
const port = node_worker_threads.parentPort;
|
|
82
|
+
if (port === null) throw new Error("wal-checkpoint-worker: no parent port");
|
|
83
|
+
if (!isWorkerData(node_worker_threads.workerData)) throw new Error("wal-checkpoint-worker: bad workerData");
|
|
84
|
+
const { dbPath, pollMs } = node_worker_threads.workerData;
|
|
85
|
+
const shmPath = `${dbPath}-shm`;
|
|
86
|
+
const db = new better_sqlite3.default(dbPath);
|
|
87
|
+
db.pragma("cache_size = -2048");
|
|
88
|
+
db.pragma("mmap_size = 0");
|
|
89
|
+
let lastRunAt = Date.now();
|
|
90
|
+
let timer = null;
|
|
91
|
+
let stopping = false;
|
|
92
|
+
const tick = () => {
|
|
93
|
+
if (stopping) return;
|
|
94
|
+
const decision = require_wal_checkpoint_policy.decideCheckpoint({
|
|
95
|
+
...readWalIndex(shmPath),
|
|
96
|
+
sinceLastRunMs: Date.now() - lastRunAt
|
|
97
|
+
});
|
|
98
|
+
if (decision.run) {
|
|
99
|
+
const startedAt = Date.now();
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = db.pragma("wal_checkpoint(PASSIVE)");
|
|
103
|
+
} catch (err) {
|
|
104
|
+
post({
|
|
105
|
+
kind: "failed",
|
|
106
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107
|
+
});
|
|
108
|
+
raw = null;
|
|
109
|
+
}
|
|
110
|
+
lastRunAt = Date.now();
|
|
111
|
+
const row = readCheckpointRow(raw);
|
|
112
|
+
if (row !== null) post({
|
|
113
|
+
kind: "checkpointed",
|
|
114
|
+
reason: decision.reason,
|
|
115
|
+
pendingFrames: decision.pendingFrames,
|
|
116
|
+
walFrames: row.log,
|
|
117
|
+
checkpointedFrames: row.checkpointed,
|
|
118
|
+
busy: row.busy !== 0,
|
|
119
|
+
ms: lastRunAt - startedAt
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
timer = setTimeout(tick, pollMs);
|
|
123
|
+
};
|
|
124
|
+
port.on("message", (message) => {
|
|
125
|
+
if (!require_wal_checkpoint_policy.isWorkerStop(message)) return;
|
|
126
|
+
stopping = true;
|
|
127
|
+
if (timer !== null) clearTimeout(timer);
|
|
128
|
+
try {
|
|
129
|
+
db.close();
|
|
130
|
+
} finally {
|
|
131
|
+
port.close();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
post({ kind: "ready" });
|
|
135
|
+
timer = setTimeout(tick, pollMs);
|
|
136
|
+
}
|
|
137
|
+
run();
|
|
138
|
+
//#endregion
|