diff options
Diffstat (limited to 'src/limits.zig')
| -rw-r--r-- | src/limits.zig | 212 |
1 files changed, 212 insertions, 0 deletions
diff --git a/src/limits.zig b/src/limits.zig new file mode 100644 index 00000000..965f4d0a --- /dev/null +++ b/src/limits.zig @@ -0,0 +1,212 @@ +//! Every board-shaped capacity in one table. +//! +//! These numbers used to be nine `platform == .esp32p4` tests scattered across +//! nine files, each one a separate place to forget. They are not nine +//! decisions: they are ONE decision — how much memory this build is allowed to +//! spend — taken nine times, in nine files, where no reader could see the +//! total. Here the whole budget is on one screen and every cap says what it is +//! measured against. +//! +//! Two booleans derive all of it, and nothing outside this file tests the +//! platform for a capacity again. +//! +//! WHAT DOES NOT BELONG HERE: capability switches. `terminal_panes`, +//! `board_memory.enabled`, `hosted`, `font_picker` and the rest answer "does +//! this build have the thing at all", which is a question about the platform +//! and not about a budget — they stay next to the thing they gate. That +//! division is also why a build option selecting the board's budget on a +//! desktop does not work; the note on `board` below records the attempt. +const std = @import("std"); +const builtin = @import("builtin"); +const config = @import("pardes_config"); + +/// `board` is the ESP32-P4 firmware's budget: a 384 KiB heap and a 240 KiB +/// chunk of L2MEM shared between `.bss`, `.data` and the stack. `reduced` is +/// any freestanding target with no OS under it — the browser's wasm linear +/// memory grown on demand, megabytes rather than tens, but not a desktop's +/// address space. +/// +/// TWO BOOLEANS AND NOT A PROFILE ENUM, and a build option was tried and +/// removed. `-Dmem-profile=board` was meant to let a native test runner +/// compile the board's capacities and boot the core under them; it does not +/// work, and cannot. The dominant term in a boot is `@sizeOf(Pane)`, which +/// carries the ghostty-vt Terminal — 1.1 MiB of it — and what removes that is +/// `pardes.terminal_panes`, a CAPABILITY keyed on the platform rather than a +/// capacity in this table. So the option shrank the rings and left the boot +/// six times over budget, producing a configuration nothing was designed for: +/// `zig build unit-test -Dmem-profile=board` deadlocked in a futex rather than +/// failing, because a hosted build with the board's effect ring silently drops +/// effects a hosted test is waiting on. +/// +/// What DOES test the board's memory pressure natively is in pardes.zig: the +/// grid-scaled cost and the allocation-failure sweep, both of which are +/// platform-independent and run on the ordinary build. See the comment block +/// above `board_heap_bytes` there. +const board = config.platform == .esp32p4; +/// No OS means no address space to reserve megabytes out of, whatever the +/// platform is called. `.web` is wasm32-freestanding and `.esp32p4` is +/// riscv32-freestanding, so the target answers this for both. +const reduced_target = builtin.os.tag == .freestanding; +const KiB = 1024; +const MiB = 1024 * KiB; + +/// THE NUMBER EVERY OTHER NUMBER HERE IS MEASURED AGAINST: the board's whole +/// heap, the 384 KiB chunk of L2MEM at 0x4FF40000 (`05-zig-p4`'s linker script +/// owns the split; the 128 KiB above it measured as L2 cache rather than +/// memory). Unconditional and not profile-derived, because it is a fact about +/// the silicon rather than a budget this build chose — a desktop build that +/// wants to know what the board affords is asking exactly this question, which +/// is what the memory tests in pardes.zig do with it. +pub const board_heap_bytes = 384 * KiB; + +/// How many effects the ring holds. SHRUNK, not moved to the heap, on the +/// board: `pump` drains this to empty on every iteration with an +/// unconditional `while (nextEffect())` — including effects `perform` itself +/// queues — so no capacity can deadlock the drain, and the only question a +/// capacity answers is how big a single-pump BURST may be before `emit` +/// refuses the overflow. The one producer that can burst is `emitWrite`, +/// which chunks arbitrary bytes into 64-byte `.write` effects for a pty, and +/// a build with `terminal_panes == false` has no pty to write to. Everything +/// else queues O(1) effects per event, and `in_q` holds at most 64 events per +/// pump, so 128 leaves two effects per queued event. +/// +/// A 1.0625 MiB inline ring cannot live in the board's 384 KiB heap at all; +/// 128 entries is 34 KiB. NOTE THE BEHAVIOUR CHANGE: `emit` has always +/// refused (not evicted) once full, so on the board a burst larger than 128 effects +/// now drops its tail where 4096 would have held it — reachable only through +/// `emitWrite`, i.e. only if a pty ever appears on this platform. +pub const effect_cap = if (board) 128 else 4096; + +/// Rows the per-pane soft-wrap map covers. `wrapWidth` refuses to wrap a pane +/// taller than this (it reads the array's own length), so shrinking it cannot +/// truncate a map — a taller pane renders unwrapped, exactly as documented on +/// `Pane.wrap_line`. A serial console is not 128 rows tall. +pub const wrap_rows = if (board) 128 else 256; + +/// A shell's reported working directory, owned inline by the pane. Zero-sized +/// where there are no processes to report one: the `PdfSlot` rule, applied to +/// a capacity whose sole producer (`Pardes.setCwd`, fed by a pty's prompt +/// report) does not exist without terminal panes. `setOwnedCwd` clamps, so a +/// zero cap reads as "no directory known" — which is the truth here. +/// +/// Keyed on the profile rather than on `pardes.terminal_panes`, which this +/// file must not import (the core imports the table, not the other way round). +/// The two agree by construction: the board is the only build with no ptys. +pub const cwd_buf_cap = if (board) 0 else 1024; + +/// EDIT BOUNDARIES REMEMBERED PER FILE PANE. Every entry owns a gpa copy of +/// the WHOLE file, so this number multiplies heap, not just the pane: 256 of +/// them is not a bound a 384 KiB board could ever reach anyway. `pushHistory` +/// evicts and frees the oldest once full, so the smaller ring loses the +/// deepest undo steps and nothing else — no truncation, no dropped edit. +pub const undo_max = if (board) 16 else 256; + +/// Bounds the only user-editable, schema-owned tag fragment. It IS the storage +/// bound: `Pane.tag_tail` is `[max_tag_tail]u8`, and every writer (appendTag, +/// tagInsert, restoreDumpTail, the acmefs `tag` file) refuses input that does +/// not fit rather than truncating it, so the schema limit and the buffer can +/// never disagree — a dump reader can reject data before copying it into a +/// pane. +/// +/// 512 on the P4 firmware. A tag is ONE line — a pane's path plus its command +/// words — and 4 KiB of it is 4 KiB per pane out of a 384 KiB heap. A serial +/// console is 80 columns; 512 is six of those. +pub const max_tag_tail: usize = if (board) 512 else 4096; + +/// HOW LONG A HOST-SUPPLIED ABSOLUTE PATH MAY BE, and the only reason that +/// record was ever kilobytes: the shell a native host resolved, the font file +/// a native picker returned, and (in pardes.zig) the one watched theme file. +/// All three name something on a FILESYSTEM, and all three are retained +/// inline because the core has no allocator at the point they arrive. +/// +/// Fixed at 4095 wherever a filesystem exists — deliberately NOT derived from +/// std.fs PATH_MAX, which web has no answer for, and 4095 rather than 4096 so +/// the macOS C bridge's NUL fits without a second, subtly different limit at +/// that boundary. Zero on the P4 firmware, which has no filesystem, no +/// processes to spawn a shell for and no font picker: `Text(0)` is a +/// zero-sized field whose `set` refuses every non-empty path, so the three +/// producers report failure instead of storing 12 KiB nothing can fill. +pub const host_path_cap: usize = if (board) 0 else 4095; + +/// WHETHER PARDES'S OWN SOURCE IS EMBEDDED — the source_manifest allowlist, +/// which is a capacity spelled as rodata rather than as a number. +/// +/// ON THE P4 the allowlist is EMPTY, and that is the whole difference: the +/// table is ~0.95 MiB of rodata against a 1.5 MiB flash partition, and the +/// firmware's filesystem is the serial host's, reached through the Host +/// vtable. The API is unchanged — `all` is a zero-length array and `find` +/// answers null — so every caller compiles identically and simply finds +/// nothing embedded. +pub const embedded_sources = !board; + +/// Bytes per dumped row, and it is a different number on the board. +/// +/// `hexdump -C`'s sixteen is the layout everyone can already read, and it needs 79 columns: ten for +/// the address, forty-eight for the hex, a gap, and the eighteen-column ASCII gutter. The P4 drives +/// a 56-column grid of which seven go to the line-number gutter, so a sixteen-byte row wraps onto a +/// second display line and the columns stop lining up - which is the entire value of the layout. +/// +/// Eight fits in 46 and keeps every property that matters: address on the left, fixed-width hex +/// columns, ASCII on the right, and a gap at the halfway mark because the eye counts in fours and +/// eights rather than in sixteens. +/// +/// NO `0x` ON WHAT THESE WORDS PRINT, which is where two of those columns came from. It reads no +/// worse - every number here is hex, there is no other kind, and the words refuse a decimal one - and +/// it buys something better than the width: an address in a dump can now be typed straight back into +/// a `Peek` without editing it, because bare hex is exactly what the parser wants. Output that is +/// valid input is worth more than a prefix restating what the whole file already says. +pub const hexdump_row_bytes: u32 = if (board) 8 else 16; + +/// Three tiers, because the address space differs by four orders of magnitude. +/// `reduced_target` is the browser: a wasm linear memory it grows on demand, so +/// the static reservations are megabytes rather than tens. +/// +/// `board` is ESP32-P4 firmware, and its tier is deliberately ALL FALLBACK. Every +/// capacity here is a `StackFallbackAllocator`'s buffer, which is a static and +/// therefore lands in `.bss` — and on the P4 `.bss`, `.data` and the stack all +/// share ONE 240 KiB chunk of L2MEM at 0x4FF03000, while the heap the fallback +/// allocator hands out is the separate 384 KiB chunk at 0x4FF40000 - the 128 KiB +/// above that measured as L2 cache rather than memory. A +/// megabyte-shaped reservation here would not fit, and every byte that did fit +/// would be taken from the stack's neighbourhood to duplicate memory the heap +/// already has. So the buffers exist only because the type requires one: 4 KiB +/// absorbs the small churn, and everything else spills to the real heap on the +/// first allocation. +pub const arena = struct { + pub const pardes = if (board) 4 * KiB else if (reduced_target) 8 * MiB else 32 * MiB; + pub const frame = if (board) 4 * KiB else if (reduced_target) 4 * MiB else 16 * MiB; + // Zero is legal and always spills, which is exactly what an arena for a + // compiled-out subsystem should do. `StackFallbackAllocator(0).buffer` is + // `[0]u8`; `get()` inits the FixedBufferAllocator over an empty slice, so + // `FixedBufferAllocator.alloc` fails every nonzero request and `alloc` + // falls through to `self.fallback_allocator.rawAlloc`, while `ownsPtr` over + // an empty range is false for every pointer so `resize`/`remap`/`free` + // route to the fallback too. See lib/std/heap.zig, StackFallbackAllocator. + pub const tree_sitter = if (board) 0 else if (reduced_target) 4 * MiB else 16 * MiB; + pub const image = if (board) 0 else if (reduced_target) 64 * KiB else 32 * MiB; + pub const pdf = if (board) 0 else if (reduced_target or !config.mupdf) 64 * KiB else 64 * MiB; +}; + +// THE REGRESSION GUARD for the refactor that created this file: nine caps +// moved out of nine files, and the one thing that must not have changed is +// what a tty/gui/macos build gets. Spelling the historical desktop numbers +// here as literals is the point — a derivation would agree with itself. +test "board limits: a desktop build keeps exactly its historical capacities" { + if (board or reduced_target) return error.SkipZigTest; + try std.testing.expectEqual(4096, effect_cap); + try std.testing.expectEqual(256, wrap_rows); + try std.testing.expectEqual(1024, cwd_buf_cap); + try std.testing.expectEqual(256, undo_max); + try std.testing.expectEqual(@as(usize, 4096), max_tag_tail); + try std.testing.expectEqual(@as(usize, 4095), host_path_cap); + try std.testing.expect(embedded_sources); + try std.testing.expectEqual(@as(u32, 16), hexdump_row_bytes); + // The arena tier a desktop gets is the third one, so it is only the + // historical desktop tier when the target is not itself reduced. + if (reduced_target) return; + try std.testing.expectEqual(32 * MiB, arena.pardes); + try std.testing.expectEqual(16 * MiB, arena.frame); + try std.testing.expectEqual(16 * MiB, arena.tree_sitter); + try std.testing.expectEqual(32 * MiB, arena.image); + try std.testing.expectEqual(if (config.mupdf) 64 * MiB else 64 * KiB, arena.pdf); +} |
