feat(scarfmon): split nous.readCache into fileExists/readFile/decode/bytes

Last perf capture showed nous.readCache as a single 60-second
interval — but the function does three things (transport.fileExists,
transport.readFile, JSONDecoder). Splitting the measure points so
the next capture localizes which step actually owns the wall-clock.

Adds:
- nous.readCache.fileExists (interval) — SSH `test -e` round-trip
- nous.readCache.readFile (interval) — SSH `cat` round-trip
- nous.readCache.bytes (event) — payload size of the cache file
- nous.readCache.decode (interval) — JSON parsing cost

If the next 60-second beach ball localizes to readFile, we know
the cache file is somehow huge or the SSH read is hung; if it's
fileExists, the path resolution is the issue; if decode, we have
malformed JSON. All three wear the same outer wrapper so the
existing nous.readCache total stays for trend comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-05-05 12:07:43 +02:00
parent 20cc3a2985
commit 00a1bbd109
@@ -98,19 +98,36 @@ public struct NousModelCatalogService: Sendable {
public func readCache() -> NousModelsCache? {
ScarfMon.measure(.diskIO, "nous.readCache") {
let transport = context.makeTransport()
guard transport.fileExists(cachePath) else { return nil }
// Split into separate measure points so the next perf
// capture localizes the 60-second observed beach ball
// was it the fileExists probe, the read itself, or
// the JSON decode? Each on its own ScarfMon row.
let exists = ScarfMon.measure(.diskIO, "nous.readCache.fileExists") {
transport.fileExists(cachePath)
}
guard exists else { return nil }
do {
let data = try transport.readFile(cachePath)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let cache = try decoder.decode(NousModelsCache.self, from: data)
guard cache.version == NousModelsCache.currentVersion else {
Self.logger.info("nous models cache schema mismatch (got v\(cache.version), expected v\(NousModelsCache.currentVersion)); ignoring")
return nil
let data = try ScarfMon.measure(.diskIO, "nous.readCache.readFile") {
try transport.readFile(cachePath)
}
ScarfMon.event(.diskIO, "nous.readCache.bytes", count: 1, bytes: data.count)
return ScarfMon.measure(.diskIO, "nous.readCache.decode") {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let cache = try decoder.decode(NousModelsCache.self, from: data)
guard cache.version == NousModelsCache.currentVersion else {
Self.logger.info("nous models cache schema mismatch (got v\(cache.version), expected v\(NousModelsCache.currentVersion)); ignoring")
return Optional<NousModelsCache>.none
}
return cache
} catch {
Self.logger.warning("couldn't decode nous models cache: \(error.localizedDescription, privacy: .public)")
return Optional<NousModelsCache>.none
}
}
return cache
} catch {
Self.logger.warning("couldn't decode nous models cache: \(error.localizedDescription, privacy: .public)")
Self.logger.warning("couldn't read nous models cache: \(error.localizedDescription, privacy: .public)")
return nil
}
}