You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(core): wire the ioPriority setting to real OS thread priorities (#170)
## What
`global.io_priority` (SPEC s22, values `normal` | `low` | `idle`) has
been in
the settings UI and seeded to `"low"` by migration 0002 for a while, but
it was
wired to nothing: `settings.rs` validated and stored it, `dtos.rs`
carried it,
and the backend never read it. This makes it real.
Motivation (from Max): "will heavy I/O apps fight with driven? ... i
want it to
be one notch below normal so it isn't fighting applications but not so
low pri
that nothing happens ... best-effort for best-cross-platform is good, as
long as
it's fail-working not fail-erroring."
## The new module: `crates/driven-core/src/priority.rs`
- `WorkPriority` (`Normal` / `Low` / `Idle`) with `from_setting`, which
degrades
an unknown string to `Normal` rather than erroring.
- `PriorityCell` - an `Arc<AtomicU8>` holding the live level.
- `begin_background_work(p) -> PriorityGuard`, an RAII guard that
restores the
calling thread on drop, plus `apply_to_current_thread(p)` (one-shot, no
restore) and `spawn_blocking(p, f)` for the common case.
Everything is **best-effort**: no `Result`s, no panics, no propagation.
A
refused OS call logs at `debug` and the work runs at normal priority.
The guard records **what it actually applied**, not what it intended, so
a
refused call never produces a bogus restore
(`THREAD_MODE_BACKGROUND_END`
without a matching successful begin fails with
`ERROR_THREAD_MODE_NOT_BACKGROUND`).
## Per-OS mapping
| | Windows | Linux | macOS |
|---|---|---|---|
| `Low` | `THREAD_PRIORITY_BELOW_NORMAL` (CPU only) | `ioprio_set`
best-effort prio 6 (I/O only) | `IOPOL_UTILITY` disk policy (I/O only) |
| `Idle` | `THREAD_MODE_BACKGROUND_BEGIN` (CPU + I/O + memory) |
`ioprio_set` `IOPRIO_CLASS_IDLE` (I/O only) | `IOPOL_THROTTLE` +
`PRIO_DARWIN_BG` (CPU + I/O) |
Two gaps are deliberate, and documented in the module:
- **Windows `Low` lowers CPU only.** The only documented per-thread I/O
hint is
background mode, which is all-or-nothing - it floors CPU, I/O *and*
memory
priority together. That is the `Idle` behaviour, so `Low` gets the CPU
notch
alone, which is exactly "one notch below normal".
- **Linux does not touch the nice value inside a guard.** `setpriority`
is a
one-way ratchet for an unprivileged process: raising the nice value
succeeds,
lowering it back needs `CAP_SYS_NICE` or a raised `RLIMIT_NICE` (soft
limit 0
on most distros). Inside a guard that would leave a pooled
`spawn_blocking`
thread deniced for the rest of the process's life. Only `ioprio_set`
(which
resets cleanly with `ioprio = 0`) runs in a guard; the nice bump is
reserved
for `apply_to_current_thread`, which makes no restore promise.
All constants and signatures were verified against current docs
([SetThreadPriority](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriority),
[ioprio_set(2)](https://man7.org/linux/man-pages/man2/ioprio_set.2.html),
[setiopolicy_np(3)](https://keith.github.io/xcode-man-pages/setiopolicy_np.3.html),
XNU `sys/resource.h`), not from memory. Windows uses local `extern
"system"`
declarations, matching the scanner's `FindFirstStreamW` ADS probe rather
than
pulling in the `windows` crate; unix uses `libc` (added to workspace
deps under
a `cfg(unix)` target section).
## Where the guard is applied, and why only there
A `PriorityGuard` is only sound where the thread cannot yield - inside a
`spawn_blocking` closure or a `fn` with no `.await`. Holding one across
an
`.await` would demote whatever thread the task resumed on and leak the
demotion
on the one it left. **`PriorityGuard` is deliberately `!Send`**, so that
mistake
is a compile error inside any spawned task rather than a runtime
mystery.
That narrows the apply surface to exactly one site on the backup path
today:
the executor's `build_bundle` `spawn_blocking` (reads every member off
disk and
gzips it). Deliberately **not** applied:
- The executor's `cpu_stage` / `read_hash_encrypt` / `stream_upload`
pipeline -
these interleave `.await`s, and toggling per 64-KiB chunk would cost a
syscall per chunk for a demotion the thread does not keep anyway.
- `restore.rs`, `exclusion_stream.rs`, `sources.rs` blocking work - all
user-initiated and in the foreground. A throttled restore or a sluggish
exclusion preview is a regression, not a feature.
- `scanner.rs` - untouched on purpose, since
`feat/scan-parallel-pruning` is
rewriting its walk internals into parallel workers. Those dedicated,
Driven-owned worker threads are the natural consumer of
`apply_to_current_thread` (including the Linux nice bump), and can adopt
it
once that PR lands. The API is in place for exactly that.
### Scope caveat - please read before judging the effect
Because `build_bundle` is the only site, this PR shapes **bundled
small-file
uploads only**. A backup dominated by large files will see no measurable
change
from flipping the setting: that path's disk reads happen on
`tokio::fs`'s
internal blocking pool, which Driven has no handle on, and its
hashing/encryption
stages interleave `.await`s. Likewise the scanner's walk and deep-verify
hashing
run inline on the async task today.
So the honest framing is: this PR lands the mechanism, the settings
plumbing,
and the one site where a guard is sound right now. The broad
user-visible win
lands when the scanner's dedicated worker threads (from
`feat/scan-parallel-pruning`) call `apply_to_current_thread` at startup
- a
one-line adoption, which is why the API has that shape.
## Settings plumbing
`OrchestratorConfig` gains `io_priority: WorkPriority`.
`load_orchestrator_config` parses it from the persisted `global` blob,
and the
assembly creates one `PriorityCell` cloned into both the executor (the
reader)
and the orchestrator (the writer) - the same one-Arc-into-two-consumers
wiring
already used for the pacer, upload pool, and latency reservoir.
`SyncOrchestrator::reconfigure` republishes the cell, so **a settings
save
applies to work that starts after it** without an app restart; work
already in
flight keeps the level it began with. `OrchestratorConfig` is never
persisted or
sent over IPC, so the new field needs no `serde(default)`.
## Behaviour change to call out
Migration 0002 seeds `io_priority: "low"`, so **every existing install
starts
running its bundle builds one CPU notch below normal after this lands.**
That is
the point of the request, but it is a live default change, not opt-in.
The Rust
`OrchestratorConfig::default()` stays `Normal`, so tests, the chaos
harness, and
any settings-load failure behave exactly as before.
## Tests
11 unit tests in `priority.rs`, plus wiring assertions:
- `from_setting` mapping, case/whitespace leniency,
unknown-degrades-to-normal,
round trip, and default-is-Normal.
- `PriorityCell` sharing across clones.
- Every level applies and restores without panicking, each on its own
thread.
- **`cfg(windows)`**: `Low` is observably `THREAD_PRIORITY_BELOW_NORMAL`
via
`GetThreadPriority`, and the guard hands the thread back at
`THREAD_PRIORITY_NORMAL`. `GetThreadPriority` cannot report background
mode,
so the `Idle` test instead proves the guard issued its
`THREAD_MODE_BACKGROUND_END` by asserting a fresh `BACKGROUND_BEGIN`
succeeds - Windows fails that call while the thread is still in
background
mode. No test needs elevation.
- `orchestrator.rs`: the shared cell is seeded at build time and
republished on
`reconfigure` (including back down to `Normal`).
- `assembly.rs`: the cold-start config test now asserts a persisted
`"low"`
arrives as `WorkPriority::Low`.
## Gates
- `cargo fmt --all -- --check` clean
- `cargo clippy --workspace --all-targets -- -D warnings` clean
- `cargo test -p driven-core`: 419 passed, 0 failed
- `cargo test -p driven-app`: 295 passed, 0 failed
- No `ui/` changes. LF endings, ASCII dashes only.
Linux and macOS backends are `cfg`-gated and could not be executed on
the
Windows dev host; CI's `ubuntu-latest` / `macos-latest` legs are the
check.
## Docs
`design/DESIGN.md` s11.2 previously described an unimplemented plan that
named
`SetPriorityClass` (process-wide, which would drag the UI/IPC threads
down too).
Updated to describe what actually shipped. SPEC s22 only enumerates the
setting
values and is still accurate, so it is untouched.
Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JLB3E2Jm7knNJd37fVpH8X
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
0 commit comments