Compare three approaches to updating a large grid of UI elements in WinUI 3, measuring FPS and memory under identical workloads.
- 80 columns x 60 rows = 4,800 cells
- Each cell displays:
SYMBOL $PRICE(e.g.AAB $142.37) - Cell foreground color: green if price went up, red if it went down
- Fixed cell size: 76 x 22 px, FontSize 10
- Grid is wrapped in a ScrollViewer
Each cell maps to a StockItem:
| Field | Type | Description |
|---|---|---|
| Symbol | string | 3-letter ticker (deterministic) |
| PrevPrice | double | Price before the last update |
| CurrentPrice | double | Current price |
| IsUp | bool | true if CurrentPrice >= PrevPrice |
Symbols are generated deterministically from row/column indices so all three apps display identical data.
- A
DispatcherTimerfires every 33 ms (~30 Hz target). - On each tick, N% of the 4,800 items are randomly mutated (price changes by up to +/-2% of current value, biased slightly upward).
- The UI is updated according to the variant's strategy.
CompositionTarget.Renderingcounts actual composed frames for FPS.
Baseline — minimal overhead:
- Build a WinUI
Gridwith 80 columns / 60 rows at startup. - Store a
TextBlock[]of 4,800 references. - On tick: loop over changed indices, set
TextBlock.TextandTextBlock.Foregrounddirectly.
Standard MVVM pattern:
- Build identical grid at startup.
- Each
TextBlockis bound (Binding,OneWay) to aStockItemViewModel : INotifyPropertyChanged. - ViewModel exposes
DisplayText(string) andPriceBrush(SolidColorBrush). - On tick: set ViewModel properties; the binding engine propagates changes.
Reactor's declarative reconciliation:
- A
ComponentholdsStockItem[]inUseState. Render()produces 4,800Text(...)elements inside aGrid(...).- On tick: call
setData(newArray)— the Reactor reconciler diffs old vs new element trees and patches only changedTextBlockcontrols.
This is the naive Reactor variant: plain fluent API, no memo, no direct-initializer tricks. It is what an unaware user writes and stays that way permanently — it's the framework-level baseline the spec-034 table compares the optimized variant against.
Same workload, same UI, same Component/Render() shape, but the
inner-loop cell construction follows the spec-034 §B/§C idioms:
- Cells are built via direct
new TextBlockElement { … }initializer withModifiers = new ElementModifiers { Layout = …, Visual = … }buckets — bypasses thewith-clone steps the fluent chain produces. UseMemoCellsByIndex(Phase 4 of spec 034) skips re-running the builder for unchanged indices, so a 10% mutation tick allocates ~10% of the cells the naive variant does.
This is the optimized Reactor variant — the reference implementation of the spec-034 perf-tips skill. Do not adopt this shape for ordinary UI; restrict it to identifiable hot loops (lists/grids with hundreds-plus elements per render).
Each app opens a window with:
- Slider (0–100%) controlling the percentage of items updated per tick.
- Start / Stop toggle button.
- FPS and Memory readout (updated every second).
- The stock grid below the controls.
StressPerf.Direct.exe --headless --percent 50 --duration 10
| Flag | Default | Description |
|---|---|---|
--headless |
off | Auto-start, run for duration, print report, exit |
--percent |
10 | Percentage of cells updated per tick |
--duration |
10 | Seconds to run before reporting |
=== StressPerf.Direct ===
Duration: 10.0s
Percent: 50%
Avg FPS: 31.2
Min FPS: 28.4
Max FPS: 33.1
Avg Update: 2.3 ms
Max Update: 5.1 ms
Avg Memory: 142.3 MB
Peak Memory: 158.7 MB
Shared library consumed by all three apps:
| Class | Responsibility |
|---|---|
StockDataSource |
Generates and mutates 4,800 StockItems |
PerfTracker |
FPS counter, update-time recorder, memory sampler |
CliOptions |
Parses --headless, --percent, --duration |
ConsoleHelper |
Attaches parent console for stdout in WinExe builds |
WinUI variants (Direct, Bound, DirectX, Reactor,
ReactorOptimized, ReactorGrid, VirtualList.Reactor) target
net10.0-windows10.0.22621.0 and pick up the repo-wide
WindowsAppSDKVersion from Directory.Build.props. The WPF variant
targets net10.0-windows; PresentTracer is plain net10.0. The
shared scaffold (StressPerf.Shared) is net10.0-windows.
# Build all
dotnet build tests/stress_perf/StressPerf.Direct -c Release -p:Platform=x64
dotnet build tests/stress_perf/StressPerf.Bound -c Release -p:Platform=x64
dotnet build tests/stress_perf/StressPerf.Reactor -c Release -p:Platform=x64
dotnet build tests/stress_perf/StressPerf.ReactorOptimized -c Release -p:Platform=x64
# Run interactive
dotnet run --project tests/stress_perf/StressPerf.Direct -c Release -p:Platform=x64
# Run headless benchmark
dotnet run --project tests/stress_perf/StressPerf.Direct -c Release -p:Platform=x64 -- --headless --percent 50 --duration 10