@volter/browser-wali 0.1.4 → 0.1.6
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 +73 -0
- package/dist/browser-wali-command.d.ts +3 -0
- package/dist/browser-wali-command.d.ts.map +1 -1
- package/dist/browser-wali-command.js +2 -1
- package/dist/browser-wali-command.js.map +1 -1
- package/dist/capabilities.d.ts +73 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +86 -2
- package/dist/capabilities.js.map +1 -1
- package/dist/child-processes.d.ts +24 -0
- package/dist/child-processes.d.ts.map +1 -0
- package/dist/child-processes.js +134 -0
- package/dist/child-processes.js.map +1 -0
- package/dist/codec-protocol.d.ts +66 -0
- package/dist/codec-protocol.d.ts.map +1 -0
- package/dist/codec-protocol.js +59 -0
- package/dist/codec-protocol.js.map +1 -0
- package/dist/codec-remote.d.ts +33 -0
- package/dist/codec-remote.d.ts.map +1 -0
- package/dist/codec-remote.js +190 -0
- package/dist/codec-remote.js.map +1 -0
- package/dist/codec-worker.d.ts +2 -0
- package/dist/codec-worker.d.ts.map +1 -0
- package/dist/codec-worker.js +438 -0
- package/dist/codec-worker.js.map +1 -0
- package/dist/coherent-file-locks.d.ts.map +1 -1
- package/dist/coherent-file-locks.js +17 -5
- package/dist/coherent-file-locks.js.map +1 -1
- package/dist/descriptors.d.ts +49 -0
- package/dist/descriptors.d.ts.map +1 -0
- package/dist/descriptors.js +115 -0
- package/dist/descriptors.js.map +1 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -2
- package/dist/events.js.map +1 -1
- package/dist/filesystem-protocol.d.ts +28 -1
- package/dist/filesystem-protocol.d.ts.map +1 -1
- package/dist/filesystem-protocol.js +18 -0
- package/dist/filesystem-protocol.js.map +1 -1
- package/dist/filesystem-remote.d.ts +26 -2
- package/dist/filesystem-remote.d.ts.map +1 -1
- package/dist/filesystem-remote.js +132 -23
- package/dist/filesystem-remote.js.map +1 -1
- package/dist/filesystem-worker.js +274 -34
- package/dist/filesystem-worker.js.map +1 -1
- package/dist/filesystem.d.ts +30 -1
- package/dist/filesystem.d.ts.map +1 -1
- package/dist/filesystem.js +180 -50
- package/dist/filesystem.js.map +1 -1
- package/dist/fork-worker.d.ts +2 -0
- package/dist/fork-worker.d.ts.map +1 -0
- package/dist/fork-worker.js +52 -0
- package/dist/fork-worker.js.map +1 -0
- package/dist/host.d.ts +36 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +153 -0
- package/dist/host.js.map +1 -0
- package/dist/loopback-host.d.ts +25 -1
- package/dist/loopback-host.d.ts.map +1 -1
- package/dist/loopback-host.js +447 -13
- package/dist/loopback-host.js.map +1 -1
- package/dist/memory.d.ts +65 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +306 -49
- package/dist/memory.js.map +1 -1
- package/dist/module-layout.d.ts +2 -0
- package/dist/module-layout.d.ts.map +1 -1
- package/dist/module-layout.js +4 -2
- package/dist/module-layout.js.map +1 -1
- package/dist/monotonic-clock.d.ts +12 -0
- package/dist/monotonic-clock.d.ts.map +1 -0
- package/dist/monotonic-clock.js +14 -0
- package/dist/monotonic-clock.js.map +1 -0
- package/dist/network-protocol.d.ts +12 -1
- package/dist/network-protocol.d.ts.map +1 -1
- package/dist/network-protocol.js +6 -0
- package/dist/network-protocol.js.map +1 -1
- package/dist/network-worker.js +149 -20
- package/dist/network-worker.js.map +1 -1
- package/dist/network.d.ts +15 -3
- package/dist/network.d.ts.map +1 -1
- package/dist/network.js +74 -39
- package/dist/network.js.map +1 -1
- package/dist/process-adapter.d.ts +4 -1
- package/dist/process-adapter.d.ts.map +1 -1
- package/dist/process-adapter.js +3 -3
- package/dist/process-adapter.js.map +1 -1
- package/dist/process-bridge.d.ts +43 -0
- package/dist/process-bridge.d.ts.map +1 -1
- package/dist/process-bridge.js +30 -1
- package/dist/process-bridge.js.map +1 -1
- package/dist/program.d.ts +60 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +92 -31
- package/dist/program.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +491 -134
- package/dist/runtime.js.map +1 -1
- package/dist/select.d.ts +16 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +100 -0
- package/dist/select.js.map +1 -0
- package/dist/shared-pipes.d.ts +17 -6
- package/dist/shared-pipes.d.ts.map +1 -1
- package/dist/shared-pipes.js +94 -70
- package/dist/shared-pipes.js.map +1 -1
- package/dist/signals.d.ts +48 -0
- package/dist/signals.d.ts.map +1 -0
- package/dist/signals.js +232 -0
- package/dist/signals.js.map +1 -0
- package/dist/snapshot-filesystem.d.ts +128 -2
- package/dist/snapshot-filesystem.d.ts.map +1 -1
- package/dist/snapshot-filesystem.js +481 -53
- package/dist/snapshot-filesystem.js.map +1 -1
- package/dist/spawn-policy.d.ts +5 -0
- package/dist/spawn-policy.d.ts.map +1 -0
- package/dist/spawn-policy.js +29 -0
- package/dist/spawn-policy.js.map +1 -0
- package/dist/stdin.d.ts +2 -0
- package/dist/stdin.d.ts.map +1 -1
- package/dist/stdin.js +4 -0
- package/dist/stdin.js.map +1 -1
- package/dist/symlink.d.ts +8 -0
- package/dist/symlink.d.ts.map +1 -1
- package/dist/symlink.js +13 -0
- package/dist/symlink.js.map +1 -1
- package/dist/system.d.ts +152 -1
- package/dist/system.d.ts.map +1 -1
- package/dist/system.js +226 -7
- package/dist/system.js.map +1 -1
- package/dist/thread-coordinator-worker.d.ts +1 -0
- package/dist/thread-coordinator-worker.d.ts.map +1 -1
- package/dist/thread-coordinator-worker.js +1 -0
- package/dist/thread-coordinator-worker.js.map +1 -1
- package/dist/thread-worker.js +241 -122
- package/dist/thread-worker.js.map +1 -1
- package/dist/threads.d.ts +12 -2
- package/dist/threads.d.ts.map +1 -1
- package/dist/threads.js +30 -10
- package/dist/threads.js.map +1 -1
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/websocket-peer.d.ts.map +1 -1
- package/dist/websocket-peer.js +4 -14
- package/dist/websocket-peer.js.map +1 -1
- package/dist/worker-program-protocol.d.ts +70 -3
- package/dist/worker-program-protocol.d.ts.map +1 -1
- package/dist/worker-program-worker.js +115 -23
- package/dist/worker-program-worker.js.map +1 -1
- package/dist/worker-program.d.ts +25 -1
- package/dist/worker-program.d.ts.map +1 -1
- package/dist/worker-program.js +470 -61
- package/dist/worker-program.js.map +1 -1
- package/dist/worker-standby.d.ts +3 -0
- package/dist/worker-standby.d.ts.map +1 -0
- package/dist/worker-standby.js +17 -0
- package/dist/worker-standby.js.map +1 -0
- package/package.json +21 -7
package/dist/memory.js
CHANGED
|
@@ -13,8 +13,34 @@ const DEFAULT_MMAP_BASE = 256 * 1024 * 1024;
|
|
|
13
13
|
const MMAP_CURSOR_INDEX = 0;
|
|
14
14
|
const PROGRAM_BREAK_INDEX = 1;
|
|
15
15
|
const ALLOCATOR_LOCK_INDEX = 2;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// The address-space instrument. These words exist because the ways this
|
|
17
|
+
// allocator could lose address space were all SILENT: a release that no
|
|
18
|
+
// free-range slot could hold was dropped with a bare `return`, a munmap from a
|
|
19
|
+
// thread that did not map the range was retained forever, and the mmap cursor
|
|
20
|
+
// was stored as a 32-bit word, so a reservation past the 4 GiB ceiling wrapped
|
|
21
|
+
// to a low address instead of refusing. Each is counted here and the ones that
|
|
22
|
+
// still can happen are surfaced through the runtime's `onDiagnostic`; the
|
|
23
|
+
// numbers are what a reader gets instead of "3 GB free" over a refused 8 MiB.
|
|
24
|
+
const DROPPED_RELEASES_INDEX = 3;
|
|
25
|
+
const DROPPED_KIB_INDEX = 4;
|
|
26
|
+
const CURSOR_HIGH_WATER_KIB_INDEX = 5;
|
|
27
|
+
const FREE_RANGE_PEAK_INDEX = 6;
|
|
28
|
+
const RETAINED_MUNMAPS_INDEX = 7;
|
|
29
|
+
const RETAINED_KIB_INDEX = 8;
|
|
30
|
+
const CEILING_REFUSALS_INDEX = 9;
|
|
31
|
+
const FREE_RANGE_COUNT_INDEX = 10;
|
|
32
|
+
const TOUCHED_HIGH_WATER_INDEX = 11;
|
|
33
|
+
const ALLOCATOR_HEADER_WORDS = 12;
|
|
34
|
+
// The free-range table is DENSE: entries [0, count) are live, a removed entry
|
|
35
|
+
// is swapped with the last, and every scan is over the ranges that exist, not
|
|
36
|
+
// over the capacity. MEASURED 2026-09-19 on the courtyard model alone: a
|
|
37
|
+
// 512-slot sparse table filled before the model's tenth call, 8,192 released
|
|
38
|
+
// ranges were then dropped (693 MiB of address space lost by the 1 GiB mark),
|
|
39
|
+
// the cursor walked to 4,095 MiB and wrapped. mallocng maps every group and
|
|
40
|
+
// every large chunk on its own and unmaps them out of order, so the live
|
|
41
|
+
// free-range count is a property of the guest's heap, not of this table.
|
|
42
|
+
const FREE_RANGE_SLOTS = 16_384;
|
|
43
|
+
const RESERVE_REFUSED = -1;
|
|
18
44
|
const FREE_RANGE_WORDS = 2;
|
|
19
45
|
const ALLOCATOR_CONTROL_WORDS = ALLOCATOR_HEADER_WORDS + FREE_RANGE_SLOTS * FREE_RANGE_WORDS;
|
|
20
46
|
const SHARED_MMAP_REGISTRY_SLOTS = 256;
|
|
@@ -40,14 +66,16 @@ export class WaliMemoryManager {
|
|
|
40
66
|
readFile;
|
|
41
67
|
fileKey;
|
|
42
68
|
writeSharedFile;
|
|
69
|
+
report;
|
|
43
70
|
mappings = [];
|
|
44
71
|
allocatorState;
|
|
45
72
|
allocatorInitialized = false;
|
|
46
|
-
constructor(memory, readFile, sharedMmapCursor, fileKey, writeSharedFile, sharedMmapRegistry) {
|
|
73
|
+
constructor(memory, readFile, sharedMmapCursor, fileKey, writeSharedFile, sharedMmapRegistry, report) {
|
|
47
74
|
this.memory = memory;
|
|
48
75
|
this.readFile = readFile;
|
|
49
76
|
this.fileKey = fileKey;
|
|
50
77
|
this.writeSharedFile = writeSharedFile;
|
|
78
|
+
this.report = report;
|
|
51
79
|
const control = sharedMmapCursor ?? createWaliAllocatorControl();
|
|
52
80
|
this.allocatorState = new Int32Array(control);
|
|
53
81
|
if (this.allocatorState.length < ALLOCATOR_CONTROL_WORDS) {
|
|
@@ -64,6 +92,15 @@ export class WaliMemoryManager {
|
|
|
64
92
|
this.initializeAllocator();
|
|
65
93
|
return this.allocatorState.buffer;
|
|
66
94
|
}
|
|
95
|
+
forkSnapshot() {
|
|
96
|
+
return { mappings: this.mappings.map(mapping => ({ ...mapping })),
|
|
97
|
+
...(this.sharedRegistry ? { registry: this.sharedRegistry.buffer.slice(0) } : {}) };
|
|
98
|
+
}
|
|
99
|
+
restoreFork(snapshot) {
|
|
100
|
+
this.mappings.push(...snapshot.mappings.map(mapping => ({ ...mapping })));
|
|
101
|
+
if (snapshot.registry && this.sharedRegistry)
|
|
102
|
+
this.sharedRegistry.set(new Int32Array(snapshot.registry));
|
|
103
|
+
}
|
|
67
104
|
imports() {
|
|
68
105
|
return {
|
|
69
106
|
SYS_brk: (address) => this.brk(Number(address)),
|
|
@@ -82,9 +119,12 @@ export class WaliMemoryManager {
|
|
|
82
119
|
return BigInt(current);
|
|
83
120
|
if (!validAddress(requested))
|
|
84
121
|
return BigInt(current);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
122
|
+
// The break grows below the mmap region and never into it. The region's
|
|
123
|
+
// base is fixed at DEFAULT_MMAP_BASE (or the initial memory size when that
|
|
124
|
+
// is larger, in which case no brk succeeds and mallocng maps its meta
|
|
125
|
+
// areas instead); comparing against the moving cursor let the break climb
|
|
126
|
+
// over live mappings.
|
|
127
|
+
if (requested > DEFAULT_MMAP_BASE)
|
|
88
128
|
return BigInt(current);
|
|
89
129
|
if (!this.ensureCapacity(requested))
|
|
90
130
|
return BigInt(current);
|
|
@@ -98,9 +138,21 @@ export class WaliMemoryManager {
|
|
|
98
138
|
const anonymous = Boolean(flags & MAP_ANONYMOUS);
|
|
99
139
|
if ((anonymous && fd !== -1) || (!anonymous && fd < 0))
|
|
100
140
|
return -ENODEV;
|
|
101
|
-
|
|
102
|
-
if (!anonymous
|
|
103
|
-
|
|
141
|
+
let fileContents;
|
|
142
|
+
if (!anonymous) {
|
|
143
|
+
try {
|
|
144
|
+
fileContents = this.readFile(fd, offset, length);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
// A kernel answers a failed file mapping with an errno; this runtime
|
|
148
|
+
// used to die here (an EMSGSIZE from the filesystem channel on a
|
|
149
|
+
// large file was a fatal for the whole program, 2026-09-19).
|
|
150
|
+
this.say(`[WALI] mmap of fd ${fd}, ${length} bytes at ${offset}, could not read the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
return -EIO;
|
|
152
|
+
}
|
|
153
|
+
if (!fileContents)
|
|
154
|
+
return -9n;
|
|
155
|
+
}
|
|
104
156
|
const size = align(length, LINUX_PAGE_SIZE);
|
|
105
157
|
const fixed = Boolean(flags & MAP_FIXED);
|
|
106
158
|
const shared = Boolean(flags & MAP_SHARED);
|
|
@@ -121,6 +173,8 @@ export class WaliMemoryManager {
|
|
|
121
173
|
}
|
|
122
174
|
}
|
|
123
175
|
const start = fixed ? requested : registryClaim?.start ?? this.reserve(size);
|
|
176
|
+
if (start === RESERVE_REFUSED)
|
|
177
|
+
return -ENOMEM;
|
|
124
178
|
if (!validAddress(start) || (fixed && start % LINUX_PAGE_SIZE !== 0))
|
|
125
179
|
return -EINVAL;
|
|
126
180
|
const end = start + size;
|
|
@@ -135,7 +189,7 @@ export class WaliMemoryManager {
|
|
|
135
189
|
...(registryClaim ? { registrySlot: registryClaim.slot } : {}),
|
|
136
190
|
});
|
|
137
191
|
if (!registryClaim?.existing) {
|
|
138
|
-
|
|
192
|
+
this.zeroForHandout(start, end);
|
|
139
193
|
if (fileContents)
|
|
140
194
|
new Uint8Array(this.memory().buffer).set(fileContents, start);
|
|
141
195
|
}
|
|
@@ -159,14 +213,24 @@ export class WaliMemoryManager {
|
|
|
159
213
|
const index = this.mappings.findIndex((candidate) => address >= candidate.start && end <= candidate.end);
|
|
160
214
|
const mapping = index >= 0 ? this.mappings[index] : undefined;
|
|
161
215
|
// Mapping metadata is local to a syscall host, but a Unix process may
|
|
162
|
-
// mmap on one pthread and munmap on another
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
216
|
+
// mmap on one pthread and munmap on another: mallocng frees a >=128 KiB
|
|
217
|
+
// chunk on whichever thread calls free(), and unmaps it there. The
|
|
218
|
+
// address space is process-wide (`allocatorState`), so the range goes back
|
|
219
|
+
// to the process's free list the way a kernel would take it. The one
|
|
220
|
+
// thing another thread's record could carry that this one cannot see is a
|
|
221
|
+
// shared FILE mapping's flush, and those live in the shared registry, so a
|
|
222
|
+
// range a live registry entry covers is retained (counted, surfaced) rather
|
|
223
|
+
// than released under it. Retaining every unknown range instead leaked the
|
|
224
|
+
// address space of every cross-thread free until the 4 GiB ceiling.
|
|
225
|
+
if (!mapping) {
|
|
226
|
+
if (this.sharedRegistryCovers(address, end)) {
|
|
227
|
+
this.noteRetainedMunmap(end - address);
|
|
228
|
+
return 0n;
|
|
229
|
+
}
|
|
230
|
+
this.release(address, end);
|
|
168
231
|
return 0n;
|
|
169
|
-
|
|
232
|
+
}
|
|
233
|
+
if (mapping.registrySlot !== undefined) {
|
|
170
234
|
if (address !== mapping.start || end !== mapping.end)
|
|
171
235
|
return -EINVAL;
|
|
172
236
|
if (!this.flushMapping(mapping, address, end))
|
|
@@ -175,17 +239,98 @@ export class WaliMemoryManager {
|
|
|
175
239
|
this.mappings.splice(index, 1);
|
|
176
240
|
return 0n;
|
|
177
241
|
}
|
|
178
|
-
if (mapping
|
|
242
|
+
if (mapping.shared && mapping.references > 1) {
|
|
179
243
|
mapping.references -= 1;
|
|
180
244
|
return 0n;
|
|
181
245
|
}
|
|
182
|
-
if (mapping
|
|
246
|
+
if (mapping.shared && !this.flushMapping(mapping, address, end))
|
|
183
247
|
return -EIO;
|
|
184
|
-
|
|
185
|
-
|
|
248
|
+
// A partial unmap keeps the remainder on record, so a later unmap of the
|
|
249
|
+
// rest is still this thread's own mapping and not an unknown range.
|
|
250
|
+
if (address === mapping.start && end === mapping.end) {
|
|
251
|
+
this.mappings.splice(index, 1);
|
|
252
|
+
}
|
|
253
|
+
else if (address === mapping.start) {
|
|
254
|
+
mapping.fileOffset += end - mapping.start;
|
|
255
|
+
mapping.start = end;
|
|
256
|
+
}
|
|
257
|
+
else if (end === mapping.end) {
|
|
258
|
+
mapping.end = address;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
this.mappings.push({ ...mapping, start: end, fileOffset: mapping.fileOffset + (end - mapping.start) });
|
|
262
|
+
mapping.end = address;
|
|
263
|
+
}
|
|
264
|
+
// No zeroing here: the next mmap of these bytes zeroes them (`zeroForHandout`),
|
|
265
|
+
// and zeroing on both sides doubled the cost of every large free.
|
|
186
266
|
this.release(address, end);
|
|
187
267
|
return 0n;
|
|
188
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Zero a range about to be handed out, and only the part of it that has
|
|
271
|
+
* been handed out before. Memory the wasm module grows into is already
|
|
272
|
+
* zero, so the bytes above the touched high-water mark need nothing; a
|
|
273
|
+
* reused range below it holds whatever its last owner left.
|
|
274
|
+
* MEASURED 2026-09-19: malloc+free of an UNTOUCHED 64 MiB took 40 ms on
|
|
275
|
+
* WALI against 4 ms on Emscripten, with the copy and the memset themselves
|
|
276
|
+
* at 2.3 and 0.9 ms -- the whole cost was two byte-view fills of shared
|
|
277
|
+
* memory, one here and one on munmap. V8 fills a SharedArrayBuffer through
|
|
278
|
+
* Uint8Array 15x slower than a plain buffer (20.9 ms vs 1.4 ms per 64 MiB,
|
|
279
|
+
* node 26); through Float64Array it is 2.5 ms, so the aligned body is
|
|
280
|
+
* filled 8 bytes at a time.
|
|
281
|
+
*/
|
|
282
|
+
zeroForHandout(start, end) {
|
|
283
|
+
end = Math.min(end, this.memory().buffer.byteLength);
|
|
284
|
+
if (end <= start)
|
|
285
|
+
return;
|
|
286
|
+
let touched = Atomics.load(this.allocatorState, TOUCHED_HIGH_WATER_INDEX) >>> 0;
|
|
287
|
+
if (start < touched)
|
|
288
|
+
this.zero(start, Math.min(end, touched));
|
|
289
|
+
while (end > touched) {
|
|
290
|
+
const seen = Atomics.compareExchange(this.allocatorState, TOUCHED_HIGH_WATER_INDEX, touched | 0, end | 0) >>> 0;
|
|
291
|
+
if (seen === touched)
|
|
292
|
+
break;
|
|
293
|
+
touched = seen;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
zero(start, end) {
|
|
297
|
+
const buffer = this.memory().buffer;
|
|
298
|
+
const bytes = new Uint8Array(buffer);
|
|
299
|
+
const bodyStart = (start + 7) & ~7;
|
|
300
|
+
const bodyEnd = end & ~7;
|
|
301
|
+
if (bodyEnd <= bodyStart) {
|
|
302
|
+
bytes.fill(0, start, end);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (bodyStart > start)
|
|
306
|
+
bytes.fill(0, start, bodyStart);
|
|
307
|
+
new Float64Array(buffer, bodyStart, (bodyEnd - bodyStart) / 8).fill(0);
|
|
308
|
+
if (end > bodyEnd)
|
|
309
|
+
bytes.fill(0, bodyEnd, end);
|
|
310
|
+
}
|
|
311
|
+
sharedRegistryCovers(start, end) {
|
|
312
|
+
const registry = this.sharedRegistry;
|
|
313
|
+
if (!registry)
|
|
314
|
+
return false;
|
|
315
|
+
this.lockSharedRegistry();
|
|
316
|
+
try {
|
|
317
|
+
for (let slot = 0; slot < SHARED_MMAP_REGISTRY_SLOTS; slot += 1) {
|
|
318
|
+
const base = registryBase(slot);
|
|
319
|
+
if (Atomics.load(registry, base + REGISTRY_STATE) === 0)
|
|
320
|
+
continue;
|
|
321
|
+
if ((Atomics.load(registry, base + REGISTRY_REFERENCES) >>> 0) === 0)
|
|
322
|
+
continue;
|
|
323
|
+
const mappedStart = Atomics.load(registry, base + REGISTRY_START) >>> 0;
|
|
324
|
+
const mappedEnd = mappedStart + (Atomics.load(registry, base + REGISTRY_SIZE) >>> 0);
|
|
325
|
+
if (start < mappedEnd && end > mappedStart)
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
this.unlockSharedRegistry();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
189
334
|
mremap(address, oldLength, newLength, flags, requested) {
|
|
190
335
|
if (!validPageRange(address, oldLength) || !Number.isSafeInteger(newLength) || newLength <= 0)
|
|
191
336
|
return -EINVAL;
|
|
@@ -202,7 +347,6 @@ export class WaliMemoryManager {
|
|
|
202
347
|
if (oldEnd > this.memory().buffer.byteLength)
|
|
203
348
|
return -ENOMEM;
|
|
204
349
|
if (newSize <= oldSize) {
|
|
205
|
-
new Uint8Array(this.memory().buffer).fill(0, address + newSize, address + oldSize);
|
|
206
350
|
if (mapping)
|
|
207
351
|
mapping.end = address + newSize;
|
|
208
352
|
this.release(address + newSize, oldEnd);
|
|
@@ -216,7 +360,7 @@ export class WaliMemoryManager {
|
|
|
216
360
|
const canReserveTail = this.reserveTail(oldEnd, expandedEnd);
|
|
217
361
|
const canExpand = canReserveTail && this.ensureCapacity(expandedEnd);
|
|
218
362
|
if (canExpand) {
|
|
219
|
-
|
|
363
|
+
this.zeroForHandout(address + oldSize, expandedEnd);
|
|
220
364
|
if (mapping)
|
|
221
365
|
mapping.end = expandedEnd;
|
|
222
366
|
return BigInt(address);
|
|
@@ -228,6 +372,8 @@ export class WaliMemoryManager {
|
|
|
228
372
|
const target = flags & MREMAP_FIXED
|
|
229
373
|
? requested
|
|
230
374
|
: this.reserve(newSize);
|
|
375
|
+
if (target === RESERVE_REFUSED)
|
|
376
|
+
return -ENOMEM;
|
|
231
377
|
const targetEnd = target + newSize;
|
|
232
378
|
if (!validAddress(target) || target % LINUX_PAGE_SIZE !== 0 || this.overlaps(target, targetEnd, mapping))
|
|
233
379
|
return -EINVAL;
|
|
@@ -237,8 +383,7 @@ export class WaliMemoryManager {
|
|
|
237
383
|
return -ENOMEM;
|
|
238
384
|
}
|
|
239
385
|
const contents = new Uint8Array(this.memory().buffer).slice(address, address + Math.min(oldSize, newSize));
|
|
240
|
-
|
|
241
|
-
new Uint8Array(this.memory().buffer).fill(0, target, targetEnd);
|
|
386
|
+
this.zeroForHandout(target, targetEnd);
|
|
242
387
|
new Uint8Array(this.memory().buffer).set(contents, target);
|
|
243
388
|
this.release(address, oldEnd);
|
|
244
389
|
if (mapping) {
|
|
@@ -307,6 +452,8 @@ export class WaliMemoryManager {
|
|
|
307
452
|
if (freeSlot < 0)
|
|
308
453
|
return;
|
|
309
454
|
const start = this.reserve(size);
|
|
455
|
+
if (start === RESERVE_REFUSED)
|
|
456
|
+
return;
|
|
310
457
|
const base = registryBase(freeSlot);
|
|
311
458
|
Atomics.store(registry, base + REGISTRY_HASH_A, hashA | 0);
|
|
312
459
|
Atomics.store(registry, base + REGISTRY_HASH_B, hashB | 0);
|
|
@@ -348,15 +495,15 @@ export class WaliMemoryManager {
|
|
|
348
495
|
reserve(size) {
|
|
349
496
|
this.lockAllocator();
|
|
350
497
|
try {
|
|
351
|
-
|
|
498
|
+
const count = this.freeRangeCount();
|
|
499
|
+
for (let slot = 0; slot < count; slot += 1) {
|
|
352
500
|
const base = freeRangeBase(slot);
|
|
353
501
|
const start = Atomics.load(this.allocatorState, base) >>> 0;
|
|
354
502
|
const available = Atomics.load(this.allocatorState, base + 1) >>> 0;
|
|
355
503
|
if (available < size)
|
|
356
504
|
continue;
|
|
357
505
|
if (available === size) {
|
|
358
|
-
|
|
359
|
-
Atomics.store(this.allocatorState, base + 1, 0);
|
|
506
|
+
this.removeFreeRange(slot);
|
|
360
507
|
}
|
|
361
508
|
else {
|
|
362
509
|
Atomics.store(this.allocatorState, base, (start + size) | 0);
|
|
@@ -368,7 +515,16 @@ export class WaliMemoryManager {
|
|
|
368
515
|
if (cursor === 0) {
|
|
369
516
|
cursor = align(Math.max(this.memory().buffer.byteLength, DEFAULT_MMAP_BASE), LINUX_PAGE_SIZE);
|
|
370
517
|
}
|
|
518
|
+
// wasm32 addresses end at 0xffff_ffff. Storing `cursor + size` past it
|
|
519
|
+
// wrapped the cursor to a low address (the store is a 32-bit word), and
|
|
520
|
+
// the next mapping was handed out over the data segment. Refuse instead;
|
|
521
|
+
// mmap turns this into ENOMEM, which the guest already handles.
|
|
522
|
+
if (cursor + size > 0xffff_ffff) {
|
|
523
|
+
this.noteCeiling(cursor, size);
|
|
524
|
+
return RESERVE_REFUSED;
|
|
525
|
+
}
|
|
371
526
|
Atomics.store(this.allocatorState, MMAP_CURSOR_INDEX, (cursor + size) | 0);
|
|
527
|
+
this.noteCursor(cursor + size);
|
|
372
528
|
return cursor;
|
|
373
529
|
}
|
|
374
530
|
finally {
|
|
@@ -390,19 +546,16 @@ export class WaliMemoryManager {
|
|
|
390
546
|
let mergedEnd = end;
|
|
391
547
|
for (;;) {
|
|
392
548
|
let merged = false;
|
|
393
|
-
for (let slot = 0; slot <
|
|
549
|
+
for (let slot = 0; slot < this.freeRangeCount(); slot += 1) {
|
|
394
550
|
const base = freeRangeBase(slot);
|
|
395
551
|
const rangeStart = Atomics.load(this.allocatorState, base) >>> 0;
|
|
396
|
-
const
|
|
397
|
-
if (rangeSize === 0)
|
|
398
|
-
continue;
|
|
399
|
-
const rangeEnd = rangeStart + rangeSize;
|
|
552
|
+
const rangeEnd = rangeStart + (Atomics.load(this.allocatorState, base + 1) >>> 0);
|
|
400
553
|
if (rangeEnd < mergedStart || rangeStart > mergedEnd)
|
|
401
554
|
continue;
|
|
402
555
|
mergedStart = Math.min(mergedStart, rangeStart);
|
|
403
556
|
mergedEnd = Math.max(mergedEnd, rangeEnd);
|
|
404
|
-
|
|
405
|
-
|
|
557
|
+
this.removeFreeRange(slot);
|
|
558
|
+
slot -= 1;
|
|
406
559
|
merged = true;
|
|
407
560
|
}
|
|
408
561
|
if (!merged)
|
|
@@ -414,12 +567,34 @@ export class WaliMemoryManager {
|
|
|
414
567
|
this.collapseFreeTail();
|
|
415
568
|
}
|
|
416
569
|
else if (mergedEnd <= cursor) {
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
570
|
+
const count = this.freeRangeCount();
|
|
571
|
+
if (count < FREE_RANGE_SLOTS) {
|
|
572
|
+
this.storeFreeRange(count, mergedStart, mergedEnd - mergedStart);
|
|
573
|
+
Atomics.store(this.allocatorState, FREE_RANGE_COUNT_INDEX, count + 1);
|
|
574
|
+
this.noteFreeRangeOccupancy(count + 1);
|
|
419
575
|
return;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
576
|
+
}
|
|
577
|
+
// The table is full. Keep the larger ranges: the smallest entry gives
|
|
578
|
+
// way to a bigger release, and whichever is lost is counted and said.
|
|
579
|
+
let smallest = 0;
|
|
580
|
+
let smallestSize = Infinity;
|
|
581
|
+
for (let slot = 0; slot < count; slot += 1) {
|
|
582
|
+
const size = Atomics.load(this.allocatorState, freeRangeBase(slot) + 1) >>> 0;
|
|
583
|
+
if (size < smallestSize) {
|
|
584
|
+
smallest = slot;
|
|
585
|
+
smallestSize = size;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (smallestSize < mergedEnd - mergedStart) {
|
|
589
|
+
this.storeFreeRange(smallest, mergedStart, mergedEnd - mergedStart);
|
|
590
|
+
this.noteDroppedRelease(smallestSize);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
this.noteDroppedRelease(mergedEnd - mergedStart);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
this.noteDroppedRelease(mergedEnd - mergedStart);
|
|
423
598
|
}
|
|
424
599
|
}
|
|
425
600
|
finally {
|
|
@@ -427,6 +602,8 @@ export class WaliMemoryManager {
|
|
|
427
602
|
}
|
|
428
603
|
}
|
|
429
604
|
reserveTail(start, end) {
|
|
605
|
+
if (end > 0xffff_ffff)
|
|
606
|
+
return false;
|
|
430
607
|
this.lockAllocator();
|
|
431
608
|
try {
|
|
432
609
|
if ((Atomics.load(this.allocatorState, MMAP_CURSOR_INDEX) >>> 0) !== start)
|
|
@@ -438,25 +615,105 @@ export class WaliMemoryManager {
|
|
|
438
615
|
this.unlockAllocator();
|
|
439
616
|
}
|
|
440
617
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
618
|
+
/**
|
|
619
|
+
* THE ADDRESS-SPACE INSTRUMENT. Read this before changing `release()` or
|
|
620
|
+
* `reserve()`: both of them could lose address space without saying a word,
|
|
621
|
+
* and that silence is what made the second Blender model in a session die at
|
|
622
|
+
* an 8 MiB numpy array while every headroom probe reported gigabytes free.
|
|
623
|
+
* A probe that allocates until refused COALESCES into a few large ranges and
|
|
624
|
+
* never fills the slot table; the pattern that fills it is many small
|
|
625
|
+
* non-adjacent mappings freed out of order, which is what mallocng does with
|
|
626
|
+
* every group it maps and every large chunk (the courtyard's free ranges
|
|
627
|
+
* averaged 80 KiB).
|
|
628
|
+
*/
|
|
629
|
+
noteDroppedRelease(bytes) {
|
|
630
|
+
const dropped = Atomics.add(this.allocatorState, DROPPED_RELEASES_INDEX, 1) + 1;
|
|
631
|
+
const kib = Atomics.add(this.allocatorState, DROPPED_KIB_INDEX, Math.round(bytes / 1024)) + Math.round(bytes / 1024);
|
|
632
|
+
// One line per power of two: the first drop is the news, and the hundredth
|
|
633
|
+
// copy of a notice buries the first (the same rule the -ENOSYS and thread
|
|
634
|
+
// pool notices follow in runtime.ts).
|
|
635
|
+
if ((dropped & (dropped - 1)) !== 0)
|
|
636
|
+
return;
|
|
637
|
+
this.say(`[WALI] the mmap free-range table is full (${FREE_RANGE_SLOTS} slots):`
|
|
638
|
+
+ ` ${dropped} released range${dropped === 1 ? "" : "s"} could not be recorded,`
|
|
639
|
+
+ ` ${(kib / 1024).toFixed(1)} MiB of address space unreusable.`);
|
|
640
|
+
}
|
|
641
|
+
noteCursor(cursor) {
|
|
642
|
+
const kib = Math.round(cursor / 1024);
|
|
643
|
+
const previous = Atomics.load(this.allocatorState, CURSOR_HIGH_WATER_KIB_INDEX) >>> 0;
|
|
644
|
+
if (kib > previous)
|
|
645
|
+
Atomics.store(this.allocatorState, CURSOR_HIGH_WATER_KIB_INDEX, kib | 0);
|
|
646
|
+
}
|
|
647
|
+
noteRetainedMunmap(bytes) {
|
|
648
|
+
const count = Atomics.add(this.allocatorState, RETAINED_MUNMAPS_INDEX, 1) + 1;
|
|
649
|
+
const kib = Atomics.add(this.allocatorState, RETAINED_KIB_INDEX, Math.round(bytes / 1024)) + Math.round(bytes / 1024);
|
|
650
|
+
if ((count & (count - 1)) !== 0)
|
|
651
|
+
return;
|
|
652
|
+
this.say(`[WALI] munmap of a range this thread did not map: ${count} retained,`
|
|
653
|
+
+ ` ${(kib / 1024).toFixed(1)} MiB of address space never released.`);
|
|
654
|
+
}
|
|
655
|
+
noteCeiling(cursor, size) {
|
|
656
|
+
const count = Atomics.add(this.allocatorState, CEILING_REFUSALS_INDEX, 1) + 1;
|
|
657
|
+
this.say(`[WALI] mmap cursor ${(cursor / 1048576).toFixed(0)} MiB + ${(size / 1048576).toFixed(2)} MiB`
|
|
658
|
+
+ ` passes the 4096 MiB wasm32 ceiling (${count}).`);
|
|
659
|
+
}
|
|
660
|
+
say(notice) {
|
|
661
|
+
const line = `${notice} ${this.stateSuffix()}`;
|
|
662
|
+
console.error(line);
|
|
663
|
+
this.report?.(line);
|
|
664
|
+
}
|
|
665
|
+
stateSuffix() {
|
|
666
|
+
const occupied = this.freeRangeCount();
|
|
667
|
+
let free = 0;
|
|
668
|
+
for (let slot = 0; slot < occupied; slot += 1)
|
|
669
|
+
free += Atomics.load(this.allocatorState, freeRangeBase(slot) + 1) >>> 0;
|
|
670
|
+
const cursor = Atomics.load(this.allocatorState, MMAP_CURSOR_INDEX) >>> 0;
|
|
671
|
+
return `{cursor ${(cursor / 1048576).toFixed(0)} MiB, memory ${(this.memory().buffer.byteLength / 1048576).toFixed(0)} MiB,`
|
|
672
|
+
+ ` free table ${occupied}/${FREE_RANGE_SLOTS} ranges holding ${(free / 1048576).toFixed(1)} MiB}`;
|
|
673
|
+
}
|
|
674
|
+
noteFreeRangeOccupancy(occupied) {
|
|
675
|
+
if (occupied > (Atomics.load(this.allocatorState, FREE_RANGE_PEAK_INDEX) >>> 0)) {
|
|
676
|
+
Atomics.store(this.allocatorState, FREE_RANGE_PEAK_INDEX, occupied | 0);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/** The instrument's totals, for a host that wants them at the end of a run. */
|
|
680
|
+
addressSpaceReport() {
|
|
681
|
+
return {
|
|
682
|
+
droppedReleases: Atomics.load(this.allocatorState, DROPPED_RELEASES_INDEX) >>> 0,
|
|
683
|
+
droppedKiB: Atomics.load(this.allocatorState, DROPPED_KIB_INDEX) >>> 0,
|
|
684
|
+
cursorHighWaterKiB: Atomics.load(this.allocatorState, CURSOR_HIGH_WATER_KIB_INDEX) >>> 0,
|
|
685
|
+
freeRangePeak: Atomics.load(this.allocatorState, FREE_RANGE_PEAK_INDEX) >>> 0,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
freeRangeCount() {
|
|
689
|
+
return Atomics.load(this.allocatorState, FREE_RANGE_COUNT_INDEX);
|
|
690
|
+
}
|
|
691
|
+
storeFreeRange(slot, start, size) {
|
|
692
|
+
const base = freeRangeBase(slot);
|
|
693
|
+
Atomics.store(this.allocatorState, base, start | 0);
|
|
694
|
+
Atomics.store(this.allocatorState, base + 1, size | 0);
|
|
695
|
+
}
|
|
696
|
+
/** Swap-remove: the last live entry takes the vacated slot. Caller holds the allocator lock. */
|
|
697
|
+
removeFreeRange(slot) {
|
|
698
|
+
const last = this.freeRangeCount() - 1;
|
|
699
|
+
if (slot !== last) {
|
|
700
|
+
const from = freeRangeBase(last);
|
|
701
|
+
this.storeFreeRange(slot, Atomics.load(this.allocatorState, from) >>> 0, Atomics.load(this.allocatorState, from + 1) >>> 0);
|
|
445
702
|
}
|
|
446
|
-
|
|
703
|
+
this.storeFreeRange(last, 0, 0);
|
|
704
|
+
Atomics.store(this.allocatorState, FREE_RANGE_COUNT_INDEX, last);
|
|
447
705
|
}
|
|
448
706
|
collapseFreeTail() {
|
|
449
707
|
for (;;) {
|
|
450
708
|
const cursor = Atomics.load(this.allocatorState, MMAP_CURSOR_INDEX) >>> 0;
|
|
451
709
|
let collapsed = false;
|
|
452
|
-
for (let slot = 0; slot <
|
|
710
|
+
for (let slot = 0; slot < this.freeRangeCount(); slot += 1) {
|
|
453
711
|
const base = freeRangeBase(slot);
|
|
454
712
|
const start = Atomics.load(this.allocatorState, base) >>> 0;
|
|
455
713
|
const size = Atomics.load(this.allocatorState, base + 1) >>> 0;
|
|
456
|
-
if (
|
|
714
|
+
if (start + size !== cursor)
|
|
457
715
|
continue;
|
|
458
|
-
|
|
459
|
-
Atomics.store(this.allocatorState, base + 1, 0);
|
|
716
|
+
this.removeFreeRange(slot);
|
|
460
717
|
Atomics.store(this.allocatorState, MMAP_CURSOR_INDEX, start | 0);
|
|
461
718
|
collapsed = true;
|
|
462
719
|
break;
|