event 设计

设计目标

事件流是实验的审计记录:报告中的每个数字和每个被重放的失败都必须能从中恢复。event 让这份记录保持只追加、面向行,并让存储方面的选择远离运行器。

数学背景

把一次运行建模为事件序列 e1,e2,…,eNe_1, e_2, \dots, e_N,把接收器建模为折叠 sk=emit(sk−1,ek)s_{k} = \text{emit}(s_{k-1}, e_k)。JSONL 序列化是从事件到行的映射 JJ,存储的流是用换行符连接的各行 J(e1),…,J(eN)J(e_1), \dots, J(e_N)。

这种形状带来两个性质。

  1. 前缀封闭。 流的每个前缀都是某个更短运行的流。在 kk 个事件后崩溃会留下 e1,…,eke_1, \dots, e_k 的有效记录;独立处理各行的读取器可以使用它。
  2. 投影与拼接可交换。 报告是逐行构建的,因此两个拼接起来的流的投影可以由各行的投影算出。合并或过滤流只是普通的文本处理。

tee 是两个折叠的积:它把 (s,t)(s, t) 映射到 (emit1(s,e),emit2(t,e))(\text{emit}_1(s, e), \text{emit}_2(t, e)),因此一次运行可以在单趟中同时存储到内存和磁盘。

设计决策

由闭包组成的记录

问题。 接收器持有状态(数组、文件句柄)并且会被组合(tee)。方案。 带方法的 trait;由函数组成的记录。选择。 由五个闭包组成的记录。理由。 闭包可以捕获接收器所需的任何状态,记录可以在测试中内联构建,而 tee 这样的组合子就是返回新记录的普通函数。

每行一个 JSON 对象

问题。 记录必须可流式传输、可追加,并且能被任何工具读取。选择。 采用 JSONL,每一行都带有 type 判别字段和 artifact_version。理由。 每一行都是自描述的,因此读取器可以跳过不认识的类型,grep 也能用,崩溃也永远不会损坏之前的行。每行带版本使来自不同版本的流可以拼接在一起,同时仍能被精确拒绝。

只记录原始值

观测携带的是一个批次的每次迭代时间,从不是均值、中位数或过滤后的值。报告中的每个统计量都从这些原始值重新计算,离群值策略无法改写历史。

种子写为字符串

seed 写为十进制字符串,因为 JSON 数值是双精度浮点数,无法表示每个 64 位整数:大于 2532^{53} 的整数会被舍入。

finish 返回位置

finish 返回记录的去向(memory://run/…、jsonl://memory/…、一个文件名),运行器把它存入摘要。调用者无需运行器了解文件,就能知道证据在哪里。

正确性与不变量

  • 每次 emit 调用恰好写出一行(JsonlSink、streaming_jsonl)或恰好追加一个元素(InMemorySink)。
  • 每一行都带有 artifact_version mmka_1 和 type。
  • tee 保持顺序:对每个事件,左侧接收器都先于右侧接收器看到它。
  • to_jsonl 每个事件一行,以 \n 分隔。
  • 字符串由 JSON 编码器转义,因此一行中永远不会包含原始换行符。

被否决的方案

  • 每次运行一个 JSON 文档。 不可追加;一次崩溃会丢失全部内容。
  • CSV。 嵌套的证据(操作数、标志、环境)放不进去。
  • 二进制格式。 在评审和 issue 报告中无法阅读。

边界

  • 没有文件 IO:把行写入磁盘是调用者的工作(由 cli 和应用完成)。
  • InMemorySink 不保存摘要。
  • JSONL 写入器省略验证状态的原因字符串以及观测的 setup_timing。
  • 这里没有读取器;由 report 和 cli 解析事件流。