vectrojs 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/README.md +828 -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 +2247 -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
|
@@ -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
|
+
}
|