性能放行复核首帧、滚动与后台任务的证据门槛一次版本回归里首屏看起来正常长列表偶尔掉帧后台同步却只在部分设备上完成。若发布评审只看一张平均耗时截图很容易把“没测到”当成“没有问题”。更稳的做法是把性能结论拆成可重复的场景、明确的预算、可追溯的样本和机器可读的放行结果。本文给出一个项目侧性能发布门禁的建模方法分别管理启动首帧、列表滚动和后台任务校验采样数据、构建版本、工作负载、设备覆盖与证据时效缺少关键证据时返回“暂停”而不是猜测通过。适用版本HarmonyOS API 26。本文讨论的是应用项目可以自行实现的预算与证据模型不代表 HarmonyOS 内置的性能门禁 API也不声称 APMS 或 Profiler 已在本文中运行。示例阈值仅用于说明配置结构必须由项目在目标设备上测量后制定。1. 发布评审为什么不能只看一个平均值平均值会掩盖长尾。比如一次冷启动数据很快但少数启动因图片解码或同步初始化明显变慢列表滚动的整体平均帧间隔正常却在快速滑动时出现连续卡顿后台任务的平均耗时较短但部分设备上从未完成。把这些结果合成一个总分会丢失“谁、在什么条件下、实际完成了什么”的上下文。发布门禁应回答三个问题一是场景是否按约定执行二是样本是否来自当前构建且数量足够三是出现超预算或证据缺失时谁有权放行。门禁只消费已经采集并归档的数据不应在 UI 渲染路径里临时跑重型统计也不应伪装成平台级诊断工具。图中的处理顺序是先固定场景和预算再采样再检查证据最后比较门槛。任一步缺失都进入暂停态工程师可以补测后重新评审。2. 先划清 API 26 与项目策略的边界版本声明只说明本文示例面向 HarmonyOS API 26 项目不等于其中的阈值、数据结构或门禁流程是系统 API。首帧时间如何取点、滚动数据由什么工具采集、后台任务如何调度必须结合项目实际接入的官方能力和目标设备确认。本文把这些外部测量结果抽象成普通数据再演示如何做项目内的判断。后台工作尤其不能为了“让测试通过”而改成无限定时器或绕过系统调度。任务类型、可运行时机、权限和生命周期处理要依照项目真实场景及对应官方文档实现。本例只校验任务的结果记录不负责启动系统任务。结构图将设备场景、采样器、预算配置、证据校验、门禁判定和发布报告分开。各模块通过只读快照传递数据避免采样代码直接改写发布结论。3. 把三类性能目标写成可审查预算预算至少要绑定场景、指标单位、统计口径、样本数、设备范围和证据有效期。首帧通常关注启动阶段的耗时分位数滚动应固定数据集与手势脚本观察帧间隔或项目采用的卡顿指标后台任务则要区分成功、失败、取消和未运行。三个场景不能用同一个“性能正常”布尔值代替。场景建议记录放行比较缺证据时首帧冷/暖启动类别、构建号、首帧取点、设备档位、样本集项目约定的 P95 上限暂停不用暖启动代替冷启动长列表滚动数据集版本、列表长度、操作脚本、帧间隔样本项目约定的 P95 或卡顿率上限暂停不能把未触发的路径计为通过后台任务任务类型、触发条件、耗时、结束状态、生命周期上下文完成率及耗时预算暂停未运行不等于成功以下预算值只是配置演示首帧 900ms、滚动帧间隔 24ms、后台任务 120000ms。它们不是 HarmonyOS 默认指标也不是实测结果。正式项目要记录基线来源、设备集合、应用版本、采样次数和变更审批人门槛变更应与代码或发布版本一起审查不能为了让单次结果变绿而临时调高。export type Scenario first-frame | list-scroll | background-task export interface BudgetPolicy { scenario: Scenario metricName: string maxP95Ms: number minSamples: number maxEvidenceAgeMs: number } export const budgetPolicies: readonly BudgetPolicy[] [ { scenario: first-frame, metricName: firstFrameMs, maxP95Ms: 900, minSamples: 20, maxEvidenceAgeMs: 7 * 24 * 60 * 60 * 1000 }, { scenario: list-scroll, metricName: frameIntervalMs, maxP95Ms: 24, minSamples: 100, maxEvidenceAgeMs: 7 * 24 * 60 * 60 * 1000 }, { scenario: background-task, metricName: durationMs, maxP95Ms: 120_000, minSamples: 10, maxEvidenceAgeMs: 7 * 24 * 60 * 60 * 1000 } ]策略表把不同指标的单位统一为毫秒便于同一比较器复用minSamples和有效期仍按场景分别设置。代码里的数字仅为了展示字段关系不能复制后当作通用验收线。4. 采样快照必须能说明“测了什么”单个耗时没有意义除非能回答它属于哪个构建、哪个场景和哪次工作负载。设备标识宜使用项目内部脱敏 ID不写账号、手机号、完整路径或用户输入。原始性能文件可能包含额外环境信息访问控制、保留期限和导出规则应放在项目隐私与测试规范中管理。export type DeviceClass phone | foldable | tablet export type TaskOutcome completed | failed | cancelled | not-run export interface PerformanceSnapshot { buildId: string scenario: Scenario deviceClass: DeviceClass deviceIdHash: string workloadId: string samplesMs: readonly number[] observedAt: number source: string artifactRef: string taskOutcome?: TaskOutcome }快照是采集层与判定层之间的契约。source应描述真实采集来源artifactRef指向受控存储中的证据不直接把大型原始文件塞进构建日志。若工具尚未接通来源应写明not-run或在报告中标为未执行而不是填造一个看似真实的 Profiler 文件名。采样前还要固定工作负载。例如首帧至少区分冷启动与暖启动列表测试约定数据量、图片状态、滚动距离和速度后台任务则固定触发条件、前后台切换动作和预期结果。工作负载 ID 变化后旧数据不能悄悄并入新基线。5. 分位数计算要处理空值、异常值和边界统计函数应是纯函数先拒绝非法参数再复制并排序样本避免改动调用方持有的数组。小样本虽然可以算出 P95但不等于统计结论可靠因此样本量门槛必须在判定层校验。export function nearestRank(values: readonly number[], percentile: number): number | undefined { if (values.length 0 || percentile 0 || percentile 1) return undefined if (values.some(value !Number.isFinite(value) || value 0)) return undefined const sorted [...values].sort((left, right) left - right) const rank Math.ceil(percentile * sorted.length) return sorted[Math.max(0, rank - 1)] }这是 nearest-rank 分位数定义的一种明确实现。项目如果采用插值分位数或其他统计口径应把口径记录在预算版本里并保证本地测试、CI 汇总和报告页面使用同一实现不能由不同脚本各算一遍后拿有利结果放行。入口校验可以单独封装避免NaN、负耗时或空来源绕过上限比较export function validSnapshot(snapshot: PerformanceSnapshot): boolean { return snapshot.buildId.length 0 snapshot.workloadId.length 0 snapshot.deviceIdHash.length 0 snapshot.source.length 0 snapshot.artifactRef.length 0 snapshot.observedAt 0 snapshot.samplesMs.length 0 snapshot.samplesMs.every(value Number.isFinite(value) value 0) }如果校验失败结果应该包含字段级原因例如invalid-sample或artifact-missing。把坏样本直接过滤掉会让样本数看起来更漂亮却隐藏采集质量问题建议报告同时记录拒绝样本数量。6. 证据时效和设备覆盖也是门槛发布评审常见的误区是拿“上次跑过”的截图代替本次版本证据。至少检查构建号一致、采集时间不在未来、证据未过期、场景覆盖完整以及声明支持的设备类别都有样本。设备档位的划分由项目维护不应根据一台设备的结果推断所有终端。export function evidenceProblems( policy: BudgetPolicy, snapshots: readonly PerformanceSnapshot[], expectedBuildId: string, now: number, requiredDevices: readonly DeviceClass[] ): string[] { const problems: string[] [] const selected snapshots.filter(item item.scenario policy.scenario) if (selected.length 0) return [scenario-missing] if (selected.some(item item.buildId ! expectedBuildId)) problems.push(build-mismatch) if (selected.some(item !validSnapshot(item))) problems.push(invalid-snapshot) if (selected.some(item now - item.observedAt 0 || now - item.observedAt policy.maxEvidenceAgeMs)) problems.push(evidence-stale) const sampleCount selected.reduce((sum, item) sum item.samplesMs.length, 0) if (sampleCount policy.minSamples) problems.push(insufficient-samples) if (requiredDevices.some(device !selected.some(item item.deviceClass device))) problems.push(device-coverage-missing) return problems }这里将设备覆盖与样本数量分开检查。20 次都来自一台手机仍可能不满足项目声明的折叠屏和平板范围。若设备类型不在支持列表中应由项目策略决定跳过、降级还是阻断并把原因写入报告不能静默丢弃。7. 判定结果只能是放行或暂停并附带原因门禁的默认态应是hold。仅当采样有效、证据新鲜、样本量足够、设备覆盖满足而且统计值未超预算时才pass。后台任务场景还要检查最终状态任务没运行、被系统取消或失败都不能被解释为“耗时为零”。export interface GateResult { decision: pass | hold scenario: Scenario p95Ms?: number reasons: readonly string[] } export function evaluateScenario( policy: BudgetPolicy, snapshots: readonly PerformanceSnapshot[], expectedBuildId: string, now: number, requiredDevices: readonly DeviceClass[] ): GateResult { const problems evidenceProblems(policy, snapshots, expectedBuildId, now, requiredDevices) const selected snapshots.filter(item item.scenario policy.scenario) if (policy.scenario background-task selected.some(item item.taskOutcome ! completed)) { problems.push(background-task-not-completed) } const values selected.flatMap(item item.samplesMs) const p95Ms nearestRank(values, 0.95) if (p95Ms undefined) problems.push(p95-unavailable) else if (p95Ms policy.maxP95Ms) problems.push(budget-exceeded) return { decision: problems.length 0 ? pass : hold, scenario: policy.scenario, p95Ms, reasons: problems } }判定函数不应直接触发重试、清缓存或修改系统配置它只返回结构化结果。恢复动作属于另一条经过授权的流程对于高风险发布超预算或缺证据时先暂停并交给负责人复核比自动“修正”阈值更安全。8. 汇总发布报告时保留可复核链路汇总器应逐场景计算任何必需场景不是pass整体状态就不能放行。报告里至少保存构建 ID、预算版本、工作负载版本、设备覆盖、样本数量、P95、证据引用、判定原因和复核人。每次重跑产生新记录不要覆盖前一次快照否则问题出现前后的差异无法比较。export interface ReleaseGateReport { buildId: string policyVersion: string generatedAt: number overall: pass | hold scenarios: readonly GateResult[] } export function overallDecision( results: readonly GateResult[], requiredScenarios: readonly Scenario[] ): pass | hold { if (requiredScenarios.length 0 || results.length ! requiredScenarios.length) return hold const required new Set(requiredScenarios) const decisions new Map(results.map(result [result.scenario, result.decision])) if (required.size ! requiredScenarios.length || decisions.size ! results.length) return hold return requiredScenarios.every(scenario decisions.get(scenario) pass) ? pass : hold }汇总函数必须接收发布配置中的必需场景集合并要求结果数量、场景唯一性和集合内容完全匹配。这样只有首帧一个pass时不会误放行滚动与后台任务缺项、重复项、额外项和任一hold都会暂停。若场景确实不适用于某个版本应在策略配置里显式登记例外与批准人而不是从本次结果中悄悄删除。日志只写必要摘要与受控引用原始设备数据按最小权限访问并设定清理周期。9. ArkUI 与后台任务验证要回到真实运行环境项目内纯函数测试只能证明比较器对给定输入的行为不能证明启动取点正确、列表在目标设备不掉帧也不能证明后台任务符合系统调度规则。启动测量应区分冷暖路径并记录构建与设备列表压测应固定数据与操作脚本同时观察状态更新和图片解码等负载后台任务要覆盖前后台切换、取消、失败和恢复并遵守项目采用的官方任务类别与权限约束。对 UI 代码还需检查计算是否误放在重复渲染路径、长列表是否进行不必要的全量更新、异步结果是否会在页面离开后回写旧状态。性能优化不应靠任意延时掩盖卡顿也不应新增无界轮询。验证结果应分成passed、failed、not-run三种没有真机或工具证据时明确写not-run。10. 验收清单与常见暂停原因本地评审可以先用纯数据测试覆盖阈值边界、空数组、非法数值、构建号不一致、过期证据、设备缺失和后台任务失败。示意断言如下需接入项目实际测试框架后执行它本身不是设备性能验证assert.equal(nearestRank([10, 20, 30, 40], 0.95), 40) assert.equal(nearestRank([], 0.95), undefined) const requiredScenarios [first-frame, list-scroll, background-task] as const assert.equal(overallDecision([], requiredScenarios), hold) assert.equal(overallDecision([{ decision: pass, scenario: first-frame, reasons: [] }], requiredScenarios), hold) assert.equal(overallDecision(requiredScenarios.map(scenario ({ decision: pass, scenario, reasons: [] })), requiredScenarios), pass)落地时还要给报告补上预算版本与必需场景集合避免只传入一个通过项就代表全量通过。验收前可以按下表排查现象优先检查处理方式结果异常快样本是否为空、是否被过滤、设备时间是否错误保留拒绝原因重新采集不把无效值视作 0有 P95 却仍不应放行样本数、构建号、设备类别和证据时效返回具体 hold 原因补齐对应场景后台任务显示 0ms任务是否实际运行、结束状态是否完成未执行写not-run取消和失败分开记录换版本后历史图表还通过快照是否绑定当前 buildId 与 workloadId将新版本作为独立记录重新执行目标场景只测过一类设备目标设备清单是否与覆盖数据一致补测缺失设备不支持时明确记录降级范围提交前逐项确认首帧、列表滚动、后台任务分别有预算、样本口径和负责人。所有快照绑定当前构建、设备类别、工作负载和采集时间。样本量不足、证据过期、工具未运行或任务未完成均返回hold。P95 算法只有一份实现边界条件已有纯函数测试。预算变更保留审批与历史记录旧证据不会被覆盖成新版本结论。报告只包含必要的脱敏信息原始文件有访问控制和保留期限。