Address performance problems for StructuredText code blocks#39
Address performance problems for StructuredText code blocks#39danielrothmann wants to merge 3 commits into
Conversation
Cherry-picked from gonzalezreal#39: - Replace linear Text reduce with balanced binary tree concatenation to prevent stack overflow on long code blocks (O(log N) vs O(N) depth) - Convert AttributedSubstring to owned String before async boundary to prevent dangling references during streaming - Skip expensive tokenizer when theme has no token properties Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Sorry for the late review. I’ve been swamped with work. @danielrothmann Thanks for putting this together! I found one issue before merging: if an app uses Could you update the task key to include the language hint and whether the theme needs tokens, and also make highlighting react to theme changes? The GitHub workflows aged out because this PR is from a fork, so we can rerun them after the update. |
|
I ended up merging #65 for the |
|
No problem @gonzalezreal and sounds good to get the stack overflow fix in. I'll try and have a look later and come back with an updated PR. |
This PR improves the reliability of
StructuredTextwhen rendering large code blocks and makesTextualmore suited to streaming.Context
It has previously been observed that the library may cause a crash for large markdown documents as discussed in #23
If using
StructuredTextfor streaming content (frequent layout updates), this problem can be excessive.The causes
I found a few things that influence this behaviour:
1) Stack overflow in
TextBuilderconcatenationTextBuilderconstructs the final Text view by reducing an array of per-run Text values with string interpolation.This creates an unbalanced tree where each step nests the previous result on the left. For syntax-highlighted code blocks, it creates a very deep
Texttree. When SwiftUI resolves it,Text.resolve()recurses through every level and with hundreds of code block tokens, causes a stack overflow.The change here balances that tree, so that each side has roughly equal depth. This makes a stack overflow from excessive recursion depth very unlikely.
2) Borrowed
AttributedSubstringcaptured across async boundaryHighlighedTextFragmentpasses its content directly to a task closure for tokenisation.AttributedSubstringborrows storage from its parentAttributedString, so if the content updates rapidly (like when streaming), the parent can be replaced while the async task may still reference the old substring storage.3) Redundant tokenisation when there is no highlighting
When content changes frequently,
HighlightedTextFragmentre-tokenises on every update. In the streaming scenario, this leads to performance degradation. If syntax highlighting is disabled, the tokenisation pass can be skipped, which allows downstream applications to disable syntax while streaming and only pay the tokenisation cost once, when done.