@tlockney/cdp-kernel 0.7.1 → 0.8.0

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/main.js CHANGED
@@ -135,19 +135,36 @@ function uninstall(names, all) {
135
135
  if (failed)
136
136
  process.exit(1);
137
137
  }
138
- const { positionals, values } = parseArgs({
139
- allowPositionals: true,
140
- options: {
141
- runtime: { type: "string", default: "node" },
142
- conn: { type: "string" },
143
- name: { type: "string" },
144
- display: { type: "string" },
145
- all: { type: "boolean", default: false },
146
- "deno-compat": { type: "boolean", default: false },
147
- "type-strip": { type: "string", default: "esbuild" },
148
- "no-language-service": { type: "boolean", default: false },
149
- },
150
- });
138
+ const USAGE = "usage: cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--type-strip esbuild|sucrase] [--no-language-service] | list | uninstall [NAME...] [--all] | kernel --runtime node|deno|bun|workerd|celld --conn FILE | version";
139
+ /** The command line, or exit 2 with the usage when it doesn't parse (an unknown option, a missing value). */
140
+ function parseCommandLine() {
141
+ try {
142
+ return parseArgs({
143
+ allowPositionals: true,
144
+ options: {
145
+ runtime: { type: "string", default: "node" },
146
+ conn: { type: "string" },
147
+ name: { type: "string" },
148
+ display: { type: "string" },
149
+ all: { type: "boolean", default: false },
150
+ "deno-compat": { type: "boolean", default: false },
151
+ "type-strip": { type: "string", default: "esbuild" },
152
+ "no-language-service": { type: "boolean", default: false },
153
+ help: { type: "boolean", short: "h", default: false },
154
+ },
155
+ });
156
+ }
157
+ catch (err) {
158
+ console.error(`cdp-kernel: ${err instanceof Error ? err.message.split(". ")[0] : String(err)}`);
159
+ console.error(USAGE);
160
+ process.exit(2);
161
+ }
162
+ }
163
+ const { positionals, values } = parseCommandLine();
164
+ if (values.help || positionals[0] === "help") {
165
+ console.log(USAGE);
166
+ process.exit(0);
167
+ }
151
168
  const runtime = values.runtime;
152
169
  if (!isRuntime(runtime)) {
153
170
  console.error(`unknown runtime ${runtime}; expected node, deno, bun, workerd or celld`);
@@ -186,6 +203,6 @@ switch (positionals[0]) {
186
203
  console.log(VERSION);
187
204
  break;
188
205
  default:
189
- console.error("usage: cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--type-strip esbuild|sucrase] [--no-language-service] | list | uninstall [NAME...] [--all] | kernel --runtime node|deno|bun|workerd|celld --conn FILE | version");
206
+ console.error(USAGE);
190
207
  process.exit(2);
191
208
  }
package/dist/message.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const PROTOCOL_VERSION = "5.3";
1
+ export declare const PROTOCOL_VERSION = "5.5";
2
2
  export type JsonObject = {
3
3
  [key: string]: unknown;
4
4
  };
package/dist/message.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Jupyter message shapes and headers, without the wire format: what the
2
2
  // kernel's message handling (src/kernel-core.ts) builds and reads. The
3
3
  // framing and HMAC signing that ZeroMQ peers need are in src/wire.ts.
4
- export const PROTOCOL_VERSION = "5.3";
4
+ export const PROTOCOL_VERSION = "5.5";
5
5
  export function makeHeader(session, msgType) {
6
6
  return {
7
7
  msg_id: crypto.randomUUID(),
package/dist/package.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
2
- export declare const VERSION = "0.7.1";
2
+ export declare const VERSION = "0.8.0";
3
3
  /**
4
4
  * The UTC day this version was released, set with VERSION. The publish
5
5
  * workflow refuses a tag unless it's today or yesterday, so it can't go
6
6
  * stale: see dependencyCutoff.
7
7
  */
8
- export declare const RELEASE_DATE = "2026-10-19";
8
+ export declare const RELEASE_DATE = "2026-11-06";
9
9
  /**
10
10
  * Deno blocks packages less than 24 hours old (minimum dependency age), so
11
11
  * a kernelspec relaunching a release from JSR would fail on release day. It
package/dist/package.js CHANGED
@@ -14,13 +14,13 @@ import { join, sep } from "node:path";
14
14
  import process from "node:process";
15
15
  import { fileURLToPath } from "node:url";
16
16
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
17
- export const VERSION = "0.7.1";
17
+ export const VERSION = "0.8.0";
18
18
  /**
19
19
  * The UTC day this version was released, set with VERSION. The publish
20
20
  * workflow refuses a tag unless it's today or yesterday, so it can't go
21
21
  * stale: see dependencyCutoff.
22
22
  */
23
- export const RELEASE_DATE = "2026-10-19";
23
+ export const RELEASE_DATE = "2026-11-06";
24
24
  /**
25
25
  * Deno blocks packages less than 24 hours old (minimum dependency age), so
26
26
  * a kernelspec relaunching a release from JSR would fail on release day. It
@@ -52,8 +52,19 @@ export interface KernelTarget {
52
52
  diagnose?(): Promise<CdpParams>;
53
53
  /** Lets go of the last cell's result and error, once they are shown (#286). */
54
54
  releaseResults?(): Promise<void>;
55
+ /** The target process's memory and CPU time, for usage_request (#409). */
56
+ usage?(): Promise<TargetUsage>;
55
57
  kill(): void;
56
58
  }
59
+ /** What a target measured of its own process; a field it can't measure is left out (#409). */
60
+ export interface TargetUsage {
61
+ /** Resident memory, in bytes. */
62
+ memory?: number;
63
+ /** CPU time used since it started, in µs. */
64
+ cpuTime?: number;
65
+ /** When it started (ms since the epoch), the start of its first CPU interval. */
66
+ startedAt?: number;
67
+ }
57
68
  /** Messages the bootstrap sends: message records on stdout, or Bun's channel. */
58
69
  export type TargetMessage = {
59
70
  kind: "iopub";
package/dist/target.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
2
2
  import { transformCell } from "./transform.ts";
3
3
  import type { DebugChannel } from "./debugger.ts";
4
- import { type CdpRuntime, Interrupted, isTargetMessage, type KernelTarget, parseBundle, parseJson, type TargetEvents, type TargetMessage } from "./target-api.ts";
4
+ import { type CdpRuntime, Interrupted, isTargetMessage, type KernelTarget, parseBundle, parseJson, type TargetEvents, type TargetMessage, type TargetUsage } from "./target-api.ts";
5
5
  export { Interrupted, isTargetMessage, parseBundle, parseJson };
6
6
  export type { CdpRuntime, KernelTarget, RuntimeName, TargetEvents, TargetMessage } from "./target-api.ts";
7
7
  export interface LaunchOptions {
@@ -89,6 +89,12 @@ export declare class Target implements KernelTarget {
89
89
  }): Promise<Target>;
90
90
  /** The Debugger domain on this target's session, for src/debugger.ts. */
91
91
  get debug(): DebugChannel;
92
+ /**
93
+ * The runtime process's resident memory and CPU time, from the OS (#409).
94
+ * Where the OS can't say, the memory is the V8 heap's size instead,
95
+ * which leaves out native memory such as Buffers; CDP has no CPU time.
96
+ */
97
+ usage(): Promise<TargetUsage>;
92
98
  /**
93
99
  * What the host is waiting on, whether the runtime still evaluates, runs
94
100
  * immediates and runs timers, and where its JavaScript is if it is
@@ -106,7 +112,8 @@ export declare class Target implements KernelTarget {
106
112
  * CDP equivalent is Runtime.terminateExecution, sent on a second session
107
113
  * because the shell session is the one blocked on the running evaluate.
108
114
  * Terminated code never settles the awaited promise, so the pending
109
- * evaluate is abandoned here and the session stays usable.
115
+ * evaluate is abandoned here and the session stays usable; jupyter.signal
116
+ * tells code still waiting on such a promise to stop (#418).
110
117
  */
111
118
  interrupt(): Promise<void>;
112
119
  /**
package/dist/target.js CHANGED
@@ -12,6 +12,7 @@ import { SHIM, transformCell } from "./transform.js";
12
12
  import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
13
13
  import { killOnExit } from "./children.js";
14
14
  import { Interrupted, isTargetMessage, parseBundle, parseJson, withStartTimeout, } from "./target-api.js";
15
+ import { processUsage } from "./usage.js";
15
16
  export { Interrupted, isTargetMessage, parseBundle, parseJson };
16
17
  /**
17
18
  * The inspector keeps every object it returns a remote reference to alive
@@ -330,7 +331,10 @@ export class Target {
330
331
  #messages = Promise.resolve();
331
332
  /** Messages received but not yet delivered. */
332
333
  #undelivered = 0;
333
- #interrupted = false;
334
+ /** Counts interrupt() calls: a call that saw one start during it was interrupted (#417). */
335
+ #interrupts = 0;
336
+ /** Cells running: a comm handler that starts with none gets its own jupyter.signal (#421). */
337
+ #cells = 0;
334
338
  #paused = false;
335
339
  /** What the debugger asked to pause on (#307), and how many kernel calls hold it off. */
336
340
  #pauseOnExceptions = "none";
@@ -338,11 +342,14 @@ export class Target {
338
342
  /** Set once the main context is gone: the runtime is exiting (#130). */
339
343
  #exiting = false;
340
344
  #exited;
345
+ /** When the runtime was spawned: the OS counts its CPU time from then (#409). */
346
+ #startedAt;
341
347
  #baselineGlobals = new Set();
342
348
  versions;
343
- constructor(runtime, child, shell, control, stdout, stderr, gates, contextId, versions) {
349
+ constructor(runtime, child, shell, control, stdout, stderr, gates, contextId, versions, startedAt) {
344
350
  this.runtime = runtime;
345
351
  this.#child = child;
352
+ this.#startedAt = startedAt;
346
353
  this.#exited = new Promise((resolve) => {
347
354
  if (child.exitCode !== null || child.signalCode !== null)
348
355
  resolve();
@@ -376,6 +383,7 @@ export class Target {
376
383
  if (esbuild !== null)
377
384
  env.CDPK_ESBUILD_BINARY = esbuild;
378
385
  delete env.NO_COLOR;
386
+ const startedAt = Date.now();
379
387
  const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
380
388
  killOnExit(child);
381
389
  // Prompt replies and comm messages go to stdin; one written as the
@@ -385,7 +393,7 @@ export class Target {
385
393
  // pipes and inspector sockets would keep this process alive (#52).
386
394
  const sessions = [];
387
395
  try {
388
- return await withStartTimeout(Target.#start(runtime, events, cmd, child, sessions), cmd, options.startTimeoutMs);
396
+ return await withStartTimeout(Target.#start(runtime, events, cmd, child, startedAt, sessions), cmd, options.startTimeoutMs);
389
397
  }
390
398
  catch (error) {
391
399
  for (const session of sessions)
@@ -394,7 +402,7 @@ export class Target {
394
402
  throw error;
395
403
  }
396
404
  }
397
- static async #start(runtime, events, cmd, child, sessions) {
405
+ static async #start(runtime, events, cmd, child, startedAt, sessions) {
398
406
  child.stdout.setEncoding("utf8");
399
407
  child.stderr.setEncoding("utf8");
400
408
  const gates = new Gates();
@@ -452,7 +460,7 @@ export class Target {
452
460
  // messages come over stdout. The control session watches for exit.
453
461
  await shell.send("Runtime.disable");
454
462
  await control.send("Runtime.enable");
455
- target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
463
+ target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" }, startedAt);
456
464
  // process.exit() or Deno.exit() destroys the context, then the runtime
457
465
  // waits for its inspector clients to disconnect before it exits. Let go,
458
466
  // so it exits and the kernel starts a fresh one (#130).
@@ -544,6 +552,26 @@ export class Target {
544
552
  this.#gates.open(msg.barrier);
545
553
  });
546
554
  }
555
+ /**
556
+ * The runtime process's resident memory and CPU time, from the OS (#409).
557
+ * Where the OS can't say, the memory is the V8 heap's size instead,
558
+ * which leaves out native memory such as Buffers; CDP has no CPU time.
559
+ */
560
+ async usage() {
561
+ const pid = this.#child.pid;
562
+ const usage = { startedAt: this.#startedAt, ...(pid === undefined ? {} : await processUsage(pid)) };
563
+ if (usage.memory === undefined) {
564
+ // Over the control session, which no cell holds; a runtime that
565
+ // doesn't answer within a second leaves the memory out.
566
+ const heap = await Promise.race([
567
+ this.#control.send("Runtime.getHeapUsage").catch(() => null),
568
+ new Promise((resolve) => setTimeout(() => resolve(null), 1000).unref()),
569
+ ]);
570
+ if (typeof heap?.totalSize === "number")
571
+ usage.memory = heap.totalSize;
572
+ }
573
+ return usage;
574
+ }
547
575
  /**
548
576
  * What the host is waiting on, whether the runtime still evaluates, runs
549
577
  * immediates and runs timers, and where its JavaScript is if it is
@@ -624,12 +652,13 @@ export class Target {
624
652
  }
625
653
  /** Runs transformed cell code. Rejects with Interrupted after interrupt(). */
626
654
  async evaluate(script) {
627
- this.#interrupted = false;
655
+ const interrupts = this.#interrupts;
656
+ this.#cells++;
628
657
  try {
629
658
  return await this.#evaluateRaw(script);
630
659
  }
631
660
  catch (err) {
632
- if (err instanceof CdpAbandoned || this.#interrupted)
661
+ if (err instanceof CdpAbandoned || this.#interrupts !== interrupts)
633
662
  throw new Interrupted("Execution interrupted");
634
663
  // The control session's executionContextDestroyed event can arrive
635
664
  // after the shell session's error reply (#271 moved it there).
@@ -643,6 +672,9 @@ export class Target {
643
672
  }
644
673
  throw err;
645
674
  }
675
+ finally {
676
+ this.#cells--;
677
+ }
646
678
  }
647
679
  async releaseResults() {
648
680
  if (this.#exiting)
@@ -666,15 +698,35 @@ export class Target {
666
698
  * CDP equivalent is Runtime.terminateExecution, sent on a second session
667
699
  * because the shell session is the one blocked on the running evaluate.
668
700
  * Terminated code never settles the awaited promise, so the pending
669
- * evaluate is abandoned here and the session stays usable.
701
+ * evaluate is abandoned here and the session stays usable; jupyter.signal
702
+ * tells code still waiting on such a promise to stop (#418).
670
703
  */
671
704
  async interrupt() {
672
- this.#interrupted = true;
705
+ this.#interrupts++;
706
+ // Before abandoning the evaluate, which ends the count.
707
+ const cellRunning = this.#cells > 0;
673
708
  // A paused isolate runs nothing to terminate: let it go first (#18).
674
709
  if (this.#paused)
675
710
  await this.#shell.send("Debugger.resume").catch(() => { });
676
711
  await this.#control.send("Runtime.terminateExecution");
677
712
  this.#shell.abandonPending();
713
+ await this.#abortSignal(cellRunning);
714
+ }
715
+ /**
716
+ * Aborts jupyter.signal (#418), once the cell is terminated: cell code
717
+ * waiting on a promise that only it can settle, such as a comm reply,
718
+ * can then stop. A termination still pending would end the first call,
719
+ * so it gets a second.
720
+ */
721
+ async #abortSignal(cellRunning) {
722
+ for (let attempt = 0; attempt < 2; attempt++) {
723
+ const res = await this.callFunctionOn(`function (cell) { globalThis.${SHIM}.abortSignal(cell); return true; }`, [
724
+ { value: cellRunning },
725
+ ])
726
+ .catch(() => ({}));
727
+ if (res.result?.value === true)
728
+ return;
729
+ }
678
730
  }
679
731
  /**
680
732
  * For synchronous functions. With awaitPromise the inspector would wrap
@@ -734,7 +786,7 @@ export class Target {
734
786
  * threw. Rejects with Interrupted after interrupt().
735
787
  */
736
788
  async dispatchComm(msgType, content, metadata, buffers) {
737
- this.#interrupted = false;
789
+ const interrupts = this.#interrupts;
738
790
  // Over stdin, not as call arguments: V8 caches an argument's source,
739
791
  // so the data would stay in the target's memory (#287).
740
792
  const record = Buffer.from(JSON.stringify({
@@ -742,6 +794,7 @@ export class Target {
742
794
  content,
743
795
  metadata,
744
796
  buffers: buffers.map((b) => Buffer.from(b).toString("base64")),
797
+ ownSignal: this.#cells === 0,
745
798
  }));
746
799
  this.#child.stdin.write(`comm ${record.length}\n`);
747
800
  this.#child.stdin.write(record);
@@ -750,7 +803,7 @@ export class Target {
750
803
  res = await this.#callAsyncFunctionOn(`function () { return globalThis.${SHIM}.commDispatchFromStdin(); }`, []);
751
804
  }
752
805
  catch (err) {
753
- if (err instanceof CdpAbandoned || this.#interrupted)
806
+ if (err instanceof CdpAbandoned || this.#interrupts !== interrupts)
754
807
  throw new Interrupted("Comm handler interrupted");
755
808
  throw err;
756
809
  }
@@ -832,7 +885,8 @@ export class Target {
832
885
  return res.names ?? [];
833
886
  }
834
887
  replyInput(value, interrupted = false) {
835
- this.#child.stdin.write(JSON.stringify({ value, interrupted }) + "\n");
888
+ // `cell`: an interrupted prompt aborts a cell's jupyter.signal only if one runs (#421).
889
+ this.#child.stdin.write(JSON.stringify({ value, interrupted, cell: this.#cells > 0 }) + "\n");
836
890
  }
837
891
  kill() {
838
892
  this.#shell.close();
package/dist/transform.js CHANGED
@@ -30,7 +30,9 @@ export const SHIM = "__cdpk";
30
30
  // collected" (#329). Yielding first moves the throw into a task, where
31
31
  // SETTLE's reasoning applies. The debug console uses YIELD for this.
32
32
  export const YIELD = `await ${SHIM}.tick();\n`;
33
- const PRELUDE = `'use strict'; ${YIELD}`;
33
+ // startCell gives the cell its own jupyter.signal (#421) before any of its
34
+ // code runs.
35
+ const PRELUDE = `'use strict'; ${SHIM}.startCell(); ${YIELD}`;
34
36
  /**
35
37
  * Each replMode cell also ends by yielding one macrotask, so the script
36
38
  * settles with no other microtasks queued ahead of the inspector's
@@ -0,0 +1,25 @@
1
+ import type { JsonObject } from "./message.ts";
2
+ import type { KernelTarget } from "./target-api.ts";
3
+ /** A process's resident memory in bytes and its CPU time so far in µs, from the OS. */
4
+ export interface ProcessUsage {
5
+ memory?: number;
6
+ cpuTime?: number;
7
+ }
8
+ /** A process's usage on Linux (/proc) or macOS (`ps`); empty elsewhere or if it can't be read. */
9
+ export declare function processUsage(pid: number): Promise<ProcessUsage>;
10
+ /**
11
+ * Several processes' usage added up, for a target that runs in more than
12
+ * one (Miniflare's workerd). A figure any of them can't report is left out.
13
+ */
14
+ export declare function processesUsage(pids: readonly number[]): Promise<ProcessUsage>;
15
+ /** `ps`'s `time`: [[dd-]hh:]mm:ss[.cc], in seconds; null if it isn't one. */
16
+ export declare function parseCpuTime(text: string): number | null;
17
+ /**
18
+ * Builds usage_reply content. CPU percentages cover the time since the
19
+ * previous request: since the process started, for the first one.
20
+ */
21
+ export declare class UsageMeter {
22
+ #private;
23
+ /** `target` is the running target, or null while none runs. */
24
+ reply(target: KernelTarget | null): Promise<JsonObject>;
25
+ }
package/dist/usage.js ADDED
@@ -0,0 +1,197 @@
1
+ // usage_request (#409): the kernel's memory and CPU, and the host's, in
2
+ // the shape ipykernel replies with. Cells run in the target, a separate
3
+ // process, so the kernel's figures are the host process's plus the
4
+ // target's. A field this platform or target can't measure is left out.
5
+ import { execFile } from "node:child_process";
6
+ import { readFile } from "node:fs/promises";
7
+ import os from "node:os";
8
+ import process from "node:process";
9
+ /**
10
+ * Linux reports CPU times in clock ticks of USER_HZ, which the kernel's
11
+ * ABI fixes at 100 on the architectures Node and Deno run on.
12
+ */
13
+ const USER_HZ = 100;
14
+ /** How long `ps` may take: Ganymede waits 2 s for a first usage_reply (tlockney/ganymede#364). */
15
+ const PS_TIMEOUT_MS = 1_000;
16
+ /** A process's usage on Linux (/proc) or macOS (`ps`); empty elsewhere or if it can't be read. */
17
+ export async function processUsage(pid) {
18
+ try {
19
+ if (process.platform === "linux")
20
+ return await procUsage(pid);
21
+ if (process.platform === "darwin")
22
+ return await psUsage(pid);
23
+ }
24
+ catch {
25
+ // Gone, or not ours to read.
26
+ }
27
+ return {};
28
+ }
29
+ async function procUsage(pid) {
30
+ const [stat, status] = await Promise.all([
31
+ readFile(`/proc/${pid}/stat`, "utf8"),
32
+ readFile(`/proc/${pid}/status`, "utf8"),
33
+ ]);
34
+ const usage = {};
35
+ // Fields after the command name, which may itself hold spaces and ")":
36
+ // the third field (state) is fields[0], so utime and stime (14, 15) are 11, 12.
37
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
38
+ const ticks = Number(fields[11]) + Number(fields[12]);
39
+ if (Number.isFinite(ticks))
40
+ usage.cpuTime = ticks * (1_000_000 / USER_HZ);
41
+ const rss = /^VmRSS:\s+(\d+) kB$/m.exec(status);
42
+ if (rss !== null)
43
+ usage.memory = Number(rss[1]) * 1024;
44
+ return usage;
45
+ }
46
+ async function psUsage(pid) {
47
+ const out = await new Promise((resolve, reject) => {
48
+ execFile("ps", ["-o", "rss=,time=", "-p", String(pid)], { timeout: PS_TIMEOUT_MS }, (err, stdout) => {
49
+ if (err)
50
+ reject(err);
51
+ else
52
+ resolve(stdout);
53
+ });
54
+ });
55
+ const [rss, time] = out.trim().split(/\s+/);
56
+ const usage = {};
57
+ if (rss !== undefined && /^\d+$/.test(rss))
58
+ usage.memory = Number(rss) * 1024;
59
+ const seconds = time === undefined ? null : parseCpuTime(time);
60
+ if (seconds !== null)
61
+ usage.cpuTime = Math.round(seconds * 1_000_000);
62
+ return usage;
63
+ }
64
+ /**
65
+ * Several processes' usage added up, for a target that runs in more than
66
+ * one (Miniflare's workerd). A figure any of them can't report is left out.
67
+ */
68
+ export async function processesUsage(pids) {
69
+ const each = await Promise.all(pids.map((pid) => processUsage(pid)));
70
+ const sum = (key) => each.length > 0 && each.every((u) => u[key] !== undefined) ? each.reduce((n, u) => n + u[key], 0) : undefined;
71
+ const usage = {};
72
+ const memory = sum("memory");
73
+ const cpuTime = sum("cpuTime");
74
+ if (memory !== undefined)
75
+ usage.memory = memory;
76
+ if (cpuTime !== undefined)
77
+ usage.cpuTime = cpuTime;
78
+ return usage;
79
+ }
80
+ /** `ps`'s `time`: [[dd-]hh:]mm:ss[.cc], in seconds; null if it isn't one. */
81
+ export function parseCpuTime(text) {
82
+ const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/.exec(text);
83
+ if (match === null)
84
+ return null;
85
+ const [, days, hours, minutes, seconds] = match;
86
+ return Number(days ?? 0) * 86_400 + Number(hours ?? 0) * 3_600 + Number(minutes) * 60 + Number(seconds);
87
+ }
88
+ /** All CPUs' idle and total time so far, in ms; null if the OS lists none. */
89
+ function hostCpuTimes() {
90
+ const cpus = os.cpus();
91
+ if (cpus.length === 0)
92
+ return null;
93
+ let idle = 0;
94
+ let total = 0;
95
+ for (const { times } of cpus) {
96
+ idle += times.idle;
97
+ total += times.user + times.nice + times.sys + times.idle + times.irq;
98
+ }
99
+ return { idle, total };
100
+ }
101
+ /** psutil.virtual_memory()'s fields that can be read here. */
102
+ async function hostMemory() {
103
+ const total = os.totalmem();
104
+ const memory = { total };
105
+ if (process.platform === "linux") {
106
+ try {
107
+ // MemAvailable is what psutil reports as available; os.freemem()
108
+ // reads the same field, so it is read here alongside MemFree.
109
+ const meminfo = await readFile("/proc/meminfo", "utf8");
110
+ const kib = (name) => {
111
+ const match = new RegExp(`^${name}:\\s+(\\d+) kB$`, "m").exec(meminfo);
112
+ return match === null ? undefined : Number(match[1]) * 1024;
113
+ };
114
+ const available = kib("MemAvailable");
115
+ const free = kib("MemFree");
116
+ if (available !== undefined) {
117
+ memory.available = available;
118
+ memory.percent = Math.round((total - available) / total * 1000) / 10;
119
+ }
120
+ if (free !== undefined)
121
+ memory.free = free;
122
+ }
123
+ catch {
124
+ // No /proc: total alone.
125
+ }
126
+ }
127
+ else {
128
+ // macOS's free pages; psutil's "available" also counts inactive ones,
129
+ // which Node doesn't report, so that is left out.
130
+ memory.free = os.freemem();
131
+ }
132
+ return memory;
133
+ }
134
+ /**
135
+ * Builds usage_reply content. CPU percentages cover the time since the
136
+ * previous request: since the process started, for the first one.
137
+ */
138
+ export class UsageMeter {
139
+ /** The host process's CPU time when last asked, and when that was (ms). */
140
+ #host = { cpuTime: 0, at: Date.now() - process.uptime() * 1000 };
141
+ /** The target's CPU time when last measured, keyed by when it started (not the target, which may be gone). */
142
+ #target = null;
143
+ #hostCpu = hostCpuTimes();
144
+ /** `target` is the running target, or null while none runs. */
145
+ async reply(target) {
146
+ const [targetUsage, virtualMemory] = await Promise.all([
147
+ target?.usage ? target.usage().catch(() => ({})) : null,
148
+ hostMemory(),
149
+ ]);
150
+ const now = Date.now();
151
+ const content = { hostname: os.hostname(), pid: process.pid };
152
+ // The host process, then the target's process: no target running
153
+ // means the kernel is only the host. A target that can't be measured
154
+ // leaves the figure out rather than reporting the host's alone.
155
+ const { user, system } = process.cpuUsage();
156
+ const hostCpu = user + system;
157
+ let cpu = percentOf(hostCpu - this.#host.cpuTime, now - this.#host.at);
158
+ this.#host = { cpuTime: hostCpu, at: now };
159
+ let memory = process.memoryUsage().rss;
160
+ if (target !== null) {
161
+ if (targetUsage?.cpuTime === undefined) {
162
+ // The previous sample stays the baseline, so one missed sample
163
+ // doesn't make the next reply average over the target's life.
164
+ cpu = null;
165
+ }
166
+ else {
167
+ const { startedAt } = targetUsage;
168
+ const last = startedAt !== undefined && this.#target?.startedAt === startedAt
169
+ ? this.#target
170
+ : { cpuTime: 0, at: startedAt ?? now };
171
+ cpu += percentOf(targetUsage.cpuTime - last.cpuTime, now - last.at);
172
+ this.#target = { startedAt, cpuTime: targetUsage.cpuTime, at: now };
173
+ }
174
+ memory = targetUsage?.memory === undefined ? null : memory + targetUsage.memory;
175
+ }
176
+ if (cpu !== null)
177
+ content.kernel_cpu = Math.round(cpu * 10) / 10;
178
+ if (memory !== null)
179
+ content.kernel_memory = memory;
180
+ const times = hostCpuTimes();
181
+ if (times !== null && this.#hostCpu !== null) {
182
+ const total = times.total - this.#hostCpu.total;
183
+ if (total > 0)
184
+ content.host_cpu_percent = Math.round((1 - (times.idle - this.#hostCpu.idle) / total) * 1000) / 10;
185
+ }
186
+ this.#hostCpu = times;
187
+ const cpuCount = os.cpus().length;
188
+ if (cpuCount > 0)
189
+ content.cpu_count = cpuCount;
190
+ content.host_virtual_memory = virtualMemory;
191
+ return content;
192
+ }
193
+ }
194
+ /** CPU time (µs) over wall time (ms), as a percentage of one CPU. */
195
+ function percentOf(cpuMicros, wallMs) {
196
+ return wallMs > 0 ? Math.max(0, cpuMicros / (wallMs * 1000) * 100) : 0;
197
+ }
@@ -1,6 +1,7 @@
1
1
  import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
2
2
  import { transformCellForEval } from "./transform.ts";
3
3
  import { type KernelTarget, type TargetEvents } from "./target.ts";
4
+ import { type TargetUsage } from "./target-api.ts";
4
5
  import { type WsChannel } from "./ws-client.ts";
5
6
  /** The driver Worker's modules; the first is its main module. */
6
7
  export declare const WORKER_MODULES: string[];
@@ -18,6 +19,16 @@ export declare function workerdConfig({ socket, compatibilityDate }: {
18
19
  * otherwise use the one on PATH.
19
20
  */
20
21
  export declare function findWorkerd(cwd: string): string;
22
+ /**
23
+ * The native workerd binary behind `executable`. npm's workerd package
24
+ * installs bin/workerd as a Node script that runs the platform package's
25
+ * binary with execFileSync, and only its postinstall swaps the binary in.
26
+ * When install scripts don't run (the npm that comes with Node 27 blocks
27
+ * those not in allowScripts), killing that script leaves the binary running
28
+ * with the driver socket open, so it's never what the target spawns (#397).
29
+ * Anything that isn't the workerd package's script is returned as given.
30
+ */
31
+ export declare function nativeWorkerd(executable: string): string;
21
32
  /** "workerd 2026-10-15" -> "2026-10-15": the newest date it supports. */
22
33
  export declare function compatibilityDateOf(versionOutput: string): string;
23
34
  /** The literal specifiers a transformed cell imports, in order. */
@@ -49,6 +60,12 @@ export interface WorkerHost {
49
60
  readonly project?: string;
50
61
  /** The open session with the driver. */
51
62
  readonly channel: WsChannel;
63
+ /**
64
+ * The workerd processes that run the driver, for usage_request (#410).
65
+ * Left out where they aren't ours to measure (celld), and the reply
66
+ * then leaves the kernel's memory and CPU out.
67
+ */
68
+ readonly pids?: readonly number[];
52
69
  /** Stops the runtime; `now` even while a cell is busy. */
53
70
  stop(now: boolean): void;
54
71
  onExit(listener: (code: number | null) => void): void;
@@ -64,6 +81,7 @@ export declare class RawWorkerd implements WorkerHost {
64
81
  #private;
65
82
  readonly runtime: string;
66
83
  readonly channel: WsChannel;
84
+ get pids(): readonly number[];
67
85
  private constructor();
68
86
  static start(cwd: string, executable?: string): Promise<RawWorkerd>;
69
87
  stop(now: boolean): void;
@@ -103,8 +121,9 @@ export declare class WorkerdTarget implements KernelTarget {
103
121
  /**
104
122
  * A cell that's waiting on something leaves the driver free: it answers a
105
123
  * ping, and the cell is abandoned with the session intact (its pending
106
- * work still runs, as on Bun). A busy cell can't be reached, so workerd is
107
- * stopped and restarts on the next cell.
124
+ * work still runs, as on Bun, and jupyter.signal tells it to stop). A busy
125
+ * cell can't be reached, so workerd is stopped and restarts on the next
126
+ * cell.
108
127
  */
109
128
  interrupt(): Promise<void>;
110
129
  /** Output arrives on the session before the reply it precedes. */
@@ -132,5 +151,7 @@ export declare class WorkerdTarget implements KernelTarget {
132
151
  lexicalNames(): Promise<string[]>;
133
152
  /** Never asked for: prompt() in workerd answers null without asking. */
134
153
  replyInput(): void;
154
+ /** The workerd processes' resident memory and CPU time, from the OS (#410). */
155
+ usage(): Promise<TargetUsage>;
135
156
  kill(): void;
136
157
  }