---
name: Core power-state probe
type: architecture-map-page
slice: 05-core-storage-hotkey-build
last_verified: 2026/05/09
---

# Core power-state probe

> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core power-state probe

**Plain English summary.** Reports whether the machine is on AC or battery so callers can drop thread counts and skip GPU offload when energy matters more than throughput. Linux uses the documented `/sys/class/power_supply/` ABI. macOS and Windows return `Unknown` for now.

## At a glance

- File: `crates/core/src/power.rs` (328 LOC, 124 of which are tests).
- External deps: standard library only (sysfs is plain text). Tests use `tempfile` (dev-dep).
- Public surface: `PowerState`, `parse_power_state_from_dir`, `probe_power_state`. Plus `with_override` and `force_clear_cache` / `force_set_cache` as `pub(crate)` test helpers.
- Consumers: [`core-tuning.md`](core-tuning.md). The runtime-capabilities banner in slice 2 may also surface the state.

## What's in here

### `PowerState` — `crates/core/src/power.rs:15`

```rust
pub enum PowerState { OnAc, OnBattery, Unknown }
```

`Unknown` is treated as `OnAc` by callers, preserving today's pre-clamp behaviour on platforms where the probe cannot fire.

### `parse_power_state_from_dir` — `crates/core/src/power.rs:40`

Pure function. Walks a `/sys/class/power_supply/`-style directory and applies these rules (matching the kernel's documented sysfs ABI):

1. Any entry with `type` in {`Mains`, `USB`} and `online == 1` → `OnAc`.
2. Else any entry with `type == Battery` → `OnBattery`.
3. Else → `Unknown`.

Top-level failures (missing dir, unreadable supply_dir) return `Unknown` without panicking. Per-entry failures are silently skipped.

### `probe_power_state()` — `crates/core/src/power.rs:115`

Resolution order, highest to lowest priority:

1. **In-process test override** (set via `with_override`). Test-only, never compiled into release builds.
2. **`MAGNOTIA_POWER_STATE_OVERRIDE` env var** — `ac` | `battery` | `unknown`, case-insensitive. Used by the `thread_sweep.rs` integration tests.
3. **Linux:** `parse_power_state_from_dir("/sys/class/power_supply")`.
4. **macOS / Windows / other:** `Unknown`.

Both override paths bypass the cache so tests always see the value they set.

### TTL cache — `crates/core/src/power.rs:75-99, 124-136`

```rust
const POWER_STATE_TTL: Duration = Duration::from_secs(10);
```

Result is cached in a `Mutex<Option<CachedState>>` for 10 seconds. Caching prevents the inference thread-tuning helper from calling sysfs on every inference call (~10 syscalls per probe; called every chunk).

### Test helpers — `crates/core/src/power.rs:88, 93, 188`

- `force_clear_cache()` — `pub(crate)` — drops the cache slot.
- `force_set_cache(state)` — `pub(crate)` — pre-populates the cache slot.
- `with_override<R>(state, body) -> R` — `pub(crate)` — sets `TEST_OVERRIDE` for the duration of `body`. Holds a dedicated `TEST_LOCK` so override-using unit tests run serially even when cargo runs the test binary multi-threaded. `OverrideGuard` resets the override on drop, so a panicking test body cannot leak stale state.

All three helpers are `#[cfg(test)]`-gated and never compiled into release.

## Data flow / contract

- `probe_power_state` is the only public entry point production callers use.
- The cache TTL means a power-source change takes up to 10 s to take effect. Acceptable for thread tuning; not adequate for a UI battery indicator.
- Cache mutex poisoning is treated as a panic via `.expect("power cache mutex poisoned")`. A panicking holder of this mutex is already a bug; the loud failure mode is on purpose.

## Tests

11 tests in `crates/core/src/power.rs:202-327`:

- `power_state_variants_are_distinct` — sanity.
- `parses_mains_online_as_on_ac` / `parses_battery_only_as_on_battery` / `parses_usb_pd_online_as_on_ac` — happy paths.
- `parses_empty_dir_as_unknown` / `parses_missing_dir_as_unknown` / `parses_malformed_files_as_unknown_gracefully` — failure paths.
- `override_drives_battery` / `override_drives_ac` / `override_drives_unknown` — in-process override.
- `env_var_override_battery_via_set_var` — env-var override under the same `TEST_LOCK`.
- `env_var_override_garbage_falls_through` — invalid env values fall through to the platform probe.
- `ttl_cache_returns_cached_value_within_window` / `ttl_cache_clears_via_force_clear` — cache invariants.

## Watch-outs

- **macOS and Windows return `Unknown` always.** Native probes (`IOPSGetProvidingPowerSourceType` on macOS, `GetSystemPowerStatus` on Windows) are deferred. Consumers must treat `Unknown` as `OnAc` or behaviour will silently halve thread counts on every non-Linux machine.
- **Per-entry sysfs read failures are silent.** The `read_trimmed().unwrap_or_default()` pattern means a permission-denied `online` file in a `Mains` entry would read as the empty string and the supply would be skipped. On a stuck-AC laptop where Mains was the unreadable entry, the function would return `OnBattery`. Documented in the function's doc comment at `power.rs:31`. Sysfs entries are world-readable in practice.
- **`TEST_OVERRIDE` is `static Mutex<Option<PowerState>>`.** Process-global, test-only. Production builds do not compile it because of `#[cfg(test)]`.

## See also

- [Inference thread tuning](core-tuning.md) — the only production caller.
- [Hardware probe](core-hardware-probe.md) — sibling.
