# Tracer 指令校验机制设计决策 ## 背景 在设计 Tracer 协议时,我们面临一个关键的架构决策:**应该在哪里对 Tracer 的操作指令进行合法性校验(例如数组越界、空指针访问)?** 我们有两个主要的选择: 1. **SDK 侧(生产者)**:在用户调用 SDK API(如 `tracer.pick(i)`)生成指令时即时校验。 2. **渲染器侧(消费者)**:SDK 只负责生成指令,由前端渲染器在回放指令序列时进行校验。 ## 决策:SDK 侧“影子状态”校验 经过讨论,我们决定采用 **SDK 侧校验** 作为主要防线,并引入 **“影子状态(Shadow State)”** 模式来实现。 ### 核心设计原则 1. **Fail Fast(快速失败)**: * 用户的逻辑错误(如访问越界)应在代码执行阶段立即抛出异常,而不是等到生成了错误的 JSON 并在渲染器播放时才报错。 * 这样可以将错误精确定位到用户代码的具体行号,极大提升调试体验。 2. **最小必要状态(Minimal Viable State)**: * SDK 不需要维护完整的数据结构副本(那会带来巨大的内存和性能开销)。 * SDK 只需要维护用于校验合法性的 **元数据(Metadata)**。 * **案例**:对于 `ArrayTracer`,我们不需要存储数组的具体元素值,只需要维护一个整数 `currentSize`。 ### 实现方案 #### 影子状态维护 在每个 Tracer 实例内部维护一个轻量级的状态变量: * **ArrayTracer**: 维护 `int currentSize`。 * `create(array)`: 初始化 `currentSize = array.length`。 * `scale(newSize)`: 更新 `currentSize = newSize`。 * `pick/patch(index)`: 校验 `0 <= index < currentSize`。 #### 跨语言适应性 这种模式具有极强的跨语言通用性: * **TypeScript/JS**: 简单变量。 * **Java/C#**: 类的私有成员字段。 * **C/C++**: 结构体中的字段(即便 C 语言原生数组不带长度,通过 Tracer 封装后反而赋予了其边界检查能力)。 ### 对比分析 | 维度 | SDK 侧校验(采用方案) | 渲染器侧校验 | | :--- | :--- | :--- | | **错误反馈时机** | **即时**(用户运行代码时) | **延迟**(用户观看回放时) | | **错误定位能力** | **高**(直接抛出异常,有堆栈信息) | **低**(难以关联回源码行号) | | **实现复杂度** | 中(需维护影子状态) | 低(仅需防御性编程) | | **性能开销** | **极低**(仅维护元数据,如 `int`) | 无额外开销 | | **用户体验** | 类似本地调试,符合直觉 | 可能会看到“崩坏”的动画 | ## 结论 虽然渲染器依然需要具备防御性编程能力以防止崩溃,但**业务逻辑的正确性校验应当由 SDK 在指令生成源头保证**。这不仅符合“数据源头治理”的原则,更能为用户提供更友好的开发和调试环境。