feat(scarfmon): instrument Nous model catalog + subscription path (beach-ball investigation)

User reported a remote-context beach-ball when opening the model
picker with Nous as the active provider. Existing measure points
showed loadProviders + loadModels at ~315ms each (fast). The
beach-ball must be in the uninstrumented Nous-overlay branch the
picker fires when nous is selected.

Adds four measure points covering every blocking call in that path:

- nous.subscription.loadState (interval, .diskIO) — auth.json read
  via NousSubscriptionService.loadState. Already known to do an SSH
  read; now precisely measurable.
- nous.readCache (interval, .diskIO) — nous_models cache read,
  TWO sequential SSH ops (fileExists + readFile).
- nous.bearerToken (interval, .diskIO) — auth.json read AGAIN inside
  fetchModels. **This is a duplicate read** — loadState already
  parsed the same file moments earlier. Comment-flagged as a
  caching candidate.
- nous.fetchModels (interval, .transport) + .bytes (event) — HTTP
  GET against the Nous /v1/models endpoint with the body byte count
  attached. The most likely beach-ball culprit if the endpoint is
  slow or hung.

After the next capture we'll know which of the four owns the user's
wall-clock; if `nous.bearerToken` shows up alongside
`nous.subscription.loadState` with similar duration, the duplicate
read is also a real cost worth fixing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-05-05 11:50:51 +02:00
parent 4efd84c119
commit 099d73dde8
2 changed files with 77 additions and 63 deletions
@@ -96,6 +96,7 @@ public struct NousModelCatalogService: Sendable {
/// malformed cache nil; the loader treats that as "no cache" and /// malformed cache nil; the loader treats that as "no cache" and
/// kicks off a fresh fetch. /// kicks off a fresh fetch.
public func readCache() -> NousModelsCache? { public func readCache() -> NousModelsCache? {
ScarfMon.measure(.diskIO, "nous.readCache") {
let transport = context.makeTransport() let transport = context.makeTransport()
guard transport.fileExists(cachePath) else { return nil } guard transport.fileExists(cachePath) else { return nil }
do { do {
@@ -113,6 +114,7 @@ public struct NousModelCatalogService: Sendable {
return nil return nil
} }
} }
}
private func writeCache(_ cache: NousModelsCache) { private func writeCache(_ cache: NousModelsCache) {
let transport = context.makeTransport() let transport = context.makeTransport()
@@ -148,6 +150,12 @@ public struct NousModelCatalogService: Sendable {
// The subscription service already checks for `present`; we // The subscription service already checks for `present`; we
// re-read the raw token here because we need the actual string, // re-read the raw token here because we need the actual string,
// not just a Bool. Mirrors the SubscriptionService parse path. // not just a Bool. Mirrors the SubscriptionService parse path.
// ScarfMon: separate `nous.bearerToken` measure point because
// this is the second auth.json read of the picker's open
// sequence (subscriptionService.loadState() did the first).
// Together with `nous.subscription.loadState`, total two SSH
// round-trips of the same file candidate for caching.
ScarfMon.measure(.diskIO, "nous.bearerToken") {
let transport = context.makeTransport() let transport = context.makeTransport()
guard transport.fileExists(context.paths.authJSON) else { return nil } guard transport.fileExists(context.paths.authJSON) else { return nil }
guard let data = try? transport.readFile(context.paths.authJSON) else { return nil } guard let data = try? transport.readFile(context.paths.authJSON) else { return nil }
@@ -158,12 +166,14 @@ public struct NousModelCatalogService: Sendable {
guard let token, !token.isEmpty else { return nil } guard let token, !token.isEmpty else { return nil }
return token return token
} }
}
/// Make the API call. Times out after `requestTimeout` so a hung /// Make the API call. Times out after `requestTimeout` so a hung
/// network doesn't block the picker indefinitely. Returns the raw /// network doesn't block the picker indefinitely. Returns the raw
/// `[NousModel]` on success, throws on any HTTP / decode error so /// `[NousModel]` on success, throws on any HTTP / decode error so
/// the caller can log + fall back. /// the caller can log + fall back.
public func fetchModels() async throws -> [NousModel] { public func fetchModels() async throws -> [NousModel] {
try await ScarfMon.measureAsync(.transport, "nous.fetchModels") {
guard let token = bearerToken() else { guard let token = bearerToken() else {
throw NousModelCatalogError.notAuthenticated throw NousModelCatalogError.notAuthenticated
} }
@@ -182,8 +192,10 @@ public struct NousModelCatalogService: Sendable {
} }
struct Envelope: Decodable { let data: [NousModel] } struct Envelope: Decodable { let data: [NousModel] }
let envelope = try JSONDecoder().decode(Envelope.self, from: data) let envelope = try JSONDecoder().decode(Envelope.self, from: data)
ScarfMon.event(.transport, "nous.fetchModels.bytes", count: envelope.data.count, bytes: data.count)
return envelope.data return envelope.data
} }
}
// MARK: - Public entry // MARK: - Public entry
@@ -82,6 +82,7 @@ struct NousSubscriptionService: Sendable {
/// on any read or parse failure callers treat "absent" and "can't /// on any read or parse failure callers treat "absent" and "can't
/// read" the same in UI (show a "not subscribed" CTA). /// read" the same in UI (show a "not subscribed" CTA).
nonisolated func loadState() -> NousSubscriptionState { nonisolated func loadState() -> NousSubscriptionState {
ScarfMon.measure(.diskIO, "nous.subscription.loadState") {
guard let data = try? transport.readFile(authJSONPath) else { guard let data = try? transport.readFile(authJSONPath) else {
return .absent return .absent
} }
@@ -109,3 +110,4 @@ struct NousSubscriptionService: Sendable {
) )
} }
} }
}