Skip to content

perf: recycle transposed grid columns in place (kill per-scroll subtree rebuild) - #138

Merged
mrcsin merged 19 commits into
masterfrom
transposed-recycle-in-place-panel
Jul 17, 2026
Merged

perf: recycle transposed grid columns in place (kill per-scroll subtree rebuild)#138
mrcsin merged 19 commits into
masterfrom
transposed-recycle-in-place-panel

Conversation

@mrcsin

@mrcsin mrcsin commented Jul 17, 2026

Copy link
Copy Markdown
Member

Makes the transposed recipe grid scroll and change step-quantity without rebuilding or re-attaching column subtrees. Completes the two-part virtualization fix in one logical change.

Problem

On a large recipe (~2100 steps), scrolling the transposed grid churned GC hard. A cpu trace showed MeasureOverride = 71% of the UI thread, dominated by TransposedColumnsPanel.Realize re-running OnAttachedToVisualTreeCore / StyleBase.Attach / ApplyStyling / CreateCompositionVisual on every viewport crossing. Root cause (verified against Avalonia 12.0.5 source): the container's ContentPresenter tore down and rebuilt the ~115-element column subtree on every recycle, because ItemContainerGenerator.ClearItemContainer clears Content+ContentTemplate and the typed (x:DataType) DataTemplate can't match the intermediate null.

Fix (two complementary halves)

  1. Recycle-in-place panelTransposedColumnsPanel : VirtualizingPanel keeps containers attached (IsVisible=false + idle stack, no RemoveInternalChild on scroll). Uniform ColumnWidth makes viewport math exact. Adds focus-anchor deferral, exact OnItemsChanged, ScrollIntoView/GetControl.
  2. Child recycleTransposedStepListBox : ListBox overrides ClearContainerForItemOverride (skip base, clear only IsSelected) and PrepareContainerForItemOverride (re-point Content explicitly), plus StyleKeyOverride => typeof(ListBox). The ContentPresenter now recycles its child in place; a scroll reuse is just a DataContext re-point.

Measured result (headless acceptance-gate trace, fixed workload)

Metric Before After
attach/styling inclusive 131,587 ms 1,181 ms (−99.1%)
MeasureOverride inclusive 129,354 ms 24,529 ms (−81%)
host re-attach / round-trip ~36 0
viewport-jump allocation 1,008,701 B/col 235,399 B/col (4.3× lower)

Gate #2's absolute MeasureOverride bar missed by 1.8% — a documented frame-overlap artifact in how the target was derived (the baseline attach sum nests partly outside MeasureOverride), not a fix shortfall. Visually confirmed smooth on the live Release build. A retention probe shows a flat post-GC memory floor on both the transposed and canonical grids, so the live scroll sawtooth is normal transient GC, not a leak.

Tests

Recycle-correctness (child identity, multi-select survival, no selection bleed, reset, commit-on-clearing, focus relocation), panel contract/layout/items-changed/scroll suites, an env-gated headless cpu-trace acceptance gate + speedscope analyzer, and allocation/host-reattach/retention probes. Full suite green.

Follow-ups (separate PRs)

  • Delete the now-dead TransposedColumnCellsHost + pool indirection (the Host no longer detaches on scroll).
  • Display-text TextLayout/SKFont cache for the residual render-thread text-shaping cost.

🤖 Generated with Claude Code

mrcsin and others added 19 commits July 16, 2026 10:30
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Env-gated (SEMISTEP_PROBE=1) probe measuring post-full-GC managed-memory
floor across a long scroll workload for both the transposed and canonical
recipe grids, to determine whether scrolling accumulates retained objects.
Reports per-round-trip retained bytes; both surfaces measure flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mrcsin
mrcsin merged commit 4a4d5f7 into master Jul 17, 2026
1 check passed
@mrcsin
mrcsin deleted the transposed-recycle-in-place-panel branch July 17, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant