Skip to content

Commit 6811e50

Browse files
authored
perf: optimize DataGridView scroll performance (#522)
* perf: optimize DataGridView scroll by pre-computing column metadata and warming display cache * fix: address review — complete cell property guard, prevent division by zero
1 parent 850489c commit 6811e50

5 files changed

Lines changed: 62 additions & 16 deletions

File tree

TablePro/Models/Query/RowProvider.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,23 @@ final class InMemoryRowProvider: RowProvider {
217217
return columnIndex < rowCache.count ? rowCache[columnIndex] : nil
218218
}
219219

220+
@MainActor
221+
func preWarmDisplayCache(upTo rowCount: Int) {
222+
let count = min(rowCount, totalRowCount)
223+
for row in 0..<count {
224+
let cacheKey = resolveCacheKey(for: row)
225+
guard displayCache[cacheKey] == nil else { continue }
226+
let src = sourceRow(at: row)
227+
let columnCount = columns.count
228+
var rowCache = [String?](repeating: nil, count: columnCount)
229+
for col in 0..<min(src.count, columnCount) {
230+
let ct = col < columnTypes.count ? columnTypes[col] : nil
231+
rowCache[col] = CellDisplayFormatter.format(src[col], columnType: ct)
232+
}
233+
displayCache[cacheKey] = rowCache
234+
}
235+
}
236+
220237
/// Invalidate entire display cache (after settings change, full reload).
221238
func invalidateDisplayCache() {
222239
displayCache.removeAll()

TablePro/Views/Results/DataGridCellFactory.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,12 @@ final class DataGridCellFactory {
230230
isNewCell = true
231231
}
232232

233-
// Re-apply single-line properties (editing may reset these on reused cells)
234-
if !isNewCell {
233+
if !isNewCell && (
234+
cell.lineBreakMode != .byTruncatingTail ||
235+
cell.maximumNumberOfLines != 1 ||
236+
cell.cell?.truncatesLastVisibleLine != true ||
237+
cell.cell?.usesSingleLineMode != true
238+
) {
235239
cell.lineBreakMode = .byTruncatingTail
236240
cell.maximumNumberOfLines = 1
237241
cell.cell?.truncatesLastVisibleLine = true

TablePro/Views/Results/DataGridCoordinator.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
9595
var lastReapplyVersion: Int = -1
9696
private(set) var cachedRowCount: Int = 0
9797
private(set) var cachedColumnCount: Int = 0
98+
private(set) var enumOrSetColumns: Set<Int> = []
99+
private(set) var fkColumns: Set<Int> = []
98100
var isSyncingSortDescriptors: Bool = false
99101
/// Suppresses selection delegate callbacks during programmatic selection sync
100102
var isSyncingSelection = false
@@ -263,6 +265,30 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
263265
cachedColumnCount = rowProvider.columns.count
264266
}
265267

268+
func rebuildColumnMetadataCache() {
269+
var enumSet = Set<Int>()
270+
var fkSet = Set<Int>()
271+
let columns = rowProvider.columns
272+
let types = rowProvider.columnTypes
273+
let enumValues = rowProvider.columnEnumValues
274+
let fkKeys = rowProvider.columnForeignKeys
275+
276+
for i in 0..<columns.count {
277+
let name = columns[i]
278+
if i < types.count {
279+
let ct = types[i]
280+
if (ct.isEnumType || ct.isSetType) && enumValues[name]?.isEmpty == false {
281+
enumSet.insert(i)
282+
}
283+
}
284+
if fkKeys[name] != nil {
285+
fkSet.insert(i)
286+
}
287+
}
288+
enumOrSetColumns = enumSet
289+
fkColumns = fkSet
290+
}
291+
266292
// MARK: - Font Updates
267293

268294
/// Update fonts on existing visible cell views in-place.

TablePro/Views/Results/DataGridView.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ struct DataGridView: NSViewRepresentable {
174174
scrollView.documentView = tableView
175175
context.coordinator.tableView = tableView
176176
context.coordinator.onMoveRow = onMoveRow
177+
context.coordinator.rebuildColumnMetadataCache()
177178
if let connectionId {
178179
context.coordinator.observeTeardown(connectionId: connectionId)
179180
}
@@ -280,6 +281,16 @@ struct DataGridView: NSViewRepresentable {
280281
}
281282

282283
coordinator.updateCache()
284+
coordinator.rebuildColumnMetadataCache()
285+
286+
if previousIdentity == nil || previousIdentity?.rowCount == 0 {
287+
let rowH = tableView.rowHeight
288+
if rowH > 0 {
289+
let visibleRows = Int(tableView.visibleRect.height / rowH) + 5
290+
coordinator.rowProvider.preWarmDisplayCache(upTo: visibleRows)
291+
}
292+
}
293+
283294
coordinator.changeManager = changeManager
284295
coordinator.isEditable = isEditable
285296
coordinator.onRefresh = onRefresh

TablePro/Views/Results/Extensions/DataGridView+Columns.swift

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,8 @@ extension TableViewCoordinator {
4343
let isDropdown = dropdownColumns?.contains(columnIndex) == true
4444
let isTypePicker = typePickerColumns?.contains(columnIndex) == true
4545

46-
let isEnumOrSet: Bool = {
47-
guard columnIndex < rowProvider.columnTypes.count,
48-
columnIndex < rowProvider.columns.count else { return false }
49-
let ct = rowProvider.columnTypes[columnIndex]
50-
let columnName = rowProvider.columns[columnIndex]
51-
guard ct.isEnumType || ct.isSetType else { return false }
52-
return rowProvider.columnEnumValues[columnName]?.isEmpty == false
53-
}()
54-
55-
let isFKColumn: Bool = {
56-
guard columnIndex < rowProvider.columns.count else { return false }
57-
let columnName = rowProvider.columns[columnIndex]
58-
return rowProvider.columnForeignKeys[columnName] != nil
59-
}()
46+
let isEnumOrSet = enumOrSetColumns.contains(columnIndex)
47+
let isFKColumn = fkColumns.contains(columnIndex)
6048

6149
return cellFactory.makeDataCell(
6250
tableView: tableView,

0 commit comments

Comments
 (0)