直接回答
先定义要测量的量,再选时钟。用户感知的耗时通常是从明确的用户操作边界到明确结果边界,包含期间发生的调度及休眠。设备活动 uptime 不包含深度休眠;CPU time 表示线程消耗的处理器时间,不是用户等待的间隔。若一个基准把三者都叫“duration”,数值可能看似合理却无法比较。记录指标含义、起止事件、单位、时钟 API,以及预期是否计入设备休眠。 在 Android 上,通用的耗时计时可使用 SystemClock.elapsedRealtimeNanos():它从开机计时、具有单调性,并包含深度休眠。若问题是实际经过多少设备时间且休眠也应计入,这是很合适的默认选择。SystemClock.uptimeMillis() 同样从开机计时且单调,但排除深度休眠,只回答更窄的活动时长问题。两个 API 的单位不同;应先在同一时钟域中求差,再进行必要的单位转换。不要把 uptime 读数与 elapsed realtime 读数相减,或称它们共享同一 epoch。 System.currentTimeMillis() 是墙上时间,适合人类可读时间戳或与外部日志对齐,却可能因校时或手动设置而改变。它不应作为基准耗时来源:时钟跳变会令区间异常变大、变成负数或产生误导。如果报告既需要事件日期又需要稳定耗时,应把墙上时间戳和单调时钟 duration 作为含义不同的字段保存。不要把负的墙上时间差静默“修复”为性能结果。 System.nanoTime() 的起点任意,通过相减适合在同一 Java 虚拟机实例中测量经过的区间。它的绝对读数不是日历时间,不应保存成可移植时间戳。Android 文档说明 nanoTime 以 uptimeMillis 为基准,因此不计深度休眠;elapsedRealtimeNanos 才是计入休眠的替代项。Java 文档说明其精度以纳秒表示,但不保证分辨率达到纳秒。nanoTime 的起止值应留在同一 JVM 实例;不要跨进程、设备、重启或独立时钟原点比较原始值。若测量跨进程,应选一个有文档定义的共享 Android 时钟,并确切记录所用 API。 线程 CPU time 是另一项指标,不是更快的墙上时钟。SystemClock.currentThreadTimeMillis() 报告当前线程消耗的 CPU 时间,可用于分析该线程计算量,但不包含等待,也不统计其他运行时线程或 native worker 执行的全部工作。本地推理可能把任务分发给其他线程,因此调用线程 CPU time 不等于推理总成本。仅当问题明确询问该线程的 CPU 消耗且范围适用时才使用。 事件边界属于数据契约的一部分。测模型加载时,要决定开始是接受请求、读取文件、初始化 runtime,还是稍后的就绪点。测推理时,应区分调用、首个输出、通过验收的终态结果和清理。选择时钟后,两个端点都用同一 API 读取,保留原始单位或清楚定义的换算,不要混合不同来源测得的区间。如果冷启动、温热任务和用户感知延迟回答不同问题,就分开记录。 休眠与后台场景能暴露错误假设。若操作可能跨屏幕关闭或进程切换,应测试受控休眠,并验证指标预期是否增长。包含休眠的 elapsed duration 应增长;uptime 与 nanoTime 区间有意不包含深度休眠。墙上时间校正不应改变单调区间。线程 CPU time 应反映计算而不是空闲等待,但仍受系统调度和测量噪声影响。这些是语义检查,不是性能承诺。 最后,不要跨开机周期比较单调时钟的原始读数。原始值只有在同一且有文档定义的时钟域与生命周期内求差才有意义。持久化时应保存 duration、单位和来源;需要时间顺序时另存墙上时间戳。只有测试 harness 已提供可信会话标签时,才在同一启动会话内对观察值分组;不得把比较延续到不同启动周期,也不要仅为比较互不相关的时钟原点而发明或收集设备启动标识。应拒绝跨启动周期相减。本文是通用 Android 测量流程,不包含 Cove 的实测延迟、CPU 使用、热影响或模型速度结论。
屏幕关闭后,两份延迟报告出现分歧
一份基准用 uptimeMillis 记录推理起止,另一份使用墙上时间戳。一次运行期间设备进入休眠,另一次运行期间系统校正时钟,因此两者看起来互相矛盾。
决策
先定义指标是包含休眠的用户耗时、活动 uptime,还是线程 CPU 消耗,再从同一且有文档定义的单调时钟域读取两个端点。
预期结果
团队可以解释差异,把日期与耗时分开保存,并仅比较边界、单位及时钟语义兼容的运行。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- Android SystemClock 文档说明 elapsedRealtimeNanos 包含深度休眠;uptimeMillis 与 System.nanoTime 使用排除深度休眠的 uptime 基准。
- Java System.nanoTime 文档说明只有同一 JVM 实例内的差值有意义,并区分纳秒精度与时钟分辨率。
发布结论只对所记录的配置和验收集有效;扩大适用范围需要更多记录。
实施流程
- 命名指标与事件边界. 决定数字表示用户感知耗时、活动 uptime 还是 CPU 时间。明确加载、首个输出、终态结果及清理的准确起止事件,不使用含混的“推理耗时”。
- 选择语义匹配的 Android 时钟. 需要包含深度休眠的单调耗时用 elapsedRealtimeNanos;只有明确排除休眠时用 uptimeMillis;线程 CPU time 只测该线程计算。日历关联另用墙上时间。
- 在相同域采集端点. 从相同 API 读取开始和结束并保留来源、单位。不得把 elapsed realtime、uptime、墙上时间和任意原点的 nanoTime 读数混合相减。
- 验证休眠和墙上时钟变化. 在可行时用受控深度休眠区间及墙上时钟校正测试计量契约。确认语义符合预期,不把此测试本身当成延迟结果。
- 明确进程与开机边界. 不要跨设备或重启比较单调时钟原始值。若测量跨进程,应使用有文档定义的共享 Android 时钟并保留上下文;否则让端点留在同一进程。
- 只分析兼容记录. 保存耗时、单位、时钟 API、事件定义、进程或启动分组、工作负载和结果。墙上时间戳另存,拒绝不兼容端点,并区分任务延迟和 CPU 消耗。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- 每个耗时都注明预期含义、准确事件边界、时钟 API 和单位。
- 两个端点来自同一时钟域;不跨墙上时间、uptime、elapsed realtime、nanoTime 原点、缺少共享契约的进程或开机周期求差。
- 报告分别描述包含休眠的耗时、活动 uptime、日历时间戳和线程 CPU 时间,不把它们合并为一个指标。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| elapsedRealtimeNanos | 深度休眠应计入时,测量单调耗时。 | 开机以来的读数包含深度休眠;两个端点都用此来源并明确纳秒单位。 |
| uptimeMillis | 仅在指标有意排除深度休眠时使用。 | 单调 uptime 在深度休眠期间不会增长,不能替代包含休眠的用户耗时。 |
| currentTimeMillis | 用于人类可读墙上时间戳或外部日志关联。 | 墙上时钟可能被校正或设置,不是稳定的耗时来源。 |
| currentThreadTimeMillis / nanoTime | 线程 CPU time 测该线程消耗;nanoTime 只在同一 JVM 实例内求差。 | 两者都不是包含休眠的用户耗时替代品;nanoTime 有纳秒精度但不保证纳秒分辨率,原始值也不是可移植时间戳。 |
需要避免的失败模式
- currentTimeMillis 是可被校正的墙上时间,不是稳定的耗时计时器。
- 单个调用线程 CPU 时间不代表用户等待时长,也不包含其他线程的全部工作。
- 单调时钟的原始值不能跨设备或重启周期比较;本文不提供 Cove 性能数据。
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
测用户可见的推理延迟应该用哪个时钟?
先定义边界。如果指标是包含设备休眠的已过时间,Android 文档将 elapsedRealtimeNanos 说明为适用的单调选项。两个端点都用一个来源,并把 CPU 时间单独记录。
System.nanoTime 可以保存后在进程重启时比较吗?
不要把任意原点读数当作持久时间戳。Java 只保证同一 JVM 实例内差值有意义;跨进程 Android 计时应选择共享且有文档定义的时钟,不要假设原点必然一致。
为什么 currentThreadTimeMillis 小于任务延迟?
它测当前线程的 CPU 时间,不是用户等待时间。等待、调度、休眠以及其他线程或 native worker 的工作不包含在单线程读数中。