@tlockney/cdp-kernel 0.7.2 → 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/browser/kernel.js +203 -41
- package/dist/browser/kernel.js.map +2 -2
- package/dist/browser/target.d.ts +2 -1
- package/dist/browser/target.js +29 -7
- package/dist/browser/worker.js +47 -4
- package/dist/browser/worker.js.map +2 -2
- package/dist/bun-target.d.ts +5 -1
- package/dist/bun-target.js +32 -7
- package/dist/crash-reports.d.ts +47 -0
- package/dist/crash-reports.js +155 -0
- package/dist/kernel-core.d.ts +7 -0
- package/dist/kernel-core.js +212 -35
- package/dist/kernel.js +10 -0
- package/dist/main.js +31 -14
- package/dist/message.d.ts +1 -1
- package/dist/message.js +1 -1
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/target-api.d.ts +11 -0
- package/dist/target.d.ts +9 -2
- package/dist/target.js +66 -12
- package/dist/transform.js +3 -1
- package/dist/usage.d.ts +25 -0
- package/dist/usage.js +197 -0
- package/dist/workerd-target.d.ts +13 -2
- package/dist/workerd-target.js +36 -6
- package/dist/workers-project.js +2 -0
- package/package.json +1 -1
- package/runtime/bootstrap.mjs +65 -7
- package/runtime/browser-worker.mjs +6 -2
- package/runtime/display.d.mts +24 -2
- package/runtime/display.mjs +42 -1
- package/runtime/evaluator.d.mts +5 -1
- package/runtime/evaluator.mjs +12 -1
- package/runtime/workerd-worker.mjs +6 -2
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
206
|
+
console.error(USAGE);
|
|
190
207
|
process.exit(2);
|
|
191
208
|
}
|
package/dist/message.d.ts
CHANGED
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.
|
|
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.
|
|
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-11-
|
|
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.
|
|
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-11-
|
|
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
|
package/dist/target-api.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/usage.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/workerd-target.d.ts
CHANGED
|
@@ -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[];
|
|
@@ -59,6 +60,12 @@ export interface WorkerHost {
|
|
|
59
60
|
readonly project?: string;
|
|
60
61
|
/** The open session with the driver. */
|
|
61
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[];
|
|
62
69
|
/** Stops the runtime; `now` even while a cell is busy. */
|
|
63
70
|
stop(now: boolean): void;
|
|
64
71
|
onExit(listener: (code: number | null) => void): void;
|
|
@@ -74,6 +81,7 @@ export declare class RawWorkerd implements WorkerHost {
|
|
|
74
81
|
#private;
|
|
75
82
|
readonly runtime: string;
|
|
76
83
|
readonly channel: WsChannel;
|
|
84
|
+
get pids(): readonly number[];
|
|
77
85
|
private constructor();
|
|
78
86
|
static start(cwd: string, executable?: string): Promise<RawWorkerd>;
|
|
79
87
|
stop(now: boolean): void;
|
|
@@ -113,8 +121,9 @@ export declare class WorkerdTarget implements KernelTarget {
|
|
|
113
121
|
/**
|
|
114
122
|
* A cell that's waiting on something leaves the driver free: it answers a
|
|
115
123
|
* ping, and the cell is abandoned with the session intact (its pending
|
|
116
|
-
* work still runs, as on Bun
|
|
117
|
-
* stopped and restarts on the next
|
|
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.
|
|
118
127
|
*/
|
|
119
128
|
interrupt(): Promise<void>;
|
|
120
129
|
/** Output arrives on the session before the reply it precedes. */
|
|
@@ -142,5 +151,7 @@ export declare class WorkerdTarget implements KernelTarget {
|
|
|
142
151
|
lexicalNames(): Promise<string[]>;
|
|
143
152
|
/** Never asked for: prompt() in workerd answers null without asking. */
|
|
144
153
|
replyInput(): void;
|
|
154
|
+
/** The workerd processes' resident memory and CPU time, from the OS (#410). */
|
|
155
|
+
usage(): Promise<TargetUsage>;
|
|
145
156
|
kill(): void;
|
|
146
157
|
}
|