perf(datagrid): draw the column separators instead of keeping one view per column (#2381) - #2388
Merged
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
…e-delay # Conflicts: # CHANGELOG.md # CLAUDE.md # TablePro/Views/Results/ColumnWindowResolver.swift # TablePro/Views/Results/DataGridColumnPool.swift # TablePro/Views/Results/DataGridCoordinator.swift # TablePro/Views/Results/Extensions/DataGridView+Sort.swift # TablePro/Views/Results/KeyHandlingTableView.swift # TableProTests/Views/Results/ColumnWindowResolverTests.swift # TableProTests/Views/Results/FocusedColumnResolutionTests.swift
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.
Opening the inline cell editor on a wide result took about a second. This fixes that, and carries PR #2385, which was merged but never reached
main.Why this carries #2385
#2382 was squash-merged to
mainat 18:30:28. #2385 was merged 40 seconds later into its stacked base branch,fix/datagrid-wide-result-windowing-2381, somaingot the column-window arithmetic fix and none of the drawn-cells work. That work survives as62bbc4439and is included here unchanged, plus the fixes below. It cannot land on its own: three of its call sites were left broken, and it makes the separator cost worse by attaching every column.Root cause
DataGridView.swift:78setgridStyleMask = [.solidVerticalGridLineMask]. AppKit implements a vertical grid line as one separatorNSViewper column, held as a direct subview of the table view, and every layout pass re-sorts that whole subview list:An O(n)
containsObject:inside an O(n log n) sort. Adding or removing any subview of the table view invalidates the ordering and forces a pass, and the inline editor adds one on the way in and removes it on the way out.Measured in the app, with an override of
NSTableView.layoutcounting passes and summing wall clock:tableView.subviewsbeginCellEdititself is 3 to 6 ms in every configuration. The cost scales with attached columns, not rows.The change
Only the vertical mask behaves this way.
.solidHorizontalGridLineMaskadds no subviews at all, because AppKit draws a horizontal separator insideNSTableRowView.drawSeparator(in:). So the grid clearsgridStyleMaskand draws the separators itself, andDataGridBodyChromeowns their geometry, thickness and colour exactly asSortableHeaderChromedoes for the header.Where they are drawn is not a free choice, and it was measured rather than assumed: a row view covers whatever the table view drew beneath it, so a line from
drawGrid(inClipRect:)ordrawBackground(inClipRect:)is invisible behind the rows and visible only past the last row.drawBackground(inClipRect:)drawGrid(inClipRect:)So a row paints the separators crossing it, in a second pass after its cells so a modified or find-match tint cannot paint over one, and
KeyHandlingTableView.drawBackground(inClipRect:)paints only the area below the last row. The separator stands at each presented column's leading edge, which is where AppKit put it and which keeps the row-number column's boundary; the boundary comes frompresentsColumnandrect(ofColumn:), never from a fixed step.Re-parenting the editor off the table view was measured as an alternative and rejected: it removes the edit-time cost but leaves 33 ms per scroll step at 500 columns, because the sort runs on every layout pass.
Three regressions from #2385, fixed here
Found by auditing the subsystem, all confirmed against the tree:
view(atColumn:row:makeIfNecessary:false), which is always nil once cells are drawn, so the JSON, blob, PHP, date, enum, set, array, dropdown and type-picker editors all returned early. They now go through onepresentsCell(row:tableColumnIndex:)instead of twelve copies of a guard.reloadData(forRowIndexes:columnIndexes:)rebuilds a cell view, and the row-number column is the only one that still has one. A committed cell edit, an undo, a display-setting change and the find-match highlight all stopped repainting. They go throughrepaintRows(_:), which reloads the row-number column and repaints the drawn cells.CellOverlayBase.applyCellTextLayout(to:), covers both.Measured after the change
Same method as the measurement above: sandboxed Debug build, the same 500-column SQLite table, real CGEvent double clicks,
sampleat 1ms.-[NSTableView _updateVerticalSeparator]-[NSView sortSubviewsUsingFunction:context:]CFSortIndexes-[NSTableView layout]The whole path is gone, not reduced.
tableView.subviewsdrops from 535 to 34 on a 500-column result.The separators are drawn on the same pixels AppKit drew them on. Rasterising the same table in the same window before and after, the vertical transitions at y=300 are identical in both:
The colour is
tableView.gridColor, the same value AppKit was filling with, so dark mode and an appearance change follow it without a second spelling.Verified
verify.sh buildPASSverify.sh testPASS, 52 cases across 7 suitesverify.sh lint TablePro TableProTestsPASS, 0 violationsDataGridBodyChromeTestsmeasures separator geometry againstrect(ofColumn:)and rasterises a row againsttableView.gridColor, so it cannot pass by agreeing with its own arithmetic.DrawnCellReachabilityTestscovers the reachability guard, the repaint path and an out-of-range row.CellOverlayTextLayoutTestscovers the overlay layout on TextKit 2.One measured detail worth knowing when reading
DataGridBodyChrome:rect(ofColumn:)includes the intercell spacing, so a column's separator sits inside the previous column's rect.columnIndexes(in:)returns both columns for that rect, so a partial repaint still redraws it, and the separators go down after the cells so a tint cannot paint over one.partialRepaintRedrawsTheSeparatorInsideItpins that.Two things left open, deliberately
commitTypedCellEditdrops the whole display cache to repaint one cell, whereapplyDelta(.cellChanged)invalidates just that cell. Pre-existing, and narrowing it changes caching semantics, so it is reported rather than changed here.