771 lines
24 KiB
Swift
771 lines
24 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the Containerization 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.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#if os(Linux)
|
|
|
|
// NOTE: Ideally this should live in ContainerizationOS/Linux, or just ContainerizationCgroups
|
|
// or something similar, but it's not there yet. It does what we need, but it'd need a lot more
|
|
// features and testing before it's ready to be public.
|
|
|
|
#if canImport(Musl)
|
|
import Musl
|
|
#elseif canImport(Glibc)
|
|
import Glibc
|
|
#endif
|
|
|
|
import LCShim
|
|
import ContainerizationOCI
|
|
import ContainerizationOS
|
|
import Foundation
|
|
import Logging
|
|
|
|
package enum Cgroup2Controller: String {
|
|
case pids
|
|
case memory
|
|
case cpuset
|
|
case cpu
|
|
case io
|
|
case hugetlb
|
|
}
|
|
|
|
// Extremely simple cgroup manager. Our needs are simple for now, and this is
|
|
// reflected in the type.
|
|
public struct Cgroup2Manager: Sendable {
|
|
public static let defaultMountPoint = URL(filePath: "/sys/fs/cgroup")
|
|
|
|
private static let killFile = "cgroup.kill"
|
|
private static let procsFile = "cgroup.procs"
|
|
private static let subtreeControlFile = "cgroup.subtree_control"
|
|
|
|
private static let cg2Magic = 0x6367_7270
|
|
|
|
private let mountPoint: URL
|
|
private let path: URL
|
|
private let logger: Logger?
|
|
|
|
package init(
|
|
mountPoint: URL = Self.defaultMountPoint,
|
|
group: URL,
|
|
logger: Logger? = nil
|
|
) {
|
|
self.mountPoint = mountPoint
|
|
self.path = mountPoint.appending(path: group.path)
|
|
self.logger = logger
|
|
}
|
|
|
|
public static func load(
|
|
mountPoint: URL = Self.defaultMountPoint,
|
|
group: URL,
|
|
logger: Logger? = nil
|
|
) throws -> Cgroup2Manager {
|
|
let path = mountPoint.appending(path: group.path)
|
|
var s = statfs()
|
|
let res = statfs(path.path, &s)
|
|
if res != 0 {
|
|
throw Error.errno(errno: errno, message: "failed to statfs \(path.path)")
|
|
}
|
|
if Int64(s.f_type) != Self.cg2Magic {
|
|
throw Error.notCgroup
|
|
}
|
|
return Cgroup2Manager(
|
|
mountPoint: mountPoint,
|
|
group: group,
|
|
logger: logger
|
|
)
|
|
}
|
|
|
|
package static func loadFromPid(pid: Int32, logger: Logger? = nil) throws -> Cgroup2Manager {
|
|
let procCgPath = URL(filePath: "/proc/\(pid)/cgroup")
|
|
let fh = try FileHandle(forReadingFrom: procCgPath)
|
|
guard let data = try fh.readToEnd() else {
|
|
throw Error.errno(errno: errno, message: "failed to read \(procCgPath)")
|
|
}
|
|
|
|
// If this fails we have bigger problems.
|
|
let str = String(data: data, encoding: .utf8)!
|
|
let parts = str.split(separator: ":")
|
|
if parts[0] != "0" {
|
|
throw Error.cgroup1
|
|
}
|
|
|
|
// We should really read /proc/pid/mountinfo, but for now just assume
|
|
// it's always at /sys/fs/cgroup.
|
|
let path = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return Cgroup2Manager(group: URL(filePath: String(path)), logger: logger)
|
|
}
|
|
|
|
package func create(perms: Int16 = 0o755) throws {
|
|
self.logger?.info(
|
|
"creating cgroup manager",
|
|
metadata: [
|
|
"mountpoint": "\(self.mountPoint.path)",
|
|
"path": "\(self.path.path)",
|
|
])
|
|
|
|
try FileManager.default.createDirectory(
|
|
at: self.path,
|
|
withIntermediateDirectories: true,
|
|
attributes: [.posixPermissions: perms]
|
|
)
|
|
}
|
|
|
|
private static func writeValue(path: URL, value: String, fileName: String) throws {
|
|
let file = path.appending(path: fileName)
|
|
let fd = open(file.path, O_WRONLY, 0)
|
|
if fd == -1 {
|
|
throw Error.errno(errno: errno, message: "failed to open \(file.path)")
|
|
}
|
|
defer { close(fd) }
|
|
|
|
let bytes = Array(value.utf8)
|
|
let res = Syscall.retrying {
|
|
bytes.withUnsafeBytes { write(fd, $0.baseAddress!, bytes.count) }
|
|
}
|
|
if res == -1 {
|
|
throw Error.errno(errno: errno, message: "failed to write to \(file.path)")
|
|
}
|
|
}
|
|
|
|
package func toggleSubtreeControllers(controllers: [Cgroup2Controller], enable: Bool) throws {
|
|
let value = controllers.map { (enable ? "+" : "-") + $0.rawValue }.joined(separator: " ")
|
|
let mountComponents = self.mountPoint.pathComponents
|
|
let pathComponents = self.path.pathComponents
|
|
|
|
// First ensure it's set on the root.
|
|
var current = self.mountPoint
|
|
try Self.writeValue(
|
|
path: current,
|
|
value: value,
|
|
fileName: Self.subtreeControlFile
|
|
)
|
|
|
|
// Toggle everything except the leaf, as otherwise we won't be able to write
|
|
// to cgroup.procs, and what fun is that :)
|
|
if mountComponents.count < pathComponents.count - 1 {
|
|
for i in mountComponents.count...pathComponents.count - 2 {
|
|
current = current.appending(path: pathComponents[i])
|
|
try Self.writeValue(
|
|
path: current,
|
|
value: value,
|
|
fileName: Self.subtreeControlFile
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
package func toggleAllAvailableControllers(enable: Bool) throws {
|
|
// Read available controllers from cgroup.controllers
|
|
let controllersFile = self.mountPoint.appending(path: "cgroup.controllers")
|
|
let controllersContent = try String(contentsOf: controllersFile, encoding: .utf8)
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
// Parse controller names and convert to our enum
|
|
let availableControllers =
|
|
controllersContent
|
|
.split(separator: " ")
|
|
.compactMap { Cgroup2Controller(rawValue: String($0)) }
|
|
|
|
if !availableControllers.isEmpty {
|
|
try toggleSubtreeControllers(controllers: availableControllers, enable: enable)
|
|
}
|
|
}
|
|
|
|
public func addProcess(pid: Int32) throws {
|
|
self.logger?.debug(
|
|
"adding new proc to cgroup",
|
|
metadata: [
|
|
"mountpoint": "\(self.mountPoint.path)",
|
|
"path": "\(self.path.path)",
|
|
])
|
|
|
|
let pidStr = String(pid)
|
|
try Self.writeValue(
|
|
path: self.path,
|
|
value: pidStr,
|
|
fileName: Self.procsFile
|
|
)
|
|
}
|
|
|
|
public func applyResources(resources: ContainerizationOCI.LinuxResources) throws {
|
|
self.logger?.debug(
|
|
"applying cgroup resources",
|
|
metadata: [
|
|
"path": "\(self.path.path)"
|
|
])
|
|
|
|
if let memory = resources.memory, let limit = memory.limit {
|
|
// The OCI spec defines -1 as unlimited; cgroup v2 expects "max".
|
|
let value = limit < 0 ? "max" : String(limit)
|
|
try Self.writeValue(
|
|
path: self.path,
|
|
value: value,
|
|
fileName: "memory.max"
|
|
)
|
|
}
|
|
|
|
if let cpu = resources.cpu, let quota = cpu.quota, let period = cpu.period {
|
|
// cpu.max format is "quota period"
|
|
let value = "\(quota) \(period)"
|
|
try Self.writeValue(
|
|
path: self.path,
|
|
value: value,
|
|
fileName: "cpu.max"
|
|
)
|
|
}
|
|
|
|
if let pids = resources.pids {
|
|
// The OCI spec defines -1 as unlimited; cgroup v2 expects "max".
|
|
let value = pids.limit < 0 ? "max" : String(pids.limit)
|
|
try Self.writeValue(
|
|
path: self.path,
|
|
value: value,
|
|
fileName: "pids.max"
|
|
)
|
|
}
|
|
}
|
|
|
|
package func setMemoryHigh(bytes: UInt64) throws {
|
|
self.logger?.debug(
|
|
"setting memory.high",
|
|
metadata: [
|
|
"path": "\(self.path.path)",
|
|
"bytes": "\(bytes)",
|
|
])
|
|
|
|
try Self.writeValue(
|
|
path: self.path,
|
|
value: String(bytes),
|
|
fileName: "memory.high"
|
|
)
|
|
}
|
|
|
|
package func setMemoryLow(bytes: UInt64) throws {
|
|
self.logger?.debug(
|
|
"setting memory.low",
|
|
metadata: [
|
|
"path": "\(self.path.path)",
|
|
"bytes": "\(bytes)",
|
|
]
|
|
)
|
|
|
|
try Self.writeValue(
|
|
path: self.path,
|
|
value: String(bytes),
|
|
fileName: "memory.low")
|
|
}
|
|
|
|
package func getMemoryEvents() throws -> MemoryEvents {
|
|
let content = try readFileContent(fileName: "memory.events")
|
|
let values = parseKeyValuePairs(content)
|
|
|
|
return MemoryEvents(
|
|
low: values["low"] ?? 0,
|
|
high: values["high"] ?? 0,
|
|
max: values["max"] ?? 0,
|
|
oom: values["oom"] ?? 0,
|
|
oomKill: values["oom_kill"] ?? 0
|
|
)
|
|
}
|
|
|
|
package func getMemoryEventsPath() -> String {
|
|
self.path.appending(path: "memory.events").path
|
|
}
|
|
|
|
package func kill() throws {
|
|
try Self.writeValue(
|
|
path: self.path,
|
|
value: "1",
|
|
fileName: Self.killFile
|
|
)
|
|
}
|
|
|
|
package func delete(force: Bool = false) throws {
|
|
self.logger?.info(
|
|
"deleting cgroup manager",
|
|
metadata: [
|
|
"mountpoint": "\(self.mountPoint.path)",
|
|
"path": "\(self.path.path)",
|
|
])
|
|
|
|
if force {
|
|
try self.kill()
|
|
}
|
|
|
|
// Recursively remove child cgroups first
|
|
try removeChildCgroups(at: self.path, force: force)
|
|
|
|
let result = rmdir(self.path.path)
|
|
if result != 0 {
|
|
throw Error.errno(errno: errno, message: "failed to remove cgroup directory \(self.path.path)")
|
|
}
|
|
}
|
|
|
|
private func removeChildCgroups(at path: URL, force: Bool) throws {
|
|
let fileManager = FileManager.default
|
|
|
|
guard let contents = try? fileManager.contentsOfDirectory(atPath: path.path) else {
|
|
return
|
|
}
|
|
|
|
// Remove child directories (potential nested cgroups) first
|
|
for item in contents {
|
|
let childPath = path.appending(path: item)
|
|
var isDirectory: ObjCBool = false
|
|
|
|
if fileManager.fileExists(atPath: childPath.path, isDirectory: &isDirectory) && isDirectory.boolValue {
|
|
if force {
|
|
try Self.writeValue(
|
|
path: childPath,
|
|
value: "1",
|
|
fileName: Self.killFile
|
|
)
|
|
}
|
|
|
|
try removeChildCgroups(at: childPath, force: force)
|
|
let result = rmdir(childPath.path)
|
|
if result != 0 {
|
|
throw Error.errno(errno: errno, message: "failed to remove child cgroup \(childPath.path)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
package func stats(_ categories: Cgroup2StatsCategory = .all) throws -> Cgroup2Stats {
|
|
Cgroup2Stats(
|
|
pids: categories.contains(.pids) ? try self.readPidsStats() : nil,
|
|
memory: categories.contains(.memory) ? try self.readMemoryStats() : nil,
|
|
cpu: categories.contains(.cpu) ? try self.readCPUStats() : nil,
|
|
io: categories.contains(.io) ? try self.readIOStats() : nil
|
|
)
|
|
}
|
|
|
|
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 }
|
|
if content == "max" {
|
|
return UInt64.max
|
|
}
|
|
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,
|
|
workingsetRefaultAnon: statValues["workingset_refault_anon"] ?? 0,
|
|
workingsetRefaultFile: statValues["workingset_refault_file"] ?? 0,
|
|
workingsetActivate: statValues["workingset_activate"] ?? 0,
|
|
workingsetNodereclaim: statValues["workingset_nodereclaim"] ?? 0,
|
|
pgstealKswapd: statValues["pgsteal_kswapd"] ?? 0,
|
|
pgstealDirect: statValues["pgsteal_direct"] ?? 0,
|
|
pgstealKhugepaged: statValues["pgsteal_khugepaged"] ?? 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..<parts.count {
|
|
let keyValue = parts[i].components(separatedBy: "=")
|
|
guard keyValue.count == 2, let value = UInt64(keyValue[1]) else { continue }
|
|
|
|
switch keyValue[0] {
|
|
case "rbytes":
|
|
rbytes = value
|
|
case "wbytes":
|
|
wbytes = value
|
|
case "rios":
|
|
rios = value
|
|
case "wios":
|
|
wios = value
|
|
case "dbytes":
|
|
dbytes = value
|
|
case "dios":
|
|
dios = value
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
entries.append(
|
|
IOEntry(
|
|
major: major,
|
|
minor: minor,
|
|
rbytes: rbytes,
|
|
wbytes: wbytes,
|
|
rios: rios,
|
|
wios: wios,
|
|
dbytes: dbytes,
|
|
dios: dios
|
|
))
|
|
}
|
|
|
|
return IOStats(entries: entries)
|
|
}
|
|
}
|
|
|
|
// Selects which cgroup stat groups to read.
|
|
package struct Cgroup2StatsCategory: OptionSet, Sendable {
|
|
package let rawValue: UInt8
|
|
|
|
package init(rawValue: UInt8) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
package static let pids = Cgroup2StatsCategory(rawValue: 1 << 0)
|
|
package static let memory = Cgroup2StatsCategory(rawValue: 1 << 1)
|
|
package static let cpu = Cgroup2StatsCategory(rawValue: 1 << 2)
|
|
package static let io = Cgroup2StatsCategory(rawValue: 1 << 3)
|
|
|
|
package static let all: Cgroup2StatsCategory = [.pids, .memory, .cpu, .io]
|
|
}
|
|
|
|
package struct Cgroup2Stats: Sendable {
|
|
package var pids: PidsStats?
|
|
package var memory: MemoryStats?
|
|
package var cpu: CPUStats?
|
|
package var io: IOStats?
|
|
|
|
package init(
|
|
pids: PidsStats? = nil,
|
|
memory: MemoryStats? = nil,
|
|
cpu: CPUStats? = nil,
|
|
io: IOStats? = nil
|
|
) {
|
|
self.pids = pids
|
|
self.memory = memory
|
|
self.cpu = cpu
|
|
self.io = io
|
|
}
|
|
}
|
|
|
|
package struct PidsStats: Sendable {
|
|
package var current: UInt64
|
|
package var max: UInt64?
|
|
|
|
package init(current: UInt64, max: UInt64? = nil) {
|
|
self.current = current
|
|
self.max = max
|
|
}
|
|
}
|
|
|
|
package struct MemoryStats: Sendable {
|
|
package var usage: UInt64
|
|
package var usageLimit: UInt64?
|
|
package var swapUsage: UInt64?
|
|
package var swapLimit: UInt64?
|
|
|
|
package var anon: UInt64
|
|
package var file: UInt64
|
|
package var kernelStack: UInt64
|
|
package var slab: UInt64
|
|
package var sock: UInt64
|
|
package var shmem: UInt64
|
|
package var fileMapped: UInt64
|
|
package var fileDirty: UInt64
|
|
package var fileWriteback: UInt64
|
|
|
|
package var pgfault: UInt64
|
|
package var pgmajfault: UInt64
|
|
|
|
package var workingsetRefaultAnon: UInt64
|
|
package var workingsetRefaultFile: UInt64
|
|
package var workingsetActivate: UInt64
|
|
package var workingsetNodereclaim: UInt64
|
|
|
|
package var pgstealKswapd: UInt64
|
|
package var pgstealDirect: UInt64
|
|
package var pgstealKhugepaged: UInt64
|
|
|
|
package var inactiveAnon: UInt64
|
|
package var activeAnon: UInt64
|
|
package var inactiveFile: UInt64
|
|
package var activeFile: UInt64
|
|
|
|
package init(
|
|
usage: UInt64,
|
|
usageLimit: UInt64? = nil,
|
|
swapUsage: UInt64? = nil,
|
|
swapLimit: UInt64? = nil,
|
|
anon: UInt64 = 0,
|
|
file: UInt64 = 0,
|
|
kernelStack: UInt64 = 0,
|
|
slab: UInt64 = 0,
|
|
sock: UInt64 = 0,
|
|
shmem: UInt64 = 0,
|
|
fileMapped: UInt64 = 0,
|
|
fileDirty: UInt64 = 0,
|
|
fileWriteback: UInt64 = 0,
|
|
pgfault: UInt64 = 0,
|
|
pgmajfault: UInt64 = 0,
|
|
workingsetRefaultAnon: UInt64 = 0,
|
|
workingsetRefaultFile: UInt64 = 0,
|
|
workingsetActivate: UInt64 = 0,
|
|
workingsetNodereclaim: UInt64 = 0,
|
|
pgstealKswapd: UInt64 = 0,
|
|
pgstealDirect: UInt64 = 0,
|
|
pgstealKhugepaged: UInt64 = 0,
|
|
inactiveAnon: UInt64 = 0,
|
|
activeAnon: UInt64 = 0,
|
|
inactiveFile: UInt64 = 0,
|
|
activeFile: UInt64 = 0
|
|
) {
|
|
self.usage = usage
|
|
self.usageLimit = usageLimit
|
|
self.swapUsage = swapUsage
|
|
self.swapLimit = swapLimit
|
|
self.anon = anon
|
|
self.file = file
|
|
self.kernelStack = kernelStack
|
|
self.slab = slab
|
|
self.sock = sock
|
|
self.shmem = shmem
|
|
self.fileMapped = fileMapped
|
|
self.fileDirty = fileDirty
|
|
self.fileWriteback = fileWriteback
|
|
self.pgfault = pgfault
|
|
self.pgmajfault = pgmajfault
|
|
self.workingsetRefaultAnon = workingsetRefaultAnon
|
|
self.workingsetRefaultFile = workingsetRefaultFile
|
|
self.workingsetActivate = workingsetActivate
|
|
self.workingsetNodereclaim = workingsetNodereclaim
|
|
self.pgstealKswapd = pgstealKswapd
|
|
self.pgstealDirect = pgstealDirect
|
|
self.pgstealKhugepaged = pgstealKhugepaged
|
|
self.inactiveAnon = inactiveAnon
|
|
self.activeAnon = activeAnon
|
|
self.inactiveFile = inactiveFile
|
|
self.activeFile = activeFile
|
|
}
|
|
}
|
|
|
|
package struct CPUStats: Sendable {
|
|
package var usageUsec: UInt64
|
|
package var userUsec: UInt64
|
|
package var systemUsec: UInt64
|
|
package var nrPeriods: UInt64
|
|
package var nrThrottled: UInt64
|
|
package var throttledUsec: UInt64
|
|
|
|
package init(
|
|
usageUsec: UInt64 = 0,
|
|
userUsec: UInt64 = 0,
|
|
systemUsec: UInt64 = 0,
|
|
nrPeriods: UInt64 = 0,
|
|
nrThrottled: UInt64 = 0,
|
|
throttledUsec: UInt64 = 0
|
|
) {
|
|
self.usageUsec = usageUsec
|
|
self.userUsec = userUsec
|
|
self.systemUsec = systemUsec
|
|
self.nrPeriods = nrPeriods
|
|
self.nrThrottled = nrThrottled
|
|
self.throttledUsec = throttledUsec
|
|
}
|
|
}
|
|
|
|
package struct IOStats: Sendable {
|
|
package var entries: [IOEntry]
|
|
|
|
package init(entries: [IOEntry] = []) {
|
|
self.entries = entries
|
|
}
|
|
}
|
|
|
|
package struct IOEntry: Sendable {
|
|
package var major: UInt64
|
|
package var minor: UInt64
|
|
package var rbytes: UInt64
|
|
package var wbytes: UInt64
|
|
package var rios: UInt64
|
|
package var wios: UInt64
|
|
package var dbytes: UInt64
|
|
package var dios: UInt64
|
|
|
|
package init(
|
|
major: UInt64,
|
|
minor: UInt64,
|
|
rbytes: UInt64 = 0,
|
|
wbytes: UInt64 = 0,
|
|
rios: UInt64 = 0,
|
|
wios: UInt64 = 0,
|
|
dbytes: UInt64 = 0,
|
|
dios: UInt64 = 0
|
|
) {
|
|
self.major = major
|
|
self.minor = minor
|
|
self.rbytes = rbytes
|
|
self.wbytes = wbytes
|
|
self.rios = rios
|
|
self.wios = wios
|
|
self.dbytes = dbytes
|
|
self.dios = dios
|
|
}
|
|
}
|
|
|
|
package struct MemoryEvents: Sendable {
|
|
package var low: UInt64
|
|
package var high: UInt64
|
|
package var max: UInt64
|
|
package var oom: UInt64
|
|
package var oomKill: UInt64
|
|
|
|
package init(
|
|
low: UInt64 = 0,
|
|
high: UInt64 = 0,
|
|
max: UInt64 = 0,
|
|
oom: UInt64 = 0,
|
|
oomKill: UInt64 = 0
|
|
) {
|
|
self.low = low
|
|
self.high = high
|
|
self.max = max
|
|
self.oom = oom
|
|
self.oomKill = oomKill
|
|
}
|
|
}
|
|
|
|
extension Cgroup2Manager {
|
|
package enum Error: Swift.Error, CustomStringConvertible {
|
|
case notCgroup
|
|
case cgroup1
|
|
case errno(errno: Int32, message: String)
|
|
case notExist(path: String)
|
|
|
|
package var description: String {
|
|
switch self {
|
|
case .errno(let errno, let message):
|
|
return "failed with errno \(errno): \(message)"
|
|
case .notExist(let path):
|
|
return "cgroup at path \(path) does not exist"
|
|
case .cgroup1:
|
|
return "tried to load a cgroup v1 path"
|
|
case .notCgroup:
|
|
return "path is not a cgroup mountpoint"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|