Skip to content

Commit 002f619

Browse files
committed
Implement container stats
This implements statistics gathering across the various components, but ultimately this is for implementing a new CLI command: `container stats`. This shows memory usage, cpu usage, network and block i/o and the number of processes in the container. The new command can inspect stats for 1-N containers and by default continuously updates in a `top` like stream.
1 parent 13a2f1a commit 002f619

File tree

17 files changed

+777
-0
lines changed

17 files changed

+777
-0
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ integration: init-block
176176
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIExecCommand || exit_code=1 ; \
177177
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLICreateCommand || exit_code=1 ; \
178178
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunCommand || exit_code=1 ; \
179+
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIStatsCommand || exit_code=1 ; \
179180
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIImagesCommand || exit_code=1 ; \
180181
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunBase || exit_code=1 ; \
181182
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIBuildBase || exit_code=1 ; \

Sources/ContainerClient/Core/ClientContainer.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,27 @@ extension ClientContainer {
314314
}
315315
return fh
316316
}
317+
318+
public func stats() async throws -> ContainerStats {
319+
let request = XPCMessage(route: .containerStats)
320+
request.set(key: .id, value: self.id)
321+
322+
let client = Self.newXPCClient()
323+
do {
324+
let response = try await client.send(request)
325+
guard let data = response.dataNoCopy(key: .statistics) else {
326+
throw ContainerizationError(
327+
.internalError,
328+
message: "no statistics data returned"
329+
)
330+
}
331+
return try JSONDecoder().decode(ContainerStats.self, from: data)
332+
} catch {
333+
throw ContainerizationError(
334+
.internalError,
335+
message: "failed to get statistics for container \(self.id)",
336+
cause: error
337+
)
338+
}
339+
}
317340
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 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+
/// Statistics for a container suitable for CLI display.
20+
public struct ContainerStats: Sendable, Codable {
21+
/// Container ID
22+
public var id: String
23+
/// Physical memory usage in bytes
24+
public var memoryUsageBytes: UInt64
25+
/// Memory limit in bytes
26+
public var memoryLimitBytes: UInt64
27+
/// CPU usage in microseconds
28+
public var cpuUsageUsec: UInt64
29+
/// Network received bytes (sum of all interfaces)
30+
public var networkRxBytes: UInt64
31+
/// Network transmitted bytes (sum of all interfaces)
32+
public var networkTxBytes: UInt64
33+
/// Block I/O read bytes (sum of all devices)
34+
public var blockReadBytes: UInt64
35+
/// Block I/O write bytes (sum of all devices)
36+
public var blockWriteBytes: UInt64
37+
/// Number of processes in the container
38+
public var numProcesses: UInt64
39+
40+
public init(
41+
id: String,
42+
memoryUsageBytes: UInt64,
43+
memoryLimitBytes: UInt64,
44+
cpuUsageUsec: UInt64,
45+
networkRxBytes: UInt64,
46+
networkTxBytes: UInt64,
47+
blockReadBytes: UInt64,
48+
blockWriteBytes: UInt64,
49+
numProcesses: UInt64
50+
) {
51+
self.id = id
52+
self.memoryUsageBytes = memoryUsageBytes
53+
self.memoryLimitBytes = memoryLimitBytes
54+
self.cpuUsageUsec = cpuUsageUsec
55+
self.networkRxBytes = networkRxBytes
56+
self.networkTxBytes = networkTxBytes
57+
self.blockReadBytes = blockReadBytes
58+
self.blockWriteBytes = blockWriteBytes
59+
self.numProcesses = numProcesses
60+
}
61+
}

Sources/ContainerClient/Core/XPC+.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ public enum XPCKeys: String {
115115
case volumeLabels
116116
case volumeReadonly
117117
case volumeContainerId
118+
119+
/// Container statistics
120+
case statistics
118121
}
119122

120123
public enum XPCRoute: String {
@@ -132,6 +135,7 @@ public enum XPCRoute: String {
132135
case containerState
133136
case containerLogs
134137
case containerEvent
138+
case containerStats
135139

136140
case pluginLoad
137141
case pluginGet

Sources/ContainerClient/SandboxClient.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,30 @@ extension SandboxClient {
273273
)
274274
}
275275
}
276+
277+
public func statistics() async throws -> ContainerStats {
278+
let request = XPCMessage(route: SandboxRoutes.statistics.rawValue)
279+
280+
let response: XPCMessage
281+
do {
282+
response = try await self.client.send(request)
283+
} catch {
284+
throw ContainerizationError(
285+
.internalError,
286+
message: "failed to get statistics for container \(self.id)",
287+
cause: error
288+
)
289+
}
290+
291+
guard let data = response.dataNoCopy(key: .statistics) else {
292+
throw ContainerizationError(
293+
.internalError,
294+
message: "no statistics data returned"
295+
)
296+
}
297+
298+
return try JSONDecoder().decode(ContainerStats.self, from: data)
299+
}
276300
}
277301

278302
extension XPCMessage {

Sources/ContainerClient/SandboxRoutes.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ public enum SandboxRoutes: String {
3939
case dial = "com.apple.container.sandbox/dial"
4040
/// Shutdown the sandbox service process.
4141
case shutdown = "com.apple.container.sandbox/shutdown"
42+
/// Get statistics for the sandbox.
43+
case statistics = "com.apple.container.sandbox/statistics"
4244
}

Sources/ContainerCommands/Application.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public struct Application: AsyncParsableCommand {
5959
ContainerLogs.self,
6060
ContainerRun.self,
6161
ContainerStart.self,
62+
ContainerStats.self,
6263
ContainerStop.self,
6364
]
6465
),

0 commit comments

Comments
 (0)