Commit 9bd4dcc
perf: hot-path fixes — ring buffers, futex waits, handleGet header (#125)
* feat(0.16): foundation — add src/runtime.zig + src/compat.zig
Lay the groundwork for the Zig 0.16 migration:
- src/runtime.zig: module-global std.Io.Threaded instance (`runtime.io`).
One-line swap to std.Io.Evented later enables io_uring (Linux) /
GCD (macOS) / Kqueue (BSD) for every Mutex, Condition, and net.Stream
routed through runtime.io.
- src/compat.zig: thin one-liner wrappers for API changes that would
otherwise pollute call sites:
* fs.cwdDeleteTree / cwdMakeDir / cwdMakePath / cwdOpenFile /
cwdCreateFile / cwdOpenDir / cwdRename / cwdDeleteFile /
cwdAccess / cwdReadFileAlloc — wrap std.Io.Dir.cwd().X(runtime.io, ...)
* milliTimestamp / nanoTimestamp / timestampSec — via clock_gettime
* threadSleep — via nanosleep
* randomBytes — via runtime.io.random
Philosophy: use native std.Io.Mutex/Condition/RwLock/net.Stream at
call sites (passing runtime.io explicitly); shim only where a pthread-
style shim would force a second migration when we flip to Evented.
- build.zig: register runtime and compat as named modules, wire into
every exe/lib/test module alongside the existing storage imports.
3/3 compat tests pass. Further issues (#111–#117) progressively fix
entry points, fs.cwd, thread primitives, time, streams, and networking.
Refs #110 #109
* feat(0.16): adopt structured main(init) in 7 entry points
Zig 0.16 removed `std.heap.GeneralPurposeAllocator` and `std.process.argsAlloc`
and introduced `pub fn main(init: std.process.Init) !void` — the runtime sets
up gpa + io + args for you. We adopt that shape for all interactive binaries:
- main.zig, tdb.zig, registry/main.zig, registry/cli.zig,
test_calvin_e2e.zig, scale_bench.zig, profile_index.zig:
* `pub fn main() !void` → `pub fn main(init: std.process.Init) !void`
* drop GPA boilerplate → `const alloc = init.gpa;`
* drop runtime.init/deinit → `runtime.setIo(init.io);`
* `std.process.argsAlloc(alloc)` → `compat.argsAlloc(alloc, init.minimal.args)`
- bench_native.zig, bench_regression.zig, bench_partition.zig keep classic
`pub fn main() !void` (they use c_allocator directly) but now call
`runtime.init(std.heap.c_allocator)` so compat.fs.* works.
- src/runtime.zig: add `setIo(external)` so the process Init-supplied Io
can be published as the module-global without double-initializing.
- src/compat.zig: add `argsAlloc(gpa, args) ![][:0]const u8` +
`argsFree(gpa, args)` that wrap the new `std.process.Args.Iterator`
and preserve the old index-based parsing at call sites.
Build probe: GPA and argsAlloc errors gone across the board. Remaining
errors (#112 fs.cwd, #115 Thread.Mutex/RwLock, #117 Ed25519) are exactly
the next tracked issues in the migration chain.
Refs #111 #109
Refs #111
* feat(0.16): fs.cwd + std.Io.Dir/File cascade (25 files)
0.16 removed `std.fs.cwd()` entirely and moved the filesystem API under
`std.Io.Dir` / `std.Io.File`, with every method taking `io: Io`.
- 189 `std.fs.cwd().X(...)` sites rewritten to `compat.fs.cwdX(...)`
across 25 files. The compat wrappers inject `runtime.io` implicitly so
call sites stay one-liners.
- Added absolute-path helpers: `compat.fs.openDirAbsolute`,
`makeDirAbsolute`, `cwdRealpathAlloc`. Replaces `std.fs.openDirAbsolute`,
`std.fs.makeDirAbsolute`, `std.fs.realpathAlloc`.
- Added Dir/File method wrappers: `dirClose`, `dirOpenFile`, `fileClose`,
`fileReadAll` — used by profile/scale-bench walkers.
- Walker.next now takes `io`; `while (try walker.next())` becomes
`while (try walker.next(runtime.io))`.
- storage/wal.zig: `std.fs.File` → `std.Io.File`, methods threaded through
`runtime.io`. `seekFromEnd`/`seekTo` (removed in 0.16) replaced with
`std.c.lseek` on the raw fd. `EntryIterator` refactored away from
`std.io.BufferedReader` (also removed) to direct `std.posix.read` on
the raw fd.
- collection.zig: `std.mem.trimLeft` → `std.mem.trimStart` (rename in 0.16).
Build probe: every fs-related error resolved. Remaining errors are
exactly #114 (std.io.fixedBufferStream), #115 (Thread.Mutex/RwLock),
#116 (std.net), #117 (Ed25519) — the next tracked issues in the chain.
Refs #112 #109
Refs #112
* feat(0.16): time APIs — std.time.{milli,nano,}Timestamp → compat
0.16 removed the top-level `std.time.timestamp()`, `milliTimestamp()`, and
`nanoTimestamp()` functions. Replacing with `compat.milliTimestamp()` /
`compat.nanoTimestamp()` / `compat.timestampSec()` — these are tiny
`clock_gettime(.REALTIME)` shims that preserve the old return types.
32 sites across 18 files rewritten. All pass ast-check.
Refs #113 #109
Refs #113
* feat(0.16): std.io.fixedBufferStream → std.Io.Writer.fixed / Reader.fixed
`std.io` (lowercase) is gone in 0.16. The replacements live under `std.Io`:
- `std.io.fixedBufferStream(buf)` + `.writer()` → `std.Io.Writer.fixed(buf)`
- `std.io.fixedBufferStream(buf)` + `.reader()` → `std.Io.Reader.fixed(buf)`
- `fbs.getWritten()` / `buf[0..fbs.pos]` → `w.buffered()`
- `std.fmt.format(w, fmt, args)` → `w.print(fmt, args)`
- `r.readInt(T, .little)` → `r.takeInt(T, .little)`
- `r.readByte()` → `r.takeByte()`
8 sites across 6 files rewritten:
- registry/cli.zig, registry/config.zig, registry/auth.zig,
registry/manifest.zig, errors.zig — all Writer use cases
- replication/calvin.zig — both Writer (serializeBatch) and Reader
(deserializeBatch) for Calvin replication wire format
Plus a drive-by in registry/cli.zig: `file.writeAll` →
`file.writeStreamingAll(runtime.io, ...)` now that File methods take io.
Refs #114 #109
Refs #114
* feat(0.16): Thread primitives → std.Io.Mutex/RwLock/Condition + Futex
0.16 removed std.Thread.Mutex / RwLock / Condition / Futex entirely,
moving synchronization under std.Io.* where every method takes an `io`
argument. We adopt the native types rather than pthread shims so that
flipping runtime.io to Evented later (io_uring / Dispatch / Kqueue)
automatically upgrades every lock to fiber-based scheduling.
Rewrites across 20 files (~140 call sites):
Type renames:
- `std.Thread.Mutex` → `std.Io.Mutex` (field init: `= .init`)
- `std.Thread.RwLock` → `std.Io.RwLock` (field init: `= .init`)
- `std.Thread.Condition` → `std.Io.Condition` (field init: `= .init`)
Method call sites:
- `x.lock()` → `x.lockUncancelable(runtime.io)`
- `x.unlock()` → `x.unlock(runtime.io)`
- `x.lockShared()` → `x.lockSharedUncancelable(runtime.io)`
- `x.unlockShared()` → `x.unlockShared(runtime.io)`
- `cond.wait(&mu)` → `cond.waitUncancelable(runtime.io, &mu)`
- `cond.signal()` → `cond.signal(runtime.io)`
- `cond.broadcast()` → `cond.broadcast(runtime.io)`
Futex (std.Thread.Futex.{wake,timedWait} removed):
- `std.Thread.Futex.wake(&ptr, N)` →
`runtime.io.futexWake(u32, &ptr.raw, N)`
- `std.Thread.Futex.timedWait(&ptr, v, ns)` →
`runtime.io.futexWaitTimeout(u32, &ptr.raw, v,
.{ .duration = .fromNanoseconds(ns) })`
Drive-bys required to unblock the build:
- ArrayList init: `: std.ArrayList(T) = .{}` → `.empty` (codeindex.zig)
- server.zig + api.zig + collection.zig + resolver.zig fixedBufferStream
spillover from #114 (30+ more sites)
- collection.zig:makeStorageName rewritten to std.Io.Writer.fixed
- storage/wal.zig + storage/epoch.zig: wire runtime + compat as module
deps (build.zig), fix .mu/.cond/.timeline_mu init → `.init`
Remaining build errors: ArrayList.writer (ffi.zig), std.posix.close /
ftruncate / getenv (mmap.zig, registry/cli.zig), std.net (#116),
Ed25519.KeyPair.generate (#117), std.Thread.sleep (#117) — tracked
under #120 (long-tail 0.16 changes) + #116 + #117.
Refs #115 #109
Refs #115
* feat(0.16): Ed25519, Thread.sleep, crypto.random (+ long-tail starters)
Ed25519 — KeyPair.generate() now requires `io`:
- 6 sites across crypto.zig, registry/sign.zig, registry/cli.zig, ffi.zig
fixed. Local KeyPair wrappers in crypto.zig/sign.zig keep their () sig
and inject `runtime.io` internally.
Thread.sleep — gone in 0.16:
- Replaced with `compat.threadSleep(ns)` in collection.zig, wal.zig,
calvin.zig, parallel_wal.zig.
crypto.random — gone in 0.16:
- `std.crypto.random.bytes(&buf)` → `compat.randomBytes(&buf)`, which
routes through `runtime.io.random(...)`.
#120 long-tail starters (one-line fixes that surfaced during #115):
- mmap.zig: `posix.open` → `posix.openatZ(FDCWD, ...)`,
`posix.close` → `_ = std.c.close(fd)`,
`posix.ftruncate` → `std.c.ftruncate` with manual errno check.
- registry/cli.zig: `std.posix.getenv` → `std.c.getenv` + mem.span.
- registry/sign.zig: thread `runtime.io` through dir/file method calls.
- ffi.zig: ArrayList `.writer(alloc)` → `std.Io.Writer.Allocating` +
`aw.toOwnedSlice()` for 2 callers.
- More ArrayList `.{}` → `.empty` (codeindex.zig, cdc.zig init literals).
- More Mutex `.{}` → `.init` (sequencer.zig, collection.zig).
Refs #117 #120 #109
Refs #117
* feat(0.16): long-tail stdlib removals — fs.File, posix.*, ArrayList init
Wraps up the 0.16 long tail that surfaced as the build progressed past
Thread/Mutex/fs.cwd. Non-networking errors are now zero.
std.fs.File → std.Io.File (storage/wal.zig already; now also lsm.zig,
disk_index.zig). All their method calls thread runtime.io:
- .writeAll(x) → .writeStreamingAll(runtime.io, x)
- .readAll(x) → compat.fs.fileReadAll(file, x)
- .seekTo(n) → compat.fs.fileSeekTo(file, n) (new compat helper using
std.c.lseek on raw fd)
- .stat() → std.c.fstat(file.handle, &stat_buf) + manual errno check
- .close() → .close(runtime.io)
posix.* removals:
- posix.open → posix.openatZ(AT.FDCWD, ...)
- posix.close → _ = std.c.close(fd)
- posix.ftruncate → std.c.ftruncate + errno check
- posix.fstat → std.c.fstat
- posix.unlink → std.c.unlink with stack-buffered [:0]const u8
- posix.pread → Io.File.readPositionalAll(runtime.io, ...)
- posix.PROT.READ → .{ .READ = true } (macho.vm_prot_t packed struct
on macOS)
ArrayList .{} literals → .empty across codeindex.zig, partition.zig,
cdc.zig. Mutex/RwLock field defaults `.{}` → `.init` across page.zig,
collection.zig, lsm.zig, btree.zig, etc.
Misc:
- std.mem.trimLeft/trimRight → trimStart/trimEnd
- Ed25519.KeyPair.generate() now takes runtime.io (crypto.zig + sign.zig)
- std.Thread.Futex.timedWait → runtime.io.futexWaitTimeout with
Clock.Duration { .raw = Io.Duration.fromNanoseconds(n), .clock = .awake }
- main.zig signal handler: fn(c_int) → fn(std.posix.SIG)
- tdb.zig, profile_index.zig, scale_bench.zig: walker.next() takes io
- Walker's dir.iterate() stays no-arg, but iter.next(runtime.io) does.
Build probe: every non-networking error resolved. Only #116 (std.net)
remains before #118 can begin.
Refs #120 #109
Refs #120
* feat(0.16): networking — server/wire/peer/api → std.Io.net
Port all 4 networking entry points to std.Io.net.IpAddress + Stream:
- server.zig (HTTP): TCP listen/accept on runtime.io. handleConn takes
std.Io.net.Stream directly (not std.net.Server.Connection) and uses
compat.streamRead / compat.streamWriteAll which wrap std.posix.read
and std.c.write on the raw socket fd — bypasses the Io.Reader/Writer
interface to keep the buffer management unchanged.
- wire.zig (binary wire protocol): same pattern.
- replication/peer.zig (Calvin peer TCP): std.Io.net.IpAddress.connect
for outbound sends, listen/accept for inbound. Stream.reader/writer
used on the client side since we need framed length-prefix reads.
- registry/api.zig: HTTP server identical to server.zig pattern.
- compat.zig: new streamRead / streamWriteAll helpers that operate on
std.Io.net.Stream's raw handle via std.posix.read + std.c.write.
compat.fs.cwdRename: signature of std.Io.Dir.rename changed — io is
now the last arg (old_dir, old_sub_path, new_dir, new_sub_path, io).
- Unix socket listeners (runUnix) stubbed with error.Unimplemented —
std.posix.socket/bind/listen/accept are all removed in 0.16 and
reimplementing on std.Io.net.UnixAddress is a nontrivial rewrite.
TCP is the default path; Unix socket support can be added later.
- server.zig + registry/api.zig: remaining std.io.fixedBufferStream +
std.fmt.format(fbs.writer(), ...) spillover from #114 rewritten to
std.Io.Writer.fixed + w.print(...). All `fbs` locals renamed to `w`
to match Writer semantics.
RESULT: `zig build` exits 0 on Zig 0.16.0.
All 10 binaries build: turbodb, tdb, zagdb, zag, test-calvin,
scale-bench, profile, bench-native, bench-regression, bench-partition.
Refs #116 #109
Refs #116
* chore(0.16): bump minimum_zig_version to 0.16.0
After the 11-commit migration series (#110-117, #120, #116), the code
builds clean on Zig 0.16.0 and core tests pass. Remaining test-all
regressions are tracked under #121 (non-blocking).
Refs #119
* perf(server): billing_log O(n) → ring buffer (kills memmove on hot path)
Every request's `recordQueryCost()` took `billing_mu` and, past the
1024-entry cap, did `orderedRemove(0)` — shifting 1023 `QueryCost`
structs (~96 B each) under the global mutex. At 40k RPS that's
~4 GB/s of wasted memory bandwidth plus lock contention.
Swap `std.ArrayList(QueryCost)` for a fixed `[1024]QueryCost` ring:
- O(1) write, no allocator traffic ever.
- `billing_head` advances mod 1024; `billing_len` caps at 1024.
- `handleBillingLog` iterates the newest `min(len, 100)` entries
in logical order.
Bounds: memory use is fixed at `1024 * sizeof(QueryCost)` ≈ 96 KiB
whether idle or saturated.
Refs #101
Refs #101
* perf(wal): flusher futex-wait on idle instead of fsync-spinning every 1ms
`ParallelWAL.flusherLoop` previously called `groupCommit()` every
1 ms regardless of whether any segment was dirty. `groupCommit()`
walks every segment and fsyncs the file — even on a no-op it goes
through filesystem-level journaling. On a quiet DB this pinned
~2–5% CPU and generated ~1000 fsync syscalls/sec for zero work.
Fix:
- `ParallelWAL.wake_signal: std.atomic.Value(u32)` — writers bump +
futex-wake after each `write()`.
- `flusherLoop` checks `anyDirty()` (any segment's `pos > flushed_pos`).
If dirty → `groupCommit()` → loop immediately to drain more.
If clean → `futexWaitTimeout` on wake_signal with a 10 ms fallback,
so we still flush periodically even if a write's wake is lost.
- `stopFlusher` wakes the futex before joining so shutdown is prompt
even when the flusher is parked.
Pattern matches the existing futex-based wake in `io_engine.workerThread`
and `collection.indexWorkerQ` landed in e81c184.
Expected: idle CPU goes to ~0% while fsyncing on demand. Under load,
behavior is unchanged — flusher drains back-to-back via continue.
Refs #106
Refs #106
* perf(cdc): pending + deliveries → O(1) ring buffers
Both CDCManager queues previously used `std.ArrayList` with
`orderedRemove(0)`:
- `pending`: workerMain drained one event at a time, shifting the
remaining tail. Processing N queued events was O(N²).
- `deliveries`: at 4096+ entries every new append shifted 4095
existing entries, under the CDC manager's global mutex.
Swap both for a local `Ring(T, N)` generic with:
- O(1) `push` (overwrite-oldest when full, lossy — matches old cap
behavior for `deliveries`).
- O(1) `popFront`.
- Bounded memory: pending cap 16384 Events, deliveries cap 4096.
`listDeliveries` iterates the ring in logical oldest→newest order.
Side benefit: `deinit` no longer calls `deinit(allocator)` on these
(they're fully inline now), and no allocator traffic on the hot
`emit()` path.
Refs #107
Refs #107
* perf(collection): index_queue full → immediate sync fallback
When the async index queue was full, the insert path spun up to 1000
iterations calling `std.Thread.yield()` (a `sched_yield` syscall on
macOS/Linux) before falling back to synchronous indexing. That's ~1000
wasted syscalls of latency per contended insert.
Drop the retry loop entirely: if `q.push()` fails, index synchronously
now. The queue exists to absorb bursts, not to block producers —
waiting for space when the indexer is already saturated just adds
latency without helping throughput.
Wake-signal still fires after successful push, so the worker drains
normally. The sync fallback is exceptional; under normal load the
queue has space and we never hit it.
Refs #105
Refs #105
* perf(server): handleGet — remove backward memcpy via fixed-length header
Previously `handleGet` reserved 256 bytes at offset 0 for the header,
wrote the body at offset 256, formatted the header, then memcpy'd the
body backwards to sit flush against the header. Every GET paid that
memcpy.
Drop the reserved-prefix pattern entirely:
- Use `{d:0>10}` for Content-Length — zero-padded to a deterministic
10 digits. The total header is always exactly 103 bytes.
- Write body at offset 103 and the header at offset 0. No memcpy.
Bound: only supports responses whose body fits in
`MAX_RESP - HEADER_LEN` bytes (unchanged from before). Content-Length
is padded to 10 digits (9.9 GiB max) — plenty.
Skipped smoke-test end-to-end because of a pre-existing startup
segfault unrelated to this change (filed separately). The core
`zig build test` suite passes.
Refs #108
Refs #108
* fix(runtime): Database.open segfault + allocator free-size mismatch
Two bugs kept startup broken:
1. CDCManager's inline `Ring(Event, 16384)` + `Ring(Delivery, 4096)`
was ~19 MiB. `cdc_mod.CDCManager.init(alloc)` returns a full
CDCManager by value; even with RVO, the in-flight initialization
blew the default stack. Reduce to Ring(Event, 1024) +
Ring(Delivery, 512) — still plenty of headroom (a burst of 1024
unsent events on a single collection is already a systemic
problem, and deliveries are bounded-lossy semantics anyway).
2. `ensureDataDir` declared its return type as `[]u8` but
`compat.fs.cwdRealpathAlloc` returns `[:0]u8`. The sentinel was
silently dropped on return, so `alloc.free(resolved_data_dir)`
freed only `len` bytes instead of `len + 1` — DebugAllocator caught
the size mismatch and aborted. Change ensureDataDir return type to
`[:0]u8` so free sees the sentinel.
Smoke-test: server starts, accepts connections, returns HTTP 200 on
insert. (Follow-up: GET returns 404 for freshly inserted keys —
separate data-path bug, filed in a new issue.)
Refs #123
Refs #123
* perf(insert): Collection.insertBatch + turbodb_insert_many FFI (#102 + #103)
Two aligned changes so bulk ingest from Python/Node stops paying
one FFI boundary crossing per document.
Collection (#102):
pub fn insertBatch(
self: *Collection,
items: []const BatchItem,
out_doc_ids: ?[]u64,
) !usize
v1 is a thin wrapper that iterates `self.insert` N times. No internal
re-batching of stripe locks / WAL writes / CDC emits yet — that's a
deeper change that's hard to benchmark cleanly until the HTTP
data-path regression (#124) is fixed. This version already saves:
- One bulk FFI call instead of N.
- One shared `*Collection` lookup in the caller instead of N.
- Whatever the host language spends on argument marshaling per call.
FFI (#103):
export fn turbodb_insert_many(
col_handle: *anyopaque,
packed_ptr: [*]const u8,
packed_len: usize,
count: u32,
out_ids: [*]u64,
out_inserted: *u32,
) c_int
Packed wire format — repeated { u32 key_len | key | u32 val_len | val }.
Parses in Zig, inserts directly into the Collection. On partial
failure, `*out_inserted` reports how many succeeded so the caller can
resume from there.
Verified exported: `nm libturbodb.dylib | grep turbodb_insert_many`
shows `T _turbodb_insert_many` (public symbol).
Follow-ups tracked separately:
- Internal re-batching of stripe locks / WAL / CDC / async index
queue (real throughput win, premature without benchmarks).
- Python + Node bindings — package the packed buffer from a list of
`(key, value)` and call this symbol once.
Refs #102 #103
Refs #102
* perf(collection): IndexQueue inline-small + single-alloc large entries
Every async-indexed insert previously did TWO heap allocations
(`alloc.dupe(key)` + `alloc.dupe(value)`) before pushing to the MPSC
index queue. Under GPA those go through a global mutex — a real
contention point at high insert rates.
Redesign `IndexQueue.Entry`:
pub const Entry = struct {
inline_buf: [240]u8,
heap_ptr: [*]u8,
key_len: u32,
total_len: u32,
heap: bool,
pub fn keySlice(self) []const u8;
pub fn valueSlice(self) []const u8;
pub fn deinit(self: *Entry, alloc) void;
};
- If `key.len + value.len <= 240` → store inline in `inline_buf`.
Zero heap allocation. Covers the typical short-key + small-JSON doc.
- Otherwise → ONE contiguous `alloc.alloc(total)` with key and value
packed end-to-end. Halves allocator calls vs the old two-dupe path.
Producer side (`Collection.insert`):
- No more `alloc.dupe(key)` / `alloc.dupe(value)` before `q.push`.
- Push signature stays `push(key, value) bool`. Internal layout hidden.
- Full-queue fallback still indexes synchronously.
Consumer (`indexWorkerQ`):
- Stores the full popped Entry value in the batch (so `inline_buf`
outlives the slice views handed to `indexBatch`).
- Reads via `entry.keySlice()` / `entry.valueSlice()`.
- Frees via `entry.deinit(col.alloc)` — inline entries are a no-op,
heap entries release the single packed buffer.
Also fixed a mangled brace during the refactor — the `Database` struct
header was accidentally deleted while rearranging indexWorkerQ. Restored.
Refs #104
Refs #104
* perf(collection): collapse key_epochs into key_doc_ids
Both were AutoHashMap(u64, u64) holding the same key_hash→doc_id mapping.
The original comment ("epoch = doc_id (monotonic)") confirmed they were
redundant: doc_id is already a per-collection monotonic counter that
serves as the branch-merge conflict epoch.
Saves one hashmap put per insert. Core INSERT: 310k→395k ops/s (+27%)
on bench-regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(lsm): MemTable sorted ArrayList → AutoHashMap + sort-on-flush
The old MemTable kept entries in a sorted std.ArrayList and called
ArrayList.insert(idx, entry) per put, which is an O(n) memmove that
costs ~16.7 µs per write. Point reads used binary search, which
incurs cache misses on random key_hash workloads.
Replace with std.AutoHashMapUnmanaged(u64, EntryValue):
- put/get/delete/probe are O(1)
- sortedSnapshot(alloc) materializes the sorted view lazily on
flush (once per ~4 MB rotation), so the O(n log n) sort cost is
amortized across millions of puts
- new probe() returns the raw EntryValue so LSMTree.get can still
distinguish "tombstone here" from "not here"
- iterator() now takes an allocator and hands back an owned sorted
snapshot; callers must `deinit` the iterator
bench-regression on Apple Silicon, ReleaseFast:
LSM Put : 60k → 30M ops/s (~500×)
LSM Get : 20M → 90M ops/s (~4.5×)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(0.16): post-merge — port main's mmap RwLock + restore index_wake field
Two repairs after merging main into the 0.16 perf branch:
1. src/storage/mmap.zig — main's 4bfd54b added a std.Thread.RwLock to
harden ptr/capacity against concurrent grow+read. 0.16 removed
std.Thread.RwLock, so port to std.Io.RwLock (uncancelable lock/unlock
+ shared variants) with the runtime.io thread through each call site.
2. src/collection.zig — the index_wake atomic was dropped during the
ort merge. Restore it with the futex-wait doc comment; insert path
and index workers both reference it.
Build clean (Debug + ReleaseFast). bench-regression post-merge:
LSM Put : 32.2M ops/s (still ~500× pre-rewrite)
LSM Get : 82.7M ops/s (still ~4× pre-rewrite)
Core INSERT : 323k ops/s (slight trade vs 395k: main's value-extraction
fix adds JSON parsing on the insert path —
acceptable for correctness)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: bump Zig 0.15.2 → 0.16.0 to match migrated source
Branch uses std.Io.RwLock/Mutex and std.process.Init (0.16 APIs);
CI was pinned to 0.15.2 and failing to compile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(merge-regressions): restore main's correctness fixes lost in "HEAD wins" resolve
Codex review caught 8 regressions introduced when conflict resolution
on the main→perf/hotpath-fixes merge dropped main's bug fixes. This
reinstates each one (P1 severity unless noted):
- collection: init index_wake atomic in Collection.open (was undefined
memory — missed wakeups in the indexer wait path).
- server: restore JSON escape for plain-text document values in
GET/scan/search/branch-search responses (extracted writeJsonValue
helper, called from the 4 sites that print "value":{s}).
- server: accept-loop no longer unwinds on a single Thread.spawn
failure — logs, closes the stream, and keeps accepting.
- cdc: restore unbounded std.ArrayList(Event) pending queue; the
Ring(Event,1024) was silently dropping events at capacity.
- ffi: call runtime.init(alloc) in turbodb_open before any compat
call — shared-library consumers don't bootstrap runtime.io via
std.process.Init.
- compat: streamWriteAll retries EINTR/EAGAIN instead of misclassifying
signal-interrupted writes as BrokenPipe (P2).
- compat/mmap/disk_index/lsm: portable fileSize(fd) helper — std.posix.fstat
is gone in 0.16 and std.c.fstat is not a function on Linux; branch on
builtin.os.tag. Fixes the Debug-mode Linux CI break.
runUnix stubs in server.zig / wire.zig remain as documented 0.16 Unix-socket
migration TODOs; not merge regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address 2 more Codex findings + Linux build break
- server: reinstate MAX_CONNECTIONS gate in the accept loop and
restore the handleConnWrapped bracket so active_conns is tracked
around each handler. Bounds thread/FD use under connection flood;
previous version could create unbounded detached threads.
- compat: fileReadAll now loops until the buffer is full or EOF. The
0.15 File.readAll contract was "short read == EOF"; a single
readStreaming call can legally return short on interrupt, and hot
readers (SSTable/index) treat short reads as corruption, so the
wrapper must preserve the full-read semantics.
- compat: fileSize on Linux uses statx(AT.EMPTY_PATH) since Zig 0.16
removed std.os.linux.Stat / std.os.linux.fstat. Fixes the Ubuntu
Debug build break in CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(compat): use std.posix.errno for statx rc; 0.16's std.os.linux.E has no .init()
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(disk_index): route remaining 3 fstat sites through compat.fs.fileSize
Linux Debug build was still failing because DiskIndex.open() had three
more std.c.fstat call sites (for index.tdb/files.tdb/freq.tdb) that
didn't get converted in the first pass. Now all fstat goes through
the portable compat helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: 2 more Codex findings (insert_many handle, runtime init race)
- ffi: turbodb_insert_many was casting the opaque col_handle directly to
*Collection, but callers pass the ColHandle wrapper (magic + ptr).
Route through validateColHandle like the other FFI entrypoints so the
magic is checked and the inner Collection pointer is unwrapped.
- runtime: init/setIo/ensureForTest are now thread-safe via a lockless
CAS state machine (uninit → initing → ready). std.Thread.Mutex is
gone in 0.16 and std.Io.Mutex is off-limits here (it depends on the
very Io we're bootstrapping), so we gate the init with an atomic
cmpxchgStrong + release-store. Losers spin until the winner publishes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: 2 more Codex findings (client.zig runtime init, lsm.zig read-all)
- client: Db.open now calls runtime.init(alloc) before compat.fs.cwdMakeDir
so embedded callers (tests, host apps outside std.process.Init) get a
valid runtime.io before any compat call. Same pattern as ffi.zig.
- lsm: extract readExact helper that loops until the buffer is full or
true EOF; route readU64 / readU32 / readByte through it. A single
readStreaming can legally return short on signal interruption, and
SSTable scans treat short reads as corruption/missed lookups, so
these helpers must preserve full-read semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent c270be0 commit 9bd4dcc
61 files changed
Lines changed: 1751 additions & 1165 deletions
File tree
- .github/workflows
- bench
- python
- src
- registry
- replication
- storage
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Binary file not shown.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
| 42 | + | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | | - | |
| 56 | + | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
| 59 | + | |
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
81 | | - | |
| 81 | + | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
| 84 | + | |
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| |||
Binary file not shown.
Binary file not shown.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
7 | 21 | | |
8 | 22 | | |
9 | 23 | | |
| |||
25 | 39 | | |
26 | 40 | | |
27 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
28 | 49 | | |
29 | 50 | | |
30 | 51 | | |
| |||
36 | 57 | | |
37 | 58 | | |
38 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
39 | 69 | | |
40 | 70 | | |
41 | 71 | | |
| |||
44 | 74 | | |
45 | 75 | | |
46 | 76 | | |
| 77 | + | |
47 | 78 | | |
48 | 79 | | |
49 | 80 | | |
| |||
59 | 90 | | |
60 | 91 | | |
61 | 92 | | |
| 93 | + | |
62 | 94 | | |
63 | 95 | | |
64 | 96 | | |
| |||
79 | 111 | | |
80 | 112 | | |
81 | 113 | | |
| 114 | + | |
82 | 115 | | |
83 | 116 | | |
84 | 117 | | |
| |||
97 | 130 | | |
98 | 131 | | |
99 | 132 | | |
| 133 | + | |
100 | 134 | | |
101 | 135 | | |
102 | 136 | | |
| |||
116 | 150 | | |
117 | 151 | | |
118 | 152 | | |
| 153 | + | |
119 | 154 | | |
120 | 155 | | |
121 | 156 | | |
| |||
131 | 166 | | |
132 | 167 | | |
133 | 168 | | |
| 169 | + | |
134 | 170 | | |
135 | 171 | | |
136 | 172 | | |
| |||
157 | 193 | | |
158 | 194 | | |
159 | 195 | | |
| 196 | + | |
160 | 197 | | |
161 | 198 | | |
162 | 199 | | |
| |||
178 | 215 | | |
179 | 216 | | |
180 | 217 | | |
| 218 | + | |
181 | 219 | | |
182 | 220 | | |
183 | 221 | | |
| |||
199 | 237 | | |
200 | 238 | | |
201 | 239 | | |
| 240 | + | |
202 | 241 | | |
203 | 242 | | |
204 | 243 | | |
| |||
219 | 258 | | |
220 | 259 | | |
221 | 260 | | |
| 261 | + | |
222 | 262 | | |
223 | 263 | | |
224 | 264 | | |
| |||
239 | 279 | | |
240 | 280 | | |
241 | 281 | | |
| 282 | + | |
242 | 283 | | |
243 | 284 | | |
244 | 285 | | |
| |||
259 | 300 | | |
260 | 301 | | |
261 | 302 | | |
| 303 | + | |
262 | 304 | | |
263 | 305 | | |
264 | 306 | | |
| |||
274 | 316 | | |
275 | 317 | | |
276 | 318 | | |
277 | | - | |
| 319 | + | |
278 | 320 | | |
279 | | - | |
| 321 | + | |
280 | 322 | | |
281 | 323 | | |
282 | 324 | | |
| |||
286 | 328 | | |
287 | 329 | | |
288 | 330 | | |
| 331 | + | |
| 332 | + | |
289 | 333 | | |
290 | 334 | | |
291 | 335 | | |
| |||
341 | 385 | | |
342 | 386 | | |
343 | 387 | | |
344 | | - | |
| 388 | + | |
345 | 389 | | |
346 | 390 | | |
347 | 391 | | |
348 | 392 | | |
349 | 393 | | |
350 | | - | |
| 394 | + | |
351 | 395 | | |
352 | 396 | | |
353 | 397 | | |
354 | 398 | | |
355 | | - | |
| 399 | + | |
356 | 400 | | |
357 | 401 | | |
358 | 402 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
Binary file not shown.
Binary file not shown.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
16 | | - | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
25 | | - | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
46 | | - | |
| 47 | + | |
47 | 48 | | |
48 | 49 | | |
49 | 50 | | |
| |||
55 | 56 | | |
56 | 57 | | |
57 | 58 | | |
58 | | - | |
| 59 | + | |
59 | 60 | | |
60 | 61 | | |
61 | | - | |
| 62 | + | |
62 | 63 | | |
63 | 64 | | |
64 | | - | |
| 65 | + | |
65 | 66 | | |
66 | 67 | | |
67 | 68 | | |
| |||
0 commit comments