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
/// kicks off a fresh fetch.
public func readCache() -> NousModelsCache? {
ScarfMon.measure(.diskIO, "nous.readCache") {
let transport = context.makeTransport()
guard transport.fileExists(cachePath) else { return nil }
do {
@@ -113,6 +114,7 @@ public struct NousModelCatalogService: Sendable {
return nil
}
}
}
private func writeCache(_ cache: NousModelsCache) {
let transport = context.makeTransport()
@@ -148,6 +150,12 @@ public struct NousModelCatalogService: Sendable {
// The subscription service already checks for `present`; we
// re-read the raw token here because we need the actual string,
// 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()
guard transport.fileExists(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 }
return token
}
}
/// Make the API call. Times out after `requestTimeout` so a hung
/// network doesn't block the picker indefinitely. Returns the raw
/// `[NousModel]` on success, throws on any HTTP / decode error so
/// the caller can log + fall back.
public func fetchModels() async throws -> [NousModel] {
try await ScarfMon.measureAsync(.transport, "nous.fetchModels") {
guard let token = bearerToken() else {
throw NousModelCatalogError.notAuthenticated
}
@@ -182,8 +192,10 @@ public struct NousModelCatalogService: Sendable {
}
struct Envelope: Decodable { let data: [NousModel] }
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
}
}
// MARK: - Public entry
@@ -82,6 +82,7 @@ struct NousSubscriptionService: Sendable {
/// on any read or parse failure callers treat "absent" and "can't
/// read" the same in UI (show a "not subscribed" CTA).
nonisolated func loadState() -> NousSubscriptionState {
ScarfMon.measure(.diskIO, "nous.subscription.loadState") {
guard let data = try? transport.readFile(authJSONPath) else {
return .absent
}
@@ -109,3 +110,4 @@ struct NousSubscriptionService: Sendable {
)
}
}
}