Fixes #5499. Skip SetNeedsLayout when Dim.Auto text recompute yields unchanged size#5549
Conversation
…ields unchanged size The View.Text setter unconditionally called SetNeedsLayout(), which propagates layout work up the entire ancestor chain and forces a redraw on every text change - even when the recomputed Dim.Auto(DimAutoStyle.Text) size is identical to the current Frame (e.g. a once-per-second clock Label). When both Width and Height are exactly DimAutoStyle.Text and the view has been laid out, the setter now predicts the new Frame size by invoking the exact same Dim.Calculate path the layout engine uses (no reimplementation, so it cannot disagree with a real layout pass). If the size is unchanged, it skips SetNeedsLayout() and the ancestor propagation, marks TextFormatter.NeedsFormat and calls SetNeedsDraw() so the new content is still reformatted and redrawn. The optimization only ever avoids *setting* NeedsLayout; it never clears it, so a pre-existing pending layout request survives. DimAutoStyle.Auto, fixed dims, and not-yet-laid-out views all fall back to the original SetNeedsLayout() path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fixed/one-axis Code review of tui-cs#5549 found two correctness gaps and one scope limitation in the Text-change layout-skip optimization: 1. Position changes were ignored. The predictor compared only size to Frame, but a Pos can depend on Text (e.g. Pos.Func reading view.Text), so a same-size text change could still require a new Frame.X/Y and was wrongly skipped. 2. The "exactly Text auto" guard used dim.Has(...), which matches nested/composite dimensions (e.g. Dim.Auto(Text) + 2), letting location- or reference-sensitive composites into the simplified fast path. 3. The optimization was narrower than tui-cs#5499 intended - it only handled both axes being DimAutoStyle.Text and forced layout for fixed-size and one-axis-auto text changes that are safe to skip. Fixes: - Extracted SetRelativeLayout's Frame computation into TryComputeRelativeFrame so the speculative check and the real layout share one source of truth. The predictor now resolves and compares the full Rectangle (position included), so a text-dependent Pos that moves the view correctly triggers layout. - The eligibility guard is now exact (`dim is DimAbsolute or DimAuto { Style: DimAutoStyle.Text }`). Composite/nested dims and DimAutoStyle.Content/.Auto (which lay out subviews while calculating) fall back to SetNeedsLayout. - Broadened to fixed dimensions and one-axis-auto, the common safe cases: a fixed or Text-only-auto view whose resolved Frame is unchanged now skips layout and only redraws. Tests updated/added: Pos-depends-on-Text moves view (regression for tui-cs#1), composite dim bypasses fast path (tui-cs#2), one-axis-auto same/different size, and fixed-size text redraw skips layout (tui-cs#3). Full UnitTestsParallelizable: 17458 passed / 0 failed; UnitTests.NonParallelizable: 72 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A new review found the widened same-frame fast path could leave TextFormatter unconstrained. UpdateTextFormatterText() clears ConstrainToWidth/Height, and the fast path then skipped SetRelativeLayout() — including its post-resolve cleanup that restores constraints from GetContentWidth()/GetContentHeight(). For a fixed-size view with WordWrap, ConstrainToWidth stayed null (treated as int.MaxValue by GetLines()), so wrapped/clipped text could render incorrectly until some later layout restored the constraints. - Extracted that finalization into FinalizeTextFormatterConstraints(), shared by SetRelativeLayout() and the fast path, so the fast path leaves TextFormatter in exactly the state a full layout pass would. - Renamed the helper to TryRedrawWithoutLayout() to reflect that it now performs the redraw-only handling (finalize constraints, mark NeedsFormat, SetNeedsDraw) rather than just answering a question. Deep review (temporary probe harnesses, since removed) compared the isolated fast path against a full-layout control for Frame, both TextFormatter constraints, and rendered driver contents across a sweep of width/height dims (Text-auto, min/max, fixed) x WordWrap x a text-change sequence — all identical, and the sweep was confirmed to catch the regression when the finalization was removed. Also probed CheckBox (overrides UpdateTextFormatterText), Pos.Align sibling groups, fixed parents with subviews, reentrant TextChanged, and a 100-tick clock simulation — no unintended consequences. Permanent coverage added: fixed-size and one-axis-auto constraint survival with WordWrap, repeated-update frame-drift/ancestor-propagation, reentrancy, and render-equivalence tests (TextFastPathRenderTests) covering fixed-size wrap/clip and max-constrained auto wrapping. Full UnitTestsParallelizable: 17467 passed / 0 failed; UnitTests.NonParallelizable: 72 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1 (formatter constraints lost on the widened fast path) — resolved in ae2047aConfirmed the bug with a probe: a Root cause: Fix:
TestingBeyond the requested constraint-survival coverage (fixed-size and one-axis-auto, with
The high-value probes were kept as permanent tests: Results: |
There was a problem hiding this comment.
Pull request overview
Optimizes View.Text updates to avoid scheduling a full layout pass (and ancestor NeedsLayout propagation) when changing text would resolve to the same Frame, while still ensuring correct redraw and TextFormatter constraint behavior. This targets the performance issue described in #5499 (e.g., clock-like labels updating frequently without changing size/position).
Changes:
- Refactors layout frame resolution into
TryComputeRelativeFrameand extractsFinalizeTextFormatterConstraintsso both normal layout and the new text fast path share the same computation and formatter finalization. - Updates
View.Textsetter to attempt a redraw-only fast path when a predicted frame matches the current frame and the dims are safely resolvable. - Adds focused diagnostics and end-to-end render comparison tests to validate redraw-only behavior and formatter constraint preservation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| Terminal.Gui/ViewBase/View.Layout.cs | Extracts frame prediction and formatter constraint finalization to share logic with the text fast path. |
| Terminal.Gui/ViewBase/View.Text.cs | Adds TryRedrawWithoutLayout to skip SetNeedsLayout() when predicted Frame is unchanged, while still forcing format+draw. |
| Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.TextOptimization.cs | Adds diagnostics covering same-frame text updates, propagation behavior, and guards for unsafe cases. |
| Tests/UnitTestsParallelizable/ViewBase/Layout/TextFastPathRenderTests.cs | Adds end-to-end rendering parity tests comparing fast-path redraw vs forced full-layout control. |
Fixes #5499.
Problem
The
View.Textsetter unconditionally calledSetNeedsLayout(). That marks the view and its whole subtree as needing layout, sets_needsDrawAfterLayout, and propagates up the entire ancestor chain on every text change, even when the recomputed frame is identical to the current one (e.g. a once-per-second clockLabel).The layout machinery already no-ops the frame mutation when nothing changed (
SetRelativeLayoutonly callsSetFramewhenFrame != newFrame), but the layout pass, formatter setup, redraw scheduling, and ancestor invalidation were still being scheduled every tick.Change
Terminal.Gui/ViewBase/View.Layout.csExtracted
SetRelativeLayout'sPos/Dim-> absolute-Frameresolution into a privateTryComputeRelativeFrame (Size, out Rectangle).SetRelativeLayoutnow calls it, and so does theTextsetter's speculative check. This keeps frame prediction and real layout on the same computation path.Also extracted
SetRelativeLayout's post-frameTextFormatterconstraint cleanup intoFinalizeTextFormatterConstraints(). This matters becauseUpdateTextFormatterText()clearsConstrainToWidth/ConstrainToHeight; if the text setter skips layout, the fast path must still restore the same formatter constraints a full layout pass would produce so wrapped/clipped text formats correctly.Terminal.Gui/ViewBase/View.Text.csThe setter now calls
TryRedrawWithoutLayout()after updatingTextFormatter.Text.When the view has already been laid out and both
WidthandHeightare eligible,TryRedrawWithoutLayout()resolves the would-beFrameviaTryComputeRelativeFrame. If the predicted frame equals the currentFrame, it skipsSetNeedsLayout()and ancestor propagation, then:TextFormatterconstraints withFinalizeTextFormatterConstraints();TextFormatter.NeedsFormat = true;SetNeedsDraw();TextChangednormally.The comparison is the full
Rectangle(position and size), not just dimensions. APoscan depend onText(e.g.Pos.Funcreadingview.Text), so a same-size text change can still move the view and must run layout.Eligibility is exact by design:
DimAbsoluteis constant; a Text-onlyDimAutodepends on this view'sTextFormatter.DimAutoStyle.Content/DimAutoStyle.Autocan lay out subviews while calculating, so those fall back toSetNeedsLayout().Dim.Auto(Text) + 2are not bareDimAbsolute/DimAuto, so they also fall back.This covers the intended safe cases: both-axis Text-auto, fixed-size text redraws, and one-axis-auto (
Width = Auto(Text), Height = 1). The optimization only avoids settingNeedsLayout; it never clears a pre-existing pending layout request.Tests
Added focused layout and render coverage:
Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.TextOptimization.cssuperView.NeedsLayout == falsewhile the view still redraws;Pos.Funcdepending onTextforces layout when same-size text moves the view;TextChangedhandling;DimAutoStyle.Auto, different-size, and pending-layout cases.Tests/UnitTestsParallelizable/ViewBase/Layout/TextFastPathRenderTests.csAdditional exploratory probes were run and removed after conversion of the high-value cases into permanent tests. They compared fast-path vs full-layout behavior across repeated text updates, fixed/min/max/Text-auto dimensions, word-wrap on/off, explicit content-size constraints,
Button,CheckBox, and reentrant handlers.Verification
Current local verification on
ae2047abec598989a10bea28d858e01e6b6b2f5f:Tests/UnitTestsParallelizable: passedDimAutoTeststext-focused tests: 67 passedTextFastPathRenderTests: 5 passedSetRelativeLayoutTests: 10 passedUnitTestsParallelizable: 17,484 total / 0 failed / 17 skippedUnitTests.NonParallelizable: 74 total / 0 failed / 2 skippedPull Request checklist
dotnet testbefore commit///style comments)🤖 Generated with Claude Code