diff --git a/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift b/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift index 15e6234..fd27b85 100644 --- a/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift +++ b/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift @@ -188,22 +188,26 @@ public actor HermesDataService { limit: Int, before: Int? = nil ) async -> [HermesMessage] { - let sql: String - let params: [SQLValue] - if let before { - sql = "SELECT \(messageColumns) FROM messages WHERE session_id = ? AND id < ? ORDER BY id DESC LIMIT ?" - params = [.text(sessionId), .integer(Int64(before)), .integer(Int64(limit))] - } else { - sql = "SELECT \(messageColumns) FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?" - params = [.text(sessionId), .integer(Int64(limit))] - } - do { - let rows = try await backend.query(sql, params: params) - // Caller wants chronological (oldest-first) order; the SELECT - // is DESC for the LIMIT to bite the newest rows, so reverse. - return rows.map { messageFromRow($0) }.reversed() - } catch { - return [] + await ScarfMon.measureAsync(.sessionLoad, "mac.fetchMessages") { + let sql: String + let params: [SQLValue] + if let before { + sql = "SELECT \(messageColumns) FROM messages WHERE session_id = ? AND id < ? ORDER BY id DESC LIMIT ?" + params = [.text(sessionId), .integer(Int64(before)), .integer(Int64(limit))] + } else { + sql = "SELECT \(messageColumns) FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?" + params = [.text(sessionId), .integer(Int64(limit))] + } + do { + let rows = try await backend.query(sql, params: params) + // Caller wants chronological (oldest-first) order; the SELECT + // is DESC for the LIMIT to bite the newest rows, so reverse. + let messages = rows.map { messageFromRow($0) }.reversed() as [HermesMessage] + ScarfMon.event(.sessionLoad, "mac.fetchMessages.rows", count: messages.count) + return messages + } catch { + return [] + } } } diff --git a/scarf/Packages/ScarfCore/Sources/ScarfCore/ViewModels/RichChatViewModel.swift b/scarf/Packages/ScarfCore/Sources/ScarfCore/ViewModels/RichChatViewModel.swift index 37feac0..8d6afa3 100644 --- a/scarf/Packages/ScarfCore/Sources/ScarfCore/ViewModels/RichChatViewModel.swift +++ b/scarf/Packages/ScarfCore/Sources/ScarfCore/ViewModels/RichChatViewModel.swift @@ -470,13 +470,15 @@ public final class RichChatViewModel { /// Re-fetch session metadata from DB to pick up cost/token updates. public func refreshSessionFromDB() async { - guard let sessionId else { return } - let opened = await dataService.open() - guard opened else { return } - if let session = await dataService.fetchSession(id: sessionId) { - currentSession = session + await ScarfMon.measureAsync(.sessionLoad, "mac.refreshSessionFromDB") { + guard let sessionId else { return } + let opened = await dataService.open() + guard opened else { return } + if let session = await dataService.fetchSession(id: sessionId) { + currentSession = session + } + await dataService.close() } - await dataService.close() } // MARK: - ACP Event Handling @@ -1015,6 +1017,7 @@ public final class RichChatViewModel { /// Load message history from the DB, optionally combining an origin session /// (e.g., CLI session) with the current ACP session. public func loadSessionHistory(sessionId: String, acpSessionId: String? = nil) async { + await ScarfMon.measureAsync(.sessionLoad, "mac.hydrateMessages") { self.sessionId = sessionId // Force a fresh snapshot pull on remote contexts. An earlier open() // would have cached a stale copy — on resume we need whatever @@ -1100,7 +1103,9 @@ public final class RichChatViewModel { .map(\.id) .min() hasMoreHistory = moreHistory + ScarfMon.event(.sessionLoad, "mac.hydrateMessages.rows", count: messages.count) buildMessageGroups() + } // end measureAsync(.sessionLoad, "mac.hydrateMessages") } // MARK: - Load Earlier (pagination) diff --git a/scarf/Packages/ScarfIOS/Sources/ScarfIOS/IOSDashboardViewModel.swift b/scarf/Packages/ScarfIOS/Sources/ScarfIOS/IOSDashboardViewModel.swift index f1d14f5..b4fea24 100644 --- a/scarf/Packages/ScarfIOS/Sources/ScarfIOS/IOSDashboardViewModel.swift +++ b/scarf/Packages/ScarfIOS/Sources/ScarfIOS/IOSDashboardViewModel.swift @@ -70,10 +70,13 @@ public final class IOSDashboardViewModel { return } - stats = await dataService.fetchStats() - recentSessions = await dataService.fetchSessions(limit: 5) - allSessions = await dataService.fetchSessions(limit: 25) - sessionPreviews = await dataService.fetchSessionPreviews(limit: 25) + await ScarfMon.measureAsync(.sessionLoad, "ios.loadDashboard") { + stats = await dataService.fetchStats() + recentSessions = await dataService.fetchSessions(limit: 5) + allSessions = await dataService.fetchSessions(limit: 25) + sessionPreviews = await dataService.fetchSessionPreviews(limit: 25) + } + ScarfMon.event(.sessionLoad, "ios.allSessions.count", count: allSessions.count) // Attribution lookup (pass-2 UX): load the session→project // sidecar + project registry once so Dashboard rows can show @@ -126,6 +129,7 @@ public final class IOSDashboardViewModel { /// Called from the pull-to-refresh gesture. public func refresh() async { + ScarfMon.event(.sessionLoad, "ios.dashboardRefresh.trigger", count: 1) await load() } } diff --git a/scarf/scarf/Core/Services/HermesFileWatcher.swift b/scarf/scarf/Core/Services/HermesFileWatcher.swift index f71909a..c43bff0 100644 --- a/scarf/scarf/Core/Services/HermesFileWatcher.swift +++ b/scarf/scarf/Core/Services/HermesFileWatcher.swift @@ -76,11 +76,21 @@ final class HermesFileWatcher { /// (Re)start the SSH polling stream over the union of `watchedCorePaths` /// and the current `remoteProjectPaths`. Called on initial start and /// whenever `updateProjectWatches` changes the project set. + /// + /// ScarfMon — `mac.fileWatcher.remoteRestart` (event) fires once per + /// poller restart with `bytes` carrying the path count. Frequent + /// restarts mean the project-list update path is churning; pair + /// with `mac.fileWatcher.remoteTick` from the upstream transport + /// (`ssh.streamScript` / `transport.watchPaths`) to see actual + /// poll cadence. private func startRemotePoller() { remotePollTask?.cancel() - let stream = transport.watchPaths(watchedCorePaths + remoteProjectPaths) + let pathSet = watchedCorePaths + remoteProjectPaths + ScarfMon.event(.transport, "mac.fileWatcher.remoteRestart", count: 1, bytes: pathSet.count) + let stream = transport.watchPaths(pathSet) remotePollTask = Task { [weak self] in for await _ in stream { + ScarfMon.event(.transport, "mac.fileWatcher.remoteDelta", count: 1) await MainActor.run { [weak self] in self?.lastChangeDate = Date() } @@ -146,6 +156,12 @@ final class HermesFileWatcher { queue: .main ) source.setEventHandler { [weak self] in + // ScarfMon — fires every time FSEvents detects a change on + // a watched core or project path. High counts during + // streaming chats are normal (state.db-wal ticks per + // message persisted); high counts when nothing's happening + // suggest a runaway watcher install. + ScarfMon.event(.transport, "mac.fileWatcher.localFire", count: 1) self?.lastChangeDate = Date() } source.setCancelHandler {