Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

// Sized by RAM and never by pressure: dispatch/source.h says not to walk and discard existing caches when pressure rises.
public enum GridDisplayCacheBudget {
public static func unmountedGrids(physicalMemory: UInt64 = ProcessInfo.processInfo.physicalMemory) -> Int {
let gigabyte: UInt64 = 1_073_741_824
switch physicalMemory {
case (32 * gigabyte)...:
return 8
case (16 * gigabyte)...:
return 5
case (8 * gigabyte)...:
return 3
default:
return 2
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Testing

@testable import TableProUtilities

struct GridDisplayCacheBudgetTests {
private static let gigabyte: UInt64 = 1_073_741_824

@Test(
"More memory keeps more unmounted grids formatted",
arguments: [
(UInt64(4) * gigabyte, 2),
(UInt64(8) * gigabyte, 3),
(UInt64(16) * gigabyte, 5),
(UInt64(24) * gigabyte, 5),
(UInt64(32) * gigabyte, 8),
(UInt64(128) * gigabyte, 8),
]
)
func tiers(physicalMemory: UInt64, expected: Int) {
#expect(GridDisplayCacheBudget.unmountedGrids(physicalMemory: physicalMemory) == expected)
}

@Test("Just under a tier stays on the lower one")
func tierBoundary() {
#expect(GridDisplayCacheBudget.unmountedGrids(physicalMemory: 16 * Self.gigabyte - 1) == 3)
}
}

This file was deleted.

2 changes: 1 addition & 1 deletion apple/TablePro/Core/Diagnostics/TabSwitchTrace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal enum TabSwitchTrigger: String, Sendable {
case tabStrip
}

// requery: the memory budget dropped the rows. firstLoad: restored from the last session, never shown since.
// requery: TablePro changed the table since the rows were read. firstLoad: restored from the last session, never shown since.
internal enum TabSwitchRows: String, Sendable {
case resident
case requery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -650,15 +650,8 @@ internal final class MainSplitViewController: NSSplitViewController {
/// Switching workspace repaints the window in place. The rail used to raise a different
/// window instead, which is what made several connections mean several windows.
internal func applySelectedWorkspace() {
/// Switching workspace is this window's key-window change as far as a coordinator is
/// concerned. Only the selected one receives the real `windowDidBecomeKey`, so without
/// this the outgoing connection keeps `isKeyWindow` true and never schedules the eviction
/// that frees its row buffers.
///
/// The outgoing side is read from each coordinator rather than from `lastActiveCoordinator`,
/// which only a switch sets. The window's first connection is made key by the window itself,
/// so the first switch away from it found no cached coordinator to resign: it kept
/// `isKeyWindow` and never scheduled the eviction.
// Only the selected connection gets the real windowDidBecomeKey, so a switch hands key status over itself.
// Each coordinator is asked, not `lastActiveCoordinator`: the window made its first connection key without a switch.
let incoming = workspaces.selected?.sessionState?.coordinator
var handedOffKey = false
for workspace in workspaces.workspaces {
Expand All @@ -668,10 +661,7 @@ internal final class MainSplitViewController: NSSplitViewController {
coordinator.handleWindowDidResignKey()
handedOffKey = true
}
/// Key status is handed over, never invented. A window that is not key can still change its
/// selection, and marking that connection key kept its row eviction from ever being
/// scheduled and let a deferred restore load in a window nobody was looking at. The real
/// `windowDidBecomeKey` reaches whichever connection is selected when the window comes forward.
// Handed over, never invented: marking a connection key in a background window ran its deferred restore load unseen.
if lastActiveCoordinator !== incoming {
if handedOffKey || view.window?.isKeyWindow == true {
incoming?.handleWindowDidBecomeKey()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Foundation
import os
import TableProUtilities

/// The subsystem work a launch has to do but the first window does not need.
///
Expand Down Expand Up @@ -28,8 +27,6 @@ internal enum PostLaunchWork {
hasStarted = true
logger.debug("post-launch work starting")

MemoryPressureAdvisor.startMonitoring()

Task { await PluginManager.shared.sweepPluginSignatures() }
Task { await QueryHistoryManager.shared.performStartupCleanup() }

Expand Down
4 changes: 0 additions & 4 deletions apple/TablePro/Models/Query/QueryTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ struct QueryTab: Identifiable, Equatable {
var schemaVersion: Int
var metadataVersion: Int
var paginationVersion: Int
var loadEpoch: Int = 0

var pendingRestoredSort: [PersistedSortColumn]?
// Held until the first load resolves the columns.
Expand Down Expand Up @@ -155,7 +154,6 @@ struct QueryTab: Identifiable, Equatable {
self.schemaVersion = 0
self.metadataVersion = 0
self.paginationVersion = 0
self.loadEpoch = 0
self.pendingRestoredSort = nil
self.restoredSortSource = .unset
self.restoredPage = nil
Expand Down Expand Up @@ -210,7 +208,6 @@ struct QueryTab: Identifiable, Equatable {
self.schemaVersion = 0
self.metadataVersion = 0
self.paginationVersion = 0
self.loadEpoch = 0
self.pendingRestoredSort = persisted.sortColumns
// A file from before sortSource decodes to its old behaviour: saved columns were the user's, none meant undecided.
self.restoredSortSource = persisted.sortSource
Expand Down Expand Up @@ -387,6 +384,5 @@ struct QueryTab: Identifiable, Equatable {
&& lhs.isPreview == rhs.isPreview
&& lhs.isPinned == rhs.isPinned
&& lhs.hasUserInteraction == rhs.hasUserInteraction
&& lhs.loadEpoch == rhs.loadEpoch
}
}
7 changes: 2 additions & 5 deletions apple/TablePro/Models/Query/TabSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ final class TabSession: ObservableObject, Identifiable {
let id: UUID

@Published var tableRows: TableRows
@Published var isEvicted: Bool

/// Bumped by `TabSessionRegistry` on every mutation of `tableRows`. `TableRows` is a
/// value type with no `Equatable` conformance, so views that derive expensive content
Expand All @@ -48,15 +47,13 @@ final class TabSession: ObservableObject, Identifiable {
var viewportStage: GridViewportStage?

/// Whether the table changed after these rows, or the definition they were read with, were
/// fetched. Unlike eviction the rows stay, so the grid goes on showing them until the reload
/// commits over them, and a tab whose last result was empty can be marked too. Not published:
/// nothing draws it.
/// fetched. The rows stay, so the grid goes on showing them until the reload commits over them,
/// and a tab whose last result was empty can be marked too. Not published: nothing draws it.
var freshness: TableFreshness

init(id: UUID = UUID()) {
self.id = id
self.tableRows = TableRows()
self.isEvicted = false
self.freshness = TableFreshness()
self.dataRevision = 0
self.bufferEpoch = 0
Expand Down
35 changes: 1 addition & 34 deletions apple/TablePro/Models/Query/TabSessionRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,17 @@ final class TabSessionRegistry {
func setTableRows(_ rows: TableRows, for tabId: UUID) {
let session = ensureSession(for: tabId)
session.tableRows = rows
session.isEvicted = false
session.dataRevision &+= 1
session.bufferEpoch &+= 1
session.rowSetRevision &+= 1
}

/// A mutation of what the tab already holds, so it cannot resurrect a tab that holds nothing.
///
/// Phase-2 metadata (foreign keys, defaults, enum values) lands at background priority long
/// after the load that asked for it, and it is keyed on the result rather than on the rows, so
/// it arrives on tabs that were evicted while it was in flight. Clearing `isEvicted` for a
/// mutation that leaves the buffer empty leaves a tab with no rows that `canAutoLoadTableTab`
/// reads as already loaded, and eviction cannot re-mark it because it has nothing left to lose:
/// the grid stays empty until an explicit refresh.
@discardableResult
func updateTableRows(for tabId: UUID, _ mutate: (inout TableRows) -> Delta) -> Delta {
let session = ensureSession(for: tabId)
var rows = session.tableRows
let delta = mutate(&rows)
session.tableRows = rows
if !rows.rows.isEmpty {
session.isEvicted = false
}
session.dataRevision &+= 1
if delta.changesRowSet {
session.rowSetRevision &+= 1
Expand All @@ -73,26 +61,6 @@ final class TabSessionRegistry {
func removeTableRows(for tabId: UUID) {
guard let session = sessions[tabId] else { return }
session.tableRows = TableRows()
session.isEvicted = false
session.dataRevision &+= 1
session.bufferEpoch &+= 1
session.rowSetRevision &+= 1
}

func isEvicted(_ tabId: UUID) -> Bool {
sessions[tabId]?.isEvicted ?? false
}

/// Drops a tab's rows and marks it evicted. Getting them back is the caller's job: SwiftUI's
/// `.task(id:)` keys on `QueryTab.loadEpoch`, so a caller that wants the lazy load to re-fire
/// bumps that itself.
///
/// A tab with no rows is left alone, so eviction never marks a tab that has nothing to lose.
func evict(for tabId: UUID) {
guard let session = sessions[tabId] else { return }
guard !session.tableRows.rows.isEmpty else { return }
session.tableRows.discardRowsKeepingMetadata()
session.isEvicted = true
session.dataRevision &+= 1
session.bufferEpoch &+= 1
session.rowSetRevision &+= 1
Expand All @@ -109,8 +77,7 @@ final class TabSessionRegistry {
}

/// Records that the tab's table changed. Nothing is discarded, and a tab holding no rows is
/// marked all the same: an empty collection that gains its first document is exactly the tab
/// eviction refuses.
/// marked all the same: an empty collection can gain its first document.
func recordChange(_ change: TableFreshness.Change, for tabId: UUID) {
ensureSession(for: tabId).freshness.record(change)
}
Expand Down
6 changes: 6 additions & 0 deletions apple/TablePro/Models/Query/TableLoadTrigger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ internal enum TableLoadTrigger {
case userInitiated
case restore
}

// Keyed by tab: a window-wide trigger was consumed against whatever tab was selected when the connection came back.
internal struct ParkedTableLoad: Equatable {
let tabId: UUID
let trigger: TableLoadTrigger
}
7 changes: 0 additions & 7 deletions apple/TablePro/Models/Query/TableRows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,6 @@ struct TableRows: Sendable {
rowLocators[id]
}

/// Releases the row payload while keeping the schema needed to render and reload the table.
mutating func discardRowsKeepingMetadata() {
rows = []
indexByID = [:]
rowLocators = [:]
}

/// Writes a value into a cell, or with `isAbsent` takes the field out of the row. A value
/// written into a missing field puts the field back.
@discardableResult
Expand Down
11 changes: 1 addition & 10 deletions apple/TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ import SwiftUI
import TableProEditorKit
import TableProPluginKit

// A change to either field cancels the old task and starts a new one, which coalesces rapid Cmd+Number switches.
private struct TabLoadKey: Hashable {
let tabId: UUID?
let loadEpoch: Int
}

// Only a tab showing rows reruns: an error, a plan or a write's count is not a view of the new database's data.
@MainActor
internal enum ContainerChangeRerunPolicy {
Expand Down Expand Up @@ -169,10 +163,7 @@ struct MainEditorContentView: View {
.onAppear {
updateHasQueryText()
}
.task(id: TabLoadKey(
tabId: tabManager.selectedTabId,
loadEpoch: tabManager.selectedTab?.loadEpoch ?? 0
)) {
.task(id: tabManager.selectedTabId) {
coordinator.lazyLoadCurrentTabIfNeeded()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,9 @@ extension MainContentCoordinator {
dataGridEntries[tabId]?.displayInputs = inputs
}

// Each model holds a whole result's formatted text, so only the tabs in use keep theirs.
// The only memory a tab switch releases: formatted text, rebuilt per drawn cell. Rows stay until the user replaces them.
func pruneGridDisplayCaches() {
let budget = MemoryPressureAdvisor.budgetForInactiveTabs() + 1
let budget = GridDisplayCacheBudget.unmountedGrids() + 1
guard dataGridEntries.count > budget else { return }
let ordered = dataGridEntries.sorted { $0.value.lastUsed > $1.value.lastUsed }
for (tabId, entry) in ordered.dropFirst(budget) where !entry.model.isMounted {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extension MainContentCoordinator {
return
}
if isAutoLoad, services.databaseManager.driver(for: connectionId)?.status != .connected {
pendingLoadTrigger = trigger
parkedTableLoad = ParkedTableLoad(tabId: tabId, trigger: trigger)
return
}
handleQueryExecutionError(error, sql: sql, tabId: tabId, connection: conn, serverOutput: serverOutput)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension MainContentCoordinator {
traceConnectUnavailable(traceToken)
guard tabExecution.settle(claim) else { return false }
retireQueryTask(.claim(claim))
pendingLoadTrigger = trigger
parkedTableLoad = ParkedTableLoad(tabId: claim.tabId, trigger: trigger)
declineTableLoad(for: claim.tabId)
return false
}
Expand Down
Loading
Loading