gaussian-splat-lite 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,776 @@
1
+ import {
2
+ closeSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ openSync,
6
+ readSync,
7
+ rmSync,
8
+ writeSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import v8 from "node:v8";
13
+ import { runInNewContext } from "node:vm";
14
+ import { Progress } from "./progress.js";
15
+
16
+ export const PAGE_BYTES = 65536;
17
+ export const GIB = 1024 ** 3;
18
+ // Memory the budget keeps for growth between measurements.
19
+ const MARGIN = 64 * 1024 ** 2;
20
+ // Copied over new pages: fill(0) writes shared memory a byte at a time.
21
+ const ZERO = new Uint8Array(PAGE_BYTES);
22
+
23
+ // gc() on this thread and on worker threads started later: arenas that the
24
+ // cache releases, and garbage holding buffers, return their memory to the
25
+ // system only once collected.
26
+ v8.setFlagsFromString("--expose-gc");
27
+ globalThis.gc ??= runInNewContext("gc");
28
+ // Collects twice: a collection frees the memory of what the one before found.
29
+ export function collect() {
30
+ globalThis.gc();
31
+ globalThis.gc();
32
+ }
33
+
34
+ // All large arrays consist of 64 KiB pages. Neither one ArrayBuffer nor one
35
+ // WebAssembly memory has to hold the model, even with a 16+ GiB budget. Pages
36
+ // live in arenas of shared WebAssembly memory: worker threads can read the
37
+ // pages of shared arrays (see PagedArray.share() and SharedColumn), and an
38
+ // arena the cache releases returns its memory to the system once collected,
39
+ // which a SharedArrayBuffer does not on every platform. The cache takes what
40
+ // the budget leaves after the rest of the process, measured as it runs.
41
+ export class Workspace {
42
+ constructor(budget, parent = tmpdir(), cancelled = null) {
43
+ this.budget = budget;
44
+ this.parent = parent;
45
+ this.cancelled = cancelled;
46
+ this.accesses = 0;
47
+ this.directory = undefined;
48
+ // Pages per arena, for at most about 256 arenas: each reserves 8 GiB of
49
+ // address space.
50
+ this.arenaPages = Math.max(256, Math.ceil(budget / 256 / PAGE_BYTES));
51
+ this.arenaBytes = this.arenaPages * PAGE_BYTES;
52
+ // The most memory the rest of the process has taken, and what it takes
53
+ // with the threads about to start; see measure() and expect().
54
+ this.outside = 0;
55
+ this.floor = 0;
56
+ this.allocations = 0;
57
+ // Makes worker threads drop and collect their views of released arenas;
58
+ // see Pool.collect().
59
+ this.collector = undefined;
60
+ this.used = 0;
61
+ this.clock = 0;
62
+ this.slots = [];
63
+ this.free = [];
64
+ this.arenas = [];
65
+ // Each slot's page object and views, kept so that loading a page
66
+ // allocates nothing: their garbage would read as memory of the process
67
+ // and take room from the cache.
68
+ this.reuse = [];
69
+ // `version` is bumped when the arenas or a shared array's directory
70
+ // change, `generation` when the arenas change.
71
+ this.version = 0;
72
+ this.generation = 0;
73
+ this.files = new Map();
74
+ this.nextFile = 0;
75
+ this.readBytes = 0;
76
+ this.writeBytes = 0;
77
+ // An earlier conversion's arenas return once collected.
78
+ this.settle();
79
+ }
80
+
81
+ check() {
82
+ if (this.cancelled && Atomics.load(this.cancelled, 0))
83
+ throw new Error("Conversion cancelled");
84
+ }
85
+
86
+ // Bytes all cached pages may take, at least one arena.
87
+ get capacity() {
88
+ return Math.max(
89
+ this.arenaBytes,
90
+ this.budget - MARGIN - Math.max(this.outside, this.floor),
91
+ );
92
+ }
93
+
94
+ // Bytes the rest of the process may still grow by.
95
+ get room() {
96
+ return this.budget - MARGIN - this.arenaBytes - this.outside;
97
+ }
98
+
99
+ // Measures the memory of the rest of the process: all of it but the
100
+ // arenas' pages that the cache has used.
101
+ measure() {
102
+ this.outside = Math.max(
103
+ this.outside,
104
+ process.memoryUsage.rss() - this.slots.length * PAGE_BYTES,
105
+ );
106
+ }
107
+
108
+ // Measures the rest of the process anew once its garbage is collected, as
109
+ // a phase starts.
110
+ settle() {
111
+ collect();
112
+ this.outside = process.memoryUsage.rss() - this.slots.length * PAGE_BYTES;
113
+ }
114
+
115
+ // Makes room for `bytes` that the process is about to take besides the
116
+ // cache.
117
+ expect(bytes) {
118
+ this.measure();
119
+ this.floor = this.outside + bytes;
120
+ if (this.budget - MARGIN - this.floor < this.arenaBytes)
121
+ throw new Error("Memory budget too small; increase --memory-budget");
122
+ this.shrink();
123
+ }
124
+
125
+ // Once the arenas' used pages take more than the capacity, releases the
126
+ // arenas past it, evicting their pages, and collects them so that their
127
+ // memory returns. The main thread allocates pages only between the jobs
128
+ // of worker threads.
129
+ shrink() {
130
+ if (this.slots.length * PAGE_BYTES <= this.capacity) return;
131
+ const slots = Math.floor(this.capacity / this.arenaBytes) * this.arenaPages;
132
+ for (const page of this.slots.slice(slots)) if (page) this.drop(page);
133
+ this.slots.length = slots;
134
+ this.reuse.length = slots;
135
+ this.free = this.free.filter((slot) => slot < slots);
136
+ this.arenas.length = slots / this.arenaPages;
137
+ this.version++;
138
+ this.generation++;
139
+ this.collector?.();
140
+ collect();
141
+ }
142
+
143
+ file() {
144
+ if (!this.directory) {
145
+ mkdirSync(this.parent, { recursive: true });
146
+ this.directory = mkdtempSync(join(this.parent, "gsl-build-rad-"));
147
+ console.log(`Temporary files: ${this.directory}`);
148
+ }
149
+ const file = join(this.directory, `${this.nextFile++}.tmp`);
150
+ const fd = openSync(file, "wx+");
151
+ this.files.set(fd, file);
152
+ return fd;
153
+ }
154
+
155
+ // Frees a file's disk space early. Anything left is removed with the
156
+ // directory in dispose(), which reports a failure.
157
+ close(fd) {
158
+ const file = this.files.get(fd);
159
+ this.files.delete(fd);
160
+ try {
161
+ closeSync(fd);
162
+ rmSync(file);
163
+ } catch {}
164
+ }
165
+
166
+ evict() {
167
+ for (let step = 0; ; step++) {
168
+ const slot = this.clock;
169
+ this.clock = (this.clock + 1) % this.slots.length;
170
+ const page = this.slots[slot];
171
+ if (!page) continue;
172
+ // Pinned pages go last, when the cache shrank below them: workers
173
+ // leave pages that are not cached to the main thread.
174
+ const pinned = page.array.pinned;
175
+ if (
176
+ step < this.slots.length * 2 &&
177
+ (page.used || (page.index >= pinned[0] && page.index < pinned[1]))
178
+ ) {
179
+ page.used = false;
180
+ continue;
181
+ }
182
+ this.drop(page);
183
+ return;
184
+ }
185
+ }
186
+
187
+ // Writes `page` to its array's file if it changed, and frees its slot.
188
+ drop(page) {
189
+ const array = page.array;
190
+ if (page.dirty) {
191
+ array.save(page.index, page.bytes);
192
+ this.writeBytes += PAGE_BYTES;
193
+ }
194
+ this.unmap(page);
195
+ }
196
+
197
+ allocate(array, index) {
198
+ if (++this.allocations % 64 === 0) {
199
+ this.measure();
200
+ this.shrink();
201
+ }
202
+ if (this.used + PAGE_BYTES > this.capacity && this.used) this.evict();
203
+ let slot = this.free.pop();
204
+ if (slot === undefined) {
205
+ slot = this.slots.length;
206
+ if (slot === this.arenas.length * this.arenaPages) {
207
+ this.arenas.push(
208
+ new WebAssembly.Memory({
209
+ initial: this.arenaPages,
210
+ maximum: this.arenaPages,
211
+ shared: true,
212
+ }).buffer,
213
+ );
214
+ this.version++;
215
+ this.generation++;
216
+ }
217
+ }
218
+ const arena = this.arenas[Math.floor(slot / this.arenaPages)];
219
+ let reuse = this.reuse[slot];
220
+ if (reuse?.arena !== arena) {
221
+ const offset = (slot % this.arenaPages) * PAGE_BYTES;
222
+ const bytes = new Uint8Array(arena, offset, PAGE_BYTES);
223
+ reuse = {
224
+ arena,
225
+ offset,
226
+ views: {},
227
+ page: {
228
+ array,
229
+ index,
230
+ bytes,
231
+ values: bytes,
232
+ words: undefined,
233
+ dirty: false,
234
+ used: true,
235
+ slot,
236
+ },
237
+ };
238
+ this.reuse[slot] = reuse;
239
+ }
240
+ const page = reuse.page;
241
+ const bytes = page.bytes;
242
+ if (array.restore(index, bytes)) this.readBytes += PAGE_BYTES;
243
+ else bytes.set(ZERO);
244
+ const { views, offset } = reuse;
245
+ const name = array.Type.name;
246
+ views[name] ??= new array.Type(arena, offset, array.items);
247
+ if (array.wordView)
248
+ views.Uint32Array ??= new Uint32Array(arena, offset, PAGE_BYTES / 4);
249
+ page.array = array;
250
+ page.index = index;
251
+ page.values = views[name];
252
+ page.words = array.wordView && views.Uint32Array;
253
+ page.dirty = false;
254
+ page.used = true;
255
+ this.slots[slot] = page;
256
+ this.used += PAGE_BYTES;
257
+ // A store far past the end would turn `pages` into a slow dictionary.
258
+ while (array.pages.length < index) array.pages.push(undefined);
259
+ array.pages[index] = page;
260
+ if (array.directory) array.map(index, slot);
261
+ return page;
262
+ }
263
+
264
+ unmap(page) {
265
+ page.array.pages[page.index] = undefined;
266
+ if (page.array.directory) page.array.directory[page.index] = -1;
267
+ this.slots[page.slot] = undefined;
268
+ this.free.push(page.slot);
269
+ this.used -= PAGE_BYTES;
270
+ }
271
+
272
+ release(array) {
273
+ for (const page of array.pages) if (page) this.unmap(page);
274
+ array.pages.length = 0;
275
+ }
276
+
277
+ dispose() {
278
+ const progress = this.directory
279
+ ? new Progress("Cleaning temporary files", this.files.size + 1)
280
+ : undefined;
281
+ let closed = 0;
282
+ for (const fd of this.files.keys()) {
283
+ this.close(fd);
284
+ progress?.update(++closed);
285
+ }
286
+ if (this.directory) removePath(this.directory, { recursive: true });
287
+ this.directory = undefined;
288
+ this.slots.length = 0;
289
+ this.reuse.length = 0;
290
+ this.free.length = 0;
291
+ this.arenas.length = 0;
292
+ progress?.finish();
293
+ if (this.readBytes || this.writeBytes) {
294
+ console.log(
295
+ `Temporary array I/O: ${(this.readBytes / 1048576).toFixed(1)} MiB read, ${(this.writeBytes / 1048576).toFixed(1)} MiB written`,
296
+ );
297
+ }
298
+ }
299
+ }
300
+
301
+ export class PagedArray {
302
+ constructor(workspace, Type, length = 0) {
303
+ this.workspace = workspace;
304
+ this.Type = Type;
305
+ this.length = length;
306
+ this.items = PAGE_BYTES / Type.BYTES_PER_ELEMENT;
307
+ this.mask = this.items - 1;
308
+ // Exact for the power of two, and faster than dividing.
309
+ this.inverse = 1 / this.items;
310
+ this.pages = [];
311
+ // Pages not cached live in files: block b in stripe b % stripes, at
312
+ // block b / stripes, so that threads reading pages at once share files
313
+ // less; see share().
314
+ this.stripes = 1;
315
+ this.fds = [];
316
+ this.sizes = [];
317
+ // Also view each page as 32-bit words; see at().
318
+ this.wordView = false;
319
+ this.directory = undefined;
320
+ // The blocks whose pages stay cached, for workers to write.
321
+ this.pinned = [0, 0];
322
+ }
323
+
324
+ // Keeps a shared block -> slot directory (-1 when not cached), so workers
325
+ // can read this array's cached pages through a SharedColumn.
326
+ // With `stripes`, before any page is written, its pages are spread over
327
+ // that many files.
328
+ share(stripes = 1) {
329
+ if (this.directory) return this;
330
+ this.stripes = stripes;
331
+ this.directory = new Int32Array(new SharedArrayBuffer(64 * 4)).fill(-1);
332
+ this.workspace.version++;
333
+ for (const page of this.pages) if (page) this.map(page.index, page.slot);
334
+ return this;
335
+ }
336
+ // The files of the pages that are not cached, created if there are none.
337
+ files() {
338
+ for (let stripe = 0; stripe < this.stripes; stripe++)
339
+ this.fds[stripe] ??= this.workspace.file();
340
+ return this.fds;
341
+ }
342
+ // Writes the page of block `block`. Writing past the end of a file
343
+ // extends it and fills the gap with zeros.
344
+ save(block, bytes) {
345
+ const stripe = block % this.stripes;
346
+ const position = Math.floor(block / this.stripes) * PAGE_BYTES;
347
+ this.fds[stripe] ??= this.workspace.file();
348
+ writeAll(this.fds[stripe], bytes, position);
349
+ this.sizes[stripe] = Math.max(
350
+ this.sizes[stripe] ?? 0,
351
+ position + PAGE_BYTES,
352
+ );
353
+ }
354
+ // Reads the page of block `block` into `bytes` if a file holds it.
355
+ restore(block, bytes) {
356
+ const stripe = block % this.stripes;
357
+ const position = Math.floor(block / this.stripes) * PAGE_BYTES;
358
+ if (position >= (this.sizes[stripe] ?? 0)) return false;
359
+ readAll(this.fds[stripe], bytes, position);
360
+ return true;
361
+ }
362
+ map(block, slot) {
363
+ if (block >= this.directory.length) {
364
+ const old = this.directory;
365
+ const size = Math.max(block + 1, old.length * 2);
366
+ this.directory = new Int32Array(new SharedArrayBuffer(size * 4)).fill(-1);
367
+ this.directory.set(old);
368
+ this.workspace.version++;
369
+ }
370
+ this.directory[block] = slot;
371
+ }
372
+
373
+ page(index, write = false) {
374
+ return this.at(index, write).values;
375
+ }
376
+ // The position of `index` in its page, after page() or at().
377
+ offset(index) {
378
+ return index & this.mask;
379
+ }
380
+ // The page of `index`: `values`, and `words` if wordView is set.
381
+ at(index, write = false) {
382
+ // Poll in-memory loops too, without an atomic read on every access.
383
+ if ((++this.workspace.accesses & 4095) === 0) this.workspace.check();
384
+ const block = Math.floor(index * this.inverse);
385
+ const page = this.pages[block] ?? this.workspace.allocate(this, block);
386
+ page.used = true;
387
+ if (write) page.dirty = true;
388
+ return page;
389
+ }
390
+
391
+ get(index) {
392
+ return this.page(index)[index & this.mask];
393
+ }
394
+ set(index, value) {
395
+ this.page(index, true)[index & this.mask] = value;
396
+ }
397
+ push(value) {
398
+ this.set(this.length++, value);
399
+ }
400
+
401
+ // Loads the pages of items [from, to), for worker threads to use.
402
+ load(from, to, write = false) {
403
+ for (let index = from; index < to; index += this.items)
404
+ this.at(index, write);
405
+ this.at(to - 1, write);
406
+ }
407
+
408
+ // Reads `count` items from `base` into `output` from `at`. set() copies
409
+ // long runs out of shared memory many times faster than a loop.
410
+ read(base, output, count = output.length, at = 0) {
411
+ let offset = 0;
412
+ while (offset < count) {
413
+ const index = base + offset;
414
+ const page = this.page(index);
415
+ const within = index & this.mask;
416
+ const size = Math.min(count - offset, this.items - within);
417
+ if (size > 64)
418
+ output.set(page.subarray(within, within + size), at + offset);
419
+ else
420
+ for (let i = 0; i < size; i++)
421
+ output[at + offset + i] = page[within + i];
422
+ offset += size;
423
+ }
424
+ return output;
425
+ }
426
+
427
+ write(base, values, count = values.length) {
428
+ let offset = 0;
429
+ while (offset < count) {
430
+ const index = base + offset;
431
+ const page = this.page(index, true);
432
+ const within = index & this.mask;
433
+ const size = Math.min(count - offset, this.items - within);
434
+ if (size > 64) page.set(values.subarray(offset, offset + size), within);
435
+ else for (let i = 0; i < size; i++) page[within + i] = values[offset + i];
436
+ offset += size;
437
+ }
438
+ this.length = Math.max(this.length, base + count);
439
+ }
440
+
441
+ dispose() {
442
+ this.workspace.release(this);
443
+ for (const fd of this.fds) if (fd !== undefined) this.workspace.close(fd);
444
+ this.fds = [];
445
+ this.sizes = [];
446
+ this.length = 0;
447
+ }
448
+ }
449
+
450
+ // A page of `fill` words for each type, made once per thread: columns are
451
+ // made anew whenever shared arrays change.
452
+ const EMPTY = new Map();
453
+ function empty(Type, fill) {
454
+ const key = `${Type.name} ${fill}`;
455
+ let page = EMPTY.get(key);
456
+ if (!page) {
457
+ const buffer = new ArrayBuffer(PAGE_BYTES);
458
+ page = {
459
+ values: new Type(buffer),
460
+ words: new Uint32Array(buffer).fill(fill),
461
+ };
462
+ EMPTY.set(key, page);
463
+ }
464
+ return page;
465
+ }
466
+
467
+ // Bytes that a thread reads at a time of a page that is not cached, and how
468
+ // many such parts it keeps for each column.
469
+ const PART_BYTES = 16384;
470
+ const PARTS = 64;
471
+
472
+ // The parts of pages that a thread read from the file of a column of `Type`.
473
+ // The main thread changes no page while workers run a job; reset() forgets
474
+ // the parts read for the next.
475
+ export class Parts {
476
+ constructor(Type) {
477
+ const buffer = new ArrayBuffer(PARTS * PART_BYTES);
478
+ this.views = { values: new Type(buffer), words: new Uint32Array(buffer) };
479
+ this.bytes = new Uint8Array(buffer);
480
+ this.items = PART_BYTES / Type.BYTES_PER_ELEMENT;
481
+ // Part number -> position, and the part at each position.
482
+ this.positions = new Map();
483
+ this.numbers = new Float64Array(PARTS).fill(-1);
484
+ this.next = 0;
485
+ // Bytes read from files since taken; see Pool.run().
486
+ this.bytesRead = 0;
487
+ }
488
+ reset() {
489
+ if (!this.positions.size) return;
490
+ this.positions.clear();
491
+ this.numbers.fill(-1);
492
+ }
493
+ // The position of part `number` of `column`, read if needed.
494
+ get(column, number) {
495
+ let position = this.positions.get(number);
496
+ if (position !== undefined) return position;
497
+ // Replaces the oldest part; callers use a part before the next at().
498
+ position = this.next;
499
+ this.next = (position + 1) % PARTS;
500
+ if (this.numbers[position] >= 0)
501
+ this.positions.delete(this.numbers[position]);
502
+ this.numbers[position] = number;
503
+ this.positions.set(number, position);
504
+ const bytes = this.bytes.subarray(
505
+ position * PART_BYTES,
506
+ (position + 1) * PART_BYTES,
507
+ );
508
+ const at = column.locate(number * PART_BYTES);
509
+ let done = 0;
510
+ while (done < PART_BYTES) {
511
+ const count = readSync(
512
+ column.fd,
513
+ bytes,
514
+ done,
515
+ PART_BYTES - done,
516
+ at + done,
517
+ );
518
+ if (!count) break;
519
+ done += count;
520
+ }
521
+ this.bytesRead += done;
522
+ // Past the end of the file, pages are zeros.
523
+ if (done < PART_BYTES) bytes.fill(0, done);
524
+ return position;
525
+ }
526
+ }
527
+
528
+ // A thread's views of the arenas by type, made once for each generation of
529
+ // arenas: columns are made anew whenever shared arrays change.
530
+ export class ArenaViews {
531
+ constructor() {
532
+ this.generation = -1;
533
+ this.arenas = [];
534
+ this.types = new Map();
535
+ }
536
+ use(arenas, generation) {
537
+ if (generation === this.generation) return;
538
+ this.generation = generation;
539
+ this.arenas = arenas;
540
+ this.types.clear();
541
+ }
542
+ get(Type) {
543
+ let views = this.types.get(Type);
544
+ if (!views) {
545
+ views = this.arenas.map((arena) => ({
546
+ values: new Type(arena),
547
+ words: new Uint32Array(arena),
548
+ }));
549
+ this.types.set(Type, views);
550
+ }
551
+ return views;
552
+ }
553
+ }
554
+
555
+ // A worker's read-only view of a shared PagedArray. Pages are views of whole
556
+ // arenas, so offset() places an index within the last page found. A page that
557
+ // is not cached reads as `fill` words and sets `missed`, unless the column
558
+ // has the array's file `fd` and Parts: then the part around the index is
559
+ // read from the file, where the page is while not cached.
560
+ export class SharedColumn {
561
+ constructor(
562
+ views,
563
+ directory,
564
+ Type,
565
+ fill = 0,
566
+ fds = undefined,
567
+ parts = undefined,
568
+ ) {
569
+ this.directory = directory;
570
+ this.arenas = views.get(Type);
571
+ this.arenaPages = this.arenas.length
572
+ ? this.arenas[0].values.byteLength / PAGE_BYTES
573
+ : 1;
574
+ this.items = PAGE_BYTES / Type.BYTES_PER_ELEMENT;
575
+ this.mask = this.items - 1;
576
+ // Exact for the power of two, and faster than dividing.
577
+ this.inverse = 1 / this.items;
578
+ this.empty = empty(Type, fill);
579
+ this.base = 0;
580
+ // Whether a page was not cached, and the first such block.
581
+ this.missed = false;
582
+ this.block = 0;
583
+ this.fds = fds;
584
+ this.fd = -1;
585
+ this.parts = parts;
586
+ parts?.reset();
587
+ }
588
+ // The position of byte `offset` of the array in its file, which it sets
589
+ // as `fd`.
590
+ locate(offset) {
591
+ const block = Math.floor(offset / PAGE_BYTES);
592
+ const stripes = this.fds.length;
593
+ this.fd = this.fds[block % stripes];
594
+ return (
595
+ Math.floor(block / stripes) * PAGE_BYTES + offset - block * PAGE_BYTES
596
+ );
597
+ }
598
+ at(index) {
599
+ const block = Math.floor(index * this.inverse);
600
+ const slot = block < this.directory.length ? this.directory[block] : -1;
601
+ if (slot < 0) {
602
+ if (this.fds) return this.part(index);
603
+ if (!this.missed) this.block = block;
604
+ this.missed = true;
605
+ this.base = 0;
606
+ return this.empty;
607
+ }
608
+ this.base = (slot % this.arenaPages) * this.items;
609
+ return this.arenas[Math.floor(slot / this.arenaPages)];
610
+ }
611
+ part(index) {
612
+ const parts = this.parts;
613
+ const number = Math.floor(index / parts.items);
614
+ const position = parts.get(this, number);
615
+ this.base = (position - (number % (PAGE_BYTES / PART_BYTES))) * parts.items;
616
+ return parts.views;
617
+ }
618
+ offset(index) {
619
+ return this.base + (index & this.mask);
620
+ }
621
+ page(index) {
622
+ return this.at(index).values;
623
+ }
624
+ get(index) {
625
+ return this.page(index)[this.offset(index)];
626
+ }
627
+ }
628
+
629
+ // Gathers columns into the order of `indices`: row indices[i] of a column with
630
+ // `rows` rows becomes row i. Each output window, 7/8 of the cache, is
631
+ // filled while the column is read in order, so a column larger than the cache
632
+ // costs a few sequential passes instead of a page load per row.
633
+ export class Permutation {
634
+ constructor(workspace, indices, rows, label) {
635
+ this.workspace = workspace;
636
+ this.length = indices.length;
637
+ this.rows = rows;
638
+ this.block = new Uint32Array(16384);
639
+ // Source row -> target row + 1; 0 marks a row that is dropped.
640
+ this.targets = new IndexArray(workspace);
641
+ const progress = label && new Progress(label, indices.length);
642
+ for (let base = 0; base < indices.length; base += this.block.length) {
643
+ const count = Math.min(this.block.length, indices.length - base);
644
+ indices.read(base, this.block, count);
645
+ for (let i = 0; i < count; i++)
646
+ this.targets.set(this.block[i], base + i + 1);
647
+ progress?.update(base + count);
648
+ }
649
+ progress?.finish();
650
+ }
651
+
652
+ // Returns `source` gathered into a new column, and disposes `source`. The
653
+ // output window's pages stay cached while workers copy blocks of source
654
+ // rows into it; rows across pages are left to the main thread. The output
655
+ // spreads over as many files as `source`.
656
+ // With `words`, rows are copied as 32-bit words, which keeps any bits of
657
+ // floats, and the output has word views; see PagedArray.at().
658
+ apply(source, stride, pool, words = false, label = undefined) {
659
+ const output = new PagedArray(
660
+ this.workspace,
661
+ source.Type,
662
+ this.length * stride,
663
+ );
664
+ output.wordView = words;
665
+ output.share(source.stripes);
666
+ source.share();
667
+ const record = words ? new Uint32Array(stride) : new source.Type(stride);
668
+ const { nodes: targets, later } = pool;
669
+ const block = later.length;
670
+ // The window's pages are pinned; the rest of the cache holds the source
671
+ // rows a block at a time.
672
+ const window = Math.floor(
673
+ (this.workspace.capacity * 7) /
674
+ 8 /
675
+ (stride * source.Type.BYTES_PER_ELEMENT),
676
+ );
677
+ const progress =
678
+ label && new Progress(label, Math.ceil(this.length / window) * this.rows);
679
+ for (let start = 0; start < this.length; start += window) {
680
+ const end = Math.min(this.length, start + window);
681
+ output.pinned = [
682
+ Math.floor((start * stride) / output.items),
683
+ Math.floor((end * stride - 1) / output.items) + 1,
684
+ ];
685
+ output.load(start * stride, end * stride, true);
686
+ for (let base = 0; base < this.rows; base += block) {
687
+ const count = Math.min(block, this.rows - base);
688
+ this.targets.read(base, targets, count);
689
+ source.load(base * stride, (base + count) * stride);
690
+ pool.copy(source, output, stride, count, base, start, end, words);
691
+ for (let i = 0; i < count; i++) {
692
+ if (!later[i]) continue;
693
+ const from = (base + i) * stride;
694
+ const to = (targets[i] - 1) * stride;
695
+ if (!words) {
696
+ source.read(from, record);
697
+ output.write(to, record);
698
+ continue;
699
+ }
700
+ // Rows of words lie within a page.
701
+ const values = source.at(from).words;
702
+ for (let d = 0; d < stride; d++)
703
+ record[d] = values[(from & source.mask) + d];
704
+ const out = output.at(to, true).words;
705
+ for (let d = 0; d < stride; d++)
706
+ out[(to & output.mask) + d] = record[d];
707
+ }
708
+ progress?.update(Math.floor(start / window) * this.rows + base + count);
709
+ }
710
+ }
711
+ output.pinned = [0, 0];
712
+ source.dispose();
713
+ progress?.finish();
714
+ return output;
715
+ }
716
+
717
+ dispose() {
718
+ this.targets.dispose();
719
+ }
720
+ }
721
+
722
+ // Keep hot integer lookups separate from the mixed attribute types so V8 can
723
+ // specialize their TypedArray access during heap and spatial-grid traversal.
724
+ export class IndexArray extends PagedArray {
725
+ constructor(workspace) {
726
+ super(workspace, Uint32Array);
727
+ }
728
+ get(index) {
729
+ return this.page(index)[index & this.mask];
730
+ }
731
+ set(index, value) {
732
+ this.page(index, true)[index & this.mask] = value;
733
+ }
734
+ }
735
+
736
+ // Cleanup never throws, so it cannot replace a conversion error. A failure is
737
+ // reported with the path, so the leftover can be removed by hand.
738
+ export function removePath(file, options) {
739
+ try {
740
+ rmSync(file, options);
741
+ } catch (error) {
742
+ if (error.code !== "ENOENT")
743
+ console.warn(`Could not remove ${file} (${error.code ?? error.message})`);
744
+ }
745
+ }
746
+
747
+ export function readAll(fd, bytes, position) {
748
+ let offset = 0;
749
+ while (offset < bytes.byteLength) {
750
+ const count = readSync(
751
+ fd,
752
+ bytes,
753
+ offset,
754
+ bytes.byteLength - offset,
755
+ position + offset,
756
+ );
757
+ if (!count) throw new Error("Truncated input or temporary file");
758
+ offset += count;
759
+ }
760
+ return bytes;
761
+ }
762
+
763
+ export function writeAll(fd, bytes, position) {
764
+ let offset = 0;
765
+ while (offset < bytes.byteLength) {
766
+ const count = writeSync(
767
+ fd,
768
+ bytes,
769
+ offset,
770
+ bytes.byteLength - offset,
771
+ position + offset,
772
+ );
773
+ if (!count) throw new Error("Unable to write output or temporary file");
774
+ offset += count;
775
+ }
776
+ }