外观
知识库总览与全指标采集溯源
本文档是 Perfowl 客户端与采集基座(Collector Core)的知识库总览与架构溯源总纲。它剥离了日常研发中的琐碎调试复盘,全景复原产品从 0 到 1 的工程演进脉络、底层技术选型抉择,以及当前 64 项性能指标的真实采集服务、命令路径、数学计算模型与文档直达索引。
一、 核心设计哲学:为什么必须是“纯粹的无侵入”?
移动端性能评测行业长期存在两类技术路线:
- 侵入式 SDK 路线:需在 App 内集成 Probe/SDK,甚至依赖 Hook、方法混淆或重签名。这种方式不仅改变了 App 的原始二进制体积与链接行为,还会引入不可忽视的 “观察者效应(Observer Effect)” —— 探针本身抢占 CPU 与内存,导致测得的并非用户真实体验。
- 外部黑盒轮询路线:不集成 SDK,但为了获取高帧率曲线,采用高频截图(如 60fps 截图 OCR 差分)伪装逐帧真值,造成 USB 负载飙升、设备发热严重,且无法归因后台真实消耗。
Perfowl 的立项基石是“对齐 PerfDog 的非侵入式可信体验”:
- 零侵入红线:不改源码、不重签、不重打包、不注入动态库、不越狱、不依赖 UIAutomation/WDA 运行底座。
- 系统服务只读采集:全部数据均通过宿主机与 iOS 设备的 USB / Wi-Fi 建立的安全通道,接入 Apple 系统底层的开发者支持守护进程(DVT Services、Lockdown Relay、CoreProfile Kernel Trace),保证目标 App 处于 100% 原始运行环境。
- 本地 Session 优先:采集到的每一条 Raw 事件(Sysmontap、Graphics、CoreProfile kd_buf、Diagnostics)均采用 Append-only 的不可变 JSONL 格式完整落盘,支持脱机重放与离线重算,绝不强制依赖云端网络。
二、 采集基座的技术抉择:DTX 独占内核与 Clean Adapter 模式
在开源生态中,与 iOS 底层协议交互的知名库有两个:pymobiledevice3 与 py-ios-device。在从 0 到 1 的开发过程中,Perfowl 经历了深刻的协议调研与架构裁决:
┌────────────────────────────────────────────────────────┐
│ MacApp (SwiftUI) │
└───────────────────────────┬────────────────────────────┘
│ JSON-RPC / Subprocess
┌───────────────────────────▼────────────────────────────┐
│ Perfowl Collector Core (Python 3.12) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ pmd3_deep_services.py (自研适配层) │ │
│ │ - ProcessNetworkStatistics (PID 网络吞吐) │ │
│ │ - GPUCounterControlService (硬件 GPU Counter) │ │
│ │ - CoreProfileSessionTap Multi-Config (采样触发)│ │
│ └───────────────────────┬────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼────────────────────────┐ │
│ │ pymobiledevice3 (11.3.1 锁定版本) │ │
│ │ ★ 唯一运行时内核 (Sole DTX Transport Owner) │ │
│ └───────────────────────┬────────────────────────┘ │
└───────────────────────────┼────────────────────────────┘
│ USBMux / RSD Tunnel
┌───────────────────────────▼────────────────────────────┐
│ iOS Device (DVT / Lockdown Services) │
└────────────────────────────────────────────────────────┘1. 为什么不能同时混用两个开源库?
- DTX 连接互斥:iOS 设备的 Instruments 守护进程(
com.apple.instruments.server.services)基于独占式的 DTX 消息分发体系。若在同一个测试会话中,让两个不同的 Python 进程或两个不同的传输层并发抢占 DVT 端口,会导致会话抢占中断、DTX 序列号错乱,甚至触发 iOS 端守护进程崩溃重启。 - 时钟源分裂:不同库在事件分发、线程等待和时间戳打标上存在毫秒级甚至秒级偏差,无法满足 FPS、CPU 与网络指标在同一微秒级时间轴上的强对齐。
2. 架构最终确立:独占内核 + Clean Adapter 模式
Perfowl 确立了铁律规范(定义于 pins.py 与 adapters.py):
pymobiledevice3(v11.3.1):被裁定为唯一的物理通信与 DTX 传输内核(Kernel)。负责处理 USBMuxd 握手、iOS 17+ RemoteXPC / RSD 隧道解包、通道复用与基础 DVT 服务生命周期。py-ios-device(v2.4.26):被定性为纯研究参考源(Research Reference Only)。它在逆向研究 Apple 内部未公开 selector(如NetworkStatistics、GPU Counter的配置参数)时提供了巨大价值,但它在运行时绝不被导入、绝不打包进分发产物、绝不碰触 DTX Socket。- Perfowl 自研 Clean Adapter 层: 在
pmd3_deep_services.py中,Perfowl 继承pymobiledevice3.dtx_service.DtxService,纯手工实现了对未公开 DVT 服务的封装。这样既复用了pymobiledevice3健壮的单一连接,又安全获得了高阶深层指标能力。
三、 64 项指标全景采集谱系与直达索引
在当前产品基座中,静态 Registry 共包含 64 项指标。下表汇总了所有指标的真实采集服务、底层命令/类方法、实现归属与计算公式,并附有对应知识库单页的直达链接:
| 指标大类 | 指标名称 (点击直达) | metricID | 作用域 (Scope) | 系统服务标识 (Service) | 底层命令 / 方法 / 协议 | 实现层归属 | 单位与计算简式 |
|---|---|---|---|---|---|---|---|
| 计算 | APPCpu | ios.process.cpu.non_normalized | process | com.apple.instruments.server.services.sysmontap | Sysmontap.create(interval=500) | PMD3 Native | %;Processes[target_pid].cpuUsage |
| 计算 | TotalCPU (Raw) | ios.device.cpu.total | device | 同上 (sysmontap) | 同上 | PMD3 Native | %;SystemCPUUsage.CPU_TotalLoad |
| 计算 | TotalCPU (Norm) | ios.device.cpu.total.normalized | device | 同上 (sysmontap) | 同上 | Perfowl Engine | %;CPU_TotalLoad / EnabledCPUs (有效核数) |
| 计算 | CPU Core Usage | ios.device.cpu.core_usage | device | 同上 (sysmontap) | 同上 | PMD3 Native | %;PerCPUUsage[*].CPU_TotalLoad 数组 |
| 调度 | Thread Count | ios.process.thread.count | process | 同上 (sysmontap) | 同上 | PMD3 Native | count;Processes[target_pid].threadCount |
| 调度 | CSwitch | ios.process.scheduler.context_switch_rate | process | 同上 (sysmontap) | 同上 | Perfowl Engine | count/s;Δ(ctxSwitch) / Δt (同代差分) |
| 调度 | Wakeups | ios.process.scheduler.interrupt_wakeup_rate | process | 同上 (sysmontap) | 同上 | Perfowl Engine | count/s;Δ(intWakeups) / Δt |
| 内存 | Memory (Footprint) | ios.process.memory.physical_footprint | process | 同上 (sysmontap) | 同上 | PMD3 Native | Bytes;Processes[target_pid].physFootprint |
| 内存 | Resident Memory | ios.process.memory.resident | process | 同上 (sysmontap) | 同上 | PMD3 Native | Bytes;Processes[target_pid].memResidentSize |
| 内存 | XcodeMemory | ios.process.memory.xcode_gauge | process | 同上 (sysmontap) | 同上 | Perfowl Engine | Bytes;映射为 physFootprint (对齐 Xcode) |
| 内存 | RealMemory | ios.process.memory.real | process | 同上 (sysmontap) | 同上 | Perfowl Engine | Bytes;取 memResidentSize |
| 内存 | VirtualMemory | ios.process.memory.virtual | process | 同上 (sysmontap) | 同上 | PMD3 Native | Bytes;Processes[target_pid].memVirtualSize |
| 内存 | AvailableMemory | ios.device.memory.available | device | 同上 (sysmontap) | 同上 | Perfowl Engine | Bytes;vmFreeCount * 16384 (ARM64 16KB) |
| 存储 | ReadDiskBytes | ios.process.disk.read_rate | process | 同上 (sysmontap) | 同上 | Perfowl Engine | Byte/s;Δ(diskBytesRead) / Δt |
| 存储 | WriteDiskBytes | ios.process.disk.write_rate | process | 同上 (sysmontap) | 同上 | Perfowl Engine | Byte/s;Δ(diskBytesWritten) / Δt |
| 网络 | Send (Device) | ios.device.network.send_rate | device | com.apple.instruments.server.services.networking | NetworkMonitor(provider) | PMD3 Native | Byte/s;ConnectionUpdateEvent.tx_bytes 平滑 |
| 网络 | Recv (Device) | ios.device.network.receive_rate | device | 同上 (networking) | 同上 | PMD3 Native | Byte/s;ConnectionUpdateEvent.rx_bytes 平滑 |
| 网络 | Send (Process) | ios.process.network.send_rate | process | com.apple.xcode.debug-gauge-data-providers.NetworkStatistics | ProcessNetworkStatistics.sampleAttributes:forPIDs: | Perfowl Adapter (pmd3 DTX) | Byte/s;基于 Cocoa 时钟 Δ(tx.bytes) / Δt |
| 网络 | Recv (Process) | ios.process.network.receive_rate | process | 同上 (NetworkStatistics) | 同上 | Perfowl Adapter (pmd3 DTX) | Byte/s;基于 Cocoa 时钟 Δ(rx.bytes) / Δt |
| 渲染 | Foreground FPS | ios.foreground.fps | foreground | com.apple.instruments.server.services.graphics.opengl | Graphics(provider) 轮询 | PMD3 Native | fps;CoreAnimationFramesPerSecond (秒级) |
| 渲染 | GPU Render | ios.foreground.gpu.renderer_utilization | foreground | 同上 (graphics.opengl) | 同上 | PMD3 Native | %;Renderer Utilization % |
| 渲染 | GPU Tiler | ios.foreground.gpu.tiler_utilization | foreground | 同上 (graphics.opengl) | 同上 | PMD3 Native | %;Tiler Utilization % |
| 渲染 | GPU Device | ios.foreground.gpu.device_utilization | foreground | 同上 (graphics.opengl) | 同上 | PMD3 Native | %;Device Utilization % |
| 逐帧 | Frame Time | ios.frame.time | foreground | com.apple.instruments.server.services.coreprofilesessiontap | RAW v2 kd_buf stream (DebugID 0x31800318) | Perfowl Custom Decoder | ms;(t_n - t_{n-1}) 精确到纳秒换算 |
| 逐帧 | Average FPS | ios.frame.fps.average | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | fps;1000 / mean(FrameTime) |
| 逐帧 | 1% Low FPS | ios.frame.one_percent_low_fps | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | fps;1000 / mean(最差 1% FrameTime) |
| 卡顿 | SmallJank | ios.frame.small_jank_per_10min | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | count/10min;FT > 2*B 且 FT > 41.66ms |
| 卡顿 | Jank | ios.frame.jank_per_10min | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | count/10min;FT > 2*B 且 FT > 83.33ms |
| 卡顿 | BigJank | ios.frame.big_jank_per_10min | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | count/10min;FT > 2*B 且 FT > 125ms |
| 卡顿 | Stutter % | ios.frame.stutter_percent | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | %;∑(FT - B) / TotalDuration * 100 |
| 逐帧 | Drop / Hour | ios.frame.drop_per_hour | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | count/h;连续帧率跌幅大于 8fps 频次折算 |
| 逐帧 | FT >= 100ms | ios.frame.ftime_ge_100_percent | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | %;耗时超过 100ms 帧数占比 |
| 逐帧 | 80%~120% 稳态比 | ios.frame.med_range_percent | foreground | 同上 (coreprofilesessiontap) | 同上 | Perfowl Pipeline | %;落在中位数 FPS ±20% 区间帧数比例 |
| 能耗 | Total Energy | ios.process.energy.total_cost | process | com.apple.xcode.debug-gauge-data-providers.Energy | EnergyMonitor(provider, [pid]) | PMD3 Native | score;energy.cost (负值哨兵过滤) |
| 能耗 | CPU Energy | ios.process.energy.cpu_cost | process | 同上 (Energy) | 同上 | PMD3 Native | score;energy.cpu.cost |
| 能耗 | GPU Energy | ios.process.energy.gpu_cost | process | 同上 (Energy) | 同上 | PMD3 Native | score;energy.gpu.cost |
| 能耗 | Network Energy | ios.process.energy.network_cost | process | 同上 (Energy) | 同上 | PMD3 Native | score;energy.networking.cost |
| 能耗 | Location Energy | ios.process.energy.location_cost | process | 同上 (Energy) | 同上 | PMD3 Native | score;energy.location.cost |
| 能耗 | Display Energy | ios.process.energy.display_cost | process | 同上 (Energy) | 同上 | PMD3 Native | score;energy.display.cost |
| 能耗 | Energy Overhead | ios.process.energy.overhead | process | 同上 (Energy) | 同上 | PMD3 Native | score;energy.overhead |
| 电池 | Battery Current | ios.device.battery.current | device | com.apple.mobile.diagnostics_relay | DiagnosticsService.get_battery() (独立通道) | PMD3 Lockdown | mA;InstantAmperage (仅纯放电模式输出) |
| 电池 | Battery Voltage | ios.device.battery.voltage | device | 同上 (diagnostics_relay) | 同上 | PMD3 Lockdown | mV;Voltage (仅纯放电模式输出) |
| 电池 | Battery Power | ios.device.battery.power | device | 同上 (diagnostics_relay) | 同上 | Perfowl Engine | mW;|InstantAmperage| * Voltage / 1000 |
| 温度 | Battery Temp Raw | ios.device.battery.temperature_raw | device | 同上 (diagnostics_relay) | 同上 | PMD3 Lockdown | source-native;Temperature (原生整数) |
| 温度 | Battery Temp ℃ | ios.device.battery.temperature_celsius | device | 同上 (diagnostics_relay) | 同上 | Perfowl Engine | ℃;Temperature / 100 |
| 温度 | ThermalStatus | ios.device.thermal.status | device | 同上 (diagnostics_relay) | 同上 | PMD3 Lockdown | enum (0~3 级热状态);非 0~3 保持 missing |
| 启动 | Observed Running | ios.process.startup.observed_running_ms | process | processcontrol + deviceinfo | 受控拉起 + runningProcesses 轮询 | PMD3 Native | ms;t(首次观察到运行) - t(拉起命令) |
| 启动 | Foreground Ready | ios.process.startup.foreground_ready_ms | process | 同上 (deviceinfo) | 进程前台状态监听 | PMD3 Native | ms;t(前台状态就绪) - t(拉起命令) |
| 启动 | App Launch Total | ios.process.startup.app_launch_total_ms | process | coreprofilesessiontap (KDebug) | Mach Absolute 时钟桥锚点 | Perfowl Custom Decoder | ms;t(首帧呈现) - t(Mach 启动零点) |
| 启动 | 9 类启动分阶段 | ios.process.startup.*_ms (共 9 项) | process | 同上 (coreprofilesessiontap) | KDebug 类别过滤 (0x1F07, 0x2BDC, 0x2B87, 0x31CA) | Perfowl Custom Decoder | ms;分段 Span 耗时(系统接口/运行时/UIKit/Scene/回调/首帧) |
| GPU | GPU Counter Native | ios.device.gpu.counter.source_native | device | com.apple.instruments.server.services.gpu | GPUCounterControlService.configureCounters | Perfowl Adapter (pmd3 DTX) | count;动态硬件 Counter 解码与差分 |
| 待定 | TinyJank / Smooth | ios.frame.tiny_jank_per_10min / smooth | foreground | — | 暂无业界公认标准算法 | 保持 Unsupported | null;缺可信定义坚决不伪造 |
四、 核心数理模型与边界计算细节
1. 逐帧卡顿判定与卡顿率(Jank & Stutter)算法模型
在 frame_derivation.py 中,Perfowl 拒绝用一秒一次的 Graphics 曲线猜测卡顿,而是通过直接监听内核层 CoreProfile RAW v2 的每一笔呈现事件(DebugID 0x31800318)精确计算 FrameTime:
- 动态平滑基线 ($B$):实时取前 3 帧的算术平均值作为基准帧耗时: $$B_n = \frac{FT_{n-1} + FT_{n-2} + FT_{n-3}}{3}$$
- 动态阈值 ($D$):设定为 $D_n = 2 \times B_n$(即当前帧耗时超过前 3 帧平均耗时的 2 倍)。
- 三档绝对耗时门槛:
- SmallJank:$FT_n > D_n \quad \text{且} \quad FT_n > 41.66\text{ ms}$(相当于 24fps 门槛)。
- Jank:$FT_n > D_n \quad \text{且} \quad FT_n > 83.33\text{ ms}$(相当于 12fps 门槛)。
- BigJank:$FT_n > D_n \quad \text{且} \quad FT_n > 125.00\text{ ms}$(相当于 8fps 严重卡死门槛)。
- Stutter(卡顿率积分): 统计所有被判定为 Jank 的帧中,超出基准耗时的累加耗时占测试总时长的百分比: $$\text{Stutter%} = \frac{\sum_{i \in \text{Jank Frames}} \max(0, FT_i - B_i)}{\text{Session Total Duration}} \times 100%$$
2. 能耗与硬件电池测量模型
- Energy Cost 负值哨兵机制:Apple 系统的 Energy Monitor 会在特定低功耗切换时回传
-1或巨大负数作为状态哨兵值。Perfowl 引擎在metric_engine.py中对 Total Energy 进行负数拦截;一旦 Total $< 0$,整帧标记source-sentinel隔离,绝不计算为错误峰值。 - 物理断电放电门禁:通过
IOPMPowerSource读取快照时,若ExternalConnected == true,表示手机正在通过 USB 接收充电供电。此时测得的电流并非整机实际功耗,Perfowl 严格遵守诚实红线,将 Current、Voltage、Power 全部置为 null,并标注external-power-connected。只有在 Wi-Fi 模式且完全断开外接充电器时,才激活真实放电功率换算。
3. Mach Absolute 时钟桥与启动 9 阶段切分
- 时钟换算:iOS 内核 KDebug 记录使用 CPU 时钟滴答(Mach Absolute Ticks),Perfowl 通过会话开始时捕获的
numer与denom时钟基(Timebase)精确折算纳秒: $$\text{Timestamp (ns)} = \frac{\text{mach_ticks} \times \text{numer}}{\text{denom}}$$ - 生命周期锚点切分:
System Interface Initialization:系统接口初始化Static Runtime Initialization:动态链接库加载与静态运行时UIKit Initialization:UIKit 核心初始化UIKit Scene Creation:Scene 会话创建willFinishLaunchingWithOptions():应用即将完成启动回调didFinishLaunchingWithOptions():应用完成启动回调sceneWillConnectTo():Scene 窗口连接sceneWillEnterForeground():Scene 进入前台Initial Frame Rendering:首帧完成 GPU 渲染呈现 (debugid == 0x31CA0006)
五、 从 0 到 1 的宏观演进里程碑 (Milestones)
回顾从第一行代码到当前全功能基座,Perfowl 严格遵循“先调研、再文档、公式入档、用户批准、后开发”的七步流程,完成了七个核心阶段的蜕变:
- P0 阶段 · 数据保真基座: 奠定 Append-only 不可变 Session 设计,确立数据源四属性(Source / Scope / Unit / Quality),确立“绝不用 0 冒充缺值”的诚信原则。
- P1 阶段 · 基础指标承接 (Batch A): 接通 DVT Sysmontap 与 Graphics 基础流,实现 APP CPU、物理内存(Footprint/Resident)、线程数、上下文切换及秒级 FPS。
- P2 阶段 · 系统级与网络级拓展: 解决 TotalCPU 归一化(除以 EnabledCPUs)、分核 CPU 解析;自研
pmd3_deep_services突破单 PID 网络吞吐(NetworkStatistics),引入源时钟同步消除网络毛刺。 - P3 阶段 · 毫秒级逐帧流水线: 突破 CoreProfile RAW v2 二进制流解码,定位
0x31800318关键呈现事件;实现 29 项流畅度统计汇总与三档 Jank 判定模型。 - P4 阶段 · 原生工作台与可复查测试链: 完成 SwiftUI 原生高性能客户端架构;实现实时多轨同轴时间线、截图时间轴关联、Session 离线复查、指标添加菜单(17 项全开)与数据导出。
- P5 阶段 · 能耗与硬件电池模型: 接通 Diagnostics Relay 独立通道,建立 20 秒自适应采样时钟;引入严格的物理断电放电门禁与负值哨兵过滤,完成电池温度与热状态监控。
- P6 阶段 · 冷启动阶段化分解: 实现受控冷启动(Controlled Launch),建立 Mach Absolute 时钟桥;实现从内核启动到首帧呈现的 9 阶段生命周期精准分解。
六、 质量语义、边界与复查实战
- 数据质量语义:数据质量与采集源选择语义 —— warmup / missing / counter-reset / suspect-static 状态定义
- 深度 Raw 源发布语义:深度 Raw 源的质量与发布语义
- 硬件动态 Schema:GPU Schema 与 Lifecycle 质量语义
- 兼容性矩阵:iOS 15+ Collector Foundation 兼容矩阵 与 连接与系统路由
- 无侵入操作边界:无侵入边界定义
- 回放复查实战:Session 回放与时间轴、Session 截图与导出、截图时间轴