From e09766d77d17596f140b8e7d3bc4f8a428771817 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 12 Oct 2026 23:15:01 -0400 Subject: [PATCH] fix: make workstation tuning conservative and verifiable Limit automatic tuning to backed-up file-watch configuration. Preserve higher limits, verify effective values, and restore on failure. Document optional system changes and rollback separately. Co-authored-by: Codex --- .github/workflows/checks.yml | 2 +- README.md | 18 ++---- docs/system-tuning.md | 67 +++++++++++++++++++ scripts/apply-system-optimizations.sh | 64 ++---------------- scripts/inotify.py | 93 +++++++++++++++++++++++++++ tests/run.sh | 2 + tests/tuning.py | 68 ++++++++++++++++++++ 7 files changed, 243 insertions(+), 71 deletions(-) create mode 100644 docs/system-tuning.md create mode 100644 scripts/inotify.py create mode 100644 tests/tuning.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 05d6e14..4e1eb2e 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -11,4 +11,4 @@ jobs: - run: sudo apt-get update && sudo apt-get install -y shellcheck jq - run: bash tests/run.sh - name: ShellCheck changed shell scripts - run: shellcheck .config/hypr/scripts/wallpaper.sh .config/rofi/scripts/{wifi,clipboard_history,wallpaper}.sh tests/*.sh + run: shellcheck .config/hypr/scripts/wallpaper.sh .config/rofi/scripts/{wifi,clipboard_history,wallpaper}.sh scripts/apply-system-optimizations.sh tests/*.sh diff --git a/README.md b/README.md index 5d2e11f..7bdc4cd 100644 --- a/README.md +++ b/README.md @@ -164,22 +164,18 @@ Existing conflicting configuration files or directories will be safely backed up ## System optimizations -An automated tuning script is included in `scripts/apply-system-optimizations.sh`. It configures: - -- DNF parallel downloads (capped at 2), fastest mirror selection, and default yes prompts. -- Tuned power profile set to `desktop` for responsive CPU governor scaling. -- Disables `NetworkManager-wait-online.service` to trim cold boot time. -- Inotify user watch limit raised to 524,288 for large development projects. -- Hardware video decode via RPM Fusion `mesa-va-drivers-freeworld` on AMD Radeon GPUs. -- Disables idle background daemons (`ModemManager`, `thermald`, `rsyslog`, `abrtd`, and `lvm2-monitor`). -- Installs modern developer CLI tools (`fzf`, `zoxide`, `eza`, `git-delta`, `duf`). - -To apply these tweaks: +The default script changes only the developer file-watch limit: ```bash sudo ./scripts/apply-system-optimizations.sh ``` +It preserves unrelated sysctl settings and higher watch limits, backs up changed +files, verifies the kernel value, and restores the previous setting if application +fails. It does not install packages, alter power profiles, or disable services. +See [optional system steps](docs/system-tuning.md) for individually verified +changes and rollback. Previously applied system settings are left in place. + ## Verification Run `bash tests/run.sh` for shell syntax and isolated regression checks. Tests use diff --git a/docs/system-tuning.md b/docs/system-tuning.md new file mode 100644 index 0000000..c012738 --- /dev/null +++ b/docs/system-tuning.md @@ -0,0 +1,67 @@ +# Optional workstation changes + +## TL;DR + +The default script only raises the developer file-watch limit. Apply the steps +below separately after recording current settings. These commands are for the +Fedora workstation; they are not automatic optimizations or measured wins. + +## DNF preferences + +Inspect `/etc/dnf/dnf.conf` and back it up with `sudo cp -a --backup=numbered +/etc/dnf/dnf.conf /etc/dnf/dnf.conf.before-dotfiles` before editing. Keep +`max_parallel_downloads=2` as the existing preference until download measurements +justify another value. `fastestmirror=True` selects by connection latency, not +throughput. `defaultyes=True` changes the default confirmation choice, not speed. +For Fedora defaults remove those overrides only after reviewing the file. Verify +with `dnf --dump-main-config` (check the installed DNF version's help first). +Restore the saved file to roll back. + +Reference: [DNF5 configuration](https://dnf5.readthedocs.io/en/latest/dnf5.conf.5.html). + +## Power profile + +Run `tuned-adm active` and `tuned-adm verify` in the actual host session. Record the +active profile before testing another. Keep the current profile by default. +If testing desktop scaling, first confirm `desktop` appears in `tuned-adm list`, +then run `sudo tuned-adm profile desktop`; verify and compare responsiveness and +idle power. Roll back with `sudo tuned-adm profile PREVIOUS_PROFILE`. A file in +`/etc/tuned` is not proof of live activation; do not disable other power managers +without understanding the host's integration. + +## Services and boot + +Measure `systemd-analyze critical-chain` and `systemd-analyze blame` first. Inspect +`systemctl list-dependencies --reverse NetworkManager-wait-online.service` and +whether network-dependent mounts/services need it. If demonstrated unnecessary, +record its enabled state, then use `sudo systemctl disable +NetworkManager-wait-online.service`; compare subsequent boots. Restore its +previous enabled state to roll back. Never blindly disable ModemManager, thermald, +rsyslog, abrtd, or lvm2-monitor; identify actual use and dependencies first. +Previously disabled services require individual review, not blanket re-enabling. + +## AMD hardware video decoding + +Inspect `rpm -q mesa-va-drivers mesa-va-drivers-freeworld` and `vainfo`; confirm +which codecs and driver the running application uses. A package's presence alone +does not establish active browser decoding. If needed, follow the current +[RPM Fusion codec instructions](https://rpmfusion.org/Howto/Multimedia) for the +installed Fedora release. Review a proposed DNF swap interactively; do not add +repositories or import keys silently. Record the transaction ID and package +versions. Review `sudo dnf history undo TRANSACTION_ID` to roll back; availability +of old packages can limit rollback. Do not claim codec acceleration until verified +in the application. + +## Developer tools + +Review and run `sudo dnf install fzf zoxide eza git-delta duf` separately. Verify +with `command -v` for each executable (`delta` is the git-delta executable). +Record the DNF transaction for rollback; avoid removing pre-existing tools. + +## File-watch rollback + +On success, the watch-limit helper prints any backup path. Restore that exact +file and apply just its watch-limit value with `sudo sysctl -w +fs.inotify.max_user_watches=PREVIOUS_VALUE`. If no file previously existed, remove +only the newly created workstation file and restore the previously recorded +kernel value. Do not apply every sysctl file as a shortcut to rollback. diff --git a/scripts/apply-system-optimizations.sh b/scripts/apply-system-optimizations.sh index 85ef969..0d1e955 100755 --- a/scripts/apply-system-optimizations.sh +++ b/scripts/apply-system-optimizations.sh @@ -1,63 +1,9 @@ #!/usr/bin/env bash +# Apply only the backed-up, verified developer file-watch setting. set -euo pipefail - -if [ "$EUID" -ne 0 ]; then - echo "[-] Please run this script with sudo: sudo ./scripts/apply-system-optimizations.sh" +if (( EUID != 0 )); then + echo 'Run: sudo ./scripts/apply-system-optimizations.sh' >&2 exit 1 fi - -echo "[+] 1. Configuring DNF for fast mirrors and parallel downloads (max 2)..." -if ! grep -q "max_parallel_downloads" /etc/dnf/dnf.conf 2>/dev/null; then - cat << 'EOF' >> /etc/dnf/dnf.conf -max_parallel_downloads=2 -fastestmirror=True -defaultyes=True -EOF - echo " Done: Added max_parallel_downloads=2, fastestmirror=True, defaultyes=True." -else - sed -i 's/^max_parallel_downloads=.*/max_parallel_downloads=2/' /etc/dnf/dnf.conf - echo " Done: Updated max_parallel_downloads to 2." -fi - -echo "[+] 2. Setting tuned power profile to 'desktop' (optimized for Ryzen desktop workloads)..." -if command -v tuned-adm >/dev/null 2>&1; then - tuned-adm profile desktop - echo " Done: Active tuned profile set to desktop." -fi - -echo "[+] 3. Shaving ~5.6s off boot time (disabling NetworkManager-wait-online.service)..." -systemctl disable NetworkManager-wait-online.service 2>/dev/null || true -echo " Done: NetworkManager-wait-online disabled." - -echo "[+] 4. Raising inotify file watch limits for dev projects..." -cat << 'EOF' > /etc/sysctl.d/99-workstation.conf -# Raise file watch limits for IDEs, Node, Rust, and Python dev -fs.inotify.max_user_watches=524288 -EOF -sysctl -p /etc/sysctl.d/99-workstation.conf -echo " Done: inotify watches set to 524288." - -echo "[+] 5. Setting up RPM Fusion Free & hardware video acceleration (VA-API freeworld)..." -if ! dnf repolist | grep -q "rpmfusion-free"; then - echo " Enabling RPM Fusion Free repository..." - dnf install -y "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm" || true -fi - -if dnf repolist | grep -q "rpmfusion-free"; then - echo " Swapping mesa-va-drivers for freeworld hardware acceleration..." - dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld || true - echo " Done: Hardware video acceleration installed." -fi - -echo "[+] 6. Disabling unnecessary background daemons (ModemManager, thermald, rsyslog, abrtd, lvm2-monitor)..." -systemctl disable --now thermald ModemManager rsyslog abrtd lvm2-monitor 2>/dev/null || true -echo " Done: Unnecessary background daemons disabled." - -echo "[+] 7. Installing modern developer CLI tools (fzf, zoxide, eza, git-delta, duf)..." -if dnf install -y fzf zoxide eza git-delta duf; then - echo " Done: Modern developer CLI tools installed." -else - echo " [!] Warning: Failed to install developer CLI tools. Install manually via 'sudo dnf install fzf zoxide eza git-delta duf'." -fi - -echo "[✓] All system-level performance optimizations applied successfully!" +python3 "$(dirname "${BASH_SOURCE[0]}")/inotify.py" +echo 'File-watch setting verified. Other system settings were not changed.' diff --git a/scripts/inotify.py b/scripts/inotify.py new file mode 100644 index 0000000..f9ef17d --- /dev/null +++ b/scripts/inotify.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Raise the workstation watch limit with preserved settings and rollback on failure.""" +import os +from pathlib import Path +import re +import shutil +import stat +import subprocess +import sys +import tempfile + +KEY = 'fs.inotify.max_user_watches' +MINIMUM = 524288 + + +def apply(config: Path) -> int: + """Preserve higher limits, update only KEY, and verify the running kernel. + + Args: + config: Sysctl file to update; production uses the fixed workstation path. + Returns: + The verified effective watch limit. + Raises: + ValueError: Existing configuration is malformed. + OSError: Configuration cannot be safely written. + subprocess.CalledProcessError: A sysctl command fails. + RuntimeError: The kernel does not report the requested value. + """ + if config.is_symlink(): + raise ValueError('Refusing to replace a symlinked sysctl file') + current = int(subprocess.check_output(['sysctl', '-n', KEY], text=True).strip()) + original = config.read_text() if config.exists() else '' + lines = original.splitlines(keepends=True) + pattern = re.compile(r'^\s*-?fs\.inotify\.max_user_watches\s*(?:=\s*|\s+)([^#;]+)') + values = [int(match.group(1).strip()) for line in lines if (match := pattern.match(line))] + desired = max(MINIMUM, current, *values) + # Keep exact formatting when an existing single setting already suffices. + if values == [desired]: + updated = original + else: + updated = ''.join(line for line in lines if not pattern.match(line)) + if updated and not updated.endswith('\n'): + updated += '\n' + updated += f'{KEY}={desired}\n' + backup = None + changed = updated != original + if changed: + config.parent.mkdir(parents=True, exist_ok=True) + metadata = config.stat() if config.exists() else None + if metadata: + fd, name = tempfile.mkstemp(prefix=config.name + '.backup-', dir=config.parent) + os.close(fd) + backup = Path(name) + shutil.copy2(config, backup) + fd, name = tempfile.mkstemp(prefix='.' + config.name, dir=config.parent) + temporary = Path(name) + try: + with os.fdopen(fd, 'w') as output: + os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode) if metadata else 0o644) + if metadata: + os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) + output.write(updated) + output.flush() + os.fsync(output.fileno()) + os.replace(temporary, config) + finally: + temporary.unlink(missing_ok=True) + if backup: + print(f'Previous configuration saved to {backup}') + try: + if current != desired: + subprocess.run(['sysctl', '-w', f'{KEY}={desired}'], check=True) + effective = int(subprocess.check_output(['sysctl', '-n', KEY], text=True).strip()) + if effective != desired: + raise RuntimeError(f'Expected {desired} watches, kernel reports {effective}') + except (OSError, ValueError, subprocess.CalledProcessError, RuntimeError): + if changed: + if backup: + os.replace(backup, config) + else: + config.unlink(missing_ok=True) + try: + subprocess.run(['sysctl', '-w', f'{KEY}={current}'], check=True) + except (OSError, subprocess.CalledProcessError) as error: + print(f'Could not restore the previous kernel value: {error}', file=sys.stderr) + raise + return effective + + +if __name__ == '__main__': + if os.geteuid() != 0: + raise SystemExit('Run through sudo ./scripts/apply-system-optimizations.sh') + print(f'Verified watch limit: {apply(Path("/etc/sysctl.d/99-workstation.conf"))}') diff --git a/tests/run.sh b/tests/run.sh index ac0b03d..db5d4da 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -4,3 +4,5 @@ set -euo pipefail cd "$(dirname "$0")/.." while IFS= read -r -d '' script; do bash -n "$script"; done < <(find scripts .config tests -type f -name '*.sh' -print0) for test in tests/*.sh; do [[ $test == tests/run.sh ]] || bash "$test"; done + +for test in tests/*.py; do [[ ! -f $test ]] || python3 -B "$test"; done diff --git a/tests/tuning.py b/tests/tuning.py new file mode 100644 index 0000000..5e39414 --- /dev/null +++ b/tests/tuning.py @@ -0,0 +1,68 @@ +"""Check sysctl preservation, verification, and rollback using disposable files.""" +import importlib.util +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location('inotify', Path(__file__).resolve().parents[1] / 'scripts/inotify.py') +inotify = importlib.util.module_from_spec(spec) +spec.loader.exec_module(inotify) + +class TuningTests(unittest.TestCase): + def test_preserves_higher_limits_and_unrelated_settings(self): + with tempfile.TemporaryDirectory() as directory: + config = Path(directory) / 'workstation.conf' + original = '# local comment\nvm.swappiness=80\nfs.inotify.max_user_watches=1048576\n' + config.write_text(original) + with patch.object(inotify.subprocess, 'check_output', side_effect=['524288', '1048576']), patch.object(inotify.subprocess, 'run') as run: + self.assertEqual(inotify.apply(config), 1048576) + self.assertEqual(config.read_text(), original) + run.assert_called_once_with(['sysctl', '-w', 'fs.inotify.max_user_watches=1048576'], check=True) + self.assertEqual(list(Path(directory).glob('*.backup-*')), []) + + def test_updates_once_and_backs_up(self): + with tempfile.TemporaryDirectory() as directory: + config = Path(directory) / 'workstation.conf' + original = '# custom\nvm.swappiness=80\nfs.inotify.max_user_watches = 8192\n' + config.write_text(original) + with patch.object(inotify.subprocess, 'check_output', return_value='524288'), patch.object(inotify.subprocess, 'run'): + inotify.apply(config) + first = config.read_text() + inotify.apply(config) + self.assertIn('vm.swappiness=80', first) + backups = list(Path(directory).glob('*.backup-*')) + self.assertEqual(len(backups), 1) + self.assertEqual(backups[0].read_text(), original) + + def test_failed_apply_restores_file(self): + with tempfile.TemporaryDirectory() as directory: + config = Path(directory) / 'workstation.conf' + original = 'vm.swappiness=80\n' + config.write_text(original) + failure = subprocess.CalledProcessError(1, 'sysctl') + with patch.object(inotify.subprocess, 'check_output', return_value='8192'), patch.object(inotify.subprocess, 'run', side_effect=[failure, None]): + with self.assertRaises(subprocess.CalledProcessError): + inotify.apply(config) + self.assertEqual(config.read_text(), original) + + def test_failed_verification_removes_new_file(self): + with tempfile.TemporaryDirectory() as directory: + config = Path(directory) / 'workstation.conf' + with patch.object(inotify.subprocess, 'check_output', side_effect=['8192', '8192']), patch.object(inotify.subprocess, 'run'): + with self.assertRaises(RuntimeError): + inotify.apply(config) + self.assertFalse(config.exists()) + + def test_malformed_existing_value_is_not_overwritten(self): + with tempfile.TemporaryDirectory() as directory: + config = Path(directory) / 'workstation.conf' + config.write_text('fs.inotify.max_user_watches=unexpected\n') + with patch.object(inotify.subprocess, 'check_output', return_value='8192'), patch.object(inotify.subprocess, 'run') as run: + with self.assertRaises(ValueError): + inotify.apply(config) + run.assert_not_called() + +if __name__ == '__main__': + unittest.main()