mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
perf(sessions): fold sessions+previews into one batched SSH round-trip
Audit Finding 1 — ChatViewModel.loadRecentSessions and SessionsViewModel.load each fired two sequential `await dataService.fetch*` calls (sessions + previews), paying the 420 ms SSH RTT twice on every reload. Visible in ScarfMon traces as back-to-back `ssh.run` intervals, totaling ~840 ms minimum overhead per sidebar refresh. Adds HermesDataService.sessionListSnapshot(limit:) — same shape as the existing dashboardSnapshot, folds both queries into a single backend.queryBatch() call. Both call sites switched. Halves the SSH round-trips for every sidebar load. With Finding 5's coalescing, redundant parallel reloads also become free. Together, the 9× redundant queries-per-minute observed in baseline captures should drop substantially. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -605,6 +605,60 @@ public actor HermesDataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bundle for the chat sidebar / Sessions tab loaders. Folds
|
||||||
|
/// `fetchSessions(limit:)` + `fetchSessionPreviews(limit:)` into
|
||||||
|
/// one `queryBatch()` round-trip — same shape as
|
||||||
|
/// `dashboardSnapshot`. Pre-fix `ChatViewModel.loadRecentSessions`
|
||||||
|
/// + `SessionsViewModel.load` each fired the two `await
|
||||||
|
/// dataService.fetch*` calls in serial, paying the SSH RTT
|
||||||
|
/// twice (~840 ms minimum on a 420 ms-RTT remote, observed in
|
||||||
|
/// ScarfMon `mac.loadRecentSessions` traces). Halves the
|
||||||
|
/// round-trips for every sidebar load. Each tick still pays
|
||||||
|
/// for `dashboard.loadRegistry` separately because that's a
|
||||||
|
/// projects.json read (not SQL) and goes through a different
|
||||||
|
/// transport call.
|
||||||
|
public struct SessionListSnapshot: Sendable {
|
||||||
|
public let sessions: [HermesSession]
|
||||||
|
public let previews: [String: String]
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sessionListSnapshot(limit: Int = QueryDefaults.sessionLimit) async -> SessionListSnapshot {
|
||||||
|
let previewLimit = limit
|
||||||
|
let statements: [(sql: String, params: [SQLValue])] = [
|
||||||
|
(
|
||||||
|
"SELECT \(sessionColumns) FROM sessions WHERE parent_session_id IS NULL ORDER BY started_at DESC LIMIT ?",
|
||||||
|
[.integer(Int64(limit))]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
SELECT m.session_id, substr(m.content, 1, \(QueryDefaults.previewContentLength))
|
||||||
|
FROM messages m
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT session_id, MIN(id) as min_id
|
||||||
|
FROM messages
|
||||||
|
WHERE role = 'user' AND content <> ''
|
||||||
|
GROUP BY session_id
|
||||||
|
) first ON m.id = first.min_id
|
||||||
|
ORDER BY m.timestamp DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
[.integer(Int64(previewLimit))]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
do {
|
||||||
|
let resultSets = try await backend.queryBatch(statements)
|
||||||
|
let sessions = (resultSets.first ?? []).map { sessionFromRow($0) }
|
||||||
|
var previews: [String: String] = [:]
|
||||||
|
for row in (resultSets.count > 1 ? resultSets[1] : []) {
|
||||||
|
previews[row.string(at: 0)] = row.string(at: 1)
|
||||||
|
}
|
||||||
|
return SessionListSnapshot(sessions: sessions, previews: previews)
|
||||||
|
} catch {
|
||||||
|
Self.logger.warning("sessionListSnapshot failed: \(error.localizedDescription, privacy: .public)")
|
||||||
|
return SessionListSnapshot(sessions: [], previews: [:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Bundle the queries Insights fires on every load into one
|
/// Bundle the queries Insights fires on every load into one
|
||||||
/// backend round-trip — same rationale as `dashboardSnapshot`.
|
/// backend round-trip — same rationale as `dashboardSnapshot`.
|
||||||
public struct InsightsSnapshot: Sendable {
|
public struct InsightsSnapshot: Sendable {
|
||||||
|
|||||||
@@ -947,8 +947,16 @@ final class ChatViewModel {
|
|||||||
// getting truncated out of the original limit). Sessions feature
|
// getting truncated out of the original limit). Sessions feature
|
||||||
// loads 500; the chat sidebar doesn't need that, but 50 keeps
|
// loads 500; the chat sidebar doesn't need that, but 50 keeps
|
||||||
// the project filter useful without measurable cost.
|
// the project filter useful without measurable cost.
|
||||||
let fetchedSessions = await dataService.fetchSessions(limit: 50)
|
//
|
||||||
let fetchedPreviews = await dataService.fetchSessionPreviews(limit: 50)
|
// v2.7: folded sessions + previews into one queryBatch round
|
||||||
|
// trip via sessionListSnapshot. Pre-fix the two awaits below
|
||||||
|
// were serialized SSH calls, paying the 420 ms RTT twice
|
||||||
|
// every time the file watcher fired (~2.2 s baseline reload).
|
||||||
|
// sessionListSnapshot halves the round-trips for every
|
||||||
|
// sidebar refresh.
|
||||||
|
let snapshot = await dataService.sessionListSnapshot(limit: 50)
|
||||||
|
let fetchedSessions = snapshot.sessions
|
||||||
|
let fetchedPreviews = snapshot.previews
|
||||||
await dataService.close()
|
await dataService.close()
|
||||||
|
|
||||||
// Project attribution + registry — single batched off-main read.
|
// Project attribution + registry — single batched off-main read.
|
||||||
|
|||||||
@@ -77,8 +77,13 @@ final class SessionsViewModel {
|
|||||||
// re-opening — cleanup() closes on disappear.
|
// re-opening — cleanup() closes on disappear.
|
||||||
let opened = await dataService.refresh()
|
let opened = await dataService.refresh()
|
||||||
guard opened else { return }
|
guard opened else { return }
|
||||||
sessions = await dataService.fetchSessions(limit: 500)
|
// v2.7: folded the two serial fetches into one batched round
|
||||||
sessionPreviews = await dataService.fetchSessionPreviews(limit: 500)
|
// trip via sessionListSnapshot. Pre-fix this paid the 420 ms
|
||||||
|
// SSH RTT twice on every Sessions tab open (~840 ms minimum
|
||||||
|
// for the two queries alone over remote).
|
||||||
|
let snapshot = await dataService.sessionListSnapshot(limit: 500)
|
||||||
|
sessions = snapshot.sessions
|
||||||
|
sessionPreviews = snapshot.previews
|
||||||
|
|
||||||
// Load attribution + registry off the main actor in one batch so
|
// Load attribution + registry off the main actor in one batch so
|
||||||
// 500 rows don't trigger 500 SFTP reads. Failure is silent — the
|
// 500 rows don't trigger 500 SFTP reads. Failure is silent — the
|
||||||
|
|||||||
Reference in New Issue
Block a user