Files
OpenStats/Packages/OpenStatsKit/Sources/Metrics/ProcessSampler.swift
T
gentpan 8875b6e709 每项独立菜单栏与详情弹窗、网络详情与 DNS、侧边栏主窗口与统一外观
- 菜单栏可每项独立(点击弹出该项窄详情)或合并为一个(点击打开主窗口),详情区块可在设置中勾选
- 网络详情:镜像流量历史、ICMP 连接探测格子、接口与 Wi-Fi、本地与公网 IPv4 / IPv6、SVG 国旗归属地与 ASN、各进程流量
- DNS:刷新缓存,一键切换自动 / Cloudflare / Google / 腾讯 / 阿里云或手动填写;辅助工具协议升至 3,服务名与地址二次校验
- 主窗口:侧边栏切换仪表盘、各指标详情与工具页,宽高可调,卡片按比例伸缩
- 统一外观:白底蓝 / 黑底蓝,窗口、设置与弹窗共用底色,去掉玻璃效果
- 新增网络解析、DNS 校验与国旗代码校验的单元测试;更新 README、架构文档与隐私说明
2026-09-13 05:39:10 +05:00

91 lines
3.5 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)会被跳过。
public mutating func sample(limit: Int = 50) -> [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)
}
}