From 8830e0310492d3deb3a02345669737c0f9453414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 31 Mar 2026 13:59:28 +0700 Subject: [PATCH] perf: lower async sort threshold to 1K rows, add display cache eviction --- TablePro/Models/Query/RowProvider.swift | 9 +++++++++ TablePro/Views/Main/Child/MainEditorContentView.swift | 4 ++-- TablePro/Views/Main/MainContentCoordinator.swift | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/TablePro/Models/Query/RowProvider.swift b/TablePro/Models/Query/RowProvider.swift index 8af31183b..fc55807d4 100644 --- a/TablePro/Models/Query/RowProvider.swift +++ b/TablePro/Models/Query/RowProvider.swift @@ -78,7 +78,9 @@ final class InMemoryRowProvider: RowProvider { /// Lazy per-cell cache for formatted display values. /// Keyed by source row index (buffer index or offset appended index). + /// Evicted when exceeding maxDisplayCacheSize to bound memory. private var displayCache: [Int: [String?]] = [:] + private static let maxDisplayCacheSize = 20_000 private(set) var columnDefaults: [String: String?] private(set) var columnTypes: [ColumnType] private(set) var columnForeignKeys: [String: ForeignKeyInfo] @@ -214,9 +216,16 @@ final class InMemoryRowProvider: RowProvider { rowCache[col] = CellDisplayFormatter.format(src[col], columnType: ct) } displayCache[cacheKey] = rowCache + evictDisplayCacheIfNeeded(nearKey: cacheKey) return columnIndex < rowCache.count ? rowCache[columnIndex] : nil } + private func evictDisplayCacheIfNeeded(nearKey: Int) { + guard displayCache.count > Self.maxDisplayCacheSize else { return } + let halfSize = Self.maxDisplayCacheSize / 2 + displayCache = displayCache.filter { abs($0.key - nearKey) <= halfSize } + } + @MainActor func preWarmDisplayCache(upTo rowCount: Int) { let count = min(rowCount, totalRowCount) diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index 9e57e025b..f1907fa4c 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -554,8 +554,8 @@ struct MainEditorContentView: View { return cached.sortedIndices } - // For large datasets sorted async, return nil (unsorted) until cache is ready - if rows.count > 10_000 { + // For datasets sorted async, return nil (unsorted) until cache is ready + if rows.count > 1_000 { return nil } diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index 44b93a6ac..5face82d3 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -1170,8 +1170,8 @@ final class MainContentCoordinator { let sortColumns = currentSort.columns let colTypes = tab.columnTypes - if rows.count > 10_000 { - // Large dataset: sort on background thread to avoid UI freeze + if rows.count > 1_000 { + // Sort on background thread to avoid UI freeze activeSortTasks[tabId]?.cancel() activeSortTasks.removeValue(forKey: tabId) tabManager.tabs[tabIndex].isExecuting = true