文章给出可复现的测量流程——在真实设备、真实网络上测量延迟、能耗和故障模式,而非发布绝对数字;含决策对照表。
几周前,我为 React Native 应用中的一个摘要功能勾画了三种部署选项:在设备上运行一个小模型、调用托管模型 API,或者在我控制的一台廉价服务器上部署一个小模型。每个选项都有听起来合理的论点。但没有一个论点经得住移动端真正重要问题的检验:一台真实手机、在真实网络上,能量消耗、延迟和故障模式究竟如何?
这篇文章是我为诚实回答这个问题而构建的测量工具。它是一种可复现的方法,而不是基准测试报告——我故意不在这里发布数字,因为我的测试设备上的数字对你的设备类别、你的模型和你的网络毫无用处。能迁移过去的是流程、仪表化方案,以及末尾的决策表。
三种部署方式,作为移动端约束的框架
第三行是免费层对原型设计有意义的所在。披露:本文是 MonkeyCode 产品推广的一部分。MonkeyCode 目前提供免费模型访问和免费服务器选项,这让在测量阶段不花钱就能实际部署第二行和第三行成为可能——API 行用托管模型调用,自托管行用免费服务器托管一个小型开源模型(甚至只是一个模拟端点)。将两者都视为原型基础设施,而非生产承诺:在围绕它们进行设计之前,自己核实当前的可用性和限制,不要假设任何免费层是永久的。
实验设计
一台设备,一个任务,三种部署方式,四种生命周期条件。任务必须在不同部署方式中保持一致——相同的提示、相同的预期输出类别——这样唯一的变量就是执行发生在哪里。
设备:一台实体手机(我用的是一款中端 Android;记录下你的具体型号、SoC 和 OS 构建版本)
电池:每次运行开始时电量为 80%,断开电源,屏幕亮度固定
网络:第一轮在稳定 Wi-Fi 下,第二轮在蜂窝网络下,且运行中途有 Wi-Fi → LTE 切换
应用状态:前台作为基准;然后在任务中途将应用切入后台 10 秒后重复,以及在低功耗模式启用后再跑一次
生命周期矩阵(刻意保持精简):
前台,稳定 Wi-Fi——你的基准
前台,请求中途 Wi-Fi → LTE 切换
任务中途切入后台 10 秒后恢复
启用低功耗模式
四个条件足以暴露有趣的差异。如果某种部署方式通过了这些,可以从那里扩展。
仪表化:Android 上的能耗
使用 batterystats 作为粗略但可重复的能耗对比工具。重置后,运行一种部署方式 N 次,然后导出:
adb shell dumpsys batterystats --reset
# run the task 20 times from the app, same prompt each time
adb shell dumpsys batterystats > placement_ondevice.txt
adb bugreport bugreport_placement_ondevice.zip # for Battery Historian
记录每种部署方式的数据:归属到你应用 UID 的预估功耗(mAh)、移动无线电活跃时间,以及 Wi-Fi 无线电活跃时间。无线电活跃时间这一行是设备端选项通常胜出的地方,也是两种网络选项因重试行为而拉开差距的地方。在 iOS 上,使用 Xcode Instruments 的 Energy Log,协议相同(固定亮度、固定电量),并通过 Network instrument 捕获网络流量。
注意:batterystats 的能耗归因是一个估算值。它足以在同设备上的不同部署方式之间进行相对比较,但不适合发布绝对数字。
仪表化:能在后台存活下测量的延迟
当应用被切入后台时,墙上时钟会撒谎——进程可能被挂起,而你的定时器在概念上仍在运行。用单调递增的方式标记事件并持久化,这样即使进程被杀也会留下痕迹:
data class TaskTrace(
val taskId: String,
val placement: String, // "ondevice" | "api" | "server"
val startedMs: Long, // SystemClock.elapsedRealtime()
var networkSentMs: Long? = null,
var responseReceivedMs: Long? = null,
var finishedMs: Long? = null,
var interruptedBy: String? = null, // "backgrounded" | "network_switch" | "process_death"
var outcome: String? = null // "completed" | "restarted" | "lost"
)
// Persist each mutation immediately (Room/DataStore), not in memory.
每次尝试记录一行追踪数据,并在每个条件结束时计算每种部署方式的数据:p50/p95 端到端延迟,以及——更重要的是——结果分布:多少任务完成了、重新启动了,或者静默消失了。如果一种部署方式很快,但在后台时丢失 30% 的任务,它就会输给一种总是完成但较慢的部署方式。
如何审视结果
设备端行:延迟在连续 20 次运行中是否有衰减?这是热节流的表现,这意味着你单次运行的演示具有欺骗性。检查低功耗模式是否将性能限制到足以破坏你的用户体验。
API 行:将无线电活跃时间与 payload 大小进行对比。如果你在发送大量上下文,蜂窝网络上的上传成本可能占据主导。
服务器行:如果你的免费服务器进入休眠或重启,空闲后第一个请求会暴露这一点。这对原型设计没问题——但要明确测量冷启动惩罚,并决定保持活跃ping的电池成本是否值得。(如果你在这里使用 MonkeyCode 免费服务器,这个冷启动测量就是在依赖它之前应该跑的东西。)
所有行,条件 2:请求中途的 Wi-Fi → LTE 切换。你的 HTTP 客户端是透明重试,还是用户看到死 spinner?可复现的 kill-switch 测试对此有帮助——让你的 API/服务器行指向一个你控制的端点(这就是拥有自己的免费服务器真正有用的地方:你可以随时杀死它)。
局限性和谁应该跳过这个
单设备结果无法跨设备类别泛化。在确定之前,至少在一台低端设备上重复测试。
batterystats 归因是近似的;只将它作为相对信号。
免费层(包括 MonkeyCode 的免费模型和免费服务器)是原型设计工具。不要在关于免费层持久性、配额或 SLA 的假设上运送生产流量,我没有在这里声明那些,因为我无法替你核实它们。
如果你的功能需要保证离线行为,网络行按定义就应该被排除——直接跳到设备端。
如果你无法在客户端持久化任务状态,没有任何部署方式能把你从后台切换的损失中拯救出来;先解决这个问题。
如果你运行了这个工具,我想对比一下笔记:你用的设备和 OS 构建版本、使用的生命周期转换,以及被中断的任务是恢复了、重新启动了,还是静默消失了。那个结果分布是我在任何单一设置中最不信任的数字——包括我自己的。