native macOS codings agent orchestrator
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf(shelf): cut sidebar tab-count subscription storm and add signposts

Fast book-switch profiling on the Shelf showed `RepositorySectionView.body`
running ~7400 times/sec under SwiftUI observation tracking — `openTabCount`
inside the body subscribed every section to the entire
`WorktreeTerminalManager.states` dictionary, which churns on any terminal
activity. Combined with `ShelfView.body` rebuilding `worktreeRowSections`
per call, a 25s fast-switching trace produced one Severe Hang (2.02s) plus
11 multi-hundred-ms Hangs.

Changes:
- RepositorySectionView: extract the tab-count badge into a dedicated leaf
view (`RepoHeaderTabCountBadge`) so the parent body never reads
`terminalManager`. Body invocations -92% in the rerun trace.
- ShelfBook.orderedShelfBooks: replace `Dictionary(uniqueKeysWithValues:)`
with `repositories[id:]` and route worktree ordering through
`orderedWorktrees(in:)` to skip per-repo `WorktreeRowSections` model and
Set construction.
- ShelfView: drop the redundant TCA action animation
(`store.send(..., animation:)`) on book open paths — the view-level
`.animation(_:value: openBookID)` already drives the spine flow.
- SupaLogger: add an `OSSignposter` plus `interval`,
`beginInterval`/`endInterval` (token form for `inout`-bound paths), and
`event` helpers — kept always-on since signposts are ~zero-cost when no
Instruments session is attached.
- Instrument hot paths: `WorktreeTerminalState.focusSelectedTab`,
`syncFocus`, `applySurfaceActivity`; `ShelfOpenBookView.onAppear` /
`onChange(selectedTabId)`; `RepositoriesFeature.selectWorktree` /
`selectRepository` reducer cases; `ShelfView` book-click events.

Verified via `make check`, `make build-app`, `make test` (928 passed).

+198 -56
+7
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 10 10 nonisolated let worktreeCreationProgressUpdateStride = 20 11 11 nonisolated let archiveScriptProgressLineLimit = 200 12 12 private let secondsPerDay: Double = 86_400 13 + private let repositoriesLogger = SupaLogger("RepositoriesFeature") 13 14 14 15 nonisolated struct WorktreeCreationProgressUpdateThrottle { 15 16 private let stride: Int ··· 792 793 return .none 793 794 794 795 case .selectRepository(let repositoryID): 796 + // `inout state` cannot be captured by a closure, so use the 797 + // begin/end token API rather than the `interval` helper. 798 + let selectRepoToken = repositoriesLogger.beginInterval("reducer.selectRepository") 799 + defer { repositoriesLogger.endInterval(selectRepoToken) } 795 800 guard let repositoryID, state.repositories[id: repositoryID] != nil else { return .none } 796 801 state.selection = .repository(repositoryID) 797 802 state.sidebarSelectedWorktreeIDs = [] ··· 802 807 return .send(.delegate(.selectedWorktreeChanged(state.selectedTerminalWorktree))) 803 808 804 809 case .selectWorktree(let worktreeID, let focusTerminal): 810 + let selectWtToken = repositoriesLogger.beginInterval("reducer.selectWorktree") 811 + defer { repositoriesLogger.endInterval(selectWtToken) } 805 812 setSingleWorktreeSelection(worktreeID, state: &state) 806 813 if focusTerminal, let worktreeID { 807 814 state.pendingTerminalFocusWorktreeIDs.insert(worktreeID)
+31 -14
supacode/Features/Repositories/Views/RepoHeaderRow.swift
··· 4 4 private static let debugHeaderLayers = false 5 5 let name: String 6 6 let isRemoving: Bool 7 - let tabCount: Int 8 7 /// User-pinned icon, when set. Renders before the repo name. 9 8 /// `nil` keeps the historical text-only layout intact. 10 9 let icon: RepositoryIconSource? ··· 34 33 .font(.caption) 35 34 .foregroundStyle(.tertiary) 36 35 } 37 - if tabCount > 0 { 38 - Text("\(tabCount)") 39 - .font(.caption2) 40 - .monospacedDigit() 41 - .foregroundStyle(.secondary) 42 - .padding(.horizontal, 5) 43 - .padding(.vertical, 1) 44 - .background(.quaternary, in: .capsule) 45 - .help("\(tabCount) active \(tabCount == 1 ? "tab" : "tabs")") 46 - } 47 36 } 48 37 .background { 49 38 if Self.debugHeaderLayers { ··· 58 47 } 59 48 } 60 49 50 + /// Leaf view that renders the open-tab count badge for a repository. 51 + /// 52 + /// Lives in its own `View` so the read of `terminalManager` (an 53 + /// `@Observable` whose `states` dictionary churns whenever terminal 54 + /// activity happens) is isolated to this subtree. Without this split, 55 + /// `RepositorySectionView.body` would subscribe to every change in 56 + /// `terminalManager.states` on every re-evaluation — which under heavy 57 + /// terminal activity caused tens of thousands of body invocations per 58 + /// second across the sidebar. 59 + struct RepoHeaderTabCountBadge: View { 60 + let repository: Repository 61 + let terminalManager: WorktreeTerminalManager 62 + 63 + var body: some View { 64 + let count = RepositorySectionView.openTabCount( 65 + for: repository, 66 + terminalManager: terminalManager 67 + ) 68 + if count > 0 { 69 + Text("\(count)") 70 + .font(.caption2) 71 + .monospacedDigit() 72 + .foregroundStyle(.secondary) 73 + .padding(.horizontal, 5) 74 + .padding(.vertical, 1) 75 + .background(.quaternary, in: .capsule) 76 + .help("\(count) active \(count == 1 ? "tab" : "tabs")") 77 + } 78 + } 79 + } 80 + 61 81 // MARK: - Previews 62 82 63 83 #Preview("RepoHeaderRow") { ··· 65 85 RepoHeaderRow( 66 86 name: "supacode", 67 87 isRemoving: false, 68 - tabCount: 3, 69 88 icon: nil, 70 89 iconTint: nil, 71 90 repositoryRootURL: nil ··· 73 92 RepoHeaderRow( 74 93 name: "ghostty", 75 94 isRemoving: false, 76 - tabCount: 0, 77 95 icon: .sfSymbol("folder.fill"), 78 96 iconTint: .blue, 79 97 repositoryRootURL: URL(fileURLWithPath: "/tmp/ghostty") ··· 81 99 RepoHeaderRow( 82 100 name: "removing-repo", 83 101 isRemoving: true, 84 - tabCount: 1, 85 102 icon: .sfSymbol("hammer.fill"), 86 103 iconTint: .orange, 87 104 repositoryRootURL: URL(fileURLWithPath: "/tmp/removing")
+20 -13
supacode/Features/Repositories/Views/RepositorySectionView.swift
··· 38 38 39 39 let appearance = repositoryAppearances[repository.id] ?? .empty 40 40 let header = HStack { 41 - RepoHeaderRow( 42 - name: repository.name, 43 - isRemoving: isRemovingRepository, 44 - tabCount: Self.openTabCount( 45 - for: repository, 41 + // Inner HStack groups the name row and the tab-count badge so they 42 + // share the leading-aligned region of the outer header. Crucially 43 + // the badge is its own leaf view (`RepoHeaderTabCountBadge`) — it 44 + // owns the `terminalManager` read so this view never subscribes 45 + // to the manager-wide states dictionary. 46 + HStack { 47 + RepoHeaderRow( 48 + name: repository.name, 49 + isRemoving: isRemovingRepository, 50 + icon: appearance.icon, 51 + iconTint: appearance.color?.color, 52 + repositoryRootURL: repository.rootURL, 53 + nameTooltip: repository.capabilities.supportsWorktrees 54 + ? (isExpanded ? "Collapse" : "Expand") 55 + : "Open terminal in folder" 56 + ) 57 + RepoHeaderTabCountBadge( 58 + repository: repository, 46 59 terminalManager: terminalManager 47 - ), 48 - icon: appearance.icon, 49 - iconTint: appearance.color?.color, 50 - repositoryRootURL: repository.rootURL, 51 - nameTooltip: repository.capabilities.supportsWorktrees 52 - ? (isExpanded ? "Collapse" : "Expand") 53 - : "Open terminal in folder" 54 - ) 60 + ) 61 + } 55 62 .frame(maxWidth: .infinity, alignment: .leading) 56 63 .background { 57 64 if Self.debugHeaderLayers {
+35 -7
supacode/Features/Shelf/Models/ShelfBook.swift
··· 1 1 import Foundation 2 + import IdentifiedCollections 2 3 3 4 /// A book on the Shelf — the unified abstraction over a Git worktree or 4 5 /// a plain folder repository. ··· 38 39 /// while in Shelf mode adds its ID here, which causes its spine to 39 40 /// materialize (with the standard spine-flow animation). 40 41 func orderedShelfBooks() -> [ShelfBook] { 41 - let repositoriesByID = Dictionary(uniqueKeysWithValues: repositories.map { ($0.id, $0) }) 42 + // `ShelfView.body` re-runs on every TCA state change, so this method 43 + // is on the per-frame hot path. The previous implementation built a 44 + // `Dictionary(uniqueKeysWithValues:)` per call and routed worktree 45 + // ordering through `worktreeRowSections(in:)` — which constructs a 46 + // full `WorktreeRowModel` per worktree (PR/info lookups, icon 47 + // resolution, etc.) plus several intermediate `Set` allocations per 48 + // repository. None of that detail is needed by the Shelf, which only 49 + // consumes id/name/branch. Use direct `IdentifiedArray` lookup and 50 + // `orderedWorktrees(in:)` for the lighter ordering path. 42 51 var books: [ShelfBook] = [] 43 52 for repositoryID in orderedRepositoryIDs() { 44 - guard let repository = repositoriesByID[repositoryID] else { continue } 53 + guard let repository = repositories[id: repositoryID] else { continue } 45 54 if repository.kind == .plain { 46 55 guard openedWorktreeIDs.contains(repository.id) else { continue } 47 56 books.append( ··· 55 64 )) 56 65 continue 57 66 } 58 - for row in worktreeRows(in: repository) { 59 - guard openedWorktreeIDs.contains(row.id) else { continue } 67 + for worktree in orderedWorktrees(in: repository) 68 + where openedWorktreeIDs.contains(worktree.id) { 60 69 books.append( 61 70 ShelfBook( 62 - id: row.id, 71 + id: worktree.id, 63 72 repositoryID: repositoryID, 64 - displayName: row.name, 73 + displayName: worktree.name, 65 74 projectName: repository.name, 66 - branchName: row.name, 75 + branchName: worktree.name, 76 + kind: .worktree 77 + )) 78 + } 79 + // Preserve prior behavior of `worktreeRowSections` which also 80 + // surfaced any pending (in-creation) worktrees that had been 81 + // marked opened. The list is typically empty so the cost is 82 + // negligible — the win is avoiding `makePendingWorktreeRow` which 83 + // builds a full `WorktreeRowModel` per entry. 84 + for pending in pendingWorktrees 85 + where pending.repositoryID == repositoryID 86 + && openedWorktreeIDs.contains(pending.id) 87 + { 88 + books.append( 89 + ShelfBook( 90 + id: pending.id, 91 + repositoryID: repositoryID, 92 + displayName: pending.progress.titleText, 93 + projectName: repository.name, 94 + branchName: pending.progress.titleText, 67 95 kind: .worktree 68 96 )) 69 97 }
+15 -9
supacode/Features/Shelf/Views/ShelfOpenBookView.swift
··· 1 1 import AppKit 2 2 import SwiftUI 3 3 4 + private let shelfLogger = SupaLogger("Shelf") 5 + 4 6 /// Renders the terminal content for the currently open book. 5 7 /// 6 8 /// Mirrors the terminal-content slice of `WorktreeTerminalTabsView` without ··· 57 59 } 58 60 ) 59 61 .onAppear { 60 - state.ensureInitialTab(focusing: false) 61 - if shouldAutoFocusTerminal { 62 - state.focusSelectedTab() 62 + shelfLogger.interval("OpenBook.onAppear") { 63 + state.ensureInitialTab(focusing: false) 64 + if shouldAutoFocusTerminal { 65 + state.focusSelectedTab() 66 + } 67 + let activity = resolvedWindowActivity 68 + state.syncFocus(windowIsKey: activity.isKeyWindow, windowIsVisible: activity.isVisible) 63 69 } 64 - let activity = resolvedWindowActivity 65 - state.syncFocus(windowIsKey: activity.isKeyWindow, windowIsVisible: activity.isVisible) 66 70 } 67 71 .onChange(of: state.tabManager.selectedTabId) { _, _ in 68 - if shouldAutoFocusTerminal { 69 - state.focusSelectedTab() 72 + shelfLogger.interval("OpenBook.onChange.selectedTabId") { 73 + if shouldAutoFocusTerminal { 74 + state.focusSelectedTab() 75 + } 76 + let activity = resolvedWindowActivity 77 + state.syncFocus(windowIsKey: activity.isKeyWindow, windowIsVisible: activity.isVisible) 70 78 } 71 - let activity = resolvedWindowActivity 72 - state.syncFocus(windowIsKey: activity.isKeyWindow, windowIsVisible: activity.isVisible) 73 79 } 74 80 } 75 81
+24 -7
supacode/Features/Shelf/Views/ShelfView.swift
··· 1 1 import ComposableArchitecture 2 2 import SwiftUI 3 3 4 + private let shelfLogger = SupaLogger("Shelf") 5 + 4 6 /// Root view for Shelf presentation mode. 5 7 /// 6 8 /// Phase 3 layout: three horizontal segments — a left stack of passed ··· 27 29 @Environment(\.surfaceBackgroundOpacity) private var surfaceBackgroundOpacity 28 30 29 31 var body: some View { 32 + // Note: `body` is a `@ViewBuilder` getter so we can't add a 33 + // `defer`-based signpost interval directly here. Body-level cost is 34 + // already visible via the SwiftUI instrument's "View Body" track — 35 + // signposts in this file focus on user-input moments instead. 30 36 let state = store.state 31 37 let books = state.orderedShelfBooks() 32 38 let openBookID = state.openShelfBookID ··· 101 107 102 108 /// Dispatch the open-book action only when `book` isn't already the open 103 109 /// one — idempotent helper for taps that imply a book change. 110 + /// 111 + /// No `animation:` is passed to `store.send` because the visible 112 + /// spine-flow animation is already driven by the view-level 113 + /// `.animation(.easeInOut(duration: 0.2), value: openBookID)` modifier 114 + /// on the root container — wrapping the dispatch in another animation 115 + /// transaction would double-run layout / transition machinery for the 116 + /// same change. 104 117 private func switchToBookIfNeeded(_ book: ShelfBook) { 105 118 guard !isOpen(book) else { return } 119 + shelfLogger.event("BookClick.NewTabSpine") 106 120 switch book.kind { 107 121 case .worktree: 108 - store.send(.selectWorktree(book.id, focusTerminal: true), animation: .easeInOut(duration: 0.2)) 122 + store.send(.selectWorktree(book.id, focusTerminal: true)) 109 123 case .plainFolder: 110 - store.send(.selectRepository(book.repositoryID), animation: .easeInOut(duration: 0.2)) 124 + store.send(.selectRepository(book.repositoryID)) 111 125 } 112 126 } 113 127 ··· 190 204 private func openBook(_ book: ShelfBook, selectingTab tabID: TerminalTabID?) { 191 205 let isAlreadyOpen = store.state.openShelfBookID == book.id 192 206 if let tabID, isAlreadyOpen, let state = terminalManager.stateIfExists(for: book.id) { 207 + shelfLogger.event("BookClick.TabSwitchSameBook") 193 208 state.tabManager.selectTab(tabID) 194 209 return 195 210 } 196 - // Animate the spine flow and terminal crossfade. The duration and 197 - // curve mirror the Shelf design doc: ~200ms ease-in-out, snappy but 198 - // legible so the user can read each spine's movement. 211 + shelfLogger.event("BookClick.SwitchBook") 212 + // The spine flow / terminal crossfade animation is already driven 213 + // by the view-level `.animation(_:value: openBookID)` on the root 214 + // container (~200ms ease-in-out per the Shelf design doc), so the 215 + // dispatch itself does not pass an `animation:` argument here. 199 216 switch book.kind { 200 217 case .worktree: 201 - store.send(.selectWorktree(book.id, focusTerminal: true), animation: .easeInOut(duration: 0.2)) 218 + store.send(.selectWorktree(book.id, focusTerminal: true)) 202 219 case .plainFolder: 203 - store.send(.selectRepository(book.repositoryID), animation: .easeInOut(duration: 0.2)) 220 + store.send(.selectRepository(book.repositoryID)) 204 221 } 205 222 if let tabID { 206 223 // Apply tab selection eagerly; the target book's state already exists
+15 -5
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 315 315 } 316 316 317 317 func focusSelectedTab() { 318 - guard let tabId = tabManager.selectedTabId else { return } 319 - focusSurface(in: tabId) 318 + terminalStateLogger.interval("focusSelectedTab") { 319 + guard let tabId = tabManager.selectedTabId else { return } 320 + focusSurface(in: tabId) 321 + } 320 322 } 321 323 322 324 @discardableResult ··· 345 347 } 346 348 347 349 func syncFocus(windowIsKey: Bool, windowIsVisible: Bool) { 348 - lastWindowIsKey = windowIsKey 349 - lastWindowIsVisible = windowIsVisible 350 - applySurfaceActivity() 350 + terminalStateLogger.interval("syncFocus") { 351 + lastWindowIsKey = windowIsKey 352 + lastWindowIsVisible = windowIsVisible 353 + applySurfaceActivity() 354 + } 351 355 } 352 356 353 357 private func applySurfaceActivity() { 358 + terminalStateLogger.interval("applySurfaceActivity") { 359 + applySurfaceActivityImpl() 360 + } 361 + } 362 + 363 + private func applySurfaceActivityImpl() { 354 364 let selectedTabId = tabManager.selectedTabId 355 365 var surfaceToFocus: GhosttySurfaceView? 356 366 for (tabId, tree) in trees {
+51 -1
supacode/Support/SupaLogger.swift
··· 5 5 #if !DEBUG 6 6 private let logger: Logger 7 7 #endif 8 + /// Signposter for emitting `os_signpost` intervals/events visible in 9 + /// Instruments. Signposts are essentially zero-cost when no Instruments 10 + /// session is attached (a single TLS read), so they are always live — 11 + /// no DEBUG gating. Intervals show up under the logger's category in 12 + /// the os_signpost track of any trace template that includes it 13 + /// (Animation Hitches and SwiftUI both do). 14 + let signposter: OSSignposter 8 15 9 16 init(_ category: String) { 10 17 self.category = category 18 + let subsystem = Bundle.main.bundleIdentifier ?? "com.onevcat.prowl" 11 19 #if !DEBUG 12 - self.logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: category) 20 + self.logger = Logger(subsystem: subsystem, category: category) 13 21 #endif 22 + self.signposter = OSSignposter(subsystem: subsystem, category: category) 14 23 } 15 24 16 25 func debug(_ message: String) { ··· 36 45 logger.warning("\(message, privacy: .public)") 37 46 #endif 38 47 } 48 + 49 + /// Wraps `body` in an `os_signpost` interval named `name`. The 50 + /// interval renders as a labeled bar on the Instruments timeline, 51 + /// making it trivial to correlate hotspots with hangs/hitches without 52 + /// post-processing the trace XML. 53 + func interval<T>(_ name: StaticString, _ body: () throws -> T) rethrows -> T { 54 + let id = signposter.makeSignpostID() 55 + let state = signposter.beginInterval(name, id: id) 56 + defer { signposter.endInterval(name, state) } 57 + return try body() 58 + } 59 + 60 + /// Manual begin/end pair for code paths that can't use the closure 61 + /// form — e.g. inside a TCA reducer case where `inout state` cannot 62 + /// be captured by a non-escaping closure. The returned `IntervalToken` 63 + /// is opaque to callers, so they don't have to import `OSLog` 64 + /// themselves. 65 + func beginInterval(_ name: StaticString) -> IntervalToken { 66 + let id = signposter.makeSignpostID() 67 + let state = signposter.beginInterval(name, id: id) 68 + return IntervalToken(name: name, state: state) 69 + } 70 + 71 + func endInterval(_ token: IntervalToken) { 72 + signposter.endInterval(token.name, token.state) 73 + } 74 + 75 + /// Emits an instantaneous `os_signpost` event marker — useful for 76 + /// marking discrete moments (e.g. "user clicked book") without an 77 + /// associated duration. 78 + func event(_ name: StaticString) { 79 + signposter.emitEvent(name) 80 + } 81 + } 82 + 83 + /// Opaque token bundling a signpost name and its interval state so 84 + /// callers can `beginInterval` / `endInterval` without depending on 85 + /// `OSLog` themselves. 86 + struct IntervalToken { 87 + fileprivate let name: StaticString 88 + fileprivate let state: OSSignpostIntervalState 39 89 }