feat(m6): one-click install + apply controls on Environment page — 0.10.0

Make the environment report actionable, not just advisory.

Install (reuses M9 installer):
- Add GameMode, MangoHud, cpupower to the component catalog (so they also show
  on the Setup page); catalog.by_id() lookup.
- "tool not installed" findings (GameMode/MangoHud) get an Install button.

Apply runtime-reversible tunables (D22, realizing the D9 consent-gated milestone):
- core/fixes.py: dropdown of live options + Apply for CPU governor, NVIDIA
  persistence, PCIe ASPM policy, vm.swappiness, THP. One pkexec command each,
  no reboot, reverts on reboot; chosen value validated against live options;
  writes go to sysfs/procfs/nvidia-smi, never GRUB. GRUB/mitigations stay
  suggestion-only.
- Finding gained optional action (install) + fix (apply) ids; shared
  finding_card renders the matching control; Environment page wires both and
  re-checks after a change.

Tests for fixes (parse, command builders, value validation, gameenv wiring).
Docs: D22 added (amends D9); SPEC/MODULES/ROADMAP updated. 0.9.0 -> 0.10.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 08:05:03 +02:00
parent 29f4a45df8
commit 9c30c9824e
15 changed files with 457 additions and 33 deletions
+26 -14
View File
@@ -49,15 +49,18 @@ def evaluate_aspm(policy_text: str | None) -> Finding | None:
WARNING, "PCIe", f"PCIe ASPM is in power-saving mode ({active})",
"Aggressive PCIe Active-State Power Management can cause the GPU to drop off the "
"bus under load (Xid 79) or stutter — the seed-case failure mode.",
"Disable ASPM via the kernel cmdline: add `pcie_aspm=off` (and optionally "
"`pcie_aspm.policy=performance`) in GRUB, then `sudo update-grub` and reboot.",
"Set the policy to performance below (live), or for a permanent change add "
"`pcie_aspm=off` in GRUB, then `sudo update-grub` and reboot.",
fix="pcie_aspm",
)
if active == "performance":
return Finding(OK, "PCIe", "PCIe ASPM set to performance", "ASPM power-saving is disabled.")
return Finding(OK, "PCIe", "PCIe ASPM set to performance", "ASPM power-saving is disabled.",
fix="pcie_aspm")
return Finding(
INFO, "PCIe", f"PCIe ASPM policy: {active}",
"ASPM is left to the kernel/BIOS default.",
"If you see GPU bus-drop events (Xid 79), try `pcie_aspm=off` on the kernel cmdline.",
"If you see GPU bus-drop events (Xid 79), set the policy to performance below.",
fix="pcie_aspm",
)
@@ -84,11 +87,13 @@ def check_gpu_persistence() -> list[Finding]:
INFO, "GPU", "NVIDIA persistence mode is off",
"The driver unloads when no client is attached, adding latency on first GPU "
"access and churning state between game launches.",
"Enable it: `sudo nvidia-smi -pm 1` (per-boot), or enable the "
"`nvidia-persistenced` service to make it permanent.",
"Enable it below (per-boot), or enable the `nvidia-persistenced` service to "
"make it permanent.",
fix="nvidia_persistence",
)]
if state.lower().startswith("enabled"):
return [Finding(OK, "GPU", "NVIDIA persistence mode on", "The driver stays resident.")]
return [Finding(OK, "GPU", "NVIDIA persistence mode on", "The driver stays resident.",
fix="nvidia_persistence")]
return []
@@ -99,18 +104,20 @@ def evaluate_governor(governors: set[str]) -> Finding | None:
return None
shown = ", ".join(sorted(governors))
if governors == {"performance"}:
return Finding(OK, "CPU", "CPU governor: performance", "CPUs run at full clocks under load.")
return Finding(OK, "CPU", "CPU governor: performance", "CPUs run at full clocks under load.",
fix="cpu_governor")
if "powersave" in governors:
return Finding(
WARNING, "CPU", f"CPU governor set to power-saving ({shown})",
"A powersave governor caps CPU frequency and can bottleneck frame times.",
"Set performance: `sudo cpupower frequency-set -g performance` "
"(install `linux-tools-common`/`cpupower`), or install GameMode to switch it per-game.",
"Set it to performance below (or install GameMode to switch it per-game).",
fix="cpu_governor",
)
return Finding(
INFO, "CPU", f"CPU governor: {shown}",
"A dynamic governor scales with load; usually fine.",
"For the most consistent frame pacing, `performance` (or GameMode) avoids ramp-up lag.",
"For the most consistent frame pacing, set performance below (or use GameMode).",
fix="cpu_governor",
)
@@ -137,6 +144,7 @@ def check_gamemode() -> list[Finding]:
"GameMode auto-applies performance tweaks (governor, scheduling) for the duration of a game.",
"Install it: `sudo apt install gamemode`, then launch games with `gamemoderun %command%` "
"(or use a global Steam launch option).",
action="gamemode",
)]
@@ -147,6 +155,7 @@ def check_mangohud() -> list[Finding]:
INFO, "Tools", "MangoHud not installed",
"MangoHud overlays live FPS, frame times, and temps in-game — handy for spotting stutter.",
"Install it: `sudo apt install mangohud`, then launch with `mangohud %command%`.",
action="mangohud",
)]
@@ -158,9 +167,11 @@ def evaluate_swappiness(value: int) -> Finding:
INFO, "Memory", f"vm.swappiness is high ({value})",
"A high swappiness lets the kernel swap out memory eagerly, which can cause "
"hitching during gaming on systems with ample RAM.",
"Lower it: `sudo sysctl vm.swappiness=10` (persist in /etc/sysctl.d/99-rigdoctor.conf).",
"Lower it below (e.g. 10); applies immediately.",
fix="swappiness",
)
return Finding(OK, "Memory", f"vm.swappiness is {value}", "Swapping is conservative.")
return Finding(OK, "Memory", f"vm.swappiness is {value}", "Swapping is conservative.",
fix="swappiness")
def check_swappiness() -> list[Finding]:
@@ -204,7 +215,8 @@ def check_thp() -> list[Finding]:
return [Finding(
INFO, "Memory", "Transparent HugePages disabled (never)",
"Some workloads benefit from THP; 'madvise' lets apps opt in without the downsides of 'always'.",
"Optional: `echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled`.",
"Optional: set 'madvise' below; applies immediately.",
fix="thp",
)]
return []