summaryrefslogtreecommitdiff
path: root/src/limits.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/limits.zig')
-rw-r--r--src/limits.zig212
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);
+}