Skip to content

perf(sidebar): scope schema state per connection so large table lists stay responsive#1901

Closed
datlechin wants to merge 4 commits into
mainfrom
fix/sidebar-perf-large-schemas
Closed

perf(sidebar): scope schema state per connection so large table lists stay responsive#1901
datlechin wants to merge 4 commits into
mainfrom
fix/sidebar-perf-large-schemas

Conversation

@datlechin

@datlechin datlechin commented Jul 17, 2026

Copy link
Copy Markdown
Member

Problem

The sidebar and the app lag on connections with many tables. Confirmed with os_signpost instrumentation against a 2000-table SQLite schema (log stream --predicate 'subsystem == "com.TablePro" && category == "SidebarPerf"'):

  • A schema load built ~8,600 TableRow values for a 2,000-table list, roughly 4x the rows. Loading the connection's stored procedures/functions rebuilt the entire table list, because every schema mutation bumped one shared generation that the table cache keyed on.
  • A SchemaService.mutation on one connection re-evaluated the sidebar body of a different connection's window. SchemaService and DatabaseTreeMetadataService kept every connection's state in one @Observable, and Observation tracks per stored property, so any connection's load invalidated every window that had read that property, including MainContentView.

Per-keystroke body passes with unchanged results built 0 rows (~1ms) and were not the cause; the cost is the full-list rebuilds above.

Fix

Scope the two metadata services per connection, following the existing SharedSidebarState / SidebarViewModel registry pattern.

  • SchemaConnectionState (new): one @Observable object per connection holding that connection's schema state, with separate tablesRevision and routinesRevision. SchemaService keeps all its load and supersession logic and becomes a thin forwarder to the per-connection holder.
  • SidebarViewModel keys its table caches on tablesRevision and its routine cache on routinesRevision. A procedures/functions load now bumps only routinesRevision, so the table list's cached array is unchanged and SwiftUI's ForEach skips rebuilding all table rows.
  • DatabaseTreeConnectionState (new): the same split for the tree metadata service, so a background load on one connection no longer redraws MainEditorContentView and the tree in every other window.
  • Per-row cost: the sidebar precomputes favorites as a Set<String> instead of allocating a FavoriteTablesStorage.FavoriteEntry per row, and each row's "show comment" flag is passed down once instead of every TableRow reading AppSettingsManager.shared.

Observation scoping was verified with a standalone probe before implementing: reading a per-connection object through the stateless facade keeps observation scoped to that object (mutating connection B fires 0 notifications for a connection-A reader; bumping routinesRevision fires 0 for a tablesRevision reader).

What this does not change

  • Keeps the SwiftUI List and the native .sidebar source-list appearance. The .listStyle(.sidebar) eager row construction is real but only material past ~2,000 tables, and it is the native look, so it stays.
  • The 0.52.1 search debounce and fingerprint cache are untouched.
  • The tree layout's separate ~400ms filter debounce is a real but separate issue for the non-default tree layout and is left for a follow-up.

Instrumentation

Keeps a small permanent SidebarPerfSignpost (OSSignposter + Logger, subsystem com.TablePro, category SidebarPerf): body evaluations, schema mutations tagged tables vs routines, and the tree refresh interval. Free when nothing is tracing. Verify before/after with:

log stream --predicate 'subsystem == "com.TablePro" && category == "SidebarPerf"' --level debug --style compact

After the fix, a routines load emits SchemaState.routines and no longer rebuilds the table rows, and a load on one connection no longer emits body SidebarView for another connection.

Tests

  • SchemaConnectionStateTests: registry identity and eviction; cross-connection isolation; a routines load bumps only routinesRevision (and a tables load only tablesRevision); reset() preserves instance identity so bound views survive a reconnect.
  • DatabaseTreeConnectionStateTests: the same isolation and lifecycle guarantees for the tree service.
  • Existing SchemaService, DatabaseTreeMetadataService, and SidebarViewModel tests keep their signatures and pass unchanged.

Notes

  • All standalone probe measurements were on macOS 27; the deployment target is macOS 14. The fix reduces invalidation frequency and per-row cost, which helps on every version; nothing here depends on the .listStyle(.sidebar) magnitude.
  • The first two commits add the diagnostics used to find the cause; the last commit is the fix.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca7b2bcd7c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


private func bumpGeneration(_ connectionId: UUID) {
generations[connectionId, default: 0] &+= 1
SidebarPerfSignpost.recordEvent("SchemaService.mutation", connectionId: connectionId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Instrument direct schema invalidations too

Direct invalidate(connectionId:) calls on disconnect/reconnect/database switches remove the observable schema dictionaries without calling bumpGeneration; because the event is only emitted here, those invalidations can still cause the cross-window observation invalidations this probe is meant to diagnose while leaving no SchemaService.mutation entry. Please emit the signpost in invalidate as well, or move the event to a wrapper that covers both bump and reset mutations.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8840b70be8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

internal static let signposter = OSSignposter(subsystem: subsystem, category: "SidebarPerf")
internal static let logger = Logger(subsystem: subsystem, category: "SidebarPerf")

private static let rowConstructions = OSAllocatedUnfairLock(initialState: 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope row construction counts by connection

This counter is process-wide, but recordBodyEvaluation drains it and tags the drained value with whichever connectionId is currently evaluating SidebarView.body. With two open windows/connections, rows constructed for connection A can be consumed by connection B's next body event, so the connection-tagged signposts become misleading exactly in the cross-window invalidation scenario this probe is meant to diagnose. Keep row counts scoped by connection or by an explicit body pass instead of using one global counter.

Useful? React with 👍 / 👎.

@datlechin datlechin changed the title chore(sidebar): add OSLog signposts to diagnose large-schema lag perf(sidebar): scope schema state per connection so large table lists stay responsive Jul 18, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70ad479e08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

isPendingTruncate: context.pendingTruncates.contains(ref.table.name),
isPendingDelete: context.pendingDeletes.contains(ref.table.name)
isPendingDelete: context.pendingDeletes.contains(ref.table.name),
showComment: context.showComment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate comment-setting changes into tree rows

When the database-tree layout is visible and the user toggles General > Object Comments, existing outline rows keep the old value because this showComment flag is captured in DatabaseTreeRowContext during AppKit cell rebuilds. I checked the DatabaseTreeOutlineView/DatabaseTreeOutlineCoordinator.update(from:) path, and that representable does not refresh on AppSettingsManager.general.showObjectComments; before this change each TableRow read AppSettingsManager.shared directly, so its NSHostingView observed the setting. This leaves comments stale until some unrelated tree reload happens.

Useful? React with 👍 / 👎.

@datlechin
datlechin force-pushed the fix/sidebar-perf-large-schemas branch from 094e61e to 0e231fe Compare July 18, 2026 04:43
@datlechin datlechin closed this Jul 18, 2026
@datlechin
datlechin deleted the fix/sidebar-perf-large-schemas branch July 18, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant