@daugasauron/dolly 0.1.4 → 0.1.5
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 +134 -14
- package/coi-serviceworker.js +28 -0
- package/dist/dolly-build-id.mjs +1 -1
- package/dist/dolly-dso-0.wasm +0 -0
- package/dist/dolly-image-build-id.mjs +1 -1
- package/dist/dolly-process-0.wasm +0 -0
- package/dist/dolly-process-abi.mjs +1 -1
- package/dist/dolly-process-dso.mjs +382 -53
- package/dist/dolly-process-gate-0.wasm +0 -0
- package/dist/dolly-process-worker.mjs +164 -35
- package/dist/dolly.wasm +0 -0
- package/host/dso/abi.mjs +3 -1
- package/host/dso/dso.mjs +1 -2
- package/host/http/abi.mjs +2 -2
- package/host/http/broker.mjs +18 -8
- package/host/http/http.mjs +0 -1
- package/host/http/policy.mjs +8 -4
- package/host/input/abi.mjs +47 -0
- package/host/modules.mjs +4 -3
- package/host/nena/abi.mjs +1 -1
- package/host/nena/nena.mjs +16 -18
- package/host/packages/service.mjs +35 -15
- package/host/packages/worker.mjs +21 -0
- package/host/runtime/abi.mjs +1 -0
- package/host/runtime/boot-files.mjs +8 -2
- package/host/runtime/grower.mjs +11 -8
- package/host/runtime/lease-bridge.mjs +8 -5
- package/host/runtime/module.json +1 -1
- package/host/runtime/runtime.mjs +37 -18
- package/host/snapshot/abi.mjs +7 -9
- package/host/snapshot/snapshot.mjs +14 -12
- package/host/snapshot/transport.mjs +4 -12
- package/host/snapshot/ui.mjs +17 -7
- package/host/stack/stack.mjs +1 -2
- package/host/threads/abi.mjs +1 -1
- package/host/threads/threads.mjs +2 -1
- package/package.json +1 -1
- package/src/browser.mjs +34 -37
- package/src/build-log.mjs +13 -2
- package/src/build-worker.mjs +182 -0
- package/src/custom-image.mjs +59 -0
- package/src/dollyfile-graph.mjs +127 -0
- package/src/dollyfile-view.mjs +335 -0
- package/src/image-artifact.mjs +248 -0
- package/src/image-build.mjs +89 -0
- package/src/image-builder.mjs +63 -0
- package/src/images.mjs +13 -2
- package/src/pack-store.mjs +141 -0
- package/src/packaged-snapshot.mjs +263 -91
- package/src/page-chords.mjs +4 -2
- package/src/process-constants.mjs +4 -2
- package/src/process-supervisor.mjs +209 -122
- package/src/runtime-worker.mjs +9 -10
- package/src/session-store.mjs +5 -7
- package/src/sha256.mjs +65 -0
- package/src/snapshot-records.mjs +135 -23
- package/src/static-asset.mjs +29 -2
- package/src/terminal-text.mjs +8 -5
- package/src/version.mjs +1 -1
- package/src/worker-image.mjs +3 -5
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
DOLLY_PROCESS_SPAWN_INTERACTIVE,
|
|
14
14
|
} from "./process-constants.mjs";
|
|
15
15
|
import { hex } from "./static-asset.mjs";
|
|
16
|
+
import { DOLLY_FS_BLOCK_SIZE } from "../host/runtime/abi.mjs";
|
|
16
17
|
|
|
17
18
|
const encoder = new TextEncoder();
|
|
18
19
|
const spawnHeaderSize = DOLLY_PROCESS_SIZEOF.dolly_process_spawn_request;
|
|
@@ -26,23 +27,58 @@ const interruptedSystemCall = -BigInt(DOLLY_ERRNO.EINTR);
|
|
|
26
27
|
// the memory of a 61st process of 8 GiB was refused.
|
|
27
28
|
const processWorkerLimit = 56;
|
|
28
29
|
const processMemoryBudgetBytes = 4 * 1024 * 1024 * 1024;
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// spawn
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// stage is admitted; a signal (Ctrl+C) ends the wait.
|
|
38
|
-
const reservationRetryMilliseconds = 50;
|
|
30
|
+
// A spawn the count refuses waits as long as a Worker asks again for a memory
|
|
31
|
+
// Chromium cannot reserve yet (src/process-worker.mjs), like any blocking
|
|
32
|
+
// call, asked again whenever a process ends and at each service tick; then it
|
|
33
|
+
// fails EAGAIN, a full process table's answer. A spawn the budget refuses
|
|
34
|
+
// waits until a process ends, however long: make and Cargo end a build at a
|
|
35
|
+
// spawn's EAGAIN. So a pipeline started over the budget waits forever if its
|
|
36
|
+
// first stage fills the pipe before the next stage is admitted; a signal
|
|
37
|
+
// (Ctrl+C) ends the wait.
|
|
39
38
|
const reservationPatienceMilliseconds = 5000;
|
|
39
|
+
// A Worker ended mid-copy into its mailbox finishes the copy within
|
|
40
|
+
// microseconds; one that holds the mailbox this long keeps it.
|
|
41
|
+
const mailboxSealMilliseconds = 1000;
|
|
40
42
|
const interruptGraceMilliseconds = 500;
|
|
41
43
|
const compiledModuleCacheEntries = 64;
|
|
42
44
|
const compiledModuleCacheBytes = 256 * 1024 * 1024;
|
|
43
45
|
// File versions known to hold a cached module's bytes (dolly_process_image_version).
|
|
44
46
|
const compiledVersionEntries = 1024;
|
|
45
47
|
const compilationNoticeMilliseconds = 250;
|
|
48
|
+
// A large executable's module is compiled and held in a Worker of its own,
|
|
49
|
+
// which goes once neither the cache nor a process holds the module: a Module
|
|
50
|
+
// held here would go only at this Worker's next collection, which compiled
|
|
51
|
+
// code does not hasten. Four 96 MB executables launched in turn kept five
|
|
52
|
+
// compiled in Firefox, where the cache held two. An idle Worker costs about
|
|
53
|
+
// 2.5 MB, so a small module is compiled and held here.
|
|
54
|
+
const heldModuleBytes = 16 * 1024 * 1024;
|
|
55
|
+
const holderUrl = URL.createObjectURL(new Blob([`let module;
|
|
56
|
+
onmessage = async ({ data: { bytes, port } }) => {
|
|
57
|
+
if (port) return port.postMessage(module);
|
|
58
|
+
try {
|
|
59
|
+
module = await WebAssembly.compile(bytes);
|
|
60
|
+
postMessage(null);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
postMessage(String(error?.message ?? error));
|
|
63
|
+
} finally {
|
|
64
|
+
bytes.transfer(0);
|
|
65
|
+
}
|
|
66
|
+
};`], { type: "text/javascript" }));
|
|
67
|
+
// Each holder until it answers: Firefox ends a Worker that only its own
|
|
68
|
+
// handlers reach once its loop is idle, as after it posts its answer, and
|
|
69
|
+
// that answer is lost.
|
|
70
|
+
const compiling = new Set();
|
|
71
|
+
// The Worker holding the module compiled from BYTES, which it takes.
|
|
72
|
+
function compileHeld(bytes) {
|
|
73
|
+
const holder = new Worker(holderUrl, { name: "dolly-module" });
|
|
74
|
+
compiling.add(holder);
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const fail = message => { holder.terminate(); reject(new Error(message)); };
|
|
77
|
+
holder.onmessage = ({ data }) => data === null ? resolve(holder) : fail(data);
|
|
78
|
+
holder.onerror = event => { event.preventDefault(); fail(event.message || "module Worker failed"); };
|
|
79
|
+
holder.postMessage({ bytes: bytes.buffer }, [bytes.buffer]);
|
|
80
|
+
}).finally(() => compiling.delete(holder));
|
|
81
|
+
}
|
|
46
82
|
const largeInteractiveProcessBytes = 128 * 1024 * 1024;
|
|
47
83
|
const workerReclamationMilliseconds = 500;
|
|
48
84
|
|
|
@@ -126,15 +162,13 @@ function encodeSpawn(arguments_, flags) {
|
|
|
126
162
|
return packet;
|
|
127
163
|
}
|
|
128
164
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
165
|
+
// A process's memory lives in its own Workers alone, never in this one: a
|
|
166
|
+
// shared memory's pages go when the last agent holding it drops it, and a
|
|
167
|
+
// living Worker drops one only when it collects garbage, which a small heap
|
|
168
|
+
// rarely needs (tasks/20261101-102515-retained-process-memory). So each
|
|
169
|
+
// Worker makes or receives the memory, holds the gate and copies its requests
|
|
170
|
+
// into a mailbox of its own in kernel memory, and the memory goes with the
|
|
171
|
+
// Worker. This Worker only names that mailbox to the kernel.
|
|
138
172
|
export class DollyProcessSupervisor {
|
|
139
173
|
constructor(kernel, kernelMemory, gateModule, workerUrl, processContract, hostAbi, serviceHost, threadContract, threadHost, processModules) {
|
|
140
174
|
if (!(kernelMemory instanceof WebAssembly.Memory) ||
|
|
@@ -227,7 +261,7 @@ export class DollyProcessSupervisor {
|
|
|
227
261
|
if (this.processes.size >= processWorkerLimit) return "count";
|
|
228
262
|
let committed = 0, fanning = false;
|
|
229
263
|
for (const process of this.processes.values()) {
|
|
230
|
-
committed += process.
|
|
264
|
+
committed += process.memoryBytes;
|
|
231
265
|
fanning ||= process.parent === parent && !process.retiring;
|
|
232
266
|
}
|
|
233
267
|
return committed < processMemoryBudgetBytes || !fanning ? null : "budget";
|
|
@@ -260,8 +294,8 @@ export class DollyProcessSupervisor {
|
|
|
260
294
|
throw new Error(`kernel supplied invalid process ${pid}`);
|
|
261
295
|
}
|
|
262
296
|
const process = {
|
|
263
|
-
pid, parent, resolve, reject, worker: null,
|
|
264
|
-
|
|
297
|
+
pid, parent, resolve, reject, worker: null, control: null, mailbox: 0,
|
|
298
|
+
memoryBytes: 0, messageHandler: null, errorHandler: null,
|
|
265
299
|
messageErrorHandler: null, started: false,
|
|
266
300
|
failure: null, interactive: (flags & DOLLY_PROCESS_SPAWN_INTERACTIVE) !== 0, retiring: false,
|
|
267
301
|
interruptTimer: null, deadlineTimer: null, retirementTimer: null, lastSignal: 0,
|
|
@@ -317,15 +351,18 @@ export class DollyProcessSupervisor {
|
|
|
317
351
|
}
|
|
318
352
|
|
|
319
353
|
#deliverSignal(process, signalNumber = DOLLY_PROCESS_SIGINT) {
|
|
320
|
-
if (!process || process.retiring || this.processes.get(process.pid) !== process) return
|
|
354
|
+
if (!process || process.retiring || this.processes.get(process.pid) !== process) return;
|
|
321
355
|
const result = this.kernel.dolly_process_signal(process.pid, signalNumber);
|
|
322
356
|
process.lastSignal = signalNumber;
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return this.#forceExit(process.pid, 128 + signalNumber, signalNumber);
|
|
357
|
+
// The kernel decides what the signal does: it may have ended the process.
|
|
358
|
+
if (this.kernel.dolly_process_exited(process.pid) !== 0) {
|
|
359
|
+
this.#retire(process);
|
|
360
|
+
return;
|
|
328
361
|
}
|
|
362
|
+
// A program not yet entered has no call to interrupt; what the kernel
|
|
363
|
+
// keeps for it, it takes when it starts. One that ignores the signal
|
|
364
|
+
// (result 1) has nothing to answer.
|
|
365
|
+
if (result !== 0 || !process.started) return;
|
|
329
366
|
const receiver = process.threaded
|
|
330
367
|
? [...process.threads.values()].sort((a, b) => a.tid - b.tid)[0] : process;
|
|
331
368
|
const deferred = this.deferred.get(receiver);
|
|
@@ -342,7 +379,6 @@ export class DollyProcessSupervisor {
|
|
|
342
379
|
this.#signal(receiver, message.sequence, interruptedSystemCall);
|
|
343
380
|
}
|
|
344
381
|
if (signalNumber !== DOLLY_PROCESS_SIGWINCH) this.#armInterrupt(process, signalNumber);
|
|
345
|
-
return true;
|
|
346
382
|
}
|
|
347
383
|
|
|
348
384
|
// A program that does not finish a delivered signal in time is ended by it.
|
|
@@ -395,15 +431,18 @@ export class DollyProcessSupervisor {
|
|
|
395
431
|
}
|
|
396
432
|
|
|
397
433
|
async #compileProcess(bytes) {
|
|
434
|
+
const byteLength = bytes.byteLength;
|
|
398
435
|
const digest = hex(await crypto.subtle.digest("SHA-256", bytes));
|
|
399
|
-
const key = `${
|
|
436
|
+
const key = `${byteLength}:${digest}`;
|
|
400
437
|
if (this.compiledModules.has(key)) return this.#cachedModule(key);
|
|
438
|
+
// What this module will replace in the cache goes before it is compiled.
|
|
439
|
+
if (byteLength <= compiledModuleCacheBytes) this.#evict(byteLength);
|
|
401
440
|
|
|
402
441
|
const started = performance.now();
|
|
403
442
|
let noticeShown = false;
|
|
404
443
|
const notice = setTimeout(() => {
|
|
405
444
|
noticeShown = true;
|
|
406
|
-
const mebibytes = (
|
|
445
|
+
const mebibytes = (byteLength / (1024 * 1024)).toFixed(1);
|
|
407
446
|
this.#writeTerminal(
|
|
408
447
|
`\r\ndolly: preparing ${mebibytes} MiB WebAssembly executable for this session...\r\n`,
|
|
409
448
|
);
|
|
@@ -422,7 +461,7 @@ export class DollyProcessSupervisor {
|
|
|
422
461
|
checkHostAbi(requirements, this.hostAbi);
|
|
423
462
|
threaded = validateThreadProfile(parsed, requirements, this.threadContract);
|
|
424
463
|
largeStack = parsed.customSections.includes("dolly.process.stack");
|
|
425
|
-
module = await WebAssembly.compile(bytes);
|
|
464
|
+
module = byteLength >= heldModuleBytes ? await compileHeld(bytes) : await WebAssembly.compile(bytes);
|
|
426
465
|
processInterface = { imports: parsed.imports, exports: parsed.exports,
|
|
427
466
|
castEmulationParameters: parsed.castEmulationParameters };
|
|
428
467
|
prepared = true;
|
|
@@ -436,21 +475,35 @@ export class DollyProcessSupervisor {
|
|
|
436
475
|
);
|
|
437
476
|
}
|
|
438
477
|
}
|
|
439
|
-
const compiled = { key, module, memoryRequirements, processInterface, threaded, largeStack, requirements, byteLength
|
|
440
|
-
if (
|
|
478
|
+
const compiled = { key, module, memoryRequirements, processInterface, threaded, largeStack, requirements, byteLength };
|
|
479
|
+
if (byteLength <= compiledModuleCacheBytes) {
|
|
480
|
+
// An exec's compile may have run beside this one.
|
|
481
|
+
this.#evict(byteLength);
|
|
441
482
|
this.compiledModules.set(key, compiled);
|
|
442
|
-
this.compiledModuleBytes +=
|
|
443
|
-
while (this.compiledModules.size > compiledModuleCacheEntries ||
|
|
444
|
-
this.compiledModuleBytes > compiledModuleCacheBytes) {
|
|
445
|
-
const oldestKey = this.compiledModules.keys().next().value;
|
|
446
|
-
const oldest = this.compiledModules.get(oldestKey);
|
|
447
|
-
this.compiledModules.delete(oldestKey);
|
|
448
|
-
this.compiledModuleBytes -= oldest.byteLength;
|
|
449
|
-
}
|
|
483
|
+
this.compiledModuleBytes += byteLength;
|
|
450
484
|
}
|
|
451
485
|
return compiled;
|
|
452
486
|
}
|
|
453
487
|
|
|
488
|
+
// Evicts the least recently used modules until one of byteLength fits.
|
|
489
|
+
#evict(byteLength) {
|
|
490
|
+
while (this.compiledModules.size && (this.compiledModules.size >= compiledModuleCacheEntries ||
|
|
491
|
+
this.compiledModuleBytes + byteLength > compiledModuleCacheBytes)) {
|
|
492
|
+
const [oldestKey, oldest] = this.compiledModules.entries().next().value;
|
|
493
|
+
this.compiledModules.delete(oldestKey);
|
|
494
|
+
this.compiledModuleBytes -= oldest.byteLength;
|
|
495
|
+
this.#release(oldest);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Ends the Worker of a held module (heldModuleBytes) that neither the cache
|
|
500
|
+
// nor a process holds.
|
|
501
|
+
#release(compiled) {
|
|
502
|
+
if (!(compiled.module instanceof Worker) || this.compiledModules.get(compiled.key) === compiled) return;
|
|
503
|
+
for (const process of this.processes.values()) if (process.compiled === compiled) return;
|
|
504
|
+
compiled.module.terminate();
|
|
505
|
+
}
|
|
506
|
+
|
|
454
507
|
#writeTerminal(text) {
|
|
455
508
|
const bytes = encoder.encode(text);
|
|
456
509
|
const address = this.kernel.malloc(BigInt(Math.max(1, bytes.byteLength)));
|
|
@@ -470,42 +523,38 @@ export class DollyProcessSupervisor {
|
|
|
470
523
|
async #launchPending() {
|
|
471
524
|
for (let pid; (pid = this.kernel.dolly_process_next_launch()) !== 0;) {
|
|
472
525
|
const process = this.processes.get(pid);
|
|
526
|
+
let compiled;
|
|
473
527
|
try {
|
|
474
528
|
if (!process) throw new Error(`kernel supplied unregistered process ${pid}`);
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
const local = this.#admit(compiled);
|
|
478
|
-
for (let waited = 0; ; waited += reservationRetryMilliseconds) {
|
|
479
|
-
try {
|
|
480
|
-
this.#start(process, compiled, local);
|
|
481
|
-
break;
|
|
482
|
-
} catch (error) {
|
|
483
|
-
if (!(error instanceof RangeError) || process.memory || waited >= reservationPatienceMilliseconds) throw error;
|
|
484
|
-
}
|
|
485
|
-
await new Promise(resolve => setTimeout(resolve, reservationRetryMilliseconds));
|
|
486
|
-
if (!this.#canLaunch(process)) break;
|
|
487
|
-
}
|
|
529
|
+
compiled = await this.#compiledImage(pid);
|
|
530
|
+
if (this.#canLaunch(process)) this.#start(process, compiled, this.#admit(compiled));
|
|
488
531
|
} catch (error) {
|
|
489
532
|
// A failed launch ends only its own process; exiting it in the kernel
|
|
490
533
|
// also stops next_launch from naming it again.
|
|
491
534
|
if (process) this.#fail(process, error);
|
|
492
535
|
else this.kernel.dolly_process_worker_exited(pid, 126, 0);
|
|
536
|
+
} finally {
|
|
537
|
+
if (compiled) this.#release(compiled);
|
|
493
538
|
}
|
|
494
539
|
}
|
|
495
540
|
}
|
|
496
541
|
|
|
497
|
-
// The bytes of the executable the kernel opened for a process,
|
|
542
|
+
// The bytes of the executable the kernel opened for a process, copied from
|
|
543
|
+
// the file's own blocks (the kernel makes no copy), which it then closes.
|
|
544
|
+
// One copy: Web Crypto rejects views of shared memory.
|
|
498
545
|
#takeImage(pid) {
|
|
499
|
-
const read = this.kernel.dolly_process_image_read(pid);
|
|
500
|
-
if (read !== 0) throw new Error(`cannot read the executable of process ${pid}: ${read}`);
|
|
501
|
-
const address = Number(this.kernel.dolly_process_image_address(pid));
|
|
502
546
|
const size = Number(this.kernel.dolly_process_image_size(pid));
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
throw new Error(`kernel supplied an invalid executable for process ${pid}`);
|
|
506
|
-
}
|
|
547
|
+
if (size < 0) throw new Error(`cannot read the executable of process ${pid}: ${size}`);
|
|
548
|
+
if (!Number.isSafeInteger(size) || size < 8) throw new Error(`kernel supplied an invalid executable for process ${pid}`);
|
|
507
549
|
const bytes = new Uint8Array(size);
|
|
508
|
-
|
|
550
|
+
for (let offset = 0; offset < size; offset += DOLLY_FS_BLOCK_SIZE) {
|
|
551
|
+
const length = Math.min(DOLLY_FS_BLOCK_SIZE, size - offset);
|
|
552
|
+
const address = Number(this.kernel.dolly_process_image_block(pid, BigInt(offset / DOLLY_FS_BLOCK_SIZE)));
|
|
553
|
+
if (!Number.isSafeInteger(address) || address <= 0 || address > this.kernelMemory.buffer.byteLength - length) {
|
|
554
|
+
throw new Error(`kernel supplied an invalid executable for process ${pid}`);
|
|
555
|
+
}
|
|
556
|
+
bytes.set(new Uint8Array(this.kernelMemory.buffer, address, length), offset);
|
|
557
|
+
}
|
|
509
558
|
if (this.kernel.dolly_process_image_consumed(pid) !== 0) {
|
|
510
559
|
throw new Error(`kernel did not release executable ${pid}`);
|
|
511
560
|
}
|
|
@@ -513,26 +562,19 @@ export class DollyProcessSupervisor {
|
|
|
513
562
|
}
|
|
514
563
|
|
|
515
564
|
// Checked at every launch, not only when compiled: nena@0 may have disabled
|
|
516
|
-
// a module since.
|
|
517
|
-
//
|
|
518
|
-
// only where it serves every thread. Returns those modules.
|
|
565
|
+
// a module since. Returns the modules served in this executable's own
|
|
566
|
+
// Workers, each thread's among them (dso@0, stack@0).
|
|
519
567
|
#admit({ requirements, threaded }) {
|
|
520
568
|
checkHostAbi(requirements, this.hostAbi);
|
|
521
569
|
const local = [...requirements.keys()].filter(requirement => this.processModules.has(requirement));
|
|
522
|
-
const unthreaded = local.filter(requirement => !this.processModules.get(requirement).threads);
|
|
523
|
-
if (threaded && unthreaded.length) throw new TypeError(`a program using threads@0 cannot use ${unthreaded.join(", ")}`);
|
|
524
570
|
if (threaded && !this.threadHost) throw new Error("threads@0 is unavailable");
|
|
525
571
|
return local;
|
|
526
572
|
}
|
|
527
573
|
|
|
528
|
-
#start(process,
|
|
529
|
-
const
|
|
530
|
-
process.
|
|
531
|
-
|
|
532
|
-
process: { memory },
|
|
533
|
-
kernel: { memory: this.kernelMemory },
|
|
534
|
-
});
|
|
535
|
-
Object.assign(process, { gate, module, processInterface, threaded, largeStack, local });
|
|
574
|
+
#start(process, compiled, local) {
|
|
575
|
+
const { memoryRequirements, processInterface, threaded, largeStack } = compiled;
|
|
576
|
+
Object.assign(process, { memoryRequirements, memoryBytes: Number(memoryRequirements.initial) * 65536,
|
|
577
|
+
maximumBytes: Number(memoryRequirements.maximum) * 65536, compiled, processInterface, threaded, largeStack, local });
|
|
536
578
|
if (threaded) {
|
|
537
579
|
const tid = this.kernel.dolly_threads_attach(process.pid);
|
|
538
580
|
if (tid <= 0) throw new Error(`kernel could not reserve main thread: ${tid}`);
|
|
@@ -551,10 +593,12 @@ export class DollyProcessSupervisor {
|
|
|
551
593
|
const execution = { thread, message };
|
|
552
594
|
process.execution = execution;
|
|
553
595
|
let compiled, local;
|
|
596
|
+
const unused = () => compiled && this.#release(compiled);
|
|
554
597
|
try {
|
|
555
598
|
compiled = await this.#compiledImage(process.pid);
|
|
556
599
|
if (process.execution === execution) local = this.#admit(compiled);
|
|
557
600
|
} catch (error) {
|
|
601
|
+
unused();
|
|
558
602
|
if (process.execution !== execution) return;
|
|
559
603
|
process.execution = null;
|
|
560
604
|
const line = encoder.encode(`dolly: process ${process.pid} cannot run its new program: ` +
|
|
@@ -564,10 +608,11 @@ export class DollyProcessSupervisor {
|
|
|
564
608
|
this.#signal(thread, message.sequence, -BigInt(DOLLY_ERRNO.ENOEXEC));
|
|
565
609
|
return;
|
|
566
610
|
}
|
|
567
|
-
if (process.execution !== execution) return;
|
|
611
|
+
if (process.execution !== execution) return unused();
|
|
568
612
|
process.execution = null;
|
|
569
613
|
const signalAtStart = this.kernel.dolly_process_exec_commit(process.pid);
|
|
570
614
|
if (signalAtStart < 0) {
|
|
615
|
+
unused();
|
|
571
616
|
this.#fail(process, new Error(`kernel rejected process ${process.pid} exec: ${signalAtStart}`));
|
|
572
617
|
return;
|
|
573
618
|
}
|
|
@@ -577,39 +622,60 @@ export class DollyProcessSupervisor {
|
|
|
577
622
|
const delay = this.#reclamationDeadline(process) - performance.now();
|
|
578
623
|
if (process.interruptTimer !== null) clearTimeout(process.interruptTimer);
|
|
579
624
|
process.interruptTimer = null;
|
|
580
|
-
this.#disposeWorker(process);
|
|
625
|
+
this.#disposeWorker(process, compiled);
|
|
581
626
|
Object.assign(process, { started: false, threaded: false, tid: 0, threads: new Map(), signalAtStart });
|
|
582
627
|
// Its descriptors with CLOEXEC are closed: a reader of their pipes goes on now.
|
|
583
628
|
this.#deliverRaised();
|
|
584
629
|
if (this.kernel.dolly_process_take_wakeup()) this.serviceDeferred();
|
|
585
630
|
const start = () => {
|
|
586
|
-
|
|
587
|
-
|
|
631
|
+
try {
|
|
632
|
+
if (this.#canLaunch(process)) this.#start(process, compiled, local);
|
|
633
|
+
} catch (error) { this.#fail(process, error); } finally { this.#release(compiled); }
|
|
588
634
|
};
|
|
589
635
|
if (delay > 0) setTimeout(start, delay);
|
|
590
636
|
else start();
|
|
591
637
|
}
|
|
592
638
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const
|
|
639
|
+
// port: for a thread, the channel the spawning Worker sends the memory on.
|
|
640
|
+
#launchWorker(process, thread, argument = undefined, port = undefined) {
|
|
641
|
+
const { pid, compiled: { module }, memoryRequirements, processInterface, threaded, local } = process;
|
|
596
642
|
const name = `dolly-process-${pid}-thread-${thread.tid}`;
|
|
597
643
|
const worker = threaded ? this.threadHost.create(pid, this.workerUrl, name)
|
|
598
644
|
: new Worker(this.workerUrl, { type: "module", name });
|
|
599
645
|
if (!worker) throw new Error("thread Worker capacity exhausted");
|
|
646
|
+
// Words: the call sequence, the answered sequence, the result's two
|
|
647
|
+
// halves, the copy word (src/process-worker.mjs copyRequest) and a
|
|
648
|
+
// thread's start word (#disposeThread).
|
|
649
|
+
const control = new SharedArrayBuffer(32);
|
|
650
|
+
thread.mailbox = Number(this.kernel.malloc(BigInt(packetLimit)));
|
|
651
|
+
if (!thread.mailbox) { worker.terminate(); throw new Error("kernel memory for a process mailbox"); }
|
|
652
|
+
if (this.kernel.dolly_process_worker_mailbox(pid, thread.tid, BigInt(thread.mailbox)) !== 0) {
|
|
653
|
+
worker.terminate();
|
|
654
|
+
this.kernel.free(BigInt(thread.mailbox));
|
|
655
|
+
thread.mailbox = 0;
|
|
656
|
+
throw new Error(`kernel does not know thread ${thread.tid} of process ${pid}`);
|
|
657
|
+
}
|
|
600
658
|
thread.worker = worker;
|
|
601
659
|
thread.control = new Int32Array(control);
|
|
602
660
|
thread.messageHandler = event => this.#message(process, event.data, thread);
|
|
603
|
-
|
|
661
|
+
// Cancelled, or the browser reports the Worker's uncaught error again in
|
|
662
|
+
// this Worker, whose end it would be (src/browser.mjs).
|
|
663
|
+
thread.errorHandler = event => { event.preventDefault(); this.#fail(process, new Error(event.message || `${name} failed`)); };
|
|
604
664
|
thread.messageErrorHandler = () => this.#fail(process, new Error(`${name} message could not be decoded`));
|
|
605
665
|
worker.addEventListener("message", thread.messageHandler);
|
|
606
666
|
worker.addEventListener("error", thread.errorHandler, { once: true });
|
|
607
667
|
worker.addEventListener("messageerror", thread.messageErrorHandler, { once: true });
|
|
608
668
|
// local: the modules this executable records that are served in its own
|
|
609
669
|
// Worker (host/dso), each as the bundle it imports and its configuration.
|
|
610
|
-
|
|
670
|
+
// A held module comes from its Worker, on a channel of this Worker's own.
|
|
671
|
+
const channel = module instanceof Worker ? new MessageChannel() : null;
|
|
672
|
+
channel && module.postMessage({ port: channel.port1 }, [channel.port1]);
|
|
673
|
+
worker.postMessage({ type: "configure", pid, tid: thread.tid, argument, threaded, control,
|
|
674
|
+
module: channel ? channel.port2 : module,
|
|
675
|
+
memory: memoryRequirements, port, mailbox: thread.mailbox, kernelMemory: this.kernelMemory, gateModule: this.gateModule,
|
|
611
676
|
promisingEntry: this.promisingEntry && process.largeStack, stackDepths: this.stackDepths,
|
|
612
|
-
clockOrigin: performance.timeOrigin, processInterface, local: local.map(requirement => this.processModules.get(requirement)) }
|
|
677
|
+
clockOrigin: performance.timeOrigin, processInterface, local: local.map(requirement => this.processModules.get(requirement)) },
|
|
678
|
+
[port, channel?.port2].filter(Boolean));
|
|
613
679
|
}
|
|
614
680
|
|
|
615
681
|
#canLaunch(process) {
|
|
@@ -653,8 +719,11 @@ export class DollyProcessSupervisor {
|
|
|
653
719
|
this.deferred.delete(thread);
|
|
654
720
|
}
|
|
655
721
|
|
|
722
|
+
// The thread's Worker has copied the request into its mailbox (thread.mailbox)
|
|
723
|
+
// and reads the response from it once answered; what this reads of the
|
|
724
|
+
// mailbox, it reads before it answers.
|
|
656
725
|
#syscall(process, message, retry = false, thread = process) {
|
|
657
|
-
const
|
|
726
|
+
const pending = this.deferred.get(thread);
|
|
658
727
|
this.#clearDeferred(thread);
|
|
659
728
|
const values = [
|
|
660
729
|
message.sequence,
|
|
@@ -663,36 +732,40 @@ export class DollyProcessSupervisor {
|
|
|
663
732
|
message.requestSize,
|
|
664
733
|
message.responseAddress,
|
|
665
734
|
message.responseCapacity,
|
|
735
|
+
message.memoryBytes,
|
|
666
736
|
];
|
|
737
|
+
const spawnsThread = process.threaded && message.operation === DOLLY_THREAD_SPAWN;
|
|
667
738
|
if (message.pid !== process.pid || message.tid !== thread.tid ||
|
|
668
739
|
values.some((value) => !Number.isSafeInteger(value) || value < 0) ||
|
|
669
740
|
message.requestSize > packetLimit || message.responseCapacity > packetLimit ||
|
|
741
|
+
spawnsThread !== (message.port instanceof MessagePort) ||
|
|
670
742
|
Atomics.load(thread.control, 0) !== message.sequence ||
|
|
671
743
|
Atomics.load(thread.control, 1) === message.sequence) {
|
|
672
744
|
this.#fail(process, new Error(`process ${process.pid} sent an invalid syscall`));
|
|
673
745
|
return;
|
|
674
746
|
}
|
|
747
|
+
// The size the budget counts: a threaded process keeps the one it reported
|
|
748
|
+
// before its memory took its maximum (src/process-worker.mjs).
|
|
749
|
+
if (!(process.threaded && message.memoryBytes >= process.maximumBytes)) process.memoryBytes = message.memoryBytes;
|
|
675
750
|
let result;
|
|
676
751
|
let signalDelivery;
|
|
677
752
|
let threadArgument;
|
|
753
|
+
let portTaken = false;
|
|
678
754
|
try {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
BigInt(message.requestSize),
|
|
683
|
-
);
|
|
755
|
+
// A deferred call is retried against the request still in the mailbox:
|
|
756
|
+
// a handler that defers has written nothing (src/process-kernel.h).
|
|
757
|
+
const mailbox = () => new DataView(this.kernelMemory.buffer, thread.mailbox, 8);
|
|
684
758
|
if (message.operation === DOLLY_THREAD_SPAWN && message.requestSize === 8)
|
|
685
|
-
threadArgument =
|
|
759
|
+
threadArgument = mailbox().getBigUint64(0, true);
|
|
686
760
|
const args = [process.pid, message.operation, BigInt(message.requestSize), BigInt(message.responseCapacity)];
|
|
687
761
|
if (process.threaded) args.splice(1, 0, thread.tid);
|
|
688
762
|
const waiting = message.operation === DOLLY_PROCESS_SPAWN ? this.#spawnWait(process.pid) : null;
|
|
689
|
-
const now = performance.now(), since = waiting === "count" ? refusedSince ?? now : undefined;
|
|
763
|
+
const now = performance.now(), since = waiting === "count" ? pending?.refusedSince ?? now : undefined;
|
|
690
764
|
if (waiting === "budget" || waiting === "count" && now - since < reservationPatienceMilliseconds) {
|
|
691
765
|
this.deferred.set(thread, { process, thread, message, timer: null, refusedSince: since });
|
|
692
766
|
return;
|
|
693
767
|
}
|
|
694
|
-
const exhausted = waiting === "count" ||
|
|
695
|
-
process.threaded && message.operation === DOLLY_THREAD_SPAWN && !this.threadHost?.available(process.pid);
|
|
768
|
+
const exhausted = waiting === "count" || spawnsThread && !this.threadHost?.available(process.pid);
|
|
696
769
|
// Firefox can enter a direct Wasm call twice. Use the generic call path
|
|
697
770
|
// so a completed syscall is never replayed against its response packet.
|
|
698
771
|
result = exhausted ? -BigInt(DOLLY_ERRNO.EAGAIN)
|
|
@@ -723,20 +796,14 @@ export class DollyProcessSupervisor {
|
|
|
723
796
|
if (result > BigInt(message.responseCapacity)) {
|
|
724
797
|
throw new Error(`kernel overfilled process ${process.pid} response`);
|
|
725
798
|
}
|
|
726
|
-
process.gate.exports.response(
|
|
727
|
-
BigInt(this.mailboxAddress),
|
|
728
|
-
BigInt(message.responseAddress),
|
|
729
|
-
result,
|
|
730
|
-
);
|
|
731
799
|
if (message.operation === DOLLY_THREAD_SPAWN) {
|
|
732
800
|
if (result !== 8n || threadArgument === undefined) throw new Error("invalid kernel thread response");
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
if (!tid || tid > 0x7fffffff || response.getUint32(4, true) || process.threads.has(tid))
|
|
801
|
+
const tid = mailbox().getUint32(0, true);
|
|
802
|
+
if (!tid || tid > 0x7fffffff || mailbox().getUint32(4, true) || process.threads.has(tid))
|
|
736
803
|
throw new Error("invalid kernel thread identity");
|
|
737
|
-
const child = { tid, worker: null, control: null };
|
|
804
|
+
const child = { tid, worker: null, control: null, mailbox: 0 };
|
|
738
805
|
process.threads.set(tid, child);
|
|
739
|
-
try { this.#launchWorker(process, child, threadArgument); }
|
|
806
|
+
try { this.#launchWorker(process, child, threadArgument, message.port); portTaken = true; }
|
|
740
807
|
catch (error) {
|
|
741
808
|
this.#disposeThread(process, child);
|
|
742
809
|
this.kernel.dolly_threads_unstarted(process.pid, tid);
|
|
@@ -745,33 +812,32 @@ export class DollyProcessSupervisor {
|
|
|
745
812
|
}
|
|
746
813
|
if (message.operation === DOLLY_PROCESS_SPAWN) {
|
|
747
814
|
if (result !== 8n) throw new Error("invalid kernel spawn response");
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
if (pid === 0 || pid > 0x7fffffff || response.getUint32(4, true) !== 0) {
|
|
815
|
+
const pid = mailbox().getUint32(0, true);
|
|
816
|
+
if (pid === 0 || pid > 0x7fffffff || mailbox().getUint32(4, true) !== 0) {
|
|
751
817
|
throw new Error("invalid kernel spawn identity");
|
|
752
818
|
}
|
|
753
819
|
this.#registerProcess(pid, process.pid);
|
|
754
820
|
}
|
|
755
821
|
if (message.operation === DOLLY_PROCESS_SIGNAL) {
|
|
756
822
|
if (result !== 8n) throw new Error("invalid kernel signal response");
|
|
757
|
-
|
|
758
|
-
signalDelivery = { pid: response.getUint32(0, true), signal: response.getUint32(4, true) };
|
|
823
|
+
signalDelivery = { pid: mailbox().getUint32(0, true), signal: mailbox().getUint32(4, true) };
|
|
759
824
|
if (signalDelivery.pid === 0 || signalDelivery.pid > 0x7fffffff) {
|
|
760
825
|
throw new Error("invalid kernel signal target");
|
|
761
826
|
}
|
|
762
827
|
}
|
|
828
|
+
if (message.operation === DOLLY_PROCESS_SIGNAL_ACKNOWLEDGE && result === 4n &&
|
|
829
|
+
mailbox().getInt32(0, true) === 0 && process.interruptTimer !== null) {
|
|
830
|
+
clearTimeout(process.interruptTimer);
|
|
831
|
+
process.interruptTimer = null;
|
|
832
|
+
}
|
|
763
833
|
}
|
|
764
834
|
} catch (error) {
|
|
835
|
+
message.port?.close();
|
|
765
836
|
this.#fail(process, error);
|
|
766
837
|
return;
|
|
767
838
|
}
|
|
839
|
+
if (!portTaken) message.port?.close();
|
|
768
840
|
this.#signal(thread, message.sequence, result);
|
|
769
|
-
if (message.operation === DOLLY_PROCESS_SIGNAL_ACKNOWLEDGE && result === 4n &&
|
|
770
|
-
new DataView(this.kernelMemory.buffer, this.mailboxAddress, 4).getInt32(0, true) === 0 &&
|
|
771
|
-
process.interruptTimer !== null) {
|
|
772
|
-
clearTimeout(process.interruptTimer);
|
|
773
|
-
process.interruptTimer = null;
|
|
774
|
-
}
|
|
775
841
|
if (signalDelivery?.signal) {
|
|
776
842
|
this.#deliverSignal(this.processes.get(signalDelivery.pid), signalDelivery.signal);
|
|
777
843
|
}
|
|
@@ -826,7 +892,7 @@ export class DollyProcessSupervisor {
|
|
|
826
892
|
|
|
827
893
|
#reclamationDeadline(process) {
|
|
828
894
|
return Math.max(process.reclamationDeadline,
|
|
829
|
-
process.interactive &&
|
|
895
|
+
process.interactive && process.memoryBytes >= largeInteractiveProcessBytes
|
|
830
896
|
? performance.now() + workerReclamationMilliseconds : 0);
|
|
831
897
|
}
|
|
832
898
|
|
|
@@ -949,10 +1015,27 @@ export class DollyProcessSupervisor {
|
|
|
949
1015
|
// for two seconds. Answered with the deferred result, which no program
|
|
950
1016
|
// is handed, a waiting Worker leaves Wasm and closes itself.
|
|
951
1017
|
if (thread.control) this.#signal(thread, Atomics.load(thread.control, 0), deferredResult);
|
|
1018
|
+
// Chrome keeps what was posted to a Worker terminated before it runs, the
|
|
1019
|
+
// memory a thread's Worker is sent among it, until this Worker collects
|
|
1020
|
+
// garbage. One that has not started (control word 5 still 0) is told so
|
|
1021
|
+
// and ends itself (src/process-worker.mjs processMemory).
|
|
1022
|
+
const unstarted = thread !== process && Atomics.compareExchange(thread.control, 5, 0, 2) === 0;
|
|
1023
|
+
if (unstarted) Atomics.notify(thread.control, 5);
|
|
952
1024
|
// threads@0 disabled since has already terminated its Workers.
|
|
953
|
-
if (process.threaded && this.threadHost) this.threadHost.release(thread.worker);
|
|
1025
|
+
if (process.threaded && this.threadHost) this.threadHost.release(thread.worker, !unstarted);
|
|
954
1026
|
else thread.worker.terminate();
|
|
955
1027
|
}
|
|
1028
|
+
// The Worker copies a request into its mailbox under control word 4
|
|
1029
|
+
// (src/process-worker.mjs copyRequest); sealed, it copies no more, and the
|
|
1030
|
+
// mailbox goes back to the kernel. Termination has no completion event: a
|
|
1031
|
+
// Worker ended inside a copy finishes it first.
|
|
1032
|
+
if (thread.mailbox) {
|
|
1033
|
+
this.kernel.dolly_process_worker_mailbox(process.pid, thread.tid, 0n);
|
|
1034
|
+
while (Atomics.compareExchange(thread.control, 4, 0, 2) === 1 &&
|
|
1035
|
+
Atomics.wait(thread.control, 4, 1, mailboxSealMilliseconds) !== "timed-out");
|
|
1036
|
+
if (Atomics.load(thread.control, 4) === 2) this.kernel.free(BigInt(thread.mailbox));
|
|
1037
|
+
}
|
|
1038
|
+
thread.mailbox = 0;
|
|
956
1039
|
thread.worker = null;
|
|
957
1040
|
thread.control = null;
|
|
958
1041
|
thread.messageHandler = thread.errorHandler = thread.messageErrorHandler = null;
|
|
@@ -967,10 +1050,13 @@ export class DollyProcessSupervisor {
|
|
|
967
1050
|
this.#disposeWorker(process);
|
|
968
1051
|
}
|
|
969
1052
|
|
|
970
|
-
|
|
1053
|
+
// next: the module an exec starts next, which stays even if it is this one.
|
|
1054
|
+
#disposeWorker(process, next = null) {
|
|
971
1055
|
for (const thread of [...process.threads.values()]) this.#disposeThread(process, thread);
|
|
972
1056
|
this.#disposeThread(process, process);
|
|
973
|
-
|
|
1057
|
+
const { compiled } = process;
|
|
1058
|
+
process.compiled = process.processInterface = null;
|
|
1059
|
+
if (compiled && compiled !== next) this.#release(compiled);
|
|
974
1060
|
}
|
|
975
1061
|
|
|
976
1062
|
serviceDeferred() {
|
|
@@ -993,6 +1079,7 @@ export class DollyProcessSupervisor {
|
|
|
993
1079
|
}
|
|
994
1080
|
this.processes.clear();
|
|
995
1081
|
this.deferred.clear();
|
|
1082
|
+
for (const compiled of this.compiledModules.values()) if (compiled.module instanceof Worker) compiled.module.terminate();
|
|
996
1083
|
this.compiledModules.clear();
|
|
997
1084
|
this.compiledModuleBytes = 0;
|
|
998
1085
|
this.compiledVersions.clear();
|
package/src/runtime-worker.mjs
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
// The runtime Worker boot() starts: it restores an image of the image directory
|
|
2
2
|
// from its packs into a new kernel and runs its ENTRY (worker-image.mjs).
|
|
3
|
-
import {
|
|
4
|
-
import { loadPackagedSnapshotMetadata, streamPackagedSystemSnapshot } from "./packaged-snapshot.mjs";
|
|
3
|
+
import { imageDefinition } from "./images.mjs";
|
|
4
|
+
import { loadPackagedSnapshotMetadata, streamPackagedSystemSnapshot, verifyVisibleRecipes } from "./packaged-snapshot.mjs";
|
|
5
5
|
import { siteReference, hex } from "./static-asset.mjs";
|
|
6
6
|
import { checkedMemoryRange, runImage } from "./worker-image.mjs";
|
|
7
7
|
|
|
8
|
-
await runImage(async ({ image,
|
|
9
|
-
const definition =
|
|
10
|
-
if (!definition
|
|
11
|
-
const metadata = await loadPackagedSnapshotMetadata(image);
|
|
12
|
-
|
|
13
|
-
host.
|
|
14
|
-
host.admit(declared);
|
|
8
|
+
await runImage(async ({ image, files: given = {} }, host) => {
|
|
9
|
+
const definition = imageDefinition(image);
|
|
10
|
+
if (!definition) throw new Error("invalid Dolly image selection");
|
|
11
|
+
const [metadata] = await Promise.all([loadPackagedSnapshotMetadata(image), verifyVisibleRecipes(image)]);
|
|
12
|
+
host.require(definition.hostRequirements);
|
|
13
|
+
host.admit(definition.hostRequirements);
|
|
15
14
|
return {
|
|
16
15
|
name: image,
|
|
17
16
|
prepare({ files }) {
|
|
@@ -31,7 +30,7 @@ await runImage(async ({ image, mode, files: given = {} }, host) => {
|
|
|
31
30
|
if (kernel.dolly_bootstrap_snapshot_begin(BigInt(metadata.byteLength)) !== 0)
|
|
32
31
|
throw new Error("Dolly snapshot stream initialization failed");
|
|
33
32
|
let loaded = 0, reported = 0;
|
|
34
|
-
await streamPackagedSystemSnapshot(
|
|
33
|
+
await streamPackagedSystemSnapshot(metadata, bytes => {
|
|
35
34
|
for (let offset = 0; offset < bytes.length; offset += capacity) {
|
|
36
35
|
const chunk = bytes.subarray(offset, offset + capacity);
|
|
37
36
|
staging().set(chunk);
|