Every retained-UI draw re-evaluates every markup-bound column property.
Several of these did real per-row work on EVERY frame instead of returning
a cached field:
- ExcludedComponentIcons (Consumables) called ResolveComponentIcon per row,
which itself calls IAutomation.Items.CaptureOwnedItems() — a live
inventory snapshot — per row per frame.
- All 23 Monsters-grid columns (14 flag columns via MonsterFlagColumn, 7
text columns, 2 move-icon columns) re-ran a LINQ .Select(...).ToArray()
over _combatSettings.Rules every frame; Weapon/Offhand additionally
called ItemDisplayName (another CaptureOwnedItems() scan) per row.
- Meta's MetaStateColumn/MetaConditionColumn/MetaActionColumn/
MetaDeleteColumn/MetaMoveUpIcons/MetaMoveDownIcons and Route's
RouteWaypointCountColumn allocated a fresh array with LINQ every frame.
Fixed by materializing each into a field, computed once at the point of
actual mutation:
- ExcludedComponentIcons: computed in RefreshItemEditors (already the sole
owner of _excludedComponentRows).
- RouteWaypointCountColumn: computed in RefreshRouteEditor (already called
at 13 real mutation points, including profile load).
- Meta's 6 columns: computed in RefreshMetaEditor (already called at every
real mutation point, including profile load).
- Monsters' 23 columns: new RefreshMonsterEditor, wired into the 4 actual
Rules-mutating methods (AddMonsterRuleCore, DeleteMonsterRuleAtCore,
MoveMonsterRuleAtCore, UpdateMonsterActionsAt — the last already covers
every flag toggle and value-cycle action) plus panel construction and
ResetProfileConsumers (profile load/switch). The now-unused
MonsterFlagColumn helper is removed.
New tests (FakeAutomation gained a CaptureOwnedItemsCallCount counter):
ExcludedComponentIconsDoesNotScanLiveInventoryOnEveryRead,
MonsterGridColumnsDoNotScanLiveInventoryOrAllocateOnEveryRead (also asserts
Assert.Same across reads), MetaAndRouteGridColumnsDoNotReallocateOnEveryRead.
All three mutation-checked by temporarily reverting to the old per-read
computation: each turned red (call-count mismatch or Assert.Same failure)
against the reverted code; restoring the cached-field getters turns each
green.
tests/AcDream.Plugins.MossTank.Tests: 674/674 (was 671/671, +3 new tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 278/3 skipped/281 (unchanged — App-layer surface untouched by this item).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>