Skip to content

iOS 15+ 分版本指标能力确认与自动判定:方案 ​

Owner 2026-11-03:产品需支持 iOS 15 及以上;Owner 准备各版本设备,由 Claude 逐版本确认每项指标能做到什么程度;最终按连接设备的系统版本自动判断支持哪些指标。

一、现状(E1,读代码与既有记录) ​

  • 平台合同已有:ios-platform-profiles.candidate.v1.json 与 Swift 镜像 IOSPlatformSupport 按大版本(15/16/17/18/26/27)规定连接路线与启动解码器;不在表内的大版本拒绝连接。没有“版本 × 指标”的支持表。
  • 指标开关已有:WorkbenchMetricCatalog.availability() 是添加指标列表的唯一门,上下文里已有 iosMajorVersion,但目前没有用它;条件类指标靠采集准备后的运行时回报决定。
  • 源家族 10 个:sysmontap(CPU / 内存 / 线程 / 调度 / 磁盘,14 项)、整机网络、进程网络、图形(GPU 利用率)、显示(FPS / 帧耗时)、GPU Counter、逐帧管线(Jank 等 14 项)、能耗、设备状态(电池 / 温度)、启动。
  • 既有真机证据:连接 + 六个实时源在 iOS 16、17.0、18.2 达到 E4(2026-09-05);iOS 26.5 连接、应用列表、截图 E4,帧耗时 / FPS(FrameUpdate)E4(2026-11-02);iOS 15、17.4+、27 无真机;卡顿仅 iOS 26 离线 E2。各指标在各版本的逐项结论没有汇总。

二、要交付的东西 ​

  1. 分版本能力表(唯一事实源,随包发布):行 = 系统版本段,列 = 工作台指标组(截图、启动、FPS、帧耗时、显示性能、卡顿、CPU、CPU 核、内存、GPU、线程、上下文切换、唤醒、磁盘、温度、网络、电池、能耗、GPU Counter)。每格一个状态:
    • supported:该版本真机 E4 通过;
    • candidate:能出数,但口径未校准或只有部分证据;
    • unsupported:该版本确认拿不到(附原因,如“系统不再提供该事件”);
    • untested:没有真机证据。 另有硬件条件列(如卡顿分类需要渲染时间点,ProMotion 影响刷新率),按设备能力再收窄。
  2. 版本段按实际差异划分,不预设:先按大版本,真机发现小版本差异(如 17.0–17.3 与 17.4+ 连接路线不同)再拆。
  3. 自动判定:连接设备后按“系统版本 → 能力表 → 硬件条件 → 运行时回报”四层决定每个指标的可用性;添加指标列表与报告都显示同一结论,不可用给出一句原因。能力表只能收紧,不能把运行时失败的指标显示成可用。

三、逐版本确认流程 ​

每台设备一轮,Owner 约 15 分钟、无需改手机上的 App:

  1. Owner 连接设备、信任电脑、打开开发者模式(iOS 16+);告诉 Claude 设备别名与系统版本(公开文档只写“设备 B / iOS 15.x”)。
  2. Claude 读取设备信息与可用系统服务清单(只读)。
  3. Owner 选一个常用 App,Claude 开全部指标,Owner 按提示操作 5 分钟:静止 1 分钟 → 滑动列表 2 分钟 → 切到后台再回来 → 静止。期间 Claude 不在手机上安装或删除任何东西。
  4. 再做一次冷启动测试(启动指标)。
  5. Claude 离线分析:每个源是否有数据、字段是否齐、数值是否合理(与同场 sysmontap、截图时间线交叉核对),帧事件是哪一种(FrameLifetime / FrameUpdate / 其他),卡顿所需时间点是否齐全。
  6. 结论写入能力表与本 devlog(证据等级逐格标注),失败样本保留在私有目录。

建议设备(每个版本段一台即可):iOS 15.x、16.x、17.0–17.3、17.4–17.x、18.x、26.x(已有设备 A);有余力再加一台 60 Hz 非 ProMotion 机型。

四、实施顺序 ​

阶段内容依赖
A能力表格式与加载(Python 与 Swift 共用一份 JSON),先用既有证据填写,其余 untested;availability() 接入版本层无,可先做
B版本探测分析脚本:读取一次会话,逐源输出“有无数据 / 字段完整度 / 帧事件类型 / 卡顿时间点”无
C逐台设备确认(第三节)Owner 设备
D按结果补解码(如某版本帧事件格式不同),能力表定稿,界面与报告显示“该系统不支持”原因C

效率要求:版本判定在连接时一次查表(内存常数时间),不增加设备请求;探测分析为离线流式读取,单会话 < 5 秒。

五、待 Owner 决定 ​

  1. untested 的指标怎么显示:建议允许选择、标“未验证”,因为大多数指标在相邻版本通常可用;只有 unsupported 才禁止选择。备选:未验证一律禁用。
  2. 可提供哪些版本的设备、大约何时可用。

Owner 决策 · 2026-11-03 ​

  1. untested 指标允许选择,界面不标“未验证”;只有 unsupported 禁止选择并给出原因。能力表内部仍保留 untested 状态与证据等级,供报告审计与后续确认。
  2. Owner 有全版本设备,稍后准备。
  3. 本方案排在“正式包第一版”发版之后执行。

Implementation Update · 2026-11-04:Fixture 支持 iOS 15(确认测试用) ​

  • Owner 指定逐版本确认统一使用 PerformanceProbeFixture(含 100 秒卡顿校准),并要求它能装在 iOS 15 上。
  • 修改:
    • 最低系统版本从 16.0 改为 15.0;
    • 网络请求计时由 ContinuousClock(iOS 16+)改为 DispatchTime.uptimeNanoseconds,同样是单调时钟,毫秒值不变;
    • 主界面导航:iOS 16+ 仍用 NavigationStack,iOS 15 用 NavigationView(stack 样式),由 FixtureNavigation 按系统版本选择。
  • 卡顿校准页面没有改动,设备 A 上的校准结果不受影响。
  • 验证:iphoneos Release 不签名构建通过,产物 MinimumOSVersion=15.0(E2)。IPA 交 Owner 重签安装。
  • 首台确认设备:设备 B,iOS 15.3.1,60 Hz 屏幕;调试组件已由 Xcode 挂载(只读核对,E3)。

Validation Update · 2026-11-04:设备 B(iOS 15.3.1,60 Hz)首轮 ​

  • 会话:内部包 259,目标为 Fixture(iOS 15 版),3 分 19 秒;冷启动 → 卡顿校准 100 秒 → 滑动 → 切后台再回来。报告已封存,收尾正常。原始数据在本机会话目录,不入库。
  • 分析工具(阶段 B):新增 tools/validation/capability/probe_session.py,只读、流式读取一次会话,逐字段输出有值样本数与质量分布、原始事件类型、帧来源、帧/卡顿/启动汇总与覆盖账本。本会话 0.47 秒(门槛 < 5 秒)。
指标组结论(设备 B / iOS 15.3.1)依据
CPU(应用/整机/逐核)、内存(5 项)、线程、上下文切换、中断唤醒、磁盘读写有值196 个样本中 194–196 个有值;缺的 2 个是首个差分窗口
网络(整机/进程 收发)有值193–195/196
GPU 利用率(Device/Renderer/Tiler)有值182/196;其余 14 个样本 App 不在前台
FPS(每秒,图形统计)有值182/196,同上
电池温度有值196/196
电池电流、电压、功率不适用(本次)全程外接电源(external-power-connected),不是采集故障
能耗(总/CPU/GPU/网络/定位)有值188–193/196
能耗(显示、开销)无值missing / source-sentinel,与其他版本相同,属既有暂停项
启动 9 个阶段有值冷启动合计 368.0 ms,阶段覆盖完整(CoreProfile)
启动 observed_running / foreground_ready无值196 个样本都是 missing,待查
帧耗时、P95、Jitter、刷新率、卡顿 4 项无值见下
GPU Counter无值RuntimeError,待查
热状态无值missing,与其他版本相同(既有开放项)

逐帧来源(主要问题)

  • ActivityTrace 全程没有返回任何消息:totalMessageCount=0,frameLifetimeMatchedCount=0,只收到 1 个 28 KB、0 行的数据块。
  • 帧来源管理器判定主来源证据不足,回退到 CoreProfile;CoreProfile 只给出 30 帧(启动阶段),之后没有再出帧。
  • 结果:报告帧统计只有这 30 帧,帧耗时实时值整场为空,卡顿 no-timing。卡顿校准的注入记录因此无法对照。

待查

  1. iOS 15 上 ActivityTrace 为什么没有数据:是订阅参数(如只取 signpost 的选项)在 iOS 15 不被接受,还是服务本身需要不同的开启方式。需要在设备 B 上做只读的短时探测。
  2. 回退到 CoreProfile 后为什么只出启动阶段的帧。
  3. 启动 observed_running / foreground_ready、GPU Counter 两项无值的原因。

能力表暂定(iOS 15)

  • candidate(有值,口径沿用其他版本):上表“有值”各项。
  • untested:其余各项。
  • 在查清 ActivityTrace 之前,帧类与卡顿不标 unsupported。

修复方案 · 2026-11-04:iOS 15 ActivityTrace 逐帧来源 ​

原因(设备 B 只读探测,E3)

用 pymobiledevice3 11.3.1 的 ActivityTraceTap 直连设备 B,参数与 Perfowl 相同,录了 12 秒:

  1. 解析中断:_handle_end_row 按 4 字节 struct.unpack("<I") 解析 process、thread。iOS 15 有一部分行的这两个字段是 6 字节,于是抛出 unpack requires a buffer of 4 bytes,整条流在第 1–2 条消息后就结束了。Perfowl 的 BoundedActivityTraceTap 执行的是同一段上游代码,所以整场 totalMessageCount=0。
    • 改用能处理长字段的解析后,12 秒收到 2,823 条消息,其中 FrameLifetime 689 条(约 57/s,与 60 Hz 屏幕一致)。
  2. 格式不同:iOS 15 的 FrameLifetime 格式串是 begin_time / frame_seed / refresh_interval / buffer_count / end_time / prev_frame / skip_request,比 iOS 17 少了 display_id。解码器要求 8 个字段全齐,因此全部 685 条都判为 unsupported-schema。
    • refresh_interval 为 399,987 个时钟周期,按 125/3 换算是 16.67 ms,与 60 Hz 一致。

修改

文件改动
activity_trace_rows.pyBoundedActivityTraceTap 的私有 globals 中加入一个 struct 替身:unpack("<I", b) 遇到长于 4 字节的值时按小端整数整体读取,其他格式原样交给标准 struct。上游代码与摘要锁定都不动,只换这一份私有 globals 的依赖,与现有的 dataclasses 替身同一做法
display_performance.py_extract_fields_from_formatted_signpost:格式串里完全没有 display_id 时记为 0(主屏),与 iOS 26 FrameUpdate 的约定一致;其余 7 个字段仍必须全部存在,否则照旧 unsupported-schema
  • 口径不变:帧耗时、FPS、P95、卡顿都沿用现有公式。
  • iOS 15 的卡顿要用到开始时刻、结束时刻(得出帧寿命)、上一帧时刻与刷新间隔;这几个字段都在,frame_timing 的 FrameLifetime 路径已有单测。
  • 开销:替身只在每行的两次 unpack 上多做一次长度判断,O(1)。
  • 测试:
    • 用设备 B 抓到的一条真实 FrameLifetime 消息(只含时间戳与计数,不含标识)做解码单测;
    • 6 字节 thread 行解析单测;
    • 摘要锁定测试照旧;
    • 再用 Perfowl 的有界解析加解码器对设备 B 做一次 12 秒只读复测;
    • 之后打包,Owner 重测。

Implementation Update · 2026-11-04:iOS 15 逐帧来源修复 ​

  • activity_trace_rows.py:新增 _WideIdStruct,放进 BoundedActivityTraceTap 行构建器的私有 globals。上游代码与摘要锁定都没有改动。
  • display_performance.py:格式串里没有 display_id、surface_id 时,两项都记为 0。
    • 与方案相比多处理了 surface_id:iOS 15 同样没有这个字段,解码器原本把它列为必需。0 与 iOS 26 FrameUpdate 的取值一致。
  • 测试:新增 tests/test_ios15_frame_lifetime.py 5 项:
    • 用设备 B 的 3 条连续真实消息(只含时间戳与计数)验证解码:帧耗时 16.664 ms,60 Hz,帧寿命为 2 个刷新间隔(双缓冲),卡顿可以判定;
    • 缺少其他字段时仍拒绝;
    • 6 字节字段整体读取、4 字节不变;
    • 有界解析确实使用了替身。
    • Sidecar 全量 3,179 项,失败清单与基线相同(16 项环境失败)(E2)。
  • 设备 B 实时复测,Perfowl 有界解析加解码器,15 秒,只读(E3):
    • 收到 3,615 条消息、863 帧,有效 859 帧,时序字段全部齐全;
    • 卡顿判定为 applicable,动画 14.3 秒,0 次卡顿;
    • 修复前同样 12 秒只有 0–2 条消息。
  • iOS 15 没有渲染开始时刻,卡顿能判出时长,但类型记为 unknown(不分提交阶段或渲染阶段)。
  • 状态:implemented(E2/E3)。打内部包 260 后请 Owner 在设备 B 上重测(E4)。

Validation Update · 2026-11-04:内部包 260(含 iOS 15 修复) ​

  • 第一次构建被源码门禁拒绝:e2c49175、f6d76d73 两个提交没有推到 GitHub(Permission denied (publickey))。原因是推送结果只按 error/reject 字样过滤,这条报错漏看了。
    • Claude 的命令环境仍然无法通过 GitHub 认证,同样的认证在 Owner 的终端里能通过。由 Owner 手动推送后,两个远程都到了 f6d76d73,再重新构建。
    • 以后每次推送都逐个核对两个远程的提交号。
  • 来源:main f6d76d73,x86_64,构建 252 秒。App 80bb9d97、DMG 4fa38df7 均 Accepted 并 stapling,Gatekeeper 结果为 Notarized Developer ID。
  • 产物:MacApp/dist/Perfowl-1.0.0-260-macos-x86_64-internal.dmg,146,892,712 字节,SHA-256 647b22d37d08fd262a210510c3976ab3d1dca8bcbebbb7526d20329dd0e7612d。
  • 核对:冻结核心 109 个模块与源码一致,CLI 一致;probe ready,stream 首条 hello;卡顿重算两个样本与源码一致;259 回滚包已核对;启动器唯一(E3)。
  • 待办:Owner 在设备 B 上重测(E4)。

Validation Update · 2026-11-04:设备 B 第二轮(内部包 260) ​

  • 会话:内部包 260,目标为 Fixture(iOS 15 版),3 分 9 秒;冷启动 → 卡顿校准 100 秒 → 滑动 → 切后台再回来。报告已封存,交付质量 candidate(第一轮是 partial)。
  • 注入标记通过 house_arrest 只读取出 App 容器(devicectl 不支持 iOS 15),存放在私有目录。

修复生效(E4)

  • ActivityTrace 选为逐帧来源,0 次回退,共 7,869 帧(第一轮为 0);数据量 0.59 MB,远低于超限门槛。
  • 帧耗时 159/185 个样本有值,其余 15 个是 missing-no-new-frame、11 个不在前台;P95、Jitter 各 158/185;FPS 改由逐帧数据计算,159/185。
  • 实时卡顿曲线 137/185 个样本有值,其余为“画面静止”、无新帧或不在前台。
  • 启动 9 个阶段完整,合计 153.4 ms。第一轮 368.0 ms 是安装后首次启动。
  • 仍然无值:刷新率、GPU Counter、热状态、启动辅助两项(observed_running / foreground_ready),以及外接电源时的电池电流、电压、功率。

卡顿校准:v1 在 iOS 15 上漏检(E4)

校准段设备 A(iOS 26,120 Hz)设备 B(iOS 15,60 Hz)
注入 20/50/100/200 ms 与 50/100 ms,共 6 次6/6 检出,时长与注入一致0/6
重渲染 15 s每帧寿命 66.7 ms,431 次卡顿15 秒内没有任何帧(GPU 94–98%),0 次
整场—8 次,每次一个刷新间隔(16.5–16.7 ms),全部为限帧起点
  • 机制:
    • iOS 15 的 FrameLifetime 是整屏合成帧。App 主线程卡住时,屏幕不会停帧,而是按静止时的 20 Hz 节奏每 50 ms 合成一帧(200 ms 注入期间是 4 帧,间隔都是 50 ms)。这几帧的开始时刻落在上一帧上屏之后,按 v1 规则算“静止后重新开始(idle)”,不参与判定。
    • 重渲染时 GPU 跑满,15 秒没有 FrameLifetime;之后的第一帧同样记为 idle。
    • iOS 26 的 FrameUpdate 在卡顿时表现为“帧寿命变长”,v1 正是按这种形态设计的。iOS 15 没有这种形态。
  • 时钟:注入标记用的是 mach_absolute_time;iOS 15 的 FrameLifetime 时间戳与它相差约 8,233 秒(推测是设备休眠时间累计,即连续时钟与绝对时钟之差,未证)。本轮按每秒帧数的规律对齐:校准开始约在第一帧后 7 秒,60 fps、20 fps、0 fps 各段与时间表一一对应。

Lead 判定(待 Owner 确认)

  • iOS 15:帧耗时、FPS、P95、Jitter 记为 candidate(有数,口径与其他版本相同)。

  • 卡顿 4 项在 iOS 15 上不能按 v1 发布:会把明显的卡顿显示成接近 0,误导使用者。两个选项:

    1. 暂时在 iOS 15 上记为 unsupported,原因写“该系统的帧数据无法判定卡顿”,添加指标时禁用,报告中不显示;
    2. 为 iOS 15 另设规则,例如把“动画中途合成节奏降到静止节奏”判为卡顿,再用本轮校准数据验证。

    建议先做 1,再评估 2。

研究方案 · 2026-11-04:iOS 15 卡顿规则(Owner 选“直接研究”) ​

数据特征(设备 B 第二轮,E4 原始数据 + E3 离线分析)

  • 注入期间合成帧与静止时完全相同:间隔 50 ms、帧种子每帧 +2、帧寿命 16.6 或 33 ms 交替。单看一帧无法区分。
  • 卡顿只能从上下文认出来:连续动画(60 fps、起点在上一帧上屏之前)中插入一段静止节奏,随后动画恢复。
  • 30 Hz 限帧时,屏幕本身的合成节奏就不规则,连续、限帧、静止三种起点交替出现。

路线 A:只用整屏合成节奏(离线试算)

  • 规则:连续动画帧之后出现静止起点的插段,插段 ≤ 1 s 后恢复连续动画,记一次卡顿,H = 恢复帧上屏时刻 − 插段前最后一帧上屏时刻 − V。另外,超过 150 ms 完全没有帧也记一次。
  • 试算结果:
校准项结果
注入 50 / 100 / 200 ms(第一组)50.0 / 100.0 / 200.0 ms,与注入值一致
注入 20 ms未检出
注入 50 / 100 ms(第二组)66.7 ms ×2、83.3 ms,偏差 1 帧
重渲染 15 s 无帧检出 1 次,15,049 ms
干净 60 Hz 动画段1–2 次,33 ms
30 Hz 限帧段149 次误报,共 10.0 s
校准前主界面58 次 33 ms
  • 结论:注入能抓到,但限帧场景大量误报,不能直接用。

路线 B:用目标 App 自己的 Core Animation 提交事件(待验证)

  • 设备 B 的探测里,com.apple.coreanimation / Transaction / Commit 12 秒有 1,405 条。它是各进程提交画面时自己发出的,带进程号。App 主线程卡住时提交必然停止;屏幕合成节奏不影响它。
  • 规则草案:目标进程在连续动画期间相邻两次提交间隔 > 1.5 V,且前后都是稳态节奏,记为一次卡顿。限帧 App 的提交节奏本身就是稳态,不会被误判。
  • 需要先确认三件事:
    1. 卡顿注入期间,目标进程的 Commit 是否按预期停止;
    2. 限帧段的提交节奏是否稳定;
    3. 带宽与开销。Commit 事件数约为帧数的 2 倍,现有白名单只放行 FrameLifetime,要扩到“目标进程的 Commit”;隐私上只保留时间戳与进程号,不保留内容。

下一步

  • 用只读探测在设备 B 上录一次完整校准(约 110 秒),同时记录 FrameLifetime 与目标进程的 Commit,验证路线 B。需要 Owner 在手机上打开校准页。
  • 路线 B 可行的话,再定 iOS 15 规则(perfowl-hitch.ios15.candidate.v1),并在同一份数据上检查漏检与误报。
  • 验收门槛沿用 Phase 2:注入召回率与精确率 ≥ 95%,单次时长误差 ≤ 1 个刷新间隔,干净段与限帧段误报为 0。
  • 在通过之前,iOS 15 卡顿仍保持现状(v1,会偏低),在报告中标注。

Validation Update · 2026-11-04:路线 B 在设备 B 上验证(第三轮校准) ​

  • 采集:tools/validation/capability/capture_frames_commits.py,只读录制 180 秒,同时记录 FrameLifetime 与 Core Animation Transaction/Commit 的时间戳,以及“是否来自目标进程”(只保留这个布尔值,不写进程名)。
    • 结果:6,420 帧,Commit 8,580 次,Fixture 自己的 Commit 合并为 6,754 次画面更新(同一更新内 4 ms 以内的多次提交合为一次)。
    • Commit 与 FrameLifetime 用同一时钟,可以直接比较。注入标记用另一个时钟,以重渲染段的 15 秒无帧为锚对齐,误差约 0.06 秒。
  • 规则(tools/validation/capability/ios15_commit_rule.py,研究版):
    • Δ = 相邻两次画面更新的间隔;
    • 稳态 = 最近 16 个间隔的 round(Δ/V) 众数。每个间隔都参与统计,所以节奏切换后稳态会跟上;
    • H = Δ − 稳态 × V。H ≥ 0.5 V,且下一个间隔恢复变短,记一次卡顿。下一个间隔一样长,说明是节奏本身变了,不计;
    • Δ > 2 s 视为 App 停止动画,不判定;
    • 另外,App 仍在提交但超过 150 ms 没有合成帧,记一次显示断流。
校准项注入检出
120 Hz 动画段20 / 50 / 100 / 200 ms22.1 / 51.7 / 102.3 / 203.8 ms
30 Hz 限帧段50 / 100 ms49.4 / 98.9 ms
干净动画段、60 Hz 限帧段、多数静止段无0
静止段 6无1 次 19.3 ms(误报)
重渲染 15 s—显示断流 15,099 ms,外加 7 次提交间隔变长(共 415 ms)
校准开始前(进入校准页)—3 次(134–333 ms),页面切换,未判定真伪
  • 召回 6/6,单次时长误差都不超过 1 个刷新间隔(最大 2.1 ms)。
  • 误报:已知无卡顿的段里有 1 次,所以按校准段计精确率 6/7。还没达到 Phase 2 的 95% 门槛。
  • 演进过程:
    • 第一版只有“非卡顿间隔”参与稳态,从 60 Hz 切到 30 Hz 后稳态停在旧值,30 Hz 段误报 428 次;
    • 改成所有间隔都参与稳态后,降到 9 次;
    • 加上“下一个间隔恢复变短”的条件后,降到只剩 2 次注入本身。

结论与下一步

  • 路线 B 可行,可作为 perfowl-hitch.ios15.candidate.v1。还差:
    1. 静止段那次误报的原因;
    2. 真实 App 滑动场景的误报率;
    3. 进入产品需要扩展白名单(放行目标进程的 Commit,只保留时间戳),并评估开销(Commit 约为帧数的 1.3 倍);
    4. 第二台 iOS 15 设备或第二轮复测。
  • 在通过之前,iOS 15 卡顿仍按 v1 输出(会偏低),没有改动产品代码。

更新 · 2026-11-04:消除节奏切换误报 ​

  • 上一版在“静止段 6”的那次 19.3 ms,实际落在 60 → 30 Hz 切换的瞬间:对齐误差约 0.06 s,它属于 30 Hz 段开头。切换时的间隔依次是 30.7、36.0、11.2、18.9 ms,长间隔后面恰好跟着一个短间隔,被误判。
  • 新增条件:长间隔的前一个间隔必须处在稳态节奏上(|Δ前 − 稳态×V| ≤ 0.5 V)。后一个间隔仍只要求比当前间隔短,因为卡顿结束后的头几帧本身就不规则(220 ms 之后是 24.8、19.8、13.7 ms)。
    • 试过同时要求后一个间隔也处在稳态,结果 100、200 ms 和第二组注入全部漏掉,所以没有采用。
  • 结果(同一份数据):
项结果
注入 6 次6/6 检出:22.1 / 51.7 / 102.3 / 203.8 / 49.4 / 98.9 ms
干净动画、限帧、静止各段0 次误报
重渲染显示断流 15,099 ms,外加 5 次提交间隔变长
校准开始前1 次 325.5 ms(打开校准页时的页面切换,未判定真伪)
  • 规则是在这一份数据上调出来的,有过拟合风险。下一步:
    1. 留出验证:再录一次校准,规则不改,直接检验;
    2. 真实 App 滑动约 2 分钟,看误报。
  • 录制脚本改为记录每次 Commit 的进程号,事后取滑动期间提交最多的进程作为前台 App。进程号只保存在本机。

Validation Update · 2026-11-04:路线 B 留出验证(第四轮校准) ​

  • 采集 300 秒:9,509 帧,Commit 11,608 次。规则参数与上一节完全相同,没有再调整。
校准项注入检出
120 Hz 动画段20 / 50 / 100 / 200 ms22.1 / 51.1 / 101.7 / 201.2 ms
30 Hz 限帧段50 / 100 ms51.8 / 102.2 ms
干净动画、限帧、静止各段无0
重渲染 15 s—显示断流 15,149 ms,外加 1 次提交间隔变长 40.7 ms
  • 召回 6/6,单次时长误差最大 2.2 ms(< 1 个刷新间隔);已知无卡顿的段误报 0。两轮校准合计 12/12,误报 0(E4 原始数据,E3 离线判定)。
  • 校准之后约 90 秒的列表滑动:前台进程仍是 Fixture(Owner 在 Fixture 的主列表里滑动)。
    • 2,453 次画面更新,可判定时长 40.3 s;
    • 检出 2 次,160.1 ms 与 18.4 ms,合计 4.43 ms/s;
    • 同期合成帧最大间隔 100 ms。
    • 没有注入,无法判定真伪,只作为“正常滑动的量级”参考。

判定:路线 B 达到 Phase 2 的门槛(召回、精确率 ≥ 95%,时长误差 ≤ 1 个刷新间隔),只在设备 B 一台设备上验证过。可以进入产品化方案:perfowl-hitch.ios15.candidate.v1。

产品化方案 · 2026-11-04:iOS 15 卡顿(commit 规则) ​

层改动
采集白名单ActivityTrace 过滤器在 iOS 15 额外放行 com.apple.coreanimation / Transaction / Commit,且只放行目标进程(按已核验的 PID)。只保留时间戳,不保留消息内容。iOS 16+ 不放行,行为不变
原始事件新增 display.ca-commit.raw(时间戳、目标代际、来源纪元),写入 Raw 与封存
判定hitch_metrics.py 新增 CommitHitchAccumulator:画面更新合并(4 ms)、16 个间隔的众数作稳态、前一个间隔须在稳态上、后一个间隔须变短、2 s 以上视为静止;另外 App 仍在提交而合成帧中断超过 150 ms 记为显示断流。summary 字段与 v1 相同,算法标记 perfowl-hitch.ios15.candidate.v1
选择按平台合同选判定器:iOS 15 用 commit 规则,其余版本用 v1。在线、离线、实时曲线三处共用同一实现
能力表iOS 15 卡顿 4 项记为 candidate,证据为设备 B 两轮校准
  • 开销:Commit 约为帧数的 1.2–1.3 倍,单条只做时间比较,O(1);按 60 fps 估算多约 80 条/秒。实施后做基准对比。
  • 风险:只有一台 iOS 15 设备、一个 App 的证据;多窗口、键盘等系统界面的提交不在目标进程里,不会计入。

Implementation Update · 2026-11-04:iOS 15 卡顿(commit 规则)产品化 ​

实际修改

文件实现
display_performance.py白名单过滤层 _ActivityTraceFilteredStream 新增可选的 commit_pid:开启后,目标 PID 的 com.apple.coreanimation / Transaction / Commit 只记下设备时间戳,挂到下一条 FrameLifetime 上,其余内容不保留;每帧最多 4,096 个,超过时标记溢出。适配器新增类属性 collect_app_commits(默认关闭),解码后把时间戳按同一时基换算成纳秒,写入 appCommitTimesNs,溢出时写 appCommitOverflow
realtime.py会话开始时 collect_app_commits = (iOS 大版本 == 15)。一个采集进程只跑一场会话,所以用类属性是安全的;iOS 16+ 行为不变。实时窗口的算法标记改为跟随判定器
hitch_metrics.py新增 _CommitJudge 与 HITCH_ALGORITHM_COMMIT = perfowl-hitch.ios15.candidate.v1。HitchAccumulator 遇到带 appCommitTimesNs 的帧行就进入 commit 模式:汇总字段与 v1 相同,algorithm 写新标记;检查点保存 commit 状态,算法不一致时拒绝恢复;遇到溢出则清空稳态,不跨缺口判定。重复的计数逻辑抽成 _count_hitch
  • 规则与研究版 tools/validation/capability/ios15_commit_rule.py 相同:4 ms 合并为一次更新,16 个间隔取众数作稳态,前一个间隔须在稳态上、后一个间隔须变短,间隔超过 2 s 视为静止,150 ms 以上无合成帧视为显示断流。
  • 在线报告、离线帧报告、hitch-recompute 与实时曲线都通过 HitchAccumulator 自动生效,不需要另改。
  • 偏差:方案里写的是新增原始事件 display.ca-commit.raw,实际改为把时间戳挂在 FrameLifetime 上。这样 Raw 记录、帧明细、在线报告和实时投影器都不用另开链路;隐私上同样只有时间戳。

测试(E2)

  • 新增 tests/test_ios15_commit_hitch.py 12 项:
    • 规则:卡住一次、节奏切换、同一更新内多次提交、静止、显示断流、溢出、检查点往返、实时标记;
    • 设备 B 第四轮录制(只含时间戳,tests/fixtures/ios15_commit_timing_r4.json.gz,71 KB):复现研究版结果,11 次、15,898.348 ms;
    • 采集层:只收目标 PID 的 Commit、挂到下一帧、缓冲有上限、默认关闭。
  • 卡顿相关 45 项通过;Sidecar 全量 3,191 项,失败清单与基线相同(16 项环境失败)。
  • 产品实现对两次录制的结果与研究版逐位一致:第三轮 14 次、16,396.392 ms;第四轮 11 次、15,898.348 ms。

开销(x86_64)

  • 过滤层每条消息 3.18 → 3.53 µs,按设备 B 每秒数百条消息计,约 0.1 ms/s;只在 iOS 15 开启。

  • commit 判定每帧 3.5 µs,低于 v1 的约 5 µs。

  • 帧事件每帧多 1–2 个整数。

  • 状态:implemented(E2/E3)。打内部包 261 后由 Owner 在设备 B 上用 Perfowl 正式测一次(E4)。

Perfowl · Performance Observer