Skip to content

Commit dd34dbc

Browse files
desperadoxhyxuhengyudatlechin
authored
perf(welcome): index connection tree to rebuild groups in linear time (#1743)
* perf(welcome): index connection tree to rebuild groups in linear time * refactor(welcome): build the tree index once and drop dead aggregation state --------- Co-authored-by: xuhengyu <xuhengyu@aspirecn.com> Co-authored-by: Ngo Quoc Dat <datlechin@gmail.com>
1 parent e57544a commit dd34dbc

4 files changed

Lines changed: 420 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- The connection Export Options dialog keeps a steady size when you turn on Include Credentials, and saves through the standard macOS save dialog.
1414
- 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.
1515
- Typing in the sidebar table search stays responsive on databases with thousands of tables; filtering runs after a short pause instead of on every keystroke.
16+
- The welcome sidebar rebuilds its connection tree in linear time, so favoriting, moving, or regrouping connections stays responsive with many connections and nested groups.
1617
- Autocomplete ranks each fuzzy candidate once per keystroke instead of three times, keeping the suggestion list snappy on wide SELECT clauses with hundreds of columns.
1718

1819
### Fixed

TablePro/Models/Connection/ConnectionGroupTree.swift

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,237 @@ func connectionCount(in groupId: UUID, connections: [DatabaseConnection], groups
163163
}.count
164164
return directCount + descendantCount
165165
}
166+
167+
// MARK: - Indexed Tree (O(G+C))
168+
169+
struct GroupTreeIndices {
170+
var connectionCountByGroup: [UUID: Int] = [:]
171+
var depthByGroup: [UUID: Int] = [:]
172+
var maxDescendantDepthByGroup: [UUID: Int] = [:]
173+
}
174+
175+
private struct GroupTreeIndex {
176+
let validGroupIds: Set<UUID>
177+
let childrenByParentId: [UUID?: [ConnectionGroup]]
178+
let connectionsByGroupId: [UUID: [DatabaseConnection]]
179+
}
180+
181+
private func sortGroups(_ groups: [ConnectionGroup]) -> [ConnectionGroup] {
182+
groups.sorted {
183+
$0.sortOrder != $1.sortOrder
184+
? $0.sortOrder < $1.sortOrder
185+
: $0.name.localizedStandardCompare($1.name) == .orderedAscending
186+
}
187+
}
188+
189+
private func sortConnections(_ connections: [DatabaseConnection]) -> [DatabaseConnection] {
190+
connections.sorted {
191+
$0.sortOrder != $1.sortOrder
192+
? $0.sortOrder < $1.sortOrder
193+
: $0.name.localizedStandardCompare($1.name) == .orderedAscending
194+
}
195+
}
196+
197+
private func buildGroupTreeIndex(groups: [ConnectionGroup], connections: [DatabaseConnection]) -> GroupTreeIndex {
198+
let validGroupIds = Set(groups.map(\.id))
199+
200+
var childrenByParentId: [UUID?: [ConnectionGroup]] = [:]
201+
for group in groups {
202+
let parentKey = group.parentId.flatMap { validGroupIds.contains($0) ? $0 : nil }
203+
childrenByParentId[parentKey, default: []].append(group)
204+
}
205+
for key in childrenByParentId.keys {
206+
if let levelGroups = childrenByParentId[key] {
207+
childrenByParentId[key] = sortGroups(levelGroups)
208+
}
209+
}
210+
211+
var connectionsByGroupId: [UUID: [DatabaseConnection]] = [:]
212+
for connection in connections {
213+
guard let groupId = connection.groupId, validGroupIds.contains(groupId) else { continue }
214+
connectionsByGroupId[groupId, default: []].append(connection)
215+
}
216+
for groupId in connectionsByGroupId.keys {
217+
if let groupConnections = connectionsByGroupId[groupId] {
218+
connectionsByGroupId[groupId] = sortConnections(groupConnections)
219+
}
220+
}
221+
222+
return GroupTreeIndex(
223+
validGroupIds: validGroupIds,
224+
childrenByParentId: childrenByParentId,
225+
connectionsByGroupId: connectionsByGroupId
226+
)
227+
}
228+
229+
func buildGroupTreeIndexed(
230+
groups: [ConnectionGroup],
231+
connections: [DatabaseConnection],
232+
maxDepth: Int = 3
233+
) -> [ConnectionGroupTreeNode] {
234+
let index = buildGroupTreeIndex(groups: groups, connections: connections)
235+
return buildGroupTreeIndexedLevel(
236+
parentId: nil,
237+
currentDepth: 0,
238+
maxDepth: maxDepth,
239+
index: index,
240+
connections: connections
241+
)
242+
}
243+
244+
private func buildGroupTreeIndexedLevel(
245+
parentId: UUID?,
246+
currentDepth: Int,
247+
maxDepth: Int,
248+
index: GroupTreeIndex,
249+
connections: [DatabaseConnection]
250+
) -> [ConnectionGroupTreeNode] {
251+
var items: [ConnectionGroupTreeNode] = []
252+
let levelGroups = index.childrenByParentId[parentId] ?? []
253+
254+
for group in levelGroups {
255+
var children: [ConnectionGroupTreeNode] = []
256+
if currentDepth < maxDepth {
257+
children = buildGroupTreeIndexedLevel(
258+
parentId: group.id,
259+
currentDepth: currentDepth + 1,
260+
maxDepth: maxDepth,
261+
index: index,
262+
connections: connections
263+
)
264+
}
265+
for conn in index.connectionsByGroupId[group.id] ?? [] {
266+
children.append(.connection(conn))
267+
}
268+
items.append(.group(group, children: children))
269+
}
270+
271+
if parentId == nil {
272+
let ungrouped = sortConnections(connections.filter { conn in
273+
guard let groupId = conn.groupId else { return true }
274+
return !index.validGroupIds.contains(groupId)
275+
})
276+
for conn in ungrouped {
277+
items.append(.connection(conn))
278+
}
279+
}
280+
281+
return items
282+
}
283+
284+
func buildGroupTreeWithIndices(
285+
groups: [ConnectionGroup],
286+
connections: [DatabaseConnection],
287+
maxDepth: Int = 3
288+
) -> (tree: [ConnectionGroupTreeNode], indices: GroupTreeIndices) {
289+
let index = buildGroupTreeIndex(groups: groups, connections: connections)
290+
let tree = buildGroupTreeIndexedLevel(
291+
parentId: nil,
292+
currentDepth: 0,
293+
maxDepth: maxDepth,
294+
index: index,
295+
connections: connections
296+
)
297+
return (tree, computeGroupTreeIndices(from: index, groups: groups, connections: connections))
298+
}
299+
300+
func computeGroupTreeIndices(groups: [ConnectionGroup], connections: [DatabaseConnection]) -> GroupTreeIndices {
301+
computeGroupTreeIndices(
302+
from: buildGroupTreeIndex(groups: groups, connections: connections),
303+
groups: groups,
304+
connections: connections
305+
)
306+
}
307+
308+
private func computeGroupTreeIndices(
309+
from index: GroupTreeIndex,
310+
groups: [ConnectionGroup],
311+
connections: [DatabaseConnection]
312+
) -> GroupTreeIndices {
313+
var result = GroupTreeIndices()
314+
315+
var depthByGroup: [UUID: Int] = [:]
316+
var visitedDepth: Set<UUID> = []
317+
var queue: [(UUID, Int)] = []
318+
let roots = index.childrenByParentId[nil] ?? []
319+
for root in roots where !visitedDepth.contains(root.id) {
320+
visitedDepth.insert(root.id)
321+
depthByGroup[root.id] = 1
322+
queue.append((root.id, 1))
323+
}
324+
var queueIndex = 0
325+
while queueIndex < queue.count {
326+
let (currentId, currentDepth) = queue[queueIndex]
327+
queueIndex += 1
328+
let children = index.childrenByParentId[currentId] ?? []
329+
for child in children where !visitedDepth.contains(child.id) {
330+
visitedDepth.insert(child.id)
331+
depthByGroup[child.id] = currentDepth + 1
332+
queue.append((child.id, currentDepth + 1))
333+
}
334+
}
335+
336+
var maxDepthByGroup: [UUID: Int] = [:]
337+
var connectionCountByGroup: [UUID: Int] = [:]
338+
for root in roots {
339+
_ = aggregateSubtree(
340+
groupId: root.id,
341+
visited: [],
342+
index: index,
343+
maxDepthByGroup: &maxDepthByGroup,
344+
connectionCountByGroup: &connectionCountByGroup
345+
)
346+
}
347+
348+
for group in groups {
349+
result.depthByGroup[group.id] = depthByGroup[group.id]
350+
?? depthOf(groupId: group.id, groups: groups)
351+
result.maxDescendantDepthByGroup[group.id] = maxDepthByGroup[group.id]
352+
?? maxDescendantDepth(groupId: group.id, groups: groups)
353+
result.connectionCountByGroup[group.id] = connectionCountByGroup[group.id]
354+
?? connectionCount(in: group.id, connections: connections, groups: groups)
355+
}
356+
357+
return result
358+
}
359+
360+
private struct SubtreeAggregate {
361+
var maxDescendantDepth: Int
362+
var connectionCount: Int
363+
}
364+
365+
private func aggregateSubtree(
366+
groupId: UUID,
367+
visited: Set<UUID>,
368+
index: GroupTreeIndex,
369+
maxDepthByGroup: inout [UUID: Int],
370+
connectionCountByGroup: inout [UUID: Int]
371+
) -> SubtreeAggregate {
372+
var nextVisited = visited
373+
nextVisited.insert(groupId)
374+
375+
let children = index.childrenByParentId[groupId] ?? []
376+
var maxChildDescendantDepth = 0
377+
var subtreeCount = index.connectionsByGroupId[groupId]?.count ?? 0
378+
379+
for child in children where !visited.contains(child.id) {
380+
let childAggregate = aggregateSubtree(
381+
groupId: child.id,
382+
visited: nextVisited,
383+
index: index,
384+
maxDepthByGroup: &maxDepthByGroup,
385+
connectionCountByGroup: &connectionCountByGroup
386+
)
387+
maxChildDescendantDepth = max(maxChildDescendantDepth, childAggregate.maxDescendantDepth)
388+
subtreeCount += childAggregate.connectionCount
389+
}
390+
391+
let maxDescendantDepthValue = children.isEmpty ? 0 : 1 + maxChildDescendantDepth
392+
let result = SubtreeAggregate(
393+
maxDescendantDepth: maxDescendantDepthValue,
394+
connectionCount: subtreeCount
395+
)
396+
maxDepthByGroup[groupId] = result.maxDescendantDepth
397+
connectionCountByGroup[groupId] = result.connectionCount
398+
return result
399+
}

TablePro/ViewModels/WelcomeViewModel.swift

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ final class WelcomeViewModel {
108108
.filter(\.isFavorite)
109109
.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }
110110

111-
let tree = buildGroupTree(groups: groups, connections: connections, parentId: nil)
111+
let (tree, indices) = buildGroupTreeWithIndices(groups: groups, connections: connections)
112112
let baseItems = searchText.isEmpty ? tree : filterGroupTree(tree, searchText: searchText)
113113
if searchText.isEmpty, !favoriteConnections.isEmpty {
114114
treeItems = baseItems.filter { node in
@@ -119,17 +119,9 @@ final class WelcomeViewModel {
119119
treeItems = baseItems
120120
}
121121

122-
var counts: [UUID: Int] = [:]
123-
var depths: [UUID: Int] = [:]
124-
var descendantDepths: [UUID: Int] = [:]
125-
for group in groups {
126-
counts[group.id] = connectionCount(in: group.id, connections: connections, groups: groups)
127-
depths[group.id] = depthOf(groupId: group.id, groups: groups)
128-
descendantDepths[group.id] = maxDescendantDepth(groupId: group.id, groups: groups)
129-
}
130-
connectionCountByGroup = counts
131-
depthByGroup = depths
132-
maxDescendantDepthByGroup = descendantDepths
122+
connectionCountByGroup = indices.connectionCountByGroup
123+
depthByGroup = indices.depthByGroup
124+
maxDescendantDepthByGroup = indices.maxDescendantDepthByGroup
133125
}
134126

135127
private func scheduleRebuildTree(oldValue: String) {

0 commit comments

Comments
 (0)