mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-08 02:14:37 +00:00
feat(scarfmon): Phase 3a — diagnostic measure points for chat-render bursts
Adds four targeted measure points so the next baseline capture can attribute the bubble-re-render storm and the slow sendPrompt to a specific cause: - mac.RichChatMessageList.body — distinguishes "the parent is re-issuing the ForEach" from "the bubbles are re-rendering on their own". If list.body fires once and bubble.body fires N times, churn is in the bubbles; if list.body fires N times, the ForEach itself is being rebuilt. - finalizeStreamingMessage (interval) — pinpoints the end-of-stream burst trigger. The 20-bubble re-eval burst we saw at the close of each turn lines up with this call; measuring it surfaces whether it's the streaming-id rewrite, the turn-duration assignment, or something downstream. - firstByte / firstThoughtByte (event) — fires once per turn on the first chunk after currentTurnStart is set. Splits user-tap → first byte (network + Hermes thinking, the dominant component of the 7-11s sendPrompt) from first byte → turn end (Scarf streaming render). - loadConfig caller hint via os.Logger — when ScarfMon is in Full mode, logs the first stack frame above each loadConfig call to the com.scarf.mon subsystem so mystery callers (the read at t=264282 with no apparent trigger in the prior baseline) become traceable via `log stream`. Symbol-only, no PII, free outside Full mode. All four are pure additions — no behavior change, same zero-cost default-off semantics as Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -641,11 +641,23 @@ public final class RichChatViewModel {
|
||||
}
|
||||
|
||||
private func appendMessageChunk(text: String) {
|
||||
// ScarfMon "first byte" — fires once per turn, on the first
|
||||
// visible message chunk. Splits "user tap → first byte"
|
||||
// (network + Hermes thinking) from "first byte → turn end"
|
||||
// (streaming + Scarf rendering) so we can attribute slow-feel
|
||||
// bugs to the right side. `bytes` carries the first chunk's
|
||||
// size, not the full turn.
|
||||
if streamingAssistantText.isEmpty && currentTurnStart != nil {
|
||||
ScarfMon.event(.chatStream, "firstByte", count: 1, bytes: text.utf8.count)
|
||||
}
|
||||
streamingAssistantText += text
|
||||
upsertStreamingMessage()
|
||||
}
|
||||
|
||||
private func appendThoughtChunk(text: String) {
|
||||
if streamingThinkingText.isEmpty && currentTurnStart != nil {
|
||||
ScarfMon.event(.chatStream, "firstThoughtByte", count: 1, bytes: text.utf8.count)
|
||||
}
|
||||
streamingThinkingText += text
|
||||
upsertStreamingMessage()
|
||||
}
|
||||
@@ -858,6 +870,12 @@ public final class RichChatViewModel {
|
||||
|
||||
/// Convert the streaming message (id=0) into a permanent message and reset streaming state.
|
||||
private func finalizeStreamingMessage() {
|
||||
ScarfMon.measure(.chatStream, "finalizeStreamingMessage") {
|
||||
_finalizeStreamingMessageImpl()
|
||||
}
|
||||
}
|
||||
|
||||
private func _finalizeStreamingMessageImpl() {
|
||||
guard let idx = messages.firstIndex(where: { $0.id == Self.streamingId }) else { return }
|
||||
|
||||
// Only finalize if there's actual content
|
||||
|
||||
@@ -18,11 +18,24 @@ struct HermesFileService: Sendable {
|
||||
|
||||
nonisolated func loadConfig() -> HermesConfig {
|
||||
ScarfMon.measure(.diskIO, "loadConfig") {
|
||||
// ScarfMon — when Full mode is on, log the first stack
|
||||
// frame above this call to the perf Logger channel so
|
||||
// mystery callers (e.g. config reads with no user action)
|
||||
// can be identified by tailing
|
||||
// `log stream --predicate 'subsystem == "com.scarf.mon"'`.
|
||||
// Symbol-only — no addresses, no PII. Backtrace alloc is
|
||||
// gated on isActive so it's free outside Full mode.
|
||||
if ScarfMon.isActive {
|
||||
let caller = Thread.callStackSymbols.dropFirst(2).first ?? "<unknown>"
|
||||
Self.perfLogger.debug("loadConfig caller: \(caller, privacy: .public)")
|
||||
}
|
||||
guard let content = readFile(context.paths.configYAML) else { return .empty }
|
||||
return parseConfig(content)
|
||||
}
|
||||
}
|
||||
|
||||
private static let perfLogger = Logger(subsystem: "com.scarf.mon", category: "HermesFileService")
|
||||
|
||||
/// Error-surfacing config load. Used by Dashboard to show the user a
|
||||
/// specific reason when config.yaml can't be read on a remote host
|
||||
/// (permission denied, missing file, sqlite3 not installed, etc.)
|
||||
|
||||
@@ -41,7 +41,12 @@ struct RichChatMessageList: View {
|
||||
/// we can reintroduce lazy with a preference-key-based height
|
||||
/// measurement, but that's a much larger change.
|
||||
var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
// ScarfMon — confirms whether the parent re-issues the
|
||||
// ForEach. If this fires once and we still see RichMessageBubble.body
|
||||
// burst N times, churn lives inside the bubbles (or in their inputs).
|
||||
// If this fires N times, the ForEach itself is being rebuilt.
|
||||
let _: Void = ScarfMon.event(.chatRender, "mac.RichChatMessageList.body")
|
||||
return ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if groups.isEmpty && !isWorking {
|
||||
|
||||
Reference in New Issue
Block a user