feat(windows): foundation — build system, stack safety, heap allocation#170
feat(windows): foundation — build system, stack safety, heap allocation#170JF10R wants to merge 4 commits intojustrach:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b9be3f687d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| var root_buf: [std.fs.max_path_bytes]u8 = undefined; | ||
| var root_buf: [compat.path_buf_size]u8 = undefined; |
There was a problem hiding this comment.
Use full path buffer when resolving repository root
On Windows, compat.path_buf_size is now 1024, and mainImpl uses that size for root_buf before calling resolveRoot/realpath. This means any valid project root whose absolute path exceeds 1024 bytes will now fail startup with cannot resolve root, whereas the previous std.fs.max_path_bytes path supported long-path Windows setups. This is a functional regression for deep workspace paths; keep the reduced buffer only for watcher event storage and use std.fs.max_path_bytes (or a dynamic buffer) for root resolution.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR improves Windows support by reducing stack pressure and adjusting allocation strategies in core components of codedb, aiming to avoid stack overflows caused by large stack frames and large inline buffers on Windows.
Changes:
- Introduce
compat.path_buf_size(1024 on Windows) and use it for path buffers to avoid hugestd.fs.max_path_bytesstack allocations. - Increase Windows executable stack size to 8MB and switch the CLI entrypoint to a
noinlineimplementation to reduce large merged stack frames. - Heap-allocate large structures (watcher
EventQueuestorage and index frequency table buffers) and update tests for the newEventQueue.init()/deinit()API.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/watcher.zig |
Switch FsEvent path buffer sizing to compat.path_buf_size; heap-allocate the queue backing array via EventQueue.init()/deinit(). |
src/tests.zig |
Update watcher queue tests to use EventQueue.init()/deinit(). |
src/main.zig |
Replace thread trampoline with noinline mainImpl(), use compat.path_buf_size for root resolution buffers, switch to stack EventQueue + init/deinit, and remove duplicate thread join. |
src/index.zig |
Heap-allocate frequency table count/output buffers and refactor finishFrequencyTable to fill an output buffer. |
src/compat.zig |
Add path_buf_size with a smaller Windows-specific value to prevent oversized path buffers. |
build.zig |
Set Windows executable stack size to 8MB. |
| var shutdown = std.atomic.Value(bool).init(false); | ||
| defer shutdown.store(true, .release); | ||
| var scan_already_done = std.atomic.Value(bool).init(true); | ||
|
|
||
| const queue = try allocator.create(watcher.EventQueue); | ||
| defer allocator.destroy(queue); | ||
| queue.* = watcher.EventQueue{}; | ||
| const watch_thread = try std.Thread.spawn(.{}, watcher.incrementalLoop, .{ &store, &explorer, queue, root, &shutdown, &scan_already_done }); | ||
| var queue = try watcher.EventQueue.init(); |
There was a problem hiding this comment.
In the serve command, defer shutdown.store(true, .release); is registered before the defer ...join() calls. Because defers run LIFO, the joins execute before shutdown is set, so watch_thread.join() / reap_thread.join() can block forever if server.serve() ever returns (e.g., accept/spawn error). Consider setting shutdown before joining (e.g., a single defer { shutdown.store(...); reap_thread.join(); watch_thread.join(); } block, or move the shutdown defer below the join defers).
| events: *[CAPACITY]?FsEvent, | ||
| head: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), | ||
| tail: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), | ||
| mu: std.Thread.Mutex = .{}, | ||
|
|
||
| pub fn init() !EventQueue { | ||
| const events = try std.heap.page_allocator.create([CAPACITY]?FsEvent); | ||
| events.* = [_]?FsEvent{null} ** CAPACITY; | ||
| return .{ .events = events }; | ||
| } | ||
|
|
||
| pub fn deinit(self: *EventQueue) void { | ||
| std.heap.page_allocator.destroy(self.events); |
There was a problem hiding this comment.
EventQueue.init() hard-codes std.heap.page_allocator, which makes the type harder to reuse and test (callers can’t choose an allocator, and it bypasses the existing GPA/testing allocator patterns used elsewhere). Consider accepting an allocator: std.mem.Allocator in init/deinit (and storing it in the struct) so production and tests can use the same allocation strategy.
| events: *[CAPACITY]?FsEvent, | |
| head: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), | |
| tail: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), | |
| mu: std.Thread.Mutex = .{}, | |
| pub fn init() !EventQueue { | |
| const events = try std.heap.page_allocator.create([CAPACITY]?FsEvent); | |
| events.* = [_]?FsEvent{null} ** CAPACITY; | |
| return .{ .events = events }; | |
| } | |
| pub fn deinit(self: *EventQueue) void { | |
| std.heap.page_allocator.destroy(self.events); | |
| allocator: std.mem.Allocator, | |
| events: *[CAPACITY]?FsEvent, | |
| head: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), | |
| tail: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), | |
| mu: std.Thread.Mutex = .{}, | |
| pub fn init(allocator: std.mem.Allocator) !EventQueue { | |
| const events = try allocator.create([CAPACITY]?FsEvent); | |
| events.* = [_]?FsEvent{null} ** CAPACITY; | |
| return .{ | |
| .allocator = allocator, | |
| .events = events, | |
| }; | |
| } | |
| pub fn deinit(self: *EventQueue) void { | |
| self.allocator.destroy(self.events); |
| pub fn buildFrequencyTableFromSlices(slices: []const []const u8) [256][256]u16 { | ||
| var counts: [256][256]u64 = .{.{0} ** 256} ** 256; | ||
| const counts = std.heap.page_allocator.create([256][256]u64) catch return default_pair_freq; | ||
| defer std.heap.page_allocator.destroy(counts); | ||
| const out = std.heap.page_allocator.create([256][256]u16) catch return default_pair_freq; | ||
| defer std.heap.page_allocator.destroy(out); |
There was a problem hiding this comment.
The docstring says “Zero extra memory”, but this function now performs heap allocations (page_allocator.create for both counts and out). Either adjust the comment to reflect the new allocation behavior, or consider an approach that avoids heap allocation on non-Windows targets if the intent is to remain allocation-free.
b9be3f6 to
143c890
Compare
Addressing reviewer feedback✅ Fixed: stale docstring in
|
|
Hey @JF10R — thanks for the Windows work! Before we can review and merge, could you please:
We'll get Codex to do a review pass once rebased. Thanks! |
justrach
left a comment
There was a problem hiding this comment.
Requesting rebase onto current main and CONTRIBUTING.md compliance before review. See comment above.
Add Windows platform support foundations that all subsequent Windows PRs depend on. No behavioral change on Linux/macOS. - compat.zig: add path_buf_size (1024 on Windows vs max_path_bytes) - build.zig: set 8MB stack for Windows (matches Linux default) - main.zig: replace thread trampoline with noinline mainImpl to avoid LLVM frame merging (~128MB) that exceeds Windows guard page stride - main.zig: fix double scan_thread.join() (UB on all platforms) - watcher.zig: heap-allocate EventQueue (avoids ~4MB stack array) - watcher.zig: use compat.path_buf_size in FsEvent - index.zig: heap-allocate frequency table buffers (~640KB each) - tests.zig: update EventQueue construction to use init()/deinit()
Upstream issue-148 added stdin HUP detection via std.posix.poll() and pipe tests. These use POSIX APIs unavailable on Windows. Guard the poll block with comptime os check (Windows uses idle timeout only) and skip pipe tests on Windows.
Upstream's mmap trigram index (justrach#167) uses std.posix.mmap/munmap which are unavailable on Windows. Guard initFromDisk to return null on Windows (falling back to heap-based index) and skip mmap-dependent tests.
cb8b339 to
e869707
Compare
|
Rebased onto current
New since last review: added comptime guards for upstream mmap trigram index (#167) —
|
Closes #181
Summary
Adds Windows build support by fixing stack overflow, heap allocation, and path buffer issues that prevent codedb from compiling and running on Windows.
Changes
build.zigexe.stack_size = 8MBfor Windows targets (Linux default is 8MB, Windows default is 1MB)src/compat.zigpath_buf_sizeconstant (1024 on Windows,max_path_byteselsewhere)src/main.zignoinline fn mainImpltrampoline to prevent LLVM frame merging; fix doublescan_thread.join()(UB on all platforms); guard POSIX-onlypoll()in idle watchdogsrc/watcher.zigEventQueueevents array viapage_allocator(4MB inline would blow stack); usepath_buf_sizeforFsEvent.path_bufsrc/index.zigcounts(512KB) andout(128KB) buffers; guard POSIX-onlymmapinMmapTrigramIndexsrc/tests.zigEventQueuetests to useinit()/deinit()API; skip POSIX-only mmap and pipe tests on WindowsTests
The 2 failures (
issue-150: --help/--h) are pre-existing upstream test issues on Windows (child process execution). The 4 skips are POSIX-only tests (mmap, pipe) correctly guarded withSkipZigTest.Before/After
zig build -Dtarget=x86_64-windowsfails —STATUS_STACK_OVERFLOWat runtime due to LLVM merging all command branch frames into one ~128MB frame exceeding the 1MB Windows default stack.EventQueueputs 4MB on stack. Frequency table buffers put 640KB on stack.Compliance
main(d7e3bfb)comptimechecks)