perf(textarea): Make typing hot paths allocation-free - #1033
Open
reneleonhardt wants to merge 7 commits into
Open
perf(textarea): Make typing hot paths allocation-free#1033reneleonhardt wants to merge 7 commits into
reneleonhardt wants to merge 7 commits into
Conversation
Word wrapping rebuilt a fresh string on every word boundary, and every cached-wrap lookup re-hashed the whole line with SHA-256. Wrap now accumulates rows in reusable buffers with zero-copy width views and materializes the result once at the end, keeping the algorithm byte-identical (parity + cursor tests pin it); the cache key is an allocation-free FNV-1a hash of runes+width.
…le buffer The View placeholder path materialized the entire buffer string just to test whether it was empty. The check is now an exact O(1) equivalent (Value() is empty only when there is at most one line with no runes; the inter-line newlines count), so an all-empty multiline buffer is still non-empty — test-pinned.
The CharLimit walk re-measured every line through the grapheme machinery even for pure ASCII. Printable ASCII runes are width 1, so the count is exact by rune count for 0x20-0x7E; control (width 0) and wide runes fall back to the exact path.
…g the buffer Value() grew its builder from zero, so a multi-line buffer was reallocated and copied several times over per rebuild. Pre-growing to the total rune count plus newlines writes the buffer once.
Length() computed len(m.value) - 1 with no nil guard, so an uninitialized model reported -1. Return 0 instead; the test pins Value()/Length() early-return behavior for a zero-value model.
Add benchmarks for word wrap (average prompt, 2k, and 20k lines), the line hash, the ASCII length check, and the multi-line value rebuild, so the hot paths' timing and allocation behavior can be verified and compared over time.
Length() counts cells, but paste truncation cut by rune count, so wide (CJK) content could overshoot CharLimit by up to ~2x. Truncate by cell width now; ASCII behavior is unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The textarea's per-keystroke and per-render hot paths were doing more work than the content deserved: soft-wrapping rebuilt strings on every word boundary, every cached-wrap lookup re-hashed the whole line, and the length/value/placeholder checks re-measured or re-materialized the entire buffer. This branch makes those paths allocation-free while keeping output identical (plus two internal behavior fixes: a zero-value model's Length() reported -1, and CharLimit is now enforced in cells).
Changes
Length()returned-1(len(m.value) - 1on nil); it now returns0, pinned by a test.Length()counts cells, letting wide (CJK) content overshoot the limit by up to ~2x; truncation now walks cell width. ASCII behavior is unchanged.Validation
Parity/cursor tests (wrap vs the line grid, every cursor position) plus a corpus/fuzz covering ASCII, CJK, ambiguous-width, trailing-space, and empty inputs; tests for the new short-circuits. Full
./textarea/...suite green,-raceclean, gofmt/vet clean.Performance (Apple M4 Max, committed benches
perf_bench_test.go+wrap_test.go)At the 20k-char paste limit: wrap() 5.75ms -> 4.54ms (-21%), 647KB -> 163KB (-75%), 13,713 -> 4,450 allocs (-68%).
Breaking changes
None (wrap/hash/value outputs are identical; the ASCII short-circuits preserve exact widths; the zero-model Length() guard changes only an uninitialized model's -1 to 0).
Notes
Commits are only split for reviewability,
Squash and mergeallows a cleaner history.Developed with carefully directed, manually reviewed AI assistance.