全量采集与流式报告改造——可执行开发任务书
文档类型:devlog / plan。本文已完成代码核查与设计,产品实施待明确批准。
partial表示本改造尚未实施验收,不表示方案已批准。本文中的接口、文件名、协议扩展和性能数字,除明确标为“当前实现”外,均为拟开发合同。面向执行者:按任务卡顺序开发;先写测试,后修改实现。验收条件未达到时保留未完成状态。禁止通过修改提示、隐藏处理进度、删减指标、补零、降低精度或丢弃尾部数据来换取“完成”。
0. 交接入口、基线与目标
仓库根目录:/Users/itreenewbee/工作/代码/perfowl。
核查基线:fb5d3bd0f896d842bb527cd1d0bb8bc1ff4bc6c0。执行者启动时重新检查 HEAD 和差异,禁止覆盖他人的未提交文件。本文代码路径均相对上述根目录;文末包含逐项指标源账本。
0.1 用户需求转成六条硬合同
- 选择设备后建立设备服务;选择应用后一次准备所有适用采集源。
- 全量采集覆盖当前 Registry 的全部64个定义;将来增加定义必须同步覆盖。图表隐藏与否不影响采集、保存、报告和导出。
- 「+」只控制显示;增删图表不改变采集计划、不发送准备命令、不改变启动方式、不重开设备连接。
- 截图属于附件,不计入64个指标;默认按既有1秒节拍保存,与是否显示截图分离。
- 报告从采集开始持续生成;停止只处理有限尾部、同步写盘、发布最终清单,不重新遍历整场历史。
- USB条件下只有明确不适用的能耗项目允许跳过;所有其他不支持或失败项目仍在报告账本中保留状态与原因,禁止悄悄消失。
0.2 成功体验
选择设备 → 设备服务就绪
选择应用 → 全量采集服务准备完成(展示覆盖情况)
点击开始 → 全部适用指标记录 + 报告持续更新
点击+ → 只增减可见图表;新增图表可读取开始以来已有数据
点击停止 → 立即反馈 → 有界尾部完成 → 报告已保存
“报告已保存”必须同时满足产品会话与采集子会话的保存合同;后台仍在制作必需报告时,不展示该状态。
0.3 本批不扩大的范围
- 不新增没有可信来源的指标算法;64项覆盖不等于64项数值均有真机证明。
- 不更换通信内核、不打开第二个竞争连接、不修改目标应用。
- 不扩大启动条件回调的映射,也不把三个未观测回调补成0。
- 不修饰既有启动→帧源切换的采集间隙。全部指标均纳入计划,但每项仍声明实际有效窗口。
- 全量明细导出、压缩归档、远端上传和独立完整审计属于显式操作,不是停止完成前置条件。停止时必需的本地报告、明细与清单必须完整。
1. 已核实的现状与修改依据(E1)
| 代码位置 | 当前行为 | 本批处理 |
|---|---|---|
MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift 的 collectorPreparationKey |
把显示选择 selectedMetricIDs 拼入准备任务标识 |
移除显示选择;改为设备、应用、采集策略版本与准备代次 |
MacApp/Sources/PerfowlCore/PerformanceSessionController.swift 的 prepareCollector |
显示计划变化可走 invalidateCollectorReadiness(),终止预热进程 |
显示更新不进入准备API;按设备/应用分层失效 |
MacApp/Sources/PerfowlCore/CollectorReadiness.swift 的 metricPlan |
对显示选择排序生成准备匹配值 | 新增全量采集计划哈希;旧字段仅供旧会话解码 |
MacApp/Sources/PerfowlCore/PerformanceSessionController.swift 的 trySendPrewarmApp |
启动图表选择决定冷启动与普通启动 | 独立测试方式,默认包含启动采集 |
MacApp/Sidecar/perfowl_collector_core_cli.py |
普通源已基本全量;startupTiming 仍控制启动trace;准备应用只允许一次 |
新协议显式全量计划、独立启动策略、可取消的应用重绑定 |
MacApp/Sidecar/perfowl_collector_core/capture_policy.py |
普通源与显示选择已有部分解耦 | 复用去重逻辑,补齐能力账本与条件门禁 |
MacApp/Sidecar/perfowl_collector_core/session_writer.py |
Raw与普通Metric共用顺序writer | 升级为所有必需报告的增量处理入口 |
MacApp/Sidecar/perfowl_collector_core/metric_engine.py 的 IncrementalMetricRecorder |
已边采集边写普通指标与哈希 | 保留并复用,不再创建第二条重复派生链 |
MacApp/Sidecar/perfowl_collector_core/delivery.py |
停止后9阶段串行finalize | 新增在线封存入口;旧finalizer保留作审计/恢复 |
MacApp/Sidecar/perfowl_collector_core/recording.py 的 validate_recorded_session |
回读Raw及blob,并将blob恢复到事件集合 | 正常在线封存不调用;独立审计仍保持校验 |
MacApp/Sidecar/perfowl_collector_core/metric_snapshot.py |
停止时回读并校验完整metric文件 | 在线封存使用writer持有的状态;旧文件独立读取继续校验 |
MacApp/Sidecar/perfowl_collector_core/frame_derivation.py |
离线构造完整序列并做median/sort | 增量帧明细与精确索引,保留离线实现作对照 |
MacApp/Sidecar/perfowl_collector_core/checkpoint_policy.py |
周期checkpoint可选,默认0关闭 | 新会话显式启用有界周期检查点,保留旧会话兼容 |
MacApp/Sources/PerfowlCore/PerformanceSessionStore.swift |
产品层另有事件/CSV/manifest写入 | 加入统一最终序号与封存确认,覆盖双层保存 |
重要结论:已有“部分增量”基础,不是从零重写。真正缺口是显示与准备仍耦合、特殊指标未全量、报告聚合未在线化、结束时仍全历史复算。
2. 全量采集、显示与测试方式的数据合同
2.1 新增四个独立类型
新建 MacApp/Sources/PerfowlCore/CapturePlan.swift;Python侧扩展 capture_policy.py,保持同一规范化JSON和测试向量。
struct CapturePlan: Codable, Equatable, Sendable {
let schemaVersion: Int // 1
let policyVersion: String // all-supported.v1
let requestedMetricIDs: [String] // Registry全部ID,去重、字典序
let registryContractHash: String
let launchMode: TargetLaunchMode // 独立配置
let startupEnabled: Bool // 默认true
let screenshotsEnabled: Bool // 默认true
let energyPolicy: String // capability-and-power-gated.v1
}
struct DisplayPreferences: Codable, Equatable, Sendable {
var visibleMetricGroups: Set<WorkbenchMetricID>
}
struct PreparationToken: Codable, Equatable, Sendable {
let deviceGeneration: UInt64
let appGeneration: UInt64
let capturePlanHash: String
let capabilityRevision: UInt64
}
ResolvedCapturePlan 由采集端根据真实源能力生成:包含请求计划哈希、64项逐项状态、去重源列表、sourceEpoch、适用窗口及失败原因。客户端展示它,不在客户端猜测哪些源支持。
2.2 哈希规则
- 字段集合固定;UTF-8;对象键排序;无空白分隔;布尔使用JSON布尔;枚举ASCII;ID数组排序去重;schema=1。
- 计划字段不含设备名、目标身份、显示选择、准备时间和临时端口。
- 设备身份与应用版本单独参与准备token匹配;路由/tunnel连接代次参与deviceKey。
- 计划哈希只含策略常量与Registry合同,不含实时能力探测结果;能力变化由独立revision表达。
- Swift和Python必须使用相同fixture得到相同SHA-256。禁止各自对description/hashValue求值。
- 首批只纳入计划所需字段,避免跨语言浮点序列化差异。
2.3 原有字段迁移
selectedMetricIDs不再作为新增会话采集源启用条件。- 旧会话中该字段可能混合Registry与显示组,读取时原样保留用于兼容;禁止重写旧文件。
- 新会话显式保存
capturePlan、resolvedCapturePlan、displayPreferencesAtStart。 - 旧
metricPlanHash仅在旧schema解码,新的准备token只用capturePlanHash。 - 默认全量已包含启动;冷启动作为新测试的默认方式,开始按钮旁明确显示“冷启动测试”。选择应用和图表操作均不执行启动或终止目标应用。
- 如果用户明确选择普通启动/附加已有进程,则保留全部指标请求,但启动窗口可能不成立;报告明确标注,不自动声称冷启动数据。
- 截图保存默认开启;有需要时由独立“采集设置”控制,当前批次不必新增复杂设置页面。图表选择没有此权限。
3. 能力与缺失账本:别把64项等同于64路连接
3.1 来源事实源
- 全部指标:
PerformanceModels.swift的Registry。 - 指标到源族:
ios-platform-profiles.candidate.v1.json的metricSourceContracts。 - 各值公式:
metric_engine.py、startup_timing.py、launch_trace.py、frame_derivation.py和对应Canonical知识页。 - 截图单独一个附件源;一个源可服务多个指标。
- 从上述合同生成完整覆盖测试;新开发者不得手写另一份漏项“全量列表”。
3.2 每项报告结构(新增覆盖账本,不替换既有quality)
{
"metricID": "METRIC_ID",
"requested": true,
"sourceIDs": ["SOURCE_ID"],
"preparationState": "prepared",
"collectionState": "recording",
"valueQuality": "missing",
"reason": "no-sample-yet",
"sampleCount": 0,
"firstSourceSequence": null,
"lastSourceSequence": null,
"coverage": [],
"sourceEpoch": 1
}
preparationState:pending / preparing / prepared / not-applicable / unsupported / failed。collectionState:not-started / recording / stopped / failed。valueQuality复用原算法quality;准备完成不代表candidate值已经存在。- 计数0合法;缺失测量值必须为null。phase未出现与合法0时长分开。
coverage以每个连续源窗口记录;同源重启、断连、启动→frame切换产生新窗口,严禁跨gap求相邻帧差。reportStatus分离保存完整性与测量可用性:报告可已封存,但部分指标仍missing。
3.3 USB能耗策略
- 电流、电压、功率依据实际外供电状态与现有物理门禁判断;USB连接不是替代原始电源字段的理由。
- CPU/GPU/网络等energy cost是系统相对成本,不等于物理电池功率;只要服务可用,仍采集。
- 某源在当前连接条件明确不适用:记录
not-applicable和现有reason,停止该源的重复尝试。 - 温度、温控、energy、battery分项判断;禁止一条“USB”分支关闭整组。
- 不确定支持性时进行一次有界探测,失败记录原因;持续采集中不每秒重试失败源。
- 连接条件发生变化必须产生capability revision;禁止把新的支持结果倒填到过去窗口。
4. 两级准备生命周期与连接复用
4.1 状态机
Device: absent → connecting → ready → disconnected/failed
App: none → binding → preparing-all → ready/ready-with-limitations
Session: idle → starting → recording → stopping → sealing → saved/partial/failed
“ready-with-limitations”是产品准备状态,不是全站devlog状态词。表现为可开始,并明确列出不适用/失败项。设备连接、目标身份或Raw持久化基础失败才阻断开始;非必需单源失败不拖垮全部普通采集。
4.2 失效矩阵(必须逐格测试)
| 变化 | 设备连接 | 应用绑定 | 全源准备 | 报告状态 |
|---|---|---|---|---|
| +/移除图表 | 保持 | 保持 | 保持 | 保持 |
| 同应用重复选择 | 保持 | 保持 | 幂等返回 | 保持 |
| A应用切B应用 | 复用健康连接 | appGeneration+1 | 清理进程相关状态并重绑;复用设备级源 | 重新生成未开始草稿 |
| 应用版本改变 | 保持 | appGeneration+1 | 重验目标能力 | 新草稿 |
| 连接路由/设备重连 | deviceGeneration+1 | 失效 | 重新准备 | 不复用旧测试边界 |
| 采集策略/测试方式改变 | 尽量复用 | 保持身份 | 仅重配受影响源 | 新计划哈希 |
| 采集中显示变化 | 保持 | 保持 | 保持 | 仅UI视图变化 |
| 采集中目标进程重启 | 保持 | targetGeneration+1 | 原有重绑定规则 | 重置差分/帧窗口;禁止混代 |
4.3 重复prepare_app协议
现有采集端通过一个只能完成一次的Future接收prepare_app,第二次会返回app_already_prepared。执行者必须修正协议,而不是只删除前端if。
- 空闲准备阶段改成应用准备命令队列,容量1,最新请求替代尚未开始的旧请求;取消旧应用任务后再处理新应用。
- 命令带requestID、deviceGeneration、appGeneration、capturePlanHash。
- 旧请求的晚到回包可以记录诊断,但不得更新当前token。
- 采集期间拒绝更换目标/采集计划,返回明确的session-active;显示变更不发送命令。
- 首批保留现有“一个预热进程服务一场Session”的模型。停止后可重新建立下一场预热进程;禁止顺手扩大为多Session常驻管理器。
- 同一预热进程中,切换应用复用DTX owner,释放旧应用专属资源;出现provider损坏才重建连接。
- 所有取消操作必须等待旧任务收口,再将新generation标为ready。
4.4 准备与实际采样分开
- 普通源:先probe/prepare_transport,不提前开始持续采样。
- 要求PID的源:准备服务通道;目标实际启动后的身份核验通过后再绑定PID和启动源。
- 启动源是例外:沿用已验证的预启动准备、bootstrap保留、早期drain和有界缓存;不回放选择应用后等待期间的全部高频数据。
- 继续保留既有completion grace、source-loss门禁和单活跃tap约束。
- 报告模板初始化只生成结构/聚合器,不创建虚假的running Session。
- Start时创建真实Session目录、确认可写和初始清单持久化成功,再执行目标启动。避免模板存在但磁盘失败仍开始测试。
5. 流式存储与报告架构
5.1 必须复用的基础
保留RawSessionRecorder、IncrementalMetricRecorder、SessionWriter的顺序关系。新增在线报告对象接到同一writer的成功写入路径,不创建不受控制的额外队列。
设备源 → 有界入队 → Raw顺序writer
↓ normalized Raw(分配全局seq)
↓ 已有Metric engine(只consume一次)
↓ 增量普通报告/帧报告/启动报告/质量与映射审计
↓ 追加明细 + 更新精确索引 + 哈希
↓ 批量提交 + 周期checkpoint
↓ 小型报告快照 → UI
5.2 新模块建议及接口
# online_report.py
class OnlineReportSession:
def append(self, raw, metric_frames, decoded_outputs) -> None: ...
def snapshot(self) -> dict: ...
def checkpoint(self, durable_boundary) -> dict: ...
def seal(self, frozen_boundary) -> dict: ...
# streaming_stats.py
class RunningStats:
def add(self, value: float) -> None: ...
def remove(self, value: float) -> None: ... # 仅处理精确同键替换所需场景
def merge(self, other) -> None: ...
def result(self) -> dict: ...
# exact_order_index.py
class ExactOrderIndex:
def add(self, series_id: str, value: float, count: int = 1) -> None: ...
def select(self, series_id: str, rank: int) -> float: ...
def range_count(self, series_id: str, low: float, high: float) -> int: ...
def largest_sum(self, series_id: str, count: int) -> float: ...
def commit(self, sequence: int) -> None: ...
# live_seal.py
class LiveSealContext: ... # 仅同一writer生命周期内构造,禁止从未校验JSON伪造
def seal_live_session(context: LiveSealContext, progress=None) -> dict: ...
decoded_outputs必须来自当前实际decoder输出,不得在OnlineReportSession里再创建一套全量decode。现有RawMetricEngine内置实时frame decoder,应扩展其内部返回结果/协作接口暴露frame events,更新所有调用点测试,保持外部Metric schema兼容。
启动decoder目前分布在早期drain和常规consume路径;迁移时先画清调用顺序,将同一sourceEpoch的消费统一到有序路径。传入append前如果某blob已消费,禁止重复consume。启动tap与frame tap分别保留独立decoder状态。
5.3 writer失败合同
normalized = raw.append(event)
frames, decoded = engine.consume_once(normalized)
metrics.append_derived(frames)
reports.append(normalized, frames, decoded)
advance_all_consumed_sequence(normalized.sequence)
伪代码表达唯一顺序,不要求直接照搬已有类签名。任一阶段失败时writer poison=true,禁止在同一状态里重试或继续宣称seal成功。Raw已写入而报告失败属于可恢复中断,不把它当完全成功。
没有生成metric的Raw也必须推进全部消费者的consumedRawSequence,防止最后一条为metadata时错误认为报告没消费完。
5.4 新文件布局
在现有采集子会话目录新增,不移动既有文件:
capture-plan.json # 请求计划、解析后的64项覆盖、版本
report-live.json # 小型原子快照,生命周期内可替换
analysis-state.sqlite # 精确有序索引、join状态、增量状态;单写者
frame-events-live.jsonl # 追加帧明细,保留sourceEpoch/generation
report-checkpoint.json # 最近持久化共同边界
report-final.json # 最终汇总及64项覆盖账本
report-seal.json # 最终文件/边界承诺
原 metrics-derived.jsonl、启动/帧/自校准结果文件和 foundation-delivery.json 保留兼容投影。UI新代码优先读取新报告;旧reader仍获得所需旧名称小型文件。
最终报告引用完整明细,不嵌入整场样本数组。frame明细在采集中写入;停止不得再创建完整samples列表。HTML/ZIP导出按需读取这些明细,不影响本地报告已保存状态。
5.5 截图与产品层存储
- 截图沿用单请求在途、1秒调度,不因UI显示第二次请求。
- 每张成功截图保存后追加附件索引,记录序号、时间、大小、SHA-256。
- 不在停止时遍历全部PNG计算哈希。
- 停止时取消后续调度,在途请求有界处理;成功且纳入边界则记录,超时则记录附件缺失。
- 产品层事件、CSV仍增量保存;新增metric组显示只查询存储和缓存,不补采过去数据。
6. 普通统计公式(实现前冻结,不允许凭印象重写)
6.1 原始公式保留原则
本批改变计算执行时机与状态组织,不擅自改变现有数值意义。对每个字段记录:输入、过滤、scope、单位、窗口、重置、聚合、取整、版本、出处。Appendix A生成64项来源账本;D0任务用固定源函数生成逐字段公式版本快照与golden后才开发;这是机械提取/校验,不要求执行者重新设计公式。
6.2 在线均值和总体方差
对通过原quality过滤的有限值x,维护n、mean、M2、min、max。
n' = n + 1
δ = x - mean
mean' = mean + δ/n'
M2' = M2 + δ*(x - mean')
populationVariance = M2/n
standardDeviation = sqrt(populationVariance)
n=0时结果null;样本总体方差采用除n而非n−1。浮点微小负方差只允许在明确误差阈值内夹到0,超过阈值记录数值失败。原始值和整数计数不提前舍入。
最小值、最大值和阈值次数O(1)更新。缺失、warmup、counter-reset、invalid-window的过滤沿用原报告字段规则,禁止使用统一“只接受valid”过滤而丢掉candidate。
6.3 网络报告特殊规则
现有报告按同一时间戳Send/Recv求交集:只有进程send序列和recv序列都非空时选择进程scope,否则选择设备scope。即使进程两侧各自非空但时间戳交集为空,也保持该scope下的missing,不再退到设备scope。该细节与当前代码严格一致。
- 维护两套scope独立状态,禁止先把二者累加在一起。
- 按现有精确timestamp key做join,禁止新增近邻时间匹配。
- 同timestamp重复更新遵循现有dict最后值生效;存旧值后撤销对应pair贡献,再加新值。
- join状态存入SQLite带主键表,禁止在内存保留整场字典。两侧均到齐才更新pair统计。
- 最终scope选择沿用冻结的旧算法条件;对“只有进程单边”“晚到另一边”单独做等价测试。
- 字节换算、每10分钟速率归一化仍用原公式,不用实际传输总量替代旧rate平均口径。
6.4 时间与归因
- 不比较来自不同clock domain的裸整数时间。
- 使用既有clock bridge;无有效桥时保留来源时间与host receive边界,并声明误差/窗口类型。
- PID与sourceEpoch变化先清理相邻差分、帧前后值、计数器基线,再消费新窗口。
- 统计汇总可组合明确有效的窗口;跨gap的时间长度不能作为连续测量时长。
- 周期采样积分/平均的窗口口径以原算法为准,禁止将样本平均悄悄改成时间加权平均。
7. 精确中位数、最慢1%和范围统计:固定实施路线
7.1 首版技术选择
使用单writer拥有的SQLite数据库,保存带子树计数与子树和的AVL树,内存只保留有界节点缓存。使用运行环境已有标准库sqlite3,不增加包依赖。
普通SQL ORDER BY ... OFFSET ... 或 SUM(count) 扫描整张频率表不作为rank查询实现;否则停止仍与历史规模线性相关。
这是高风险独立组件。先完成D1原型、正确性和真实性能门禁,再接入产品。门禁失败回到方案评审,禁止执行者自行改成近似分位数或停机排序。
7.2 节点及表
CREATE TABLE order_nodes (
series_id TEXT NOT NULL,
node_id INTEGER NOT NULL,
value_key BLOB NOT NULL,
value REAL NOT NULL,
frequency INTEGER NOT NULL,
left_id INTEGER,
right_id INTEGER,
height INTEGER NOT NULL,
subtree_count INTEGER NOT NULL,
subtree_sum REAL NOT NULL,
PRIMARY KEY(series_id, node_id),
UNIQUE(series_id, value_key)
);
CREATE TABLE order_roots (
series_id TEXT PRIMARY KEY,
root_id INTEGER,
total_count INTEGER NOT NULL,
committed_sequence INTEGER NOT NULL
);
- 首批索引对象为有限正frameTime、frameFPS、普通Graphics FPS;合法0按对应算法过滤规则处理。
- value_key为规范化IEEE754位模式(−0归一为+0),不对测量值做分桶/舍入;树比较使用有限数值。
- 重复值frequency+1;子树计数包含重复次数,所有rank使用1-based。
- 树节点ID单调分配;每次更新重算height/count/sum;AVL失衡执行单/双旋转。
- 子树和用稳定求和维护;定义并验证浮点误差容差,禁止逐字节等价要求掩盖正确的浮点求和顺序差异。
- 缓存按实测对象字节数限制,初始16MiB;禁止仅按节点个数声称满足字节上限。
- 每批最多256条分析记录或250ms提交一次事务;同一dirty节点一批内只写一次;所有DB操作在writer线程,不在UI主线程。
7.3 必须实现并测试的操作
add(value):查找/插入;更新到根的count/sum/height;做必要旋转。O(log U)。select(rank):比较左子树count、当前frequency,向左/右下降。O(log U)。prefix_count(value, inclusive):查询≤value或<value的数量。O(log U)。range_count(a,b)=prefix_count(b,true)-prefix_count(a,false)。largest_sum(k):先取右子树;可整棵取用subtree_sum;不足时取当前节点部分frequency,再递归左树。O(log U)。commit(sequence):索引与其业务序号在同一事务提交。restore(checkpoint):只恢复同合同版本、同边界、已验证checkpoint;禁止给旧状态套新版本。
7.4 最终查询公式
median(x):
n奇数: select((n+1)/2)
n偶数: (select(n/2) + select(n/2+1))/2
k = max(1, ceil(n*0.01))
onePercentLowFPS = 1000 / (largest_sum_frameTime(k)/k)
medianFPS = median(frameFPS)
MedRange = 100 * range_count_frameFPS(0.8*medianFPS, 1.2*medianFPS)/n
- 保留边界包含关系、最少样本数量和现有quality。
- frameFPS中位数必须针对FPS序列,禁止直接用1000/median(frameTime)替代,偶数样本时两者可能不同。
- Smooth当前定义是相对整场frameTime中位数的均方误差。设b为该中位数、μ为在线均值、v为总体方差,数学等价式:
100*sqrt((v+(μ-b)^2)/(b^2))。沿用原四个有效间隔门槛及b>0检查。 - Smooth上述重排先写公式对照与误差测试;累计状态只按同窗口有效间隔更新。
- 所有整数计数/顺序/quality必须精确一致;浮点比较默认
abs(a-b) <= max(1e-9, abs(b)*1e-9),偏差超限不得自动放宽。
7.5 复杂度与代价如实记录
累计N条数据、U个不同值:总处理O(N log U),磁盘状态O(U),内存缓存有界;停止约O(K log U + B),K为固定统计查询数量,B为尾部待处理量。不是严格O(1),但移除了O(N)回读/排序。
若索引磁盘增长、随机写或CPU导致总资源门禁失败,本任务保持partial,提案另行评审。执行者不自行引入新的近似公式。
8. 帧与启动报告在线化
8.1 帧数据
- 从现有RealtimeFrameDecoder一次性输出读取;保留完整frame event并追加落盘。
- 维护最近3个有效interval,复用已有TinyJank/Jank/BigJank阈值和strict >比较。
- out-of-order、重复timestamp、首点warmup、gap重置逐项沿用测试合同。
- 对每个连续sourceEpoch分别保持first/last/previous,不跨tap链接。
- 维护drop、positive delta、stutter累计量;最终只用窗口计数/时长生成归一化值。
- 禁止把秒级Graphics FPS代替逐帧数据。
8.2 启动数据
- 单次完整Span闭合后即追加保存和发布revision,不等stop。
- 保存decoder profile版本/哈希、线程归因、source-loss质量和Total端点证据。
- 启动tap关闭时生成启动结果小文件及哈希;Session停止时只绑定全场最终Raw哈希和质量,不重新decode历史blob。
- 将“Span数值结果”与“Session最终质量”分开缓存;晚来的源丢包/归因失败可以撤销有效质量,不能因旧缓存已有值而忽略。
- 正常源切换无启动事件时,后续frame-only字节不进入启动decoder;相反也不拿frame-only事件补启动端点。
- 所有9个启动阶段始终显示覆盖状态;“核心6项有值”与“9项都有值”明确区分。
8.3 映射审计与自校准
- 自校准复用现有逐frame bucket累积函数,加入在线writer,不改变L1–L4含义。
- 来源字段、归因计数、未知fingerprint在启动消费时积累,不停止时重扫trace。
- 未知fingerprint多时写磁盘聚合,内存有界。达到显式预算则记录审计不完整,禁止伪造完整覆盖。
- 最终兼容投影生成原有小型审计/自校准结果,记录算法版本与边界。
9. 报告模板、实时快照与最终状态
9.1 模板内容
提前建立64项覆盖结构、原29项统计摘要结构、9个启动阶段结构、附件/事件入口、公式版本表与报告布局。模板中的sampleCount为0,测量结果为null、no-sample-yet。
应用切换丢弃未开始模板内存,替换为新目标模板;磁盘只保留可复用schema资源,不制造历史Session。
9.2 实时快照
- 每250ms最多产生一个UI快照;没有新revision直接短路。
- 每1秒最多保存一次
report-live.json;快照序号等于所有被展示聚合器已处理的共同水位。 - 先更新dirty状态,再由单调deadline调度;静默期间只保留一个下一次刷新任务,无busy polling。
- 单快照初始上限256KiB,仅含摘要/覆盖/有限最新点。动态维度明细分页保存,禁止截断后伪称全部结果。
- UI对迟到或回退revision直接忽略;不能把不同generation的增量拼在一起。
9.3 状态文案
| 事件 | 界面文字 |
|---|---|
| 全源准备中 | 正在准备全部可采集指标 |
| 部分源不适用 | 采集服务已就绪 · 查看不可采集项 |
| 正常运行 | 正在采集全部适用指标 |
| 点击停止 | 停止请求已发送 |
| 收口中 | 采样正在结束 · 正在保存末尾数据 |
| 已持久化封存 | 采集结束 · 报告已保存 |
| 尾部不完整但可读 | 采集结束 · 报告已保存,部分数据不完整 |
| 磁盘失败 | 保存失败 · 已保留可恢复数据 |
全量覆盖标签显示“请求64项、准备X项、不适用Y项、失败Z项”;分别按定义与能力计数,不拿9类源计数冒充64项。
10. 停止边界协议与缓冲处理
10.1 时间点和序号
增加以下审计字段:
stopRequestedHostNs # 点击停止时主机单调时钟
stopAcknowledgedHostNs # 采集端接收停止请求
stopCutoffMode # source-time / host-receive
lastAcceptedRawSequence # 最终接受原始事件边界
lastMetricConsumedRawSeq
lastReportConsumedRawSeq
lastProductWireSequence
sourceReleaseCompletedNs
reportDurableHostNs
productSessionDurableHostNs
所有跨进程host时间必须确认同机monotonic定义一致;否则用分别测量的持续时间或建立桥,不直接相减。
10.2 接纳尾部规则
- 停止后立即禁止发起新的周期采样/截图任务,并请求关闭源。
- 停止前发出而晚返回的数据:有可信source→host桥,按测量结束边界判断;无可信桥,采用host接收边界并标注窗口语义。
- 包含前后混合时段的blob原样保留到审计Raw,派生值按可验证事件时间筛选;若源时间不足以筛选,采用已声明的host边界策略,不发明精确设备截止。
- 传输审计流可包含stop后关闭/丢包等控制事件,记录最终accepted序号;测量窗口截止与文件最后序号是两个不同概念。
- 多源共用一个绝对drain deadline,不给每个源串行叠加2秒。
- 延续最大2秒drain边界作为初始容错值;有明确source end则提前结束。静默只能说明quiescent,不宣称有终点证明。
- deadline到但尚有未处理尾部:显式partial/tail-incomplete,保存已完成边界;禁止为了≤2秒指标丢尾部并标complete。
- 物理服务未释放前不允许新Session复用同源;UI响应和设备释放分开记录。
10.3 封存伪代码
async def stop_session(request):
accept_stop_once(request)
freeze_measurement_cutoff(request)
stop_periodic_scheduling()
await close_sources_until_shared_deadline()
# 最后一条closeout质量事件也进入有序writer
boundary = await writer.barrier_after_final_control_record()
assert boundary.raw_seq == boundary.metric_consumed_raw_seq
assert boundary.raw_seq == boundary.report_consumed_raw_seq
result = await writer.seal_live(boundary)
emit_report_sealed(result) # 采集端封存;产品端仍须同步保存
禁止等所有历史UI绘图渲染完再执行barrier;UI只接收最新快照。禁止只设置stop flag便显示“报告已保存”。
11. 持久化、共同水位与完整性
11.1 检查点顺序
每1秒或新增4MiB待提交数据触发一次检查点;两者取先到,但最小间隔250ms、最多一个checkpoint在途,合并积压请求。
冻结writer边界S
→ Raw/metric/frame/附件索引明细flush + fsync
→ 分析数据库提交到S(事务持久化)
→ 写小型checkpoint manifest临时文件并fsync
→ 原子rename为report-checkpoint.json
→ 同步父目录元数据
→ 宣布S为共同durable边界
sqlite采用WAL、synchronous=FULL;检查点预算在采集中测量,避免stop时强制全量VACUUM/REINDEX/TRUNCATE。DB仅作为中间索引与恢复状态,最终可读报告不依赖把整个DB导出成JSON。
WAL增长也要有界:不保持长寿命读事务,采集中按不超过4MiB未checkpoint日志的初始预算执行增量checkpoint;记录其耗时与阻塞。最后连接close可能触发checkpoint,因此D1/D6基准必须覆盖commit、close和重开全部时间,禁止把大WAL留到stop才处理。若平台API和数据库行为导致该预算持续超限,门禁失败;不要用隐藏未完成DB关闭来假装完整持久化。
若SQLite在崩溃时领先checkpoint文件,恢复时不得给分析状态倒填旧seq。首版恢复策略:校验Raw连续前缀并在新恢复目录全量重建派生状态,保留原会话;恢复速度不属于正常stop目标。以后再单独开发安全的增量恢复优化。
11.2 在线快速封存与独立审计区分
- 正常seal只允许由当前健康writer持有的LiveSealContext发起。
- Context包含每个文件句柄对应的实际写入哈希、字节计数、最终seq、文件身份和算法版本。
- 拒绝路径逃逸、符号链接替换、文件身份变化、长度不符、source gap未交代、consumer水位不齐和poison状态。
- 正常seal不调用全量validate_recorded_session/load_metric_snapshot。
- 在线写入哈希证明“当前writer提交的字节”;不等于已逐字节回读磁盘,更不等于对恶意同长度原地篡改的全面检测。
- 新报告显式写
integrityMode=writer-seal.v1;独立打开不可信文件、导入、审计和恢复采用full-replay-verified等明确模式,执行原完整验证。 - 拟新增模式先落质量文档;禁止把writer-seal标为独立完整回读认证。
- 停止后的同机报告可以即时查看小型已封存结果;请求全面核验另行执行,不替代尚未完成的必需报告。
11.3 最终原子清单
每个最终报告文件先写临时文件、fsync并rename。最后发布report-seal.json及兼容foundation-delivery.json,列出全部必需文件哈希和最终边界。清单不含自身哈希;其SHA-256由上层记录,避免递归依赖。
读者只接受最后seal所引用的一代完整文件;途中的新旧文件混合不构成成功报告。任何一步失败都保留上一完整checkpoint和原始数据。
11.4 产品层与采集层双重封存
采集端report_sealed不能直接使界面saved:
- 产品层消费到该事件之前全部wire事件,并写入事件/CSV/附件索引。
- 产品层核对sessionID、sequence、采集seal哈希与必需文件。
- 产品层flush并原子保存顶层manifest,记录
coreSealSHA256与最后wire序号。 - 第3步成功后才发出
session_saved并允许用户把它当保存完成。
任一层失败都进入保存失败或partial。产品层禁止在这一步重新读取全部明细算统计;仅处理自己的有界尾部和清单。
12. 协议与兼容升级
沿用现有NDJSON外层协议,通过hello新增能力all-metric-plan.v1、online-report-seal.v1。字段扩展使用严格schemaVersion,不擅自破坏旧客户端。
新增/扩展事件:
{
"kind": "app_ready",
"requestID": "REQUEST_A",
"deviceGeneration": 1,
"appGeneration": 2,
"capturePlanHash": "HASH",
"capabilityRevision": 1,
"requestedMetricCount": 64,
"preparedMetricCount": 0,
"coverage": []
}
以上count=0仅用于结构示例;真实app_ready必须带64项coverage并满足计数校验,禁止复制示例伪造准备成功。
{
"kind": "report_sealed",
"sessionID": "SESSION_A",
"finalRawSequence": 100,
"reportConsumedRawSequence": 100,
"sealSHA256": "HASH",
"integrityMode": "writer-seal.v1",
"dataCompleteness": "complete",
"relativeReportPath": "report-final.json"
}
complete只表示本次声明窗口的持久化/完整性满足合同,valueQuality仍可能missing/candidate。- 新客户端遇到缺少新能力的旧采集端:明确“采集组件版本不匹配”,禁止降级后仍显示新功能已就绪。
- 旧会话继续用旧reader/finalizer;没有新seal的旧会话不推断成快速封存。
- startup报告revision按原机制保留;新整体报告快照不覆盖它的更晚代次。
13. 有界队列、线程与执行效率
13.1 固定初始预算(必须基准验证)
| 项目 | 初始设置 | 实现要求 |
|---|---|---|
| DTX owner | 1 | 共享服务去重;避免并发争抢tap |
| 准备 | 单源3s、总12s | 绝对deadline;普通串行,确实独立操作最多2路 |
| 主分析writer | 1线程 | 顺序状态唯一所有者,不在主线程fsync |
| Raw待处理队列 | 64MiB字节预算 | 复用既有预算,不额外再排一份64MiB复制队列 |
| UI事件批次 | 每250ms最多1次 | coalesce快照,不丢弃持久化明细 |
| 精确索引缓存 | 16MiB初始 | 按字节计量,磁盘状态增长独立记录 |
| DB提交 | ≤256分析记录或250ms | 事务批量更新,禁止一条数据一次fsync |
| durable checkpoint | 1s或4MiB,最小250ms间隔 | 单在途、请求合并 |
| 设备尾部drain | 全源共享≤2s初始deadline | 超时明确partial,禁止每源叠加 |
| 停止失败兜底 | 保留现有约10s强制收口机制 | 不把此上限当正常目标;强制后partial |
持续处理lag超过500ms发布诊断;超过队列上限必须记录真实loss、降级质量。禁止偷偷降低采样率或关源。工程调优先批量操作、去复制、去重复decode和索引缓存;持续吞吐未过门禁时不开默认新路径。
13.2 正常条件下验收目标
- 点击停止到可见反馈P95≤100ms。
- 输入到报告快照P95≤500ms(使用同机可比较时钟)。
- 尾部达到共同边界后到双层durable完成P95≤500ms。
- 点击停止到最终报告可用P95≤1s、P99≤2s为目标;设备未结束、持续回包、磁盘故障单独进入异常组,不能从失败统计中删除。
- 10分钟、1小时、4小时测试停止时间不随历史量线性增长;1h→4h正常stop P95增长≤20%且绝对增长≤200ms为初始门禁。
- 新增分析吞吐≥实测输入P95的1.5倍;同时测试突发峰值与持续平均负载。
- 同输入规模新增CPU平均开销≤基线的15%,RSS增量≤64MiB作为初始门禁;基线接近0时同时报告绝对CPU时间,避免百分比误导。
- 新增持久化索引/报告占用≤原Raw+Metric总字节的15%(截图单列);超限单独列为门禁,不隐藏成本。
- 全部目标是待验证设计目标;本方案没有宣称已达到E4。
14. 开发任务卡(按顺序执行,禁止跳步)
每张任务卡的提交范围只包含本卡产品、匹配测试、合同文档与验证记录。任务卡通过不等于整体交付完成。
D0:冻结基线与公式账本,预计1–2工作日
修改:只新增internal基线记录与基准脚本,实施前先保留原代码基线。
操作:
- 记录HEAD、运行时版本、主机/磁盘条件、构建哈希;匿名化设备与目标。
- 导出64项请求ID、源族/adapter、现有quality和公式版本;断言每个Registry ID恰有一个owner。
- 对9个收尾阶段加入基线计时(新增审计字段,不改变计算)。
- 记录preparation request、provider创建/关闭、app bind计数,复现+重复准备。
- 形成固定原始输入与旧报告golden;所有失败样本保留。
通过:可重复复现两个现象;旧结果与输入哈希绑定;本方案附录的公式来源检查齐全。
D1:增量统计与精确索引原型,预计3–5工作日
新增:streaming_stats.py、exact_order_index.py与独立测试/benchmark。
操作:先实现RunningStats,再实现AVL持久化缓存及五类查询;与排序参考逐步对照;运行100万以上有效帧数据。
通过:所有整数/quality/边界一致,浮点容差满足;100万/500万distinct场景无内存线性增长;吞吐/空间开销门禁通过。未通过不接产品。
D2:采集计划与显示配置彻底分离,预计1–2工作日
修改:CapturePlan.swift、CollectorReadiness.swift、PerfowlWorkbenchView.swift、PerformanceSessionController.swift、CollectorSupervisor.swift、capture_policy.py、CLI协议与对应测试。
操作:所有准备API移除DisplayPreferences参数;添加新计划/能力协商;测试启动与截图默认启用;历史decode兼容。
通过:待机/采集中各增删图表100次,prepare/provider重启计数均不变;隐藏CPU/FPS/Startup/Screenshot后底层仍保存对应事实或明确缺失原因。
D3:全源准备与应用重绑定,预计2–3工作日
修改:source_preparation.py、prewarm命令循环、控制器token处理和设备层缓存。
操作:应用generation隔离;同设备A→B→A复用owner;按能力构造64项账本;USB电源分项门禁;目标绑定晚到回包丢弃。
通过:切应用不串目标;取消期间不泄漏旧任务;单源失败不阻断其他适用源;重复app请求幂等。
D4:统一在线报告writer,预计2–3工作日
新增/修改:online_report.py、session_writer.py、metric_engine.py、report_engine.py、self_calibration.py。
操作:Raw与Metric一次派生;增加共同consumedRawSequence;普通统计、network join、quality在线化;失败poison;小型快照。
通过:在线与旧离线报告字段匹配;空Raw/no-output事件正确推进;模拟写失败后无成功seal;报告快照不包含整场数组。
D5:帧、启动与映射报告增量化,预计3–4工作日
修改:frame_derivation.py、launch_trace.py、launch_mapping.py、startup_timing.py、realtime.py。
操作:decoder只consume一次;启动早期/正式consumer顺序交接;帧事件追加;精确索引接入;启动结果冻结且质量仍可撤销;兼容报告投影。
通过:分块、gap、warmup、source-loss、晚到终点、重复span、不同tap切换全部回归;停止路径禁止重decode启动/帧历史。
D6:持久化检查点与快速封存,预计3–4工作日
新增/修改:live_seal.py、persistence.py、checkpoint_policy.py、delivery.py、CLI。
操作:实现共同durable边界、原子最后清单、writer-owned seal、恢复隔离;新正常路径绕过offline finalizer。
通过:用spy拦截完整Raw/Metric读取和offline derive,正常stop调用次数0;每一步失败注入无假完成;原独立审计仍能发现篡改。
D7:产品层保存、截图及界面闭环,预计2–3工作日
修改:PerformanceSessionController.swift、PerformanceSessionStore.swift、CollectorNDJSONCodec/模型、SessionReview/Exporter及工作台。
操作:双层seal;截图默认采集;显示选择纯UI;旧会话reader;可见准备覆盖;保存状态只在durable后切换。
通过:UI不等待9阶段滚动;关闭后重新打开报告一致;新增图表有历史数据;stop→马上退出应用的已保存会话仍可恢复读取。
D8:性能、异常、真机和交付,预计3–5工作日+外部门禁时间
操作:完成第15–17章矩阵;匿名输出基线对比与统计;打测试包;按仓库发布规则分别验收签名/公证/安装;窄提交、双远程推送和通知。
通过:所有必需功能、性能与数值门禁有证据;未满足项明确partial/blocked。不把编译成功等同真机通过。
单名开发者粗估20–31工作日,不含设备通道恢复、公证凭据和其他外部等待;实际以D0/D1结果修正。这是调度估计,不是完成日期承诺。优先做D1可尽早暴露精确统计成本,避免先改全部UI后发现停止目标仍未解决。
15. 必须新增的自动化测试清单
所有测试必须断言实际调用/文件/序号,不仅检查提示字符串。建议名称可直接沿用。
准备与显示
test_display_change_does_not_prepare_or_restart_providertest_all_registry_ids_requested_with_empty_displaytest_hidden_startup_and_screenshot_still_collectedtest_same_app_preparation_is_idempotenttest_app_rebind_reuses_device_ownertest_stale_app_ready_cannot_override_new_generationtest_route_change_invalidates_device_and_app_tokenstest_one_optional_source_failure_preserves_other_sourcestest_usb_energy_gates_are_per_metric_not_entire_familytest_source_transports_prepared_once_and_sampling_starts_only_on_start
增量统计与顺序索引
- 空、单点、偶数/奇数中位数、重复值、已排序/逆序/随机、极端值、合法0与非有限值。
- AVL每种旋转;每次插入检查height/count/sum与排序reference。
- 最慢1%在99/100/101、199/200/201点的ceil边界。
- median范围含边界值;偶数frameFPS反例,禁止倒数中位数近似。
- Smooth按旧公式对照;方差使用总体而非样本公式。
- network同键替换/晚到、仅一边、进程优先与设备fallback。
- 磁盘缓存淘汰后结果一致;DB重开后rank/sum一致;随机固定seed和失败seed落盘。
流式完整性
test_every_raw_sequence_advances_report_even_without_metricstest_poisoned_writer_never_produces_success_sealtest_stop_live_path_never_reads_full_raw_or_metrictest_report_snapshot_matches_its_published_watermarktest_source_loss_invalidates_cached_startup_qualitytest_frame_tap_gap_never_creates_cross_gap_intervaltest_old_report_reader_still_works_without_new_sealtest_offline_audit_detects_tampering_despite_live_sealtest_product_saved_waits_for_both_core_and_product_fsync
崩溃与磁盘故障
在以下位置逐一终止子进程/注入错误:Raw写后、Metric写后、frame写后、DB事务前/后、checkpoint临时写后、rename前/后、core seal后、product seal前/后。
每个失败点验证:原文件保留;无虚假saved;恢复目录独立;可恢复前缀连续;重复恢复不污染原文件;旧请求重发无双重计数。额外测试磁盘满、只读目录、fsync失败、路径替换、文件截断、同长度内容篡改的独立审计。
16. 性能基准怎么跑、怎么判断
16.1 数据规模
- 时长:10分钟、1小时、4小时。
- 原始数据:200MiB、1GiB、4GiB;有外部blob和旧内嵌编码两类。
- 有效帧:100万、500万;重复值多与distinct接近N两种。
- 流模式:稳定输入、峰值突发、安静尾部、持续回包尾部、延迟启动终点、source gap。
- 包含真实留存Raw回放和真实写盘;禁止只用纯内存synthetic证明磁盘/设备表现。
- 全指标/截图开启;默认显示、空显示、全部显示、持续增删显示四种UI负载。
16.2 方法
- 固定baseline与candidate构建,记录哈希与环境;使用同一输入。
- 前后交替ABBA运行,暖缓存与冷启动分组;不声称清空OS缓存,实际条件写明。
- 每组至少20轮报告中位数与P95;P99至少100轮才进入正式门禁,少于100轮只给max及样本数。
- nearest-rank:排序后P95取ceil(0.95*n),P99同理。失败不删除,单列失败率并影响验收。
- 跑到报告文件durable为止,另记源释放、UI反馈、尾部处理、索引查询、产品封存持续时间。
- 记录CPU seconds/平均峰值CPU、RSS峰值、DB/Raw/Metric/PNG增量字节、读写吞吐、队列高水位与p95 lag。
- cache命中导致OS block I/O为0时,仍统计应用层逻辑读取量,不宣传磁盘零开销。
- 所有数字输出JSON + Markdown表;版本、数据输入哈希和运行命令可复现。
16.3 两个必须成立的不变量
相同Raw与公式合同 → online与offline值/quality/计数一致
相同设备/应用/采集计划 → display变化不会改变prepare/source生命周期
若切断某指标采集后才能达到性能目标,则方案未达成用户要求。
17. 真机验收脚本(执行者逐项打勾)
目标设备/应用按用户指定,本地记录真实身份,公开材料统一“设备A/目标应用A”。
- 新包独立安装运行,记录可执行文件哈希,确认不是旧进程。
- 选择设备,等待设备ready;选择应用,只出现一次全源准备周期。
- 在准备后等待15秒,增删显示图表10次:连接计数/准备请求均不变。
- 保持默认显示开始冷启动;启动阶段完成即发布;观察普通曲线继续。
- 采集中新增启动、GPU、能耗和截图显示,确认新增图表读取已有结果;Raw中没有再次prepare。
- 隐藏全部图表继续30秒:Raw/Metric持续增长,截图持续追加。
- 点击停止并录像/记录时间:界面响应、设备释放、报告保存分别计时。
- 退出再打开,核验完整覆盖、原始附件、最终报告和seal;核对隐藏指标数据仍在。
- 重复第二次冷启动,分别列出9阶段值/quality,禁止只报Total或“6/6核心”代替全部阶段账本。
- 进行至少1次1小时全量采集和1次4小时长稳;短两轮启动成功不关闭长稳门禁。
- USB与可用无线/无外供电条件分组;缺少相应条件明确未测。
- 收口异常组:停止时断连、目标退出、晚到数据、磁盘压力。存在尾部不完整时显示partial。
设备通道未就绪时可以继续E1/E2工作,但E4保持未完成;禁止重复使用旧版本两轮数字作为本批成功证据。
18. 回滚、版本与发布
- 新路径先设内部开关
online-report.v1;测试默认开启,正式默认切换以D8门禁为准。 - 回滚为下一场新Session选择旧保存路径;进行中的Session不切换算法与存储schema。
- 旧会话只读保留,恢复输出到新目录。
- 新小型最终报告兼容投影必须与旧reader集成测试;缺失新能力明确报告组件不匹配。
- 禁止在出现性能退化时自动关闭隐蔽指标源。
- 产品改造完成后按仓库规则打包、签名、公证、staple、Gatekeeper、独立安装和哈希验证,测试包与正式外部分发分别记账。
- 本方案交接仅产生文档,未触发产品包构建;后续实现交付需窄提交、双远程推送尝试及结构化通知。
19. 文档联动与交付清单
公式或quality变更先修订Canonical知识页,追加版本历史,再修改代码;旧devlog和归档验收数字保持不变。进度更新到站点时间线、八阶段表与开放门禁表。
执行者最终提交以下材料:
- [ ] 全量64项覆盖账本及附件覆盖。
- [ ] D0–D8任务状态表,每项对应提交和测试证据。
- [ ] 新增协议字段和跨语言fixture。
- [ ] 全部新模块单元测试、故障注入、旧会话兼容结果。
- [ ] 同输入online/offline差异报告。
- [ ] ABBA性能基准,CPU/RSS/I/O/吞吐/stop延迟前后对比。
- [ ] 真机两次启动9阶段逐项结果及1小时/4小时记录。
- [ ] 必需报告无停止后全历史复算的spy/调用图证据。
- [ ] 实际安装测试包、签名与哈希;当前运行包身份。
- [ ] 提交、分支、工作区保留项、双远程结果、通知结果。
- [ ] 未关闭门禁与下一阶段,不以“代码已写”代替完成。
20. 给执行者的禁止简化清单
- 只把“所选指标”改成“全部指标”。
- 只删除UI task key,仍从显示列表发送startup/截图开关。
- 只创建报告模板,停止仍调用原9阶段finalizer。
- 把原finalizer放后台,UI先显示报告已完成。
- 保留全历史数组在内存,称其为流式计算。
- 用SQL OFFSET或全表聚合做中位数查询,声称对数时间。
- 用近似分位数替代精确统计却沿用旧算法版本。
- stop时丢掉队列尾部以满足耗时指标,且不记partial。
- 跳过fsync或产品层保存确认,提前saved。
- 禁用原始数据审计,或把writer-seal说成独立磁盘全量校验。
- 把启动9阶段中条件性缺失写为0/通过。
- 用过去真机测试和旧包证明新方案已完成。
21. 执行者可直接复制的工作指令
从D0开始,按本任务书开发。先验证基线,再写当前任务卡测试,再实现最小范围。显示选择与采集计划严格分离;默认覆盖全部Registry定义和截图附件;所有质量、时钟、归因和缺失语义保持可审计。正常停止路径只处理有界尾部和最终封存,禁止全历史回读、排序和重新派生。每个任务卡提交必须给出测试输出与差异清单;不得修改无关工作区文件。遇到任务书没有覆盖的语义冲突,记录具体冲突及最小决策问题,不自行降低精度、减少采集或伪装保存完成。D1精确索引、完整性合同和D8真机性能未过门禁时,整体保持partial。
附录A:64项指标到源族的基线账本
下面由当前固定版本的源合同生成。列表只声明请求覆盖与源归属;实际可采集状态以准备时探测与运行质量为准。执行者必须从所列函数机械提取每个字段的版本与golden测试,不把源族名称当数值能力证明。
| # | metricID | 源族 | Adapter归属 | 公式定位 |
|---|---|---|---|---|
| 1 | ios.process.cpu.non_normalized |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 2 | ios.device.cpu.total |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 3 | ios.device.cpu.total.normalized |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 4 | ios.device.cpu.core_usage |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 5 | ios.process.memory.physical_footprint |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 6 | ios.process.memory.resident |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 7 | ios.process.memory.xcode_gauge |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 8 | ios.process.memory.real |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 9 | ios.process.memory.virtual |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 10 | ios.device.memory.available |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 11 | ios.process.thread.count |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 12 | ios.process.scheduler.context_switch_rate |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 13 | ios.process.scheduler.interrupt_wakeup_rate |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 14 | ios.process.disk.read_rate |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 15 | ios.process.disk.write_rate |
sysmontap |
pmd3.sysmontap.raw |
计算沿用metric_engine.py;CPU/内存/调度/磁盘Canonical页 |
| 16 | ios.device.network.send_rate |
device-network |
pmd3.device-network.raw |
metric_engine.py;network-send-recv.md |
| 17 | ios.device.network.receive_rate |
device-network |
pmd3.device-network.raw |
metric_engine.py;network-send-recv.md |
| 18 | ios.process.network.send_rate |
process-network |
perfowl.process-network.raw |
metric_engine.py;network-send-recv.md |
| 19 | ios.process.network.receive_rate |
process-network |
perfowl.process-network.raw |
metric_engine.py;network-send-recv.md |
| 20 | ios.foreground.fps |
graphics |
pmd3.graphics.raw |
metric_engine.py;frame-performance.md与GPU页 |
| 21 | ios.foreground.gpu.renderer_utilization |
graphics |
pmd3.graphics.raw |
metric_engine.py;frame-performance.md与GPU页 |
| 22 | ios.foreground.gpu.tiler_utilization |
graphics |
pmd3.graphics.raw |
metric_engine.py;frame-performance.md与GPU页 |
| 23 | ios.foreground.gpu.device_utilization |
graphics |
pmd3.graphics.raw |
metric_engine.py;frame-performance.md与GPU页 |
| 24 | ios.device.gpu.counter.source_native |
gpu-counter |
perfowl.gpu-counter.raw |
metric_engine.py;gpu-counter.md |
| 25 | ios.frame.time |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 26 | ios.frame.fps.average |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 27 | ios.frame.fps.at_least_18_percent |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 28 | ios.frame.fps.at_least_25_percent |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 29 | ios.frame.ftime_ge_100_percent |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 30 | ios.frame.ftime_positive_delta_gt_100_per_hour |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 31 | ios.frame.drop_per_hour |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 32 | ios.frame.med_range_percent |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 33 | ios.frame.one_percent_low_fps |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 34 | ios.frame.small_jank_per_10min |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 35 | ios.frame.jank_per_10min |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 36 | ios.frame.big_jank_per_10min |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 37 | ios.frame.stutter_percent |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 38 | ios.frame.tiny_jank_per_10min |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 39 | ios.frame.smooth |
core-profile-frame |
pmd3.core-profile.raw<br>perfowl.frame-pipeline |
frame_derivation.py;frame-performance.md |
| 40 | ios.process.energy.total_cost |
energy |
pmd3.energy.raw |
metric_engine.py;energy-battery-thermal.md |
| 41 | ios.process.energy.cpu_cost |
energy |
pmd3.energy.raw |
metric_engine.py;energy-battery-thermal.md |
| 42 | ios.process.energy.gpu_cost |
energy |
pmd3.energy.raw |
metric_engine.py;energy-battery-thermal.md |
| 43 | ios.process.energy.network_cost |
energy |
pmd3.energy.raw |
metric_engine.py;energy-battery-thermal.md |
| 44 | ios.process.energy.location_cost |
energy |
pmd3.energy.raw |
metric_engine.py;energy-battery-thermal.md |
| 45 | ios.process.energy.display_cost |
energy |
pmd3.energy.raw |
metric_engine.py;energy-battery-thermal.md |
| 46 | ios.process.energy.overhead |
energy |
pmd3.energy.raw |
metric_engine.py;energy-battery-thermal.md |
| 47 | ios.device.battery.current |
device-conditions |
pmd3.device-condition.raw |
metric_engine.py;energy-battery-thermal.md |
| 48 | ios.device.battery.voltage |
device-conditions |
pmd3.device-condition.raw |
metric_engine.py;energy-battery-thermal.md |
| 49 | ios.device.battery.power |
device-conditions |
pmd3.device-condition.raw |
metric_engine.py;energy-battery-thermal.md |
| 50 | ios.device.battery.temperature_raw |
device-conditions |
pmd3.device-condition.raw |
metric_engine.py;energy-battery-thermal.md |
| 51 | ios.device.battery.temperature_celsius |
device-conditions |
pmd3.device-condition.raw |
metric_engine.py;energy-battery-thermal.md |
| 52 | ios.device.thermal.status |
device-conditions |
pmd3.device-condition.raw |
metric_engine.py;energy-battery-thermal.md |
| 53 | ios.process.startup.observed_running_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 54 | ios.process.startup.foreground_ready_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 55 | ios.process.startup.app_launch_total_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 56 | ios.process.startup.system_interface_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 57 | ios.process.startup.static_runtime_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 58 | ios.process.startup.uikit_initialization_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 59 | ios.process.startup.uikit_scene_creation_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 60 | ios.process.startup.will_finish_launching_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 61 | ios.process.startup.did_finish_launching_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 62 | ios.process.startup.scene_will_connect_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 63 | ios.process.startup.scene_will_enter_foreground_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
| 64 | ios.process.startup.initial_frame_rendering_ms |
startup |
perfowl.lifecycle.passive.raw<br>pmd3.core-profile.raw<br>perfowl.startup-control.raw |
startup_timing.py / launch_trace.py;startup-timing.md |
附录B:文件级实施清单与验收依赖
| 文件 | 操作 | 必须删除/替换的职责 | 新职责 | 对应任务 |
|---|---|---|---|---|
MacApp/Sources/PerfowlCore/CapturePlan.swift |
新建 | 无 | 请求计划/显示偏好分离、规范化编码、跨语言hash | D2 |
MacApp/Sources/PerfowlCore/CollectorReadiness.swift |
修改 | 显示集合参与准备匹配 | 设备/应用代次和计划匹配 | D2/D3 |
MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift |
修改 | +触发prepare;截图由显示决定 | 显示纯UI、准备覆盖和保存状态 | D2/D7 |
MacApp/Sources/PerfowlCore/PerformanceSessionController.swift |
修改 | 显示驱动startup;采集seal直接等于保存 | 独立测试模式、双层封存 | D2/D7 |
MacApp/Sources/PerfowlCore/CollectorSupervisor.swift |
修改 | 旧prepare_app单次隐式计划 | 新能力协商与严格协议编码 | D2/D3 |
MacApp/Sources/PerfowlCore/PerformanceSessionStore.swift |
修改 | 停止临时重算/扫描若有 | 产品层增量哈希、序号及durable确认 | D7 |
MacApp/Sources/PerfowlCore/SessionReview.swift |
修改 | 只读旧报告假设 | 新报告小型入口与旧reader | D7 |
MacApp/Sources/PerfowlCore/SessionExporter.swift |
修改 | 显示集合过滤完整导出若有 | 按采集账本导出全部记录 | D7 |
MacApp/Sidecar/perfowl_collector_core_cli.py |
修改 | display决定launch_trace、停止直接调用offline finalize | 全量计划/可取消准备、live seal | D2/D3/D6 |
MacApp/Sidecar/perfowl_collector_core/capture_policy.py |
扩展 | startup/截图隐式UI门禁 | 全量请求、条件能力、源去重 | D2/D3 |
MacApp/Sidecar/perfowl_collector_core/source_preparation.py |
扩展 | 整体一刀切失效 | 分级缓存、generation、有界探测 | D3 |
MacApp/Sidecar/perfowl_collector_core/session_writer.py |
扩展 | 只推进Raw/Metric水位 | 统一Raw/Metric/报告/索引水位 | D4 |
MacApp/Sidecar/perfowl_collector_core/online_report.py |
新建 | 无 | 报告聚合主入口、checkpoint、snapshot | D4 |
MacApp/Sidecar/perfowl_collector_core/streaming_stats.py |
新建 | 无 | 稳定均值/方差、计数及join贡献 | D1/D4 |
MacApp/Sidecar/perfowl_collector_core/exact_order_index.py |
新建 | 停止全量排序 | 磁盘AVL与有界缓存 | D1 |
MacApp/Sidecar/perfowl_collector_core/metric_engine.py |
修改 | 第二次decoder派生 | 暴露一次派生的结果并继续原Metric写入 | D4/D5 |
MacApp/Sidecar/perfowl_collector_core/realtime.py |
修改 | 双路径重复分析风险、无共同stop边界 | 单次派生、共享deadline、最终barrier | D5/D6 |
MacApp/Sidecar/perfowl_collector_core/frame_derivation.py |
扩展 | 在线完成依赖offline全量samples | 增量帧与原离线对照双入口 | D5 |
MacApp/Sidecar/perfowl_collector_core/launch_trace.py |
扩展 | 最终必须重扫历史 | 终点冻结与最终质量投影 | D5 |
MacApp/Sidecar/perfowl_collector_core/launch_mapping.py |
扩展 | 在线交付重扫Raw审计 | 在线计数/指纹状态投影 | D5 |
MacApp/Sidecar/perfowl_collector_core/report_engine.py |
扩展 | 正常stop从完整frames构造series | 增量状态到原29字段投影 | D4 |
MacApp/Sidecar/perfowl_collector_core/self_calibration.py |
扩展 | 正常stop全历史检查 | 在线bucket投影 | D4 |
MacApp/Sidecar/perfowl_collector_core/live_seal.py |
新建 | 无 | writer-owned封存合同 | D6 |
MacApp/Sidecar/perfowl_collector_core/delivery.py |
扩展 | CLI正常stop调用offline入口 | 新live入口;旧独立审计保留 | D6 |
MacApp/Sidecar/perfowl_collector_core/persistence.py |
扩展 | 只文件级rename当完整多文件事务 | 文件同步、最后清单、目录同步结果 | D6 |
MacApp/Sidecar/perfowl_collector_core/checkpoint_policy.py |
扩展 | 新路径checkpoint默认关闭 | 单在途周期/字节阈值检查点 | D6 |
MacApp/Sidecar/perfowl_collector_core/metric_snapshot.py |
保留独立功能 | 正常stop无条件完整扫描 | 独立审计/旧会话读取 | D6 |
MacApp/Sidecar/perfowl_collector_core/recording.py |
保留并扩展 | 无需删除独立验证 | writer哈希导出、恢复完整验证 | D6 |
拟新增测试文件与模块一一对应:test_capture_plan.py、test_streaming_stats.py、test_exact_order_index.py、test_online_report.py、test_live_seal.py、test_stop_boundary.py、test_dual_seal.py;Swift侧CapturePlanTests.swift、AllMetricPreparationTests.swift、OnlineReportLifecycleTests.swift。基线已有test_capture_policy.py等测试只扩展,不覆盖删除。
附录C:开发者执行命令与输出位置
以下为批准实施后执行的命令示例,本方案阶段没有运行产品新测试或生成新产品包。现有测试数量会随新增测试变化,禁止硬编码旧通过数量。
cd /Users/itreenewbee/工作/代码/perfowl
git status --short
git rev-parse HEAD
MacApp/.sidecar-build/venv-py312/bin/python -m unittest discover \
-s MacApp/Sidecar/tests -p 'test_*.py' -v
cd /Users/itreenewbee/工作/代码/perfowl/MacApp
PERFOWL_DISABLE_SWIFTPM_SANDBOX=1 \
CLANG_MODULE_CACHE_PATH=/private/tmp/perfowl-clang-module-cache \
SWIFTPM_MODULECACHE_OVERRIDE=/private/tmp/perfowl-swiftpm-module-cache \
swift test --disable-sandbox
cd /Users/itreenewbee/工作/代码/perfowl/site
npm run build
拟新增基准脚本:MacApp/scripts/benchmark-online-report.py;先实现参数与JSON输出,再进行性能验收。
--mode baseline|candidate
--input INPUT_SESSION
--output OUTPUT_JSON
--rounds 20
--replay-speed realtime|unlimited|burst
--stop-at-event SEQUENCE
--fault none|disk-full|fsync-fail|disconnect|delayed-tail
基准文件必含:构建哈希、输入SHA-256、公式合同、round index、成功/失败状态、错误原因、accepted/durable序号、Raw/Metric/frame/截图字节数、逻辑读取量、CPU时间、RSS峰值、索引大小、队列字节峰值、快照lag、stop各阶段时长及最终报告哈希。
- 本机未匿名化明细放
internal/或系统临时目录,禁止直接公开。 - 公开验证摘要放新devlog与新HTML归档;先做身份/凭据扫描。
- 预期性能文件使用“target”,真实结果使用“measured”;两列禁止混合。
附录D:停止完成判定的最小布尔表达式
saved =
sourcesReachedDeclaredBoundary
AND rawWriterHealthy
AND metricWriterHealthy
AND reportWriterHealthy
AND rawSeq == metricConsumedRawSeq
AND rawSeq == reportConsumedRawSeq
AND requiredArtifactsDurable
AND coreSealPublished
AND productConsumedWireSeq >= coreSealWireSeq
AND productArtifactsDurable
AND productSealPublished
dataCompleteness=partial与saved=true可同时成立:表示已保存一份明确不完整的数据报告。只有上述持久化条件满足时才允许saved;尾部缺失单独呈现,不被保存成功遮蔽。
附录E:必须先通过的小型数值样例
这些样例是单元测试输入,不代表真机测量。
- FPS=[10,20,40,80]:median=30;[24,36]内0个;MedRange=0%。注意1000/median([100,50,25,12.5])≈26.6667,不等于30。
- frameTime=[10,10,10,1000]:n=4,k=1,1% Low=1FPS;合法长帧完整参与Smooth,禁止裁剪。
- value=[1,2,3]:mean=2,populationVariance=2/3,非1。
- value=[missing,0,10]且0合法:n=2,mean=5;missing不计入分母。
- phase有开始无结束:duration=null;下一场出现结束也不能补齐上一场。
- 源A最后timestamp=100、源B首timestamp=200:新epoch首点warmup,不生成100的frame interval。
- 同timestamp发送10、接收20,发送更新为15:pair最终35,不是65。
- Raw seq=100但Metric/Report最后只到99,即使最后一条无数值也拒绝成功seal。
- 先到旧应用A的app_ready,当前选择B:保持B状态;token不匹配事件不生效。
- 显示集合为空:请求指标仍64项,所有适用源继续记录;最终覆盖账本仍64行。
附录F:计划交接与整体进度
本任务书交付完成不代表产品改造完成。当前为E1代码核查与设计;D0–D8尚待批准后实施。已有旧版本测试和安装包仅为历史基线,本计划未生成新的测试包。
下一阶段:先批准任务书中的全量采集语义、精确索引与writer-seal完整性边界,再执行D0/D1;其余工作按任务卡推进。现有新包真机复验、即时性、完整报告性能、frame drain、全源丢包、原生UI以及正式发布门禁继续保持开放。