Skip to content

Columns.Add is cubic in column count: UpdateFrozenColumns re-materialises VisibleColumns per column on every add (350 columns ≈ 1.9 s) #428

Description

@SolidRockProgrammer

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 windowLoaded 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions