port the swap section, and size it against the disk
Four things the original got wrong, all of which only show up on a machine that is not this one: It detected swap with `swapon --show | grep -q '/'` — a test for a swap FILE. A machine using zram or a swap partition reports no swap at all, and the step would add a swapfile beside working swap. Reads SwapTotal from /proc/meminfo now, which covers every kind. It never looked at free disk. On a VPS with 4G free and 16G of RAM it would fallocate 8G, fail, and take the run down under `set -e`. The recommendation is now capped by what is actually there, keeping 5G back, and refuses rather than shrinking to something useless. fallocate was assumed to work. It produces a file that btrfs and zfs will not swap on, so dd is the fallback — slow, but it always works. swappiness was written by sed'ing /etc/sysctl.conf in place, tangling it with whatever else lives there. It is a drop-in at /etc/sysctl.d now, so what this script set is visible as its own file. Role-dependent, which is the first use of MACHINE_ROLE: swappiness 10 on a server, where swapping is the emergency valve and a page fault on a request path is latency somebody is waiting for; the kernel default of 60 on dev, where swapping out an application nobody has touched in an hour is exactly what you want. WSL is left alone entirely — WSL2 runs its own managed swap inside the VM, and a swapfile written here is wasted disk the kernel will not use. Sizes are reported rounded rather than floored. A 4 GiB swapfile is 4194300 kB, which floors to 3 and reads as though a gigabyte went missing. Free disk stays floored, deliberately: it decides how much to allocate, and rounding up invents space. Verified on this host — 4G RAM, 4G existing swap correctly detected and left alone — and with the disk check stubbed at 6G free (caps to 1G) and 3G free (refuses). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,90 @@ locale_set() {
|
||||
esac
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Swap
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
SWAPFILE=/swapfile
|
||||
|
||||
# Rounded to nearest, not floored: a 4 GiB swapfile is 4194300 kB, which floors
|
||||
# to 3 and reads as though a gigabyte went missing. Same for RAM, where 3.7 GiB
|
||||
# reporting as "3G" makes the sizing tiers look wrong.
|
||||
kb_to_gb_rounded() { echo $((($1 + 524288) / 1048576)); }
|
||||
|
||||
# Total active swap in GiB, 0 if there is none.
|
||||
#
|
||||
# From /proc/meminfo rather than by grepping swapon's output for a slash, which
|
||||
# is what the original did to spot a swap FILE — that test reports no swap at all
|
||||
# on a machine using zram or a swap partition, and the step would then add a
|
||||
# swapfile beside perfectly good swap.
|
||||
swap_active_gb() { kb_to_gb_rounded "$(awk '/^SwapTotal:/ { print $2 }' /proc/meminfo)"; }
|
||||
|
||||
ram_gb() { kb_to_gb_rounded "$(awk '/^MemTotal:/ { print $2 }' /proc/meminfo)"; }
|
||||
|
||||
# Free space on the filesystem that would hold the swapfile, in GiB. Floored
|
||||
# rather than rounded, deliberately: this one decides how much to allocate, and
|
||||
# rounding up invents space that is not there.
|
||||
disk_free_gb() { echo $(($(df -Pk "$(dirname "$SWAPFILE")" | awk 'NR == 2 { print $4 }') / 1024 / 1024)); }
|
||||
|
||||
# How much swap this machine should have.
|
||||
#
|
||||
# The tiers are the original's. What is new is that the answer is capped by what
|
||||
# is actually on the disk — the original would try to fallocate 8G on a VPS with
|
||||
# 4G free, fail, and take the run down with it.
|
||||
swap_recommended_gb() {
|
||||
local ram size
|
||||
ram="$(ram_gb)"
|
||||
if ((ram <= 2)); then
|
||||
size=2
|
||||
elif ((ram <= 8)); then
|
||||
size=4
|
||||
else
|
||||
size=8
|
||||
fi
|
||||
|
||||
# Leave a few gigabytes behind. A swapfile that fills the disk is a worse
|
||||
# problem than no swapfile.
|
||||
local room=$(($(disk_free_gb) - 5))
|
||||
((room < size)) && size="$room"
|
||||
((size < 1)) && size=0
|
||||
echo "$size"
|
||||
}
|
||||
|
||||
# How eagerly the kernel swaps, by role.
|
||||
#
|
||||
# 10 on a server: swapping is the emergency valve, not a routine, and the cost of
|
||||
# a page fault on a request path is latency somebody is waiting for. A desktop is
|
||||
# the opposite case — swapping out an application nobody has touched in an hour
|
||||
# is exactly what you want — so dev keeps the kernel default of 60.
|
||||
swappiness_for_role() { if is_server; then echo 10; else echo 60; fi; }
|
||||
|
||||
swap_create() {
|
||||
local gb="$1"
|
||||
|
||||
# fallocate is instant but produces a file some filesystems refuse to swap on
|
||||
# (btrfs without the right attributes, zfs at all). dd is slow and always
|
||||
# works, so it is the fallback rather than the default.
|
||||
if ! fallocate -l "${gb}G" "$SWAPFILE" 2>/dev/null; then
|
||||
info " fallocate is not usable here — writing the file with dd, which is slower"
|
||||
dd if=/dev/zero of="$SWAPFILE" bs=1M count=$((gb * 1024)) status=none
|
||||
fi
|
||||
|
||||
chmod 600 "$SWAPFILE"
|
||||
mkswap "$SWAPFILE" >/dev/null
|
||||
swapon "$SWAPFILE"
|
||||
|
||||
grep -qs "^${SWAPFILE}[[:space:]]" /etc/fstab || echo "${SWAPFILE} none swap sw 0 0" >>/etc/fstab
|
||||
}
|
||||
|
||||
# Written as a drop-in rather than by rewriting /etc/sysctl.conf in place. The
|
||||
# original sed'd that file, which means the setting is tangled up with whatever
|
||||
# else lives there and is invisible to anyone looking for what this script did.
|
||||
swappiness_set() {
|
||||
echo "vm.swappiness=$1" >/etc/sysctl.d/99-machine-setup-swappiness.conf
|
||||
sysctl -q -w "vm.swappiness=$1"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Timezone
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -261,13 +261,67 @@ if ! skip; then
|
||||
step_ok
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 7. Swap
|
||||
# =============================================================================
|
||||
#
|
||||
# Disk the kernel can park cold pages on when RAM fills, so a spike costs
|
||||
# latency instead of a process. A `bun install` or a Docker build on a small
|
||||
# machine is exactly the spike this is for.
|
||||
|
||||
step "Swap"
|
||||
if ! skip; then
|
||||
ACTIVE_SWAP_GB="$(swap_active_gb)"
|
||||
WANT_SWAP_GB="$(swap_recommended_gb)"
|
||||
SWAPPINESS="$(swappiness_for_role)"
|
||||
CURRENT_SWAPPINESS="$(sysctl -n vm.swappiness 2>/dev/null || echo unknown)"
|
||||
|
||||
echo ""
|
||||
info "Swap — overflow space so a memory spike costs speed rather than a process"
|
||||
echo " RAM: $(ram_gb)G"
|
||||
echo " active swap: ${ACTIVE_SWAP_GB}G"
|
||||
echo " swappiness: ${CURRENT_SWAPPINESS} -> ${SWAPPINESS} (${MACHINE_ROLE})"
|
||||
|
||||
if [[ "$IS_WSL" == true ]]; then
|
||||
# WSL2 runs its own managed swap inside the VM; a swapfile here is wasted
|
||||
# disk and is not what the kernel would use anyway.
|
||||
echo " WSL manages its own swap — leaving it alone"
|
||||
SUMMARY+=("Swap: left to WSL")
|
||||
elif ((ACTIVE_SWAP_GB > 0)); then
|
||||
echo " already has ${ACTIVE_SWAP_GB}G of swap, leaving it alone"
|
||||
if [[ "$CURRENT_SWAPPINESS" != "$SWAPPINESS" ]] && confirm "Set swappiness to ${SWAPPINESS}?"; then
|
||||
swappiness_set "$SWAPPINESS"
|
||||
ok "swappiness set to ${SWAPPINESS}"
|
||||
SUMMARY+=("Swap: kept ${ACTIVE_SWAP_GB}G, swappiness ${SWAPPINESS}")
|
||||
else
|
||||
SUMMARY+=("Swap: kept ${ACTIVE_SWAP_GB}G")
|
||||
fi
|
||||
elif ((WANT_SWAP_GB == 0)); then
|
||||
# Capped to nothing by the disk check rather than by choice.
|
||||
warn "not enough free disk to add swap safely — $(disk_free_gb)G free"
|
||||
SUMMARY+=("Swap: none added, disk too full")
|
||||
else
|
||||
echo " to create: ${WANT_SWAP_GB}G at ${SWAPFILE} ($(disk_free_gb)G free now)"
|
||||
if confirm "Proceed?"; then
|
||||
swap_create "$WANT_SWAP_GB"
|
||||
swappiness_set "$SWAPPINESS"
|
||||
ok "${WANT_SWAP_GB}G swap active, swappiness ${SWAPPINESS}"
|
||||
SUMMARY+=("Swap: ${WANT_SWAP_GB}G created, swappiness ${SWAPPINESS}")
|
||||
else
|
||||
warn "skipped by request"
|
||||
SUMMARY+=("Swap: SKIPPED by request")
|
||||
fi
|
||||
fi
|
||||
step_ok
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# NOT PORTED YET
|
||||
# =============================================================================
|
||||
#
|
||||
# Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order:
|
||||
#
|
||||
# swap · auto-suspend · boot-hang fix · user creation ·
|
||||
# auto-suspend · boot-hang fix · user creation ·
|
||||
# ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades ·
|
||||
# git config · docker · zsh + prompt · tailscale · neovim · js runtimes ·
|
||||
# dev tools · ufw · zshrc · disk ballast
|
||||
|
||||
Reference in New Issue
Block a user