Skip to content

Commit 293e020

Browse files
author
xuhengyu
committed
perf(datagrid): serve row count from cache to smooth scrolling
1 parent 07961dc commit 293e020

3 files changed

Lines changed: 133 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Data grid now serves the row count from its existing cache instead of recomputing it on every layout pass, reducing CPU churn while scrolling large result sets.
13+
1014
### Fixed
1115

1216
- Oracle connections no longer crash the app when the server sends a backend message the driver cannot decode; the query fails with a clear error and the connection reconnects. (#483)

TablePro/Views/Results/DataGridCoordinator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
713713
// MARK: - NSTableViewDataSource
714714

715715
func numberOfRows(in tableView: NSTableView) -> Int {
716-
sortedIDs?.count ?? tableRowsProvider().count
716+
sortedIDs?.count ?? cachedRowCount
717717
}
718718
}
719719

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// TableViewCoordinatorRowCountCacheTests.swift
3+
// TableProTests
4+
//
5+
6+
import SwiftUI
7+
import TableProPluginKit
8+
import Testing
9+
10+
@testable import TablePro
11+
12+
@Suite("TableViewCoordinator cachedRowCount sync")
13+
@MainActor
14+
struct TableViewCoordinatorRowCountCacheTests {
15+
private func makeCoordinator(rows: ContiguousArray<Row>) -> TableViewCoordinator {
16+
let coordinator = TableViewCoordinator(
17+
changeManager: AnyChangeManager(DataChangeManager()),
18+
isEditable: true,
19+
selectedRowIndices: .constant([]),
20+
delegate: nil,
21+
layoutPersister: FakeRowCountPersister()
22+
)
23+
var captured = TableRows(rows: rows, columns: ["c"])
24+
coordinator.tableRowsProvider = { captured }
25+
coordinator.tableRowsMutator = { mutation in mutation(&captured) }
26+
coordinator.updateCache()
27+
return coordinator
28+
}
29+
30+
@Test("cachedRowCount tracks provider after initial load")
31+
func cacheMatchesProviderOnLoad() {
32+
let rows: ContiguousArray<Row> = [
33+
Row(id: .existing(0), values: [.text("a")]),
34+
Row(id: .existing(1), values: [.text("b")]),
35+
Row(id: .existing(2), values: [.text("c")]),
36+
]
37+
let coordinator = makeCoordinator(rows: rows)
38+
39+
#expect(coordinator.cachedRowCount == 3)
40+
#expect(coordinator.cachedRowCount == coordinator.tableRowsProvider().count)
41+
}
42+
43+
@Test("updateCache picks up appended rows")
44+
func updateCacheReflectsAppendedRows() {
45+
let coordinator = makeCoordinator(rows: [])
46+
47+
coordinator.tableRowsMutator { rows in
48+
_ = rows.appendInsertedRow(values: [.text("a")])
49+
_ = rows.appendInsertedRow(values: [.text("b")])
50+
}
51+
coordinator.updateCache()
52+
53+
#expect(coordinator.cachedRowCount == 2)
54+
#expect(coordinator.cachedRowCount == coordinator.tableRowsProvider().count)
55+
}
56+
57+
@Test("updateCache picks up removed rows")
58+
func updateCacheReflectsRemovedRows() {
59+
let rows: ContiguousArray<Row> = [
60+
Row(id: .existing(0), values: [.text("a")]),
61+
Row(id: .existing(1), values: [.text("b")]),
62+
Row(id: .existing(2), values: [.text("c")]),
63+
]
64+
let coordinator = makeCoordinator(rows: rows)
65+
66+
coordinator.tableRowsMutator { rows in
67+
_ = rows.remove(rowIDs: [.existing(1)])
68+
}
69+
coordinator.updateCache()
70+
71+
#expect(coordinator.cachedRowCount == 2)
72+
#expect(coordinator.cachedRowCount == coordinator.tableRowsProvider().count)
73+
}
74+
75+
@Test("updateCache reflects full replace")
76+
func updateCacheReflectsFullReplace() {
77+
let rows: ContiguousArray<Row> = [
78+
Row(id: .existing(0), values: [.text("a")]),
79+
]
80+
let coordinator = makeCoordinator(rows: rows)
81+
82+
coordinator.tableRowsMutator { rows in
83+
_ = rows.replace(rows: [[.text("x")], [.text("y")], [.text("z")], [.text("w")]])
84+
}
85+
coordinator.updateCache()
86+
87+
#expect(coordinator.cachedRowCount == 4)
88+
#expect(coordinator.cachedRowCount == coordinator.tableRowsProvider().count)
89+
}
90+
91+
@Test("sortedIDs count takes precedence over cachedRowCount fallback")
92+
func sortedIDsCountPrecedesCache() {
93+
let rows: ContiguousArray<Row> = [
94+
Row(id: .existing(0), values: [.text("a")]),
95+
Row(id: .existing(1), values: [.text("b")]),
96+
Row(id: .existing(2), values: [.text("c")]),
97+
]
98+
let coordinator = makeCoordinator(rows: rows)
99+
100+
coordinator.sortedIDs = [.existing(2), .existing(0)]
101+
coordinator.updateCache()
102+
103+
#expect(coordinator.cachedRowCount == 2)
104+
}
105+
106+
@Test("releaseData zeroes cachedRowCount")
107+
func releaseDataZeroesCache() {
108+
let rows: ContiguousArray<Row> = [
109+
Row(id: .existing(0), values: [.text("a")]),
110+
Row(id: .existing(1), values: [.text("b")]),
111+
]
112+
let coordinator = makeCoordinator(rows: rows)
113+
#expect(coordinator.cachedRowCount == 2)
114+
115+
coordinator.releaseData()
116+
117+
#expect(coordinator.cachedRowCount == 0)
118+
}
119+
}
120+
121+
@MainActor
122+
private final class FakeRowCountPersister: ColumnLayoutPersisting {
123+
func load(for tableName: String, connectionId: UUID) -> ColumnLayoutState? { nil }
124+
125+
func save(_ layout: ColumnLayoutState, for tableName: String, connectionId: UUID) {}
126+
127+
func clear(for tableName: String, connectionId: UUID) {}
128+
}

0 commit comments

Comments
 (0)