-
Notifications
You must be signed in to change notification settings - Fork 891
TextBlock re-parses NoWrap text in arrange when only the width changed, wasting memory on huge single lines #24918
Copy link
Copy link
Open
Labels
area/performance 📈Categorizes an issue or PR as relevant to performanceCategorizes an issue or PR as relevant to performancearea/skia ✏️Categorizes an issue or PR as relevant to SkiaCategorizes an issue or PR as relevant to Skiaarea/skia/text ✏️difficulty/medium 🤔Categorizes an issue for which the difficulty level is reachable with a good understanding of WinUICategorizes an issue for which the difficulty level is reachable with a good understanding of WinUIkind/bugSomething isn't workingSomething isn't workingplatform/allCategorizes an issue or PR as relevant to the all platformsCategorizes an issue or PR as relevant to the all platformsproject/text 🔤Categorizes an issue or PR as relevant to text (TextBox, PasswordBox, TextBlock, Fonts, …)Categorizes an issue or PR as relevant to text (TextBox, PasswordBox, TextBlock, Fonts, …)triage/untriagedIndicates an issue requires triaging or verificationIndicates an issue requires triaging or verification
Description
Activity
Metadata
Metadata
Assignees
Labels
area/performance 📈Categorizes an issue or PR as relevant to performanceCategorizes an issue or PR as relevant to performancearea/skia ✏️Categorizes an issue or PR as relevant to SkiaCategorizes an issue or PR as relevant to Skiaarea/skia/text ✏️difficulty/medium 🤔Categorizes an issue for which the difficulty level is reachable with a good understanding of WinUICategorizes an issue for which the difficulty level is reachable with a good understanding of WinUIkind/bugSomething isn't workingSomething isn't workingplatform/allCategorizes an issue or PR as relevant to the all platformsCategorizes an issue or PR as relevant to the all platformsproject/text 🔤Categorizes an issue or PR as relevant to text (TextBox, PasswordBox, TextBlock, Fonts, …)Categorizes an issue or PR as relevant to text (TextBox, PasswordBox, TextBlock, Fonts, …)triage/untriagedIndicates an issue requires triaging or verificationIndicates an issue requires triaging or verification
Current behavior
TextBlock.ArrangeOverridere-parses (re-shapes and re-lays out) the whole text whenever the arrange width differs from the width used inMeasureOverride. AScrollContentPresenter(ScrollViewer, TextBox, RichEditBox) measures its content at an infinite width and arranges it at the desired width, so aTextWrapping="NoWrap"block gets laid out a second time with identical line breaks.For a single very long NoWrap line this is expensive: each
UnicodeTextbuild allocates hundreds of MB for 1M characters (LinkedList<Cluster>~96 MB,LinkedList<Glyph>~56 MB, plus the width table), and the nodes get promoted to gen2, so garbage piles up until a full GC. A 1,000,000-character NoWrap line in a RichEditBox peaked at ~3.8 GB working set on Win32 desktop (the layout half is built 4 times for the same text: measure + arrange, before and after an edit), and the app was killed by the low-memory killer at ~2.4 GB on Android.Expected behavior
Arranging a NoWrap, untrimmed, left-aligned block at a width that is >= the laid-out width should reuse the measure-time layout. WinUI avoids this re-layout via
BlockNode::CanBypassMeasure(src/dxaml/xcp/core/text/...), which only re-measures when the width or alignment changes in a way that can affect line breaks.How to reproduce it
Big.Text = new string('a', 1_000_000);GC.GetTotalAllocatedBytes()before and after the layout pass, ordotnet-tracewith the GC allocation provider).UnicodeText..ctorruns twice for the one text: once fromMeasureOverride(width = infinity) and once fromArrangeOverride(width = desired width), although the lines are identical.Same effect with a TextBox/RichEditBox holding a long single line, where it shows up as multi-GB transient peaks.
Workaround
Keep single lines short (wrap or split text), or avoid giant single-line content.
Works on UWP/WinUI
Yes (line layout is not repeated when the width change cannot alter line breaks).
Environment
Skia (measured on Win32 desktop), net11.0-desktop, Uno master @ 7a22141
NuGet package version(s)
master @ 7a22141
Affected platforms
Skia (all heads; shared
UnicodeTextlayout)IDE
N/A
Relevant plugins
N/A
Anything else we need to know?
Confirmed by code inspection on master (no runtime measurement was taken for this specific report; the numbers above come from a profiling session of a RichEditBox test):
TextBlock.MeasureOverridealways callsParseText.TextBlock.ArrangeOverridecallsParseTextwhen_lastParsedTextCreationValues.availableSize != availableSizeWithoutPadding, i.e. on any width or height difference.Related: #24228 (open PR) makes arrange tolerate height differences, but still re-parses when the width differs, which is exactly the infinite-measure / desired-width-arrange case here.
Suggested direction: skip the re-parse when the result cannot change (NoWrap, no trimming, left-aligned, new width >= laid-out width), or let
UnicodeTextrebind width-only state. Justification, list markers and RTL alignment read_availableSize.Width, so they need care.Separately, glyph drawing for the same text is addressed by #24654.