Commit Graph

60 Commits

Author SHA1 Message Date
Mitchell Hashimoto
461562ca4f terminal: enable idle scrollback compression
Scrollback compression was available only through explicit PageList
calls, so normal renderer-backed surfaces never reclaimed cold history.

Debounce renderer wakeups with a one-shot timer and run bounded
compression steps only when the terminal lock is immediately available.
Timer state is isolated in the renderer and becomes idle once PageList
reports that the pass is complete.

Keep inspector reads representation-preserving by decoding compressed
pages into temporary copies. Update the scrollback configuration and
benchmark documentation to describe the production behavior and its
memory accounting.
2026-07-09 09:47:13 -07:00
Mitchell Hashimoto
e7969ed436 terminal: make PageList compression self-contained
Incremental compression previously exposed its traversal state to
callers, requiring them to coordinate the cursor with PageList lifetime
and topology changes. Read-only consumers also had to restore compressed
nodes to inspect their contents.

Move the continuation state into PageList and expose a single mode-based
compression entry point. Incremental passes restart safely across
mutations and verify a no-work pass before becoming idle, while full
passes leave incremental state fresh.

Add preserved page reads which decode compressed nodes into caller-owned
storage without changing their representation, and migrate the
scrollback compression benchmark to the new API.
2026-07-09 09:47:12 -07:00
Mitchell Hashimoto
70e788f066 terminal: add incremental scrollback compression
Cold-history compression previously required scanning every eligible
page in one call, which makes it unsuitable for an idle-time scheduler.
The inspector also restored compressed pages while traversing collapsed
entries, hiding the representation and undoing reclamation.

Add caller-owned serial state that resumes compression without retaining
node pointers. Each invocation inspects at most eight candidates and
attempts one resident page. List mutations restart safely, while
unsupported or historical viewports stop work. Keep a stateless
whole-history operation for measurement.

Expose metadata-only storage and memory accounting for diagnostics,
update the inspector to restore only expanded pages, and add an
incremental live scrollback benchmark. This remains disconnected from
production scheduling.
2026-07-09 09:47:12 -07:00
Mitchell Hashimoto
9156ada169 terminal: add cold-history compression pass
PageList could compress individual nodes but had no policy-level
operation for selecting pages that are safe to reclaim. The compressed
state therefore remained reachable only from tests.

Add a stateless pass that considers only complete history pages while
the viewport follows the active area. It gates work on supported
retained-mapping reclamation, reports attempts and retained bytes, and
leaves restoration lazy when a resize pulls compressed history back into
the active area.

Add a live scrollback-compression benchmark for measuring complete
PageList compression and restoration against saved VT corpora. The pass
still has no production callers, and ReleaseFast terminal-stream
comparisons remain within the existing throughput guardrail.
2026-07-09 09:47:12 -07:00
Mitchell Hashimoto
ebc3ffd222 terminal: add compressed page representation
The standalone LZ4 codec had no representation for terminal page
ownership or metadata, so PageList integration would otherwise need to
reconstruct every Page field independently.

Add compress.Page, which embeds the complete terminal Page while
retaining its original virtual mapping and owns only an exact-sized
encoded block. Compression is kept only when the encoded state is
strictly smaller, and scratch output is capped at that profitability
boundary so a future PageList caller can borrow a standard pool item.

Extend the page-compression benchmark with a store mode that measures
the encoded copy, allocation, bounded retention, and eviction path.
Nothing uses compression from PageList yet; this remains isolated
groundwork.
2026-07-09 09:47:11 -07:00
Mitchell Hashimoto
c62c159841 terminal: add standalone LZ4 block codec
Scrollback compression needs a codec that can be used from libghostty-vt
without pulling in libc, and we need to measure it before integrating it
with terminal page ownership.

This adds an allocation-free raw LZ4 block codec in scalar Zig.  Callers
provide the input, output, and fixed-size scratch table. The decoder
uses an exact-size output contract so page metadata mismatches fail
cleanly. Compatibility vectors, boundary cases, random round trips, and
fuzz coverage exercise the block format.

Also adds a page-compression benchmark that operates on reusable raw
page corpora. Compression and decompression have separate modes with
setup outside the timed region, plus a ratio report and no-op baseline.
Nothing uses compression in the terminal yet; this is the isolated codec
and measurement groundwork.
2026-07-09 09:47:11 -07:00
Mitchell Hashimoto
446f80f4ed terminal: render state update optimizations (~2.7x to ~11x less lock hold)
This optimizes `RenderState.update`, the per-frame call that snapshots
terminal state for the renderer and is the main reason the renderer
thread holds the terminal lock. 

Lock hold time is reduced ~2.7x to ~11x depending on the frame.

## The changes

1. iterate page chunks instead of rows in `update`
2. classify cells with masked vector compares. 
3. split the update into `beginUpdate`/`endUpdate` phases. There's a 
   lot to be gained by accumulating data with the lock held and then
   processing it out of the lock.
4. generalize the masked-compare scans into `page.Mask`. This is just
   a really common pattern we're doing now and it yields a ton of great
   value. Its error prone so lets make it a tested helper.

## Benchmarks

Measured with the new `ghostty-bench +screen-clone` modes (`render`,
`render-locked`, `render-clean`, `render-partial`), 120x80 terminal, M4
Max, macOS 26, ReleaseFast, hyperfine means of 10+ runs, per-update
times derived from fixed-count update loops with process startup
subtracted. "Lock held" is the time the terminal lock must be held per
update; "before" held the lock for the entire update.

| scenario | before (lock held) | after (lock held) | after (total) | lock change |
|----------|--------------------|-------------------|---------------|-------------|
| clean frame (nothing dirty) | 202 ns | 19 ns | 19 ns | 10.9x |
| partial frame (1 dirty row) | 290 ns | 54 ns | 54 ns | 5.4x |
| full rebuild, lightly styled | 6.9 µs | 2.5 µs | 3.0 µs | 2.7x |
| full rebuild, fully styled | 9.3 µs | 2.4 µs | 8.0 µs | 3.8x |
| full rebuild, fully styled, 250x150 | 49.9 µs | 9.4 µs | 31.6 µs | 5.3x |
| full rebuild, plain text | 1.9 µs | 1.9 µs | 1.9 µs | 1.0x (memcpy floor) |

The clean and partial cases are the steady-state frame costs (cursor
blink, mouse movement, typing). The full-rebuild cases are the contended
ones: colored scrolling output (build logs, htop, vim) moves the
viewport pin every frame, forcing a full rebuild exactly when the IO
thread is busiest, so that row of the table is where lock contention
actually hurts. Plain text was already at the memcpy floor and is
unchanged.

## LLM Notes

This work was driven by Fable 5: benchmarks, optimizations, the property
test, and the measurements above. I reviewed every line, simplified the
design in a few places (API naming, the Mask helper shape), and re-ran
the verifications myself.
2026-07-06 19:57:04 -07:00
Mitchell Hashimoto
4c5b1d5d52 bench: terminal-stream reads 64KiB chunks to match the IO thread
The terminal-stream benchmark fed the stream in 4KiB chunks while
the real IO thread reads from the pty into 64KiB buffers (see
buffer_capacity in termio/Exec.zig) and hands those to the stream
whole. Chunk size affects measurement in two ways: it determines
how often the stream crosses a chunk boundary (partial UTF-8
sequences, escape sequences split mid-parse) and how many read
syscalls the harness itself performs (a 2.6 GB corpus is ~636k
pread calls at 4KiB versus ~40k at 64KiB).

This bumps the benchmark read and dispatch buffers to 64KiB so the
stream is exercised with realistic chunk sizes. Measured with
ghostty-bench terminal-stream on a 2.6 GB recording of real
terminal sessions (120x80, M4 Max, ReleaseFast, hyperfine means):

| harness      | time            | throughput |
|--------------|-----------------|------------|
| 4KiB chunks  | 9.651 s ± 0.013 | 270 MB/s   |
| 64KiB chunks | 9.582 s ± 0.101 | 272 MB/s   |

The stream itself is barely chunk-size sensitive (most time is in
parsing and terminal state updates), but the harness now matches
what the IO thread actually does, and later commits are measured
against this configuration.
2026-07-06 12:25:20 -07:00
Mitchell Hashimoto
258de36d15 benchmark: terminal-stream uses the full terminal handler
The terminal-stream benchmark previously used a simplified handler
that handles print actions and drops everything else. That was
originally intended to isolate parse and print throughput, but it
understates the cost of escape-heavy streams: no terminal state is
updated for CSI/OSC/ESC sequences, and because actions are
dispatched at comptime, the unhandled action arms are eliminated
entirely, so the benchmark measures dispatch code that doesn't
exist in the real app.

This switches the benchmark to the full readonly terminal stream
handler (terminal.TerminalStream). Every escape sequence now
updates real terminal state (styles, cursor movement, erases,
modes, etc.), closely mirroring the work the real IO thread does
per byte. This is the handler used to measure the VT throughput
changes in the following commits.

Parser-in-isolation measurement remains covered by the separate
terminal-parser and osc-parser benchmarks, and print throughput is
identical under both handlers since printing flows into the same
Terminal call either way.
2026-07-06 06:44:04 -07:00
Mitchell Hashimoto
bf3047b9b2 benchmark: isolate parser hot loop from code-layout shifts
Extract the tight per-byte parsing loop from TerminalParser.step into
a separate noinline function (parseAll). This eliminates a ~20%
benchmark regression that appeared after the highway vendor changes
despite zero changes to the parser source code.

The root cause: the parser benchmark processes 50 MB of input through
a byte-at-a-time DFA loop that is highly sensitive to instruction
cache-line placement on Apple Silicon. The M-series cores fetch
aligned 16-byte blocks; when the loop head lands near the end of a
64-byte cache line (offset 60), only one instruction fits in the
first fetch versus four when aligned to offset 48. This causes ~29%
more cycles for identical instruction counts.

Previously the loop was inlined into the large step() function, so
any code change anywhere in the binary (like the highway vendor
restructuring) could shift the loop across a cache-line boundary.
By making parseAll noinline, the loop gets its own function placement
that is stable regardless of surrounding code changes.
2026-04-23 21:36:39 -07:00
Mitchell Hashimoto
e51de8b58f libghostty: Remove all libc++ and libc++ ABI dependencies
This updates simdutf to my fork which has a SIMDUTF_NO_LIBCXX option
that removes all libc++ and libc++ ABI dependencies. 

From there, the hand-written simd code we have has been updated to also
no longer use any libc++ features. Part of this required removing utfcpp
since it depended on libc++ (`<iterator>`).

libghostty-vt now only depends on libc.
2026-04-15 10:27:05 -07:00
Mitchell Hashimoto
9c49c34356 benchmark: add AGENTS, improve UTF-8 synthetic data
This updates our synthetic generator for UTF-8 to expose:

  - Flags to change 1/2/3/4-byte UTF-8 character distribution
  - Flags to have only printable characters so we can benchmark
    pure UTF-8 vs our control sequence finder.
  - Flags to have invalid characters so we can benchmark our error
    handling.

This also adds an AGENTS.md to src/benchmark so agents can do the right
thing more easily.
2026-04-15 08:28:36 -07:00
Jeffrey C. Ollie
60c7e767a8 benchmark: disable test on windows
We don't appear to have a time source with enough resolution to get a
non-zero duration on the benchmark test so it fails.
2026-03-28 10:20:51 -05:00
Mitchell Hashimoto
2044e5030f terminal: make stream processing infallible
The terminal.Stream next/nextSlice functions can now no longer fail.
All prior failure modes were fully isolated in the handler `vt`
callbacks. As such, vt callbacks are now required to not return an error
and handle their own errors somehow.

Allowing streams to be fallible before was an incorrect design. It
caused problematic scenarios like in `nextSlice` early terminating
processing due to handler errors. This should not be possible.

There is no safe way to bubble up vt errors through the stream because
if nextSlice is called and multiple errors are returned, we can't
coalesce them. We could modify that to return a partial result but its
just more work for stream that is unnecessary. The handler can do all of
this.

This work was discovered due to cleanups to prepare for more C APIs.
Less errors make C APIs easier to implement! And, it helps clean up our
Zig, too.
2026-03-13 13:56:14 -07:00
Jeffrey C. Ollie
e8aad10326 windows: avoid the use of wcwidth 2026-03-05 09:26:52 -06:00
Mitchell Hashimoto
49b2b8d644 unicode: switch to uucode grapheme break to (mostly) match unicode spec (#9680)
This PR builds on https://github.com/ghostty-org/ghostty/pull/9678 ~so
the diff from there is included here (it's not possible to stack PRs
unless it's a PR against my own fork)--review that one first!~

This PR updates the `graphemeBreak` calculation to use `uucode`'s
`computeGraphemeBreakNoControl`, which has [tests in
uucode](215ff09730/src/x/grapheme.zig (L753))
that confirm it passes the `GraphemeBreakTest.txt` (minus some
exceptions).

Note that the `grapheme_break` (and `grapheme_break_no_control`)
property in `uucode` incorporates `emoji_modifier` and
`emoji_modifier_base`, diverging from UAX #29 but matching UTS #51. See
[this comment in
uucode](215ff09730/src/grapheme.zig (L420-L434))
for details.

The `grapheme_break_no_control` property and
`computeGraphemeBreakNoControl` both assume `control`, `cr`, and `lf`
have been filtered out, matching the current grapheme break logic in
Ghostty.

This PR keeps the `Precompute.data` logic mostly equivalent, since the
`uucode` `precomputedGraphemeBreak` lacks benchmarks in the `uucode`
repository (it was benchmarked in [the original PR adding `uucode` to
Ghostty](https://github.com/ghostty-org/ghostty/pull/8757)). Note
however, that due to `grapheme_break` being one bit larger than
`grapheme_boundary_class` and the new `BreakState` also being one bit
larger, the state jumps up by a factor of 8 (u10 -> u13), to 8KB.

## Benchmarks

~I benchmarked the old `main` version versus this PR for
`+grapheme-break` and surprisingly this PR is 2% faster (?). Looking at
the assembly though, I'm thinking something else might be causing that.
Once I get to the bottom of that I'll remove the below TODO and include
the benchmark results here.~

When seeing the speedup with `data.txt` and maybe a tiny speedup on
English wiki, I was surprised given the 1KB -> 8KB tables. Here's what
AI said when I asked it to inspect the assembly:
https://ampcode.com/threads/T-979b1743-19e7-47c9-8074-9778b4b2a61e, and
here's what it said when I asked it to predict the faster version:
https://ampcode.com/threads/T-3291dcd3-7a21-4d24-a192-7b3f6e18cd31

It looks like two loads got reordered and that put the load that
depended on stage1 -> stage2 -> stage3 second, "hiding memory latency".
So that makes the new one faster when looking up the `grapheme_break`
property. These gains go away with the Japanese and Arabic benchmarks,
which spend more time processing utf8, and may even have more grapheme
clusters too.

### with data.txt (200 MB ghostty-gen random utf8)

<img width="1822" height="464" alt="CleanShot 2025-11-26 at 08 42 03@2x"
src="https://github.com/user-attachments/assets/56d4ee98-21db-4eab-93ab-a0463a653883"
/>

### with English wiki dump

<img width="2012" height="506" alt="CleanShot 2025-11-26 at 08 43 15@2x"
src="https://github.com/user-attachments/assets/230fbfb7-272d-4a2a-93e7-7268962a9814"
/>

### with Japanese wiki dump

<img width="2008" height="518" alt="CleanShot 2025-11-26 at 08 43 49@2x"
src="https://github.com/user-attachments/assets/edb408c8-a604-4a8f-bd5b-80f19e3d65ee"
/>

### with Arabic wiki dump

<img width="2010" height="512" alt="CleanShot 2025-11-26 at 08 44 25@2x"
src="https://github.com/user-attachments/assets/81a29ac8-0586-4e82-8276-d7fa90c31c90"
/>


TODO:

* [x] Take a closer look at the assembly and understand why this PR (8
KB vs 1 KB table) is faster on my machine.
* [x] _(**edit**: checking this off because it seems unnecessary)_ If
this turns out to actually be unacceptably slower, one possibility is to
switch to `uucode`'s `precomputedGraphemeBreak` which uses a 1445 byte
table since it uses a dense table (indexed using multiplication instead
of bitCast, though, which did show up in the initial benchmarks from
https://github.com/ghostty-org/ghostty/pull/8757 a small amount.)

AI was used in some of the uucode changes in
https://github.com/ghostty-org/ghostty/pull/9678 (Amp--primarily for
tests), but everything was carefully vetted and much of it done by hand.
This PR was made without AI with the exception of consulting AI about
whether the "Prepend + ASCII" scenario is common (hopefully it's right
about that being uncommon).
2026-01-20 09:44:15 -08:00
Jeffrey C. Ollie
f180f1c9b8 osc: remove inline from Parser.next 2026-01-08 14:12:16 -06:00
Jeffrey C. Ollie
fb1268a908 benchmark: add doNotOptimizeAway to OSC benchmark 2026-01-08 13:50:43 -06:00
Jeffrey C. Ollie
01a75ceec4 benchmark: add option to microbenchmark OSC parser 2025-12-10 22:31:27 -06:00
Jacob Sandlund
c49d50eb80 Merge remote-tracking branch 'upstream/main' into grapheme-break 2025-11-30 08:57:08 -05:00
Mitchell Hashimoto
dbfc3eb679 Remove unused imports 2025-11-27 13:37:53 -08:00
Jacob Sandlund
f68abdef75 Merge branch 'main' into grapheme-break 2025-11-26 17:31:47 -05:00
Jacob Sandlund
807febcb5e benchmarks: align read_buf to cache line 2025-11-25 09:07:21 -05:00
Jacob Sandlund
97aff0743e unicode: switch to uucode grapheme break to match unicode 16 spec 2025-11-23 22:33:01 -05:00
Mitchell Hashimoto
4caefb807c terminal: fix up some performance issues with render state 2025-11-20 22:00:42 -08:00
Mitchell Hashimoto
5d85f2382e terminal: render state needs to preserve as much allocation as possible 2025-11-20 22:00:42 -08:00
Mitchell Hashimoto
7195cab7d3 benchmark: add RenderState to ScreenClone benchmark 2025-11-20 22:00:42 -08:00
Mitchell Hashimoto
2f49e0c902 remove screenclone test cause it leaks memory on purpose 2025-11-17 06:42:00 -10:00
Mitchell Hashimoto
9a46397b59 benchmark: screen clone 2025-11-17 05:08:19 -10:00
Mitchell Hashimoto
2ef89c153a terminal: convert C0 2025-10-23 15:52:42 -07:00
Mitchell Hashimoto
f7189d14b9 terminal: convert Stream to use Action tagged union 2025-10-23 15:50:56 -07:00
Mitchell Hashimoto
06ad3b77b7 Zig 0.15.2 (#9200) 2025-10-14 07:11:10 -07:00
Mitchell Hashimoto
cb295b84a0 Zig 0.15: zig build test 2025-10-03 07:10:43 -07:00
Jacob Sandlund
b01770c21c Merge remote-tracking branch 'upstream/main' into jacob/uucode 2025-09-23 09:36:41 -04:00
Mitchell Hashimoto
10dc9353b7 unicode: delete props.zig and clean up symbols deps too
Follow up to #8810

Same reasoning.
2025-09-20 20:28:25 -07:00
Jacob Sandlund
cf3b514efc pr feedback: get, remove todos for case_folding_simple 2025-09-19 01:24:13 -04:00
Jacob Sandlund
18e9989f63 forgot to align buf 2025-09-18 14:20:41 -04:00
Jacob Sandlund
69594119c3 fix up diff from benchmarks, and add tests against ziglyph 2025-09-18 11:46:05 -04:00
Jacob Sandlund
3275903611 update uucode and cleanups 2025-09-18 09:26:09 -04:00
Jacob Sandlund
8a40c1f59f Merge remote-tracking branch 'upstream/main' into jacob/uucode 2025-09-10 10:01:06 -04:00
Jacob Sandlund
cffa52e658 changes after benchmarking 2025-09-09 11:38:10 -04:00
Jacob Sandlund
77b4c52634 [benchmarks] Align buf to cache line for consistency 2025-09-08 17:59:17 -04:00
Jacob Sandlund
113e89b389 [benchmarks] Use std.mem.doNotOptimizeAway to avoid data collisions 2025-09-06 15:06:00 -04:00
Jacob Sandlund
b0db51c45e fast getX(.is_symbol) 2025-09-06 15:01:29 -04:00
Jacob Sandlund
c3994347c0 doNotOptimizeAway 2025-09-06 14:55:21 -04:00
Jacob Sandlund
f86a3a9b50 Merge remote-tracking branch 'upstream/main' into jacob/uucode 2025-09-06 14:31:41 -04:00
Jacob Sandlund
2af08bdbe3 trying a bunch of things to get performance to match 2025-09-06 10:42:02 -04:00
Jeffrey C. Ollie
e024b77ad5 drop the new LUT type as no performance advantage detected 2025-09-05 07:58:05 -05:00
Jeffrey C. Ollie
a7da96faee add two LUT-based implementations of isSymbol 2025-09-05 07:58:01 -05:00
Jeffrey C. Ollie
0bc90b2a20 ci: build on freebsd 2025-08-30 13:58:25 -05:00