Skip to content

Commit 7358102

Browse files
authored
Fix system df to count content blobs and deduplicate shared storage (apple#1555)
- Closes apple#1526 and apple#1527. ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context This PR fixes `system df` to report actual on-disk allocated bytes (content blobs + snapshots) instead of summing per-image snapshot sizes. Orphaned blobs are now included as reclaimable, and storage shared across tags is no longer double counted. Also consolidates three identical `calculateDirectorySize` implementations into a shared `FileManager.allocatedSize(of:)` extension. ## Testing - [x] Tested locally - [x] Added/updated tests - [ ] Added/updated docs
1 parent da8daf3 commit 7358102

14 files changed

Lines changed: 233 additions & 132 deletions

File tree

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ INTEGRATION_TEST_SUITES ?= \
211211
TestCLIKernelSet \
212212
TestCLIAnonymousVolumes \
213213
TestCLINotFound \
214-
TestCLINoParallelCases
214+
TestCLINoParallelCases \
215+
TestCLISystemDF
215216

216217
empty :=
217218
space := $(empty) $(empty)

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import PackageDescription
2323
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
2424
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
2525
let builderShimVersion = "0.12.0"
26-
let scVersion = "0.33.2"
26+
let scVersion = "0.33.3"
2727

2828
let package = Package(
2929
name: "container",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
19+
extension FileManager {
20+
/// Total bytes allocated on disk for all files in a directory (recursive).
21+
///
22+
/// Caveats: hidden files are skipped, symlinks to directories are not followed, but
23+
/// symlinks-to-files and hard links each contribute their target's full allocation
24+
/// so shared inodes are counted multiple times.
25+
public func allocatedSize(of directory: URL) -> UInt64 {
26+
guard
27+
let enumerator = self.enumerator(
28+
at: directory,
29+
includingPropertiesForKeys: [.totalFileAllocatedSizeKey],
30+
options: [.skipsHiddenFiles]
31+
)
32+
else {
33+
return 0
34+
}
35+
36+
var size: UInt64 = 0
37+
for case let fileURL as URL in enumerator {
38+
guard let resourceValues = try? fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey]),
39+
let fileSize = resourceValues.totalFileAllocatedSize
40+
else {
41+
continue
42+
}
43+
size += UInt64(fileSize)
44+
}
45+
return size
46+
}
47+
}

Sources/Plugins/CoreImages/ImagesHelper.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@ extension ImagesHelper {
9898
let imageStore = try ImageStore(path: rootURL, contentStore: contentStore)
9999
let unpackStrategy = SnapshotStore.defaultUnpackStrategy(initImage: containerSystemConfig.vminit.image)
100100
let snapshotStore = try SnapshotStore(path: rootURL, unpackStrategy: unpackStrategy, log: log)
101-
let service = try ImagesService(contentStore: contentStore, imageStore: imageStore, snapshotStore: snapshotStore, log: log)
101+
let service = try ImagesService(
102+
contentStore: contentStore,
103+
imageStore: imageStore,
104+
snapshotStore: snapshotStore,
105+
log: log
106+
)
102107
let harness = ImagesServiceHarness(service: service, log: log)
103108

104109
routes[ImagesServiceXPCRoute.imagePull.rawValue] = XPCServer.route(harness.pull)
@@ -124,6 +129,7 @@ extension ImagesHelper {
124129
routes[ImagesServiceXPCRoute.contentClean.rawValue] = XPCServer.route(harness.clean)
125130
routes[ImagesServiceXPCRoute.contentGet.rawValue] = XPCServer.route(harness.get)
126131
routes[ImagesServiceXPCRoute.contentDelete.rawValue] = XPCServer.route(harness.delete)
132+
routes[ImagesServiceXPCRoute.contentSize.rawValue] = XPCServer.route(harness.totalSize)
127133
routes[ImagesServiceXPCRoute.contentIngestStart.rawValue] = XPCServer.route(harness.newIngestSession)
128134
routes[ImagesServiceXPCRoute.contentIngestCancel.rawValue] = XPCServer.route(harness.cancelIngestSession)
129135
routes[ImagesServiceXPCRoute.contentIngestComplete.rawValue] = XPCServer.route(harness.completeIngestSession)

Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public actor ContainersService {
216216

217217
for (id, state) in await self.containers {
218218
let bundlePath = self.containerRoot.appendingPathComponent(id)
219-
let containerSize = Self.calculateDirectorySize(at: bundlePath.path)
219+
let containerSize = FileManager.default.allocatedSize(of: bundlePath)
220220
totalSize += containerSize
221221

222222
if state.snapshot.status == .running {
@@ -243,39 +243,6 @@ public actor ContainersService {
243243
}
244244
}
245245

246-
/// Calculate directory size using APFS-aware resource keys
247-
/// - Parameter path: Path to directory
248-
/// - Returns: Total allocated size in bytes
249-
private static nonisolated func calculateDirectorySize(at path: String) -> UInt64 {
250-
let url = URL(fileURLWithPath: path)
251-
let fileManager = FileManager.default
252-
253-
guard
254-
let enumerator = fileManager.enumerator(
255-
at: url,
256-
includingPropertiesForKeys: [.totalFileAllocatedSizeKey],
257-
options: [.skipsHiddenFiles]
258-
)
259-
else {
260-
return 0
261-
}
262-
263-
var totalSize: UInt64 = 0
264-
for case let fileURL as URL in enumerator {
265-
guard
266-
let resourceValues = try? fileURL.resourceValues(
267-
forKeys: [.totalFileAllocatedSizeKey]
268-
),
269-
let fileSize = resourceValues.totalFileAllocatedSize
270-
else {
271-
continue
272-
}
273-
totalSize += UInt64(fileSize)
274-
}
275-
276-
return totalSize
277-
}
278-
279246
/// Create a new container from the provided id and configuration.
280247
public func create(configuration: ContainerConfiguration, kernel: Kernel, options: ContainerCreateOptions, initImage: String? = nil, runtimeData: Data? = nil) async throws {
281248
log.debug(
@@ -900,7 +867,7 @@ public actor ContainersService {
900867

901868
let containerPath = self.containerRoot.appendingPathComponent(id).path
902869

903-
return Self.calculateDirectorySize(at: containerPath)
870+
return FileManager.default.allocatedSize(of: URL(fileURLWithPath: containerPath))
904871
}
905872

906873
public func exportRootfs(id: String, archive: URL) async throws {

Sources/Services/ContainerAPIService/Server/Volumes/VolumesService.swift

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public actor VolumesService {
158158
}
159159

160160
let volumePath = self.volumePath(for: name)
161-
return self.calculateDirectorySize(at: volumePath)
161+
return FileManager.default.allocatedSize(of: URL(fileURLWithPath: volumePath))
162162
}
163163

164164
/// Calculate disk usage for volumes
@@ -201,7 +201,7 @@ public actor VolumesService {
201201
// Calculate sizes
202202
for volume in allVolumes {
203203
let volumePath = self.volumePath(for: volume.name)
204-
let volumeSize = self.calculateDirectorySize(at: volumePath)
204+
let volumeSize = FileManager.default.allocatedSize(of: URL(fileURLWithPath: volumePath))
205205
totalSize += volumeSize
206206

207207
if !inUseSet.contains(volume.name) {
@@ -214,33 +214,6 @@ public actor VolumesService {
214214
}
215215
}
216216

217-
private nonisolated func calculateDirectorySize(at path: String) -> UInt64 {
218-
let url = URL(fileURLWithPath: path)
219-
let fileManager = FileManager.default
220-
221-
guard
222-
let enumerator = fileManager.enumerator(
223-
at: url,
224-
includingPropertiesForKeys: [.totalFileAllocatedSizeKey],
225-
options: [.skipsHiddenFiles]
226-
)
227-
else {
228-
return 0
229-
}
230-
231-
var totalSize: UInt64 = 0
232-
for case let fileURL as URL in enumerator {
233-
guard let resourceValues = try? fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey]),
234-
let fileSize = resourceValues.totalFileAllocatedSize
235-
else {
236-
continue
237-
}
238-
totalSize += UInt64(fileSize)
239-
}
240-
241-
return totalSize
242-
}
243-
244217
private func parseSize(_ sizeString: String) throws -> UInt64 {
245218
let measurement = try Measurement.parse(parsing: sizeString)
246219
let bytes = measurement.converted(to: .bytes).value

Sources/Services/ContainerImagesService/Client/ImageServiceXPCRoutes.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public enum ImagesServiceXPCRoute: String {
3333
case contentGet
3434
case contentDelete
3535
case contentClean
36+
case contentSize
3637
case contentIngestStart
3738
case contentIngestComplete
3839
case contentIngestCancel

Sources/Services/ContainerImagesService/Client/RemoteContentStoreClient.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ public struct RemoteContentStoreClient: ContentStore {
143143
request.set(key: .ingestSessionId, value: id)
144144
try await client.send(request)
145145
}
146+
147+
public func totalAllocatedSize() async throws -> UInt64 {
148+
let client = Self.newClient()
149+
let request = XPCMessage(route: .contentSize)
150+
let response = try await client.send(request)
151+
return response.uint64(key: .imageSize)
152+
}
146153
}
147154

148155
#endif

Sources/Services/ContainerImagesService/Server/ContentServiceHarness.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,12 @@ public struct ContentServiceHarness: Sendable {
111111
reply.set(key: .digests, value: d)
112112
return reply
113113
}
114+
115+
@Sendable
116+
public func totalSize(_ message: XPCMessage) async throws -> XPCMessage {
117+
let size = try await self.service.totalAllocatedSize()
118+
let reply = message.reply()
119+
reply.set(key: .imageSize, value: size)
120+
return reply
121+
}
114122
}

0 commit comments

Comments
 (0)