Files
OpenStats/Packages/OpenStatsKit/Sources/Metrics/ProcessSampler.swift
T
gentpan 3caf690987 CPU 与内存详情改为 OpenStats 自己的设计:热力图、水位条、压力走势与按应用汇总
- CPU:占用、状态、30 秒变化与温度余量;核心热力图;核心分工与最忙核心;排队程度(每核平均负载与趋势);按应用汇总
- 内存:还可用多少与压力走势条;内存水位条;压缩省下的内存、压缩比与交换区实时读写;按应用汇总
- 数据层新增各核心占用历史、内存压力历史、压缩前大小与交换区换入换出累计量;进程采样返回全部可读进程
- 去掉仿 Stats 的圆环与仪表组件
2026-09-13 06:13:32 +05:00

92 lines
3.6 KiB
Swift

import Darwin
import Foundation
public struct ProcessSampler {
private struct Identity: Hashable {
let pid: Int32
let startTime: UInt64
}
struct Meta {
let name: String
let path: String?
let bundle: String?
}
private var previousCPU: [Identity: UInt64] = [:]
private var previousTime: UInt64 = 0
private var metaCache: [Identity: Meta] = [:]
public init() {}
/// 返回按 CPU 排序的进程;无权限读取的进程(通常属于 root)会被跳过。
/// 默认返回全部可读进程:按应用汇总内存时,CPU 很低但内存很大的进程也要算进去
public mutating func sample(limit: Int = .max) -> [ProcessUsage] {
let capacity = Int(proc_listallpids(nil, 0)) + 64
guard capacity > 64 else { return [] }
var pids = [pid_t](repeating: 0, count: capacity)
let count = Int(proc_listallpids(&pids, Int32(capacity * MemoryLayout<pid_t>.stride)))
guard count > 0 else { return [] }
// CPU 时间与墙钟时间都用 mach 绝对时间单位,比值无需换算时基
let now = mach_absolute_time()
let elapsed = previousTime > 0 ? now - previousTime : 0
var cpuTimes: [Identity: UInt64] = [:]
var liveMeta: [Identity: Meta] = [:]
var results: [ProcessUsage] = []
results.reserveCapacity(count)
for pid in pids.prefix(count) where pid > 0 {
var info = rusage_info_v4()
let status = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) {
proc_pid_rusage(pid, RUSAGE_INFO_V4, $0)
}
}
guard status == 0 else { continue }
let identity = Identity(pid: pid, startTime: info.ri_proc_start_abstime)
let cpuTime = info.ri_user_time + info.ri_system_time
cpuTimes[identity] = cpuTime
let meta = metaCache[identity] ?? Self.meta(for: pid)
liveMeta[identity] = meta
var cpu = 0.0
if elapsed > 0, let before = previousCPU[identity], cpuTime >= before {
cpu = Double(cpuTime - before) / Double(elapsed)
}
results.append(ProcessUsage(pid: pid, name: meta.name, executablePath: meta.path,
appBundlePath: meta.bundle, cpu: cpu, memory: info.ri_phys_footprint))
}
previousCPU = cpuTimes
previousTime = now
metaCache = liveMeta
results.sort { $0.cpu == $1.cpu ? $0.memory > $1.memory : $0.cpu > $1.cpu }
return Array(results.prefix(limit))
}
static func meta(for pid: pid_t) -> Meta {
var pathBuffer = [CChar](repeating: 0, count: 4 * Int(MAXPATHLEN))
let pathLength = proc_pidpath(pid, &pathBuffer, UInt32(pathBuffer.count))
let path = pathLength > 0 ? String(nullTerminated: pathBuffer) : nil
var name = path.map { URL(fileURLWithPath: $0).lastPathComponent } ?? ""
if name.isEmpty {
var nameBuffer = [CChar](repeating: 0, count: 256)
proc_name(pid, &nameBuffer, UInt32(nameBuffer.count))
name = String(nullTerminated: nameBuffer)
}
// 取最外层 .app,辅助进程也能显示主应用图标
let bundle = path.flatMap { path -> String? in
guard let range = path.range(of: ".app/") else { return nil }
return String(path[..<range.lowerBound]) + ".app"
}
return Meta(name: name.isEmpty ? "PID \(pid)" : name, path: path, bundle: bundle)
}
}