Skip to content

base: Walk rows, not characters, for Inline text line bounds - #3070

Merged
huacnlee merged 1 commit into
mainfrom
perf/inline-line-bounds
Sep 14, 2026
Merged

huacnlee merged 1 commit into
mainfrom
perf/inline-line-bounds

Conversation

@huacnlee

@huacnlee huacnlee commented Sep 14, 2026

Copy link
Copy Markdown
Member

Description

Inline::paint registered its selection line boxes by calling TextLayout::position_for_index twice per character, and each call scans the line's rows and glyphs, so every selectable inline cost O(chars × glyphs) per frame.

Profiling the Longbridge AI chat on iOS (Time Profiler, iPhone 17 Pro simulator, Release) while scrolling a real conversation put that walk at 10% of all CPU — the largest single symbol (WrappedLineLayout::position_for_index self time), and at 3% even on plain synthetic paragraphs.

This builds the boxes from the wrapped line layouts instead: one box per row, from the row's start to its last glyph, extended by half a line height when another row follows — the width the character walk gave to a character whose successor sat on the next row, which the selection geometry was tuned against.

One behavioural difference, on purpose: the character walk placed a wrapped row's first character at the end of the row before it (that is where position_for_index reports it), so that row's box started one glyph in, and a row holding only that one glyph had no box at all. The row walk covers every row from its start. The new test pins agreement on every row the old walk could see (same top / bottom / right edge; left edge never further right).

Measured on the same synthetic scroll benchmark (20 s of continuous finger swipes through 80 markdown messages, same gesture script, Release):

before after
total CPU 6.33 s 5.53 s (−13%)
Inline::paint 0.35 s 0.15 s
position_for_index self 0.19 s 0

On real chat content (long paragraphs) the walk was 1.13 s of a 25 s window and is now 0.

How to Test

  • cargo test -p gpui-base --lib — includes line_bounds_tests::row_walk_matches_the_character_walk, which keeps the old character walk as an oracle and compares it against the row walk over wrapped, multi-line, CJK and trailing-newline texts at three wrap widths and two clip masks.
  • Text selection in any TextView (drag across wrapped lines, across paragraphs, triple-click) behaves as before.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes. (not run — no story renders this path differently; covered by the unit test)
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

🤖 Generated with Claude Code

https://claude.ai/code/session_01HY6KNBpkTcKkW2PpjrFhji

Inline::paint registered its line boxes by calling
TextLayout::position_for_index twice per character, and each call scans
the line's rows and glyphs, so every selectable inline cost
O(chars × glyphs) per frame. Profiling the AI chat on iOS while scrolling
a real conversation put that walk at 10% of all CPU — the largest single
symbol — and at 3% even on plain synthetic paragraphs.

Build the boxes from the wrapped line layouts instead: one box per row,
from the row's start to its last glyph, extended by half a line height
when another row follows, which is the width the character walk gave to
a character whose successor sat on the next row. The row walk also covers
the first glyph of a wrapped row, which the character walk credited to
the end of the row before it (and so skipped a row holding only that
glyph); the test pins the agreement on every row the old walk could see.

Synthetic scroll benchmark (iPhone 17 Pro simulator, Release): total CPU
over 20 s of continuous swiping 6.33 s -> 5.53 s; Inline::paint
0.35 s -> 0.15 s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6KNBpkTcKkW2PpjrFhji
@huacnlee
huacnlee merged commit c815585 into main Sep 14, 2026
9 checks passed
@huacnlee
huacnlee deleted the perf/inline-line-bounds branch September 14, 2026 15:42
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.

1 participant