@swiftbrowser/web 0.9.0 → 0.9.1
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/node/dist/plugins/playground.d.ts +5 -2
- package/node/dist/plugins/playground.d.ts.map +1 -1
- package/node/dist/plugins/playground.js +29 -11
- package/node/dist/plugins/playground.js.map +1 -1
- package/node/dist/src/playground/files.d.ts +6 -0
- package/node/dist/src/playground/files.d.ts.map +1 -1
- package/node/dist/src/playground/files.js.map +1 -1
- package/node/dist/src/playground/sections.d.ts +5 -0
- package/node/dist/src/playground/sections.d.ts.map +1 -0
- package/node/dist/src/playground/sections.js +88 -0
- package/node/dist/src/playground/sections.js.map +1 -0
- package/package.json +1 -1
- package/plugins/playground.ts +27 -11
- package/src/playground/assets.ts +53 -134
- package/src/playground/build.ts +30 -19
- package/src/playground/download.ts +125 -0
- package/src/playground/files.ts +6 -0
- package/src/playground/freeze.ts +105 -0
- package/src/playground/main.ts +28 -2
- package/src/playground/read-once.ts +75 -0
- package/src/playground/sections.ts +77 -0
- package/src/playground/tool-host.ts +163 -0
- package/src/playground/tool-worker.ts +67 -0
- package/src/playground/worker.ts +41 -17
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Inputs a tool reads whole once, dropped as soon as it has. The linker
|
|
2
|
+
// reads each archive into its own memory and keeps it there, so after that
|
|
3
|
+
// the worker's copy is a second one, a hundred megabytes for an app's
|
|
4
|
+
// archives, at the point where a phone's browser has least to spare.
|
|
5
|
+
|
|
6
|
+
import { File, OpenFile, wasi } from '@bjorn3/browser_wasi_shim';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A read-only file whose bytes go once every open of it is closed and one
|
|
10
|
+
* read all of them. Its size stays. Opening it after that fails
|
|
11
|
+
* (`onReopen` is told), since its bytes are gone: the tool has to run again
|
|
12
|
+
* with them kept.
|
|
13
|
+
*/
|
|
14
|
+
export class ReadOnceFile extends File {
|
|
15
|
+
private readonly length: bigint;
|
|
16
|
+
private open = 0;
|
|
17
|
+
private wholeRead = false;
|
|
18
|
+
released = false;
|
|
19
|
+
|
|
20
|
+
constructor(data: Uint8Array, private readonly onReopen: () => void) {
|
|
21
|
+
// The buffer, not the view, so the File keeps the bytes rather than a copy.
|
|
22
|
+
super(data.byteOffset === 0 && data.byteLength === data.buffer.byteLength ? data.buffer : data, { readonly: true });
|
|
23
|
+
this.length = BigInt(data.byteLength);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override get size(): bigint {
|
|
27
|
+
return this.length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override path_open(oflags: number, fs_rights_base: bigint, fd_flags: number): { ret: number; fd_obj: OpenFile } {
|
|
31
|
+
if (this.released) {
|
|
32
|
+
this.onReopen();
|
|
33
|
+
return { ret: wasi.ERRNO_IO, fd_obj: null as unknown as OpenFile };
|
|
34
|
+
}
|
|
35
|
+
const opened = super.path_open(oflags, fs_rights_base, fd_flags);
|
|
36
|
+
if (opened.ret !== wasi.ERRNO_SUCCESS) return opened;
|
|
37
|
+
this.open += 1;
|
|
38
|
+
return { ret: opened.ret, fd_obj: new ReadOnceOpenFile(this) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** How an open of it ended: whether it read every byte. */
|
|
42
|
+
closed(readAll: boolean): void {
|
|
43
|
+
this.open -= 1;
|
|
44
|
+
this.wholeRead ||= readAll;
|
|
45
|
+
if (this.open === 0 && this.wholeRead) {
|
|
46
|
+
this.data = new Uint8Array(0);
|
|
47
|
+
this.released = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ReadOnceOpenFile extends OpenFile {
|
|
53
|
+
private bytesRead = 0;
|
|
54
|
+
|
|
55
|
+
constructor(private readonly once: ReadOnceFile) {
|
|
56
|
+
super(once);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fd_read(size: number): { ret: number; data: Uint8Array } {
|
|
60
|
+
const result = super.fd_read(size);
|
|
61
|
+
this.bytesRead += result.data.byteLength;
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fd_pread(size: number, offset: bigint): { ret: number; data: Uint8Array } {
|
|
66
|
+
const result = super.fd_pread(size, offset);
|
|
67
|
+
this.bytesRead += result.data.byteLength;
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fd_close(): number {
|
|
72
|
+
this.once.closed(BigInt(this.bytesRead) >= this.once.size);
|
|
73
|
+
return super.fd_close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// A WebAssembly module without its custom sections, as it streams in. A
|
|
2
|
+
// tool's or plugin's run reads none of them, but a browser keeps them beside
|
|
3
|
+
// the compiled code: the function names are a quarter of the compiler and of
|
|
4
|
+
// the linker, and a macro plugin carries its DWARF.
|
|
5
|
+
|
|
6
|
+
/** Passes a module through, dropping each custom section (id 0). */
|
|
7
|
+
export function withoutCustomSections(): TransformStream<Uint8Array, Uint8Array> {
|
|
8
|
+
// The preamble's bytes still to pass, then a section header's bytes so far
|
|
9
|
+
// (its id and LEB128 size), then the section's bytes to pass or to drop.
|
|
10
|
+
let preamble = 8;
|
|
11
|
+
let header: number[] = [];
|
|
12
|
+
let pass = 0;
|
|
13
|
+
let drop = 0;
|
|
14
|
+
return new TransformStream({
|
|
15
|
+
transform(chunk, controller) {
|
|
16
|
+
let at = 0;
|
|
17
|
+
while (at < chunk.length) {
|
|
18
|
+
if (preamble > 0 || pass > 0) {
|
|
19
|
+
const count = Math.min(preamble > 0 ? preamble : pass, chunk.length - at);
|
|
20
|
+
controller.enqueue(chunk.subarray(at, at + count));
|
|
21
|
+
if (preamble > 0) preamble -= count;
|
|
22
|
+
else pass -= count;
|
|
23
|
+
at += count;
|
|
24
|
+
} else if (drop > 0) {
|
|
25
|
+
const count = Math.min(drop, chunk.length - at);
|
|
26
|
+
drop -= count;
|
|
27
|
+
at += count;
|
|
28
|
+
} else {
|
|
29
|
+
const byte = chunk[at++]!;
|
|
30
|
+
header.push(byte);
|
|
31
|
+
if (header.length === 1 || byte & 0x80) {
|
|
32
|
+
if (header.length > 6) throw new Error('a WebAssembly section size is longer than five bytes');
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let size = 0;
|
|
36
|
+
for (let i = header.length - 1; i >= 1; i--) size = size * 128 + (header[i]! & 0x7f);
|
|
37
|
+
if (header[0] === 0) drop = size;
|
|
38
|
+
else {
|
|
39
|
+
controller.enqueue(new Uint8Array(header));
|
|
40
|
+
pass = size;
|
|
41
|
+
}
|
|
42
|
+
header = [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
flush() {
|
|
47
|
+
if (preamble > 0 || pass > 0 || drop > 0 || header.length > 0) throw new Error('a WebAssembly module ended early');
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `module` without its custom sections, at once: what `withoutCustomSections` streams. */
|
|
53
|
+
export function stripCustomSections(module: Uint8Array): Uint8Array {
|
|
54
|
+
const kept: Uint8Array[] = [module.subarray(0, 8)];
|
|
55
|
+
let at = 8;
|
|
56
|
+
while (at < module.length) {
|
|
57
|
+
const start = at;
|
|
58
|
+
const id = module[at++]!;
|
|
59
|
+
let size = 0;
|
|
60
|
+
for (let shift = 0; ; shift += 7) {
|
|
61
|
+
if (at >= module.length) throw new Error('a WebAssembly module ended early');
|
|
62
|
+
const byte = module[at++]!;
|
|
63
|
+
size += (byte & 0x7f) * 2 ** shift;
|
|
64
|
+
if (!(byte & 0x80)) break;
|
|
65
|
+
}
|
|
66
|
+
if (at + size > module.length) throw new Error('a WebAssembly module ended early');
|
|
67
|
+
if (id !== 0) kept.push(module.subarray(start, at + size));
|
|
68
|
+
at += size;
|
|
69
|
+
}
|
|
70
|
+
const out = new Uint8Array(kept.reduce((total, part) => total + part.length, 0));
|
|
71
|
+
let offset = 0;
|
|
72
|
+
for (const part of kept) {
|
|
73
|
+
out.set(part, offset);
|
|
74
|
+
offset += part.length;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Each tool run in a worker of its own, which loads the tool and the SDK's
|
|
2
|
+
// and kit's files it reads from the cache (assets.ts), runs it over an
|
|
3
|
+
// in-memory file system, sends back what it wrote and is ended. A phone's
|
|
4
|
+
// browser runs out of memory where a desktop's does not: this way the
|
|
5
|
+
// compiler's code, its memory and the Swift modules are gone before the
|
|
6
|
+
// linker loads, with its own memory and the archives, and the page's worker
|
|
7
|
+
// holds only the small files. A run compiles its tool again, which a
|
|
8
|
+
// browser does fast enough (a WebAssembly.Module could be sent to the
|
|
9
|
+
// worker instead, but its code would stay as long as the page kept it).
|
|
10
|
+
|
|
11
|
+
import type { Directory } from '@bjorn3/browser_wasi_shim';
|
|
12
|
+
import { type MemoryHost, type RunResult, type ToolName, directoryFiles, memoryDirectory, sdkPaths } from '@swiftbrowser/wasm-compiler';
|
|
13
|
+
import type { Download } from './download';
|
|
14
|
+
|
|
15
|
+
/** What a tool's worker runs. */
|
|
16
|
+
export interface ToolJob {
|
|
17
|
+
/** argv, the program name first. */
|
|
18
|
+
args: string[];
|
|
19
|
+
env: Record<string, string>;
|
|
20
|
+
module: Download;
|
|
21
|
+
/** Guest directory → the files it holds, every mount the tool sees in order. */
|
|
22
|
+
files: Record<string, [string, Uint8Array][]>;
|
|
23
|
+
/** Guest directory → files of it the worker downloads. */
|
|
24
|
+
downloads: Record<string, [string, Download][]>;
|
|
25
|
+
/** The macro plugins, as guest paths of downloads. */
|
|
26
|
+
plugins: string[];
|
|
27
|
+
/** The mounts whose files the worker sends back: those a tool writes to. */
|
|
28
|
+
writable: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Whether the downloads go once the tool has read them (read-once.ts): the
|
|
31
|
+
* linker's, which it keeps whole. Not those named in `keep` (file names).
|
|
32
|
+
*/
|
|
33
|
+
readOnce: boolean;
|
|
34
|
+
keep: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What a tool's worker is sent: a tool to run, or downloads to put in the cache. */
|
|
38
|
+
export type ToToolWorker = { type: 'run'; job: ToolJob } | { type: 'prefetch'; files: Download[] };
|
|
39
|
+
export type ToolReply =
|
|
40
|
+
/** `reopened`: a download the tool opened again after it was dropped (`readOnce`): it has to run again keeping them. */
|
|
41
|
+
| { type: 'done'; result: RunResult; files: Record<string, [string, Uint8Array][]>; reopened?: string }
|
|
42
|
+
/** One of a prefetch's files is in the cache, by its index. */
|
|
43
|
+
| { type: 'fetched'; index: number }
|
|
44
|
+
| { type: 'prefetched' }
|
|
45
|
+
| { type: 'error'; error: string };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a tool reads a file of the SDK or the kit: the linker their
|
|
49
|
+
* archives and objects, the compiler the rest.
|
|
50
|
+
*/
|
|
51
|
+
export const toolReads = (tool: ToolName, file: string): boolean => /\.[ao]$/.test(file) === (tool === 'wasm-ld');
|
|
52
|
+
|
|
53
|
+
export interface ToolHostOptions {
|
|
54
|
+
tools: Partial<Record<ToolName, Download>>;
|
|
55
|
+
/** Guest directory → the files held here. */
|
|
56
|
+
mounts: Record<string, Directory>;
|
|
57
|
+
/** Guest directory → files of it a tool's worker downloads, those it reads (`toolReads`). */
|
|
58
|
+
downloads: Record<string, [string, Download][]>;
|
|
59
|
+
/** Macro plugins by guest path, for the compiler: its worker downloads them too. */
|
|
60
|
+
plugins: Record<string, Download>;
|
|
61
|
+
/** The Swift SDK's `wasm32-unknown-wasip1` directory, as a guest path. */
|
|
62
|
+
sdkRoot: string;
|
|
63
|
+
/** Clang's module cache to start from (memoryHost's). */
|
|
64
|
+
moduleCache?: Iterable<readonly [string, Uint8Array]>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The module cache's directory under the host's /tmp, as memoryHost has it. */
|
|
68
|
+
const MODULE_CACHE = 'module-cache';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Sends `message` to a new tool worker and answers with its last reply (a
|
|
72
|
+
* `done` or `prefetched`), ending the worker then: with it goes whatever it
|
|
73
|
+
* loaded, which no browser frees as surely otherwise.
|
|
74
|
+
*/
|
|
75
|
+
function inWorker<Reply extends ToolReply>(message: ToToolWorker, onReply: (reply: ToolReply) => void = () => {}): Promise<Reply> {
|
|
76
|
+
const worker = new Worker(new URL('./tool-worker.ts', import.meta.url), { type: 'module' });
|
|
77
|
+
const name = message.type === 'run' ? message.job.args[0] : 'the download';
|
|
78
|
+
return new Promise<Reply>((resolve, reject) => {
|
|
79
|
+
worker.onmessage = (event: MessageEvent<ToolReply>) => {
|
|
80
|
+
const reply = event.data;
|
|
81
|
+
if (reply.type === 'error') reject(new Error(reply.error));
|
|
82
|
+
else if (reply.type === 'done' || reply.type === 'prefetched') resolve(reply as Reply);
|
|
83
|
+
else onReply(reply);
|
|
84
|
+
};
|
|
85
|
+
worker.onerror = (event) => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
reject(new Error(`${name}'s worker failed: ${event.message || 'no message'}`));
|
|
88
|
+
};
|
|
89
|
+
worker.postMessage(message);
|
|
90
|
+
}).finally(() => worker.terminate());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Puts `files` in the cache from a worker of their own, calling `onFetched`
|
|
95
|
+
* with each one's index as it is in: the brotli decoder's memory, which only
|
|
96
|
+
* grows, goes with the worker.
|
|
97
|
+
*/
|
|
98
|
+
export async function prefetchInWorker(files: Download[], onFetched: (index: number) => void = () => {}): Promise<void> {
|
|
99
|
+
await inWorker({ type: 'prefetch', files }, (reply) => reply.type === 'fetched' && onFetched(reply.index));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The archives and objects a link names more than once, by file name: the
|
|
104
|
+
* linker reads such a file again each time (the SDK's flags and the
|
|
105
|
+
* objects' autolink entries both name -lwasi-emulated-mman, for one).
|
|
106
|
+
*/
|
|
107
|
+
export function namedTwice(args: readonly string[]): string[] {
|
|
108
|
+
const counts = new Map<string, number>();
|
|
109
|
+
for (const arg of args) {
|
|
110
|
+
const name = arg.startsWith('-l') ? `lib${arg.slice(2)}.a` : /\.[ao]$/.test(arg) ? arg.split('/').pop()! : null;
|
|
111
|
+
if (name) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
112
|
+
}
|
|
113
|
+
return [...counts].filter(([, count]) => count > 1).map(([name]) => name).sort();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A host like memoryHost (wasm-compiler) that runs each tool in its own worker. */
|
|
117
|
+
export function toolHost(options: ToolHostOptions): MemoryHost {
|
|
118
|
+
const tmp = memoryDirectory(options.moduleCache ? [...options.moduleCache].map(([file, data]): [string, Uint8Array] => [`${MODULE_CACHE}/${file}`, data]) : []);
|
|
119
|
+
const plugins = Object.entries(options.plugins);
|
|
120
|
+
const pluginMounts = new Set(plugins.map(([guest]) => `/${guest.split('/')[1]}`));
|
|
121
|
+
const mounts: Record<string, Directory> = { '/': memoryDirectory([]), '/tmp': tmp, ...options.mounts };
|
|
122
|
+
for (const dir of [...Object.keys(options.downloads), ...pluginMounts]) mounts[dir] ??= memoryDirectory([]);
|
|
123
|
+
const downloads: Record<string, [string, Download][]> = { ...options.downloads };
|
|
124
|
+
for (const [guest, file] of plugins) {
|
|
125
|
+
const dir = `/${guest.split('/')[1]}`;
|
|
126
|
+
downloads[dir] = [...(downloads[dir] ?? []), [guest.slice(dir.length + 1), file]];
|
|
127
|
+
}
|
|
128
|
+
const writable = Object.keys(mounts).filter((dir) => !(dir in downloads));
|
|
129
|
+
return {
|
|
130
|
+
description: 'WebAssembly tools, each in a worker, over an in-memory file system',
|
|
131
|
+
sdk: sdkPaths(options.sdkRoot),
|
|
132
|
+
moduleCachePath: `/tmp/${MODULE_CACHE}`,
|
|
133
|
+
moduleCacheFiles() {
|
|
134
|
+
const prefix = `${MODULE_CACHE}/`;
|
|
135
|
+
return directoryFiles(mounts['/tmp']!).filter(([file]) => file.startsWith(prefix)).map(([file, data]) => [file.slice(prefix.length), data]);
|
|
136
|
+
},
|
|
137
|
+
async readFile(file: string): Promise<Uint8Array> {
|
|
138
|
+
const dir = Object.keys(mounts).filter((mount) => file.startsWith(mount === '/' ? '/' : `${mount}/`)).sort((a, b) => b.length - a.length)[0];
|
|
139
|
+
const data = dir === undefined ? undefined : directoryFiles(mounts[dir]!).find(([path]) => `${dir === '/' ? '' : dir}/${path}` === file)?.[1];
|
|
140
|
+
if (!data) throw new Error(`${file}: no such file`);
|
|
141
|
+
return data;
|
|
142
|
+
},
|
|
143
|
+
async run(tool: ToolName, args: readonly string[]): Promise<RunResult> {
|
|
144
|
+
const module = options.tools[tool];
|
|
145
|
+
if (!module) throw new Error(`no WebAssembly module for ${tool}`);
|
|
146
|
+
const job: ToolJob = {
|
|
147
|
+
args: [tool, ...args],
|
|
148
|
+
env: { TMPDIR: '/tmp' },
|
|
149
|
+
module,
|
|
150
|
+
files: Object.fromEntries(Object.entries(mounts).map(([dir, contents]) => [dir, directoryFiles(contents)])),
|
|
151
|
+
downloads: Object.fromEntries(Object.entries(downloads).map(([dir, list]) => [dir, list.filter(([file]) => toolReads(tool, file))])),
|
|
152
|
+
plugins: tool === 'swift-frontend' ? plugins.map(([guest]) => guest) : [],
|
|
153
|
+
writable,
|
|
154
|
+
readOnce: tool === 'wasm-ld',
|
|
155
|
+
keep: namedTwice(args),
|
|
156
|
+
};
|
|
157
|
+
let reply = await inWorker<Extract<ToolReply, { type: 'done' }>>({ type: 'run', job });
|
|
158
|
+
if (reply.reopened) reply = await inWorker<Extract<ToolReply, { type: 'done' }>>({ type: 'run', job: { ...job, readOnce: false } });
|
|
159
|
+
for (const [dir, files] of Object.entries(reply.files)) mounts[dir] = memoryDirectory(files);
|
|
160
|
+
return reply.result;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// A tool's worker (tool-host.ts): loads the tool and the files it reads from
|
|
2
|
+
// the cache, runs it once, sends back the files of the mounts it writes to.
|
|
3
|
+
// Or it puts downloads in the cache. The page's worker ends it then, and with
|
|
4
|
+
// it everything it loaded.
|
|
5
|
+
|
|
6
|
+
import { Directory, File } from '@bjorn3/browser_wasi_shim';
|
|
7
|
+
import { directoryFiles, memoryDirectory, runMemoryCommand } from '@swiftbrowser/wasm-compiler';
|
|
8
|
+
import { CONCURRENCY, compileModule, download, downloadBytes, openCache, pooled, prefetch } from './download';
|
|
9
|
+
import { ReadOnceFile } from './read-once';
|
|
10
|
+
import type { ToToolWorker, ToolJob, ToolReply } from './tool-host';
|
|
11
|
+
|
|
12
|
+
const post = (message: ToolReply, transfer: Transferable[] = []): void => (self as unknown as Worker).postMessage(message, transfer);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The tool's mounts and its plugins compiled, with the downloads in their
|
|
16
|
+
* places; nothing else keeps the downloads' bytes once this returns. With
|
|
17
|
+
* `job.readOnce` the downloads (but `job.keep`) are dropped as soon as the
|
|
18
|
+
* tool has read them (read-once.ts), and `onReopen` hears of one opened
|
|
19
|
+
* again after.
|
|
20
|
+
*/
|
|
21
|
+
async function load(job: ToolJob, cache: Cache | null, onReopen: (path: string) => void): Promise<{ mounts: Record<string, Directory>; plugins: Record<string, WebAssembly.Module> }> {
|
|
22
|
+
const wanted = Object.entries(job.downloads).flatMap(([dir, list]) => list.map(([file, from]) => ({ dir, file, from })));
|
|
23
|
+
const downloaded = await pooled(wanted, CONCURRENCY, async ({ dir, file, from }) => ({ dir, file, data: await downloadBytes(cache, from) }));
|
|
24
|
+
const mounts = Object.fromEntries(Object.entries(job.files).map(([dir, list]) => [dir, memoryDirectory(list)]));
|
|
25
|
+
for (const { dir, file, data } of downloaded) {
|
|
26
|
+
const parts = file.split('/');
|
|
27
|
+
let parent = (mounts[dir] ??= memoryDirectory([]));
|
|
28
|
+
for (const part of parts.slice(0, -1)) {
|
|
29
|
+
const next = parent.contents.get(part) ?? new Directory(new Map());
|
|
30
|
+
parent.contents.set(part, next);
|
|
31
|
+
parent = next as Directory;
|
|
32
|
+
}
|
|
33
|
+
// The buffer (a download's bytes are all of it), so the File keeps it rather than a copy.
|
|
34
|
+
const once = job.readOnce && !job.keep.includes(parts.at(-1)!);
|
|
35
|
+
parent.contents.set(parts.at(-1)!, once ? new ReadOnceFile(data, () => onReopen(`${dir}/${file}`)) : new File(data.buffer));
|
|
36
|
+
}
|
|
37
|
+
const plugins = Object.fromEntries(await Promise.all(job.plugins.map(async (guest) => {
|
|
38
|
+
const { data } = downloaded.find(({ dir, file }) => `${dir}/${file}` === guest)!;
|
|
39
|
+
return [guest, await WebAssembly.compile(data)] as const;
|
|
40
|
+
})));
|
|
41
|
+
return { mounts, plugins };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function run(job: ToolJob): Promise<void> {
|
|
45
|
+
const cache = await openCache();
|
|
46
|
+
let reopened: string | undefined;
|
|
47
|
+
const [module, { mounts, plugins }] = await Promise.all([download(cache, job.module).then(compileModule), load(job, cache, (path) => (reopened ??= path))]);
|
|
48
|
+
const result = await runMemoryCommand({ module, args: job.args, mounts, env: job.env, plugins });
|
|
49
|
+
const written = Object.fromEntries(job.writable.map((dir) => [dir, directoryFiles(mounts[dir]!)]));
|
|
50
|
+
const buffers = new Set(Object.values(written).flatMap((list) => list.map(([, data]) => data.buffer as ArrayBuffer)));
|
|
51
|
+
post({ type: 'done', result, files: written, ...(reopened ? { reopened } : {}) }, [...buffers]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
self.onmessage = async (event: MessageEvent<ToToolWorker>) => {
|
|
55
|
+
const message = event.data;
|
|
56
|
+
try {
|
|
57
|
+
if (message.type === 'run') return await run(message.job);
|
|
58
|
+
const cache = await openCache();
|
|
59
|
+
await pooled(message.files.map((file, index) => ({ file, index })), CONCURRENCY, async ({ file, index }) => {
|
|
60
|
+
await prefetch(cache, file);
|
|
61
|
+
post({ type: 'fetched', index });
|
|
62
|
+
});
|
|
63
|
+
post({ type: 'prefetched' });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
post({ type: 'error', error: error instanceof Error ? (error.stack ?? error.message) : String(error) });
|
|
66
|
+
}
|
|
67
|
+
};
|
package/src/playground/worker.ts
CHANGED
|
@@ -1,46 +1,70 @@
|
|
|
1
1
|
// The playground's compiler, in a worker: the page sends an app's sources,
|
|
2
|
-
// the worker answers with the linked module or the diagnostics. The
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// runs again.
|
|
2
|
+
// the worker answers with the linked module or the diagnostics. The files
|
|
3
|
+
// are downloaded once, and each compile starts from the module cache the
|
|
4
|
+
// previous one left; each tool runs in a worker of its own (tool-host.ts).
|
|
5
|
+
// The macro plugins load only when a compile expands a macro: it fails for
|
|
6
|
+
// want of them, waits for them and runs again. Until then they are only
|
|
7
|
+
// downloaded into the cache, once a compile is done.
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { memoryDirectory } from '@swiftbrowser/wasm-compiler';
|
|
9
10
|
import { type LoadedToolchain, loadToolchain } from './assets';
|
|
11
|
+
import { type AppSources, type Toolchain, buildApp, missingPlugin } from './build';
|
|
10
12
|
import type { PlaygroundSource } from './files';
|
|
13
|
+
import { toolHost } from './tool-host';
|
|
11
14
|
|
|
12
15
|
export type ToWorker = { type: 'compile'; id: number; source: PlaygroundSource; app: AppSources };
|
|
13
16
|
export type FromWorker =
|
|
14
17
|
| { type: 'progress'; text: string }
|
|
15
18
|
| { type: 'warning'; text: string }
|
|
16
|
-
| { type: 'result'; id: number; ok: boolean; output: string; wasm?: Uint8Array<ArrayBuffer>; steps: { tool: string; seconds: number }[] }
|
|
19
|
+
| { type: 'result'; id: number; ok: boolean; output: string; wasm?: Uint8Array<ArrayBuffer>; steps: { tool: string; seconds: number; memoryBytes?: number }[] }
|
|
17
20
|
| { type: 'failure'; id: number; error: string };
|
|
18
21
|
|
|
19
22
|
const post = (message: FromWorker, transfer: Transferable[] = []): void => (self as unknown as Worker).postMessage(message, transfer);
|
|
20
23
|
let loaded: Promise<LoadedToolchain> | null = null;
|
|
21
24
|
let moduleCache: [string, Uint8Array][] | null = null;
|
|
25
|
+
/** Whether compiles load the macro plugins: once one has wanted them. */
|
|
26
|
+
let withPlugins = false;
|
|
27
|
+
|
|
28
|
+
/** The toolchain the compiles run on, with or without the plugins. */
|
|
29
|
+
function toolchain(loaded: LoadedToolchain, plugins: boolean): Toolchain {
|
|
30
|
+
const names = plugins ? Object.keys(loaded.plugins) : [];
|
|
31
|
+
return {
|
|
32
|
+
host: ({ mounts, moduleCache }) =>
|
|
33
|
+
toolHost({
|
|
34
|
+
tools: loaded.tools,
|
|
35
|
+
mounts: { ...mounts, '/sdk': memoryDirectory(loaded.sdk.files) },
|
|
36
|
+
downloads: { '/sdk': loaded.sdk.downloads, '/kit': loaded.kit },
|
|
37
|
+
plugins: Object.fromEntries(names.map((name) => [`/plugins/${name}.wasm`, loaded.plugins[name]!])),
|
|
38
|
+
sdkRoot: '/sdk',
|
|
39
|
+
...(moduleCache ? { moduleCache } : {}),
|
|
40
|
+
}),
|
|
41
|
+
plugins: names,
|
|
42
|
+
kitManifest: loaded.kitManifest,
|
|
43
|
+
icu: loaded.icu,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
22
46
|
|
|
23
47
|
self.onmessage = async (event: MessageEvent<ToWorker>) => {
|
|
24
48
|
const { id, source, app } = event.data;
|
|
25
49
|
try {
|
|
26
50
|
loaded ??= loadToolchain(source, (text) => post({ type: 'progress', text })).then((toolchain) => {
|
|
27
|
-
// Later compiles use the plugins as soon as they are in.
|
|
28
|
-
toolchain.plugins.then((plugins) => (toolchain.toolchain.plugins = plugins), () => {});
|
|
29
51
|
if (toolchain.warning) post({ type: 'warning', text: toolchain.warning });
|
|
30
52
|
return toolchain;
|
|
31
53
|
});
|
|
32
|
-
const
|
|
33
|
-
post({ type: 'progress', text:
|
|
34
|
-
let result = await buildApp(toolchain, app, { moduleCache: moduleCache ??
|
|
54
|
+
const files = await loaded;
|
|
55
|
+
const onStep = (tool: string): void => post({ type: 'progress', text: `${tool === 'wasm-ld' ? 'linking' : 'compiling'} ${app.name}${withPlugins ? ' with the macro plugins' : ''}…` });
|
|
56
|
+
let result = await buildApp(toolchain(files, withPlugins), app, { moduleCache: moduleCache ?? files.moduleCache, onStep });
|
|
35
57
|
moduleCache = result.moduleCache;
|
|
36
|
-
if (!result.ok &&
|
|
58
|
+
if (!result.ok && !withPlugins && missingPlugin(result.output)) {
|
|
37
59
|
post({ type: 'progress', text: 'waiting for the macro plugins…' });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
result = await buildApp(toolchain, app, { moduleCache });
|
|
60
|
+
await files.prefetchPlugins();
|
|
61
|
+
withPlugins = true;
|
|
62
|
+
result = await buildApp(toolchain(files, withPlugins), app, { moduleCache, onStep });
|
|
41
63
|
moduleCache = result.moduleCache;
|
|
42
64
|
}
|
|
43
|
-
|
|
65
|
+
// Behind the result: a macro later finds its plugin downloaded.
|
|
66
|
+
if (!withPlugins) void files.prefetchPlugins().catch(() => {});
|
|
67
|
+
const steps = result.steps.map(({ tool, seconds, memoryBytes }) => ({ tool, seconds, ...(memoryBytes !== undefined ? { memoryBytes } : {}) }));
|
|
44
68
|
if (result.wasm) {
|
|
45
69
|
const wasm = new Uint8Array(result.wasm);
|
|
46
70
|
post({ type: 'result', id, ok: result.ok, output: result.output, wasm, steps }, [wasm.buffer]);
|