Skip to content

Stop buffering huge ANSI styled lines - #450

Merged
walles merged 9 commits into
walles:masterfrom
subotac:fix/ansi-huge-line-limit
Jul 29, 2026
Merged

Stop buffering huge ANSI styled lines#450
walles merged 9 commits into
walles:masterfrom
subotac:fix/ansi-huge-line-limit

Conversation

@subotac

@subotac subotac commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #358.

A leading ANSI sequence bypassed the plain-text shortcut, after which styledStringSplitter buffered the entire same-style run before invoking the cells-counting callback. Flush the run after the existing conservative bytes-per-cell window so the callback can stop parsing once the requested cells are filled.

On Windows/amd64, the 5 MiB leading-SGR benchmark improved from 39.17 ms and 26.5 MB allocated per operation to 37–41 µs and 16.9 KB.

Testing

  • MOOR_SKIP_INTERACTIVE_TESTS=1 ./test.sh
  • MOOR_SKIP_INTERACTIVE_TESTS=1 GOARCH=386 ./test.sh
  • Windows go build -race ./...
  • Windows go test -race -timeout 60s ./internal/textstyles

@walles
walles force-pushed the fix/ansi-huge-line-limit branch from af48cc9 to 384c26c Compare July 29, 2026 08:23
walles added 8 commits July 29, 2026 11:19
The test feeds single byte characters, so the flushed chunk is exactly
maxBytesPerCell bytes per requested cell. Asserting that exact size says
more than an upper bound does, and gives a clearer failure message.

🤖 Committed by Claude (Opus 5)
Nothing covered this: the whole suite, TestCellsLimitOnlyTruncates
included, still passes if styledStringsFromString() truncates its input
to the escape sequence scan window instead of buffering up to it. That
returns zero cells for a line whose escapes outweigh its text.

🤖 Committed by Claude (Opus 5)
b.Loop() both resets the timer and runs the loop, so the separate
b.ResetTimer() call goes away.

🤖 Committed by Claude (Opus 5)
It exercises StyledRunesFromString(), which lives in ansiTokenizer.go,
and the package's other two benchmarks are both at the end of that
file's test file. Also say where the 81 comes from.

🤖 Committed by Claude (Opus 5)
Two of the three call sites had to remember to reset the builder
afterwards, and the third one must not care since the splitter is done by
then. Doing it inside finalizeCurrentPart() removes that obligation from
the callers.

🤖 Committed by Claude (Opus 5)
The escape sequence scan window and the part flushing threshold are the
same number for the same reason: maxBytesPerCell bytes per cell is an
upper bound, so that many bytes per requested cell covers everything that
can reach the result. Deriving it twice meant explaining it twice.

🤖 Committed by Claude (Opus 5)
An escape sequence we can't parse gets replayed as plain runes, and that
replay loop iterates the whole sequence without noticing that the cells
limit has been reached. So a huge malformed sequence reaches the callback
in full, 2341 calls for the five megabytes in this test.

🤖 Committed by Claude (Opus 5)
Reaching the cells limit used to mean pointing nextByteIndex past the end
of the input, which stops nextChar() and with it run()'s main loop. But
the loop replaying an unparseable escape sequence iterates that sequence
directly, so nextByteIndex never came up and the replay ran to the end.

A named flag says what it means and can be tested where it matters. On a
five megabyte malformed sequence that takes us from 39.7 ms and 31.8 MB
per operation to 15.9 ms and 5.3 MB.

🤖 Committed by Claude (Opus 5)
@walles
walles merged commit 439e7ba into walles:master Jul 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants