Skip to content

refactor(datagrid): persistent column pool + typed cell hierarchy#951

Merged
datlechin merged 15 commits intomainfrom
refactor/datagrid-phase2-cells-and-columns
Apr 30, 2026
Merged

refactor(datagrid): persistent column pool + typed cell hierarchy#951
datlechin merged 15 commits intomainfrom
refactor/datagrid-phase2-cells-and-columns

Conversation

@datlechin
Copy link
Copy Markdown
Member

Summary

DataGrid Phase 2: replaces the destructive rebuildColumns and 15-param DataGridCellFactory.makeDataCell mega-cell with:

  • DataGridColumnPool keeping stable NSTableColumn instances so NSTableView's cell reuse pool stays warm across table switches
  • DataGridBaseCellView + 7 typed leaf subclasses (text, foreignKey, dropdown, boolean, date, json, blob) each with their own reuse identifier
  • DataGridCellRegistry resolving kind from column type/FK/dropdown flags and dequeuing the typed cell
  • Slot-based ColumnIdentitySchema (dataColumn-N) with name to slot translation APIs
  • Schema change detection via column names rather than slot identifiers
  • NSAnimationContext + CATransaction.disableActions wrap on reconcile to commit rename and reorder in a single visual frame

Why

CPU spiked to 100% on every table click. Instrumentation traced 87% of viewFor time to per-cell allocation (~0.26 ms/cell). Root cause: rebuildColumns removed and recreated all NSTableColumn instances on every table switch, which discarded all cell views from NSTableView's reuse pool. Every visible cell had to allocate DataGridCellView + CellTextField + 2 NSButtons + 2 SF Symbol lookups + 12 NSLayoutConstraints from scratch.

Results

  • Per-cell render cost: 0.26 ms → 0.10 ms on warm pool
  • viewFor batch for 14 cols x 1000 rows: ~135 ms → 41 ms
  • sectionA makeView/reuse per cell: 0.26 ms → 0.00 ms (reuse hit)
  • CPU spike on table click eliminated

Audit issues addressed

#5 (partial), #7 (prepared, native widgets defer to Phase 2c), #19, #20, #21, #28, #40, #50. See docs/development/datagrid-audit.md.

Out of scope (deferred)

Design doc

docs/development/datagrid-phase2-design.md

Test plan

  • xcodebuild test -only-testing:TableProTests/DataGridColumnPoolTests
  • xcodebuild test -only-testing:TableProTests/DataGridCellRegistryTests
  • xcodebuild test -only-testing:TableProTests/ColumnIdentitySchemaTests
  • swiftlint lint --strict
  • Manual: open many tables, switch rapidly, observe no CPU spike, no header swap, no flicker
  • Manual: reorder columns, close + reopen tab, verify saved order applied without intermediate natural-order frame
  • Manual: column resize, hide/show, sort (single + Shift multi), header divider double-click "Size to Fit"
  • Manual: VoiceOver row/column index range
  • Manual: theme change updates fonts (DataGridFontVariant tag dispatch)

@datlechin datlechin changed the title refactor(datagrid): persistent column pool + typed cell hierarchy (Phase 2) refactor(datagrid): persistent column pool + typed cell hierarchy Apr 30, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36c04cfb35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 214 to 218
let columnsChanged = !latestRows.columns.isEmpty
&& coordinator.lastReconciledColumnNames != latestRows.columns

let isInitialDataLoad = structureChanged && oldRowCount == 0 && !latestRows.columns.isEmpty
let shouldRebuildColumns = columnsChanged || isInitialDataLoad
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile columns when hidden-column state changes

This rebuild gate only checks schema/name changes, so updates to configuration.hiddenColumns do not trigger reconcileColumnPool. In flows like dataGridHideColumn/dataGridShowAllColumns, the column list stays the same, shouldRebuildColumns remains false, and the else branch only updates isEditable, leaving NSTableColumn.isHidden stale until some unrelated schema change happens.

Useful? React with 👍 / 👎.

Comment on lines +131 to +132
for slot in 0..<pooledColumns.count where slot >= visibleCount && !attachedIdentifiers.contains(pooledColumns[slot].identifier) {
tableView.addTableColumn(pooledColumns[slot])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid attaching pooled columns outside visible schema

Adding every pooled slot to tableView.tableColumns (even slots >= visibleCount) introduces hidden phantom columns after switching from a wider table to a narrower one. Because NSTableView.numberOfColumns includes hidden columns, keyboard navigation logic that advances by column index can move focus into these non-schema columns, causing edit/focus behavior to target columns that viewFor later rejects as out of range.

Useful? React with 👍 / 👎.

@datlechin datlechin merged commit 0631570 into main Apr 30, 2026
2 checks passed
@datlechin datlechin deleted the refactor/datagrid-phase2-cells-and-columns branch April 30, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant