Skip to content

画面流畅度(Hitch)指标:可行性研究与实施方案 ​

Owner 2026-11-03:当前 FPS 与帧耗时不足以反映画面流畅度,需要研究一套能帮助用户真正发现“画面是否流畅”的指标,给出可行性研究报告与实施方案。研究入口为内部待办《专项研究待办:Hitch 与视觉流畅度评估》(2026-10-29,internal/design/hitch-visual-fluidity-research-backlog-2026-10-29.md)。

本文是研究结论与方案,不是已验证的指标规范;文中所有自算数值均为候选(candidate),未与官方工具配对校准前不得冠以官方同名或进入正式报告。状态 paused 表示等待 Owner 决定是否启动实施。

一、结论摘要 ​

  1. 可行,且不需要新增采集通道。 产品已在用的 ActivityTrace 流里,iOS 26 的 com.apple.coreanimation / IOMFBServer / FrameUpdate 每帧带有计算 Hitch 所需的全部关键时刻:帧开始时刻、实际呈现时刻、渲染开始/结束、GPU 执行区间、就绪时刻,以及每帧的目标刷新间隔(本次研究新发现,见第四节)。滑动区间(UIKit / ScrollView)与 App 提交事件(coreanimation / Transaction / Commit)也已经在同一条流中到达,目前只是被白名单丢弃。
  2. FPS 与帧耗时为什么不够:它们只看“两帧之间隔了多久”,无法区分“画面本来就不需要更新”和“画面该更新却晚了”。设备 A 150 秒样本中,帧间隔超过 1.5 个刷新间隔的帧有 60 个,绝大多数帧生命周期正常——是内容静止,不是卡顿;另有 24 帧是 ProMotion 主动降频(120→80 Hz)造成的间隔变化,也不是卡顿。
  3. 建议主指标:卡顿时间比(Hitch Time Ratio,ms/s)= 连续动画期间卡顿时长之和 ÷ 连续动画时长;配套卡顿次数、最长卡顿、时长分布、动画时长与覆盖率,以及 Commit / Render 卡顿分类。平均 FPS 保留为补充,不把 Hitch 折算成 FPS。
  4. 样本试算(candidate,E3):150 秒样本中连续动画 10.50 秒,检出 6 次卡顿共 91.8 ms,卡顿时间比 8.7 ms/s,最长 37.5 ms;其中 4 次与系统自身标记的 IOMFBServer.Stalls 帧重合。
  5. 主要不确定性:官方“预期帧生命周期”在事件中没有直接给出,需要从稳态推导;动画起始帧、三缓冲吸收的晚帧两类边界的判定,必须与 Instruments「Animation Hitches」在同一会话上配对校准后才能定稿。官方模板只支持录制结束后分析(deferred),只能作为离线校准基准,不能作为产品实时来源。

二、官方口径核实(2026-11-03 读取 Apple 文档 JSON 源) ​

项官方原文要点对本方案的约束
Hitch 定义显示没有按预期节奏更新:下一帧未就绪,帧迟到。“dropping a frame is just one potential response… not every hitch causes a frame drop”不能用“掉帧数”或固定 16.67 ms 阈值替代
帧生命周期预期帧生命周期 = begin time → presentation time;实际帧生命周期 = begin time → 实际切换上屏的 vsync;Hitch duration = 实际 − 预期需要每帧的开始时刻、实际呈现时刻与预期生命周期
双/三缓冲双缓冲预期生命周期 2 个 vsync,三缓冲 3 个;切换到三缓冲以从 Render 卡顿恢复,此后每个 vsync 都有新帧则“isn’t a hitch”预期生命周期随缓冲模式变化,不能写死
Commit / RenderApp 未赶上 commit deadline → Commit Hitch;App 准时但渲染服务未按时完成 → Render Hitch;commit deadline 通常是呈现前一个 vsync,begin time 再前一个可用“渲染开始是否晚于 commit deadline”区分
可变刷新率显示按最高刷新率运行,无内容时降低刷新率;“use the begin time, commit deadline, and presentation time of the relevant API instead of vsyncs”刷新间隔逐帧取,不用全局常数
何时报告只有存在交互或其他更新触发时才能检测 Hitch,“When no such trigger exists… the frame can’t be late”静止期不计入分母,不能记 0 卡顿
分级(Xcode Organizer Hitches)≤10 ms/s good;≤25 warning;≤50 critical;>50 需立即处理可作展示参考,须注明“参照 Organizer 分级”,不等于官方判定
MetricKitMXAnimationMetric.scrollHitchTimeRatio(iOS 14+,仅 UIScrollView 滑动);HitchTimeMetric(iOS 27+)“normalized against total animation duration and incorporates perceptual adjustments”需改 App 才能拿到,产品不可用;“感知修正”未公开,自算值不得声称等同
Hang主线程 run loop 无响应;“Most of Apple’s developer tools start reporting issues when … exceeds 250 ms”;离散交互 100 ms 内很少可察觉Hang 与 Hitch 分开评价,第二阶段研究
Instruments 模板既有实验(2026-10-29):Animation Hitches 模板仅 deferred,不支持 immediate/windowed只能做录后校准基准

历史待办中“旧视频 5/10 ms/s”线索未在本轮文档中找到对应原文,不采用。

三、为什么现有 FPS / 帧耗时看不出流畅度 ​

以设备 A(iOS 26,ProMotion)150 秒、Owner 期间滑动页面的抓包为例(1,046 帧,链式相接 1,045 对):

  • 静止与卡顿不可分:帧间隔 > 1.5 个刷新间隔的帧 60 个,其中只有 4 个被系统标为 Stalls;其余多数帧生命周期正常(如 120 Hz 下 25 ms),说明帧“准时完成,只是没有新内容要画”。FPS 会把这段计为低帧率,帧耗时会把它记为长帧。
  • 主动降频被误判:滑动减速时系统把刷新率从 120 Hz 降到 80/60 Hz,帧间隔从 8.34 ms 变为 12.5/16.67 ms。若以固定 120 Hz 衡量,会产生 24 个“4.2 ms 卡顿”假阳性。
  • 分母失真:150 秒里真正处于连续动画的只有 10.5 秒;整场平均 FPS 把卡顿稀释在 139.5 秒的静止中。

四、数据来源可行性(source-to-contract) ​

4.1 FrameUpdate 字段语义(E3,设备 A 原始元组解码) ​

格式串:base_mct=… args1=…#llx args2=…#llx contributors=….*P FrameLifetimeInterval=…(begin_end_32b) FrameRender=…(begin_end_32b) FrameReady=…(event) FramePrevious=…(event) FrameGPUExecute=…(begin_end_32b)。时间单位为 Mach tick(设备 A 为 24 MHz,1 ms = 24,000 tick)。

字段原始形态解码判断依据
base_mctuint64帧开始时刻(begin time)其后各偏移均相对于它
FrameLifetimeInterval4 字节实际帧生命周期(结束偏移);base_mct + 它 = 实际呈现时刻1,045 对与下一帧 FramePrevious 精确相接(既有 E4)
FrameRender8 字节,高 32 位开始、低 32 位结束渲染服务处理区间渲染开始偏移恒约 1 个刷新间隔(120 Hz 7.9–8.7 ms、80 Hz 12.55 ms、60 Hz 16.2 ms),与官方“commit deadline = begin time 后一个 vsync”一致
FrameGPUExecute8 字节,同上GPU 执行区间位于渲染区间内
FrameReady4 字节帧就绪偏移略晚于 GPU 结束
FramePreviousuint64上一帧实际呈现时刻链式校验
args2 低 32 位—本帧目标刷新间隔(tick)只出现 200,044 / 300,066 / 400,088 三种值 = 8.335 / 12.50 / 16.67 ms,对应 120 / 80 / 60 Hz;与相邻呈现间隔一致
args1、args2 高 32 位—未知(args1 低位在 3 个值间轮换,疑为缓冲区标识)待证,不使用
contributorsdata参与合成的客户端(未解码)待证,见风险
类别 IOMFBServer.Stalls—系统自身标记的停顿帧(11/1,046)其生命周期均高于同刷新率基线;语义未公开

稳态帧生命周期(非 Stalls 帧众数):120 Hz 为 3.0 个间隔(25.0 ms,三缓冲),60 Hz 为 2.0 个间隔(33.4 ms,双缓冲),80 Hz 为 2.33 个间隔(29.2 ms)。说明缓冲模式随刷新率与负载变化,预期生命周期必须动态推导。

4.2 指标所需信息与来源 ​

所需信息来源状态
实际呈现时刻FrameUpdate base_mct + FrameLifetimeInterval产品已解码(E4)
每帧刷新间隔FrameUpdate args2 低 32 位新发现(E3),待解析入库
开始时刻 / commit deadline / 渲染与 GPU 区间FrameUpdate base_mct、FrameRender、FrameGPUExecute、FrameReady字段已到达,待解析(E3)
预期帧生命周期无直接字段;按同刷新率稳态推导candidate,需校准
系统停顿标记IOMFBServer.Stalls 类别已到达;作为辅助证据,不作为唯一判据
滑动区间UIKit / ScrollView / Scroll_Dragging、Scroll_Deceleration、Scroll_DraggingAndDeceleration(150 秒约 45 次)已到达、当前被丢弃;需按目标进程过滤
App 提交coreanimation / Transaction / Commit(约 62 次/秒)、UIKit / UpdateCycle / UpdateSequence(约 107 次/秒)已到达、当前被丢弃
目标 App 归属contributors 或提交事件的进程号待证;暂按 foreground-device
iOS 17 设备FrameLifetime(含 begin/end/prev/refresh_interval)字段可映射,需单独核验
Hang主线程 run loop 无响应;候选为目标进程提交/更新序列的长间隔第二阶段研究,无直接来源

成本:上述事件已随 onlySignposts=1、predicate="" 的现有订阅传输,不新增 USB 流量与设备侧负载。增量只在主机解析:FrameUpdate 已逐条解析,新增字段为同一次线性遍历;接收 ScrollView 与 Commit 事件约增加 170 条/秒的字段读取,按现有解析成本估计主机 CPU 增量 < 1%(实施时用基准验证)。

五、指标设计(candidate) ​

均为 foreground-device 范围,算法版本 perfowl-hitch.candidate.v1;未经第六节校准前只在开发者模式显示。

5.1 逐帧判定 ​

对链式相接的帧 i(FramePrevious(i) 等于上一帧呈现时刻):

text
P(i)   = base_mct(i) + FrameLifetimeInterval(i)                 实际呈现
B(i)   = base_mct(i)                                            开始时刻
V(i)   = args2(i) & 0xffffffff                                  目标刷新间隔
Vx(i)  = max(V(i−1), V(i), V(i+1))                              容忍刷新率切换
L*(i)  = round(lifetime / V) 的近 16 个非 Stalls 同刷新率帧众数 × V(i)   稳态生命周期
连续(i) = B(i) ≤ P(i−1)                                         本帧在上一帧上屏前已开始,属于连续动画
E(i)   = max(P(i−1) + Vx(i), B(i) + L*(i))                      预期呈现
H(i)   = P(i) − E(i),当 连续(i) 且 H(i) ≥ 0.5 × V(i) 时为一次卡顿,否则 0
  • Commit / Render 分类:渲染开始偏移 FrameRender.begin − 0 超过 1.5 × V(i) → Commit 卡顿(App 晚提交);否则 Render 卡顿(渲染服务/GPU 晚完成)。阈值待校准。
  • 断链帧、首帧、会话/代际切换:不判定,不计入分母。

5.2 会话级指标 ​

指标公式单位回答的问题
连续动画时长 AΣ 连续帧的呈现间隔 P(i) − P(i−1)s分母,有多少时间在动
卡顿时间比(主指标)ΣH ÷ Ams/s动的时候卡了多少
卡顿次数#{H > 0};另报每分钟动画的次数次频繁短卡还是偶发
最长卡顿max Hms最严重的一次
卡顿时长分布按刷新间隔个数分档:1 个、2–3 个、≥4 个次卡顿轻重结构
Commit / Render 占比两类 ΣH 各占比例%优化方向是 App 主线程还是渲染
滑动段卡顿时间比仅统计目标进程 ScrollView 区间内的帧ms/s与 MetricKit 滑动口径同类比较
覆盖率有效判定帧时长 ÷ 会话中帧链覆盖时长%数据是否可信

5.3 质量语义 ​

  • A = 0(整场静止)时,卡顿类指标为 not-applicable,显示“无动画”,不写 0。
  • 断链区间、来源缺失区间不计入 A,覆盖率如实下降;覆盖率 < 80% 时报告标注“覆盖不足”。
  • 刷新率切换(Vx)只用于容忍,不改变 V(i) 的记录值。
  • 展示分级参照 Xcode Organizer:≤10 良好、≤25 注意、≤50 严重、>50 需立即处理,明确标注“参照 Apple Organizer 分级,非官方判定”。
  • 名称:界面使用“卡顿时间比(Perfowl)”,不使用“Apple Hitch Ratio”等官方同名。

5.4 实时与报告 ​

  • 实时:每秒输出“本秒卡顿时长(ms)”与“本秒是否在动”,工作台新增一条曲线,静止秒留空。
  • 报告:画面表现区新增卡顿时间比卡片(附分级色、动画时长、覆盖率)、卡顿时间轴标记(点击定位对应秒的 CPU/GPU/截图)、Commit/Render 占比;Excel 逐秒页新增“卡顿时长(ms)、是否动画”,逐帧页新增“刷新间隔、帧生命周期、卡顿时长、卡顿类型”。

六、样本试算(candidate,E3) ​

数据:设备 A,2026-11-02 150 秒抓包(Owner 期间滑动),算法 5.1。

项值
连续动画时长10.50 s(1,011 帧;静止断开 34 处)
卡顿次数 / 总时长6 次 / 91.8 ms
卡顿时间比8.7 ms/s(参照分级:良好)
最长卡顿37.5 ms
时长4.2、8.3、8.3、12.5、20.9、37.5 ms
与系统 Stalls 重合4 / 6
未容忍刷新率切换时31 次、19.5 ms/s(24 次为 ProMotion 降频假阳性)

未被检出的 7 个 Stalls 帧:3 个是静止约 10 秒后动画的第一帧(生命周期偏长,但不满足“连续”);4 个生命周期偏长但呈现节奏未受影响(三缓冲吸收)。这两类是否计为 Hitch,必须以 Instruments 结果为准(第七节 Phase 2)。

七、实施方案 ​

阶段内容产出与门禁估计
Phase 1 数据保留解析器保留 V、B、渲染/GPU/就绪偏移、Stalls 标记,写入逐帧记录;白名单接收目标进程 ScrollView 与 Commit 事件;离线重算工具(读取会话逐帧记录输出 5.1/5.2)。iOS 17 FrameLifetime 映射核验。单元:原始元组样例逐字段解码、begin_end_32b 拆分、刷新率切换容忍、静止不计;基准:解析 CPU、内存、逐帧记录体积前后对比(每帧增加约 40 字节)1–2 天
Phase 2 校准Fixture 新增可控卡顿场景:DisplayLink 匀速动画中按计划时刻注入主线程阻塞 20/50/100 ms(Commit 卡顿)、重渲染负载(Render 卡顿)、静止→动画起步、ProMotion 降频减速。同一会话同时录 Instruments「Animation Hitches」(deferred)与 Perfowl,导出后按设备 Mach 时间逐个对齐。另选 2 个真实 App 滑动场景。逐次卡顿召回率与精确率 ≥ 95%;单次时长误差 ≤ 1 个刷新间隔;卡顿时间比与 Instruments 差 ≤ max(10%, 2 ms/s);两类边界规则按结果定稿,公式版本升为 v1(非 candidate)。若并行录制互相干扰,改为 Fixture 确定性重放分别录制2–3 天,需 Owner 配合约 30 分钟
Phase 3 产品化实时每秒卡顿曲线;报告卡片、时间轴标记、分级;Excel 列;注册 metricID(卡顿时间比、次数、最长、动画时长、Commit/Render 占比);知识页按十要素发布;首页与总览联动真机 E4:Owner 在新包上录含滑动/静止/转场的会话,报告数值与离线重算一致;静止会话显示“无动画”2–3 天
Phase 4 诊断与 Hang卡顿时刻关联主线程/CPU/GPU 证据;contributors 解码以实现目标 App 归属;Hang(≥250 ms 主线程无响应)候选来源研究另立方案待定

执行效率要求:解析为逐帧 O(1) 增量,众数窗口固定 16 帧;不新增进程、轮询或全量扫描;Phase 1 交付时附解析吞吐与内存前后对比。

八、风险与待证 ​

  1. 预期生命周期推导:官方以 API 提供的 presentation time 为准,事件未直接给出预期值,稳态众数可能在缓冲模式切换瞬间误判——Phase 2 以 Instruments 对齐验证。
  2. args2 语义:刷新间隔解读基于 3 种取值与呈现间隔一致的统计证据(E3),未见官方说明;Phase 1 增加断言,出现其他取值时降级为“刷新间隔未知”、不判定。
  3. 系统更新风险:FrameUpdate 为私有 signpost,iOS 版本变更可能改字段;解析失败按 unsupported-schema 退化,不输出卡顿数。
  4. 前台归属:合成器帧包含系统 UI(状态栏、键盘),范围为 foreground-device;目标 App 精确归属依赖 Phase 4。
  5. 感知修正:Apple HitchTimeMetric 的感知修正未公开,Perfowl 值与 Organizer/MetricKit 数值不承诺相等。
  6. 并行录制干扰:Instruments 与 Perfowl 同时占用 DVT 可能互相影响,Phase 2 首先验证。

九、需要 Owner 决定 ​

  1. 是否按上述阶段启动(建议先做 Phase 1 + Phase 2,校准通过后再做界面)。
  2. Phase 2 需要 Owner:保持设备 A 连接解锁、Xcode Instruments 可见设备,按脚本在 Fixture 与两个真实 App 上操作约 30 分钟。
  3. 界面名称:建议“卡顿时间比”(主)、“卡顿次数”“最长卡顿”“动画时长”。

Owner 决策 · 2026-11-03 ​

启动 Phase 1(数据保留)与 Phase 2(配对校准);界面名称采用“卡顿时间比 / 卡顿次数 / 最长卡顿 / 动画时长”。Phase 3 在 Phase 2 校准通过后另行汇报再启动。

Phase 1 Plan · 2026-11-03:数据保留与离线重算 ​

范围调整(Lead 决定):ActivityTrace 白名单是冻结的隐私分发协议,逐帧记录同时被实时、离线与 Swift 报告读取;接入 ScrollView / Commit 事件需改动这两处协议。它们只服务“滑动段卡顿时间比”与诊断,不影响主指标校准,因此移到 Phase 2 收尾(校准用的滑动区间直接取自 Instruments 轨迹)。Phase 1 只做逐帧时序保留、卡顿算法与离线重算。

  1. 解码保留(display_performance.py):FrameUpdate 在既有字段之外解析 args2 低 32 位、FrameRender、FrameGPUExecute、FrameReady 与类别(Stalls);逐帧输出新增 frameTiming 对象:lifetimeNs、targetIntervalNs、renderBeginNs/renderEndNs、gpuBeginNs/gpuEndNs、readyNs(均为相对帧开始的 ns 偏移)、stall。iOS 17 FrameLifetime 输出 lifetimeNs 与 targetIntervalNs(取 refresh_interval)。目标刷新间隔只接受 1–100 ms,否则为空。refresh_interval、帧耗时、FPS、P95、刷新率等既有字段与公式一律不变(不把 args2 喂给 12× 长间隔门)。
  2. 卡顿算法(新 hitch_metrics.py,perfowl-hitch.candidate.v1):按方案 5.1/5.2 逐帧增量计算;为取得 V(i+1) 延迟一帧定稿;稳态生命周期按目标刷新间隔分组、各保留最近 16 个非 Stalls 帧;输出逐帧结果、每秒桶(卡顿 ms、动画 ms)与会话汇总。O(1)/帧。
  3. 离线重算:采集组件新增 hitch-recompute --session DIR [--frames-csv PATH],读取会话 frame-events-live.jsonl 输出汇总 JSON(与 Phase 2 对齐脚本共用)。
  4. 测试:真机原始元组样例逐字段解码;合成序列覆盖稳态无卡顿、单帧迟到、静止断开不计、ProMotion 降频容忍、三缓冲切换、无动画 not-applicable、断链与 warmup;2026-11-02 设备 A 150 秒样本(仅时间戳)回放复现方案第六节数值。
  5. 基准:同一批原始 FrameUpdate 的解码吞吐前后对比、逐帧记录字节数前后对比、卡顿累加器吞吐。

Phase 1 Implementation Update · 2026-11-03 ​

文件修改
perfowl_collector_core/display_performance.pyFrameUpdate 额外解析 args2 低 32 位、FrameRender、FrameGPUExecute、FrameReady;新增 frame_timing() 与 signpost_identifier(),逐帧输出 frameTiming:lifetimeNs、targetIntervalNs(仅 1–100 ms)、renderBeginNs/renderEndNs、gpuBeginNs/gpuEndNs、readyNs、stall(仅为真时写出)、signpostId(64 位,含 swap_id,用于与 Instruments 对齐)。iOS 17 FrameLifetime 输出 lifetimeNs 与 targetIntervalNs。refresh_interval、帧耗时、FPS、P95、刷新率字段不变。
perfowl_collector_core/hitch_metrics.py(新)perfowl-hitch.candidate.v1:HitchAccumulator 逐帧增量判定(一帧前瞻),会话汇总、每秒桶、逐帧结果;recompute_session() 流式读取 frame-events-live.jsonl。
perfowl_collector_core_cli.py新增 hitch-recompute --session DIR [--frames-csv PATH]。
tests/test_hitch_metrics.py(新)、tests/fixtures/frameupdate_150s_timing.json(新,仅时间戳)16 项:真机原始元组逐字段解码、Stalls 与异常刷新间隔、iOS 17 映射、稳态无卡顿、单帧 Commit 卡顿、Render 卡顿、静止断开、ProMotion 降频、缓冲模式切换只计一次、静止会话 not-applicable、断链/无时序不判定、Stalls 不进稳态、分级、150 秒回放、会话流式重算。

偏差 ​

  • ScrollView / Commit 事件接入移到 Phase 2 收尾(见 Phase 1 Plan)。
  • 新增 signpostId:Instruments 表以 swap ID 标识帧,按 swap ID 对齐比按时钟对齐可靠;两半中哪一半是 swap_id 由配对数据判定(150 秒样本中两半都基本逐帧加 1,无法单独判定)。

验证(E2) ​

  • 新增 16 项通过;150 秒设备 A 样本回放:连续动画 10.504 s、卡顿 6 次 91.776 ms、8.737 ms/s、最长 37.537 ms、Stalls 11 个中 4 个落在卡顿帧,与研究报告一致。
  • Python 全量 3160 项,失败仅为既有 16 项环境基线(native_direct_archive、native_raw_graph、sw1)。
  • 基准(同一批 5 万条合成 FrameUpdate,x86_64):解码 34.9 → 44.9 µs/帧(单条微基准 31.3 → 38.9 µs,frame_timing 自身 2.6 µs);按 120 Hz 持续动画最坏情况约多占 0.09% 个核。逐帧记录 915 → 1,090 字节/行(+19%),持续 120 Hz 动画约多写 76 MB/小时,静止期几乎无帧。卡顿累加器为 O(1)/帧;150 秒样本 1,046 帧重算 < 10 ms。

遗留 ​

  • 逐帧记录体积增量:Phase 3 评估把诊断字段(渲染/GPU/就绪偏移)改为紧凑编码或仅开发者模式写出。
  • iOS 17 FrameLifetime 的 begin_time 是否同为帧开始时刻,未在 iOS 17 真机核验。

Phase 2 Prep · 2026-11-03:校准工具与前置条件 ​

已完成(E1/E2):

  • Fixture 卡顿校准场景(fixtures/ios/PerformanceProbeFixture/.../FixtureHitchCalibrationView.swift):60 秒固定时间表——静止 5 s → 120 Hz 动画 15 s(第 2/5/8/11 s 注入主线程阻塞 20/50/100/200 ms)→ 静止 5 s → 重渲染动画 15 s(160 个带阴影与模糊的图层)→ 静止 5 s → 干净动画 15 s;进入页面时暂停主界面常驻显示回调以保证静止段真的不更新;每次注入写设备 Mach 时间到 Documents/PerfowlHitchMarkers-<run>.jsonl。swiftc 类型检查通过。
  • 对齐脚本 tools/validation/hitch/align_instruments.py:读取 Perfowl 会话与 Instruments hitches、hitches-frame-lifetimes 导出,按 swap ID 对齐,输出召回率、精确率、逐次时长差、帧生命周期差、同分母卡顿时间比与四项门禁;可选读取 Fixture 标记核对每次注入。合成数据测试 2 项通过;已能解析本机 Instruments 真实导出(Mac 3 秒轨迹:10 行去重为 2 次卡顿、62 个帧生命周期)。
  • 配对录制脚本 tools/validation/hitch/record_instruments.sh:设备标识只取环境变量,录制后自动导出两张表并记录耗时与体积。
  • 本机 3 秒 Mac 轨迹实测:录制与收尾共 105 秒、CPU 110 秒,再次确认官方模板只适合离线校准。

阻塞(需 Owner 操作):

  1. 本机 Xcode 26.6 未安装 iOS 26.5 平台组件(xcodebuild 报 “iOS 26.5 is not installed”),导致 xctrace 将设备 A 列为离线、也无法为真机构建 Fixture。需在 Xcode › Settings › Components 安装 iOS 26.5(数 GB)。
  2. 本机开发描述文件均不含设备 A;Fixture 真机安装需在 Xcode 中用开发团队自动签名登记设备(会修改开发者账户的设备列表),或由 Owner 在 Xcode 中直接运行安装。
  3. 两者完成后执行:Owner 打开 Perfowl 开始测试 → 运行 record_instruments.sh(75 秒)→ 手机上进入 Fixture「卡顿校准」点开始 → 结束后对齐;再在 2 个真实 App 上各滑动 60 秒重复一次。

Validation Update · 2026-11-03:内部测试包 250 安装 ​

  • 来源:main 412a5f49(源码门禁 passed);DMG Perfowl-250-macos-x86_64-internal.dmg,SHA-256 714e0abb94e1ad0f20c160bbaed0ed7eec27b672fbee266044a4b7ab5733193a,145,455,163 字节。
  • codesign --verify --deep --strict 通过;安装后目录树与 DMG 一致;冻结 Collector 109 个源模块与源码一致,CLI 与源码一致;包内 hitch-recompute 对 150 秒样本复现 6 次 / 91.776 ms / 8.737 ms/s(E2)。
  • 公证按 Owner 决定暂缓(Gatekeeper 退出码 3,符合预期);回滚包 249 已校验。
  • 启动器唯一:mdfind 与 lsregister 均只剩 /Applications/Perfowl.app(E3)。
  • 真机记录新字段(E4)尚未进行,随 Phase 2 配对录制一并核验。

Phase 2 执行方案 · 2026-11-03(1.0.0 发布后) ​

前置条件进展:Owner 同意由 Claude 安装 iOS 26.5 平台组件。xcodebuild -downloadPlatform iOS 两次在注册阶段失败(“Expected a single .dmg file, but found multiple []”),原因是系统模拟器运行时缓存中有一个 2026-06-12 遗留的 48 KB 不完整资产,Xcode 将其当作已下载。删除需管理员权限,由 Owner 执行删除后重新下载。安装前 xctrace record 对设备 A 报“Timed out waiting for device to boot”(CoreDevice 显示已配对可用)。

调整:先不做 Fixture(需要签名决定),先用 2 个真实 App 滑动与 Instruments 对照;Fixture 在需要精确定位时再做。

执行顺序(Owner 约 15 分钟,Claude 不在手机上安装或删除任何东西):

  1. 3 秒试录确认 Instruments 能连上设备 A。
  2. 对照 1:Owner 在 Perfowl(1.0.0)选目标 App A 并开始测试;Claude 同时运行 record_instruments.sh(75 秒);Owner 在 App A 中滑动 60 秒(含快速滑动、停顿、再滑动);结束后停止测试。
  3. 对照 2:目标 App B 重复第 2 步。
  4. 样本采集:Perfowl 停止状态下,Claude 运行只读脚本 probe_scroll_commit.py 30 秒,Owner 在任一 App 滑动;保存 ScrollView 区间与 Transaction/Commit 事件原始样本,用于确定解析字段(区间开始/结束配对方式、目标进程过滤字段)。
  5. Claude 用 align_instruments.py 对齐两组数据,按第六节门槛(召回率与精确率 ≥ 95%、单次时长误差 ≤ 1 个刷新间隔、卡顿时间比差 ≤ max(10%, 2 ms/s))给出结论;若 Perfowl 与 Instruments 同时订阅互相干扰(任一方丢帧或断流),改为分别录制同一操作并只比较分布,结论降级为 partial。

滑动 / 提交事件接入:等第 4 步样本后写解析方案。改动点为 ActivityTrace 白名单(is_frame_lifetime_signpost)与逐帧记录协议,按 Phase 1 范围调整中的说明在 Phase 2 收尾实施。

Implementation Update · 2026-11-03:Instruments 前置条件就绪 ​

  • iOS 26.5(23F77)平台组件安装完成,simctl runtime list 显示 Ready(E3)。
  • 安装后 xctrace record 仍报“Timed out waiting for device to boot”。原因是设备 A 的 CoreDevice 通道处于 disconnected,xctrace 不会主动建立通道。执行一次只读的 devicectl device info details 后,通道变为 connected,开发者磁盘镜像服务可用,设备 A 出现在 xctrace 在线列表中(E3)。
  • 用 Animation Hitches 模板对全部进程试录 3 秒成功,生成 71 MB 的 trace,包含 hitches、hitches-frame-lifetimes、hitches-renders、hitches-updates、hitches-gpu、hitches-framewait 与 display-* 表(E3)。试录期间未在手机上安装或删除任何东西。
  • tools/validation/hitch/record_instruments.sh 录制前先执行上述只读查询来建立通道。
  • 下一步:Owner 配合对照 1 / 2 与样本采集(见本节步骤 2–4)。

Validation Update · 2026-11-03:对照 1 失败(Instruments 与 Perfowl 同时录制) ​

  • 第一次开录报“No devices found”:xctrace 的在线设备列表比 CoreDevice 通道晚一拍。脚本改为建立通道后最多等 30 秒,直到 xctrace 把设备列为在线再开录。第二次正常开录(E3)。
  • 对照 1(目标 App A,Perfowl 测试同时进行,限时 75 秒):xctrace 到时后停在“Stopping recording…”超过 12 分钟。trace 只写出原始 trace-data.atrc,共 19.4 GB,缺少 instrument_data / shared_data 等,无法导出。系统临时目录另有同次的 19 GB ktrace。磁盘可用空间降到 11 GiB 后,Claude 中断了 xctrace(E3)。
  • 对比:同一设备只开 Instruments、全部进程试录 3 秒,得到 71 MB,按比例 75 秒约 1.8 GB。本次约为 10 倍。未证假设:Perfowl 的 coreprofilesessiontap 与 Instruments 同时订阅设备 kdebug,双方相互影响。需要单独录 Instruments 来排除,在此之前不作定论。
  • 清理:删除系统临时目录中已无进程引用的 3 个 instruments ktrace(3 s 试录、本次、上午探测的临时文件)。失败的 trace 暂时保留,可否删除由 Owner 决定。
  • 依照第六节“相互干扰则改为分别录制”的预案,对照改为:Instruments 单独录 30 秒 → Perfowl 单独测同一操作,只比较分布,结论上限为 partial。Owner 在本次对照中完成的 App B 滑动没有 Instruments 录制,不计入。
  • Owner 决定(2026-11-03):原始数据较大时用完即删。已删除失败的 19.4 GB trace 与已用完的 3 秒试录 trace,可用空间回升到 30 GiB。record_instruments.sh 改为导出表格后删除 trace(设 KEEP_TRACE=1 时保留),后续只保留导出表格、数字与本记录。

Validation Update · 2026-11-03:分别录制对照(App A / App B,各 30 秒) ​

方法:Instruments(Animation Hitches,全部进程)单独录 30 秒 → Perfowl 单独测同一页面、同样滑动 30 秒,只比较分布,结论上限 partial。工具改进:record_instruments.sh 开录前重复建立 CoreDevice 通道,直到 xctrace 列为在线(最多约 20 秒,否则退出);align_instruments.export_tables 对 xctrace export 偶发的 SIGSEGV(Xcode 26.6,Intel,崩溃在 autorelease pool 释放)最多重试 5 次。单独录 30 秒的 trace 为 551 MB / 312 MB,导出后删除。

App A · InstrumentsApp A · Perfowl candidate v1App B · InstrumentsApp B · Perfowl candidate v1
帧节奏8.3 ms 为主(120 Hz)8.3 ms 为主应用帧 16.7 / 33 ms 为主目标间隔 8.3 / 12.5 / 16.7 ms 混合
时长约 28 s 逐帧数据动画 34.2 s约 30.5 s 逐帧数据动画 31.7 s
卡顿次数89045
单次时长均 12.5 ms8.3 ms ×8、16.7 ms ×1—16.7 ms 为主,最大 33.4 ms
卡顿时间比约 3.6 ms/s2.4 ms/s022.4 ms/s(warning)

结论(E4,真机;分别录制,不能逐次配对):

  • App A(120 Hz 持续滑动):分布在门槛内(时间比相差约 1.2 ms/s ≤ 2 ms/s,单次时长相差约 4.2 ms,不到一个刷新间隔),partial 通过。Instruments 报的单次时长系统性地多约半帧,原因待查。
  • App B(应用自行降低帧率 / 刷新率可变):Perfowl 报 45 次卡顿,Instruments 为 0,FAIL。待证假设:candidate v1 以 FrameUpdate 的 targetIntervalNs 推算预期上屏时间,刷新率在 8.3 / 12.5 / 16.7 ms 之间切换时,把正常的降频帧判成了卡顿;Instruments 按应用请求的帧节奏判定。
  • 在弄清 App B 误报并修正算法之前,卡顿指标保持 candidate,不进入产品界面。
  • 下一步:在 App B 上做第 4 步原始事件采样(probe_scroll_commit.py 30 秒),并查看 Perfowl 逐帧的 target interval 与 lifetime,确定判定预期上屏时间的依据。

Validation Update · 2026-11-03:App B 原始事件采样与误报初查 ​

  • 采样:Perfowl 停止状态下,probe_scroll_commit.py 只读订阅 ActivityTraceTap 30 秒,Owner 在 App B 滑动。共有 2748 个 UIKit UpdateCycle/UpdateSequence、2146 个 Transaction/Commit、880 个 IOMFBServer/FrameUpdate,ScrollView 区间 Dragging 46 / DraggingAndDeceleration 47 / Deceleration 37 / Animating 2。另有 UpdateCycle.Stalls/UpdateSequence 8 个、IOMFBServer.Stalls/FrameUpdate 7 个(E3)。原始样本约 2 MB,保存在私有目录。
  • Commit 带 transaction_seed 与 begin_time,作用域是进程,可按目标 App 进程过滤;FrameUpdate 来自系统合成进程,ID 由 update_seed 与 swap_id 组成。两者可以通过 seed 关联。
  • 前一假设被推翻:App B 的 45 次 Perfowl 卡顿中,36 次的前后帧目标间隔都是稳定的 16.67 ms,与刷新率切换无关(切换处只有 7 次)。
  • 新的待证假设:App B 本身约每 33 ms 才提交一帧新内容(Instruments 逐帧表中 32.9 ms 最多),屏幕按 60 Hz 合成时会有“没有新内容”的空档。candidate v1 只看整机 FrameUpdate 的目标间隔,把这些空档算成了卡顿;Instruments 只在“已提交的帧比预期晚上屏”时才算卡顿。
  • 修正方向(待写入方案后再实施):用目标 App 进程的 Transaction/Commit 确定哪些帧真正由该 App 提交,预期上屏时间以提交为起点计算,没有新提交的合成帧不参与判定。这就是 Phase 2 收尾计划中的“ScrollView / Commit 接入”。效率评估:Commit 全机约 70 条/秒,按进程过滤后只保留目标 App。

Phase 2 修正方案 · 2026-11-03:candidate v2(连续判定留出起始余量) ​

Owner 2026-11-03:“继续,写方案并实现”。

证据(E3,离线重放两次真机会话的逐帧数据) ​

  • App B 的 36 次 16.7 ms 卡顿,都是同一种帧:目标间隔 16.7 ms(60 Hz),开始时刻 B(i) 正好等于上一帧上屏 P(i-1)(差 0~0.02 ms),寿命 33.4 ms,上屏间隔 33.4 ms。它与“开始晚于上一帧上屏 16.7 ms、寿命 16.6 ms”的帧交替出现,整体是 App 按 30 帧/秒正常出帧(Instruments 逐帧表的 32.9 ms 间隔与此一致)。
  • v1 的连续判定 B(i) <= P(i-1) 包含等号,所以这类“上一帧上屏时才开始”的帧被算进动画,并按 60 Hz 的预期判成晚了一帧。
  • 真正连续出帧时,下一帧在上一帧上屏前约 0.39~0.49 ms 就开始(两台会话的 B(i) − P(i-1) 分布:120 Hz 集中在 −0.48 / −0.49 ms,60 Hz 在 −0.39~−0.49 ms),与 0~−0.02 ms 的“上屏时才开始”之间有明显空档。
  • 也试过只按寿命判定(去掉 P(i-1)+V 一项),误报反而更多(App A 65 次、App B 285 次),已否决。

公式变更(仅 5.1 的连续判定) ​

text
continuous(i) = B(i) <= P(i-1) − M,  M = 0.2 ms   (v1:M = 0)

其余(预期上屏、卡顿时长、动画时间、会话汇总、质量语义)不变。M 取两类分布空档的中点;在 0.05~0.3 ms 之间结果基本稳定(App B 0.1~0.3 ms 都是 4 次,0.05 ms 时 5 次)。算法版本改为 perfowl-hitch.candidate.v2,仍是候选值,不进产品界面。

离线预期(E3) ​

会话v1 次数 / 时间比v2 次数 / 时间比Instruments(分别录制)
App A 单独9 / 2.44 ms/s7 / 1.72 ms/s8 / 约 3.6 ms/s
App B 单独45 / 22.36 ms/s4 / 2.09 ms/s0 / 0
同时录制失败那次的 App A / App B 会话30 / 5.66;22 / 4.5522 / 4.31;4 / 1.01无
  • App B 的时间比差为 2.09 ms/s,略高于 2 ms/s 的门槛。剩下的 4 次(8.3 ms ×2、12.5、33.4 ms)可能是真实卡顿,因为两边不是同一次滑动,无法逐次核对。所以结论只能写“误报的主要来源已消除”,不能写 PASS。
  • 风险:M 是两台会话、一部设备(120 Hz ProMotion,iOS 26)得到的经验值;60 Hz 机型与其他系统版本需在能力确认阶段复核。

实施与验证 ​

  • hitch_metrics.py:新增常量 CONTINUITY_MARGIN_NS = 200_000,判定改为 begin > before.present - CONTINUITY_MARGIN_NS 则不连续;HITCH_ALGORITHM 改为 v2;文档字符串同步。
  • 单测:新增“上屏时才开始的 30 帧节奏不是卡顿”;“晚一帧提交”等原有用例保持通过;150 秒回放的基准数字若变化,按新结果更新并在此记录。
  • 效率:判定仍是每帧 O(1),无新增数据。对比改动前后离线重算 4 个会话的耗时。
  • 真机:再找一个自带限帧的 App 复核(Owner 配合,可与 iOS 版本能力确认一起做)。

Implementation Update · 2026-11-03:candidate v2 ​

文件修改
MacApp/Sidecar/perfowl_collector_core/hitch_metrics.pyCONTINUITY_MARGIN_NS = 200_000;连续判定改为 B(i) <= P(i-1) − M;HITCH_ALGORITHM = "perfowl-hitch.candidate.v2";文档字符串写明原因与设备 A 的分布
MacApp/Sidecar/perfowl_collector_core/display_performance.py注释中的算法版本号
MacApp/Sidecar/tests/test_hitch_metrics.py新增 test_app_paced_below_display_rate_is_not_a_hitch(60 Hz 下 App 每两次刷新出一帧、在上一帧上屏时才开始):v2 为 0 次,把 M 改回 0 时为 8 次(已验证会失败);150 秒回放基准中连续帧 1011 → 1007、动画时间 10.50 → 10.37 s、时间比 8.7 → 8.85 ms/s,卡顿次数(6)、总时长(91.8 ms)、最大值(37.5 ms)不变

偏差:无。方案中“对比 4 个会话”的结果与离线预期一致:App A 7 次 / 1.715 ms/s,App B 4 次 / 2.092 ms/s;同时录制那次的两个会话 25 次 / 4.468 和 4 次 / 1.005。离线预期用的是简化脚本,与正式实现的帧链接条件略有差别,后两个数字(预期 22 / 4)以本段为准。

验证:

  • 卡顿单测 16 项全部通过;Sidecar 全量 3162 项,失败与错误只有已知的 16 项环境基线(native_direct_archive 11、native_raw_graph 4、sw1 1),无新增(E2)。
  • 效率:4 个会话离线重算,改动前后交替各测 2 轮,每轮取 5 次最快值。改动前 111~269 ms,改动后 111~271 ms,差异在噪声范围内;内存峰值约 30 MB,不变。判定仍是每帧 O(1)(E3)。
  • 真机:用的是本次两个 App 的真机会话离线重算(E3)。新 Collector 尚未打包,仍需在一个自带限帧的 App 上复核。

状态:v2 已实现,仍是 candidate,不进产品界面。App B 时间比差 2.09 ms/s,略高于门槛(Instruments 为分别录制),不能写 PASS。

遗留:

  1. 打包:Collector 有改动,但卡顿指标只在开发者模式下离线重算,不影响 1.0.0 的界面与已有指标。双架构包需要 Apple 芯片机器重新冻结 arm64 工具,因此本批不单独打包,等下一批产品改动一起打包公证,交付状态记为 partial。
  2. Instruments 报的单次卡顿时长比 Perfowl 系统性地长约半帧,原因待查。
  3. M 需要在 60 Hz 机型和其他系统版本上复核,与 iOS 15+ 能力确认一起做。

Phase 2 方案 · 2026-11-04:Fixture 限帧校准(v2 真机复核) ​

Owner 2026-11-03:验收用的 Fixture 能做限帧;授权 Claude 构建并安装 Fixture 到设备 A(Xcode 自动签名,允许登记设备;只动这一个测试 App)。

  • 校准时间表扩展为 hitch-calibration/2(100 秒):原有 60 秒不变(静止 → 120 Hz 动画含 20/50/100/200 ms 注入 → 静止 → 重渲染 → 静止 → 干净 120 Hz);之后追加“静止 5 s → 30 Hz 限帧动画 15 s(第 3 / 9 s 注入 50 / 100 ms)→ 静止 5 s → 60 Hz 限帧动画 15 s(无注入)”。限帧段用 CADisplayLink.preferredFrameRateRange = (hz, hz, hz);每段开始的标记写入 hz。
  • 真值与判定:
    • 120 Hz 与 30 Hz 段的每次注入都应识别为卡顿:时间对得上,时长误差不超过一个刷新间隔。
    • 30 Hz、60 Hz 段除注入外不应有卡顿:v1 预期会误报,v2 应为 0 或接近 0。
    • 静止段不计入动画时间。
    • 标记用设备 Mach 时间,与 Perfowl 帧时间戳同源,可以直接对齐。标记文件在 Fixture 自己的 Documents 里,Claude 只读取这一个开发签名 Fixture 的容器。
  • 执行:Fixture 真机构建 → devicectl device install app 只装 Fixture → Owner 开 Perfowl 测 Fixture,在 Fixture 里进入“卡顿校准”点开始,等 100 秒 → Claude 读取标记,用 v1 / v2 各重算一次后对比。整个过程不需要 Instruments。
  • 效率:Fixture 只增加一个时间表分支;离线对比是一次流式读取,单会话 < 1 秒。

Implementation Update · 2026-11-04:Fixture 限帧校准 ​

  • FixtureHitchCalibrationView.swift:每个时间段带 hz(默认 120);新增 30 Hz(注入 50 / 100 ms)与 60 Hz 两个限帧段,总长 100 秒;每段开始时设置 preferredFrameRateRange,120 Hz 段保持原来的 80~120 范围;标记写入 hz,计划名改为 hitch-calibration/2;入口文字改为“100 秒”。
  • build.sh:证书主题匹配兼容 OpenSSL 3 的 OU = X 格式(此前只认 LibreSSL 的 OU=X,在本机找不到开发身份)。
  • 验证:不签名真机编译 BUILD SUCCEEDED(E2)。
  • 阻塞:真机签名构建报 “No Accounts”,描述文件不含设备 A,原因是本机 Xcode 没有登录开发者账号,自动登记设备做不了。Claude 改用已保存的 API 密钥登记,被本机权限机制拦下,未绕过。三条出路由 Owner 选:
    1. 在 Xcode › Settings › Accounts 登录开发团队账号后,Claude 重跑构建;
    2. Owner 在 Xcode 里直接运行到设备 A;
    3. Owner 在 Claude Code 权限设置中允许这一步。

Validation Update · 2026-11-04:Fixture 限帧校准真机结果(v1 / v2) ​

  • 签名与安装:两条签名路径都没有走通。本机证书所属团队在 Xcode 里没有账号;Owner 登录的账号属于另一个团队,而该账号为本机签发过的开发证书私钥不在本机(Xcode 要求先吊销,Claude 没有吊销)。最后由 Claude 从 main 76b1afd9 打出不签名 IPA(仓库外私有目录),Owner 自行重签后装到设备 A。
  • 一次 100 秒校准:会话约 1 分 52 秒,逐帧记录 3857 帧,标记 24 行,hitch-calibration/2(E4 原始数据,E3 离线重算)。
  • 设备本次全程以 60 Hz 刷新:所有帧的目标间隔都是 16.67 ms,标为 120 Hz 的段也一样,原因未查(可能是低电量模式或“限制帧速率”设置)。因此“60 Hz 限帧段”与显示刷新率相同,没有测到“低于刷新率”的情况;30 Hz 段正好是 App B 的模式:446 帧中 441 帧在上一帧上屏时才开始(B − P(i-1) 在 ±0.2 ms 内),寿命 33.4 ms。
  • 逐段结果(每次注入的卡顿 → 检出时长;“额外”指注入之外的卡顿次数):
段v1v2
120 Hz 标称(实际 60 Hz)注入 20 / 50 / 100 / 200 ms16.7 / 50.0 / 100.1 / 200.2,额外 0同 v1
重渲染 15 s432 次(每次 16.7 ms)同 v1
重渲染后的静止段2 次同 v1
30 Hz 限帧,注入 50 / 100 ms50.1 / 100.1,额外 0两次都漏检,额外 0
60 Hz 限帧、干净 120 Hz 段、其他静止段00
  • 解读:
    1. 20 ms 的注入检出 16.7 ms,是 60 Hz 下按一帧量化的结果,在一个刷新间隔之内。
    2. 重渲染段每帧寿命 66.7 ms、上屏间隔约 34.6 ms,433 帧全部带系统 Stalls 标记,432 次卡顿与之一致,判为真实卡顿;静止段那 2 次是重渲染的尾巴(同样带 Stalls 标记)。
    3. v2 在限帧 App 上丢了真卡顿:限帧 App 卡住的那一帧,同样是在上一帧上屏时才开始,v2 把它判成“不连续”,直接跳过,所以漏检。v1 在 30 Hz 段没有误报,是因为这里所有帧都是限帧帧,稳态寿命的众数是 2 个刷新间隔;App B 误报的原因是限帧帧与连续帧混在同一个刷新间隔下,众数被连续帧的“1 个间隔”占了。
  • 结论:v2 在 Fixture 上失败(限帧段召回 0/2),不能保留。

Phase 2 修正方案 · 2026-11-04:candidate v3(限帧帧单独计稳态) ​

依据上面的 Fixture 结果与 App B 的逐帧数据。

公式变更(5.1) ​

text
gap(i)   = B(i) − P(i-1)
start(i) = continuous  当 gap(i) <= −M
           paced       当 −M < gap(i) <= M        (App 在上一帧上屏时才开始)
           idle        当 gap(i) > M              (静止后重新开始,不判定)
L*(i)    = 按 (V(i), start(i)) 分组,取最近 16 个非 Stalls 帧 round(lifetime / V) 的众数,乘 V(i)
judged(i) = start(i) ∈ {continuous, paced}        (v1 的 B(i) <= P(i-1) 基本等价;v2 只判 continuous)
E(i), H(i) 同 v1;动画时间累计 judged 帧

M 仍为 0.2 ms。限帧帧与连续帧各用自己的稳态寿命:App B 的限帧帧稳态为 2 个间隔,不再被判晚一帧;限帧 App 真正卡住的那一帧寿命变长,仍能检出。算法版本 perfowl-hitch.candidate.v3。

离线预期(E3,原型在私有目录) ​

数据v1v2v3对照
Fixture 注入 6 次6/64/66/6标记真值
Fixture 限帧与干净段额外卡顿0000
App A 单独9 / 2.44 ms/s7 / 1.728 / 1.95Instruments 8 / 约 3.6(分别录制)
App B 单独45 / 22.36 ms/s4 / 2.095 / 2.11Instruments 0(分别录制)

App B 剩下的 5 次里,4 次出现在刷新率在 8.3 / 12.5 / 16.7 ms 之间切换的帧附近,1 次是带系统 Stalls 标记的 33.4 ms 帧;两边不是同一次滑动,无法逐次判定真伪,记为遗留,不在本批处理。

实施与验证 ​

  • hitch_metrics.py:稳态历史改为按 (V, start) 分组;跳过条件改为只跳 idle;版本 v3;文档字符串同步。
  • 单测:保留“限帧不是卡顿”;新增“限帧 App 卡住一帧要检出”(v2 下会失败);新增“限帧帧与连续帧交替时不误报”(v1 下会失败)。150 秒回放的基准数字若变化,按新结果更新并在此记录。
  • 效率:每帧仍是 O(1),稳态分组键多一维,组数最多翻 3 倍(每组 16 个整数)。对比改动前后 4 个会话离线重算耗时与内存。
  • 真机:用本次 Fixture 会话与 App A / B 会话离线重算(E3)。下一轮 Fixture 校准需让设备真正跑 120 Hz(关闭低电量模式与“限制帧速率”),才能覆盖“60 Hz 限帧低于刷新率”的情况。

Implementation Update · 2026-11-04:candidate v3 ​

文件修改
MacApp/Sidecar/perfowl_collector_core/hitch_metrics.py按 gap(i) 把帧分为 continuous / paced / idle;稳态寿命历史按 (V, start) 分组;只跳过 idle;HITCH_ALGORITHM = "perfowl-hitch.candidate.v3";文档字符串改为 v3 公式与原因
MacApp/Sidecar/perfowl_collector_core/display_performance.py注释中的算法版本号
MacApp/Sidecar/tests/test_hitch_metrics.py新增 test_paced_frame_that_stalls_is_a_hitch(60 Hz 上 30 帧/秒限帧,注入 3 个间隔的卡顿,应检出 1 次 50 ms);“限帧不是卡顿”改为断言限帧帧计入动画时间;Stalls 稳态用例改用新的分组键;150 秒回放回到 v1 的数字(连续帧 1011、动画 10.50 s、8.74 ms/s;次数 6、总 91.8 ms、最大 37.5 ms 不变)

偏差:方案写“新增限帧与连续帧交替时不误报”的用例,实际复用了 v2 已有的“限帧不是卡顿”用例,它本身就是 20 个连续帧后接限帧交替,不再重复新增。

验证:

  • 卡顿单测 17 项全部通过。把实现换回 v2 时,“限帧卡住要检出”会失败;换回 v1(M = 0、不分组)时,“限帧不是卡顿”会失败(E2)。
  • Sidecar 全量 3163 项,失败与错误只有已知的 16 项环境基线(native_direct_archive 11、native_raw_graph 4、sw1 1),无新增(E2)。
  • 离线重算(正式实现,E3):
会话v2v3
App A 单独7 / 1.715 ms/s8 / 1.953 ms/s
App B 单独4 / 2.092 ms/s5 / 2.105 ms/s
同时录制那次的两个会话25 / 4.468;4 / 1.00525 / 4.478;3 / 0.783
Fixture 校准(6 次注入)438 次,限帧段注入 0/2440 次,注入 6/6,限帧与干净段额外 0

Fixture 的次数主要来自重渲染段(432 次真实卡顿),时间比没有对照意义。

  • 效率:5 个会话离线重算,改动前后交替测 3 轮,每轮取 5 次最快值。改动前 105~429 ms,改动后 109~556 ms;556 ms 是单轮离群(另两轮同一会话 366 / 417 ms),其余同会话差异都在噪声内。内存峰值约 30 MB,不变;每帧仍是 O(1)(E3)。

状态:v3 已实现,仍是 candidate,不进产品界面。Fixture 限帧段的召回问题已修复;App B 与 Instruments 的差距(2.11 ms/s,门槛 2 ms/s)仍略超,不能写 PASS。

遗留:

  1. App B 剩下的 5 次与刷新率切换、系统 Stalls 帧有关,需要同一次滑动的 Instruments 对照才能定真伪;同时录制会撑爆 trace,方法待定。
  2. 下一轮 Fixture 校准让设备真正跑 120 Hz,覆盖“低于刷新率的 60 Hz 限帧”。
  3. 打包:同 v2,等下一批产品改动一起打包公证,交付状态 partial。
  4. Fixture 签名:本机开发证书与 Xcode 登录账号不属于同一团队,目前靠 Owner 手动重签 IPA。

Validation Update · 2026-11-04:Fixture 校准第 2 轮(Owner 关闭低电量与帧率限制后) ​

  • 一次 100 秒校准,标记 24 行,逐帧记录约 3830 帧(E4 原始数据,E3 离线重算)。
  • 仍然全程 60 Hz:每帧目标间隔都是 16.67 ms。根因在 Fixture 本身:Info.plist 没有 CADisableMinimumFrameDurationOnPhone = YES。iPhone 上的 App 不声明这个键时,系统把显示回调限制在 60 Hz 以内,请求 120 Hz 也只得到 60 Hz(E1,Apple 文档行为,与两轮数据一致)。手机设置不是原因。
  • v3 结果与第 1 轮一致:6 次注入全部检出(16.7 / 50.0 / 100.1 / 200.2 / 50.1 / 100.1 ms),30 Hz、60 Hz 限帧段与干净段额外 0 次;重渲染段 431 次(每帧都带系统 Stalls 标记),其后静止段 1 次。v1、v2 本轮结果相同:v2 本轮在 30 Hz 段没有漏检,因为两次注入帧恰好落在“连续”一侧(该段 3 帧 continuous),说明 v2 的漏检取决于卡住时刻,不稳定。

Phase 2 方案 · 2026-11-04:Fixture 开启 120 Hz ​

  • fixtures/ios/PerformanceProbeFixture/PerformanceProbeFixture/Info.plist 增加 CADisableMinimumFrameDurationOnPhone = YES。
  • 影响范围:只解除“请求超过 60 Hz 时被限到 60 Hz”。请求 ≤ 60 Hz 的场景(实验室帧场景 30 Hz、P95 场景 30/60 Hz、GPU 场景 30 Hz、MTKView 60/30/15 Hz)行为不变;首页“显示回调负载”的 120 Hz 选项从此真正跑 120 Hz(此前实际为 60 Hz),历史结果不回改。
  • 验证:不签名构建并检查包内 Info.plist 带此键(E2);打出不签名 IPA 由 Owner 重签安装,再跑一次校准,确认 120 Hz 段目标间隔为 8.33 ms,60 Hz 限帧段落在“低于刷新率”的情况。

Implementation Update · 2026-11-04:Fixture 开启 120 Hz ​

  • Info.plist 增加 CADisableMinimumFrameDurationOnPhone = YES,其他不变。不签名 IPA 从本提交构建(仓库外私有目录),由 Owner 重签安装;包内 Info.plist 是否带此键在构建后核对,结果见下一条 Validation Update。

Validation Update · 2026-11-04:Fixture 120 Hz 构建核对 ​

  • 从 main d9f3a28f 不签名构建 BUILD SUCCEEDED,包内 Info.plist 的 CADisableMinimumFrameDurationOnPhone 为 true(E2)。不签名 IPA 已交 Owner 重签安装,真机 120 Hz 待第 3 轮校准确认。

Validation Update · 2026-11-04:Fixture 校准第 3 轮(120 Hz) ​

  • 新包重签安装后一次 100 秒校准,标记 24 行(E4 原始数据,E3 离线重算)。
  • 刷新率:120 Hz 段目标间隔 8.34 ms、稳态寿命 25.0 ms(3 个间隔),确认已解除 60 Hz 限制。30 Hz 与 60 Hz 限帧段系统把显示降到 60 Hz(目标间隔 16.67 ms),所以“60 Hz 限帧低于刷新率”在 Fixture 上无法复现——ProMotion 会跟随 App 的请求降刷新率。App B 那种情况需要屏幕上另有内容维持 120 Hz,Fixture 当前做不到,记为遗留。
  • 逐段结果(v1 / v2 / v3 本轮相同):
段结果
120 Hz 注入 20 / 50 / 100 / 200 ms检出 16.7 / 50.0 / 100.1 / 200.2 ms;后三次每次紧跟一次 8.3 ms
重渲染 15 s(446 帧全带 Stalls)445 次
重渲染后的静止段2 次(25.0、8.3 ms,均带 Stalls,重渲染尾巴)
第 0 段开头1 次 16.7 ms(带 Stalls,校准刚启动)
30 Hz 限帧注入 50 / 100 ms检出 50.1 / 100.1 ms,额外 0
干净 120 Hz、60 Hz 限帧、其余静止段0
  • 注入后紧跟的 8.3 ms:卡住的那帧之后,下一帧上屏间隔 16.68 ms、寿命 33.4 ms,比稳态 25.0 ms 多一个刷新间隔,屏幕上确实少刷新了一次。它算一次独立卡顿,还是 Instruments 会并入前一次,目前没有对照,不改算法,作为待核对项。
  • 结论:120 Hz 下注入卡顿召回 6/6,时长误差都在一个刷新间隔内;限帧与干净段 0 误报(E3,Fixture 单机单轮)。

下一步提案 ​

Fixture 的校准是确定性的,可以不同时录制:先用 Perfowl 跑一轮(已有本轮),再单独用 Instruments Animation Hitches 录一轮同样的 100 秒校准,按“段 + 注入时刻”对齐两边的卡顿次数与时长。这样避开了同时录制撑爆 trace 的问题,可以核对“注入后紧跟的一帧”和重渲染段 Instruments 怎么算。Instruments 录 100 秒约 2 GB,导出后按 Owner 规则立即删除。

Validation Update · 2026-11-04:Fixture 校准 Instruments 单独录制对照(第 4 轮) ​

  • 方法:Perfowl 不开会话,用 record_instruments.sh 录 Animation Hitches 140 秒(全进程),Owner 在录制开始后点校准“开始”。Fixture 时间表是确定性的,与第 3 轮 Perfowl 会话按“段 + 注入时刻”对齐;两边时间用 200 ms 注入那次卡顿定偏移(5.712 s),其余注入点在偏移后误差 ≤ 8 ms。
  • 成本:录制 140 s,xctrace 停止与落盘耗时,墙钟共 514 s;导出 83 s。trace 1.0 GB 已由脚本删除,另有 6.0 GB 的系统临时 ktrace 残留,确认 xctrace 已退出、无进程占用后删除;保留导出表 2.3 MB(私有目录)。
  • 对照(Instruments 同一 swap 多行取最大;E4 真机,两边为不同轮次):
段Perfowl v3(第 3 轮)Instruments(第 4 轮)
120 Hz 注入 20 / 50 / 100 / 200 ms16.7 / 50.0 / 100.1 / 200.2,后三次各紧跟 8.316.7 / 50.0 / 100.1 / 200.2,后三次各紧跟 8.3
重渲染 15 s445 次,共 11 174 ms409 次,共 11 524 ms
重渲染后的静止段2 次(25.0、8.3)1 次(12.5)
第 0 段开头1 次 16.7(带 Stalls)0
30 Hz 限帧注入 50 / 100 ms50.1 / 100.1不报(帧寿命表里有 83.4 / 133.5 ms 的帧,卡顿表无记录)
干净 120 Hz、60 Hz 限帧、其他静止段00
  • 结论:
    1. 连续动画下的注入卡顿,两边逐次一致,时长相同到 0.1 ms;注入后紧跟的 8.3 ms,Instruments 同样单独计一次,Perfowl 的做法与之一致,待核对项关闭。
    2. 重渲染段总时长差 −3.0%,次数差 +8.8%(Perfowl 更多 20.9 ms 的短卡顿,Instruments 更多 25.0 ms),不同轮次,在门槛(时间比 10%)内。
    3. 限帧 App 的卡顿 Instruments 不计:画面实际停了 50 / 100 ms(Fixture 标记为真值),Instruments 的卡顿表没有记录,行为与 v2 跳过“在上一帧上屏时才开始的帧”一致。这是口径差异,不是 Perfowl 误报。是否沿用 Instruments 的口径(v2)还是按用户可见卡顿报告(v3),交 Owner 决定;Lead 建议保留 v3,在报告中把这类卡顿单独标注“Instruments 不计”。

Owner 决定 · 2026-11-04:限帧卡顿照报并标注 ​

Owner 选择“报,并标注”:用户看得到的停顿都算卡顿(保留 v3);起点为 paced 的卡顿单独标注“Instruments 不计”,与 Instruments 对数时可以剔除。

实施(同一算法版本 v3,只加输出字段,不改判定):

  • 逐帧结果与卡顿列表加 pacedStart(布尔)。
  • 会话汇总加 pacedHitchCount、pacedHitchTotalMs,以及去掉这部分后的 instrumentsComparableRatioMsPerS(分母仍是同一动画时间),无动画时都为 None。
  • 单测:限帧卡住用例断言 pacedHitchCount = 1;连续动画用例断言为 0。
  • 效率:每帧多一次布尔判断,无新增数据。

Implementation Update · 2026-11-04:限帧卡顿标注 ​

  • hitch_metrics.py:卡顿帧与卡顿列表加 pacedStart;汇总加 pacedHitchCount、pacedHitchTotalMs、instrumentsComparableRatioMsPerS(无动画时为 None);算法版本仍为 v3(判定不变)。
  • test_hitch_metrics.py:按方案补断言。偏差:无。
  • 验证:卡顿单测 17 项通过;Sidecar 全量 3163 项,只有已知的 16 项环境基线失败(E2)。
  • 离线重算(E3):
会话次数 / 时间比其中限帧卡顿去掉限帧后的时间比
App A 单独8 / 1.95301.953
App B 单独5 / 2.1052 次 / 16.8 ms1.576(Instruments 0,差距进入 2 ms/s 门槛内)
Fixture 第 1 轮(60 Hz)440 / 106.72 次 / 150.2 ms(即 30 Hz 段两次注入)104.8
Fixture 第 3 轮(120 Hz)457 / 153.92 次 / 150.2 ms(同上)152.0

App B 的结论仍限于“分别录制、不是同一次滑动”,不能写 PASS。

  • 效率:6 个会话离线重算,改动前后交替 4 轮,每轮取 5 次最快值;各会话取 4 轮最小值求和,改动前 1266 ms、改动后 1278 ms(+0.9%,噪声内;单轮波动达 ±15%,来自本机负载)。内存峰值约 30 MB,不变(E3)。
  • 状态:v3 加标注已实现,仍是 candidate,不进产品界面。打包同前,待下一批产品改动一起打包公证(partial)。

知识沉淀 · 2026-11-04 ​

卡顿校准中可复用的方法与坑点写入 性能采集实验:已验证经验与对照方法 第 9 节:iPhone 60 Hz 上限与 CADisableMinimumFrameDurationOnPhone、ProMotion 跟随请求降刷新率、用确定性 Fixture 分开录制对照、Instruments 不计限帧卡顿、trace 与临时 ktrace 的体积和清理。卡顿仍未注册 metricID,不建指标页(F 十要素中 metricID 缺项)。

Lead 验收 · 2026-11-04:Phase 2 校准结论 ​

对照第七节 Phase 2 门禁(按方案允许的“Fixture 确定性重放、分别录制”方式):

门禁结果证据
注入卡顿召回 ≥ 95%120 Hz 4/4、60 Hz 4/4、30 Hz 限帧 2/2(v3)Fixture 标记真值,E4 原始 + E3 重算
精确率 ≥ 95%干净动画与限帧段 0 次额外;额外卡顿只出现在带系统 Stalls 标记的帧(启动、重渲染尾巴)同上
单次时长误差 ≤ 1 个刷新间隔注入 20 ms 量化为 1 帧,其余误差 ≤ 0.2 ms;与 Instruments 逐次相同到 0.1 msFixture 对 Instruments 分别录制
时间比差 ≤ max(10%, 2 ms/s)Fixture 重渲染段总时长差 3%;App B 去掉限帧卡顿后 1.58 ms/s(Instruments 0);App A 1.95 对约 3.6分别录制,非同一次操作
边界规则定稿注入后恢复帧单独计(与 Instruments 一致);限帧卡顿照报并标注“Instruments 不计”(Owner 决定);静止后首帧不判定本记录

结论:Phase 2 有条件通过(Fixture 范围 E4/E3)。真实 App 的对照只能比分布,两次都在门槛内,但不是逐次对齐,保留为已知限制。适用范围:设备 A、iOS 26、120 Hz ProMotion。其他 iOS 版本与 60 Hz 机型并入分版本能力表核验。

算法定稿为 perfowl-hitch.v1(去掉 candidate,公式即 v3 + 限帧标注),在 Phase 3 与界面一起切换版本号。

Phase 3 方案 · 2026-11-04:产品化(待 Owner 确认启动) ​

  1. 采集端(Sidecar):在现有逐帧处理链中挂一个 HitchAccumulator(每帧 O(1),约 30 MB 进程内存不变),每秒向报告输出“本秒卡顿 ms、本秒动画 ms”,会话结束写入 frame-report.json 的 hitch 段(汇总、分布、限帧标注)。不新增采集通道、进程或轮询。
  2. 指标注册:PerformanceModels.swift 新增 4 项,范围 foreground-device,版本 perfowl-hitch.v1:卡顿时间比(ms/s)、卡顿次数、最长卡顿(ms)、动画时长(s)。无动画时为空,不是 0。
  3. 界面:
    • 实时工作台:新增“卡顿(ms/s)”曲线,静止秒留空。
    • 报告画面区:卡顿时间比卡片,带 Organizer 参照分级色(注明“非官方判定”)、动画时长、覆盖率、限帧卡顿数;时间轴上标出卡顿位置。
    • Excel:逐秒页加“卡顿时长 ms、是否动画”两列。
  4. 知识与站点:按十要素新建 Canonical 指标页 site/knowledge/metrics/hitch-time-ratio.md;同步侧栏、知识总览、首页指标数与开发记录时间线(C1–C5)。
  5. 验证与交付:单测与回放基准;离线与在线数值一致性核对;从 main 打包、公证、安装 /Applications/Perfowl.app;Owner 在新包上录一段含滑动、静止和转场的会话(约 3 分钟),确认报告数值与离线重算一致,静止会话显示“无动画”(E4)。
  6. 效率门禁:在线逐帧增量计算,对比改动前后的解析吞吐、CPU、内存与报告生成耗时;任何一项明显退化都不交付。
  7. 打包限制:双架构包需要 Apple 芯片主机重新冻结 arm64 组件;若届时不可用,先交 Intel 单架构内部包,状态 partial。

预计 2–3 天开发,Owner 需要配合约 10 分钟。

Owner 决定 · 2026-11-04:启动 Phase 3 ​

Owner 选择“启动”。按上节方案执行,第一步为采集端接入与 frame-report.json 的 hitch 段。

Phase 3 实施拆分 · 2026-11-04 ​

按改动面拆成四步,每步单独提交:

步内容关键文件
3a在线帧累加器内挂 HitchAccumulator,frameSummary.hitch 输出汇总(时间比、次数、最长、动画时长、覆盖率、限帧标注、分布、算法版本);报告读取后在“帧统计”区展示 4 项,并在 Excel 统计说明中给出公式online_report.py(_FrameAccumulator)、hitch_metrics.py(版本改为 perfowl-hitch.v1)、SessionReport.swift、ReportDerivedStatistics.swift、ReportWorkbookPresentation.swift
3b指标注册:4 个 metricID 进 Registry、采集计划与范围映射;十要素知识页;站点侧栏、总览、首页联动;知识门的 Registry 数量随之更新PerformanceModels.swift、WorkbenchModels.swift、capture_plan.py、collection_scope.py、site/data/metric-contracts.json、site/knowledge/metrics/hitch-time-ratio.md 等
3c实时:每秒“本秒卡顿 ms”曲线,静止秒留空实时样本发布与工作台图表
3d打包公证、安装,Owner 录一段约 3 分钟会话做 E4打包脚本

3a 的边界:

  • 不改 PerfDog 29 项统计表(report-stat.json 的形状与数量不变),卡顿走 frameSummary.hitch 独立段。
  • 静止会话时卡顿各值为 None,报告不显示卡顿项(不是 0)。
  • 报告 4 项的窗口秒数用动画时长,说明文字写明“只统计在动的时间”。
  • hitch_metrics.py 的版本号在 3a 改为 perfowl-hitch.v1;判定逻辑不变,回放基准数字不变。
  • 效率:在线每帧多一次 O(1) 判定;对比改动前后的在线帧处理吞吐与内存,以及报告生成耗时。

Implementation Update · 2026-11-04:Phase 3a 采集端汇总与报告展示 ​

文件修改
hitch_metrics.py版本 perfowl-hitch.v1(判定不变);逐条卡顿列表改为运行中的合计(次数、总时长、最大值、分档、类型、限帧),状态有界;新增 to_state / restore_state(版本不符拒绝恢复);per_second=False 时不保留逐秒桶;稳态众数改为增量计数(_SteadyWindow,平局时回退到 Counter,结果与原来逐位一致);帧字段读取走 dict 快速路径
online_report.py_FrameAccumulator 内挂 HitchAccumulator(per_second=False),同一批帧行逐帧喂入;summary() 增加 hitch 段;检查点写入并恢复卡顿状态,缺失时按既有规则要求离线重算
SessionReport.swift读取 frameSummary.hitch,仅在 applicable 时生成 5 个报告值,窗口秒数取动画时长
ReportDerivedStatistics.swift帧统计定义增加卡顿时间比、卡顿次数、最长卡顿、其中限帧卡顿、动画时长;hitchFields、isAnimationWindow
ReportWorkbookPresentation.swiftExcel 单位 ms/s、s;卡顿项单独分组“画面表现(卡顿 · 动画 N 秒)”;5 项公式说明(含 Organizer 参照分级与“Instruments 不计”)
SessionReportView.swift单位格式;卡顿卡片下方显示“动画时长 N 秒”
SessionAggregateReport.swift多次汇总:卡顿时间比按动画时长加权,次数与动画时长合计,最长卡顿取最大,动画时长不评优劣
测试Python:存档恢复与不中断结果一致(含限帧卡顿与 commit 卡顿,状态 < 4 KB)、版本不符拒绝、增量众数与 Counter 5000 次随机一致;Swift:报告读取卡顿 5 项且窗口为动画时长、静止会话不生成卡顿项

偏差:方案写“报告展示 4 项”,实际加了“其中限帧卡顿”共 5 项,落实 Owner 的标注决定。

验证:

  • Sidecar 全量 3166 项,只有已知的 16 项环境基线失败;卡顿单测 20 项通过(E2)。
  • Swift 全量 463 项通过,17 项按条件跳过(E2)。
  • 在线与离线一致(E3,3 个真机会话的逐帧记录喂入在线累加器):App A 8 次 / 1.954 对 1.953 ms/s(差异来自在线比离线少定稿最后一帧),App B 5 / 2.105,Fixture 第 3 轮 457 / 153.939,完全一致;存档约 1.3 KB,恢复后汇总相同。
  • 效率(E3,同机交替多轮取最快值):
    • 卡顿判定本身从每帧约 14 µs 降到约 6.4 µs;
    • 挂入在线帧累加器后,帧统计每帧从约 41–47 µs 增至约 48–53 µs(+14~15%),120 帧/秒时约 0.8 ms CPU/秒;
    • 离线重算 6 个会话快 25~35%(改动前 118~545 ms,改动后 94~297 ms),内存峰值约 30 MB 不变。
  • 真实会话报告的界面渲染要等新 Collector 打包后才能看(3d,E4)。

状态:3a 已实现(E2/E3),未打包。下一步 3b:指标注册与十要素知识页。

Implementation Update · 2026-11-04:Phase 3b 指标注册与知识页 ​

  • Registry:从 66 项增加到 70 项。新增 ios.frame.hitch_time_ratio(ms/s)、ios.frame.hitch_count(count)、ios.frame.hitch_max(ms)、ios.frame.animation_time(s),范围 foreground-device,版本 perfowl-hitch.v1。“其中限帧卡顿”不单独注册,作为报告标注。
  • 采集端:
    • 平台来源合同把 4 项挂到既有帧来源家族(core-profile-frame),自检数量改为常量 EXPECTED_METRIC_CONTRACT_COUNT = 70;
    • 采集计划的后备列表、范围裁剪(未勾选时 hitch 段置空)、在线覆盖账本同步更新。在线账本只在 applicable 时记为有值,静止会话记 missing 并写原因,不当成 0;
    • 离线帧报告 analyze_display_frames 用同一实现生成 hitch 段,自校准增加 4 项。
  • 工作台:画面表现组和旧 FrameTime 组都加上 4 项,默认全采从 66 个 ID 增加到 70 个。实时卡片不变,实时曲线在 3c 做。
  • 测试:Python 与 Swift 中断言 66 项、14 个帧报告、默认计划哈希的用例改为新值。新的默认计划哈希两种语言一致:0d5d2521…。
  • 站点:
    • 新建 Canonical 指标页 卡顿时间比(Hitch),十要素齐全;
    • 合同索引新增 67~70 行并加 v5 历史;合同快照重新生成;知识门禁预期数改为 70;
    • 侧栏、知识库总览、验收状态页(正式 55/70,4 项候选)与首页流畅度卡片及说明同步更新。

偏差:无。

验证:

  • Sidecar 全量 3166 项,只有已知的 16 项环境基线失败(E2)。
  • Swift 全量 463 项通过,17 项按条件跳过(E2)。
  • 站点 npm run build 0 错误;知识门禁 PASS,70 项 Registry 与快照、Canonical 归属、十要素、侧栏、总览一致(E2)。
  • 效率:离线帧报告每帧多一次约 6 µs 的判定,在线部分沿用 3a 的测量,没有新的常驻开销。

状态:3b 已实现(E2),卡顿 4 项为候选,不计入正式验收。下一步 3c 实时卡顿曲线。

Lead 调整 · 2026-11-04:先做 3d,再做 3c ​

实时卡顿曲线(3c)要改实时投影器(逐帧解码路径)、1 Hz 产品样本结构、metrics.csv 列、Swift 样本模型与工作台图表,并且要重做实时调度公平性的基准,改动面最大。3a/3b 已经能在报告里给出卡顿结果,先打包请 Owner 做真机验收(3d),确认在线结果与离线重算一致、静止会话显示“无动画”。3c 之后单独立方案。

3d 执行:

  • 从 main 构建内部包 255,单架构 x86_64(Collector 有改动,arm64 组件需 Apple 芯片主机重新冻结,本批不做,状态 partial);嵌套二进制带时间戳,App 与 DMG 都提交公证并 stapling。
  • 核对:严格签名、spctl、stapler validate、包内冻结核心与源码一致、CLI 一致、stream --prewarm 冒烟;保留回滚包 254;同址覆盖 /Applications/Perfowl.app;启动器唯一。
  • MacApp/dist/ 只留 255;1.0.0 的 DMG 移到私有归档 build-254 目录保存。
  • Owner 验收:在 255 上测一段约 3 分钟会话(含滑动、停顿和转场),停止后打开报告,看“画面统计”里的卡顿项;Claude 用离线重算核对(E4)。

Validation Update · 2026-11-04:内部包 255 打包、公证与安装(3d) ​

  • 来源:main 9e5d3da8(含 3a、3b),源码门禁 passed,x86_64 单架构,构建 360 秒。
  • 公证:App 6213308f Accepted 并 stapling;DMG aac99a90 Accepted 并 stapling。
  • 产物:MacApp/dist/Perfowl-1.0.0-255-macos-x86_64-internal.dmg,146,712,823 字节,SHA-256 2cb5c05a4b401eba4039335556092dc787d1194eb57d9801a69cd6074ebd63b4。dist/ 只有这一个包;1.0.0(254)的 DMG 已移入私有归档,哈希不变。
  • 核对(E3,本机 Intel):
    • codesign --verify --deep --strict 通过;spctl 结果 source=Notarized Developer ID;stapler validate 通过;
    • DMG 内的 App 与构建树一致(12,913 个文件),安装后同样一致;
    • 冻结核心 109 个模块与源码一致,CLI 与源码一致;probe ready,stream --prewarm 首条为 hello;
    • 卡顿冒烟:已安装核心对 Fixture 第 3 轮会话 hitch-recompute,得到 perfowl-hitch.v1、457 次、153.939 ms/s、限帧 2 次,与源码一致;
    • 回滚包 254(224 MB)已解包核对;启动器唯一,lsregister 与 mdfind 都只剩 /Applications/Perfowl.app。
  • 过程问题:
    1. 第一次运行用了系统 Python 3.9,缺 hashlib.file_digest,开头就停了,未做任何改动;
    2. 第二次在公证通过后、给已安装的 254 做回滚核对时停下:254 是双架构,用了单架构核对脚本。/Applications 未动。改为旧包用双架构核对脚本,从挂载 DMG 那一步继续,没有重复公证。
  • 状态:partial。arm64 未构建,只能在 Intel Mac 上用。待 Owner 真机验收(E4)。

Validation Update · 2026-11-04:内部包 255 真机会话(3d,E4) ​

  • 会话:设备 A、目标 App A,内部包 255,约 103 秒,含滑动、停顿与页面切换(Owner 操作)。原始数据在本机会话目录,不入库。
  • 对照:在线报告 report-final.json 的 frameSummary.hitch 与同一会话 hitch-recompute(离线逐帧重算):
项在线报告离线重算
卡顿次数2222
卡顿总时长385.419 ms385.419 ms
最长卡顿166.81 ms166.81 ms
限帧卡顿00
动画时长67.112 s67.145 s
卡顿时间比5.743 ms/s5.740 ms/s
已判定帧7,1417,142
时长分档(1 / 2–3 / ≥4 个刷新间隔)19 / 2 / 119 / 2 / 1
  • 停顿期没有计入:离线逐秒结果里,静止的秒没有动画时间,也没有卡顿;整场 103 秒里动画时间 67 秒。
  • 差别只有一帧:会话最后一帧(帧间隔 33.36 ms)。卡顿判定要看下一帧(容忍刷新率切换),所以最后一帧一直挂起等待;离线重算在文件读完时会补判,在线汇总没有补判。用“所有帧去掉最后一帧”离线重算,得到的 7,141 帧、67.112 s、5.743 ms/s 与在线完全一致,原因确认(E3)。帧统计其余部分(7,143 帧、7,142 个间隔)不受影响。
  • 本轮没有录“整场静止”的会话,静止会话不显示卡顿项目前只有单测(E2)。

Lead 判定: 卡顿次数、总时长、最长卡顿在线与离线逐项相同,真机链路可用(E4,设备 A、iOS 26、120 Hz)。动画时长与卡顿时间比差一帧,不满足“完全一致”,按小修复处理(下节)。修复后的新包上补一次对照,再加一段整场静止的会话,之后 4 项再计入正式验收。在此之前保持候选,正式验收仍为 55/70。

修复方案 · 2026-11-04:在线汇总补判最后一帧 ​

  • 问题:_FrameAccumulator.summary() 直接取 HitchAccumulator.summary(),挂起的最后一帧不在结果里。
  • 方案:HitchAccumulator 新增 settled_summary():没有挂起帧时等于 summary();有挂起帧时用检查点状态复制一份(约 1.3 KB),在副本上补判后汇总,原累加器不变,之后的新帧照常判定。在线报告改用它。离线路径本来就补判,结果不变。
  • 口径不变(仍是 perfowl-hitch.v1),只修在线实现与公式不一致的地方。
  • 开销:每次发布报告多做一次状态复制,O(刷新间隔种类 × 16),微秒级;不增加检查点体积。
  • 测试:在线累加器与离线 finish() 后的结果逐项相同;补判后再追加帧,结果与一次性跑完一致。
  • 交付:随 3c 的下一个内部包一起打包;Owner 届时在新包上测一段含滑动与停顿的会话,加一段约 30 秒整场静止的会话。

Implementation Update · 2026-11-04:在线汇总补判最后一帧 ​

  • 修改:hitch_metrics.py 新增 HitchAccumulator.settled_summary();online_report.py 的 _FrameAccumulator.summary() 改用它。与方案一致,无偏差。
  • 测试:test_hitch_metrics.py 新增 1 项(补判前后与离线逐项相同,补判不消耗挂起帧,重复调用结果不变),21 项通过;Sidecar 全量 3,167 项,只有原有的 16 项环境失败(native_direct_archive 11、native_raw_graph 4、sw1 1),无新增(E2)。
  • 真实数据回放(E3):把 255 会话的逐帧记录喂给在线累加器,frameSummary.hitch 与 hitch-recompute 全部字段相同(67.145 s、5.740 ms/s、7,142 帧)。
  • 开销(x86_64):每次汇总 83 µs(原 5.8 µs)。报告每秒发布一次,约占 0.008% 个核;检查点体积不变。
  • 状态:implemented(E2/E3),未打包。随 3c 的下一个内部包交付,届时补真机对照与整场静止会话(E4)。

Phase 3c 方案 · 2026-11-04:实时卡顿曲线 ​

目标:测试时工作台显示一条“卡顿(ms/s)”曲线,画面静止的时段不画点,并且该曲线写入 metrics.csv 与 Excel 明细。会话报告的整场卡顿值仍以 frameSummary.hitch 为准,本批不改。

口径(公式入档)

  • 实时累加器:realtime.py 在 FrameLifetime 分支把同一份 raw_payload 交给一个独立的 HitchAccumulator(per_second=False)。判定规则和报告完全一样(perfowl-hitch.v1),不另写一套。
  • 每次发布样本(约 1 Hz)取“上次发布以来新判定的帧”:
text
hitchMs           = Σ H(i)                    (本窗口内判定帧的卡顿时长)
hitchAnimationMs  = Σ 动画帧的 P(i) − P(i−1)  (本窗口内的动画时长)
hitchTimeRatioMsPerS = hitchMs ÷ (hitchAnimationMs ÷ 1000)   当 hitchAnimationMs > 0
  • 用比值而不是“本秒卡顿 ms”:ActivityTrace 会把几秒的帧攒在一起送到主机(R8 实测约 6.7 秒),按到达窗口直接加总,会在补到的那一秒出现假尖峰;比值按动画时间归一,攒批不影响数值。任意区间的卡顿时间比 = Σ hitchMs ÷ Σ hitchAnimationMs,与整场汇总口径相同,可以合并。
  • 质量与空值(不写 0):
    • 窗口内有判定帧、动画时长为 0:not-applicable-no-animation,空;
    • 窗口内没有新判定的帧:missing-no-new-frame,空;
    • 非前台:not-foreground;来源过期:stale-device-source;帧来源不是 FrameLifetime:不发布字段;
    • 有动画:candidate。
  • 延迟:每帧要等下一帧才判定,曲线比画面晚至少一帧;再叠加传输攒批,值落在“到达”的那次样本上。
  • 报告的区间统计:报告时间轴使用这条曲线;选中区间时,卡顿时间比按 Σ hitchMs ÷ Σ hitchAnimationMs 计算,不用逐点平均。整场值仍取 frameSummary.hitch。

改动范围

层文件改动
采集realtime.pyFrameLifetime 分支喂实时累加器;样本组装时取窗口差值,发布 hitchTimeRatioMsPerS、hitchMs、hitchAnimationMs 及质量、来源、算法、scope、unit;来源过期与非前台置空
采集hitch_metrics.py无规则改动;取窗口差值只读现有累计值
范围裁剪collection_scope.py三个字段归入 ios.frame.hitch_time_ratio,未选卡顿时置空
Swift 模型PerformanceModels.swift样本与指标结构各加 3 个可选字段(旧会话缺字段时为空)
CSV / ExcelPerformanceSessionStore.swift、SessionExporter.swift在已有列之后追加列,不改动已有列的位置
工作台WorkbenchModels.swift、PerfowlWorkbenchView.swift、SessionReviewViews.swift“画面表现”组新增“卡顿”曲线,单位 ms/s
报告SessionReport.swift、ReportDerivedStatistics.swift时间轴绑定 ios.frame.hitch_time_ratio;区间值按动画时长加权

协议仍是 perfowl.collector/1;schemaVersion=2,只追加可选字段,旧客户端会忽略这些字段。Registry 仍是 70 项,不新增 metricID。

效率门禁

  • 关键路径:实时线程每帧多一次 add()。此前单测约 6.4 µs/帧,120 fps 时约 0.8 ms CPU/s;样本组装时每次多做 O(1) 的差值。
  • 前后对比(x86_64):
    • 5 万帧合成 FrameUpdate 经 RealtimeMetricProjector 的耗时;
    • r8fix7/bounded-burst 实时调度基准,看 p95、p99、max 与丢弃数;
    • 内存不随会话时长增长(累加器有界)。
  • 门槛:每帧新增 ≤ 10 µs;bounded-burst 的 p99 退化 ≤ 10%,丢弃仍为 0。

测试

  • Python:
    • 合成流:稳态、注入卡顿、静止,覆盖逐窗口的值与质量;
    • 攒批到达:3 秒的帧一次到达,比值不出现尖峰;
    • 窗口之和等于整场汇总;
    • 非前台与来源过期时置空;
    • 范围裁剪。
  • Swift:
    • 样本解码,旧样本缺字段时为空;
    • CSV 追加列;
    • 工作台曲线定义;
    • 报告区间值按动画时长加权,静止区间为空。
  • 回放:255 真机会话逐帧喂实时投影器,各窗口 hitchMs 之和等于离线卡顿总时长 385.419 ms(除最后一帧外)。

交付:从 main 打内部包、公证、安装。Owner 测两段会话:一段有滑动和停顿(看实时曲线,静止时没有点),一段约 30 秒完全不动。同时复核上节最后一帧的修复(E4)。

预计 1 天开发与自测。

Implementation Update · 2026-11-04:实时卡顿曲线(3c) ​

实际修改

层文件实现
采集hitch_metrics.py新增 LiveHitchWindow:内含一个 HitchAccumulator,take() 返回上次以来的窗口值与质量;新增 HITCH_LIVE_FIELDS、HITCH_LIVE_ALGORITHM(perfowl-hitch.v1.window-since-last-sample)、HITCH_LIVE_UNITS。HitchAccumulator 新增 details=False,只累计、不生成逐帧明细;在线报告、离线帧报告、补判汇总和实时窗口都用它
采集realtime.pyFrameLifetime 分支在 DisplayPerformanceProjector 之后喂实时窗口;样本组装时发布三个字段,并带上质量、来源、算法、scope 和 unit;非前台置空;静止水位确认且无新帧时记 not-applicable-no-animation;来源过期时置空
范围裁剪collection_scope.py三个字段挂在 4 个卡顿 metricID 上,4 项都没选时不发布
SwiftPerformanceModels.swiftCollectorMetrics、PerformanceSample 各加 3 个可选字段
SwiftPerformanceSessionStore.swift、SessionExporter.swiftmetrics.csv 末尾追加 hitchTimeRatioMsPerS,hitchMs,hitchAnimationMs,hitchQuality;会话导出 CSV 末尾追加 3 列(82→85)
SwiftWorkbenchModels.swift、PerfowlWorkbenchView.swift、DisplayMetricPresentation.swift“画面表现”卡片加第 3 条曲线“卡顿”(ms/s);空值读数:静止时显示“— · 画面静止”,其余沿用帧类说明
SwiftSessionReport.swift、ReportDerivedStatistics.swift时间轴绑定 ios.frame.hitch_time_ratio;报告保存逐窗口的卡顿与动画时长(hitchWindows);选中区间时用 rangeHitch() 按 Σ卡顿 ÷ Σ动画时长 计算;整场值仍取 frameSummary.hitch

相对方案的偏差

  1. 没有改 SessionReviewViews.swift:它不按曲线类型分支,不需要改。
  2. 新增 details=False。原方案没有这一项,是为了降低每帧开销(见下)。
  3. 静止的判定复用了已有的 FPS 静止水位(_static_idle_confirmed):确认静止时,“无新帧”记为“不适用”。

测试

  • Python:
    • test_hitch_metrics.py 新增 6 项:窗口之和等于整场、攒批到达不出尖峰、静止窗口为“不适用”、投影器发布字段与元数据、后台与未选时不发布、没有帧来源时不出字段;
    • 共 27 项通过;Sidecar 全量 3,173 项,只有原有的 16 项环境失败(E2)。
  • Swift:
    • 新增 testLiveHitchSeriesAndRangeRatioWeightedByAnimationTime,覆盖 CSV 追加列、曲线与“不适用”点、区间按动画时长加权(23.33 ms/s,不是逐点平均的 62.5)、静止区间为空、旧样本缺字段时为空;
    • 卡片契约测试由 2 条曲线改为 3 条;导出列数 82→85;
    • 全量 464 项,0 失败,17 跳过(E2)。
  • 真实数据回放(E3):把 255 会话的 7,143 帧按主机到达时刻送入实时投影器,每秒取一次样本,共 98 个样本:
    • 质量:candidate 93、无新帧 3、不适用 2;
    • 各窗口卡顿之和 385.423 ms,动画时长之和 67.112 s,合计比值 5.743 ms/s,与在线报告相同;挂起的最后一帧不在任何窗口里,与 Phase 3d 的结论一致;
    • 单个窗口最高 250.1 ms/s(长卡顿所在的那一秒)。

效率(x86_64,255 会话 7,143 帧 × 5 轮,交替运行)

项修改前修改后
实时投影器每帧 consume12.9–14.9 µs24.7–27.5 µs
卡顿判定单独计时(同一进程)—4.6–5.2 µs/帧
每次样本组装335–348 µs347–407 µs
r8fix7/bounded-burst 7,200 帧(两档)p991.16–1.68 ms1.09–1.94 ms,丢弃 0
  • 结论:判定本身每帧约 5 µs,满足“≤ 10 µs”的门槛。但和投影器其他组件交替运行时,每帧总耗时多了 10–13 µs,略超门槛。
    • 原因:主要是交替运行时的缓存干扰。同一进程里单独循环只要 5 µs,profile 中也没有其他热点。
    • 按 120 fps 持续动画计,约 1.2–1.6 ms CPU/s,即 0.12–0.16% 个核。
    • 调度公平性基准前后都在噪声范围内,丢弃为 0。
  • 已做的优化:
    • 不生成逐帧明细字典(details=False);
    • _Frame 改用位置参数;
    • 刷新间隔取最大值时不再建临时列表;
    • 不统计逐秒时跳过秒桶计算。
    • 单独计时从 6.1 µs 降到 5.0 µs。
  • 没有做的方案:让实时曲线直接读在线报告的累加器,可以省掉重复判定。但在线报告在写入线程上,按来源分段,还可能被禁用。两边耦合会影响 R8 的线程隔离,所以不采用。
  • Lead 判定:边际成本略超门槛,绝对值很小,调度无退化,接受;在交付汇报中如实说明。
  • 状态:implemented(E2/E3)。下一步从 main 打内部包 256,Owner 真机验收(E4)。

Validation Update · 2026-11-04:内部包 256 打包、公证与安装 ​

  • 来源:main 2318cf3b(含 3c 与最后一帧补判),源码门禁 passed,x86_64 单架构,构建 433 秒。
  • 公证:App b3bd3521、DMG 01c6ddcd 均 Accepted 并 stapling。
  • 产物:MacApp/dist/Perfowl-1.0.0-256-macos-x86_64-internal.dmg,146,019,389 字节,SHA-256 753527629105da73621bd5338402c0005b364e65ed6c87767bb361629292f2b6。dist/ 只留这一个包;255 的 DMG 已移入私有归档。
  • 核对(E3,本机 Intel):
    • codesign --verify --deep --strict 通过;spctl 结果 source=Notarized Developer ID;stapler validate 通过;
    • 安装后的 App 与 DMG 内一致;冻结核心 109 个模块与源码一致,CLI 与源码一致;probe ready,stream --prewarm 首条 hello;
    • 已安装核心的 hitch-recompute:Fixture 第 3 轮 457 次、153.939 ms/s、限帧 2 次;255 真机会话 22 次、385.419 ms、67.145 s,都与源码一致;
    • 255 的回滚包已解包核对;lsregister 与 mdfind 都只剩 /Applications/Perfowl.app。
  • 过程问题:第一次运行在公证完成、覆盖安装之前停下,因为检查到 Perfowl 正在运行(当时没有会话在录制),/Applications 没有改动。Owner 退出应用后,从挂载 DMG 那一步继续,没有重复公证。
  • 状态:partial。arm64 未构建;待 Owner 真机验收(E4):
    1. 一段含滑动与停顿的会话:看实时曲线、报告与离线重算;
    2. 一段约 30 秒完全不动的会话:报告不显示卡顿项。

Validation Update · 2026-11-04:内部包 256 真机会话(E4) ​

Owner 在 256 上录了 5 段会话(设备 A;目标 App A 4 段,目标 App B 1 段)。原始数据在本机会话目录,不入库。

会话时长结果
第 1 段(App A)19 s收尾 partial,报告未封存(见问题 2)
第 2 段(App A)20 s同上
第 3 段(App B)19 s同上
第 4 段(App A)67 s在线报告与离线重算 8 项逐项相同(见下)
第 5 段(App A)76 s帧来源“资源超限”,报告帧类全空;实时曲线仍有值(见问题 1)

第 4 段对照(在线 frameSummary.hitch 与 hitch-recompute)

项在线离线
卡顿次数1313
卡顿总时长197.255 ms197.255 ms
最长卡顿50.05 ms50.05 ms
限帧卡顿11
动画时长33.377 s33.377 s
卡顿时间比5.910 ms/s5.910 ms/s
已判定帧3,6783,678
  • 最后一帧补判在真机上确认:动画时长与已判定帧数完全一致(255 时差一帧)。
  • 实时曲线:62 个样本,candidate 56、画面静止(不适用)5、首个样本无字段 1。各窗口卡顿之和 197.257 ms,与报告一致(差值来自逐窗口取 3 位小数)。动画时长之和 33.118 s,比报告少 0.26 s,是最后一次发布样本之后到停止之间的帧,没有落在任何样本里,符合定义。单个窗口最高 148.2 ms/s。
  • 4 个卡顿 metricID 的在线覆盖账本都是 candidate。

问题 1(本批引入,已修复):帧来源未选用时,实时曲线仍发布数值

  • 第 5 段开头约 3.8 秒收到 27.6 MB ActivityTrace 数据,超过帧来源管理器每秒 4 MB 的上限,判定 resource-limited,整场不再选用逐帧来源。报告因此没有判定帧(no-timing),帧类指标都为空,这是对的。
  • 实时窗口却在帧来源管理器之外直接消费原始帧,照样发布了 35 个有值样本,与报告不一致。
  • 修复:发布前与帧耗时用同一个门控。帧来源管理器没有选用 ActivityTrace 时三个字段置空,质量取管理器的原因(如 resource-limited);选了其他来源时为 frame-source-not-selected。新增测试 1 项,共 28 项通过(E2)。
  • 同一 App 在第 4 段没有触发超限。为什么第 5 段开头数据量这么大,不属于卡顿口径,记为帧来源的遗留问题。

问题 2(另立方案):约 20 秒的短会话收尾 partial,报告未封存

  • 3 段短会话停止时,stream-pmd3.core-profile.raw 与 stream-device-conditions 两个消费任务没有在 500 ms 内退出(CoreProfile 任务当时已在 waiting-source 停了约 14 秒)。按 R8-Fix-6 的判定规则,这算 source-consumer-drain-timeout,收尾 partial,在线报告没有封存(live-report-seal-missing)。原始数据与写入都完整。
  • 这台机器上 256 之前的 9 段会话收尾都正常,但都长于 1 分钟;256 上两段长会话也正常。本批没有改动收尾或 CoreProfile 代码,但 255 上没有做过 20 秒的短会话,所以不能断定是不是本批引入的(未证)。
  • 不在卡顿批次里修。下一步单独立方案:复现条件、停止时 CoreProfile 等待源的取消路径、以及“数据完整但消费任务超时”是否仍应阻止报告封存。

整场静止会话:这 5 段都有动画,“整场静止时不显示卡顿项”仍只有单测(E2)。

Lead 判定

  • 卡顿报告链路真机一致性通过(E4,设备 A、iOS 26、120 Hz):在线与离线逐项相同,最后一帧修复确认。
  • 实时曲线有条件通过:正常情况下与报告一致;帧来源未选用时的门控已修复,待新包复核。
  • 4 项暂不计入正式验收,仍为 55/70。还差两件事:
    1. 新包上测一段约 1 分钟、完全不动的会话(要长于 1 分钟,避开问题 2);
    2. 门控修复随新包复核。

Validation Update · 2026-11-04:内部包 257 打包、公证与安装 ​

  • 来源:main ffd5f613(实时曲线跟随帧来源选择),源码门禁 passed,x86_64 单架构,构建 260 秒。
  • 回归:Sidecar 全量 3,174 项。除原有的 16 项环境失败外,多了 1 项 test_build74_closeout_source_terminal.test_b_network_backlog_stop_drains_completed_with_no_timeout:它在 500 ms 内排空网络积压,当时机器负载约 7;单独连跑 5 次都通过,判为负载下的时序偶发,和本批无关(E2)。
  • 公证:App d59966e4、DMG 3c3456a7 均 Accepted 并 stapling。
  • 产物:MacApp/dist/Perfowl-1.0.0-257-macos-x86_64-internal.dmg,148,250,047 字节,SHA-256 460128809e7fa13a35ee77b6950be69b2d554851e4b5479b6f7cab4736f64285。256 的 DMG 已移入私有归档。
  • 核对(E3):
    • 签名 strict、spctl(Notarized Developer ID)与 stapler validate 都通过;
    • 安装后的 App 与 DMG 内一致;冻结核心 109 个模块与源码一致,CLI 与源码一致;probe ready,stream 首条 hello;
    • hitch-recompute 两个样本与源码一致;
    • 256 的回滚包已核对;启动器只剩 /Applications/Perfowl.app。
  • 过程问题:
    1. 第一次运行时,版本号批量替换把 sha256 误改成了 sha257,脚本一开头就停了,没有任何改动。
    2. 第二次在公证完成后、覆盖安装前停下:Owner 退出后 Perfowl 又被打开了(15:48)。留在 /Applications 的 257 暂存副本核对版本号后删除,并注销了它的启动登记。Owner 再次退出后,从挂载 DMG 那一步继续,没有重复公证。
  • 状态:partial(arm64 未构建)。待 Owner 测一段超过 1 分钟、完全不动的会话,再测一段普通会话(E4)。

Validation Update · 2026-11-04:内部包 257 真机验收(E4) ​

Owner 在 257 上录了 2 段会话(设备 A、目标 App A,iOS 26,120 Hz)。原始数据在本机会话目录,不入库。两段收尾都正常,报告已封存,帧来源全程选用 ActivityTrace。

项第 1 段(静止,129 s)第 2 段(操作,88 s)
卡顿次数(在线 / 离线)0 / 0371 / 371
卡顿总时长0 / 0 ms12,415.913 / 12,415.913 ms
最长卡顿0 / 0 ms200.165 / 200.165 ms
限帧卡顿0 / 01 / 1
动画时长9.391 / 9.391 s59.364 / 59.364 s
卡顿时间比0 / 0 ms/s209.148 / 209.148 ms/s
已判定帧602 / 6025,068 / 5,068
静止秒占比76.7%0%
  • 两段在线报告与离线重算 8 项逐项相同。256 上的第 4 段同样逐项相同,三段累计零差异。
  • 实时曲线:
    • 第 2 段:85 个样本,有值 69、画面静止 15、首样本无字段 1;各窗口卡顿之和 12,415.912 ms,与报告一致;单窗口最高 809.2 ms/s。
    • 第 1 段:125 个样本,有值 20、画面静止 28、来源过期 76(长时间没有新帧时,显示源时钟判为过期,读数是“暂无近期帧”),首样本无字段 1。所有有值窗口的卡顿都是 0;静止时段没有画出任何 0。
  • 第 1 段不是“整场零动画”:目标 App A 约每 5 秒有一次约 0.18 秒的界面动画,中间还有一次约 5 秒的连续变化,合计动画 9.4 秒。这个 App 上做不出零动画的会话。“整场零动画时报告不显示卡顿项”这条路径,仍只由单测覆盖(E2),它与静止时段不画点用的是同一条 animation_ns == 0 规则。
  • 帧来源门控(resource-limited 时置空):这两段都没有触发超限,真机没有复现条件,仍为 E2。

Lead 判定:PASS

  • ios.frame.hitch_time_ratio、ios.frame.hitch_count、ios.frame.hitch_max、ios.frame.animation_time 4 项通过正式验收,计入正式 59/70(原 55 项 + 卡顿 4 项;暂停 11 项不变)。
  • 适用范围:设备 A、iOS 26、120 Hz、FrameUpdate 帧来源;perfowl-hitch.v1;scope 为 foreground-device。
  • 证据:
    • Fixture 注入卡顿与 Instruments 逐次一致(E4/E3);
    • 真机三段会话在线与离线逐项一致(E4);
    • 实时曲线与报告一致(E4);
    • 整场零动画和帧来源超限两条路径为 E2。
  • 不覆盖:iOS 15–25 映射、60 Hz 机型、App 自己限帧低于屏幕刷新率、与 Instruments 同一操作逐次对齐,以及约 20 秒短会话的报告封存(另立方案)。
  • 状态:validated。剩余门禁见 handoff:短会话收尾、帧来源超限原因、arm64 包。

Perfowl · Performance Observer