| 场景 | 目标 |
|---|---|
| App 退到后台 → 挂起(保存完成) | < 2 s(iOS 后台时间有限) |
| App 被杀后再点开 → 可交互桌面 | macOS HVF < 3 s;iOS 模拟器 < 5 s |
| 后台未被杀、回到前台 | 立即(cont) |
| 首次启动 / 快照无效 | 冷启动(现状 70–190 s),开机完成后立刻建一次快照 |
- 普通
migrate file:(mapped-ram)保存整机:19.8 s,写出 1.88 GB 非零页 → 太慢,不能用作后台挂起。 - 没有任何设备阻止迁移(无 blocker)。
-incoming恢复时崩溃:goldfish_address_space.c:607 get_state → host_block_new → memory_region_init_ram_ptr → ram_block_notify_add → bdrv_register_buf → assert(!qemu_in_coroutine())(加载在迁移协程里跑,不能在其中新建 RAM 块)。- 方案验证:改用文件内存 +
cpr-reboot+x-ignore-shared后,挂起只用 218 ms(downtime 218 ms),state.bin94 MB(仍有约 96 MB 内存被拷贝,多半是 address_space 的宿主内存块),ram.img2 GB 不再拷贝。 - 图形 pipe 的 save/load 目前是 NULL:恢复后 SurfaceFlinger 的 GL 连接会断 → 必须做渲染器快照。
客户机内存用共享的文件后端,内存本身就是文件,挂起时不用拷贝内存:
-object memory-backend-file,id=ram0,size=2G,mem-path=<snap>/ram.img,share=on -machine memory-backend=ram0 -m 2048
- 挂起(QMP):
stop→migrate-set-parameters {mode: cpr-reboot}→migrate-set-capabilities [{capability: x-ignore-shared, state: true}]→migrate {uri: file:<snap>/state.tmp}→ 等到postmigrate→ 改名为state.bin→ 写manifest.json(clean: true)。 内存页已在文件里(MAP_SHARED),由内核回写;state.bin只有设备状态 + 渲染器状态(MB 级)。 - 恢复:同样的 argv 加
-incoming defer,然后设置同样的 mode/capability →migrate-incoming {uri: file:<snap>/state.bin}→ 迁移完成后 QEMU 自动继续运行。在cont/恢复运行之前先把 manifest 改成clean: false(运行中内存文件和磁盘都会变,快照随即失效)。 - 回到前台但进程还在:直接
cont,同样先置clean: false。 - 被杀时正在运行(manifest 不是 clean):丢弃快照,冷启动(相当于断电,ext4 日志会恢复)。
- 磁盘:保持 raw;CPR reboot 要求挂起后到恢复前不改磁盘内容,满足。挂起前磁盘已 flush(
stop会 drain/flush)。 - 兼容性:manifest 记录引擎/渲染器/镜像版本哈希和完整 argv 摘要;任一不同就作废,冷启动。
| 文件 | 内容 |
|---|---|
ram.img |
2 GB 稀疏文件,客户机内存本体 |
state.bin |
QEMU 迁移流:CPU、设备、pipe、va-gfx 段 |
gfx/ |
渲染器大块数据(color buffer/纹理),由 va_gfx_snapshot_save 写入 |
manifest.json |
{version, engine, gfx, image, argv_hash, clean, saved_at} |
last-frame.png |
挂起时最后一帧,App 冷启动时先显示它(视觉上"秒开") |
引擎(hw/android,board)
goldfish_address_space加载不在协程里建 RAM:先读进缓冲,post_load/BH 里建 MR,或预分配。- 新增
va-gfx迁移段(优先级高于 goldfish_pipe),save/load 时调用va_gfx_snapshot_save/load, io 绑到 QEMUFile,aux_dir 来自机器属性snapshot-dir=(gfx 子目录)。 - 逐个验证 hw/android 与 services 的设备状态往返:pipe(含 qemud)、sync(时间线与未完成 fence 要和渲染器一致)、 battery、events、address_space(只存已分配块)、va-vsock(恢复后 TRANSPORT_RESET,adbd 重连)。
- 恢复后客户机墙上时间要校正(查官方模拟器 quickboot 怎么做)。
tools/run-macos.sh增加--snapshot(用文件内存 + 快照目录)、tools/snapshot.py suspend|restore。- macOS HVF 实测:挂起耗时、恢复到可交互耗时;恢复后 adb 可用、桌面立即重绘、打开应用、2 分钟无 watchdog;连续 3 轮。
渲染器(gfx):实现 va_gfx_snapshot_save/load(gfx/include/va_gfx.h)——gfxstream
RendererImpl::save/load + aemu TextureSaver/TextureLoader,图形 pipe(opengles/GLProcessPipe/refcount)的
AndroidPipeFuncs.save/load,加载后重新 present 最后一帧;测试程序里跨进程 save→load 后继续渲染、像素一致。
App:生命周期——后台挂起(beginBackgroundTask)、前台 cont、冷启动时有效快照就 -incoming,否则冷启动;
首次开机完成后立即建快照;启动时先显示 last-frame.png;版本不符自动作废。模拟器实测启动到可交互耗时。
用法(macOS):tools/run-macos.sh --machine ranchu --snapshot --name N --adb-port P
- 如果有干净的快照,就自动
-incoming defer,再由tools/snapshot.py restore恢复,日志在engine/run/N/restore.log。 - 否则丢弃快照并冷启动。
- 挂起:
python3 -I tools/snapshot.py suspend --run engine/run/N [--quit]。 - 另有
status、check、resume、discard子命令。 - QMP 固定在
engine/run/N/qmp.sock。
快照目录:snapshot/ 下的文件:
ram.img:客户机内存。as.img:goldfish_address_space 的宿主内存块,8 GiB 稀疏文件,实际只占已分配的块。gfx/:渲染器的大块数据。state.bin、manifest.json。launch.json:run-macos.sh 写入,记录 qemu、渲染器和镜像文件在启动时的身份(大小+修改时间),以及冷启动用的 argv。last-frame.png。
manifest:
- 有
engine、gfx、image、argv_hash、clean、saved_at、state_bytes、suspend_ms字段。 - 只要有一项和本次启动不一致,快照就作废;实测替换 qemu 二进制后,快照被判为 "engine changed"。
clean只在挂起完成到客户机再次运行之间为 true;restore 和 resume 在cont之前先把它置为 false。
机器属性:-machine ranchu,snapshot-dir=DIR[,time-sync=on|off]
迁移段与恢复规则:
va-gfx段的优先级高于 pipe 和 goldfish_sync。段内格式是"魔数 + 长度 + 渲染器写入的字节",渲染器读回的字节数必须和写入的完全一致。- 加载的 libva_gfx 没有
va_gfx_snapshot_*,或者没设snapshot-dir,都会注册迁移 blocker,不会产出缺 GPU 状态的快照。 VA_GFX_SNAPSHOT_STUB=1是临时开关:写一个空的 va-gfx 段,恢复后图形 pipe 被强制关闭。goldfish_address_space:加载时只建块列表,内存区在 VM 开始运行时才创建(迁移协程里不能新建 RAM 块)。cpr-reboot 模式下块数据留在 as.img 里,不进流。- vsock:pipe-port 连接(剪贴板等)随服务状态一起保存和恢复;宿主 TCP 连接(adb)在恢复后逐个发 RST,不再发 TRANSPORT_RESET。
- 平台事件:VM 停着的时候,按键和触摸直接丢弃;电池、传感器、定位、剪贴板事件排队,等 VM 运行后再处理。
- 恢复后在新进程里重新调用平台回调:
sensor_set_enabled、location_set_enabled,正在取流的相机会调camera_start。 - boot-completed 状态存在
va-host段里,恢复后会再报一次VA_GUEST_BOOT_COMPLETED。
客户机时间:
- 官方模拟器在 quickboot 加载后,通过 modem 模拟器发一条 NITZ 来校时。
- 我们目前在恢复后(或暂停 ≥1 s 后)向引擎自有的控制台输入
su 0 toybox date -u @<now>(hw/android/guest_time.c)。 - 前提:serial 0 必须是 ringbuf chardev,并且能探测到 shell 提示符,否则跳过。
- App 要传
-chardev ringbuf,id=vaconsole,size=65536 -serial chardev:vaconsole。 - 以后应改成一个很小的 NITZ 式或专用时间 pipe 服务,不再借用控制台。
实测(macOS HVF,M2 Max,ranchu,VA_GFX_SNAPSHOT_STUB=1,即不含 GPU 状态):
| 项目 | 结果 |
|---|---|
冷启动到 sys.boot_completed=1 |
8–15 s |
| 挂起(stop + migrate,cpr-reboot) | 31–103 ms,state.bin 0.7 MB(原先 94 MB 里多出来的那部分是 address_space 块数据,已移到 as.img) |
| 恢复:进程启动到 QMP 可用 | 200–270 ms |
| 恢复:加载 + 继续运行 | 21–49 ms(启动后 228–318 ms 客户机就在跑) |
恢复:adb getprop sys.boot_completed=1 |
启动后 441 ms |
| 连续 3 轮挂起/恢复 | 每轮都恢复成功;pipe 服务恢复情况:qemud 全部 restored=1,QemuMiscPipe restored=1,图形 pipe 因为是 stub 所以 restored=0 |
| 恢复后 2 分钟 | 没有 watchdog;am start Settings 返回 ok,但它的 RenderThread 因 "no surface" abort(stub 没有 GPU 状态,属预期) |
| 运行中被杀 | 快照被拒(not clean),冷启动,ext4 日志恢复正常 |
| 客户机时间 | 恢复后客户机 date +%s 和宿主一致 |
实测(真实 GPU 状态:libva_gfx 47fe495 + 引擎 0e17cd59b,不开 stub):
ranchu 必须把 pipe_stream_put_buffer/get_buffer 传给 libva_gfx(0e17cd59b)。否则图形 pipe 恢复后被关闭,客户机报 EGL_BAD_ALLOC,launcher 崩溃循环。
| 项目 | 主机负载低时 | 主机满载时(load 310,同时有其他 agent 的 VM 和编译) |
|---|---|---|
| 挂起 | 317–458 ms(va-gfx 段 277 ms);state.bin 2.8 MB,gfx/ 9–13 MB | 0.6–2.1 s;打开 Settings 后 state.bin 13 MB |
| 恢复:客户机开始运行 | 启动后 757–842 ms(va-gfx 加载约 0.5 s) | 1.3–4.1 s |
| 恢复:第一帧新画面 | 启动后 783–871 ms | 1.3–4.6 s |
| 恢复:adb boot_completed=1 | 启动后 879–932 ms | 1.7–8.2 s |
- 恢复后的桌面正常,时钟正确。所有图形 pipe 都恢复成功(restored=1)。
- 打开 Settings 返回 ok,画面正常。之后 2 分钟:watchdog 0 次、native crash 0 次、EGL_BAD_ALLOC 0 次。
- 连续 3 轮"挂起 → 退出 → 恢复 → HOME → 打开 Settings"都通过:每轮 fatal=0、badalloc=0、watchdog=0。
待办:
- 在主机空闲时复测上面的耗时(目标 < 3 s 已经达到;满载时的波动主要是 CPU 竞争)。
- 剪贴板 pipe 没有 save/load,恢复后会重连。