@tlockney/cdp-kernel 0.7.2 → 0.9.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/README.md +4 -2
- package/dist/browser/kernel.js +211 -42
- 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/globals.d.ts +35 -4
- package/dist/kernel-core.d.ts +7 -0
- package/dist/kernel-core.js +215 -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.d.ts +10 -1
- package/dist/transform.js +23 -3
- 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/comm.d.mts +87 -1
- package/runtime/display.d.mts +261 -4
- package/runtime/display.mjs +42 -1
- package/runtime/evaluator.d.mts +5 -1
- package/runtime/evaluator.mjs +12 -1
- package/runtime/widget.d.mts +205 -6
- package/runtime/workerd-worker.mjs +6 -2
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.d.ts
CHANGED
|
@@ -51,7 +51,16 @@ export declare function isTypeStripperName(value: unknown): value is TypeStrippe
|
|
|
51
51
|
* imported (#238).
|
|
52
52
|
*/
|
|
53
53
|
export declare function loadTypeStripper(name: TypeStripperName): Promise<TypeStripper>;
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Rewrites module-only syntax in plain JavaScript into script syntax.
|
|
56
|
+
*
|
|
57
|
+
* Top-level `const` becomes `let`, as do import bindings (#444). Under
|
|
58
|
+
* replMode a later evaluation may redeclare a top-level name only with the
|
|
59
|
+
* same kind of declaration: `const a` then `let a`, or `const a` then
|
|
60
|
+
* `import { a }`, fails with "already been declared" until a restart. With
|
|
61
|
+
* one kind, cells can be re-run and edited freely; the cost is that
|
|
62
|
+
* assigning to a top-level `const` from another cell doesn't throw.
|
|
63
|
+
*/
|
|
55
64
|
export declare function rewriteModuleSyntax(js: string): string;
|
|
56
65
|
export declare function transformCell(code: string, options?: CellTransformOptions): string;
|
|
57
66
|
/**
|
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
|
|
@@ -111,7 +113,9 @@ function rewriteImport(code, node, index) {
|
|
|
111
113
|
return `${local} = ${ns}`;
|
|
112
114
|
return `${local} = ${ns}[${JSON.stringify(importedName(spec))}]`;
|
|
113
115
|
});
|
|
114
|
-
|
|
116
|
+
// `let`, like top-level `const` (see toLet): replMode lets a later
|
|
117
|
+
// cell redeclare a name only with the same kind of declaration.
|
|
118
|
+
return `var ${ns} = ${load}; let ${bindings.join(", ")};`;
|
|
115
119
|
}
|
|
116
120
|
function parseCell(js) {
|
|
117
121
|
try {
|
|
@@ -126,10 +130,26 @@ function parseCell(js) {
|
|
|
126
130
|
throw new CellSyntaxError(err instanceof Error ? err.message : String(err));
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Rewrites module-only syntax in plain JavaScript into script syntax.
|
|
135
|
+
*
|
|
136
|
+
* Top-level `const` becomes `let`, as do import bindings (#444). Under
|
|
137
|
+
* replMode a later evaluation may redeclare a top-level name only with the
|
|
138
|
+
* same kind of declaration: `const a` then `let a`, or `const a` then
|
|
139
|
+
* `import { a }`, fails with "already been declared" until a restart. With
|
|
140
|
+
* one kind, cells can be re-run and edited freely; the cost is that
|
|
141
|
+
* assigning to a top-level `const` from another cell doesn't throw.
|
|
142
|
+
*/
|
|
130
143
|
export function rewriteModuleSyntax(js) {
|
|
131
144
|
const program = parseCell(js);
|
|
132
145
|
const edits = [];
|
|
146
|
+
for (const statement of program.body) {
|
|
147
|
+
const decl = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
148
|
+
if (decl?.type === "VariableDeclaration" && decl.kind === "const") {
|
|
149
|
+
// Padded to `const`'s length, so columns stay where the debugger expects them.
|
|
150
|
+
edits.push({ start: decl.start, end: decl.start + "const".length, text: "let " });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
133
153
|
let importIndex = 0;
|
|
134
154
|
full(program, (node) => {
|
|
135
155
|
switch (node.type) {
|
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
|
}
|
package/dist/workerd-target.js
CHANGED
|
@@ -30,6 +30,7 @@ import { runtimeDir } from "./package.js";
|
|
|
30
30
|
import { killOnExit } from "./children.js";
|
|
31
31
|
import { Interrupted, isTargetMessage, parseBundle, parseJson } from "./target.js";
|
|
32
32
|
import { withStartTimeout } from "./target-api.js";
|
|
33
|
+
import { processesUsage } from "./usage.js";
|
|
33
34
|
import { connectUnixWebSocket } from "./ws-client.js";
|
|
34
35
|
import { findWranglerConfig, startMiniflare } from "./workers-project.js";
|
|
35
36
|
import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
|
|
@@ -309,6 +310,9 @@ export class RawWorkerd {
|
|
|
309
310
|
runtime;
|
|
310
311
|
channel;
|
|
311
312
|
#child;
|
|
313
|
+
get pids() {
|
|
314
|
+
return this.#child.pid === undefined ? [] : [this.#child.pid];
|
|
315
|
+
}
|
|
312
316
|
constructor(child, channel, runtime) {
|
|
313
317
|
this.#child = child;
|
|
314
318
|
this.channel = channel;
|
|
@@ -409,9 +413,13 @@ export class WorkerdTarget {
|
|
|
409
413
|
/** Requests that interrupt() abandons when the driver can still answer. */
|
|
410
414
|
#interruptible = new Set();
|
|
411
415
|
#nextId = 0;
|
|
412
|
-
|
|
416
|
+
/** Counts interrupt() calls: a call that saw one start during it was interrupted (#417). */
|
|
417
|
+
#interrupts = 0;
|
|
418
|
+
/** Cells running: a comm handler that starts with none gets its own jupyter.signal (#421). */
|
|
419
|
+
#cells = 0;
|
|
413
420
|
#stopNote;
|
|
414
421
|
#exitedOnce = false;
|
|
422
|
+
#startedAt = Date.now();
|
|
415
423
|
constructor(host, cwd, events, runtime) {
|
|
416
424
|
this.runtime = runtime;
|
|
417
425
|
this.#host = host;
|
|
@@ -530,17 +538,26 @@ export class WorkerdTarget {
|
|
|
530
538
|
});
|
|
531
539
|
}
|
|
532
540
|
async #interruptibly(op, params, what) {
|
|
533
|
-
|
|
541
|
+
const interrupts = this.#interrupts;
|
|
534
542
|
try {
|
|
535
543
|
return await this.#request(op, params, true);
|
|
536
544
|
}
|
|
537
545
|
catch (err) {
|
|
538
|
-
if (err instanceof Abandoned || this.#
|
|
546
|
+
if (err instanceof Abandoned || this.#interrupts !== interrupts)
|
|
539
547
|
throw new Interrupted(`${what} interrupted`);
|
|
540
548
|
throw err;
|
|
541
549
|
}
|
|
542
550
|
}
|
|
543
551
|
async evaluate(script) {
|
|
552
|
+
this.#cells++;
|
|
553
|
+
try {
|
|
554
|
+
return await this.#evaluate(script);
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
this.#cells--;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async #evaluate(script) {
|
|
544
561
|
const modules = {};
|
|
545
562
|
for (const specifier of importSpecifiers(script)) {
|
|
546
563
|
if (BUILTIN.test(specifier) || this.#sent.has(specifier))
|
|
@@ -560,11 +577,14 @@ export class WorkerdTarget {
|
|
|
560
577
|
/**
|
|
561
578
|
* A cell that's waiting on something leaves the driver free: it answers a
|
|
562
579
|
* ping, and the cell is abandoned with the session intact (its pending
|
|
563
|
-
* work still runs, as on Bun
|
|
564
|
-
* stopped and restarts on the next
|
|
580
|
+
* work still runs, as on Bun, and jupyter.signal tells it to stop). A busy
|
|
581
|
+
* cell can't be reached, so workerd is stopped and restarts on the next
|
|
582
|
+
* cell.
|
|
565
583
|
*/
|
|
566
584
|
async interrupt() {
|
|
567
|
-
this.#
|
|
585
|
+
this.#interrupts++;
|
|
586
|
+
// Before abandoning the cell's request, which ends the count.
|
|
587
|
+
const cellRunning = this.#cells > 0;
|
|
568
588
|
const running = [...this.#interruptible];
|
|
569
589
|
const answered = await Promise.race([
|
|
570
590
|
this.#request("ping").then(() => true, () => false),
|
|
@@ -576,6 +596,10 @@ export class WorkerdTarget {
|
|
|
576
596
|
this.#pending.delete(id);
|
|
577
597
|
this.#interruptible.delete(id);
|
|
578
598
|
}
|
|
599
|
+
// After the cell is abandoned, so its reply can't end it first
|
|
600
|
+
// (#418). Not awaited: requests are answered in order, and an abort
|
|
601
|
+
// listener that never returns mustn't hold up the interrupt.
|
|
602
|
+
void this.#request("abort", { cell: cellRunning }).catch(() => { });
|
|
579
603
|
return;
|
|
580
604
|
}
|
|
581
605
|
this.#stopNote = restartNote(this.runtime);
|
|
@@ -596,6 +620,7 @@ export class WorkerdTarget {
|
|
|
596
620
|
content,
|
|
597
621
|
metadata,
|
|
598
622
|
buffers: buffers.map((b) => Buffer.from(b).toString("base64")),
|
|
623
|
+
ownSignal: this.#cells === 0,
|
|
599
624
|
}, "Comm handler");
|
|
600
625
|
if (isRecord(reply.error)) {
|
|
601
626
|
const { name, message, stack } = reply.error;
|
|
@@ -628,6 +653,11 @@ export class WorkerdTarget {
|
|
|
628
653
|
}
|
|
629
654
|
/** Never asked for: prompt() in workerd answers null without asking. */
|
|
630
655
|
replyInput() { }
|
|
656
|
+
/** The workerd processes' resident memory and CPU time, from the OS (#410). */
|
|
657
|
+
async usage() {
|
|
658
|
+
const pids = this.#host.pids ?? [];
|
|
659
|
+
return { startedAt: this.#startedAt, ...(await processesUsage(pids)) };
|
|
660
|
+
}
|
|
631
661
|
kill() {
|
|
632
662
|
// workerd's graceful shutdown waits for open connections, the session
|
|
633
663
|
// included, so close it first.
|
package/dist/workers-project.js
CHANGED
|
@@ -132,8 +132,10 @@ class MiniflareHost {
|
|
|
132
132
|
#exitListeners = [];
|
|
133
133
|
#stopped = false;
|
|
134
134
|
#forget;
|
|
135
|
+
pids;
|
|
135
136
|
constructor(mf, channel, version, project, workerd) {
|
|
136
137
|
this.#mf = mf;
|
|
138
|
+
this.pids = workerd;
|
|
137
139
|
// Miniflare kills its workerd only on a normal exit; the helper in
|
|
138
140
|
// src/children.ts kills it if this process is SIGKILLed (#241).
|
|
139
141
|
this.#forget = workerd.map(reap);
|
package/package.json
CHANGED