Fixes #5357 - Avoid redraw fan-out from ancestor-only layout propagation#5373
Conversation
Adds TabsFanOutDiagnosticTests with seven parallelizable tests that observe SubViewsLaidOut, DrawComplete, ClearedViewport, DrawingContent, and FrameChanged events per tab to measure layout/draw fan-out when an active TextView inside Tabs scrolls. Captures the current gui-cs#4973 behavior so that future fixes can be verified and silent regressions caught: - Active tab vs. inactive tab layout work - Active tab vs. inactive tab draw work (DrawComplete, ClearedViewport, DrawingContent) - A comparable single-TextView baseline vs. tabbed scenario fan-out ratio - ViewportSettings.Transparent does not mask the diagnostic - Shadow margin does not mask active-tab activity - IOutput-level output (not Driver.Contents) is observed - Layout and draw counters reported separately so regressions can be localized to one pipeline The tests are instrumentation-only and do not change rendering or invalidation semantics.
TextView is being deprecated in favor of Code (read-only) and Editor
(editable). The fan-out behavior lives in the View base class layout/draw
pipeline, so any scrollable view inside Tabs reproduces it — swap to Code
so the diagnostic survives the deprecation.
Adjusts assertions to use only widget-agnostic signals (DrawComplete on
each tab, ClearedViewport on the Tabs container). Code overrides
OnClearingViewport/OnDrawingContent and returns true, which suppresses
the ClearedViewport/DrawingContent events on the Code instances. The
per-Code data is still reported but no longer asserted at that level.
Adds Tests/IntegrationTests/TabsFanOutIntegrationTests.cs that drives
the active tab via a real Key.PageDown through the input processor →
command dispatch → main-loop LayoutAndDraw path (rather than mutating
Viewport directly). Runs against all registered drivers. Confirms the
fan-out is observable end-to-end:
Driver: <each>
Active tab Viewport.Y after 3 PageDowns: 3
Tabs 3 3 3
Code1 (active) 3 3 0
Code2-4 (inact) 3 3 0
Sum inactive DrawComplete = 9
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Pin the new SetNeedsLayout contract: changed view + own subtree + adornment subviews + ancestors are marked, but unaffected sibling subtrees stay clean. Three tests currently fail and will pass after the split lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ree invalidation SetNeedsLayout previously did downward subtree marking AND upward propagation via SuperView?.SetNeedsLayout, which re-entered the same downward cascade and re-marked every sibling subtree. In overlapping Tabs this meant activity in the active tab page dirtied every inactive page. Extract the downward subtree pass into MarkSubtreeNeedsLayout, the adornment subview helper into MarkAdornmentSubViewTrees, and the upward walk into MarkAncestorsNeedLayout. The ancestor walk marks each ancestor (and its adornment subview trees) without descending into the ancestor's regular SubViews tree. Sibling-dependent layout still works because TopologicalSort in LayoutSubViews already calls Layout on every child in dependency order regardless of NeedsLayout, so siblings that actually reference the changed view via Pos/Dim still recompute their frames. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pagation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…stics Fixes gui-cs#5356. Add tab fan-out layout/draw diagnostic tests
Pin the new SetNeedsLayout contract: changed view + own subtree + adornment subviews + ancestors are marked, but unaffected sibling subtrees stay clean. Three tests currently fail and will pass after the split lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ree invalidation SetNeedsLayout previously did downward subtree marking AND upward propagation via SuperView?.SetNeedsLayout, which re-entered the same downward cascade and re-marked every sibling subtree. In overlapping Tabs this meant activity in the active tab page dirtied every inactive page. Extract the downward subtree pass into MarkSubtreeNeedsLayout, the adornment subview helper into MarkAdornmentSubViewTrees, and the upward walk into MarkAncestorsNeedLayout. The ancestor walk marks each ancestor (and its adornment subview trees) without descending into the ancestor's regular SubViews tree. Sibling-dependent layout still works because TopologicalSort in LayoutSubViews already calls Layout on every child in dependency order regardless of NeedsLayout, so siblings that actually reference the changed view via Pos/Dim still recompute their frames. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pagation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Refines View.SetNeedsLayout () propagation to avoid layout invalidation “fan-out” into unaffected sibling subtrees (notably in Tabs), while keeping redraw behavior for directly-invalidated views and views whose resolved Frame changes.
Changes:
- Split layout invalidation into (1) downward subtree marking for the changed view and (2) ancestor-only upward marking that avoids re-dirtying sibling subtrees.
- Gate “draw-after-layout” so ancestor-only propagation doesn’t automatically trigger redraw work for unaffected peers.
- Add regression tests covering sibling/tab-page non-fan-out and adornment-related propagation paths.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| Terminal.Gui/ViewBase/View.Layout.cs | Implements split subtree vs. ancestor-only layout propagation and redraw-after-layout tracking. |
| Tests/UnitTestsParallelizable/ViewBase/Layout/SetNeedsLayoutPropagationTests.cs | Adds regression tests to lock in the new propagation/redraw behavior (including Tabs and adornment scenarios). |
…n' into fix-5357-split-layout-propagation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs:243
- This doc comment references issue #5356, but these tests are keyed to #5357 (and #4973). Update the issue number to keep the references consistent.
/// <summary>
/// Issue #5356 / #4973 acceptance criterion: diagnostics can show whether inactive tabs
/// received draw work when the active tab scrolls.
/// </summary>
Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs:312
- This doc comment references issue #5356, but the surrounding tests and PR are for #5357. Update the issue number for consistency.
/// <summary>
/// Issue #5356 acceptance criterion: a comparable fan-out metric between an equivalent
/// single-Code scenario and a tabbed-Code scenario.
/// </summary>
|
I ran the tab fan-out layout/draw diagnostic tests added in PR #5364 before and after this PR. Here are the results:
Summary
|
tig
left a comment
There was a problem hiding this comment.
Concerns:
-
_needsDrawAfterLayoutreset logic (warning) - The flag only resets when!NeedsLayoutpost-layout, butLayoutSubViews()can re-setNeedsLayout. This may cause stale draw invalidation on subsequent passes. Consider clearing unconditionally after capturing the local. -
MarkAncestorsNeedLayoutmarks ancestor adornment trees (informational) - This partially reintroduces fan-out into adornment subview trees of uninvolved ancestors. Not a correctness issue but a missed optimization. -
Trailing whitespace on lines 921 and 1001.
The core design is sound. Splitting upward propagation from downward cascade is the right fix for the O(N^2) behavior.
I think this is good to go. Your choice if you want to address the above minor issues found by a coding agent ;-)
→ gui-cs#5357 references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
Terminal.Gui/ViewBase/View.Layout.cs:950
- Avoid an extra allocation in MarkSubtreeNeedsLayout:
Snapshot()already returns an array, soToList()is unnecessary before constructing the Stack.
// TODO: Optimize this - see Setting_Thickness_Causes_Adornment_SubView_Layout
// Use a stack to avoid recursion
Stack<View> stack = new (InternalSubViews.Snapshot ().ToList ());
|
I addressed the two minor issues raised by Tig in #5373 (review) and re-ran CoPilot review here. |
|
Excellent work Kevin. Thank you! Side note: Configuration Editor using |
DrawComplete fires unconditionally for clip-exclusion bookkeeping even when NeedsDraw is false, so it is the wrong signal for measuring whether a view actually did draw work. Add a DrawingText counter to both the diagnostic and integration tests — DrawingText is gated on NeedsDraw inside DoDrawText and is not intercepted by Code (which does intercept ClearedViewport / DrawingContent via OnClearingViewport / OnDrawingContent returning true). Use DrawingText to verify per-Code inactive-tab behavior, and use tabsCounts.ClearedViewport to verify the parent's own behavior. TabsFanOutDiagnosticTests (synthetic Layout/Draw path) now asserts: - inactive tabs have DrawingText == 0 - Tabs container has ClearedViewport == 0 - text-draw fan-out ratio == 1.0 (parity with single-Code baseline) TabsFanOutIntegrationTests (end-to-end via main loop) keeps the existing "documents remaining fan-out" assertion: a separate cascade source remains in ApplicationImpl.LayoutAndDraw (force=true when neededLayout, calling SetNeedsDraw on the top runnable which cascades to overlapping subviews). Removing that force=true uncovers stale-content bugs in the shrink/move path (already covered by ShadowTests / BorderViewTests) and is out of scope for #5358. Layout fan-out is fully eliminated by PR #5373. Refs #5358 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Tighten the #5357 layout invalidation split so ancestor-only propagation does not force unchanged peers into redraw work.
Changes
AdornmentView.SetNeedsLayout ()path used by border arrangement code.Testing
dotnet build --no-restoredotnet test --project Tests/UnitTestsParallelizable --filter-class "*SetNeedsLayoutPropagationTests"dotnet test --project Tests/UnitTestsParallelizable --filter-class "*LayoutTests"dotnet test --project Tests/UnitTestsParallelizable --filter-class "*NeedsDrawTests"dotnet test --project Tests/UnitTestsParallelizable --filter-class "*Tabs*"To pull down this PR locally: