Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions docs/system-tuning.md
Original file line number Diff line number Diff line change
@@ -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.
64 changes: 5 additions & 59 deletions scripts/apply-system-optimizations.sh
Original file line number Diff line number Diff line change
@@ -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.'
93 changes: 93 additions & 0 deletions scripts/inotify.py
Original file line number Diff line number Diff line change
@@ -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"))}')
2 changes: 2 additions & 0 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
68 changes: 68 additions & 0 deletions tests/tuning.py
Original file line number Diff line number Diff line change
@@ -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()
Loading