From 8dd939cb6dcb414e670c87bf1954f17d23a31a3d Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Thu, 13 Aug 2026 10:32:52 +0000 Subject: [PATCH] Ask for continuous discard where the runtime makes an ext4 A container's root filesystem and writable layer are sparse files, and the guest never returns the blocks it frees: deleting inside the guest frees guest blocks, the host file keeps every block it has ever touched, so its real size is the high water mark of everything ever written rather than what is live. The filesystem hands freed blocks back the moment they are freed when its mount asks for discard, and the runtime forwards those discards to hole punches in the backing file. The ask belongs to the file: every ext4 the runtime creates is such a sparse file, so its mount options carry discard from the moment the mount exists, and every place the file is later mounted, container or pod, lower layer or writable, inherits the ask with the rest of the options. A read-only mount parses the option and leaves it idle, and a caller who builds a mount of their own chooses their own options, as they do for everything else. The swap area has asked for the same thing since it was added, for the same reason. --- Sources/Containerization/ContainerManager.swift | 7 +++++-- Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index 27e9fbe47..abd84e492 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -350,7 +350,7 @@ public struct ContainerManager: Sendable { format: "ext4", source: destination.absolutePath(), destination: "/", - options: [] + options: ["discard"] ) } throw err @@ -364,11 +364,14 @@ public struct ContainerManager: Sendable { } let filesystem = try EXT4.Formatter(FilePath(path), minDiskSize: size) try filesystem.close() + // The filesystem lives in a sparse file that only gives freed blocks + // back to the host when the guest discards them, so the mount asks + // for continuous discard from birth. return .block( format: "ext4", source: path, destination: "/", - options: [] + options: ["discard"] ) } } diff --git a/Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift b/Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift index f69449625..3bfc13ce8 100644 --- a/Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift +++ b/Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift @@ -139,11 +139,15 @@ public struct EXT4Unpacker: Unpacker { try await filesystem.unpack(reader: reader, progress: progress) } + // The filesystem lives in a sparse file that only gives freed blocks + // back to the host when the guest discards them, so the mount asks for + // continuous discard from birth. A read-only mount of it parses the + // option and leaves it idle. return .block( format: "ext4", source: cleanedPath, destination: "/", - options: [] + options: ["discard"] ) }