diff --git a/vminitd/Sources/vminitd/Cgroup2Manager.swift b/vminitd/Sources/vminitd/Cgroup2Manager.swift index ae02a1c3..bbbb818b 100644 --- a/vminitd/Sources/vminitd/Cgroup2Manager.swift +++ b/vminitd/Sources/vminitd/Cgroup2Manager.swift @@ -197,6 +197,354 @@ struct Cgroup2Manager: Sendable { } try FileManager.default.removeItem(at: self.path) } + + func stats() throws -> Cgroup2Stats { + let pidsStats = try self.readPidsStats() + let memoryStats = try self.readMemoryStats() + let cpuStats = try self.readCPUStats() + let ioStats = try self.readIOStats() + + return Cgroup2Stats( + pids: pidsStats, + memory: memoryStats, + cpu: cpuStats, + io: ioStats + ) + } + + private func readFileContent(fileName: String) throws -> String? { + let filePath = self.path.appending(path: fileName) + guard FileManager.default.fileExists(atPath: filePath.path) else { + return nil + } + return try String(contentsOf: filePath, encoding: .utf8) + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + private func parseSingleValue(_ content: String?) -> UInt64? { + guard let content = content, !content.isEmpty else { return nil } + return UInt64(content) + } + + private func parseKeyValuePairs(_ content: String?) -> [String: UInt64] { + guard let content = content else { return [:] } + var result: [String: UInt64] = [:] + + for line in content.components(separatedBy: .newlines) { + let parts = line.components(separatedBy: .whitespaces) + if parts.count == 2, let value = UInt64(parts[1]) { + result[parts[0]] = value + } + } + return result + } + + private func readPidsStats() throws -> PidsStats? { + guard let currentContent = try readFileContent(fileName: "pids.current"), + let current = parseSingleValue(currentContent) + else { + return nil + } + + let maxContent = try readFileContent(fileName: "pids.max") + let max = parseSingleValue(maxContent) + + return PidsStats(current: current, max: max) + } + + private func readMemoryStats() throws -> MemoryStats? { + guard let usageContent = try readFileContent(fileName: "memory.current"), + let usage = parseSingleValue(usageContent) + else { + return nil + } + + let usageLimit = parseSingleValue(try readFileContent(fileName: "memory.max")) + let swapUsage = parseSingleValue(try readFileContent(fileName: "memory.swap.current")) + let swapLimit = parseSingleValue(try readFileContent(fileName: "memory.swap.max")) + + let statContent = try readFileContent(fileName: "memory.stat") + let statValues = parseKeyValuePairs(statContent) + + return MemoryStats( + usage: usage, + usageLimit: usageLimit, + swapUsage: swapUsage, + swapLimit: swapLimit, + anon: statValues["anon"] ?? 0, + file: statValues["file"] ?? 0, + kernelStack: statValues["kernel_stack"] ?? 0, + slab: statValues["slab"] ?? 0, + sock: statValues["sock"] ?? 0, + shmem: statValues["shmem"] ?? 0, + fileMapped: statValues["file_mapped"] ?? 0, + fileDirty: statValues["file_dirty"] ?? 0, + fileWriteback: statValues["file_writeback"] ?? 0, + pgfault: statValues["pgfault"] ?? 0, + pgmajfault: statValues["pgmajfault"] ?? 0, + workingsetRefault: statValues["workingset_refault"] ?? 0, + workingsetActivate: statValues["workingset_activate"] ?? 0, + workingsetNodereclaim: statValues["workingset_nodereclaim"] ?? 0, + inactiveAnon: statValues["inactive_anon"] ?? 0, + activeAnon: statValues["active_anon"] ?? 0, + inactiveFile: statValues["inactive_file"] ?? 0, + activeFile: statValues["active_file"] ?? 0 + ) + } + + private func readCPUStats() throws -> CPUStats? { + let statContent = try readFileContent(fileName: "cpu.stat") + let statValues = parseKeyValuePairs(statContent) + + guard !statValues.isEmpty else { + return nil + } + + return CPUStats( + usageUsec: statValues["usage_usec"] ?? 0, + userUsec: statValues["user_usec"] ?? 0, + systemUsec: statValues["system_usec"] ?? 0, + nrPeriods: statValues["nr_periods"] ?? 0, + nrThrottled: statValues["nr_throttled"] ?? 0, + throttledUsec: statValues["throttled_usec"] ?? 0 + ) + } + + private func readIOStats() throws -> IOStats? { + guard let statContent = try readFileContent(fileName: "io.stat") else { + return IOStats(entries: []) + } + + var entries: [IOEntry] = [] + + for line in statContent.components(separatedBy: .newlines) { + guard !line.isEmpty else { continue } + + let parts = line.components(separatedBy: .whitespaces) + guard parts.count >= 2 else { continue } + + let deviceParts = parts[0].components(separatedBy: ":") + guard deviceParts.count == 2, + let major = UInt64(deviceParts[0]), + let minor = UInt64(deviceParts[1]) + else { + continue + } + + var rbytes: UInt64 = 0 + var wbytes: UInt64 = 0 + var rios: UInt64 = 0 + var wios: UInt64 = 0 + var dbytes: UInt64 = 0 + var dios: UInt64 = 0 + + for i in 1..