vectrojs 0.0.0-stage → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +822 -2
  4. package/bin/vectro.mjs +82 -0
  5. package/dist/vectro.wasm +0 -0
  6. package/mcp/server.mjs +288 -0
  7. package/package.json +91 -4
  8. package/src/arrow.mjs +905 -0
  9. package/src/bitpack.mjs +135 -0
  10. package/src/browser.mjs +129 -0
  11. package/src/bytes.mjs +60 -0
  12. package/src/csv-kernel.mjs +205 -0
  13. package/src/csv.mjs +270 -0
  14. package/src/dataset.mjs +1763 -0
  15. package/src/dates.mjs +139 -0
  16. package/src/emitter.mjs +109 -0
  17. package/src/engine.mjs +881 -0
  18. package/src/files-browser.mjs +104 -0
  19. package/src/files-node.mjs +59 -0
  20. package/src/frames.mjs +191 -0
  21. package/src/fuse.mjs +512 -0
  22. package/src/interp.mjs +305 -0
  23. package/src/join.mjs +534 -0
  24. package/src/kernels.mjs +312 -0
  25. package/src/ndjson.mjs +136 -0
  26. package/src/node.mjs +120 -0
  27. package/src/pack.mjs +114 -0
  28. package/src/parallel-sort.mjs +99 -0
  29. package/src/partition-kernels.mjs +388 -0
  30. package/src/partition.mjs +395 -0
  31. package/src/plan.mjs +551 -0
  32. package/src/portable.mjs +21 -0
  33. package/src/rank-kernels.mjs +81 -0
  34. package/src/rows.mjs +91 -0
  35. package/src/serve/catalog.mjs +148 -0
  36. package/src/serve/flight.mjs +270 -0
  37. package/src/serve/formats.mjs +87 -0
  38. package/src/serve/http.mjs +182 -0
  39. package/src/serve/index.mjs +9 -0
  40. package/src/serve/odata.mjs +291 -0
  41. package/src/serve/pg.mjs +588 -0
  42. package/src/serve/pgcatalog.mjs +220 -0
  43. package/src/serve/pgtypes.mjs +199 -0
  44. package/src/sketch.mjs +75 -0
  45. package/src/sort-kernels.mjs +117 -0
  46. package/src/sort.mjs +102 -0
  47. package/src/split-worker-core.mjs +252 -0
  48. package/src/split-worker-node.mjs +8 -0
  49. package/src/split-worker.mjs +7 -0
  50. package/src/split.mjs +741 -0
  51. package/src/sql.mjs +1558 -0
  52. package/src/sqltext.mjs +132 -0
  53. package/src/static-kernels.mjs +164 -0
  54. package/src/stream-worker.mjs +553 -0
  55. package/src/table-kernels.mjs +323 -0
  56. package/src/table.mjs +2206 -0
  57. package/src/textmap.mjs +159 -0
  58. package/src/vectors.mjs +138 -0
  59. package/src/wasm.mjs +254 -0
  60. package/src/window.mjs +160 -0
  61. package/src/worker-browser.mjs +21 -0
  62. package/src/worker-loop.mjs +106 -0
  63. package/src/worker.mjs +22 -0
  64. package/src/zones.mjs +75 -0
@@ -0,0 +1,395 @@
1
+ // Group-by with many groups on every thread, with no group numbers, global
2
+ // sort or gathers (the kernels are in partition-kernels.mjs):
3
+ //
4
+ // 1. Each key becomes a bit field of r, a key that sorts like the keys do.
5
+ // 2. A sample of rows gives P partitions of about the same number of rows,
6
+ // in key order (a lookup on the top bits of r's first word), and an
7
+ // estimate of the number of groups.
8
+ // 3. Count the rows of each partition in each unit, turn the counts into
9
+ // each unit's first place in each partition, and copy every row's record
10
+ // (r and the values to add up) there: each partition holds its rows in
11
+ // row order.
12
+ // 4. One task per partition: a hash table in that thread's own scratch
13
+ // memory adds up the rows (in row order, so the sums are the same on any
14
+ // number of threads), then its groups are sorted by r and staged over
15
+ // the partition's first records. A partition with more groups than the
16
+ // scratch holds is split again by key range (recursively; by the next
17
+ // word of r once a word is the same in all its rows).
18
+ // 5. Each partition's groups go into the result columns at its offset,
19
+ // which leaves them sorted by key.
20
+ //
21
+ // partitionedGroupsG returns null when the keys or the memory needed don't
22
+ // suit it (or memory runs out); the caller then numbers the groups with a
23
+ // hash table instead.
24
+
25
+ import { OP, unitCount } from './kernels.mjs';
26
+ import { analyze, toSource, walk } from './fuse.mjs';
27
+ import { decodeOne } from './pack.mjs';
28
+ import { NB, FIELD, rowsLayout, rowsModule, aggregateModule, scratchBytes, writeLayout, writeModule, splitModule } from './partition-kernels.mjs';
29
+
30
+ export const PARTITION_ROWS = 65_536; // smaller tables: the hash path (less to set up)
31
+ export const PARTITION_GROUPS = 16_384; // fewer groups expected: the hash path (per-thread totals are cheaper)
32
+ export const DIRECT_SLOTS = 2 ** 22; // threads x totals x groups past which numbering groups directly is slower
33
+ const MAX_WORDS = 4, MAX_ACCS = 32, SAMPLE = 16_384, MAX_DEPTH = 12;
34
+ const RECORD_BYTES = 2 ** 30; // every row's record at once; more: the hash path
35
+ const SCRATCH_BYTES = 2 ** 28; // every thread's scratch together (fewer groups per partition fit: more splits)
36
+ const OVERFLOW = 0xffffffff; // a partition with more groups than the scratch holds
37
+ const NO_PLAN = Symbol('no plan'); // splitting didn't converge: the hash path instead
38
+
39
+ function kernel(engine, name, make) {
40
+ engine._partitionKernels ??= new Map();
41
+ let v = engine._partitionKernels.get(name);
42
+ if (!v) { v = engine._register(make()); engine._partitionKernels.set(name, v); }
43
+ return v;
44
+ }
45
+
46
+ const bitsFor = (maxCode) => { let b = 1; while (2 ** b <= maxCode) b++; return b; };
47
+ export const partitionsFor = (rows) => Math.max(16, Math.min(1024, 2 ** Math.round(Math.log2(rows / 8192))));
48
+ const view = (engine, a) => { const b = engine.live(a); return new DataView(b.buffer, b.byteOffset, b.byteLength); };
49
+
50
+ // keys (and t, sources: 'count(1)', 'sum(x)'... with missing-value rules
51
+ // applied, the first counting rows; est: groups expected, see
52
+ // estimateGroups) -> { G, keyCols, accCols, map } or null: G groups, sorted
53
+ // by key; keyCols: each key's values (codes for text), accCols: each
54
+ // accumulator's totals (engine Float64Arrays, the caller's to free); source
55
+ // i's totals are accCols[map[i]] (identical sources share one).
56
+ export function* partitionedGroupsG(t, keys, sources, params, est, { countOnly = false } = {}) { // countOnly: just { G }
57
+ const { engine, n } = t;
58
+ if (!n) return null;
59
+ const T = engine.T, shared = engine.isShared, tf = t.type === Float32Array ? 'f32' : 'f64';
60
+ const kf = yield* keyFieldsG(t, keys);
61
+ if (!kf) return null;
62
+ const { fields, W } = kf;
63
+ const { accs, map } = accumulators(sources);
64
+ const J = accs.length, RB = 8 * W + 8 * J;
65
+ if (J > MAX_ACCS || n * RB > (engine._partitionBytes ?? RECORD_BYTES)) return null;
66
+ // the values to add up, and the columns and scalars they read
67
+ const vals = accs.flatMap((a, slot) => (a.ast ? [{ ast: a.ast, slot }] : []));
68
+ const arrays = [...keys], scalarNames = [];
69
+ for (const { ast } of vals) {
70
+ walk(ast, (m) => {
71
+ if (m.t !== 'var') return;
72
+ const list = t.cols.has(m.name) ? arrays : scalarNames;
73
+ if (!list.includes(m.name)) list.push(m.name);
74
+ });
75
+ }
76
+ const encs = arrays.map((nm) => t.cols.get(nm).enc ?? null);
77
+ const accShape = accs.map((a) => a.key).join(';');
78
+ const shape = JSON.stringify([tf, arrays, encs, fields.map((k) => [k.kind, k.word]), accShape, scalarNames]);
79
+ const spec = { t: tf, arrays, encs, fields, W, vals, scalarNames, RB };
80
+ const count = kernel(engine, `count|${shape}`, () => rowsModule(spec, 'count', shared));
81
+ const scatter = kernel(engine, `scatter|${shape}`, () => rowsModule(spec, 'scatter', shared));
82
+ const write = kernel(engine, `write|${JSON.stringify([W, RB, fields.map((k) => [k.kind, k.word]), J])}`, () => writeModule({ W, RB, fields, J }, shared));
83
+ const P = engine._partitionP ?? partitionsFor(n);
84
+ const plan = planPartitions(sampleWord0(t, fields, Math.min(n, SAMPLE)), P);
85
+ const branchFree = est > 0.2 * n; // many rows start a group: no branch on which
86
+ const agg = kernel(engine, `aggregate|${branchFree}|${W}|${RB}|${accShape}|${countOnly}`, () => aggregateModule({ W, RB, accs: accs.map((a) => ({ op: a.op, stored: !!a.ast })) }, branchFree, shared, countOnly));
87
+ const splitCount = kernel(engine, `split|count|${RB}`, () => splitModule(RB, 'count', shared));
88
+ const splitCopy = kernel(engine, `split|copy|${RB}`, () => splitModule(RB, 'copy', shared));
89
+ const at = (x) => engine.live(x).byteOffset;
90
+ const ptr = (b) => (((at(b.arr) - b.base * RB) % 2 ** 32) + 2 ** 32) % 2 ** 32; // where record 0 of b would be (b.arr holds records b.base on)
91
+ const U = unitCount(OP.UNITS, T, n, null);
92
+ const made = [], out = [];
93
+ const A = (Type, len, list = made) => { const x = engine.alloc(Type, len); list.push(x); return x; };
94
+ try {
95
+ const lay = rowsLayout(arrays.length, fields.length, scalarNames.length);
96
+ const lut = A(Uint16Array, NB), cnt = A(Uint32Array, U * P), prm = A(Uint8Array, lay.bytes), starts = A(Uint32Array, P + 1);
97
+ const recs = A(Uint8Array, n * RB);
98
+ engine.live(lut).set(plan.lut);
99
+ engine.live(cnt).fill(0);
100
+ {
101
+ const dv = view(engine, prm);
102
+ lookupParams(dv, at(lut), at(cnt), at(recs), P, plan);
103
+ arrays.forEach((nm, i) => dv.setUint32(32 + 4 * i, at(t.cols.get(nm).data), true));
104
+ fieldParams(dv, lay.FIELDS, fields);
105
+ scalarNames.forEach((nm, i) => dv.setFloat64(lay.SC + 8 * i, Number(params[nm]), true));
106
+ }
107
+ yield* engine._unitsG(count, n, [at(prm)]);
108
+ const maxRows = offsets(engine.live(cnt), U, P, engine.live(starts));
109
+ yield* engine._unitsG(scatter, n, [at(prm)]);
110
+
111
+ // per-thread scratch: room for gMax groups in a partition (more: split again)
112
+ const gMax = Math.max(1, Math.min(maxRows, engine._partitionScratch ?? Math.max(65_536, 4 * Math.ceil(n / P)), Math.floor(SCRATCH_BYTES / (T * (RB + 32)))));
113
+ let capMax = 16;
114
+ while (capMax < 2 * gMax) capMax *= 2;
115
+ const stride = scratchBytes(capMax, gMax, RB);
116
+ const scratch = A(Uint8Array, T * stride + 64), aprm = A(Uint8Array, 32);
117
+ const groups = A(Uint32Array, P), order = A(Uint32Array, P);
118
+ const aggregate = function* (src, partStarts, partGroups, partOrder, parts) {
119
+ bySize(engine.live(partStarts), engine.live(partOrder), parts);
120
+ const dv = view(engine, aprm);
121
+ [ptr(src), at(partStarts), at(partGroups), Math.ceil(at(scratch) / 64) * 64, stride, capMax, gMax, at(partOrder)].forEach((x, i) => dv.setUint32(4 * i, x, true));
122
+ yield* engine._tasksG(agg, parts, [at(aprm)]);
123
+ };
124
+ const all = { arr: recs, base: 0 };
125
+ yield* aggregate(all, starts, groups, order, P);
126
+
127
+ // A partition with too many groups: split again by key range (by r's
128
+ // word w, once the words before it are the same in all its records):
129
+ // records s..e of src go to the same places in dst, part by part, each
130
+ // part is aggregated there (or split again, back into src's free
131
+ // places), and the parts' groups, in order, come back over src's first
132
+ // records. src and dst take turns: one extra copy however deep it goes.
133
+ const split = function* (src, dst, s, e, depth, w) {
134
+ if (depth > MAX_DEPTH) throw NO_PLAN;
135
+ const rows = e - s, P2 = partitionsFor(rows), U2 = unitCount(OP.UNITS, T, rows, null);
136
+ for (;;) {
137
+ if (w >= W) throw NO_PLAN; // (can't happen: one key is one group)
138
+ const local = [], B = (Type, len) => A(Type, len, local);
139
+ try {
140
+ const lut2 = B(Uint16Array, NB), cnt2 = B(Uint32Array, U2 * P2), sprm = B(Uint8Array, 48), mm = B(BigUint64Array, 2 * U2);
141
+ const countRecords = function* (pl) {
142
+ engine.live(lut2).set(pl.lut);
143
+ engine.live(cnt2).fill(0);
144
+ const dv = view(engine, sprm);
145
+ lookupParams(dv, at(lut2), at(cnt2), ptr(src), P2, pl);
146
+ [ptr(dst), s, 8 * w, at(mm)].forEach((x, i) => dv.setUint32(32 + 4 * i, x, true));
147
+ yield* engine._unitsG(splitCount, rows, [at(sprm)]);
148
+ };
149
+ yield* countRecords(planPartitions(sampleRecords(engine.live(src.arr), s - src.base, e - src.base, RB, Math.min(rows, SAMPLE), w), P2));
150
+ const x = engine.live(mm);
151
+ let lo = x[0], hi = x[1];
152
+ for (let u = 1; u < U2; u++) { if (x[2 * u] < lo) lo = x[2 * u]; if (x[2 * u + 1] > hi) hi = x[2 * u + 1]; }
153
+ if (lo === hi) { w++; continue; } // this word is the same in every record here
154
+ // the sample's plan put every record in one part: parts evenly over the word's range instead
155
+ if (largest(engine.live(cnt2), U2, P2) === rows) yield* countRecords(planUniform(lo, hi, P2));
156
+ const starts2 = B(Uint32Array, P2 + 1), groups2 = B(Uint32Array, P2), order2 = B(Uint32Array, P2);
157
+ offsets(engine.live(cnt2), U2, P2, engine.live(starts2), s);
158
+ yield* engine._unitsG(splitCopy, rows, [at(sprm)]);
159
+ yield* aggregate(dst, starts2, groups2, order2, P2);
160
+ for (let q = 0; q < P2; q++) {
161
+ if (engine.live(groups2)[q] !== OVERFLOW) continue;
162
+ const st = engine.live(starts2), g = yield* split(dst, src, st[q], st[q + 1], depth + 1, w);
163
+ engine.live(groups2)[q] = g;
164
+ }
165
+ if (countOnly) { let o = 0; const g = engine.live(groups2); for (let q = 0; q < P2; q++) o += g[q]; return o; }
166
+ const from = engine.live(dst.arr), to = engine.live(src.arr), st = engine.live(starts2), g = engine.live(groups2);
167
+ let o = s;
168
+ for (let q = 0; q < P2; q++) { to.set(from.subarray((st[q] - dst.base) * RB, (st[q] + g[q] - dst.base) * RB), (o - src.base) * RB); o += g[q]; }
169
+ return o - s;
170
+ } finally {
171
+ for (const y of local) engine.free(y);
172
+ }
173
+ }
174
+ };
175
+ {
176
+ const over = [];
177
+ for (let p = 0; p < P; p++) if (engine.live(groups)[p] === OVERFLOW) over.push(p);
178
+ if (over.length) {
179
+ const st0 = engine.live(starts), alt = A(Uint8Array, Math.max(...over.map((p) => st0[p + 1] - st0[p])) * RB);
180
+ for (const p of over) {
181
+ const st = engine.live(starts), g = yield* split(all, { arr: alt, base: st[p] }, st[p], st[p + 1], 1, 0);
182
+ engine.live(groups)[p] = g;
183
+ }
184
+ }
185
+ }
186
+
187
+ if (countOnly) { let G = 0; const g = engine.live(groups); for (let p = 0; p < P; p++) G += g[p]; return { G }; }
188
+ // every partition's groups into the result columns at its offset
189
+ const outOff = A(Uint32Array, P);
190
+ let G = 0;
191
+ { const g = engine.live(groups), o = engine.live(outOff); for (let p = 0; p < P; p++) { o[p] = G; G += g[p]; } }
192
+ const keyCols = fields.map(() => A(Float64Array, G, out)), accCols = accs.map(() => A(Float64Array, G, out));
193
+ const wlay = writeLayout(fields.length, J), wprm = A(Uint8Array, wlay.bytes);
194
+ {
195
+ const dv = view(engine, wprm);
196
+ [at(recs), at(starts), at(groups), at(outOff)].forEach((x, i) => dv.setUint32(4 * i, x, true));
197
+ keyCols.forEach((c, i) => dv.setUint32(wlay.KO + 4 * i, at(c), true));
198
+ accCols.forEach((c, j) => dv.setUint32(wlay.AO + 4 * j, at(c), true));
199
+ fieldParams(dv, wlay.FIELDS, fields);
200
+ }
201
+ if (G) yield* engine._tasksG(write, P, [at(wprm)]);
202
+ out.length = 0; // (the caller's now)
203
+ return { G, keyCols, accCols, map };
204
+ } catch (err) {
205
+ if (err === NO_PLAN || err instanceof RangeError) return null; // (RangeError: out of memory; the hash path needs less)
206
+ throw err;
207
+ } finally {
208
+ for (const x of [...made, ...out]) engine.free(x);
209
+ }
210
+ }
211
+
212
+ // Each key's bit field: text codes and whole numbers (within 2^53 of their
213
+ // minimum) as their offset from it, NaN as the code after the largest;
214
+ // anything else as float bits (64). Fields go into 64-bit words in key
215
+ // order without straddling one; more than MAX_WORDS words: null.
216
+ function* keyFieldsG(t, keys) {
217
+ const { engine } = t, fields = [];
218
+ for (const arr of keys) {
219
+ const c = t.cols.get(arr);
220
+ if (c.dict) { // (codes can be NaN, e.g. a join's rows without a match)
221
+ const span = Math.max(0, c.dict.length - 1);
222
+ fields.push({ arr, kind: 'int', lo: 0, nanCode: span + 1, maxCode: span + 1 });
223
+ continue;
224
+ }
225
+ const k = c.enc ? { data: c.data, enc: c.enc, n: t.n, type: t.type === Float32Array ? 'f32' : 'f64' } : c.data;
226
+ const [lo, hi, frac, nan] = yield* engine.evalG(['min(k == k ? k : Infinity)', 'max(k == k ? k : -Infinity)', 'count(k == k && k != floor(k))', 'count(k != k)'], { k });
227
+ if (!(hi >= lo)) fields.push({ arr, kind: 'int', lo: 0, nanCode: 1, maxCode: 1 }); // only NaN
228
+ else if (frac === 0 && Number.isFinite(lo) && Number.isFinite(hi) && hi - lo < 2 ** 53) fields.push({ arr, kind: 'int', lo, nanCode: hi - lo + 1, maxCode: hi - lo + (nan ? 1 : 0) });
229
+ else fields.push({ arr, kind: 'float' });
230
+ }
231
+ let word = 0, used = 0;
232
+ for (const k of fields) {
233
+ k.bits = k.kind === 'float' ? 64 : bitsFor(k.maxCode);
234
+ if (used + k.bits > 64) { word++; used = 0; }
235
+ k.word = word;
236
+ used += k.bits;
237
+ }
238
+ if (word >= MAX_WORDS) return null;
239
+ fields.forEach((k, i) => { k.shift = fields.slice(i + 1).filter((x) => x.word === k.word).reduce((s, x) => s + x.bits, 0); });
240
+ return { fields, W: word + 1 };
241
+ }
242
+
243
+ // sources -> accumulators: identical ones shared; count(1) adds a constant
244
+ // 1 (nothing stored per row). Each is { key, op, ast (null: constant) }.
245
+ function accumulators(sources) {
246
+ const accs = [], map = [];
247
+ for (const src of sources) {
248
+ const it = analyze(src).items[0]; // (one at a time: no limit on reductions here)
249
+ const key = `${it.reduce}|${toSource(it.ast)}`;
250
+ let j = accs.findIndex((a) => a.key === key);
251
+ if (j < 0) {
252
+ const constant = it.reduce === 'count' && toSource(it.ast) === '(1 != 0)';
253
+ j = accs.push({ key, op: it.reduce === 'min' || it.reduce === 'max' ? it.reduce : 'add', ast: constant ? null : it.ast }) - 1;
254
+ }
255
+ map.push(j);
256
+ }
257
+ return { accs, map };
258
+ }
259
+
260
+ function fieldParams(dv, o, fields) {
261
+ fields.forEach((k, i) => {
262
+ const b = o + FIELD * i, int = k.kind === 'int';
263
+ dv.setFloat64(b, int ? k.lo : 0, true);
264
+ dv.setBigUint64(b + 8, BigInt(int ? k.nanCode : 0), true);
265
+ dv.setBigUint64(b + 16, BigInt(k.bits), true);
266
+ dv.setBigUint64(b + 24, BigInt(k.shift), true);
267
+ dv.setBigUint64(b + 32, (1n << BigInt(k.bits)) - 1n, true);
268
+ });
269
+ }
270
+
271
+ function lookupParams(dv, lut, cnt, recs, P, plan) {
272
+ [lut, cnt, recs, P].forEach((x, i) => dv.setUint32(4 * i, x, true));
273
+ dv.setBigUint64(16, plan.base, true);
274
+ dv.setUint32(24, plan.sh, true);
275
+ }
276
+
277
+ // The number of groups, estimated from how often each key (tuple) shows up
278
+ // among rows spread over the table (Chao1: seen + once^2 / (2 twice); with
279
+ // no key seen twice, the keys seen once scaled up to the whole table); keys
280
+ // as 53-bit hashes of their bits, -0 as 0, every NaN alike. Each row is at a
281
+ // random place in its stretch of the table: at evenly spaced rows, a table
282
+ // of sorted runs of the same keys (partial groups put together) would show
283
+ // every key once per run.
284
+ export function estimateGroups(t, keys) {
285
+ const { engine, n } = t, m = Math.min(n, SAMPLE), f32 = t.type === Float32Array, step = n / m;
286
+ const cols = keys.map((k) => { const c = t.cols.get(k); return { d: engine.live(c.data), enc: c.enc ?? null }; });
287
+ const hashes = new Float64Array(m), f = new Float64Array(1), u = new Uint32Array(f.buffer);
288
+ let x = 0x9e3779b9;
289
+ for (let j = 0; j < m; j++) {
290
+ x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
291
+ const i = Math.min(n - 1, Math.floor((j + (x >>> 0) / 2 ** 32) * step));
292
+ let h1 = 0x811c9dc5, h2 = 0x2545f491;
293
+ for (const { d, enc } of cols) {
294
+ const v = enc ? decodeOne(enc, d[i], f32) : d[i];
295
+ f[0] = v === v ? v + 0 : NaN;
296
+ h1 = Math.imul(Math.imul(h1 ^ u[0], 0x01000193) ^ u[1], 0x01000193);
297
+ h2 = Math.imul(Math.imul(h2 ^ u[1], 0x5bd1e995) ^ u[0], 0x5bd1e995);
298
+ h2 ^= h2 >>> 15;
299
+ }
300
+ hashes[j] = (h1 >>> 0) * 2 ** 21 + (h2 >>> 11);
301
+ }
302
+ hashes.sort();
303
+ let seen = 0, once = 0, twice = 0;
304
+ for (let j = 0; j < m; ) {
305
+ let q = j + 1;
306
+ while (q < m && hashes[q] === hashes[j]) q++;
307
+ seen++;
308
+ if (q - j === 1) once++; else if (q - j === 2) twice++;
309
+ j = q;
310
+ }
311
+ return Math.min(n, seen + (twice ? (once * once) / (2 * twice) : once * (n / m)));
312
+ }
313
+
314
+ // word 0 of r for m rows spread over the table, as doubles (only the
315
+ // partitions' balance depends on them), sorted
316
+ function sampleWord0(t, fields, m) {
317
+ const { engine, n } = t, f32 = t.type === Float32Array, step = n / m;
318
+ const cols = fields.filter((k) => k.word === 0).map((k) => { const c = t.cols.get(k.arr); return { k, d: engine.live(c.data), enc: c.enc ?? null }; });
319
+ const out = new Float64Array(m), f = new Float64Array(1), u = new Uint32Array(f.buffer);
320
+ for (let j = 0; j < m; j++) {
321
+ const i = Math.floor(j * step);
322
+ let r = 0;
323
+ for (const { k, d, enc } of cols) {
324
+ const v = enc ? decodeOne(enc, d[i], f32) : d[i];
325
+ if (k.kind === 'int') r = r * 2 ** k.bits + (v === v ? v - k.lo : k.nanCode);
326
+ else if (v !== v) r = 2 ** 64;
327
+ else { f[0] = v + 0; r = u[1] >= 0x80000000 ? (~u[1] >>> 0) * 2 ** 32 + (~u[0] >>> 0) : (u[1] + 0x80000000) * 2 ** 32 + u[0]; }
328
+ }
329
+ out[j] = r;
330
+ }
331
+ return out.sort();
332
+ }
333
+
334
+ // word w of m records spread over records s..e (as doubles), sorted
335
+ function sampleRecords(b, s, e, RB, m, w) {
336
+ const u = new Uint32Array(b.buffer, b.byteOffset, Math.floor(b.length / 4)), out = new Float64Array(m), step = (e - s) / m;
337
+ for (let j = 0; j < m; j++) { const a = ((s + Math.floor(j * step)) * RB + 8 * w) / 4; out[j] = u[a + 1] * 2 ** 32 + u[a]; }
338
+ return out.sort();
339
+ }
340
+
341
+ // The lookup from sorted samples of word 0: NB buckets of (r0 - base) >>
342
+ // sh over the bulk of the samples (the m/2P smallest and largest may fall
343
+ // outside, in the end buckets), merged in order into P partitions of about
344
+ // the same number of samples. Any lookup in order keeps the partitions in
345
+ // key order; the samples only make them even.
346
+ function planPartitions(sorted, P) {
347
+ const m = sorted.length, cut = Math.floor(m / (2 * P));
348
+ let lo = sorted[cut], hi = sorted[m - 1 - cut];
349
+ if (!(hi > lo)) { lo = sorted[0]; hi = sorted[m - 1]; } // (few distinct values: keep them all apart)
350
+ lo = Math.min(lo, 2 ** 64 - 2 ** 12); // (below 2^64 as a uint64)
351
+ let sh = 0;
352
+ while ((hi - lo) / 2 ** sh >= NB) sh++;
353
+ const counts = new Float64Array(NB), scale = 2 ** -sh;
354
+ for (let j = 0; j < m; j++) { const b = sorted[j] < lo ? 0 : Math.floor((sorted[j] - lo) * scale); counts[b < NB ? b : NB - 1]++; }
355
+ const lut = new Uint16Array(NB);
356
+ for (let b = 0, cum = 0; b < NB; b++) { lut[b] = Math.min(P - 1, Math.floor((cum * P) / m)); cum += counts[b]; }
357
+ return { lut, base: BigInt(lo), sh };
358
+ }
359
+
360
+ // buckets evenly over an exact range [lo, hi] (BigInts), P partitions
361
+ // evenly over the buckets: lo and hi always end up apart
362
+ function planUniform(lo, hi, P) {
363
+ let sh = 0;
364
+ while ((hi - lo) >> BigInt(sh) >= BigInt(NB)) sh++;
365
+ const span = Number((hi - lo) >> BigInt(sh)) + 1, lut = new Uint16Array(NB);
366
+ for (let b = 0; b < NB; b++) lut[b] = Math.min(P - 1, Math.floor((Math.min(b, span - 1) * P) / span));
367
+ return { lut, base: lo, sh };
368
+ }
369
+
370
+ // counters [units][P] -> partition starts (P + 1, from `base`) and, in
371
+ // place, each unit's first place in each partition; returns the most rows
372
+ // in one partition
373
+ function offsets(c, U, P, starts, base = 0) {
374
+ const tot = new Float64Array(P);
375
+ for (let u = 0; u < U; u++) for (let p = 0, b = u * P; p < P; p++) tot[p] += c[b + p];
376
+ let at = base, most = 0;
377
+ for (let p = 0; p < P; p++) { starts[p] = at; at += tot[p]; if (tot[p] > most) most = tot[p]; }
378
+ starts[P] = at;
379
+ const next = Float64Array.from(starts.subarray(0, P));
380
+ for (let u = 0; u < U; u++) for (let p = 0, b = u * P; p < P; p++) { const x = c[b + p]; c[b + p] = next[p]; next[p] += x; }
381
+ return most;
382
+ }
383
+
384
+ function largest(c, U, P) {
385
+ const tot = new Float64Array(P);
386
+ for (let u = 0; u < U; u++) for (let p = 0, b = u * P; p < P; p++) tot[p] += c[b + p];
387
+ return Math.max(...tot);
388
+ }
389
+
390
+ // partitions, biggest first (the last task to start shouldn't be the longest)
391
+ function bySize(starts, order, P) {
392
+ const idx = Array.from({ length: P }, (_, p) => p);
393
+ idx.sort((a, b) => starts[b + 1] - starts[b] - (starts[a + 1] - starts[a]) || a - b);
394
+ order.set(idx);
395
+ }