|
| 1 | +# Profile Redesign Build Plan |
| 2 | + |
| 3 | +## Infrastructure Change: Dual-Cadence Tick |
| 4 | + |
| 5 | +**`ThermalMonitor.swift`** — split the single 2s tick into two cadences: |
| 6 | + |
| 7 | +| Cadence | Interval | What It Does | |
| 8 | +|---|---|---| |
| 9 | +| **Thermal tick** | 100ms | Read temps, calculate curve, apply ramp governor, write fan speed | |
| 10 | +| **Monitor tick** | Every 20th thermal tick (2s) | Process capture, anomaly detection, history logging | |
| 11 | + |
| 12 | +New state: `private var tickCounter: Int = 0` — increments every thermal tick, runs monitor logic when `tickCounter % 20 == 0`. |
| 13 | + |
| 14 | +## Architecture Change: Per-Profile Personality |
| 15 | + |
| 16 | +**`Profile.swift`** — add to `Curve` struct: |
| 17 | + |
| 18 | +```swift |
| 19 | +enum CurveShape { case linear, easeIn, easeOut, sCurve } |
| 20 | + |
| 21 | +let curveShape: CurveShape // how targetPercent maps temp to speed |
| 22 | +let rampUpPerSec: Float // max fan speed increase per second (0.0-1.0) |
| 23 | +let rampDownPerSec: Float // max fan speed decrease per second |
| 24 | +let sustainedTriggerSec: Float // seconds above startTemp before engaging |
| 25 | +let instantEngage: Bool // skip ramp-up entirely (MAX) |
| 26 | +``` |
| 27 | + |
| 28 | +`targetPercent()` updated to apply curve shape: |
| 29 | +- **easeIn**: `pos² * maxRPM` — quiet start, accelerates |
| 30 | +- **linear**: `pos * maxRPM` — current behavior |
| 31 | +- **easeOut**: `√pos * maxRPM` — fast start, levels off |
| 32 | +- **sCurve**: `pos² * (3 - 2*pos) * maxRPM` — smooth both ends |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## Profile Definitions |
| 37 | + |
| 38 | +### Silent (Apple Default) |
| 39 | +No changes. Hands-off monitoring only. Apple controls fans. |
| 40 | + |
| 41 | +### Balanced — "Everyday, keep it quiet" |
| 42 | +| Parameter | Value | Rationale | |
| 43 | +|---|---|---| |
| 44 | +| Stop | 50°C | Same as Apple's observed off range | |
| 45 | +| Start | 55°C | 5°C hysteresis | |
| 46 | +| Ceiling | 70°C | Reaches max fan speed at 70°C | |
| 47 | +| Max fan | 60% | Caps noise | |
| 48 | +| Curve shape | **Ease-in** (`pos²`) | Quiet at low temps, ramps faster as heat builds | |
| 49 | +| Ramp up | ~400 RPM/s | Gentle, not jarring | |
| 50 | +| Ramp down | ~200 RPM/s | Smooth deceleration | |
| 51 | +| Sustained trigger | **8 seconds** | Filters all transients — this profile prioritizes quiet | |
| 52 | + |
| 53 | +Balanced is the "don't bother me" profile. It accepts higher temps in exchange for less fan noise. The ease-in curve means at 60°C you're barely hearing the fans; at 65°C+ they start pulling harder. |
| 54 | + |
| 55 | +### Performance — "Keep it cool, noise is fine" |
| 56 | +| Parameter | Value | Rationale | |
| 57 | +|---|---|---| |
| 58 | +| Stop | 50°C | Unified off threshold | |
| 59 | +| Start | 55°C | Same start, but faster response | |
| 60 | +| Ceiling | 65°C | Reaches max speed 5°C earlier than Balanced | |
| 61 | +| Max fan | 85% | High but not ear-splitting | |
| 62 | +| Curve shape | **Linear** | Direct, proportional, responsive | |
| 63 | +| Ramp up | ~800 RPM/s | 2× Balanced — gets to cooling faster | |
| 64 | +| Ramp down | ~300 RPM/s | Moderate deceleration | |
| 65 | +| Sustained trigger | **4 seconds** | Filters brief spikes but responds to real work quickly | |
| 66 | + |
| 67 | +Performance is for compiles, renders, LLM inference. It doesn't wait around. Linear curve means the cooling response is proportional and predictable. 85% cap because 100% on these fans is loud and the last 15% of RPM gives diminishing thermal returns. |
| 68 | + |
| 69 | +### Max — "Attack dog" |
| 70 | +| Parameter | Value | Rationale | |
| 71 | +|---|---|---| |
| 72 | +| Stop | 50°C | Unified off threshold | |
| 73 | +| Start | **65°C** | Higher start because the response is instant — no need to engage early | |
| 74 | +| Ceiling | N/A | No curve — it's binary | |
| 75 | +| Max fan | 100% | Full send | |
| 76 | +| Curve shape | N/A up / **S-curve down** | Instant on, gentle off | |
| 77 | +| Ramp up | **Instant** | Single reading above 65°C sustained → 100% immediately | |
| 78 | +| Ramp down | ~200 RPM/s with S-curve | Give temps time to stabilize | |
| 79 | +| Sustained trigger | **5 seconds** (50 ticks at 100ms) | Filters transient spikes but still catches real events | |
| 80 | + |
| 81 | +**Up behavior**: `sustainedAboveCount >= 50 && peakTemp >= 65` → instant `setMax()`. No ramp governor. They spike, we spike. |
| 82 | + |
| 83 | +**Down behavior**: Once temp drops below 65°C, S-curve governor ramps down at ~200 RPM/s. Below 50°C with rate-of-change ≤ 0 → fans off. |
| 84 | + |
| 85 | +**Why this fixes the logs**: The Apr 7 22:12 event hit 56°C at tick 1, then 72°C at tick 2. With Option C at 65°C, the 72°C reading (2 seconds later) would have instantly triggered 100% fans. Instead of waiting for safety override at 98°C, fans would have been at 7800+ RPM 26°C earlier. |
| 86 | + |
| 87 | +### Smart — "Proactive adaptive" |
| 88 | +| Parameter | Value | Rationale | |
| 89 | +|---|---|---| |
| 90 | +| Stop | 50°C | Unified off threshold | |
| 91 | +| Start | 53°C | 2°C earlier than others — gets ahead of rising temps | |
| 92 | +| Ceiling | 85°C | Wide range for proportional control | |
| 93 | +| Max fan | 100% | Uncapped when needed | |
| 94 | +| Curve shape | **S-curve** | Smooth across the full range (existing) | |
| 95 | +| Ramp up | Adaptive (rate-of-change based) | Existing logic — faster when temps rising fast | |
| 96 | +| Ramp down | ~200 RPM/s | Smooth deceleration | |
| 97 | +| Sustained trigger | **6 seconds** (60 ticks) | Proactive but filtered | |
| 98 | + |
| 99 | +Smart keeps its existing rate-of-change awareness and calibration data support. The main improvements it gets are the 100ms tick (10× smoother fan transitions) and the non-linear curve shapes. |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +## Files to Modify |
| 104 | + |
| 105 | +| File | Changes | |
| 106 | +|---|---| |
| 107 | +| **Profile.swift** | Add `CurveShape` enum, add ramp/trigger/shape fields to `Curve`, update `targetPercent()` for curve shapes, update all 5 profile definitions, update `Codable` conformance | |
| 108 | +| **ThermalMonitor.swift** | 100ms tick with monitor cadence at 2s, per-profile ramp rates from `Curve`, per-profile sustained trigger from `Curve`, MAX-specific instant engage path, remove hardcoded `maxRampUp`/`maxRampDown`/`sustainedTriggerCount` | |
| 109 | +| **ProfileTests.swift** | Update all threshold/parameter tests, add curve shape tests, add per-profile ramp rate tests, add MAX instant-engage test, add sustained trigger per-profile tests | |
| 110 | +| **MenuBarView.swift** | Update labels — MAX shows "65°C instant", others show start→ceiling range | |
| 111 | + |
| 112 | +## What Will NOT Change |
| 113 | +- Safety override (95°C) — working correctly, stays as-is |
| 114 | +- Anomaly detection — working, just moves to 2s cadence |
| 115 | +- Process capture — working, stays at 2s cadence |
| 116 | +- Smart rate-of-change logic — working, just benefits from faster tick |
| 117 | +- Calibration system — unchanged |
| 118 | +- Silent profile — unchanged |
| 119 | +- Daemon/heartbeat/SMC layer — unchanged |
0 commit comments