Skip to content

[iOS][功耗] 高频 NEProvider sleep 无条件执行 FreeOSMemory,短周期 sleep/wake 下反复强制 GC #6

Description

@wangwei354

环境

  • 客户端测试版本:1.0.4
  • 平台:iOS
  • 场景:VPN 保持连接,设备夜间闲置
  • 观测日期:2026-09-09(北京时间,UTC+8)

本次没有确认 1.0.4 安装包与当前公开源码的精确提交映射。下方源码链接用于说明与设备日志吻合的当前实现路径:Hako 35c9bb675f1cf5b0befd4290809d06dead728768,Hako-Client b05832246fcac69d13ff16df871a8d53fd394c10

问题概述

Hako 将每次 NEPacketTunnelProvider.sleep() 转发给 BoxService.Pause()。当前 Pause 在停止已注册的周期任务并重置一分钟回退定时器之后,无条件同步调用 debug.FreeOSMemory()

Go 对 debug.FreeOSMemory() 的定义是:强制执行一次垃圾回收,然后尝试尽可能把内存归还给操作系统。即使不显式调用,运行时也会在后台逐步归还内存。

在本次 iOS 1.0.4 夜间测试中,系统出现持续的短周期 sleep/wake。因而这个用于降低 Network Extension footprint 的保护动作可能平均约每 8 秒执行一次。问题不是要求删除内存保护,而是当前实现没有区分长时间睡眠、真实内存压力和几秒一次的短暂 sleep/wake。

这里报告的是“Hako 对系统 sleep 的处理方式”,不主张频繁的 iOS sleep/wake 最初由 Hako 产生。

设备日志证据

按北京时间分析 2026-09-09 00:00–06:00

  • 01:01:14–05:59:57 共记录 2146 组 provider sleep/wake
  • 平均每小时约 431 次,平均每 8.35 秒一组。
  • wake → 下一次 sleep 中位数约 3.865 秒。
  • sleep → wake 中位数约 2.278 秒。
  • 05:23–06:00 区间有 278 次 provider sleep;同期 core 健康检查日志持续产生,说明服务处于运行状态。
  • 该区间 [Memory] probe admission 记录的 footprint 位于约 26.3–27.1 MiB,中位数约 26.7 MiB。
  • 整份导出日志没有出现内存压力、threshold triggered 或内存阈值状态转换记录。

ExtensionProvider.sleep() 先同步调用 currentService?.pause(),返回后才写入 provider sleep 日志。因此,日志中的 sleep 时间戳位于 FreeOSMemory() 返回之后,不能用于测量单次 GC/内存归还耗时。需要额外的低频耗时统计或 Instruments profile 才能量化 CPU 成本。

脱敏日志样本见
issue-sample-ios-sleep-freeosmemory-1.0.4.log

与当前源码吻合的调用链

  1. Hako-Client 的 PacketTunnelProvider.sleep() 将系统 sleep 交给 ExtensionProvider
  2. ExtensionProvider.sleep() 先调用 currentService?.pause(),随后记录 sleep 并同步等待日志队列 flush。
  3. BoxService.Pause() 每次依次执行:
    • 增加 pause 计数;
    • pause.DevicePause()
    • 重置 iOS 一分钟回退定时器;
    • debug.FreeOSMemory()

当前源码注释说明保留 FreeOSMemory() 是为了降低 iOS Network Extension 在 jetsam 限制下的 footprint。这是合理目标,但代码没有最小执行间隔、当前 footprint 判断、内存压力判断或“已经在近期执行过”的状态。

为什么短周期 sleep 需要与内存保护分开处理

pause.DevicePause() 应立即停止健康检查等周期任务,这部分不需要延后。debug.FreeOSMemory() 则是不同类型的动作:它会主动触发完整 GC 和内存归还工作。

当 sleep 持续很久或 footprint 接近 Network Extension 预算时,主动归还页面可能降低 jetsam 风险;但当系统几秒后又 wake,并重新开始网络和代理工作时,反复强制 GC 可能产生:

  • GC 扫描和清扫 CPU 时间;
  • 内存页归还后很快重新分配或重新触碰的 churn;
  • 与 wake 后健康检查、DNS 重建等分配活动交错;
  • sleep 回调返回延迟增加;
  • 高频重置 runtime 的自然后台回收节奏。

当前 footprint 较低不能证明 FreeOSMemory() 没有价值,因为较低 footprint 可能正是它的结果;同样,它也不能证明平均每 8 秒执行一次是必要的。需要在保持 jetsam 安全的前提下进行对照。

期望行为

  • pause.DevicePause() 继续在每次真实 sleep 时立即执行。
  • FreeOSMemory() 根据当前 footprint、内存压力、距离上次执行的时间或睡眠持续时间决定,而不是每次短暂 sleep 都无条件执行。
  • 接近安全阈值或收到内存压力事件时,仍能立即主动回收。
  • 高频 sleep/wake 被合并或限频,不影响服务关闭、内存压力处理和 jetsam 保护。

建议的最小改进方向

  1. 将暂停周期任务与主动内存回收拆成两个决策:Pause 始终通知 DevicePause(),内存回收增加独立条件。
  2. FreeOSMemory() 增加每个 BoxService 的最小执行间隔;具体间隔应通过设备测试确定,不在 Issue 中预设固定最优值。
  3. 当 footprint 接近阈值或收到内存压力时绕过限频,保留即时保护能力。
  4. 可选方案是在确认设备保持 sleep 一段时间后再执行回收;新的延迟任务必须在 wake/close 时可取消,不能增加永久短周期轮询。
  5. 增加低频诊断计数:Pause 次数、实际 FreeOSMemory 次数、跳过原因、执行耗时分位数及执行前后 footprint。不要为每次回收增加一组高频日志。

建议验证

  1. 对比当前实现、仅限频实现、footprint/压力条件实现,测试多个相近夜晚。
  2. 记录 Packet Tunnel Extension 的 CPU 时间、GC CPU、实际 FreeOSMemory() 次数、执行耗时和每小时电量变化。
  3. 同时记录峰值 footprint、内存压力事件、jetsam、服务重启和配置加载成功率,确认没有用功耗优化换取稳定性下降。
  4. 覆盖高节点数量和健康检查突发、持续大流量、锁屏空闲、频繁短 sleep/wake、长时间睡眠以及从后台恢复。
  5. 验证 wake 和 stopTunnel 能取消尚未执行的延迟回收任务,服务重建后不会继承旧 generation 的回收动作。

边界

  • 当前日志和源码可以确认高频调用路径,不能从现有时间戳计算单次 FreeOSMemory() 耗时。
  • 当前没有 GC profile,不能承诺该项单独造成的电量比例。
  • 较低 footprint 不能证明主动回收多余,也不能证明每次 sleep 执行都是必要的。
  • 不建议直接删除 FreeOSMemory();应通过限频或压力条件保留 jetsam 安全边界。
  • 本问题建议提交到 Hako 内核项目;Hako-Client 只负责转发系统生命周期回调和记录日志。
  • 建议在“wake 触发全量健康检查”修复后再次复测,确认高频 sleep/wake 是否仍然存在以及本问题的独立收益。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions