mvdocs

# 节点最近输出与动态调试

目标设计与 API 草案,尚未实施。总体边界和推进状态见设计概述。

本文负责输出集合、KeepLatest、动态连接/订阅、历史及查看引用。最近输出是观察状态,不是 ToolFlow 执行依赖仓库。

关联:ToolFlow · 稳定发布 · SDK · 停止

AddOwned 转移的是一个 handle 的所有权,不禁止图像被其他持有者访问。节点若交出后仍需读取,可事先保留独立的稳定 Share,并承担替换及关闭责任。调试器不借用队列拥有的对象,也不要求节点字段永久保留图像;编排运行时提供独立的节点输出存储,调试连接只读取这份状态。

必须分开三种生命周期:

对象 生命周期 职责
本次调用的输出收集器 一次节点调用 暂存 0~N 输出,成功后路由,失败时清理
节点最近输出存储 可检查的流程会话 按保留策略保存最近成功调用的输出集合,与有没有调试器连接无关
调试订阅与查看者 动态连接到关闭 接收变化标识,按需打开独立稳定引用,关闭时结束自己的保留
节点成功完成输出集合
       ├─ 按 fan-out 交给各下游
       └─ 按会话策略更新最近输出存储
                         ↓
              调试器随时连接、打开或断开

# 最近输出保留策略

支持动态查看当前结果的编辑/调试会话默认 KeepLatest,按节点保存最近一次成功调用的完整输出集合;即使调试器尚未连接,也维护该状态。纯生产会话可以显式选择 ReleaseAfterRouting,仅路由不保留,以减少内存与复制成本。保留策略是会话/端口配置,不是调试窗口是否打开的隐式副作用。

KeepLatest 的范围仍受每调用输出数量、保留字节和会话总预算约束,不保留所有历史。新调用输出集合准备成功后原子替换旧集合,旧引用释放;新调用成功但输出为空时更新为空,不能继续把上次非空结果标为当前输出。节点失败可保留上次成功结果,但必须带其真实运行/调用身份,与最新执行失败状态区分。

所有工具流输出都稳定只读,自有存储的最近结果通常直接 Share,不因调试另做像素复制,下游也不能改写它。相机 SDK 等外部缓冲默认复制到受配额管理的独立存储,避免每个节点的最近结果长期占用采集槽。保留策略由相机/会话集成声明,不能按格式或地址猜测;无法确认时采用独立快照。

需要脱离 SDK 时,输出存储作为消费者自行读取并复制;它与只读下游可以并行,但复制期间的原图引用仍须计量和保留。独立快照有新的 BufferId/Generation,条目另存来源版本作关联。路由层不替节点提交;ReleaseAfterRouting 不增加这项保留复制。若实现异步复制,完成与最近条目替换协调,过期复制不覆盖已更新结果。

保留配额不足或复制失败默认将该次结果标为快照不可用,不把业务节点改判失败,也不遗失待路由输出。要求“最近结果必须始终可检查”的会话须提前配置可容纳新旧替换峰值的预算,并采用明确背压/失败策略,不能在预算失败时默默展示旧结果冒充新结果。

# 动态连接与打开

调试缓存以运行、节点、调用、端口及项索引定位,不能仅按节点名、ImageId 或 BufferId。连续运行/重试中的同一节点会多次执行,一次调用也可产生多项共享 ROI。最新索引指向完整身份,不把观察索引用作跨运行的数据依赖。

// 编排/调试集成层的示意契约,不属于图像核心。
public readonly record struct ImageOutputKey(
    Guid RunId, string NodeId, Guid InvocationId,
    string PortName, int ImageIndex);

public interface IImageOutputInspector
{
    // 成功时 key 与 image 来自同一条目;image 是查看者拥有的稳定 Share。
    bool TryOpenLatestOutput(
        Guid runId, string nodeId, string portName, int imageIndex,
        out ImageOutputKey key, out IPixelImage? image);

    bool TryOpenOutput(ImageOutputKey key, out IPixelImage? image);

    // 只通知标识,不交接图像;replayLatest 包括已保留的当前条目。
    IDisposable SubscribeOutputs(
        Action<ImageOutputKey> observer, bool replayLatest = true);
}

上面的接口只描述单图打开和已有条目标识通知,尚未完整表达空输出集合、保留失败和集合淘汰事件。冻结观察 API 前需要补齐集合级变化协议,不能用虚构的图像项索引代替空集合;该项与订阅一致性一起列入验证与完善路线。

建立订阅与取得当前快照索引必须协调,避免“先查询再订阅”丢掉中间更新;回调在锁外执行。连接后可立即打开已保留结果,并订阅后续结果,不要求暂停或重建流程。通知允许按节点/端口合并为最新标识,不能为每次采集积累带图像引用的无界事件队列。实际条目在通知后可能已淘汰,TryOpen 返回不可用是正常竞争结果,不返回已关闭对象。

打开与替换/淘汰原子协调:成功返回前已取得独立稳定引用,缓存关闭旧条目不会破坏打开的查看者。查看者自行 OpenRead 或将其交给 ImageDisplay;临时读取、查看者 handle 与控件访问分别关闭。调试器断开只释放订阅及它拥有的查看引用,不清空运行时最近输出,也不关闭算法或队列拥有的对象。重新连接仍可读取保留状态。

ReleaseAfterRouting 会话也允许运行中添加调试器,并从此启用选定节点的保留或观察后续输出;但是已经回收的过去结果无法补取。动态连接能力不等于无限历史恢复。需要即时查看上次结果时,必须事先采用 KeepLatest,或拥有其他明确保留者。

# 历史、失败与 SDK 退出

历史和断点固定项是最近输出之外的显式保留,分别限制数量、字节及期限。已打开查看者也计入保留成本;它们不会因缓存淘汰被强制关闭。固定项超限时拒绝继续保留或导出外部存储,不无限增长。调试修改通过 CloneAsync 创建独立图像,不修改业务输出。

若需查看失败调用的部分结果,可以保留已完成写入的暂存项并标为未提交/失败调用,不路由为成功输出,也不替换最近成功集合。观察失败不能改变业务所有权,输出收集器仍按节点结果正常清理。

SDK 场景默认在开放查看前形成独立快照,流程可归还采集槽。若显式选择共享观察,缓存复制或淘汰不关闭此前已打开的原图引用;停相机必须等待这些查看者、显示读取和 pin 退出。会话关闭释放其输出存储;若调试状态需要在运行结束后继续保留,显式移交到独立调试会话。导出的独立快照可继续查看,SDK 共享引用不能被伪装成已脱离。