3DGS 端侧重建最难处理的状态,往往不是明确失败,而是“看起来还在运行”。进度停在 63%,页面动画继续,轮询也没有报错;用户返回后重新开始,旧会话又可能迟到回调。若应用只区分成功与失败,这种停滞既没有退出条件,也没有可复查证据。

本文构造一个会话级看门狗。示例项目为 ReconWatch,页面为 ReconWatchdogPage,任务 ID 是 RECON-WD-0143,Native 会话名为 recon_g42。演示中进度停在 63%,连续 12.0 秒没有前进,超过应用层 10 秒阈值后从 RUNNING 进入 SUSPECT,再执行单次销毁,generation 从 52 提升到 53。数字用于说明状态合同,不冒充某台设备的算法耗时。

一、先区分慢、停滞与页面没有刷新

进度不变有三种来源。第一种是算法阶段本身耗时,底层仍正常工作;第二种是会话无法继续,进度长期无变化;第三种只是页面丢了更新,Native 侧进度已经变化。把三者都叫“卡死”,会诱导代码在一次相同采样后立即销毁会话,反而破坏正常任务。

ReconWatch 不根据单个百分比下结论。它保存最近一次前进的单调时间、最近进度、连续相同采样数、Native 返回码和页面 generation。采样周期设为 500 ms,10 秒只是示例阈值;从 RUNNING 进入 SUSPECT 还要求连续两个确认采样。这样可以容忍页面调度抖动,也让销毁决定有明确证据。

状态机只允许单向前进:CREATED → RUNNING → SUSPECT → STOPPING → DESTROYED → RETRY_READY。正常完成走 RUNNING → COMPLETED → DESTROYED。不存在从 DESTROYED 回到 RUNNING 的路径;重试必须创建新 sessionId 和新 generation。

二、把 Native 进度采样收敛为一条窄接口

官方管线提供会话创建、推帧、启动、进度查询和销毁等能力。文章不把应用看门狗伪装成系统 API:HMS_SpatialRecon_GetProgress 只提供采样事实,阈值、连续确认和恢复策略都属于项目代码。

这段代码解决什么问题:在 C++ 边界统一读取进度与返回码,避免 ArkTS 层直接猜测指针、会话句柄或错误语义。

struct ProgressSample {
  int32_t code;
  int32_t percent;
  int64_t sampledAtMs;
};

ProgressSample ReadProgress(ReconSessionHandle session,
                            int64_t monotonicMs) {
  int32_t percent = 0;
  int32_t code = HMS_SpatialRecon_GetProgress(session, &percent);
  if (code != 0) {
    return {code, -1, monotonicMs};
  }
  if (percent < 0 || percent > 100) {
    return {-2001, -1, monotonicMs};
  }
  return {0, percent, monotonicMs};
}

返回结构同时带 code 和 percent,失败时不沿用上一次进度。时间使用单调时钟,不用墙上时间;系统校时、时区变化都不应改变“多久没前进”。易错点是把错误采样当作 0%,这会制造进度倒退,也可能把已经进行到 63% 的会话误判成重新开始。实际项目应根据目标 SDK 头文件核对句柄类型与返回码,不复制示例别名当作真实声明。

三、看门狗观察的是“最后一次前进”

页面每 500 ms 请求一个 ProgressSample。若本次百分比大于上次,更新 lastAdvanceAt 并清零相同计数;若相等,只增加计数;若倒退,记录协议异常,不自动覆盖基线。只有 now-lastAdvanceAt >= 10000 且确认采样不少于 2 时,才进入 SUSPECT。

这段代码解决什么问题:用单调时间、连续确认与 generation 构成应用层停滞判定,避免一次慢采样触发破坏性销毁。

type ReconState = 'CREATED' | 'RUNNING' | 'SUSPECT' |
  'STOPPING' | 'DESTROYED' | 'RETRY_READY' | 'COMPLETED';

interface WatchSnapshot {
  taskId: string;
  sessionId: string;
  generation: number;
  progress: number;
  lastAdvanceAt: number;
  sameSamples: number;
  state: ReconState;
}

const POLL_MS = 500;
const STALL_MS = 10_000;
const CONFIRM_SAMPLES = 2;

function reduceSample(s: WatchSnapshot, percent: number,
  now: number): WatchSnapshot {
  if (percent > s.progress) {
    return { ...s, progress: percent, lastAdvanceAt: now,
      sameSamples: 0, state: percent === 100 ? 'COMPLETED' : 'RUNNING' };
  }
  if (percent < s.progress) throw new Error('PROGRESS_REGRESSION');
  const same = s.sameSamples + 1;
  const stalled = now - s.lastAdvanceAt >= STALL_MS && same >= CONFIRM_SAMPLES;
  return { ...s, sameSamples: same, state: stalled ? 'SUSPECT' : s.state };
}

sameSamples 不是时间替代品,它只证明至少看到了两个独立事实;真正阈值仍由单调时间计算。状态变化发生在一个 reducer 中,页面收到的是完整快照,不会出现进度 63%、停滞 12 秒、状态却仍显示 RUNNING 的撕裂组合。若进度接口只在大阶段更新,10 秒可能过短,必须用目标设备和真实素材建立阈值分布。

四、SUSPECT 不是立刻重启,而是先冻结提交权

进入 SUSPECT 后,页面停止“即将完成”动画,按钮改为“结束并重试”,同时撤销 generation 52 的结果提交权。此时若下一次采样突然到 64%,策略可以选择恢复 RUNNING,也可以坚持人工确认。示例采用保守策略:进入 SUSPECT 后不自动恢复,避免进度在阈值边缘来回抖动;用户确认后进入 STOPPING。

冻结提交权比立即释放 UI 更重要。旧会话在销毁过程中仍可能产生 Native 回调或文件事件,如果页面已经把同一个 taskId 复用给重试,会把旧结果误贴到新会话。RECON-WD-0143 始终属于 generation 52;重试会生成新的任务身份,而不是把原对象清零。

DevEco Studio 演示图采用白色主题:左侧 ReconWatch 工程,中间是 reduceSample 和 Native 适配层,右侧模拟器显示 recon_g42 / 63% / 12.0s / SUSPECT / generation 52,底部 HiLog 记录 threshold=10s sameSamples=24。它是技术说明配图,不作为真实 IDE 或真机性能证据。

五、销毁必须只有一个调用者

页面返回、用户点击重试、看门狗超时和 Ability 生命周期都可能请求销毁。如果每条路径都直接调用 Native destroy,同一句柄会被释放两次。ReconWatch 用原子门闩让第一个请求者取得销毁权,其余请求只等待同一个完成结果。

这段代码解决什么问题:在 C++ 侧保证 HMS_SpatialRecon_DestroySession 对同一会话最多执行一次,并在完成后把句柄置空。

class SessionLease {
 public:
  explicit SessionLease(ReconSessionHandle handle) : handle_(handle) {}

  int32_t DestroyOnce() {
    bool expected = false;
    if (!destroying_.compare_exchange_strong(expected, true)) {
      return destroyCode_.load();
    }
    int32_t code = 0;
    if (handle_ != nullptr) {
      code = HMS_SpatialRecon_DestroySession(handle_);
      handle_ = nullptr;
    }
    destroyCode_.store(code);
    destroyed_.store(true);
    return code;
  }

  bool IsDestroyed() const { return destroyed_.load(); }

 private:
  ReconSessionHandle handle_{nullptr};
  std::atomic_bool destroying_{false};
  std::atomic_bool destroyed_{false};
  std::atomic_int32_t destroyCode_{0};
};

门闩保护的是调用次数,不代表第二个调用者已经观察到销毁结束。生产实现应返回可等待的 future 或由上层统一串行调度,不能只看到 destroying=true 就创建新会话。状态从 STOPPING 到 DESTROYED 只有在 Native 调用返回并清空句柄后发生。即使 destroy 返回错误,也不应继续使用原句柄;错误进入诊断,是否允许重试由能力文档和实际返回码决定。

六、ArkTS 层要拒绝 generation 52 的迟到事件

Native 销毁完成后,协调器把 generation 从 52 提升到 53,并发布 RETRY_READY。轮询定时器、回调和文件监听都带启动时的 generation;回到主线程准备写状态时再比较一次。只在开始前比较,会漏掉“检查后、提交前”发生的代次切换。

这段代码解决什么问题:把定时器取消、迟到回调拒绝和页面离开收敛为同一个会话代次协议。

class ReconWatchdog {
  private generation = 52;
  private timer: number = -1;
  private disposed = false;

  startPoll(poll: (gen: number) => Promise<number>): void {
    const captured = this.generation;
    this.timer = setInterval(async () => {
      const percent = await poll(captured);
      if (this.disposed || captured !== this.generation) return;
      ReconStore.acceptProgress('RECON-WD-0143', captured, percent);
    }, POLL_MS);
  }

  markDestroyed(): number {
    if (this.timer !== -1) clearInterval(this.timer);
    this.timer = -1;
    this.generation += 1;
    ReconStore.publishRetryReady('RECON-WD-0143', this.generation);
    return this.generation;
  }

  dispose(): void {
    this.disposed = true;
    if (this.timer !== -1) clearInterval(this.timer);
    this.timer = -1;
    this.generation += 1;
  }
}

定时器创建与清理成对出现,页面销毁和会话销毁都能让 captured generation 失效。易错点是 setInterval 内异步调用重叠:若一次轮询超过 500 ms,下一次又开始,会产生并发采样。实际项目更适合用一次性 timer,在本次完成后安排下一次,或增加 polling 门闩。代码为突出代次机制保留最小形式,工程实现必须补上不可重入约束。

七、63% 运行页和销毁详情页承担不同解释

03 是纯手机运行画面:19:31、Wi-Fi、5G、76% 电量,任务 RECON-WD-0143,会话 recon_g42,进度 63%,无前进 12.0 秒,阈值 10.0 秒,状态 SUSPECT,generation 52。红色标注只指出“超过阈值”和“提交权已冻结”。这里没有显示“失败”,因为销毁尚未完成。

04 是明显不同的销毁诊断页:19:32、75% 电量,状态 DESTROYED,destroyCalls=1、旧回调拒绝 3、timer 已清理、handle 已置空、generation 53、状态 RETRY_READY。返回码单独记录,不用一句“清理成功”覆盖所有资源。两张图合在一起能说明状态变化,而不是用第二张相似进度图凑数。

八、测试要主动制造停滞与竞争销毁

最小测试矩阵有五类。第一,进度每九秒前进一次,不能误报;第二,63% 保持 12 秒,进入 SUSPECT;第三,SUSPECT 后页面返回与用户重试同时触发,DestroySession 只执行一次;第四,销毁后 generation 52 的三条回调到达,全部拒绝;第五,generation 53 创建失败,页面停留 RETRY_READY 并给出可行动提示,不能复活旧句柄。

测试替身要允许控制时间,不要真实等待 12 秒。将 monotonic clock 注入 reducer,便能按 500 ms 推进采样。Native 适配层也应统计 destroyCalls 并模拟延迟,证明第二个销毁请求不会越过门闩。若测试只检查最终页面,没有检查句柄与 timer,资源泄漏会被“看起来恢复了”掩盖。

还应检查进度倒退、越界和错误返回。63% 后出现 61% 不是普通停滞,应记录 PROGRESS_REGRESSION;101% 或负值是协议异常;连续查询失败不能沿用旧时间无限等待。不同异常可以共享销毁收口,但原因码必须保留,否则后续无法区分算法停滞、桥接层错误和状态污染。

九、阈值应按阶段和设备证据调整

固定 10 秒只适合演示。重建早期、优化阶段和结果写出阶段可能有不同进度粒度;高端与低端设备也不同。可靠做法是收集“每次进度前进的间隔”分布,按阶段选取保守分位数,再附加绝对上限。阈值配置要带版本,例如 wd-policy-v2,诊断页记录本次采用的版本。

阈值过短会增加无谓销毁,过长会让用户看着假运行。产品可以提供“继续等待”和“结束并重试”两种动作,但两者都要走同一状态机。继续等待只重置观察窗口,不改变 sessionId;结束并重试必须先完成 DESTROYED,才能创建新会话。不能为了响应按钮而让两个 Native 会话短暂并存。

十、边界与落地判断

1. 会话创建与页面创建不能绑成同一个动作

页面进入不等于立刻创建 Native 会话。相机输入、帧清单、设备支持检查和权限状态都可能尚未准备好。ReconWatch 先创建应用任务,再在前置条件通过后创建 recon_g42,把 sessionId 回写任务快照。这样页面重建只会重新订阅任务,不会因为 aboutToAppear 再创建一份会话。若创建失败,任务停在 CREATED_ERROR,根本不启动看门狗。

页面离开也不必自动销毁,取决于产品是否允许任务在当前 Ability 会话中继续。但无论选择继续还是结束,所有权必须唯一:继续时由协调器持有 session lease,页面只撤销订阅;结束时由协调器进入 STOPPING,页面等待状态结果。最危险的实现是页面和全局服务各保留一份裸句柄,双方都认为自己负责清理。

2. 轮询不能掩盖错误码连续出现

连续相同百分比与连续查询失败不是同一问题。前者说明能力返回了合法但未前进的进度,后者说明应用已经失去观察通道。示例可以再维护 queryFailures:一次瞬时失败记录并重试,连续三次则进入 OBSERVE_ERROR,冻结提交权并要求销毁。不能把失败时刻算成“最后前进”,否则每次错误都会刷新计时,让真正失联的会话永远不触发门禁。

日志要分别记录 progress=63 code=0 与 progress=NA code=-x。诊断页也不应把错误查询画成 63% 的新采样;最多以灰色显示“上次确认 63%”。这种区别能帮助判断是算法阶段耗时,还是 NAPI/Native 桥接、句柄或线程出了问题。

3. 停滞证据需要一个小而完整的环形缓冲

只保存最后一个样本无法解释为什么进入 SUSPECT。ReconWatch 为每个任务保留最近 32 条采样:单调时间偏移、进度、返回码、线程来源和 generation。32 条按 500 ms 约覆盖 16 秒,足以还原 10 秒阈值,又不会把高频日志无限写入磁盘。进入终态后把摘要落盘,原始缓冲可以丢弃。

摘要至少包含最后前进时刻、进入 SUSPECT 时刻、相同样本数、查询失败数、销毁请求来源和 destroyCalls。04 中的“旧回调拒绝 3”同样来自计数器,不是人工填写。若没有这些字段,线上只会看到用户点击了重试,却不知道看门狗是否先触发、页面返回是否参与竞争。

4. 销毁顺序要覆盖所有上游生产者

会话销毁不是唯一清理动作。相机或图像生产者仍可能继续推帧,轮询 timer 仍可能读取,结果监听仍可能准备提交。正确顺序是先把应用状态改为 STOPPING 并撤销提交权,再停止新帧入口与轮询,等待在途适配调用收口,最后执行 DestroySession。若先销毁句柄再停生产者,在途 PushFrame 可能命中已经失效的资源。

本文代码没有展开相机生命周期,是为了保持主题集中,但项目检查表必须列出每个上游:采集回调、帧队列、进度轮询、结果读取、临时文件和 UI 订阅。每项都要写出“停止入口”和“最终释放方”。会话从 STOPPING 到 DESTROYED 的条件,是清单全部完成,而不只是 Native destroy 返回 0。

5. 重试不是复用旧帧的默认许可

用户点击重试后,是否复用 generation 52 已采集的帧,取决于管线对输入所有权和一致性的要求。若帧清单仍然完整、校准版本未变且官方能力允许重新推送,可以创建新会话逐帧重放;若旧会话已消费并释放底层缓冲,就必须重新采集。不能因为文件路径还在,就假定指针、时间戳和位姿都仍有效。

RETRY_READY 只表示资源已收口、应用可以开始新的前置检查,不表示已经恢复任务。新任务要生成新 sessionId、新 generation 和新的输入清单摘要。诊断页可以提供“重新使用已验证帧”或“重新采集”的决策,但必须展示校准版本和帧数量,避免用户把一次销毁误解为无损续跑。

6. 前后台与锁屏要有独立策略

应用进入后台后,定时器调度和资源可用性可能变化。若产品不支持后台重建,进入后台时应主动走 STOPPING,而不是依赖看门狗十秒后误判。若支持后台继续,也不能用前台 500 ms 轮询与阈值照搬,需要按后台能力限制重新设计观察通道。本文的看门狗只对前台页面会话成立。

锁屏、来电和系统资源回收同样可能造成长间隔。恢复前台后,先读取会话事实,再决定是否继续计时;不能用墙上时间差直接判定停滞。单调时间虽然不会被校时影响,但应用挂起期间是否计入阈值属于产品语义,需要明确。通常更稳妥的是把挂起时刻冻结,恢复后给一个观察宽限期。

7. 状态快照要一次提交,避免 UI 自相矛盾

进度、停滞时长、状态、按钮文案和 generation 是同一事务快照。若分别修改多个 @State,某一帧可能显示 SUSPECT,却仍保留“继续采集”按钮,或者显示 generation 53 但会话名仍是 recon_g42。协调器应构造完整 ReconUiSnapshot,一次替换页面状态;按钮事件再携带 snapshot revision 回到协调器。

当用户看到 63% 并点击“结束并重试”时,若快照已经更新为 DESTROYED,协调器返回当前状态,不再执行第二次销毁。revision 比按钮节流更可靠,因为它验证用户动作针对的确实是当时那份状态。节流只能减少连点,不能解决状态已经变化的逻辑竞态。

8. 性能指标不能替代正确性指标

看门狗本身应很轻:一次 Native 进度查询、一次小对象归约和有限缓冲写入。优化时不要先追求把轮询从 500 ms 改成 50 ms,这只会增加桥接压力,并不能让算法进度更准确。需要关注的是查询 P95、重叠调用数、timer 漂移和销毁收口耗时,而不是页面动画帧率。

正确性指标更直接:误报停滞次数、同句柄重复销毁次数、旧 generation 提交次数、DESTROYED 后仍在运行的生产者数量。目标分别是可解释、0、0、0。只有这些指标稳定,才值得讨论缩短恢复时间。

9. 发布前的逆向验收

验收人员可以从 04 倒推整条链:generation 53 为什么出现;generation 52 的 recon_g42 何时最后前进;谁在 12.0 秒时标记 SUSPECT;哪个来源获得销毁权;为什么 destroyCalls 等于 1;三条迟到事件为什么没有进入结果页。每个问题都应能在快照或日志里找到一条明确证据。

如果只能回答“用户点了重试,所以清理了”,说明看门狗仍不可审计。真正完成的方案,应在用户未点击时也能识别停滞,在用户多次点击时仍只销毁一次,在页面销毁后也不会让旧结果复活。这样才配得上会话级故障收口,而不只是一个十秒倒计时。

看门狗不证明 3DGS 算法有 bug,它只证明“应用在给定阈值内没有观察到进度前进”。销毁也不是恢复全部输入:已推帧数据是否需要重新准备、临时产物如何清理,要按管线合同处理。本文不发明暂停、取消或恢复 API,只使用已核对的进度查询与会话销毁边界。

完成标准可以写成六点:进度采样有返回码;时间使用单调时钟;SUSPECT 前有连续确认;提交权先于销毁撤销;同一句柄只销毁一次;新会话必须使用新 generation。满足这些条件,63% 才不再是一个无解的数字,而是一条能观察、判断、收口和复查的工程状态。

还有一个容易被忽略的发布条件:诊断能力必须在关闭详细日志时仍然成立。生产环境可以减少每次采样的文本输出,但不能移除状态计数器和终态摘要。否则调试版能够解释 destroyCalls,线上版只剩一个“重试失败”。更合适的做法是内存中保留有限环形缓冲,进入 SUSPECT 或 DESTROYED 时一次写出脱敏摘要;普通运行不持续刷盘。这样既控制日志成本,又保留故障发生前后的关键证据。

参考资料:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐