Describe the bug
TableView.Columns.Add(...) gets super-linearly slower as the collection grows: building a wide table is cubic in the column count, and the whole cost is paid before the control reaches the visual tree.
Measured on the Uno Skia desktop head (net10.0-desktop, Windows, Release package), 10,000 rows held constant, discarded warm-up, median of 3, timed from construction to first Loaded + one low-priority dispatcher drain:
| Columns |
1.4.1 |
1.5.0-preview2 |
| 25 |
3 ms |
5 ms |
| 75 |
23 ms |
28 ms |
| 175 |
227 ms |
226 ms |
| 350 |
1,927 ms |
1,721 ms |
Doubling the columns from 175 → 350 costs ~8.5× the time — the signature of an O(n³) term. Instrumenting the harness confirms all of it is inside the Columns.Add loop (built in 1393 ms; attaching to window → Loaded at 1398 ms).
Cause
TableViewColumnsCollection.OnVectorChanged calls UpdateFrozenColumns() on every add, and UpdateFrozenColumns does this:
foreach (var column in this.OfType<TableViewColumn>())
{
column.IsFrozen = VisibleColumns.IndexOf(column) < (TableView?.FrozenColumnCount ?? 0);
}
VisibleColumns is not a cached list — it is a property that materialises a fresh filtered + OrderBy-sorted list on every read:
public IList<TableViewColumn> VisibleColumns => [.. this.OfType<TableViewColumn>()
.Where(x => x.Visibility == Visibility.Visible)
.OrderBy(x => x.Order ?? 0)];
So one UpdateFrozenColumns call is n × (build a sorted n-list + IndexOf over it) = O(n² log n), and building an n-column table (n adds) is O(n³ log n). With 350 columns that is on the order of 10⁸ element operations and ~120,000 list allocations for one table.
The same file is byte-identical in v1.4.1, v1.5.0-preview2 and current main, which is why the preview does not change the numbers.
Fix
Resolve VisibleColumns once per call and look each column up in a set. Semantics are unchanged (a non-visible column still has IndexOf == -1 and is still reported as frozen, exactly as today). Same harness afterwards:
| Columns |
before |
after |
| 175 |
194 ms |
7–14 ms |
| 350 |
1,750 ms |
25–46 ms |
(before here is a local Release build of the unpatched v1.4.1 source, to rule out the build itself — it reproduces the package numbers.) Scroll and bulk-edit timings in the same sweep are unaffected. PR to follow.
To Reproduce
var table = new TableView { AutoGenerateColumns = false, ItemsSource = rows };
var sw = Stopwatch.StartNew();
for (var i = 0; i < 350; i++)
table.Columns.Add(new TableViewTextColumn { Header = $"Field_{i}", Binding = new Binding { Path = new PropertyPath($"[Field_{i}]") }, Width = new GridLength(110) });
sw.Stop(); // ~1.7–1.9 s on 1.4.1 / 1.5.0-preview2
Environment
- WinUI.TableView 1.4.1 and 1.5.0-preview2 (also inspected
main)
- Uno.Sdk 6.6.33,
net10.0-desktop (Skia) on Windows 11 — the loop is pure managed code, so the WinAppSDK target will show the same shape
Describe the bug
TableView.Columns.Add(...)gets super-linearly slower as the collection grows: building a wide table is cubic in the column count, and the whole cost is paid before the control reaches the visual tree.Measured on the Uno Skia desktop head (
net10.0-desktop, Windows, Release package), 10,000 rows held constant, discarded warm-up, median of 3, timed from construction to firstLoaded+ one low-priority dispatcher drain:Doubling the columns from 175 → 350 costs ~8.5× the time — the signature of an O(n³) term. Instrumenting the harness confirms all of it is inside the
Columns.Addloop (built in 1393 ms; attaching to window→Loaded at 1398 ms).Cause
TableViewColumnsCollection.OnVectorChangedcallsUpdateFrozenColumns()on every add, andUpdateFrozenColumnsdoes this:VisibleColumnsis not a cached list — it is a property that materialises a fresh filtered +OrderBy-sorted list on every read:So one
UpdateFrozenColumnscall is n × (build a sorted n-list +IndexOfover it) = O(n² log n), and building an n-column table (n adds) is O(n³ log n). With 350 columns that is on the order of 10⁸ element operations and ~120,000 list allocations for one table.The same file is byte-identical in
v1.4.1,v1.5.0-preview2and currentmain, which is why the preview does not change the numbers.Fix
Resolve
VisibleColumnsonce per call and look each column up in a set. Semantics are unchanged (a non-visible column still hasIndexOf == -1and is still reported as frozen, exactly as today). Same harness afterwards:(
beforehere is a local Release build of the unpatchedv1.4.1source, to rule out the build itself — it reproduces the package numbers.) Scroll and bulk-edit timings in the same sweep are unaffected. PR to follow.To Reproduce
Environment
main)net10.0-desktop(Skia) on Windows 11 — the loop is pure managed code, so the WinAppSDK target will show the same shape