Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions Sources/Containerization/ContainerStatistics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public struct ContainerStatistics: Sendable {
public var blockIO: BlockIOStatistics?
public var networks: [NetworkStatistics]?
public var memoryEvents: MemoryEventStatistics?
public var filesystem: [FilesystemStatistics]?

public init(
id: String,
Expand All @@ -31,7 +32,8 @@ public struct ContainerStatistics: Sendable {
cpu: CPUStatistics? = nil,
blockIO: BlockIOStatistics? = nil,
networks: [NetworkStatistics]? = nil,
memoryEvents: MemoryEventStatistics? = nil
memoryEvents: MemoryEventStatistics? = nil,
filesystem: [FilesystemStatistics]? = nil
) {
self.id = id
self.process = process
Expand All @@ -40,6 +42,7 @@ public struct ContainerStatistics: Sendable {
self.blockIO = blockIO
self.networks = networks
self.memoryEvents = memoryEvents
self.filesystem = filesystem
}

/// Process statistics for a container.
Expand Down Expand Up @@ -220,6 +223,31 @@ public struct ContainerStatistics: Sendable {
self.oomKill = oomKill
}
}

/// Filesystem occupancy for a single mount, from statfs(2).
public struct FilesystemStatistics: Sendable {
/// The mount point this entry describes.
public var mountPoint: String
/// f_bsize: block size in bytes; the unit blocks/freeBlocks are counted in.
public var blockSize: UInt64
/// f_blocks: total blocks in the filesystem.
public var blocks: UInt64
/// f_bfree: free blocks in the filesystem (includes blocks reserved for root).
public var freeBlocks: UInt64
/// f_files: total inodes in the filesystem.
public var inodes: UInt64
/// f_ffree: free inodes in the filesystem.
public var freeInodes: UInt64

public init(mountPoint: String, blockSize: UInt64, blocks: UInt64, freeBlocks: UInt64, inodes: UInt64, freeInodes: UInt64) {
self.mountPoint = mountPoint
self.blockSize = blockSize
self.blocks = blocks
self.freeBlocks = freeBlocks
self.inodes = inodes
self.freeInodes = freeInodes
}
}
}

/// Categories of statistics that can be requested.
Expand All @@ -242,7 +270,9 @@ public struct StatCategory: OptionSet, Sendable {
public static let network = StatCategory(rawValue: 1 << 4)
/// Memory event counters (OOM kills, pressure events, etc.).
public static let memoryEvents = StatCategory(rawValue: 1 << 5)
/// Filesystem occupancy statistics.
public static let filesystem = StatCategory(rawValue: 1 << 6)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this 1 << 6 mean

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Followed the convention. Internally, the guest side checks that bit to stat filesystem or not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though it'd be better using a named variable instead of a number..

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree variable is better than magic number. I was just trying other understand why these StatCategories are bit based type not an enum or something else


/// All available statistics categories.
public static let all: StatCategory = [.process, .memory, .cpu, .blockIO, .network, .memoryEvents]
public static let all: StatCategory = [.process, .memory, .cpu, .blockIO, .network, .memoryEvents, .filesystem]
}
105 changes: 103 additions & 2 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public nonisolated enum Com_Apple_Containerization_Sandbox_V3_StatCategory: Swif
case blockIo // = 4
case network // = 5
case memoryEvents // = 6
case filesystem // = 7
case UNRECOGNIZED(Int)

public init() {
Expand All @@ -66,6 +67,7 @@ public nonisolated enum Com_Apple_Containerization_Sandbox_V3_StatCategory: Swif
case 4: self = .blockIo
case 5: self = .network
case 6: self = .memoryEvents
case 7: self = .filesystem
default: self = .UNRECOGNIZED(rawValue)
}
}
Expand All @@ -79,6 +81,7 @@ public nonisolated enum Com_Apple_Containerization_Sandbox_V3_StatCategory: Swif
case .blockIo: return 4
case .network: return 5
case .memoryEvents: return 6
case .filesystem: return 7
case .UNRECOGNIZED(let i): return i
}
}
Expand All @@ -92,6 +95,7 @@ public nonisolated enum Com_Apple_Containerization_Sandbox_V3_StatCategory: Swif
.blockIo,
.network,
.memoryEvents,
.filesystem,
]

}
Expand Down Expand Up @@ -1595,6 +1599,11 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_ContainerStats:
/// Clears the value of `memoryEvents`. Subsequent reads from it will return its default value.
public mutating func clearMemoryEvents() {_uniqueStorage()._memoryEvents = nil}

public var filesystem: [Com_Apple_Containerization_Sandbox_V3_FilesystemStats] {
get {_storage._filesystem}
set {_uniqueStorage()._filesystem = newValue}
}

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand Down Expand Up @@ -1768,12 +1777,42 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_MemoryEventStats
public init() {}
}

/// Raw statfs(2) fields for a single mount, sufficient to compute used
/// bytes/inodes host-side.
public nonisolated struct Com_Apple_Containerization_Sandbox_V3_FilesystemStats: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.

/// The mount point this entry describes.
public var mountPoint: String = String()

/// f_bsize: block size in bytes; the unit blocks/free_blocks are counted in.
public var blockSize: UInt64 = 0

/// f_blocks: total blocks in the filesystem.
public var blocks: UInt64 = 0

/// f_bfree: free blocks in the filesystem (includes blocks reserved for root).
public var freeBlocks: UInt64 = 0

/// f_files: total inodes in the filesystem.
public var inodes: UInt64 = 0

/// f_ffree: free inodes in the filesystem.
public var freeInodes: UInt64 = 0

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
}

// MARK: - Code below here is support for the SwiftProtobuf runtime.

fileprivate nonisolated let _protobuf_package = "com.apple.containerization.sandbox.v3"

nonisolated extension Com_Apple_Containerization_Sandbox_V3_StatCategory: SwiftProtobuf._ProtoNameProviding {
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0STAT_CATEGORY_UNSPECIFIED\0\u{1}STAT_CATEGORY_PROCESS\0\u{1}STAT_CATEGORY_MEMORY\0\u{1}STAT_CATEGORY_CPU\0\u{1}STAT_CATEGORY_BLOCK_IO\0\u{1}STAT_CATEGORY_NETWORK\0\u{1}STAT_CATEGORY_MEMORY_EVENTS\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0STAT_CATEGORY_UNSPECIFIED\0\u{1}STAT_CATEGORY_PROCESS\0\u{1}STAT_CATEGORY_MEMORY\0\u{1}STAT_CATEGORY_CPU\0\u{1}STAT_CATEGORY_BLOCK_IO\0\u{1}STAT_CATEGORY_NETWORK\0\u{1}STAT_CATEGORY_MEMORY_EVENTS\0\u{1}STAT_CATEGORY_FILESYSTEM\0")
}

nonisolated extension Com_Apple_Containerization_Sandbox_V3_Stdio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
Expand Down Expand Up @@ -4232,7 +4271,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStatisticsR

nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".ContainerStats"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}container_id\0\u{1}process\0\u{1}memory\0\u{1}cpu\0\u{3}block_io\0\u{1}networks\0\u{3}memory_events\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}container_id\0\u{1}process\0\u{1}memory\0\u{1}cpu\0\u{3}block_io\0\u{1}networks\0\u{3}memory_events\0\u{1}filesystem\0")

fileprivate class _StorageClass {
var _containerID: String = String()
Expand All @@ -4242,6 +4281,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif
var _blockIo: Com_Apple_Containerization_Sandbox_V3_BlockIOStats? = nil
var _networks: [Com_Apple_Containerization_Sandbox_V3_NetworkStats] = []
var _memoryEvents: Com_Apple_Containerization_Sandbox_V3_MemoryEventStats? = nil
var _filesystem: [Com_Apple_Containerization_Sandbox_V3_FilesystemStats] = []

// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
Expand All @@ -4259,6 +4299,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif
_blockIo = source._blockIo
_networks = source._networks
_memoryEvents = source._memoryEvents
_filesystem = source._filesystem
}
}

Expand All @@ -4284,6 +4325,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif
case 5: try { try decoder.decodeSingularMessageField(value: &_storage._blockIo) }()
case 6: try { try decoder.decodeRepeatedMessageField(value: &_storage._networks) }()
case 7: try { try decoder.decodeSingularMessageField(value: &_storage._memoryEvents) }()
case 8: try { try decoder.decodeRepeatedMessageField(value: &_storage._filesystem) }()
default: break
}
}
Expand Down Expand Up @@ -4317,6 +4359,9 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif
try { if let v = _storage._memoryEvents {
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
} }()
if !_storage._filesystem.isEmpty {
try visitor.visitRepeatedMessageField(value: _storage._filesystem, fieldNumber: 8)
}
}
try unknownFields.traverse(visitor: &visitor)
}
Expand All @@ -4333,6 +4378,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif
if _storage._blockIo != rhs_storage._blockIo {return false}
if _storage._networks != rhs_storage._networks {return false}
if _storage._memoryEvents != rhs_storage._memoryEvents {return false}
if _storage._filesystem != rhs_storage._filesystem {return false}
return true
}
if !storagesAreEqual {return false}
Expand Down Expand Up @@ -4736,3 +4782,58 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_MemoryEventStats: Sw
return true
}
}

nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".FilesystemStats"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}mount_point\0\u{3}block_size\0\u{1}blocks\0\u{3}free_blocks\0\u{1}inodes\0\u{3}free_inodes\0")

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.mountPoint) }()
case 2: try { try decoder.decodeSingularUInt64Field(value: &self.blockSize) }()
case 3: try { try decoder.decodeSingularUInt64Field(value: &self.blocks) }()
case 4: try { try decoder.decodeSingularUInt64Field(value: &self.freeBlocks) }()
case 5: try { try decoder.decodeSingularUInt64Field(value: &self.inodes) }()
case 6: try { try decoder.decodeSingularUInt64Field(value: &self.freeInodes) }()
default: break
}
}
}

public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.mountPoint.isEmpty {
try visitor.visitSingularStringField(value: self.mountPoint, fieldNumber: 1)
}
if self.blockSize != 0 {
try visitor.visitSingularUInt64Field(value: self.blockSize, fieldNumber: 2)
}
if self.blocks != 0 {
try visitor.visitSingularUInt64Field(value: self.blocks, fieldNumber: 3)
}
if self.freeBlocks != 0 {
try visitor.visitSingularUInt64Field(value: self.freeBlocks, fieldNumber: 4)
}
if self.inodes != 0 {
try visitor.visitSingularUInt64Field(value: self.inodes, fieldNumber: 5)
}
if self.freeInodes != 0 {
try visitor.visitSingularUInt64Field(value: self.freeInodes, fieldNumber: 6)
}
try unknownFields.traverse(visitor: &visitor)
}

public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_FilesystemStats, rhs: Com_Apple_Containerization_Sandbox_V3_FilesystemStats) -> Bool {
if lhs.mountPoint != rhs.mountPoint {return false}
if lhs.blockSize != rhs.blockSize {return false}
if lhs.blocks != rhs.blocks {return false}
if lhs.freeBlocks != rhs.freeBlocks {return false}
if lhs.inodes != rhs.inodes {return false}
if lhs.freeInodes != rhs.freeInodes {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
19 changes: 19 additions & 0 deletions Sources/Containerization/SandboxContext/SandboxContext.proto
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ enum StatCategory {
STAT_CATEGORY_BLOCK_IO = 4;
STAT_CATEGORY_NETWORK = 5;
STAT_CATEGORY_MEMORY_EVENTS = 6;
STAT_CATEGORY_FILESYSTEM = 7;
}

message ContainerStatisticsRequest {
Expand All @@ -427,6 +428,7 @@ message ContainerStats {
BlockIOStats block_io = 5;
repeated NetworkStats networks = 6;
MemoryEventStats memory_events = 7;
repeated FilesystemStats filesystem = 8;
}

message ProcessStats {
Expand Down Expand Up @@ -500,3 +502,20 @@ message MemoryEventStats {
// Number of times charge for memory failed because of limit.
uint64 oom_group_kill = 6;
}

// Raw statfs(2) fields for a single mount, sufficient to compute used
// bytes/inodes host-side.
message FilesystemStats {
// The mount point this entry describes.
string mount_point = 1;
// f_bsize: block size in bytes; the unit blocks/free_blocks are counted in.
uint64 block_size = 2;
// f_blocks: total blocks in the filesystem.
uint64 blocks = 3;
// f_bfree: free blocks in the filesystem (includes blocks reserved for root).
uint64 free_blocks = 4;
// f_files: total inodes in the filesystem.
uint64 inodes = 5;
// f_ffree: free inodes in the filesystem.
uint64 free_inodes = 6;
}
16 changes: 15 additions & 1 deletion Sources/Containerization/Vminitd.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,18 @@ extension Vminitd: VirtualMachineAgent {
max: protoStats.memoryEvents.max,
oom: protoStats.memoryEvents.oom,
oomKill: protoStats.memoryEvents.oomKill
) : nil
) : nil,
filesystem: categories.contains(.filesystem)
? protoStats.filesystem.map { entry in
ContainerStatistics.FilesystemStatistics(
mountPoint: entry.mountPoint,
blockSize: entry.blockSize,
blocks: entry.blocks,
freeBlocks: entry.freeBlocks,
inodes: entry.inodes,
freeInodes: entry.freeInodes
)
} : nil
)
}
}
Expand Down Expand Up @@ -632,6 +643,9 @@ extension StatCategory {
if contains(.memoryEvents) {
categories.append(.memoryEvents)
}
if contains(.filesystem) {
categories.append(.filesystem)
}
return categories
}
}
Expand Down
13 changes: 13 additions & 0 deletions Sources/Integration/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -884,11 +884,24 @@ extension IntegrationSuite {
throw IntegrationError.assert(msg: "CPU usage should be > 0, got \(stats.cpu?.usageUsec ?? 0)")
}

guard let filesystem = stats.filesystem?.first, filesystem.blocks > filesystem.freeBlocks else {
throw IntegrationError.assert(
msg: "filesystem used blocks should be > 0, got blocks=\(stats.filesystem?.first?.blocks ?? 0) freeBlocks=\(stats.filesystem?.first?.freeBlocks ?? 0)")
}

guard filesystem.inodes > filesystem.freeInodes else {
throw IntegrationError.assert(msg: "filesystem used inodes should be > 0, got inodes=\(filesystem.inodes) freeInodes=\(filesystem.freeInodes)")
}

let usedBytes = (filesystem.blocks - filesystem.freeBlocks) * filesystem.blockSize
let inodesUsed = filesystem.inodes - filesystem.freeInodes

print("Container statistics:")
print(" Processes: \(process.current)")
print(" Memory: \(memory.usageBytes) bytes")
print(" CPU: \(cpu.usageUsec) usec")
print(" Networks: \(stats.networks?.count ?? 0) interfaces")
print(" Filesystem: \(usedBytes) bytes, \(inodesUsed) inodes")

try await container.stop()
} catch {
Expand Down
21 changes: 21 additions & 0 deletions vminitd/Sources/LCShim/include/syscall.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,25 @@ int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags);

int CZ_prctl_set_no_new_privs();

// Mirrors struct statfs (minus its reserved padding). struct statfs itself
// isn't reliably importable from Swift across all target architectures via
// the Musl/Glibc modulemaps, so this wraps statfs(2) and copies out plain
// integer fields instead. f_fsid (a 2-int opaque identifier) is packed into
// a single 64-bit value.
typedef struct {
long long f_type;
unsigned long long f_bsize;
unsigned long long f_blocks;
unsigned long long f_bfree;
unsigned long long f_bavail;
unsigned long long f_files;
unsigned long long f_ffree;
long long f_fsid;
unsigned long long f_namelen;
unsigned long long f_frsize;
unsigned long long f_flags;
} CZ_Statfs;

int CZ_statfs(const char *path, CZ_Statfs *out);

#endif
Loading
Loading