Skip to content

Commit 7100335

Browse files
Producer GuyProducer Guy
authored andcommitted
Upgrade anomaly logging: two-tier detection (5°C instant + 10°C sustained)
Tier 1: >5°C change between consecutive readings (2 seconds). Catches instant spikes that the 30-second window would miss. Logs with process capture at the exact moment of the spike. Tier 2: >10°C change over 30 seconds (unchanged). Catches gradual sustained changes. Both tiers log: temp change, direction, fan state, active profile, and top 5 processes by CPU at that moment.
1 parent 230a32d commit 7100335

1 file changed

Lines changed: 41 additions & 19 deletions

File tree

Sources/ThermalForgeCore/ThermalMonitor.swift

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,28 +140,50 @@ public final class ThermalMonitor {
140140
let gpuTemp = peakTemp(status, prefixes: ["TG", "Tg"])
141141
let maxTemp = max(cpuTemp, gpuTemp)
142142

143-
// Anomaly detection: log significant temperature changes
144-
anomalyHistory.append(maxTemp)
145-
if anomalyHistory.count > 15 { anomalyHistory.removeFirst() }
146-
if !isCalibrating && anomalyHistory.count >= 15 {
147-
let oldest = anomalyHistory.first!
148-
let delta = maxTemp - oldest
149-
if abs(delta) > 10 {
150-
let direction = delta > 0 ? "spike" : "drop"
151-
let fan0 = status.fans.first
152-
let topProcs = captureTopProcesses()
153-
TFLogger.shared.info(
154-
"Temperature \(direction): \(String(format: "%.1f", oldest))\(String(format: "%.1f", maxTemp))°C " +
155-
"(\(String(format: "%+.1f", delta))°C in 30s) | " +
156-
"Fan0: \(fan0?.actualRPM ?? 0) RPM (\(fan0?.mode ?? "?")) | " +
157-
"Profile: \(activeProfile.name) | " +
158-
"Processes: \(topProcs)"
159-
)
160-
// Reset so we don't log the same event repeatedly
161-
anomalyHistory.removeAll()
143+
// Anomaly detection: two tiers
144+
// Tier 1: instant spike — >5°C between consecutive readings (2 seconds)
145+
// Tier 2: sustained change — >10°C over 30 seconds
146+
if !isCalibrating {
147+
// Tier 1: check against previous reading
148+
if let prevTemp = anomalyHistory.last {
149+
let instantDelta = maxTemp - prevTemp
150+
if abs(instantDelta) > 5 {
151+
let direction = instantDelta > 0 ? "spike" : "drop"
152+
let fan0 = status.fans.first
153+
let topProcs = captureTopProcesses()
154+
TFLogger.shared.info(
155+
"Instant \(direction): \(String(format: "%.1f", prevTemp))\(String(format: "%.1f", maxTemp))°C " +
156+
"(\(String(format: "%+.1f", instantDelta))°C in 2s) | " +
157+
"Fan0: \(fan0?.actualRPM ?? 0) RPM (\(fan0?.mode ?? "?")) | " +
158+
"Profile: \(activeProfile.name) | " +
159+
"Processes: \(topProcs)"
160+
)
161+
}
162+
}
163+
164+
// Tier 2: check over 30-second window
165+
if anomalyHistory.count >= 15 {
166+
let oldest = anomalyHistory.first!
167+
let sustainedDelta = maxTemp - oldest
168+
if abs(sustainedDelta) > 10 {
169+
let direction = sustainedDelta > 0 ? "spike" : "drop"
170+
let fan0 = status.fans.first
171+
let topProcs = captureTopProcesses()
172+
TFLogger.shared.info(
173+
"Sustained \(direction): \(String(format: "%.1f", oldest))\(String(format: "%.1f", maxTemp))°C " +
174+
"(\(String(format: "%+.1f", sustainedDelta))°C in 30s) | " +
175+
"Fan0: \(fan0?.actualRPM ?? 0) RPM (\(fan0?.mode ?? "?")) | " +
176+
"Profile: \(activeProfile.name) | " +
177+
"Processes: \(topProcs)"
178+
)
179+
anomalyHistory.removeAll()
180+
}
162181
}
163182
}
164183

184+
anomalyHistory.append(maxTemp)
185+
if anomalyHistory.count > 15 { anomalyHistory.removeFirst() }
186+
165187
// Safety override: any sensor > 95°C
166188
if maxTemp >= FanProfile.safetyTempThreshold {
167189
if state != .safetyOverride {

0 commit comments

Comments
 (0)