Skip to content

TextBlock re-parses NoWrap text in arrange when only the width changed, wasting memory on huge single lines #24918

Description

@MartinZikmund

Current behavior

TextBlock.ArrangeOverride re-parses (re-shapes and re-lays out) the whole text whenever the arrange width differs from the width used in MeasureOverride. A ScrollContentPresenter (ScrollViewer, TextBox, RichEditBox) measures its content at an infinite width and arranges it at the desired width, so a TextWrapping="NoWrap" block gets laid out a second time with identical line breaks.

For a single very long NoWrap line this is expensive: each UnicodeText build 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

  1. Create a Skia Desktop app with:
    <ScrollViewer HorizontalScrollBarVisibility="Auto" Height="200" Width="400">
        <TextBlock x:Name="Big" TextWrapping="NoWrap" />
    </ScrollViewer>
  2. In code-behind set Big.Text = new string('a', 1_000_000);
  3. Watch managed allocations / working set (e.g. GC.GetTotalAllocatedBytes() before and after the layout pass, or dotnet-trace with the GC allocation provider). UnicodeText..ctor runs twice for the one text: once from MeasureOverride (width = infinity) and once from ArrangeOverride (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 UnicodeText layout)

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):

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 UnicodeText rebind 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/performance 📈Categorizes an issue or PR as relevant to performancearea/skia ✏️Categorizes 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 WinUIkind/bugSomething isn't workingplatform/allCategorizes an issue or PR as relevant to the all platformsproject/text 🔤Categorizes an issue or PR as relevant to text (TextBox, PasswordBox, TextBlock, Fonts, …)triage/untriagedIndicates an issue requires triaging or verification

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions