perf(datagrid): draw data cells instead of building a view for each one (#2381) - #2385
Merged
datlechin merged 2 commits intoAug 22, 2026
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
datlechin
merged commit Aug 22, 2026
62bbc44
into
fix/datagrid-wide-result-windowing-2381
8 checks passed
datlechin
added a commit
that referenced
this pull request
Aug 23, 2026
…w per column (#2381) (#2388) * fix(datagrid): keep the column window with the viewport on a wide result (#2381) * fix(datagrid): classify a clicked column by what the result presents * docs(claude-md): record the data grid column window invariants * perf(datagrid): draw data cells instead of building a view for each one (#2381) (#2385) * refactor(datagrid): extract cell geometry, appearance and drawing from the cell view * perf(datagrid): draw data cells instead of building a view for each one (#2381) * perf(datagrid): draw the column separators instead of keeping one view per column (#2381) * fix(datagrid): bound the rows a repaint reloads and pin the separator geometry (#2381) * docs(data-grid): move the cell editor shortcuts to the page that owns them (#2381)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2382, which fixes the column window's arithmetic. This removes the window instead.
Root cause
NSTableColumn.isHiddencosts O(attached columns) per write. AppKit walks every available row view and re-sorts its subviews to rebuild the key view loop, which the profile shows directly:The column window used that write as its virtualization primitive, so windowing a wide result was quadratic by construction. Measured in isolation, hiding the columns outside a 30-column band:
The memory went the same way: the relayout debris of those writes is never reclaimed, which is where a 500-column table's 837MB came from. The cell views themselves are 5.8 KB each and the model side is 4.7 MB.
No AppKit knob helps. All measured, all on the same 500-column table:
autorecalculatesKeyViewLoop = false7,744ms,beginUpdates/endUpdates7,980ms, hiding before any row exists 9,621ms, detaching from the view hierarchy 3,345ms.The change
Every column stays attached and visible, and the grid pays nothing for the ones off screen by not building a view for them.
tableView(_:viewFor:row:)returns nil for every data column, andDataGridRowViewdraws the cells the viewport touches with the CoreText path that was already inside the cell view.Keeping the columns is what makes this small.
rect(ofColumn:),frameOfCell(atColumn:row:),column(at:)andcolumnIndexes(in:)all keep answering, so the overlays, the popovers, drag selection and the header need no new geometry model, and AppKit keeps running the header's resize, reorder and tooltips.Deleted:
ColumnWindowResolver, the two spacer columns,DataGridColumnPool's whole windowing half, andDataGridCellView. 2,013 lines out, 450 in.Measured, in the app
A Debug build in an isolated sandbox, opened on a 500-column SQLite table:
And in a standalone harness at the same workload, so the shape is visible:
Scrolling the full width of the 500-column table, frame by frame, leaves at worst 93px of one column gutter unpainted across 132 captured frames.
Two things this had to rebuild
Per-cell accessibility. AppKit synthesised an AXCell per cell view, and there are no cell views now.
DataGridRowViewvends oneNSAccessibilityElementper data column as its accessibility children, with the role, label, value and row and column index ranges the cell views carried, andKeyHandlingTableViewanswersaccessibilityCell(forColumn:row:)and the selected-cells list from those. They are built when something asks and never on the draw path, because building them there cost a formatted value per column per repaint.Partial reloads.
reloadData(forRowIndexes:columnIndexes:)rebuilt a cell view per pair, which is how a mounted cell was refreshed. It reaches nothing now, so the cell cursor and a committed edit never repainted. The three call sites ask the row views to repaint those cells instead.Verified
verify.sh buildPASSverify.sh testPASS, 116 cases across the 15 affected suitesverify.sh lint TablePro TableProTestsPASS, 0 violationsmouseDown60 samples,handleCellClick11,showOverlayEditor11 across six double clicks, andrebuildAccessibilityCellsdoes not appear. The first editor of a session costs about 40ms more, insideNSTextView.sizeToFitrunning TextKit 2'sNSTextSelectionNavigationclass initialiser, which pulls inNSUserDefaultsand KVO delivery. That is AppKit's own lazy setup, once per process.Not verified by me
VoiceOver. The element tree is built to what
NSAccessibilityElement.handNSAccessibilityProtocols.hdocument, but confirming that VoiceOver announces "cell, row N, column M: value" and navigates by coordinate needs a live VoiceOver session or Accessibility Inspector.