Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
dd480f0
docs: design for QWP ingest over ws:// in the Node.js client
nwoolmer Aug 7, 2026
4e5536c
docs: correct QWP wire format after line-by-line Java re-review
nwoolmer Aug 7, 2026
d46cb6f
docs: fix config-key ownership and poison escalation in QWP spec
nwoolmer Aug 7, 2026
96ab58f
docs: fill in QWP store-and-forward internals after fourth review pass
nwoolmer Aug 7, 2026
6b092f0
docs: add reconnect dict catch-up and recovery no-dedup rule to QWP spec
nwoolmer Aug 7, 2026
1c48919
docs: add hot-spare provisioning and close() ordering to QWP spec
nwoolmer Aug 7, 2026
a47fbd6
docs: add cap-split flush, dual dict modes and WS control frames to Q…
nwoolmer Aug 7, 2026
b8a0e40
docs: add reset() watermark rollback and the three callback surfaces
nwoolmer Aug 7, 2026
4246912
docs: specify the Gorilla bitstream, including the bit-reversed prefixes
nwoolmer Aug 7, 2026
56e4dbf
docs: correct the Node-side integration scope in the QWP spec
nwoolmer Aug 7, 2026
bf84b57
docs: resolve single-endpoint scope contradiction and stale testing c…
nwoolmer Aug 7, 2026
a2e69e6
docs: specify upgrade-failure classification and TLS trust-store mapping
nwoolmer Aug 7, 2026
9ad6d69
docs: add the commit frame, row rollback, and missing wire limits
nwoolmer Aug 7, 2026
9cba6dc
docs: cover PR 4/5 column rules and the remaining ingest classes
nwoolmer Aug 7, 2026
b04a237
docs: describe the staging buffer swap and close the class coverage m…
nwoolmer Aug 7, 2026
94193ad
docs: correct notification inbox policy and the close() suppression test
nwoolmer Aug 7, 2026
52f643e
docs: correct per-dispatcher capacities and complete the event kind list
nwoolmer Aug 7, 2026
63bf938
docs: add config value grammars and correct the auto_flush_bytes default
nwoolmer Aug 7, 2026
ead61b2
docs: add cross-key validation and correct the sf_durability mode count
nwoolmer Aug 7, 2026
26920e5
docs: add connect timing and precise sf_durability semantics
nwoolmer Aug 7, 2026
267aec8
docs: add quarantine mechanics, the unset sentinel rule, and missing …
nwoolmer Aug 7, 2026
07b303b
docs: add auth precedence rules and the wss-only TLS constraint
nwoolmer Aug 7, 2026
7c11e57
docs: bring multi-host failover into scope and add the addr grammar
nwoolmer Aug 7, 2026
536eb65
docs: point-by-point consistency fixes after the failover scope change
nwoolmer Aug 7, 2026
5d88829
docs: resolve the staging-buffer decision and renumber the PR stack
nwoolmer Aug 7, 2026
65b78b0
docs: remove ingest-side zstd, which is an egress-only feature
nwoolmer Aug 7, 2026
a14d239
docs: correct what actually drives endpoint rotation
nwoolmer Aug 7, 2026
8168a52
docs: complete the configuration defaults and note durability without…
nwoolmer Aug 7, 2026
a064c07
docs: verify the golden-vector harness is buildable and record its te…
nwoolmer Aug 7, 2026
6c12996
docs: specify frame parsing, including inbound defragmentation
nwoolmer Aug 7, 2026
098cc9c
docs: add ACK-to-FSN correlation and the delta-dict runtime fallback
nwoolmer Aug 7, 2026
6b6f95a
docs: define the FSN model, which eight other sections depended on
nwoolmer Aug 7, 2026
8c815ba
docs: specify torn-tail segment recovery
nwoolmer Aug 7, 2026
eba0ec7
docs: add implementation plan 1, the ws:// walking skeleton
nwoolmer Aug 7, 2026
34b40d1
docs: add implementation plans 2 to 4 and a plan index
nwoolmer Aug 7, 2026
9586a6a
feat(qwp): add unsigned LEB128 varint codec
nwoolmer Aug 7, 2026
7b43966
feat(qwp): add protocol constants, columnar table buffer, and frame e…
nwoolmer Aug 7, 2026
1058ebf
feat(qwp): add websocket masking, frame codec, handshake, and transpo…
nwoolmer Aug 7, 2026
8f0738f
feat(qwp): add QwpBuffer and QwpTransport
nwoolmer Aug 7, 2026
f4f3100
feat(qwp): route ws:// and wss:// to the QWP buffer and transport
nwoolmer Aug 7, 2026
57adecb
test(qwp): add end-to-end ws:// ingest test against QuestDB
nwoolmer Aug 7, 2026
9d377b0
fix(qwp): encode the designated timestamp as an empty-name column
nwoolmer Aug 7, 2026
f6582a4
test(qwp): allow the e2e test to target an already-running QuestDB
nwoolmer Aug 7, 2026
39e6357
docs: add plan 1 -> plan 2 handoff for the next agent
nwoolmer Aug 7, 2026
c573fb2
refactor(qwp): allow a flush to produce multiple frames
nwoolmer Aug 7, 2026
93cba9f
feat(qwp): add remaining fixed-width column types
nwoolmer Aug 7, 2026
dffcd95
feat(qwp): add VARCHAR and BINARY columns
nwoolmer Aug 7, 2026
c2d5a94
feat(qwp): add array, decimal and geohash columns with their locks
nwoolmer Aug 7, 2026
c917b42
feat(qwp): roll back the in-progress row when a setter throws
nwoolmer Aug 7, 2026
0d83058
feat(qwp): add connection-scoped symbol dictionary
nwoolmer Aug 7, 2026
9e8df43
feat(qwp): add delta symbol dictionary encoding
nwoolmer Aug 7, 2026
ac48760
feat(qwp): add Gorilla timestamp encoding
nwoolmer Aug 7, 2026
4f02488
feat(qwp): add cap-splitting and the commit frame
nwoolmer Aug 7, 2026
666fa74
test(qwp): end-to-end coverage for all supported column types
nwoolmer Aug 7, 2026
c0449d0
test(qwp): use a still-unsupported setter for the not-supported guard
nwoolmer Aug 7, 2026
e336848
docs: add plan 2 -> plan 3 handoff for the next agent
nwoolmer Aug 7, 2026
90eb9bc
feat(qwp): decode server response frames
nwoolmer Aug 7, 2026
8289e8f
feat(qwp): add error categories and default policy mapping
nwoolmer Aug 7, 2026
4378a9d
feat(qwp): correlate connection-scoped ACK sequences to FSNs
nwoolmer Aug 7, 2026
abe782d
feat(qwp): handle ACK and NACK responses, surface in-flight loss
nwoolmer Aug 7, 2026
8a62b98
feat(qwp): add poison-frame detector with strike and dwell conditions
nwoolmer Aug 7, 2026
fcf7b37
feat(qwp): parse multi-host addr lists with IPv6 support
nwoolmer Aug 7, 2026
72281a2
feat(qwp): add state-ranked host tracker with private cursors
nwoolmer Aug 7, 2026
9dfee9b
feat(qwp): add reconnect, endpoint rotation and dictionary catch-up
nwoolmer Aug 7, 2026
0db1b2c
feat(qwp): add drop-oldest dispatchers and derived connect mode
nwoolmer Aug 7, 2026
8b03974
fix(qwp): enforce spec config minimum of 16 for notification inbox ca…
nwoolmer Aug 7, 2026
b5399b2
docs: add plan 3 -> plan 4 handoff for the next agent
nwoolmer Aug 7, 2026
5995104
feat(qwp): add CRC32C (Castagnoli) implementation
nwoolmer Aug 7, 2026
b5f2b6c
feat(qwp): add SF01 segment format with torn-tail recovery
nwoolmer Aug 7, 2026
e6b271e
feat(qwp): add FSN-keyed segment ring with ACK-driven trim
nwoolmer Aug 7, 2026
9b9c59e
feat(qwp): replay unacked frames from the ring after reconnect
nwoolmer Aug 7, 2026
4ff4a2f
feat(qwp): add alternating-generation crash-safe boundary record
nwoolmer Aug 7, 2026
68cb938
feat(qwp): add SYD1 persisted symbol dictionary
nwoolmer Aug 7, 2026
0250872
feat(qwp): degrade to full-dict mode when the symbol side file fails
nwoolmer Aug 7, 2026
36daae2
feat(qwp): add slot locking and the store-and-forward engine
nwoolmer Aug 7, 2026
d183eb3
test(qwp): add crash-recovery tests for store-and-forward
nwoolmer Aug 7, 2026
f878edd
feat(qwp): wire the store-and-forward engine into the transport
nwoolmer Aug 7, 2026
34a9ed6
docs(qwp): document ws:// support and release 4.3.0
nwoolmer Aug 7, 2026
e5f6876
fix(qwp): review fixes for segment clobbering, delta sizing, boundary…
nwoolmer Aug 7, 2026
b378dbc
feat(qwp): periodic durability, quarantine, durable acks, and connect…
nwoolmer Aug 7, 2026
f5fa8d2
feat(qwp): wire the symbol dictionary into the buffer for delta mode …
nwoolmer Aug 7, 2026
b98cbea
feat(qwp): orphan scan + background drainers to replay crashed slots …
nwoolmer Aug 7, 2026
ddedf4b
docs(qwp): handoff plan7 — A1 orphan drainers complete
nwoolmer Aug 7, 2026
6b46c43
feat(qwp): sf-manifest.bin chain-head manifest for recovery cross-che…
nwoolmer Aug 7, 2026
2d3ddb7
docs(qwp): handoff plan8 — C2 manifest complete
nwoolmer Aug 7, 2026
f9e2fbb
docs: benchmark suite design for the QWP Node client
nwoolmer Aug 8, 2026
c8c367e
docs: add Plan B, the QWP benchmark suite
nwoolmer Aug 8, 2026
4c34fce
docs: fix four defects in Plan B found by checking it against the code
nwoolmer Aug 8, 2026
cf18eee
docs: second review pass on Plan B — three more defects
nwoolmer Aug 8, 2026
f9461ec
docs: third review pass on Plan B — verified the harness by running it
nwoolmer Aug 8, 2026
a0f6400
docs: fourth review pass on Plan B — reorder validation before measur…
nwoolmer Aug 8, 2026
622cdad
chore: release 5.0.0 instead of 4.3.0, with the rationale recorded
nwoolmer Aug 8, 2026
d72ccbc
docs: fifth review pass on Plan B — priming the dict does not enable …
nwoolmer Aug 8, 2026
7540628
docs: sixth review pass on Plan B — Tasks 7 to 9 line by line
nwoolmer Aug 8, 2026
84fdd3b
docs: seventh review pass — Task 9 README and a spec-to-plan sweep
nwoolmer Aug 8, 2026
ef9b4f2
docs: eighth review pass — sinks, and a Self-Review that can catch gaps
nwoolmer Aug 8, 2026
118b45d
docs: review passes nine to thirteen — ten defects, then a clean pass
nwoolmer Aug 8, 2026
2c95693
docs: review passes fourteen to eighteen — six defects
nwoolmer Aug 8, 2026
aa26ba1
docs: review passes nineteen to twenty-three — seven defects
nwoolmer Aug 8, 2026
0e98f46
docs: review passes twenty-four to twenty-eight — the benchmark tree …
nwoolmer Aug 8, 2026
68c8237
docs: review passes twenty-nine to thirty-three — one defect, four clean
nwoolmer Aug 8, 2026
efb78d0
docs: review passes thirty-four to thirty-eight — widened beyond Plan B
nwoolmer Aug 8, 2026
ef61af3
docs: review passes thirty-nine to forty-three — the handoff chain
nwoolmer Aug 8, 2026
d652cf4
docs: review passes forty-four to forty-eight — plan vs current source
nwoolmer Aug 8, 2026
21a75ff
docs: review passes forty-nine to fifty-three — auditing "nothing is …
nwoolmer Aug 8, 2026
7384910
docs: review passes fifty-four to fifty-eight — spec 9.1 defaults vs …
nwoolmer Aug 8, 2026
ea28b7a
docs: review passes fifty-nine to sixty-three — four spec-9.1 gaps in…
nwoolmer Aug 8, 2026
50afb9f
docs: review passes sixty-four to sixty-eight — wire format holds, on…
nwoolmer Aug 8, 2026
2a12dc6
bench: add seeded workload generators
nwoolmer Aug 8, 2026
f04cd0d
bench: add hand-written floor baselines
nwoolmer Aug 8, 2026
48bd28b
bench: add bench scripts and split bench files from tests
nwoolmer Aug 8, 2026
712a0e1
test: add wire-format invariants over the benchmark workloads
nwoolmer Aug 8, 2026
2ad0e88
bench: add encoder throughput benchmarks with floor comparison
nwoolmer Aug 8, 2026
519fd79
bench: add row-building benchmarks for QwpBuffer
nwoolmer Aug 8, 2026
b55ab21
bench: add store-and-forward benchmarks with a disk baseline
nwoolmer Aug 8, 2026
da35467
bench: add end-to-end latency script for both flush contracts
nwoolmer Aug 8, 2026
171a18e
docs: how to run the QWP benchmarks and how to read them
nwoolmer Aug 8, 2026
131d46b
chore: remove internal QWP planning docs (plans and specs) from the p…
nwoolmer Aug 8, 2026
215b44f
fix(qwp): close five spec 9.1/6.4/7.4 gaps in the shipped client
nwoolmer Aug 8, 2026
f41da02
fix(qwp): address review data safety findings
nwoolmer Aug 8, 2026
9ffdc6c
fix(qwp): harden rereview failure paths
nwoolmer Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,55 @@ The Undici HTTP agent was introduced in 4.0.0, and it is the default HTTP transp
The standard HTTP/HTTPS modules of Node.js are still supported for backwards compatibility.
Use the <i>stdlib_http</i> option to switch to the standard HTTP/HTTPS modules.

## Transport support matrix

| Protocol | Transport | Notes |
|---|---|---|
| `http` / `https` | HTTP | ILP, request/response |
| `tcp` / `tcps` | TCP | ILP, persistent |
| `ws` / `wss` | WebSocket | **QWP** — columnar binary, store-and-forward |

### QWP caveats

These properties of the WebSocket (QWP) sender differ from what the HTTP/ILP
senders do, and each one can surprise a user who assumes otherwise:

- `flush()` resolves once rows are **published** to the send log, not when the
server acknowledges them. This differs from `http::`.
- Delivery is **at-least-once**. A retried batch can duplicate rows; use a
`DEDUP` table if you need idempotence.
- Symbol columns are serialized in **delta mode**: a connection dictionary is
shared with the buffer and persisted to `<sf_dir>/.symbol-dict` in disk mode,
so only newly-introduced symbols ride each frame. Recovery re-seeds it
positionally, keeping ids dense from 0 — so **don't** assume symbol ids are
stable across senders or restarts.
- An acknowledgement means server-side **commit**, not object-store durability.
Set `request_durable_ack=on` if durability gates downstream work — the client
then requests durable ACKs and **fails fast at connect** if the server cannot
confirm them (`X-QWP-Durable-Ack: enabled`).
- `sf_dir` alone is **not** power-loss durability. The default
`sf_durability=memory` never fsyncs, so bytes survive a *process* crash but
not a power loss. `sf_durability=periodic` (requires `sf_dir`) runs a
background barrier every `sf_sync_interval_millis` that fsyncs the active
segment and the ack watermark, making bytes survive an OS/power crash up to
that cadence.
- `drain_orphans` (off by default) replays slots left by a crashed process: on
startup, the client scans `<sf_dir>/` for slot directories not held by a live
lock and hands each to a background drainer (bounded by
`max_background_drainers`) that opens its own WebSocket, re-registers the
recovered symbol dictionary, and replays unacked frames to the server before
releasing the slot. A terminal failure drops a `.failed` sentinel so the slot
is retried only after an operator clears it; a transient outage is retried
indefinitely. Requires `sf_dir`.
- In disk mode, recovery cross-checks the scanned segment chain against
`sf-manifest.bin`, a crash-safe boundary record (magic `SFM1`) written on
each segment rotation using the same alternating-generation scheme as the ack
watermark, and quarantines the slot when the manifest's recorded chain head is
ahead of what is actually on disk (a vanished tail segment = real data loss).
- `tls_roots` accepts PEM or PKCS#12.
- `tls_roots` accepts PEM or PKCS#12. **JKS keystores are not supported** by
Node; convert them first.

## Configuration options

Detailed description of the client's configuration options can be found in
Expand Down
107 changes: 107 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# QWP benchmarks

Ad hoc. No CI job, no gate. Run them when you want a number.

```bash
# Point this at REAL storage, not tmpfs — see "Am I measuring disk?" below.
export QWP_BENCH_DIR=/var/tmp/qwp-bench

pnpm bench # encoder, buffer, sf (no server needed)
pnpm bench:e2e # needs QuestDB on :9000
QDB_ADDR=host:9000 pnpm bench:e2e

pnpm typecheck:bench # tsc over src + benchmarks
pnpm lint:bench # eslint over benchmarks/
```

**If `pnpm <script>` fails before running anything**, that is this repo's
ignored-builds check, not your benchmark. Every earlier QWP handoff hit it. Call
the binary directly instead — the scripts above are still the canonical
definitions, and they work wherever the gate is satisfied:

```bash
./node_modules/.bin/vitest bench --run benchmarks/ # pnpm bench
./node_modules/.bin/tsx benchmarks/e2e.ts # pnpm bench:e2e
./node_modules/.bin/tsc -p tsconfig.bench.json --noEmit # pnpm typecheck:bench
```

## Reading the output

**`hz` is callbacks per second, not rows per second.** Each encoder callback
covers 10,000 rows, so rows/s is `hz × 10_000`. This is the easiest way to
write down a number that is wrong by four orders of magnitude.

**The disk arm spikes every tenth iteration, and that is real.** `APPENDS × 4 KiB
= 400 KiB` per iteration against `segmentBytes: 4 << 20`, so a segment roll fires
on roughly every 10th body. A roll closes the active fd, creates the next `.sfa`,
writes its header, and rewrites the whole 8 KiB `sf-manifest.bin` (`persistManifest`
in `sf/engine.ts`, added by the C2 work in handoff 8 — *after* this plan was first
drafted). At the default durability there is no `fsync` on that path, so the spike
is syscalls and a file creation rather than a device round-trip. Read **p75 as
steady-state append cost** and `max`/`p999` as roughly the cost of a roll: a
bimodal distribution here is the benchmark working, not noise or GC.

It also means **the memory arm is not the disk arm minus the disk.** Memory mode
never reaches `persistFrame`, so it never rolls and never writes a manifest — the
gap between the two arms is segment management *plus* writing, not writing alone.
Don't quote the ratio as "the cost of durability".

**`rme` is the relative margin of error.** Check it before believing a
difference: a 5% gap between two arms each carrying ±3% rme is not a result.

There is no p50 or p90 in `vitest bench` output. The e2e script computes its own
percentiles, which is why it is a separate script.

**sf-on is not always faster than sf-off.** `e2e.ts` runs the two flush
contracts on one server, and the SF engagement is real (the slot dir gets a
`.sfa` + `sf-manifest.bin`). On a same-host server the localhost round-trip can
be *faster* than a disk-backed append, so sf-on can legitimately come out
slower — check that SF is engaged (slot files appear) and the disk baseline is
sane before reading anything into the sign of the gap. They answer different
questions regardless: sf-on is "recoverable if I crash now"; sf-off is "the
server has it". Do not quote one as the other.

## Am I measuring disk?

`os.tmpdir()` is `/tmp` on most Linux boxes, and `/tmp` is frequently **tmpfs —
RAM**. If the SF benchmarks and the e2e sf-on arm run there, they measure memory
while claiming to measure disk, and the `dd` baseline reports an *excellent*
figure — so the guard confirms a false conclusion instead of catching it.

```bash
df -T "$QWP_BENCH_DIR" # type must not be tmpfs
```

## Before you quote anything

1. **Check the disk line.** `sf.bench.ts` prints a `dd` figure first. If it is
far below what the hardware should do, the SF numbers are meaningless —
fix the machine, do not average around it.
2. **Check the spread.** `e2e.ts` runs every arm three times and prints a range.
If p99 varies by more than about 2× across repeats, the machine is too busy.
3. **Do not mix the two e2e numbers.** With SF on, `flush()` returns once the
row is durable *locally*. With SF off it waits for the *server*. They answer
different questions and the SF-on number is not a faster version of the
SF-off one.

## What the floors mean

`encoder.bench.ts` compares against hand-written baselines that do the minimum
byte movement — no null bitmap, no schema, no framing. The floor is not a
target. The gap between floor and encoder is what the protocol costs.

Both sides accumulate into a `sink` so neither can be optimised away. If the
encoder still beats its floor, the comparison itself is wrong — not dead-code
removal. (A flat LONG column lands *at* its floor by design: the encoder uses
the same `writeBigInt64LE` primitive, so near-parity is the correct result.)

## What this suite does not cover

- **Gorilla's raw fallback.** Every workload has perfectly regular timestamps,
so delta-of-delta is always 0 and the compressed path always wins. The
fallback needs a delta-of-delta beyond signed int32 — about a 35-minute jump
at microsecond resolution. `validate.test.ts` asserts the behaviour; these
numbers say nothing about it.
- **Reconnect, replay and drainers.** Dominated by network and disk timing
rather than client code, so a number there measures the environment.
- **Concurrency.** Single sender, single connection throughout.
87 changes: 87 additions & 0 deletions benchmarks/buffer.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { bench, describe } from "vitest";
import { QwpBuffer } from "../src/qwp/buffer";
import { SymbolDict } from "../src/qwp/protocol/symbolDict";
import { WORKLOADS } from "./workloads";

const N = 10_000;
const CAP = 1 << 30; // large enough that nothing splits

/** See Task 5's note — consumes results so nothing is optimised away. */
let sink = 0;

/**
* Handles every column family the workloads generate — see Task 5's note.
*
* Note `Number(v)`: `intColumn` takes a `number` while the workloads carry
* `bigint`, so this layer pays a per-value conversion the encoder benchmarks in
* Task 5 do not (they push the bigint straight into `values`). The two tasks are
* therefore not directly comparable — some of the gap is representation, not
* buffer overhead. All generated values are well under 2^53, so no precision is
* lost; raise N or change a generator and re-check that.
*/
function fill(b: QwpBuffer, rows: ReturnType<typeof WORKLOADS.trades.rows>): void {
for (const row of rows) {
b.table(row.table);
for (const [n, v] of row.symbols) b.symbol(n, v);
for (const [n, v] of row.longs) b.intColumn(n, Number(v));
for (const [n, v] of row.doubles) b.floatColumn(n, v);
for (const [n, v] of row.strings) b.stringColumn(n, v);
// QwpBuffer.at() is synchronous (src/qwp/buffer.ts:173) — do NOT await it.
b.at(row.ts, "us");
}
}

describe("QwpBuffer build + seal", () => {
for (const name of ["trades", "wide", "sparse"] as const) {
const rows = WORKLOADS[name].rows(N);
bench(`build+seal / ${name}`, () => {
const b = new QwpBuffer();
fill(b, rows);
// sealFrames returns Buffer[]; sum BYTES, not the array length --
// `.length` here would be the frame count (always 1 under CAP) and
// would leave the encoded contents unobserved.
for (const f of b.sealFrames(CAP)) sink += f.length;
});
}
});

describe("dictionary mode", () => {
const rows = WORKLOADS.highCardSymbol.rows(N);

bench("full-dict (no dict attached)", () => {
const b = new QwpBuffer();
fill(b, rows);
for (const f of b.sealFrames(CAP)) sink += f.length;
});

// Steady state needs BOTH halves: a populated dictionary and a confirmed
// baseline. confirmedMaxId lives on the buffer, starts at -1, and only moves
// via confirmDeltaPublished() from the transport's publish path -- so a
// fresh buffer with a primed dict still ships every symbol, because
// entriesFrom(-1 + 1) returns everything. Priming the dict alone measures
// nothing new.
const primed = new SymbolDict();
{
const warm = new QwpBuffer();
warm.attachDict(primed);
fill(warm, rows);
warm.sealFrames(CAP);
}

bench("delta-dict (primed + baseline confirmed)", () => {
const b = new QwpBuffer();
b.attachDict(primed);
b.setConfirmedMaxId(primed.size() - 1);
fill(b, rows);
for (const f of b.sealFrames(CAP)) sink += f.length;
});

bench("delta-dict (cold, first batch)", () => {
const b = new QwpBuffer();
b.attachDict(new SymbolDict());
fill(b, rows);
for (const f of b.sealFrames(CAP)) sink += f.length;
});
});

export const _sink = () => sink;
Loading