vectrojs 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +821 -4
- package/bin/vectro.mjs +82 -0
- package/dist/vectro.wasm +0 -0
- package/mcp/server.mjs +288 -0
- package/package.json +84 -6
- package/src/arrow.mjs +905 -0
- package/src/bitpack.mjs +135 -0
- package/src/browser.mjs +129 -0
- package/src/bytes.mjs +60 -0
- package/src/csv-kernel.mjs +205 -0
- package/src/csv.mjs +270 -0
- package/src/dataset.mjs +1763 -0
- package/src/dates.mjs +139 -0
- package/src/emitter.mjs +109 -0
- package/src/engine.mjs +881 -0
- package/src/files-browser.mjs +104 -0
- package/src/files-node.mjs +59 -0
- package/src/frames.mjs +191 -0
- package/src/fuse.mjs +512 -0
- package/src/interp.mjs +305 -0
- package/src/join.mjs +534 -0
- package/src/kernels.mjs +312 -0
- package/src/ndjson.mjs +136 -0
- package/src/node.mjs +120 -0
- package/src/pack.mjs +114 -0
- package/src/parallel-sort.mjs +99 -0
- package/src/partition-kernels.mjs +388 -0
- package/src/partition.mjs +395 -0
- package/src/plan.mjs +551 -0
- package/src/portable.mjs +21 -0
- package/src/rank-kernels.mjs +81 -0
- package/src/rows.mjs +91 -0
- package/src/serve/catalog.mjs +148 -0
- package/src/serve/flight.mjs +270 -0
- package/src/serve/formats.mjs +87 -0
- package/src/serve/http.mjs +182 -0
- package/src/serve/index.mjs +9 -0
- package/src/serve/odata.mjs +291 -0
- package/src/serve/pg.mjs +588 -0
- package/src/serve/pgcatalog.mjs +220 -0
- package/src/serve/pgtypes.mjs +199 -0
- package/src/sketch.mjs +75 -0
- package/src/sort-kernels.mjs +117 -0
- package/src/sort.mjs +102 -0
- package/src/split-worker-core.mjs +252 -0
- package/src/split-worker-node.mjs +8 -0
- package/src/split-worker.mjs +7 -0
- package/src/split.mjs +741 -0
- package/src/sql.mjs +1558 -0
- package/src/sqltext.mjs +132 -0
- package/src/static-kernels.mjs +164 -0
- package/src/stream-worker.mjs +553 -0
- package/src/table-kernels.mjs +323 -0
- package/src/table.mjs +2206 -0
- package/src/textmap.mjs +159 -0
- package/src/vectors.mjs +138 -0
- package/src/wasm.mjs +254 -0
- package/src/window.mjs +160 -0
- package/src/worker-browser.mjs +21 -0
- package/src/worker-loop.mjs +106 -0
- package/src/worker.mjs +22 -0
- package/src/zones.mjs +75 -0
package/src/engine.mjs
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
// The engine core. It imports nothing platform-specific: threads are a
|
|
2
|
+
// plug-in (`platform`, provided by node.mjs), and without one the engine runs
|
|
3
|
+
// single-threaded on plain (non-shared) memory, which works in browsers
|
|
4
|
+
// without cross-origin isolation and on edge runtimes.
|
|
5
|
+
import { C, OP, MAX_ARGS, MAX_OUTPUTS, TYPES, WASM_MEMORY, SEQ_WRITING, TAG_SHIFT, runUnits, rangeKernel, hasWasmKernel, unitCount, maxUnits, tagOf } from './kernels.mjs';
|
|
6
|
+
import { buildStaticModule } from './static-kernels.mjs';
|
|
7
|
+
import { analyze, generate } from './fuse.mjs';
|
|
8
|
+
import { interpretReduce, interpretMap } from './interp.mjs';
|
|
9
|
+
import { Table } from './table.mjs';
|
|
10
|
+
import { decodeOne } from './pack.mjs';
|
|
11
|
+
|
|
12
|
+
const COMPRESS_ALL = !!globalThis.process?.env?.VECTRO_COMPRESS; // tests: pack every table
|
|
13
|
+
const POISON = !!globalThis.process?.env?.VECTRO_POISON; // tests: freed memory overwritten (a read after free gives wrong answers)
|
|
14
|
+
import { parseCSV, parseCSVAsync } from './csv.mjs';
|
|
15
|
+
import { readArrow } from './arrow.mjs';
|
|
16
|
+
import { encode } from './bytes.mjs';
|
|
17
|
+
|
|
18
|
+
// Below these sizes the work is done on the calling thread: waking other
|
|
19
|
+
// threads would cost more than it saves. Tuned on an M1 Pro; see bench.mjs.
|
|
20
|
+
const DEFAULT_MIN_PARALLEL = {
|
|
21
|
+
[OP.SUM]: 100_000, [OP.MIN]: 100_000, [OP.MAX]: 100_000, [OP.DOT]: 100_000,
|
|
22
|
+
[OP.ADD]: 50_000, [OP.SCALE]: 50_000, [OP.COUNT_GT]: 100_000,
|
|
23
|
+
[OP.HEAVY]: 5_000, [OP.SORT_CHUNK]: 50_000, [OP.COUNT_BYTES]: 500_000,
|
|
24
|
+
};
|
|
25
|
+
// WASM SIMD kernels are ~5-20x faster per thread, so it takes more data
|
|
26
|
+
// before splitting it up is worth it.
|
|
27
|
+
const DEFAULT_MIN_PARALLEL_WASM = {
|
|
28
|
+
[OP.SUM]: 200_000, [OP.MIN]: 200_000, [OP.MAX]: 200_000, [OP.DOT]: 200_000,
|
|
29
|
+
[OP.COUNT_GT]: 200_000, [OP.ADD]: 100_000, [OP.SCALE]: 100_000, [OP.FUSED]: 100_000,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const PAGE = 65536;
|
|
33
|
+
const MAX_PAGES = 65536; // 4GB, the most a 32-bit WASM address can reach
|
|
34
|
+
const HEAP_START = 64; // keep address 0 unused
|
|
35
|
+
const align16 = (n) => (n + 15) & ~15;
|
|
36
|
+
|
|
37
|
+
// Can this runtime compile WebAssembly from bytes? (Cloudflare Workers and
|
|
38
|
+
// Vercel Edge can't: they only run modules bundled ahead of time.)
|
|
39
|
+
let compileAllowed;
|
|
40
|
+
export function canCompileWasm() {
|
|
41
|
+
if (compileAllowed === undefined) {
|
|
42
|
+
try {
|
|
43
|
+
new WebAssembly.Module(new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]));
|
|
44
|
+
compileAllowed = true;
|
|
45
|
+
} catch {
|
|
46
|
+
compileAllowed = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return compileAllowed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class Engine {
|
|
53
|
+
// options:
|
|
54
|
+
// threads how many threads (needs a platform with workers; default: all cores)
|
|
55
|
+
// platform thread support, provided by the Node entry (node.mjs)
|
|
56
|
+
// jit true: compile a kernel per expression (fastest);
|
|
57
|
+
// false: run expressions with the vectorized interpreter;
|
|
58
|
+
// 'auto' (default): jit wherever the runtime allows it
|
|
59
|
+
// wasmModule the precompiled dist/vectro.wasm, for runtimes that can't
|
|
60
|
+
// compile WebAssembly (Cloudflare Workers)
|
|
61
|
+
// initialMemoryMB memory to start with (single-thread default 16): plain
|
|
62
|
+
// memory moves when it grows, so starting bigger avoids that
|
|
63
|
+
constructor({ threads, spin = 20_000, minParallel, wasm = true, jit = 'auto', wasmModule = null, platform = null, initialMemoryMB } = {}) {
|
|
64
|
+
const canThread = Boolean(platform) && typeof SharedArrayBuffer === 'function';
|
|
65
|
+
this.T = canThread ? Math.max(1, threads ?? platform.cpus()) : 1;
|
|
66
|
+
this.W = this.T - 1; // the calling thread does part of the work itself
|
|
67
|
+
this.isShared = this.W > 0;
|
|
68
|
+
this.platform = platform;
|
|
69
|
+
// Browsers don't let the page's main thread block (Atomics.wait throws
|
|
70
|
+
// there). With such a platform, sync calls run on the calling thread
|
|
71
|
+
// alone and only the async API uses the workers.
|
|
72
|
+
this.canBlock = platform?.canBlock !== false;
|
|
73
|
+
this.spin = spin;
|
|
74
|
+
this.minParallel = minParallel ?? DEFAULT_MIN_PARALLEL;
|
|
75
|
+
this.minParallelWasm = minParallel ?? DEFAULT_MIN_PARALLEL_WASM;
|
|
76
|
+
this.useWasm = wasm;
|
|
77
|
+
|
|
78
|
+
const compile = canCompileWasm();
|
|
79
|
+
this.jit = jit === 'auto' ? compile : jit;
|
|
80
|
+
if (this.jit && !compile) throw new Error('vectro: this runtime cannot compile WebAssembly, so jit must be false');
|
|
81
|
+
if (!wasmModule && !compile) {
|
|
82
|
+
throw new Error("vectro: this runtime cannot compile WebAssembly; pass the precompiled module: new Engine({ wasmModule }) with dist/vectro.wasm");
|
|
83
|
+
}
|
|
84
|
+
if (wasmModule && this.isShared) throw new Error('vectro: a precompiled wasmModule is single-threaded; use threads: 1');
|
|
85
|
+
|
|
86
|
+
// One WebAssembly memory; arrays are allocated in it. Shared between
|
|
87
|
+
// threads when there are workers, plain memory otherwise.
|
|
88
|
+
const Buf = this.isShared ? SharedArrayBuffer : ArrayBuffer;
|
|
89
|
+
const initialPages = Math.max(1, Math.ceil(((initialMemoryMB ?? (this.isShared ? 0 : 16)) * 2 ** 20) / PAGE));
|
|
90
|
+
this.memory = new WebAssembly.Memory({ initial: initialPages, maximum: MAX_PAGES, shared: this.isShared });
|
|
91
|
+
this.module = wasmModule ?? new WebAssembly.Module(buildStaticModule(this.isShared));
|
|
92
|
+
this.kernels = new WebAssembly.Instance(this.module, { env: { memory: this.memory } }).exports;
|
|
93
|
+
this.memBuffers = new WeakSet(); // every buffer object handed out for this memory
|
|
94
|
+
this.allocInfo = new WeakMap(); // array -> { ptr, n, Type }, to rebuild views (see live())
|
|
95
|
+
this.heapTop = HEAP_START;
|
|
96
|
+
this.freeList = []; // [{ ptr, size }] sorted by ptr
|
|
97
|
+
this.sizes = new Map(); // ptr -> allocated size
|
|
98
|
+
|
|
99
|
+
this.ctrl = new Int32Array(new Buf(4 * (C.ARGS + MAX_ARGS * 4)));
|
|
100
|
+
this.ctrlU = new Uint32Array(this.ctrl.buffer);
|
|
101
|
+
this.sc = new Float64Array(new Buf(8 * 32));
|
|
102
|
+
this.res = new Float64Array(new Buf(8 * maxUnits(this.T) * MAX_OUTPUTS)); // result slots per unit of work
|
|
103
|
+
this.seq = 0;
|
|
104
|
+
this.ids = new WeakMap();
|
|
105
|
+
this.nextId = WASM_MEMORY + 1;
|
|
106
|
+
this.ports = [];
|
|
107
|
+
this.workers = [];
|
|
108
|
+
this.scratch = null;
|
|
109
|
+
this.patBuf = null;
|
|
110
|
+
this.compiled = new Map(); // spec -> Kernel
|
|
111
|
+
this.nextKernelId = 0;
|
|
112
|
+
this.interpScratch = null; // the interpreter's registers (see interpreterScratch)
|
|
113
|
+
if (!this.jit) this.interpreterScratch(64 * 4096 * 8);
|
|
114
|
+
|
|
115
|
+
for (let i = 1; i <= this.W; i++) {
|
|
116
|
+
const { worker, port } = platform.spawn({
|
|
117
|
+
ctrlBuf: this.ctrl.buffer, scalarBuf: this.sc.buffer, resultBuf: this.res.buffer,
|
|
118
|
+
spin, index: i, module: this.module, memory: this.memory,
|
|
119
|
+
});
|
|
120
|
+
this.ports.push(port);
|
|
121
|
+
this.workers.push(worker);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#received(port) {
|
|
126
|
+
const out = [];
|
|
127
|
+
let m;
|
|
128
|
+
while ((m = this.platform.receive(port))) out.push(m.message);
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- memory -------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
#memoryBuffer() {
|
|
135
|
+
const b = this.memory.buffer;
|
|
136
|
+
this.memBuffers.add(b);
|
|
137
|
+
return b;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Allocate a typed array in the engine's memory. These arrays get the
|
|
141
|
+
// WASM SIMD kernels; other shared arrays fall back to JS kernels.
|
|
142
|
+
alloc(Type, n) {
|
|
143
|
+
const size = align16(Math.max(16, n * Type.BYTES_PER_ELEMENT));
|
|
144
|
+
let ptr = -1;
|
|
145
|
+
for (let i = 0; i < this.freeList.length; i++) {
|
|
146
|
+
const blk = this.freeList[i];
|
|
147
|
+
if (blk.size < size) continue;
|
|
148
|
+
ptr = blk.ptr;
|
|
149
|
+
if (blk.size === size) this.freeList.splice(i, 1);
|
|
150
|
+
else { blk.ptr += size; blk.size -= size; }
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (ptr < 0) {
|
|
154
|
+
ptr = this.heapTop;
|
|
155
|
+
const top = ptr + size;
|
|
156
|
+
const have = this.memory.buffer.byteLength;
|
|
157
|
+
if (top > have) {
|
|
158
|
+
const havePages = have / PAGE;
|
|
159
|
+
let pages = Math.ceil((top - have) / PAGE);
|
|
160
|
+
// Plain memory moves when it grows (old views go stale), so grow in
|
|
161
|
+
// big steps to make that rare.
|
|
162
|
+
if (!this.isShared) pages = Math.max(pages, Math.min(MAX_PAGES - havePages, Math.max(256, havePages >> 1)));
|
|
163
|
+
// (nothing may end at the 4GB address itself: kernels' end pointers would wrap to 0)
|
|
164
|
+
if (havePages + pages > MAX_PAGES || top > MAX_PAGES * PAGE - 64) throw new RangeError('vectro: out of memory (4GB limit)');
|
|
165
|
+
this.memory.grow(pages);
|
|
166
|
+
}
|
|
167
|
+
this.heapTop = top;
|
|
168
|
+
}
|
|
169
|
+
this.sizes.set(ptr, size);
|
|
170
|
+
const view = new Type(this.#memoryBuffer(), ptr, n);
|
|
171
|
+
this.allocInfo.set(view, { ptr, n, Type });
|
|
172
|
+
return view;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Byte offset of a scratch area of at least `bytes` for the interpreter's
|
|
176
|
+
// registers. Reserved once (and only regrown for unusually big
|
|
177
|
+
// expressions), so running queries doesn't grow memory.
|
|
178
|
+
interpreterScratch(bytes) {
|
|
179
|
+
if (!this.interpScratch || this.interpScratch.length < bytes) {
|
|
180
|
+
if (this.interpScratch) this.free(this.interpScratch);
|
|
181
|
+
this.interpScratch = this.alloc(Uint8Array, bytes);
|
|
182
|
+
}
|
|
183
|
+
return this.live(this.interpScratch).byteOffset;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Plain (non-shared) WebAssembly memory gets a new buffer whenever it
|
|
187
|
+
// grows, and views of the old one become empty. live() returns a working
|
|
188
|
+
// view of the same array; every engine method calls it on its inputs, so
|
|
189
|
+
// stale arrays can still be passed in. Shared memory never goes stale.
|
|
190
|
+
live(ta) {
|
|
191
|
+
const info = this.allocInfo.get(ta);
|
|
192
|
+
const stale = ta.buffer.detached ?? (ta.byteLength === 0 && (info?.n ?? 0) > 0);
|
|
193
|
+
if (!stale) return ta;
|
|
194
|
+
if (!info) throw new Error('vectro: this array is stale (engine memory grew); use engine.live(array) on the array engine.alloc() returned');
|
|
195
|
+
const fresh = new info.Type(this.#memoryBuffer(), info.ptr, info.n);
|
|
196
|
+
this.allocInfo.set(fresh, info);
|
|
197
|
+
return fresh;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
free(ta) {
|
|
201
|
+
const ptr = this.allocInfo.get(ta)?.ptr ?? ta.byteOffset;
|
|
202
|
+
const size = this.sizes.get(ptr);
|
|
203
|
+
if (size === undefined || !this.memBuffers.has(ta.buffer)) throw new Error('vectro: free() of an array not allocated by this engine');
|
|
204
|
+
this.sizes.delete(ptr);
|
|
205
|
+
if (POISON) new Uint8Array(this.memory.buffer, ptr, size).fill(0xff);
|
|
206
|
+
let i = 0;
|
|
207
|
+
while (i < this.freeList.length && this.freeList[i].ptr < ptr) i++;
|
|
208
|
+
this.freeList.splice(i, 0, { ptr, size });
|
|
209
|
+
// merge with neighbours
|
|
210
|
+
for (const j of [i, i - 1]) {
|
|
211
|
+
const a = this.freeList[j], b = this.freeList[j + 1];
|
|
212
|
+
if (a && b && a.ptr + a.size === b.ptr) { a.size += b.size; this.freeList.splice(j + 1, 1); }
|
|
213
|
+
}
|
|
214
|
+
const last = this.freeList[this.freeList.length - 1];
|
|
215
|
+
if (last && last.ptr + last.size === this.heapTop) { this.heapTop = last.ptr; this.freeList.pop(); }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
shared(Type, n) { return this.alloc(Type, n); }
|
|
219
|
+
|
|
220
|
+
// Copy a normal typed array into engine memory (costs a full copy).
|
|
221
|
+
toShared(ta) {
|
|
222
|
+
const out = this.alloc(ta.constructor, ta.length);
|
|
223
|
+
out.set(ta);
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#inMemory(ta) { return this.memBuffers.has(ta.buffer); }
|
|
228
|
+
_inMemory(ta) { return this.#inMemory(ta); }
|
|
229
|
+
|
|
230
|
+
#bufferId(ta) {
|
|
231
|
+
const buf = ta.buffer;
|
|
232
|
+
if (this.memBuffers.has(buf)) return WASM_MEMORY;
|
|
233
|
+
if (typeof SharedArrayBuffer !== 'function' || !(buf instanceof SharedArrayBuffer)) {
|
|
234
|
+
throw new TypeError('vectro: array must be shared memory (use engine.alloc() or engine.toShared())');
|
|
235
|
+
}
|
|
236
|
+
let id = this.ids.get(buf);
|
|
237
|
+
if (id === undefined) {
|
|
238
|
+
id = this.nextId++;
|
|
239
|
+
this.ids.set(buf, id);
|
|
240
|
+
for (const p of this.ports) p.postMessage({ id, buffer: buf });
|
|
241
|
+
}
|
|
242
|
+
return id;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---- dispatch -----------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
#wasmFor(op, arrays) {
|
|
248
|
+
return this.useWasm && hasWasmKernel(op, arrays) && arrays.every((a) => !a || this.#inMemory(a)) ? this.kernels : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Write a task into the control block and wake the workers.
|
|
252
|
+
#publish(op, n, arrays, W) {
|
|
253
|
+
const ctrl = this.ctrl, u = this.ctrlU;
|
|
254
|
+
// Seqlock: mark the task as being rewritten first. A worker still reading
|
|
255
|
+
// the previous task sees SEQ change and drops what it read.
|
|
256
|
+
Atomics.store(ctrl, C.SEQ, SEQ_WRITING);
|
|
257
|
+
for (let k = 0; k < (this.W ? MAX_ARGS : 0); k++) { // workers read the arrays from here
|
|
258
|
+
const base = C.ARGS + k * 4;
|
|
259
|
+
const ta = arrays[k];
|
|
260
|
+
if (!ta) { ctrl[base] = -1; continue; }
|
|
261
|
+
ctrl[base] = this.#bufferId(ta);
|
|
262
|
+
u[base + 1] = ta.byteOffset;
|
|
263
|
+
u[base + 2] = ta.length;
|
|
264
|
+
u[base + 3] = TYPES.findIndex((T) => ta instanceof T); // Buffer counts as Uint8Array
|
|
265
|
+
}
|
|
266
|
+
ctrl[C.OP] = op;
|
|
267
|
+
ctrl[C.N] = n;
|
|
268
|
+
ctrl[C.THREADS] = this.T;
|
|
269
|
+
ctrl[C.WASM] = W ? 1 : 0;
|
|
270
|
+
const tag = tagOf(++this.seq);
|
|
271
|
+
const units = unitCount(op, this.T, n, this.sc);
|
|
272
|
+
Atomics.store(ctrl, C.DONE, 0);
|
|
273
|
+
Atomics.store(ctrl, C.ERROR, 0);
|
|
274
|
+
Atomics.store(ctrl, C.NEXT, tag << TAG_SHIFT);
|
|
275
|
+
Atomics.store(ctrl, C.SEQ, this.seq); // publishes everything written above
|
|
276
|
+
Atomics.notify(ctrl, C.SEQ);
|
|
277
|
+
return { tag, units };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#results(op, units, mainError) {
|
|
281
|
+
if (Atomics.load(this.ctrl, C.ERROR)) this.#throwWorkerError(mainError);
|
|
282
|
+
return this.res.subarray(0, units * Math.max(1, op === OP.FUSED ? this.ctrl[C.NOUT] : 1));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Run `op` on all threads and block until done; returns the per-unit results.
|
|
286
|
+
#run(op, n, arrays, W = null, F = null) {
|
|
287
|
+
this.#assertIdle();
|
|
288
|
+
arrays = arrays.map((a) => a && this.live(a));
|
|
289
|
+
if (this.W && !this.canBlock) return this.#runHere(op, n, arrays, W, F);
|
|
290
|
+
const { tag, units } = this.#publish(op, n, arrays, W);
|
|
291
|
+
|
|
292
|
+
// The calling thread starts working right away; whichever workers wake up
|
|
293
|
+
// in time help. We wait only for unfinished units, never for idle threads.
|
|
294
|
+
let mainError = null;
|
|
295
|
+
runUnits(op, this.T, n, arrays, this.sc, this.ctrl, W, this.res, tag, F, 0, (err) => { mainError ??= err; });
|
|
296
|
+
|
|
297
|
+
const ctrl = this.ctrl;
|
|
298
|
+
let d = Atomics.load(ctrl, C.DONE);
|
|
299
|
+
for (let k = 0; d < units && k < this.spin * 10; k++) d = Atomics.load(ctrl, C.DONE);
|
|
300
|
+
while (d < units) {
|
|
301
|
+
Atomics.wait(ctrl, C.DONE, d);
|
|
302
|
+
d = Atomics.load(ctrl, C.DONE);
|
|
303
|
+
}
|
|
304
|
+
return this.#results(op, units, mainError);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Run every unit of `op` on the calling thread, without waking the workers
|
|
308
|
+
// (SEQ is not changed, so they don't see a task). For hosts that can't block.
|
|
309
|
+
#runHere(op, n, arrays, W, F) {
|
|
310
|
+
const ctrl = this.ctrl;
|
|
311
|
+
ctrl[C.OP] = op;
|
|
312
|
+
ctrl[C.N] = n;
|
|
313
|
+
ctrl[C.THREADS] = this.T;
|
|
314
|
+
const tag = tagOf(++this.seq); // a fresh tag: late workers can't claim these units
|
|
315
|
+
Atomics.store(ctrl, C.DONE, 0);
|
|
316
|
+
Atomics.store(ctrl, C.ERROR, 0);
|
|
317
|
+
Atomics.store(ctrl, C.NEXT, tag << TAG_SHIFT);
|
|
318
|
+
// (workers don't run this task: put back the last published SEQ so idle
|
|
319
|
+
// ones keep sleeping; a late one sees the fresh tag and skips the fields)
|
|
320
|
+
if (this.W && Atomics.load(ctrl, C.SEQ) === SEQ_WRITING) Atomics.store(ctrl, C.SEQ, this.#prevSeq);
|
|
321
|
+
let mainError = null;
|
|
322
|
+
runUnits(op, this.T, n, arrays, this.sc, ctrl, W, this.res, tag, F, 0, (err) => { mainError ??= err; });
|
|
323
|
+
if (mainError) throw mainError;
|
|
324
|
+
return this.#results(op, unitCount(op, this.T, n, this.sc), null);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Run `op` on the workers without blocking: the calling thread only waits
|
|
328
|
+
// (Atomics.waitAsync), so its event loop keeps serving other work. Without
|
|
329
|
+
// workers there is nothing to hand off, so this runs like #run.
|
|
330
|
+
async #runAsync(op, n, arrays, W = null, F = null) {
|
|
331
|
+
if (this.W === 0) return this.#run(op, n, arrays, W, F);
|
|
332
|
+
arrays = arrays.map((a) => a && this.live(a));
|
|
333
|
+
const { units } = this.#publish(op, n, arrays, W);
|
|
334
|
+
const ctrl = this.ctrl;
|
|
335
|
+
this.#inFlight = true;
|
|
336
|
+
// Atomics.waitAsync doesn't keep the event loop alive (and the workers
|
|
337
|
+
// are unref'd so scripts can exit), so hold a timer open while we wait.
|
|
338
|
+
const keepAlive = setTimeout(() => {}, 2 ** 31 - 1);
|
|
339
|
+
try {
|
|
340
|
+
let d = Atomics.load(ctrl, C.DONE);
|
|
341
|
+
while (d < units) {
|
|
342
|
+
if (typeof Atomics.waitAsync === 'function') {
|
|
343
|
+
const w = Atomics.waitAsync(ctrl, C.DONE, d);
|
|
344
|
+
if (w.async) await w.value;
|
|
345
|
+
} else {
|
|
346
|
+
await new Promise((r) => setTimeout(r, 0)); // browsers without waitAsync: poll
|
|
347
|
+
}
|
|
348
|
+
d = Atomics.load(ctrl, C.DONE);
|
|
349
|
+
}
|
|
350
|
+
} finally {
|
|
351
|
+
this.#inFlight = false;
|
|
352
|
+
clearTimeout(keepAlive);
|
|
353
|
+
}
|
|
354
|
+
// A worker failed: in browsers its message may still be on its way.
|
|
355
|
+
if (Atomics.load(ctrl, C.ERROR) && this.platform?.nextMessage) await this.#awaitWorkerError();
|
|
356
|
+
return this.#results(op, units, null);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async #awaitWorkerError() {
|
|
360
|
+
const deadline = Date.now() + 2000;
|
|
361
|
+
for (;;) {
|
|
362
|
+
for (const p of this.ports) {
|
|
363
|
+
for (const m of this.#received(p)) {
|
|
364
|
+
if (m.kind === 'error') { Atomics.store(this.ctrl, C.ERROR, 0); throw new Error(`vectro: a worker thread failed:\n${m.message}`); }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (Date.now() > deadline) return; // #results reports a generic failure
|
|
368
|
+
await Promise.race([this.platform.nextMessage(this.ports), new Promise((r) => setTimeout(r, 50))]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- sync and async drivers ------------------------------------------------
|
|
373
|
+
//
|
|
374
|
+
// Operations are written once, as generators that yield "run this on the
|
|
375
|
+
// threads" requests: { op, n, arrays, W?, F?, setup?(), after?(parts) }.
|
|
376
|
+
// _drive runs them blocking; _driveAsync awaits each run, one query at a
|
|
377
|
+
// time (a queue), so several awaited queries can be in flight safely.
|
|
378
|
+
|
|
379
|
+
#inFlight = false; // an async run is on the workers right now
|
|
380
|
+
#queue = Promise.resolve();
|
|
381
|
+
|
|
382
|
+
// True while an async query is running on the worker threads.
|
|
383
|
+
get busy() { return this.#inFlight; }
|
|
384
|
+
|
|
385
|
+
// The control block and scalar slots belong to the running task; touching
|
|
386
|
+
// them while an async run is on the workers would corrupt it.
|
|
387
|
+
#assertIdle() {
|
|
388
|
+
if (this.#inFlight) {
|
|
389
|
+
throw new Error('vectro: an async query is running on this engine; await it, or use the async API (e.g. aggAsync) for this call too');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#exec1(req, parts) {
|
|
394
|
+
return req.after ? req.after(parts) : Float64Array.from(parts); // results are copied out before the next run
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Before a request's setup() writes the control block (kernel, scalars...),
|
|
398
|
+
// mark it as being rewritten: a worker still reading the previous task
|
|
399
|
+
// must not take the new fields for it (the seqlock in #publish).
|
|
400
|
+
#prevSeq = 0;
|
|
401
|
+
#beginWrite() {
|
|
402
|
+
if (!this.W) return;
|
|
403
|
+
const s = Atomics.load(this.ctrl, C.SEQ);
|
|
404
|
+
if (s !== SEQ_WRITING) this.#prevSeq = s;
|
|
405
|
+
Atomics.store(this.ctrl, C.SEQ, SEQ_WRITING);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_execSync(req) {
|
|
409
|
+
this.#assertIdle(); // before setup(), which writes the shared control block
|
|
410
|
+
this.#beginWrite();
|
|
411
|
+
req.setup?.();
|
|
412
|
+
return this.#exec1(req, this.#run(req.op, req.n, req.arrays, req.W ?? null, req.F ?? null));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async _execAsync(req) {
|
|
416
|
+
const before = this.#queue;
|
|
417
|
+
let release;
|
|
418
|
+
this.#queue = new Promise((r) => { release = r; });
|
|
419
|
+
await before;
|
|
420
|
+
try {
|
|
421
|
+
this.#beginWrite();
|
|
422
|
+
req.setup?.();
|
|
423
|
+
return await this.#exec1(req, await this.#runAsync(req.op, req.n, req.arrays, req.W ?? null, req.F ?? null));
|
|
424
|
+
} finally {
|
|
425
|
+
release();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_drive(gen) {
|
|
430
|
+
let step = gen.next();
|
|
431
|
+
while (!step.done) {
|
|
432
|
+
let out;
|
|
433
|
+
try {
|
|
434
|
+
out = this._execSync(step.value);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
step = gen.throw(err);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
step = gen.next(out);
|
|
440
|
+
}
|
|
441
|
+
return step.value;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async _driveAsync(gen) {
|
|
445
|
+
// One thread: nothing runs in the background, so run the whole operation
|
|
446
|
+
// at once (other operations can't slip in between its steps).
|
|
447
|
+
if (this.W === 0) return this._drive(gen);
|
|
448
|
+
let step = gen.next();
|
|
449
|
+
while (!step.done) {
|
|
450
|
+
let out;
|
|
451
|
+
try {
|
|
452
|
+
out = await this._execAsync(step.value);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
step = gen.throw(err);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
step = gen.next(out);
|
|
458
|
+
}
|
|
459
|
+
return step.value;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// A unit failed somewhere. Every unit has finished (so nothing is still
|
|
463
|
+
// running); report the first error. Other messages are dropped.
|
|
464
|
+
#throwWorkerError(mainError) {
|
|
465
|
+
Atomics.store(this.ctrl, C.ERROR, 0);
|
|
466
|
+
if (mainError) throw mainError;
|
|
467
|
+
for (const p of this.ports) {
|
|
468
|
+
for (const m of this.#received(p)) {
|
|
469
|
+
if (m.kind === 'error') throw new Error(`vectro: a worker thread failed:\n${m.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
throw new Error('vectro: a worker thread failed');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#serial(op, n, W = null) {
|
|
476
|
+
return this.W === 0 || n < ((W ? this.minParallelWasm : this.minParallel)[op] ?? 0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#exec(op, n, arrays) {
|
|
480
|
+
arrays = arrays.map((a) => a && this.live(a));
|
|
481
|
+
if (n === undefined) n = arrays[0].length;
|
|
482
|
+
const W = this.#wasmFor(op, arrays);
|
|
483
|
+
if (this.#serial(op, n, W)) return [rangeKernel(op, 0, n, arrays, this.sc, W)];
|
|
484
|
+
return this.#run(op, n, arrays, W);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ---- operations ---------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
sum(a) { return reduce(this.#exec(OP.SUM, undefined, [a]), (x, y) => x + y, 0); }
|
|
490
|
+
min(a) { return reduce(this.#exec(OP.MIN, undefined, [a]), Math.min, Infinity); }
|
|
491
|
+
max(a) { return reduce(this.#exec(OP.MAX, undefined, [a]), Math.max, -Infinity); }
|
|
492
|
+
dot(a, b) { return reduce(this.#exec(OP.DOT, undefined, [a, b]), (x, y) => x + y, 0); }
|
|
493
|
+
heavy(a) { return reduce(this.#exec(OP.HEAVY, undefined, [a]), (x, y) => x + y, 0); }
|
|
494
|
+
|
|
495
|
+
countGreater(a, x) {
|
|
496
|
+
this.#assertIdle();
|
|
497
|
+
this.sc[0] = x;
|
|
498
|
+
return reduce(this.#exec(OP.COUNT_GT, undefined, [a]), (p, q) => p + q, 0);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
add(a, b, out) { this.#exec(OP.ADD, undefined, [a, b, out]); return this.live(out); }
|
|
502
|
+
|
|
503
|
+
// out = a * k + c
|
|
504
|
+
scale(a, k, c, out) {
|
|
505
|
+
this.#assertIdle();
|
|
506
|
+
this.sc[0] = k;
|
|
507
|
+
this.sc[1] = c;
|
|
508
|
+
this.#exec(OP.SCALE, undefined, [a, out]);
|
|
509
|
+
return this.live(out);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Count occurrences of `pattern` (string or bytes) in shared bytes.
|
|
513
|
+
countBytes(text, pattern) {
|
|
514
|
+
this.#assertIdle();
|
|
515
|
+
const p = typeof pattern === 'string' ? encode(pattern) : pattern;
|
|
516
|
+
if (!this.patBuf || this.patBuf.length < p.length) {
|
|
517
|
+
this.patBuf = new Uint8Array(new (this.isShared ? SharedArrayBuffer : ArrayBuffer)(Math.max(256, p.length)));
|
|
518
|
+
}
|
|
519
|
+
const pat = this.patBuf.subarray(0, p.length);
|
|
520
|
+
pat.set(p);
|
|
521
|
+
return reduce(this.#exec(OP.COUNT_BYTES, undefined, [text, pat]), (x, y) => x + y, 0);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// In-place numeric sort: threads sort small chunks, then merge them in rounds.
|
|
525
|
+
sort(a) {
|
|
526
|
+
this.#assertIdle();
|
|
527
|
+
a = this.live(a);
|
|
528
|
+
const n = a.length;
|
|
529
|
+
if (this.#serial(OP.SORT_CHUNK, n)) return a.sort();
|
|
530
|
+
const chunks = this.T * 4;
|
|
531
|
+
this.sc[1] = chunks;
|
|
532
|
+
this.#run(OP.SORT_CHUNK, n, [a]);
|
|
533
|
+
let src = a;
|
|
534
|
+
let dst = this.#scratchFor(a);
|
|
535
|
+
for (let width = 1; width < chunks; width *= 2) {
|
|
536
|
+
this.sc[0] = width;
|
|
537
|
+
this.#run(OP.MERGE, n, [src, dst]);
|
|
538
|
+
[src, dst] = [dst, src];
|
|
539
|
+
}
|
|
540
|
+
if (src !== a) this.#run(OP.COPY, n, [src, a]);
|
|
541
|
+
return a;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#scratchFor(a) {
|
|
545
|
+
const bytes = a.length * a.BYTES_PER_ELEMENT;
|
|
546
|
+
if (!this.scratch || this.scratch.byteLength < bytes) this.scratch = new SharedArrayBuffer(bytes);
|
|
547
|
+
return new a.constructor(this.scratch, 0, a.length);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ---- kernel fusion ------------------------------------------------------
|
|
551
|
+
|
|
552
|
+
// Compile an expression (or a list/object of reductions) into one SIMD
|
|
553
|
+
// kernel. Compiling is cached; the WASM is generated per element type.
|
|
554
|
+
// engine.compile('sum(a * 2 + 1)').run({ a })
|
|
555
|
+
// engine.compile(['sum(a)', 'sum(a*a)', 'max(a)']).run({ a })
|
|
556
|
+
// engine.compile('a * k + b').run({ a, b, k: 2 }, out)
|
|
557
|
+
compile(spec) {
|
|
558
|
+
const key = typeof spec === 'string' ? spec : JSON.stringify(spec);
|
|
559
|
+
let k = this.compiled.get(key);
|
|
560
|
+
if (!k) {
|
|
561
|
+
k = new Kernel(this, analyze(spec));
|
|
562
|
+
this.compiled.set(key, k);
|
|
563
|
+
}
|
|
564
|
+
return k;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
eval(spec, vars, out) { return this.compile(spec).run(vars, out); }
|
|
568
|
+
evalAsync(spec, vars, out) { return this.compile(spec).runAsync(vars, out); }
|
|
569
|
+
*evalG(spec, vars, out) { return yield* this.compile(spec).runG(vars, out); }
|
|
570
|
+
|
|
571
|
+
// Generate, compile and hand a kernel variant to every thread.
|
|
572
|
+
_variant(analysis, t, arrayNames, scalarNames, encs = []) {
|
|
573
|
+
return this._register(generate(analysis, t, arrayNames, scalarNames, this.isShared, encs));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Compile generated WASM and send it to every thread; returns { id, fn }.
|
|
577
|
+
_register(bytes) {
|
|
578
|
+
const module = new WebAssembly.Module(bytes);
|
|
579
|
+
const id = this.nextKernelId++;
|
|
580
|
+
for (const p of this.ports) p.postMessage({ kind: 'kernel', id, module });
|
|
581
|
+
const fn = new WebAssembly.Instance(module, { env: { memory: this.memory } }).exports.k;
|
|
582
|
+
return { id, fn };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Each of these yields at most one run request (see _drive / _driveAsync).
|
|
586
|
+
// Small inputs run right here on the calling thread instead.
|
|
587
|
+
|
|
588
|
+
// A fused kernel over n elements; returns per-unit results (R per unit).
|
|
589
|
+
*_fusedG(v, n, arrays, scalars, R) {
|
|
590
|
+
arrays = arrays.map((a) => this.live(a));
|
|
591
|
+
if (arrays.some((a) => !this.#inMemory(a))) {
|
|
592
|
+
throw new TypeError('vectro: fused expressions need arrays from engine.alloc() (or engine.toShared())');
|
|
593
|
+
}
|
|
594
|
+
if (this.#serial(OP.FUSED, n, this.kernels)) {
|
|
595
|
+
const r = v.fn(...arrays.map((a) => a.byteOffset), n, ...scalars);
|
|
596
|
+
return { parts: R === 1 ? [r] : r ?? [], units: 1 };
|
|
597
|
+
}
|
|
598
|
+
const parts = yield {
|
|
599
|
+
op: OP.FUSED, n, arrays, F: v.fn,
|
|
600
|
+
setup: () => {
|
|
601
|
+
for (let j = 0; j < scalars.length; j++) this.sc[j] = scalars[j];
|
|
602
|
+
this.ctrl[C.KERNEL] = v.id;
|
|
603
|
+
this.ctrl[C.NOUT] = R;
|
|
604
|
+
this.ctrl[C.NSCAL] = scalars.length;
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
return { parts, units: parts.length / Math.max(1, R) };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Group-by kernel over n rows; arrays = [columns..., key]. Each thread
|
|
611
|
+
// accumulates into its own block: accBase + threadIndex * stride.
|
|
612
|
+
*_groupG(v, n, arrays, scalars, accBase, stride, K) {
|
|
613
|
+
arrays = arrays.map((a) => this.live(a));
|
|
614
|
+
if (this.#serial(OP.FUSED, n, this.kernels)) {
|
|
615
|
+
v.fn(...arrays.map((a) => a.byteOffset), n, accBase, K, ...scalars);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
yield {
|
|
619
|
+
op: OP.GROUP, n, arrays, F: v.fn,
|
|
620
|
+
setup: () => {
|
|
621
|
+
const ns = scalars.length;
|
|
622
|
+
for (let j = 0; j < ns; j++) this.sc[j] = scalars[j];
|
|
623
|
+
this.sc[ns] = accBase;
|
|
624
|
+
this.sc[ns + 1] = stride;
|
|
625
|
+
this.sc[ns + 2] = K;
|
|
626
|
+
this.ctrl[C.KERNEL] = v.id;
|
|
627
|
+
this.ctrl[C.NSCAL] = ns;
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// A kernel run over n rows in units on every thread (see runIndexed);
|
|
633
|
+
// returns the number of units (they decide each unit's own slots).
|
|
634
|
+
*_unitsG(v, n, scalars) {
|
|
635
|
+
yield {
|
|
636
|
+
op: OP.UNITS, n, arrays: [], F: v.fn,
|
|
637
|
+
setup: () => {
|
|
638
|
+
for (let j = 0; j < scalars.length; j++) this.sc[j] = scalars[j];
|
|
639
|
+
this.ctrl[C.KERNEL] = v.id;
|
|
640
|
+
this.ctrl[C.NSCAL] = scalars.length;
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// A kernel run once per task, n tasks claimed one at a time on every
|
|
646
|
+
// thread (see runTask): (task, thread index, scalars...).
|
|
647
|
+
*_tasksG(v, n, scalars) {
|
|
648
|
+
yield {
|
|
649
|
+
op: OP.TASKS, n, arrays: [], F: v.fn,
|
|
650
|
+
setup: () => {
|
|
651
|
+
for (let j = 0; j < scalars.length; j++) this.sc[j] = scalars[j];
|
|
652
|
+
this.ctrl[C.KERNEL] = v.id;
|
|
653
|
+
this.ctrl[C.NSCAL] = scalars.length;
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Group-by kernel split by group ranges (see runGroupRange): one set of
|
|
659
|
+
// accumulators at accBase, J blocks of K slots.
|
|
660
|
+
*_groupRangeG(v, n, arrays, scalars, accBase, K) {
|
|
661
|
+
arrays = arrays.map((a) => this.live(a));
|
|
662
|
+
if (this.#serial(OP.FUSED, n, this.kernels)) {
|
|
663
|
+
v.fn(...arrays.map((a) => a.byteOffset), n, accBase, K, 0, K, ...scalars);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
yield {
|
|
667
|
+
op: OP.GROUP_RANGE, n, arrays, F: v.fn,
|
|
668
|
+
setup: () => {
|
|
669
|
+
const ns = scalars.length;
|
|
670
|
+
for (let j = 0; j < ns; j++) this.sc[j] = scalars[j];
|
|
671
|
+
this.sc[ns] = accBase;
|
|
672
|
+
this.sc[ns + 1] = K;
|
|
673
|
+
this.ctrl[C.KERNEL] = v.id;
|
|
674
|
+
this.ctrl[C.NSCAL] = ns;
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Compact kernel; offsets[u] is where unit u starts writing. units must
|
|
680
|
+
// match the counting pass (1 = it ran on the calling thread alone).
|
|
681
|
+
*_compactG(v, n, src, dst, offsets, scalars, units) {
|
|
682
|
+
src = src.map((a) => this.live(a));
|
|
683
|
+
dst = dst.map((a) => this.live(a));
|
|
684
|
+
if (units === 1) {
|
|
685
|
+
v.fn(...src.map((a) => a.byteOffset), n, ...dst.map((a) => a.byteOffset), ...scalars);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
yield {
|
|
689
|
+
op: OP.COMPACT, n, arrays: [...src, ...dst, offsets], F: v.fn,
|
|
690
|
+
setup: () => {
|
|
691
|
+
for (let j = 0; j < scalars.length; j++) this.sc[j] = scalars[j];
|
|
692
|
+
this.ctrl[C.KERNEL] = v.id;
|
|
693
|
+
this.ctrl[C.NOUT] = src.length;
|
|
694
|
+
this.ctrl[C.NSCAL] = scalars.length;
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// A per-chunk op (CSV_SCAN / CSV_PARSE) over `chunks` units. Returns the
|
|
700
|
+
// per-unit results and the messages threads sent back; `expect` is how many
|
|
701
|
+
// messages there will be in total (browsers deliver them asynchronously).
|
|
702
|
+
*_chunksG(op, chunks, arrays, scalars, expect = 0) {
|
|
703
|
+
const messages = [];
|
|
704
|
+
return yield {
|
|
705
|
+
op, n: chunks, arrays, F: (msg) => messages.push(msg),
|
|
706
|
+
setup: () => { for (let j = 0; j < scalars.length; j++) this.sc[j] = scalars[j]; },
|
|
707
|
+
after: (parts) => {
|
|
708
|
+
parts = Float64Array.from(parts.subarray(0, chunks));
|
|
709
|
+
const pull = () => { for (const p of this.ports) messages.push(...this.#received(p)); };
|
|
710
|
+
pull();
|
|
711
|
+
if (messages.length >= expect || !this.platform?.nextMessage) return { parts, messages };
|
|
712
|
+
return (async () => {
|
|
713
|
+
while (messages.length < expect) {
|
|
714
|
+
await this.platform.nextMessage(this.ports);
|
|
715
|
+
pull();
|
|
716
|
+
}
|
|
717
|
+
return { parts, messages };
|
|
718
|
+
})();
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Blocking versions, kept for callers that want them.
|
|
724
|
+
_runFused(...args) { return this._drive(this._fusedG(...args)); }
|
|
725
|
+
_runGroup(...args) { return this._drive(this._groupG(...args)); }
|
|
726
|
+
_runCompact(...args) { return this._drive(this._compactG(...args)); }
|
|
727
|
+
_runChunks(...args) { return this._drive(this._chunksG(...args)); }
|
|
728
|
+
|
|
729
|
+
// ---- tables ---------------------------------------------------------------
|
|
730
|
+
|
|
731
|
+
// Build a table from columns: { price: Float64Array | number[], region: string[] }
|
|
732
|
+
// opts.compress: pack the columns (see Table.compress).
|
|
733
|
+
table(columns, opts) { return Table.fromColumns(this, columns, { ...opts, compress: opts?.compress ?? COMPRESS_ALL }); }
|
|
734
|
+
|
|
735
|
+
// Load Arrow IPC bytes (stream or file format, e.g. a .arrow / .feather
|
|
736
|
+
// file or what pyarrow, Polars or DuckDB send). columns: names to read.
|
|
737
|
+
fromArrow(bytes, { columns, type } = {}) {
|
|
738
|
+
const r = readArrow(bytes, { columns });
|
|
739
|
+
return Table.fromColumns(this, r.columns, { type, dates: r.dates, compress: COMPRESS_ALL });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Build a table from an array of objects (one-time conversion cost).
|
|
743
|
+
tableFromRows(rows, opts) { return Table.fromRows(this, rows, opts); }
|
|
744
|
+
|
|
745
|
+
// Load CSV text or bytes into a table, parsing on all threads.
|
|
746
|
+
// (Reading files is in node.mjs: engine.readCSV(path).)
|
|
747
|
+
// opts.compress: pack the columns afterwards (see Table.compress).
|
|
748
|
+
parseCSV(text, opts) { const t = parseCSV(this, text, opts); return opts?.compress ?? COMPRESS_ALL ? t.compress() : t; }
|
|
749
|
+
async parseCSVAsync(text, opts) { const t = await parseCSVAsync(this, text, opts); return opts?.compress ?? COMPRESS_ALL ? t.compress() : t; }
|
|
750
|
+
|
|
751
|
+
async close() {
|
|
752
|
+
this.ctrl[C.OP] = OP.EXIT;
|
|
753
|
+
Atomics.store(this.ctrl, C.SEQ, ++this.seq);
|
|
754
|
+
Atomics.notify(this.ctrl, C.SEQ);
|
|
755
|
+
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
756
|
+
for (const p of this.ports) p.close?.();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const COMBINE = { sum: (x, y) => x + y, count: (x, y) => x + y, mean: (x, y) => x + y, min: Math.min, max: Math.max };
|
|
761
|
+
const IDENTITY = { sum: 0, count: 0, mean: 0, min: Infinity, max: -Infinity };
|
|
762
|
+
|
|
763
|
+
class Kernel {
|
|
764
|
+
constructor(engine, analysis) {
|
|
765
|
+
this.engine = engine;
|
|
766
|
+
this.analysis = analysis;
|
|
767
|
+
this.variants = new Map(); // "f64|a,b|k" -> { id, fn }
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// vars: { name: typedArray | number }. Arrays must share one type and length.
|
|
771
|
+
run(vars, out) { return this.engine._drive(this.runG(vars, out)); }
|
|
772
|
+
runAsync(vars, out) { return this.engine._driveAsync(this.runG(vars, out)); }
|
|
773
|
+
|
|
774
|
+
*runG(vars, out) {
|
|
775
|
+
const { analysis } = this;
|
|
776
|
+
if (!this.engine.jit) return this.#interpret(vars, out);
|
|
777
|
+
const { v, n, arrays, scalars, R } = this.#prepare(vars, out);
|
|
778
|
+
const { parts, units } = yield* this.engine._fusedG(v, n, arrays, scalars, R);
|
|
779
|
+
if (analysis.isMap) return this.engine.live(out);
|
|
780
|
+
|
|
781
|
+
const values = analysis.items.map((it, j) => {
|
|
782
|
+
let r = IDENTITY[it.reduce];
|
|
783
|
+
for (let u = 0; u < units; u++) r = COMBINE[it.reduce](r, parts[u * R + j]);
|
|
784
|
+
return it.reduce === 'mean' ? r / n : r;
|
|
785
|
+
});
|
|
786
|
+
if (analysis.shape === 'single') return values[0];
|
|
787
|
+
if (analysis.shape === 'array') return values;
|
|
788
|
+
return Object.fromEntries(analysis.items.map((it, j) => [it.key, values[j]]));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Like run() for one reduction, but returns each unit's partial result
|
|
792
|
+
// (used to find where each unit's rows go when compacting a filter).
|
|
793
|
+
runParts(vars) { return this.engine._drive(this.runPartsG(vars)); }
|
|
794
|
+
|
|
795
|
+
*runPartsG(vars) {
|
|
796
|
+
const { v, n, arrays, scalars, R } = this.#prepare(vars);
|
|
797
|
+
const { parts, units } = yield* this.engine._fusedG(v, n, arrays, scalars, R);
|
|
798
|
+
return { parts: Float64Array.from(parts.slice(0, units)), units };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Same results as the compiled kernels, with only precompiled code.
|
|
802
|
+
#interpret(vars, out) {
|
|
803
|
+
const { analysis } = this;
|
|
804
|
+
const { type, n, arrayNames, arrays: raw, scalarNames, scalars, encs } = this.#bind(vars, out);
|
|
805
|
+
// packed columns: decode first (the interpreter's kernels read plain floats)
|
|
806
|
+
const temps = [];
|
|
807
|
+
const arrays = raw.map((a, i) => {
|
|
808
|
+
if (!encs[i]) return a;
|
|
809
|
+
const plain = this.engine.alloc(type === 'f32' ? Float32Array : Float64Array, n);
|
|
810
|
+
const codes = this.engine.live(a);
|
|
811
|
+
for (let r = 0; r < n; r++) plain[r] = decodeOne(encs[i], codes[r], type === 'f32');
|
|
812
|
+
temps.push(plain);
|
|
813
|
+
return plain;
|
|
814
|
+
});
|
|
815
|
+
try { return this.#interpretPlain(analysis, type, n, arrayNames, arrays.map((x) => this.engine.live(x)), scalarNames, scalars, out); } finally { for (const x of temps) this.engine.free(x); }
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
#interpretPlain(analysis, type, n, arrayNames, arrays, scalarNames, scalars, out) {
|
|
819
|
+
if (arrays.some((a) => !this.engine._inMemory(a))) {
|
|
820
|
+
throw new TypeError('vectro: fused expressions need arrays from engine.alloc() (or engine.toShared())');
|
|
821
|
+
}
|
|
822
|
+
const cols = Object.fromEntries(arrayNames.map((nm, i) => [nm, arrays[i]]));
|
|
823
|
+
const params = Object.fromEntries(scalarNames.map((nm, i) => [nm, scalars[i]]));
|
|
824
|
+
if (analysis.isMap) return interpretMap(this.engine, type, analysis.items[0].ast, cols, params, arrays[arrays.length - 1], n);
|
|
825
|
+
const values = interpretReduce(this.engine, type, analysis.items, cols, params, n);
|
|
826
|
+
if (analysis.shape === 'single') return values[0];
|
|
827
|
+
if (analysis.shape === 'array') return values;
|
|
828
|
+
return Object.fromEntries(analysis.items.map((it, j) => [it.key, values[j]]));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
#prepare(vars, out) {
|
|
832
|
+
const { analysis } = this;
|
|
833
|
+
const { type, n, arrayNames, arrays, scalarNames, scalars, encs } = this.#bind(vars, out);
|
|
834
|
+
const key = `${type}|${arrayNames}|${scalarNames}|${encs.map((e) => (e ? `${e.kind}:${e.mul}:${e.div}:${e.base}:${e.nan}` : '')).join(',')}`;
|
|
835
|
+
let v = this.variants.get(key);
|
|
836
|
+
if (!v) {
|
|
837
|
+
v = this.engine._variant(analysis, type, arrayNames, scalarNames, encs);
|
|
838
|
+
this.variants.set(key, v);
|
|
839
|
+
}
|
|
840
|
+
const R = analysis.isMap ? 0 : analysis.items.length;
|
|
841
|
+
return { v, n, arrays, scalars, R };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// vars[name]: a number, a Float64Array / Float32Array, or a packed column
|
|
845
|
+
// { data, enc, n, type } (see pack.mjs).
|
|
846
|
+
#bind(vars, out) {
|
|
847
|
+
const { analysis } = this;
|
|
848
|
+
const arrayNames = [], scalarNames = [], arrays = [], scalars = [], encs = [], lengths = [];
|
|
849
|
+
let packedType = null;
|
|
850
|
+
for (const name of analysis.vars) {
|
|
851
|
+
const v = vars[name];
|
|
852
|
+
if (typeof v === 'number') { scalarNames.push(name); scalars.push(v); }
|
|
853
|
+
else if (ArrayBuffer.isView(v)) { const a = this.engine.live(v); arrayNames.push(name); arrays.push(a); encs.push(null); lengths.push(a.length); } // (live: a stale view has length 0)
|
|
854
|
+
else if (v?.enc && ArrayBuffer.isView(v.data)) { arrayNames.push(name); arrays.push(this.engine.live(v.data)); encs.push(v.enc); lengths.push(v.n); packedType = v.type; }
|
|
855
|
+
else throw new TypeError(`vectro: missing value for "${name}"`);
|
|
856
|
+
}
|
|
857
|
+
if (analysis.isMap) {
|
|
858
|
+
if (!out) throw new TypeError('vectro: an elementwise expression needs an output array: run(vars, out)');
|
|
859
|
+
const o = this.engine.live(out);
|
|
860
|
+
arrays.push(o); encs.push(null); lengths.push(o.length);
|
|
861
|
+
} else if (!arrays.length) {
|
|
862
|
+
throw new TypeError('vectro: a reduction needs at least one array');
|
|
863
|
+
}
|
|
864
|
+
const plain = arrays.filter((_, i) => !encs[i]);
|
|
865
|
+
const ctor = plain[0]?.constructor ?? (packedType === 'f32' ? Float32Array : Float64Array);
|
|
866
|
+
const type = ctor === Float64Array ? 'f64' : ctor === Float32Array ? 'f32' : null;
|
|
867
|
+
if (!type) throw new TypeError('vectro: fused expressions support Float64Array and Float32Array');
|
|
868
|
+
const n = lengths[0];
|
|
869
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
870
|
+
if (!encs[i] && arrays[i].constructor !== ctor) throw new TypeError('vectro: all arrays in an expression must have the same type');
|
|
871
|
+
if (lengths[i] !== n) throw new RangeError('vectro: all arrays in an expression must have the same length');
|
|
872
|
+
}
|
|
873
|
+
return { type, n, arrayNames, arrays, scalarNames, scalars, encs };
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function reduce(parts, f, init) {
|
|
878
|
+
let r = init;
|
|
879
|
+
for (let i = 0; i < parts.length; i++) r = f(r, parts[i]);
|
|
880
|
+
return r;
|
|
881
|
+
}
|