|
| 1 | +# Kernel module compression in EVE |
| 2 | + |
| 3 | +**Decision: leave it exactly as it is.** `core` and `rt` compress modules with xz; `hwe` and |
| 4 | +`ai` do not compress them at all. Both are correct, and this document exists so nobody |
| 5 | +"fixes" the second one. |
| 6 | + |
| 7 | +Measured 2026-08-25 on 6.18.35. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Why this document exists |
| 12 | + |
| 13 | +`arch/x86/configs/eve-hwe_defconfig` contains, inherited from Ubuntu's config: |
| 14 | + |
| 15 | +``` |
| 16 | +CONFIG_MODULE_COMPRESS=y |
| 17 | +CONFIG_MODULE_COMPRESS_ZSTD=y |
| 18 | +# CONFIG_MODULE_COMPRESS_ALL is not set |
| 19 | +``` |
| 20 | + |
| 21 | +`MODULE_COMPRESS_ALL` is `default y` (`kernel/module/Kconfig:384`) and is the switch that makes |
| 22 | +`modules_install` actually compress. Ubuntu turns it off explicitly, so `hwe` ships 4429 |
| 23 | +uncompressed `.ko` files — 634.8 MiB — while declaring a compression algorithm it never uses. |
| 24 | + |
| 25 | +That reads like an oversight. It is not, and the obvious one-line fix makes things worse. |
| 26 | +Three kernel builds and three EVE rootfs builds were spent establishing this; the numbers are |
| 27 | +below so it does not have to happen again. |
| 28 | + |
| 29 | +## Results |
| 30 | + |
| 31 | +EVE rootfs is squashfs with **xz**, the x86 BCJ filter and 128 KiB blocks. All rootfs figures |
| 32 | +are `installer/rootfs-generic.img` from `make HV=kvm PLATFORM=generic pkgs rootfs`. |
| 33 | + |
| 34 | +| flavour | algorithm | `MODULE_COMPRESS_ALL` | modules | module tree in image | rootfs.img | Δ rootfs | |
| 35 | +|---|---|---|---:|---:|---:|---:| |
| 36 | +| **core** | xz | **on** — committed | 297 | 16.4 MiB | **291.97 MiB** | — | |
| 37 | +| core | xz | off | 297 | 91.4 MiB | 292.79 MiB | +0.82 MiB | |
| 38 | +| **hwe** | zstd | **off** — committed | 4429 | 634.8 MiB | **417.17 MiB** | — | |
| 39 | +| hwe | zstd | on | 4429 | 196.7 MiB | 474.67 MiB | **+57.50 MiB** | |
| 40 | +| hwe | xz | on | 4429 | 143.7 MiB | 418.44 MiB | +1.27 MiB | |
| 41 | + |
| 42 | +## The mechanism |
| 43 | + |
| 44 | +Per-module compression is nearly free on the rootfs **only when it uses the same algorithm as |
| 45 | +squashfs**. EVE's squashfs is xz, so: |
| 46 | + |
| 47 | +- **xz modules** cost +0.82 MiB (core) and +1.27 MiB (hwe). Squashfs loses its dedup and |
| 48 | + cross-file window over that data, but per-file xz with a larger dictionary recovers almost |
| 49 | + all of it. |
| 50 | +- **zstd modules** cost **+57.50 MiB**. The output is high-entropy, so squashfs can neither |
| 51 | + dedup nor recompress it, and zstd's own ratio is worse than what squashfs-xz achieves over |
| 52 | + the raw tree. It loses on both axes at once — its module tree (196.7 MiB) is also bigger |
| 53 | + than xz's (143.7 MiB). |
| 54 | + |
| 55 | +Leaving modules **uncompressed** lets squashfs-xz work across the whole 634.8 MiB tree with |
| 56 | +dedup and a shared dictionary, which is why it produces the smallest rootfs of the three. |
| 57 | + |
| 58 | +## The trap |
| 59 | + |
| 60 | +The tempting change is one line in `common.fragment`: |
| 61 | + |
| 62 | +``` |
| 63 | +CONFIG_MODULE_COMPRESS_ALL=y # DO NOT |
| 64 | +``` |
| 65 | + |
| 66 | +Because `eve-hwe_defconfig` selects zstd, that lands on the **worst row in the table**: |
| 67 | ++57.50 MiB on every device's rootfs. If you want compression on `hwe` you must also switch the |
| 68 | +algorithm to xz — and even then it costs +1.27 MiB of rootfs and roughly 10-18 minutes of |
| 69 | +build time, to save container-image size. |
| 70 | + |
| 71 | +## Why `hwe` stays uncompressed |
| 72 | + |
| 73 | +Uncompressed wins on the two things that affect deployed devices: |
| 74 | + |
| 75 | +- **smallest rootfs** — 417.17 MiB, better than either compressed variant |
| 76 | +- **fastest module load** — one decompression pass instead of two |
| 77 | + |
| 78 | +It loses only on **container image size**: 634.8 MiB of modules versus 143.7 MiB with xz, so |
| 79 | +roughly 491 MiB more per image pulled, cached and stored. That is a CI and registry cost, not |
| 80 | +a device cost, and EVE optimises for the device. |
| 81 | + |
| 82 | +## Why `core` stays compressed |
| 83 | + |
| 84 | +`core` serves eve-kvm under a hard rootfs cap (`ROOTFS_MAXSIZE_MB`, 290 historically and 295 |
| 85 | +after the resize work), and xz compression makes its rootfs *smaller*, not larger. It is |
| 86 | +already `default y` because `eve-core_defconfig` does not carry Ubuntu's opt-out. Nothing to |
| 87 | +do. |
| 88 | + |
| 89 | +For reference, `core` on 6.18.35 measures 291.97 MiB against a shipping 6.12 kvm rootfs of |
| 90 | +290.13 MiB — it clears the 295 cap and misses the 290 one. |
| 91 | + |
| 92 | +## Costs, measured |
| 93 | + |
| 94 | +**Build time** (kernel + EVE rootfs, same machine): |
| 95 | + |
| 96 | +| | | |
| 97 | +|---|---| |
| 98 | +| hwe, no compression | 33 min *(ccache cold)* | |
| 99 | +| hwe, zstd | 43 min *(ccache warm)* | |
| 100 | +| hwe, xz | 51 min *(ccache warm)* | |
| 101 | + |
| 102 | +Compression is added work at `modules_install` and it is not free at 4429 modules. |
| 103 | + |
| 104 | +**Module load** — decompressing all 297 `core` modules, 16.4 MB → 91.4 MB, took 3.75 s |
| 105 | +single-threaded including per-file process spawn (an upper bound; in-kernel decompression |
| 106 | +avoids that). The two passes are not equal in size: |
| 107 | + |
| 108 | +| | squashfs pass | module pass | |
| 109 | +|---|---|---| |
| 110 | +| uncompressed | decompresses 91.4 MB of output | — | |
| 111 | +| compressed | decompresses 16.4 MB of output | 16.4 → 91.4 MB | |
| 112 | + |
| 113 | +Squashfs has 5.6× less to do when modules arrive pre-compressed, so the module pass largely |
| 114 | +*replaces* work rather than adding to it — the net penalty is about **+16%**, not +100%. And |
| 115 | +a real device loads a few dozen modules, not all of them. |
| 116 | + |
| 117 | +## Reproducing this |
| 118 | + |
| 119 | +No push is needed; linuxkit resolves a locally built kernel from `~/.linuxkit/cache`. |
| 120 | + |
| 121 | +```sh |
| 122 | +# build a kernel flavour |
| 123 | +cd eve-kernel |
| 124 | +make -f Makefile.eve KERNEL_CONFIG_FLAVOR=<core|hwe> kernel-gcc |
| 125 | + |
| 126 | +# build an EVE rootfs against it |
| 127 | +cd ../eve |
| 128 | +make KERNEL_TAG=$(make -C ../eve-kernel -s -f Makefile.eve \ |
| 129 | + KERNEL_CONFIG_FLAVOR=<core|hwe> docker-tag-gcc) \ |
| 130 | + HV=kvm PLATFORM=generic pkgs rootfs |
| 131 | +``` |
| 132 | + |
| 133 | +Two things that will otherwise waste your time: |
| 134 | + |
| 135 | +- **`pkgs` before `rootfs` is required** if your eve checkout has local modifications. Packages |
| 136 | + are addressed by source hash, so a modified `pkg/pillar` hashes to a tag that exists in no |
| 137 | + registry and the pull fails. |
| 138 | +- **The rootfs size check runs *after* the image is written** (`eve/Makefile:935`), so a build |
| 139 | + that exceeds `ROOTFS_MAXSIZE_MB` still leaves a measurable `rootfs-generic.img`. You do not |
| 140 | + need to raise the cap to measure something over it. |
| 141 | + |
| 142 | +To vary compression, append to `arch/x86/configs/common.fragment` (uncommitted — the kernel |
| 143 | +then tags `-dirty` and cannot collide with a clean build): |
| 144 | + |
| 145 | +``` |
| 146 | +CONFIG_MODULE_COMPRESS_XZ=y # or leave the flavour's own choice |
| 147 | +CONFIG_MODULE_COMPRESS_ALL=y |
| 148 | +``` |
| 149 | + |
| 150 | +A quick synthetic check, useful before spending a full build: `mksquashfs` the module tree both |
| 151 | +ways with EVE's real settings. It predicted core's result to within 30 KB. |
| 152 | + |
| 153 | +```sh |
| 154 | +mksquashfs <tree> out.sqfs -comp xz -Xbcj x86 -b 131072 -no-progress -quiet |
| 155 | +``` |
| 156 | + |
| 157 | +## When to revisit |
| 158 | + |
| 159 | +- **The rootfs compressor changes.** The whole result turns on modules and squashfs using the |
| 160 | + same algorithm. If EVE's squashfs moves to zstd, the conclusions inverts — zstd modules |
| 161 | + become the cheap option and xz the expensive one. |
| 162 | +- **Container image size starts to matter more than device size** — for example if image pulls |
| 163 | + dominate a deployment or CI cost, `hwe` with **xz** (not zstd) buys ~491 MiB per image for |
| 164 | + +1.27 MiB of rootfs. |
| 165 | +- **`core` approaches its cap.** It is already within ~3 MiB of the 295 MiB limit, and |
| 166 | + compression is already on, so there is no headroom left to buy here. |
0 commit comments