From 5da5e31ecf314851dce67909efcf75c1964d1f0c Mon Sep 17 00:00:00 2001 From: DenisCDev Date: Mon, 26 Oct 2026 22:39:35 -0300 Subject: [PATCH 1/2] Skip the boot balance when it cannot free a chunk big-btrfs-balance runs `btrfs balance start -dusage=50 -musage=50` five minutes after every boot. A usage filter also selects the last, partly filled chunk that the previous balance packed the data into, so on a filesystem that barely changed since the last boot the balance moves that chunk into a new one, frees nothing, and does the same again next boot. On a test filesystem that left ~495 MiB rewritten on every boot after the first one. It runs while the person is already playing or working, and the ionice idle class it asks for has no effect under kyber, which 60-ioschedulers.rules selects for SSDs and NVMe drives. Freeing a chunk needs at least one chunk of allocated-but-unused space, which `btrfs filesystem df -b` reports per type. The data filter now runs only with 1 GiB of it (the data chunk size on any filesystem of 10 GiB or more) and the metadata filter only with one metadata chunk, whose size the kernel reports in /sys/fs/btrfs//allocation/metadata/chunk_size (256 MiB when that file is missing, the smallest metadata chunk). Mixed data+metadata block groups keep the previous behavior. Checked on a 12 GiB btrfs loopback image with compress-force=zstd, three boots in a row, three repetitions, original vs patched script: first boot 1438 vs 1430 MiB written, 4048 vs 4096 MiB returned to unallocated; second and third boots 494-495 vs 0 MiB written. Co-Authored-By: Claude Opus 5.5 --- auto-rules-io/usr/bin/big-btrfs-balance | 28 +++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/auto-rules-io/usr/bin/big-btrfs-balance b/auto-rules-io/usr/bin/big-btrfs-balance index 08b8280..1c593a9 100755 --- a/auto-rules-io/usr/bin/big-btrfs-balance +++ b/auto-rules-io/usr/bin/big-btrfs-balance @@ -7,12 +7,36 @@ FolderRoot=$(btrfs device stats / | tail -n1 | cut -f1 -d] | cut -f2 -d[) FolderHome=$(btrfs device stats /home | tail -n1 | cut -f1 -d] | cut -f2 -d[) +# A usage filter also picks the last, partly filled chunk that the previous +# balance just packed, so a filesystem that changed little since then has the +# same data rewritten on every boot and frees nothing. Freeing a chunk needs +# at least a chunk of allocated-but-unused space: data chunks are 1 GiB on any +# filesystem of 10 GiB or more, and the kernel reports the metadata chunk size, +# which is at least 256 MiB. Its data chunk size there is only the upper limit. +balance() { + local type slack meta_chunk filters=() + meta_chunk=$(cat "/sys/fs/btrfs/$(findmnt -no UUID "$1")/allocation/metadata/chunk_size" 2>/dev/null) + while read -r type slack; do + case $type in + Data) (( slack >= 1073741824 )) && filters+=(-dusage=50) ;; + Metadata) (( slack >= ${meta_chunk:-268435456} )) && filters+=(-musage=50) ;; + Data+Metadata) filters+=(-dusage=50 -musage=50) ;; + esac + done < <(btrfs filesystem df -b "$1" | awk -F'[ ,=]+' '$1 == "Data" || $1 == "Metadata" || $1 == "Data+Metadata" { + for (i = 2; i < NF; i++) { if ($i == "total") t[$1] += $(i+1); if ($i == "used") u[$1] += $(i+1) } + } END { for (k in t) print k, t[k] - u[k] }') + + if (( ${#filters[@]} )); then + nice -n+20 ionice -c 3 btrfs balance start "${filters[@]}" "$1" + fi +} + if [ "$FolderRoot" != "" ]; then - nice -n+20 ionice -c 3 btrfs balance start -dusage=50 -musage=50 / + balance / fi if [ "$FolderHome" != "" ] && [ "$FolderHome" != "$FolderRoot" ]; then - nice -n+20 ionice -c 3 btrfs balance start -dusage=50 -musage=50 /home + balance /home fi From a754bd8b3ee65311fe0607dbfc21c82ab9c640ae Mon Sep 17 00:00:00 2001 From: DenisCDev Date: Tue, 27 Oct 2026 00:24:02 -0300 Subject: [PATCH 2/2] Use the real chunk size on filesystems under 10 GiB With regular allocation and single, DUP or RAID1 profiles, the kernel limits a chunk to 10% of the filesystem's writable size, rounded up to 16 MiB, and data chunks to 1 GiB (init_alloc_chunk_ctl_policy_regular and decide_stripe_size_regular in fs/btrfs/volumes.c). So on a filesystem under 10 GiB data chunks are smaller than 1 GiB, and the fixed 1 GiB threshold kept a balance that could free space from running. Take the size from `btrfs filesystem usage -b` and cap both thresholds at that rounded tenth. If that command fails, the thresholds stay as before. Checked on a 5 GiB btrfs image (512 MiB data chunks) with 768 MiB of unused data space in two 25%-used chunks, three boots: the previous version started no balance and left it allocated; this one relocated 2 chunks and returned 520 MiB on the first boot (the original script 472 MiB), then started no balance on the next two, where the original relocated 2 chunks on each. Co-Authored-By: Claude Opus 5.5 --- auto-rules-io/usr/bin/big-btrfs-balance | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/auto-rules-io/usr/bin/big-btrfs-balance b/auto-rules-io/usr/bin/big-btrfs-balance index 1c593a9..a3ce899 100755 --- a/auto-rules-io/usr/bin/big-btrfs-balance +++ b/auto-rules-io/usr/bin/big-btrfs-balance @@ -10,16 +10,25 @@ FolderHome=$(btrfs device stats /home | tail -n1 | cut -f1 -d] | cut -f2 -d[) # A usage filter also picks the last, partly filled chunk that the previous # balance just packed, so a filesystem that changed little since then has the # same data rewritten on every boot and frees nothing. Freeing a chunk needs -# at least a chunk of allocated-but-unused space: data chunks are 1 GiB on any -# filesystem of 10 GiB or more, and the kernel reports the metadata chunk size, -# which is at least 256 MiB. Its data chunk size there is only the upper limit. +# at least a chunk of allocated-but-unused space. With single, DUP or RAID1 +# profiles, data chunks are at most 1 GiB, and the kernel reports the +# metadata chunk size, which is at least 256 MiB (its data chunk size there +# is only the upper limit). On smaller filesystems both are 10% of the size, +# rounded up to 16 MiB. balance() { - local type slack meta_chunk filters=() + local type slack size tenth data_chunk=1073741824 meta_chunk filters=() meta_chunk=$(cat "/sys/fs/btrfs/$(findmnt -no UUID "$1")/allocation/metadata/chunk_size" 2>/dev/null) + meta_chunk=${meta_chunk:-268435456} + size=$(btrfs filesystem usage -b "$1" 2>/dev/null | awk '/Device size:/ {print $3; exit}') + if (( size )); then + tenth=$(( (size / 10 + 16777215) / 16777216 * 16777216 )) + (( tenth < data_chunk )) && data_chunk=$tenth + (( tenth < meta_chunk )) && meta_chunk=$tenth + fi while read -r type slack; do case $type in - Data) (( slack >= 1073741824 )) && filters+=(-dusage=50) ;; - Metadata) (( slack >= ${meta_chunk:-268435456} )) && filters+=(-musage=50) ;; + Data) (( slack >= data_chunk )) && filters+=(-dusage=50) ;; + Metadata) (( slack >= meta_chunk )) && filters+=(-musage=50) ;; Data+Metadata) filters+=(-dusage=50 -musage=50) ;; esac done < <(btrfs filesystem df -b "$1" | awk -F'[ ,=]+' '$1 == "Data" || $1 == "Metadata" || $1 == "Data+Metadata" {