commit afd967245f8b5ff626d547ea218180f364f39f16
parent 4ce3988c737c98c74b88adf13db65886bb65c215
Author: Michael Camilleri <[email protected]>
Date: Sun, 7 Jun 2026 00:36:34 +0900
Retain diagnostics events by time, not count
EventLog was a fixed 1000-entry ring trimmed by count. During an active
co-solve the log can churn at ~5 events/sec which would mean that a
buffer would only hold ~3 minutes.
This commit switches to a 60-minute time window as the governing limit
so that the buffer always reaches back a known wall-clock span
regardless of how chatty the session is. The entry count cap stays only
as a runaway guard (raised to 30k) so a tight error-retry loop can't
grow the buffer without bound inside the window.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat:
1 file changed, 30 insertions(+), 2 deletions(-)
diff --git a/Crossmate/Services/DebuggingMonitors.swift b/Crossmate/Services/DebuggingMonitors.swift
@@ -189,14 +189,42 @@ struct EventLogEntry: Identifiable, Sendable {
final class EventLog {
private(set) var entries: [EventLogEntry] = []
- private let maxEntries = 1000
+ /// How far back the log reaches. A time window — rather than a fixed entry
+ /// count — guarantees the buffer always spans a known wall-clock duration
+ /// regardless of how chatty the session is. During an intense co-solve the
+ /// log churns at ~5 events/sec, so a count cap of a few thousand only holds
+ /// a couple of minutes and evicts the start of the session before anyone
+ /// can grab it. An hour comfortably covers a full shared solve plus its
+ /// opening minutes.
+ private let retention: TimeInterval = 60 * 60
+
+ /// Hard ceiling so a runaway producer (e.g. a tight error-retry loop) can't
+ /// grow the buffer without bound inside the retention window. Sized well
+ /// above an hour of normal+intense traffic (~18k entries) so the time
+ /// window stays the governing limit in practice; this only trips on a fault.
+ private let maxEntries = 30_000
func note(_ message: String, level: String = "info") {
append(level: level, message)
}
fileprivate func append(level: String, _ message: String) {
- entries.append(EventLogEntry(timestamp: Date(), level: level, message: message))
+ let now = Date()
+ entries.append(EventLogEntry(timestamp: now, level: level, message: message))
+ prune(now: now)
+ }
+
+ /// Drops entries older than the retention window, then enforces the count
+ /// ceiling. Entries are appended in timestamp order, so the expired ones are
+ /// always a prefix — `firstIndex` stops at the first fresh entry, making the
+ /// scan proportional to how many just aged out (typically zero or one per
+ /// append), not to the buffer size. The entry just appended carries `now`,
+ /// so at least one entry always survives the cutoff.
+ private func prune(now: Date) {
+ let cutoff = now.addingTimeInterval(-retention)
+ if let firstFresh = entries.firstIndex(where: { $0.timestamp >= cutoff }), firstFresh > 0 {
+ entries.removeFirst(firstFresh)
+ }
if entries.count > maxEntries {
entries.removeFirst(entries.count - maxEntries)
}