Skip to content

Latest commit

 

History

History
128 lines (95 loc) · 6.04 KB

File metadata and controls

128 lines (95 loc) · 6.04 KB

v2.33 charter — 大 run 的服务端渲染缓存

主题: 把"每个请求都从零重建结果"这条热路径掐掉。 动机: 上一轮 Q4 审计给性能方向排了队,但排错了对象。这一版先 用测量推翻审计结论,再修真正的瓶颈。


0. 先推翻审计的那条结论(诚实记录)

Q4 审计写的是:

5 万张以上的 run,占位符会全部留在 DOM 里,内存与节点数不可控。

实测不成立。 用 20,000 行合成 run 量到的是:

指标 审计预期 实测
占位符节点 ~50,000(全量) 700
DOM 总节点 不可控 7,362
JS 堆 不可控 30 MB

v2.18 的分片水合 + v2.24/v2.26 的窗口化去物化已经把 DOM 兜住了。 审计那条是纸面推演,没有量过。照它去改,会花一整版改一个不存在的问题。

真瓶颈在服务端,不在浏览器。

1. 真实瓶颈:_build_results 无缓存

_build_results() 每次调用都完整重做一遍:

  1. pandas 重新解析整个 scores.csv;
  2. 重新合并 annotations.jsonl;
  3. 逐行重新推导 6 轴 rubric + advice 文案。

一次页面加载会调它 5 次以上(页面本体 + 每个 /results_rows 水合分片各一次)。20,000 行 run 上量到:

路径 v2.32.1 v2.33 倍数
/results/<run> 首次(冷) 6.2s 7.7s¹
/results/<run> 之后(热) 6.2s 0.055s ~140x
/results_rows 每个水合分片 6.3s 0.045s ~140x

400 张合成 run 走 HTTP 全链路复测:冷 1.04s → 热 0.023s

¹ 冷启动没有变快也不该变快 —— CSV 总得解析一次。这一版消掉的是 第 2…N 次的重复劳动,而那才是 20k run 上真正卡住审片的部分。

2. 实现:按 mtime 键的缓存,不引入新机制

原函数体原样改名 _build_results_uncached,外面套一层:

cache_key = (run_id, str(output_dir),
             scores_path.stat().st_mtime_ns, ann_mtime)

设计取舍:

  • 键是输入文件的 mtime,不是显式失效调用。 重新打分或存一条标注 会自然改 mtime → 缓存自然失效。写入方不需要知道缓存存在,这正是 _JSONL_CACHE(_MtimeLRUCache)已经在用的纪律,不新造一套。
  • 只键这两个文件,因为构建只读这两个文件。 用 AST 核过 _build_results_uncached 的全部读取点:scores.csv + annotations.jsonl,没有第三个。run["vertical"] 也读,但它在 run 创建时固定,全仓没有第二个写入点。
  • ann_mtime 缺失记 0。 没有标注文件 ≡ 还没写过标注,与写之前是 同一个可观测状态,复用同一条缓存是正确的。
  • output_dir 进键。 run_id 只在单个 demo root 内唯一,测试会 重映射 _DEMO_ROOT 复用同名 run id;不带路径的键会串。
  • 构建在锁外。 这是数秒级的路径,持锁会把并发请求串成队列。两个 请求同时构建只是重复劳动,结果一致。
  • 同一 run 的旧世代立刻清掉。 重新打分过的 run 否则会把自己好几份 多 MB 行表钉在缓存里等自然淘汰(_MtimeLRUCache 对它的单路径也是 这么做的)。

3. 正确性:缓存最危险的地方不是慢,是静默的错

缓存把"看得见的慢"换成"看不见的错"。所以这一版真正花时间的是这三条:

(a) 缓存对象是按引用交出去的 → 任何调用方原地改写都会污染后续请求。 用 AST 追了 18 个调用点(含 for r in rows: / enumerate(rows) 的 循环变量别名),再追了所有接收 rows 的下游函数: _build_face_clusters_info_build_locations_infobuild_gallery_zipgenerate_for_runapply_style_guide —— 全部 只读。这条契约已写进函数上方的注释,后续改动要守住,或者改前先拷贝。

(b) 复合失效:_read_human_by_fn_cached 自己也是 mtime 缓存。 如果它的精度比我的粗,就会出现"我这层失效了、重建时又从它那儿拿到旧 标注"——然后把错的结果缓存起来。核过:它同样用 st_mtime_ns,同精度, 不存在这个窗口。

(c) 回归测试要有牙。 tests/test_results_cache.py(9 条)覆盖:热 命中不重建、存标注必失效、追加第二条标注再失效、重打分必失效、同名 run 跨 root 不串、缓存有界、旧世代不钉住、None 不入缓存。 做过变异验证:把键里的 ann_mtime 改成常量 0,恰好且仅有两条失效 测试变红 —— 证明它们不是空过。

4. 过程中修掉的一件事(记录教训)

验证失效时,我先在 owner 的真实照片标注 run(labelrun,121 张真 照片)上 POST 了一条 overall_label: cull 的测试标注。这正是 RESCORER-V3 那条教训里"伪造标签污染模型"的同一个坑 —— 真实标注必须 是 owner 的。已删除该条,并把后续验证全部改到合成 run 上。

另外,我一度据此误判"缓存失效失败了":真实观察到的是 decision 字段 没变。跟无缓存基线 A/B 后确认 decision 存的是规则栈原判定,人工判定 走 human_decided / n_human_decided,基线行为一致 —— 不是 bug。 结论:任何"这是 bug"的判断,先跟 stash 基线 A/B 再说。

5. 验收

  • 门禁绿:1341 passed, 5 skipped(2 face fixture + 3 zeroconf,预期)。
  • ruff:serve_app.py 162 → 162(未新增);新测试文件 all clean。
  • 版本 2.32.1 → 2.33.0 lockstep(pyproject.toml + pixcull/__init__.py)。
  • 20k run 实测 + 400 张 HTTP 全链路实测,数字见上表。

6. 顺延

  • 近重复 O(N²)(20k=2.4s / 50k=13s):用时间窗剪枝,不是 ANN。 触发条件仍是真实的 20k+ 单 run 使用。
  • 跑完自动入库(v2.32 计划里记的自然缺口):run 结束后自动 library index,目前仍需手动跑一次。
  • data-theme 未设:/library/history/tether 等独立页面 没有一个设 data-theme,浅色主题在这些页从未生效(v2.29 之前就存在)。 要跨 5+ 个页面改,单独排一版。
  • ANN 压缩(int8/PQ):库超过约 30 万张再说。