-
Notifications
You must be signed in to change notification settings - Fork 766
Fix system df to count content blobs and deduplicate shared storage #1555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // Copyright © 2026 Apple Inc. and the container project authors. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // https://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import Foundation | ||
|
|
||
| extension FileManager { | ||
| /// Total bytes allocated on disk for all files in a directory (recursive). | ||
| /// | ||
| /// Caveats: hidden files are skipped, symlinks to directories are not followed, but | ||
| /// symlinks-to-files and hard links each contribute their target's full allocation | ||
| /// so shared inodes are counted multiple times. | ||
| public func allocatedSize(of directory: URL) -> UInt64 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't write any more code that uses URL for fs paths.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also noting that this would be a good candidate for extracting to a FilePathOps utility type in ContainerizationOS: apple/containerization#744. |
||
| guard | ||
| let enumerator = self.enumerator( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added |
||
| at: directory, | ||
| includingPropertiesForKeys: [.totalFileAllocatedSizeKey], | ||
| options: [.skipsHiddenFiles] | ||
| ) | ||
| else { | ||
| return 0 | ||
| } | ||
|
|
||
| var size: UInt64 = 0 | ||
| for case let fileURL as URL in enumerator { | ||
| guard let resourceValues = try? fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey]), | ||
| let fileSize = resourceValues.totalFileAllocatedSize | ||
| else { | ||
| continue | ||
| } | ||
| size += UInt64(fileSize) | ||
| } | ||
| return size | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,11 +29,19 @@ import TerminalProgress | |
| public actor ImagesService { | ||
| private let log: Logger | ||
| private let contentStore: ContentStore | ||
| private let contentStoreTotalSize: @Sendable () async throws -> UInt64 | ||
| private let imageStore: ImageStore | ||
| private let snapshotStore: SnapshotStore | ||
|
|
||
| public init(contentStore: ContentStore, imageStore: ImageStore, snapshotStore: SnapshotStore, log: Logger) throws { | ||
| public init( | ||
| contentStore: ContentStore, | ||
| contentStoreTotalSize: @Sendable @escaping () async throws -> UInt64, | ||
| imageStore: ImageStore, | ||
| snapshotStore: SnapshotStore, | ||
| log: Logger | ||
| ) throws { | ||
| self.contentStore = contentStore | ||
| self.contentStoreTotalSize = contentStoreTotalSize | ||
| self.imageStore = imageStore | ||
| self.snapshotStore = snapshotStore | ||
| self.log = log | ||
|
|
@@ -270,8 +278,7 @@ public actor ImagesService { | |
|
|
||
| /// Calculate disk usage for images | ||
| /// - Parameter activeReferences: Set of image references currently in use by containers | ||
| /// - Returns: Tuple of (total count, active count, total size, reclaimable size) | ||
| public func calculateDiskUsage(activeReferences: Set<String>) async throws -> (Int, Int, UInt64, UInt64) { | ||
| public func calculateDiskUsage(activeReferences: Set<String>) async throws -> (totalCount: Int, activeCount: Int, totalSize: UInt64, reclaimableSize: UInt64) { | ||
| self.log.debug( | ||
| "ImagesService: enter", | ||
| metadata: [ | ||
|
|
@@ -290,49 +297,41 @@ public actor ImagesService { | |
| } | ||
|
|
||
| let images = try await self._list() | ||
| var totalSize: UInt64 = 0 | ||
| var reclaimableSize: UInt64 = 0 | ||
| var activeCount = 0 | ||
| var activeContentSizes: [String: UInt64] = [:] | ||
| var activeSnapshotSizes: [String: UInt64] = [:] | ||
| var processedDigests = Set<String>() | ||
|
|
||
| for image in images { | ||
| // Calculate size for all platform variants | ||
| let imageSize = try await self.calculateImageSize(image) | ||
| totalSize += imageSize | ||
|
|
||
| // Check if image is referenced by any container | ||
| let isActive = activeReferences.contains(image.reference) | ||
| if isActive { | ||
| activeCount += 1 | ||
| } else { | ||
| reclaimableSize += imageSize | ||
| guard activeReferences.contains(image.reference) else { continue } | ||
| activeCount += 1 | ||
| let imageDigest = image.digest.trimmingDigestPrefix | ||
| guard processedDigests.insert(imageDigest).inserted else { continue } | ||
|
|
||
| for digest in try await image.referencedDigests() where activeContentSizes[digest] == nil { | ||
| guard let content: Content = try await self.contentStore.get(digest: digest) else { continue } | ||
| activeContentSizes[digest] = try self.contentDiskSize(content) | ||
| } | ||
| for (digest, size) in try await self.snapshotStore.getSnapshotSizes(for: image) { | ||
| activeSnapshotSizes[digest] = size | ||
| } | ||
| } | ||
|
|
||
| return (images.count, activeCount, totalSize, reclaimableSize) | ||
| } | ||
|
|
||
| /// Calculate total size for an image including all platform variants | ||
| private func calculateImageSize(_ image: Containerization.Image) async throws -> UInt64 { | ||
| var totalSize: UInt64 = 0 | ||
| let index = try await image.index() | ||
|
|
||
| for descriptor in index.manifests { | ||
| // Skip attestation manifests | ||
| if let refType = descriptor.annotations?["vnd.docker.reference.type"], | ||
| refType == "attestation-manifest" | ||
| { | ||
| continue | ||
| } | ||
| let snapshotDiskSize = await self.snapshotStore.totalAllocatedSize() | ||
| let contentDiskSize = try await self.contentStoreTotalSize() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What prevents us from invoking |
||
| let totalOnDisk = contentDiskSize + snapshotDiskSize | ||
| let activeSize = activeContentSizes.values.reduce(0, +) + activeSnapshotSizes.values.reduce(0, +) | ||
| let reclaimable = totalOnDisk > activeSize ? totalOnDisk - activeSize : 0 | ||
|
|
||
| guard descriptor.platform != nil else { continue } | ||
| return (images.count, activeCount, totalOnDisk, reclaimable) | ||
| } | ||
|
|
||
| // Get snapshot size for this platform | ||
| if let snapshotSize = try? await self.snapshotStore.getSnapshotSize(descriptor: descriptor) { | ||
| totalSize += snapshotSize | ||
| } | ||
| private func contentDiskSize(_ content: Content) throws -> UInt64 { | ||
| let values = try? content.path.resourceValues(forKeys: [.totalFileAllocatedSizeKey]) | ||
| if let allocatedSize = values?.totalFileAllocatedSize { | ||
| return UInt64(allocatedSize) | ||
| } | ||
|
|
||
| return totalSize | ||
| return try content.size() | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comments should include more details on what is and isn't counted. As far as I can tell:
directorywill include those files' sizes in the total.directorywill include those files' sizes in the total, resulting in inaccuracy due to multiple counting.Do we need to address the double counting issues, or will the directories we enumerate never have these sorts of things?