terminal: print repeated characters through printSlice (#13625)

`printRepeat` (CSI `b`, repeat the previous character N times) calls
`print()` once per repeat, so something like `\x1b[2000b` ran grapheme
checks, width lookups, wrap handling, and the integrity assert 2000
times for what is usually the same character on the same row.

`Terminal.print` was 24% of samples on a REP-heavy micro benchmark.

This PR just aims to add a fast path by introducing a chunking
mechanism. anything that needs care (insert mode, grapheme clustering,
hyperlinks) still falls back to per-codepoint print() inside printSlice,
so behavior *should* stay unchanged.

Some profiling data:

Generated with some plain stupid logic:

```py
D = "benchdata"
parts, total = [], 0
while total < 40_000_000:
    line = "x" + "\x1b[80b" + "y" + "\x1b[35b" + "\r\n"
    parts.append(line); total += len(line)
open(f"{D}/rep.bin", "wb").write("".join(parts).encode())
```
**macOS (hyperfine, 15 runs, warmup 3):**

| | mean |
|---|---|
| before | 2.360 s |
| after | 1.166 s |


And now the really interesting and promising stuff

**Linux, 24-core NixOS x86_64 (poop, 6s sampling):**

| | wall_time | instructions | branch_misses | peak_rss |
|---|---|---|---|---|
| before | 1.51 s | 50.9 G | 9.41 M | 6.82 MB |
| after | 562 ms | 9.07 G | 114 K | 6.74 MB |
This commit is contained in:
Mitchell Hashimoto
2026-08-05 06:56:01 -07:00
committed by GitHub

View File

@@ -490,9 +490,19 @@ pub fn printString(self: *Terminal, str: []const u8) !void {
/// Print the previous printed character a repeated amount of times.
pub fn printRepeat(self: *Terminal, count_req: usize) !void {
if (self.previous_char) |c| {
const count = @max(count_req, 1);
for (0..count) |_| try self.print(c);
const c = self.previous_char orelse return;
var remaining = @max(count_req, 1);
// Print the repeated codepoint in slices so that eligible runs
// take the batched printSlice fast path. printSlice is semantically
// identical to calling print per codepoint: ineligible characters
// or terminal states (insert mode, grapheme clustering, hyperlinks,
// etc.) fall back to the per-codepoint print() path internally.
var buf: [4096]u32 = @splat(c);
while (remaining > 0) {
const n = @min(remaining, buf.len);
try self.printSlice(buf[0..n]);
remaining -= n;
}
}