Repository navigation
perf: tail and virtualize webhook event logs - #50
Conversation
Event and sandbox log views open on the latest 2000 lines, load earlier bytes on demand, and keep full history for download only.
Ignore metadata-only log chunks when deciding the tail start, and keep the earlier-log action when that read stops short.
Keep the page on the loading state until history resolves, so a conversation still opens on the conversation tab.
|
@monkeyscan 评审下这个PR |
|
PR Title: perf: tail and virtualize webhook event logs Commit: 整体评估本次变更重构了运行日志查看链路:RunLogViewer 改为按行虚拟滚动并新增搜索计数/跳转,sandbox-log-runs 改为按 run 拉取 tail 分片并支持向前补载更早日志,followRunLogs 改为结构化 chunk 与 options 参数,另含事件时间线展开与文案补充。整体方向一致,新契约与各调用方基本同步更新,未发现数据完整性或安全层面的高风险问题,问题主要集中在日志检索与预插日志的交互行为上。 总结整体可合入,但没有阻断性缺陷。最需要留意的是新增日志检索“日志搜索没有任何匹配高亮,用户看不到匹配位置”(只有计数,匹配行与普通行外观相同)以及“点击“加载更早日志”后视口会跳到其它内容”(预插更早日志未补偿滚动位置)。这两点都出现在新特性的核心使用路径上,建议合并前补齐。 Findings (2)
Prompt To Fix All With AI |
Highlight matched log rows and scroll to the first hit. Shift the viewport by the prepended row count so loading earlier logs keeps the current line.
|
@monkeyscan 评审下这个PR的最新提交 |
|
PR Title: perf: tail and virtualize webhook event logs Commit: 整体评估本次改动把沙箱/运行日志查看从“一次性渲染整段文本”改为窗口化加虚拟滚动:sandbox-log-runs.svelte 先拉取日志尾部(2000 行)并支持按字节偏移惰性加载更早内容,run-log-viewer.svelte 改为按行虚拟渲染并新增匹配定位与“加载更早日志”,api/runs.ts 把 followRunLogs 改为选项对象并暴露带偏移的 RunLogChunk,RunDetail、sessions、workbench、event-timeline、i18n 与 e2e 同步适配。整体方向合理,大日志的渲染与内存压力明显改善,各调用方的签名迁移基本自洽。 总结整体可用,主要风险集中在新增的搜索与滚动保持逻辑:实时跟随的日志下搜索会被反复重置回第一处匹配,多运行并行“加载更早日志”时共享的行数补偿可能把视口推到错误位置。这两处建议在合并前修正。 Findings (2)
Prompt To Fix All With AI |
Reset a log search only when the query changes, and compensate earlier-log inserts only for sections above the anchored line.
winterfx
left a comment
There was a problem hiding this comment.
结论
方向认可:FollowRunLogs 从「全量一次拉 + 每 64KB 整段字符串拼接并全量重渲染」改成「tail 2000 行 + 虚拟化 + 缓冲批量 flush」是对症的。但当前实现有一个硬 blocker,这个页面现在完全跑不起来,建议先修再合。
🔴 Blocker:$effect 自触发死循环,日志面板整页卡死
sandbox-log-runs.svelte:81-92 这个「重置」effect 读了 controllers(forEach)、flushTimers(forEach)、requested(clear)、pendingText(clear)。SvelteMap.forEach 内部会 #read_all() → get(#version),所以「读一下」就等于声明了依赖;而订阅 effect(:96-103)→ loadTail → readLogs → controllers.set(...)(:155,新 key)会 increment(#version),于是形成闭环:
- 订阅 effect 给每个 run 开 tail,
controllers.set写入新 key; - 重置 effect 被标脏重跑 → abort 全部、
clear()、requested.clear()、windows = {}; requested被清空 → 订阅 effect 重跑,判定所有 run 都需要重新加载 → 回到第 1 步。
两个 effect 互相把对方标脏,Svelte 的更新深度守卫会在 ~1000 次时抛 effect_update_depth_exceeded(生产构建也抛,不是 dev 警告)。我用一份最小复现(同样用 SvelteMap/SvelteSet + 这两个 effect 的结构,生产构建 + headless Chromium)复现到了:计数停在 1000,页面抛 https://svelte.dev/e/effect_update_depth_exceeded,主线程一直不 yield,tab 直接卡死、一行日志都不渲染。
修法:controllers / requested / pendingText / flushTimers 都是纯内部登记表,一个都不需要响应式,换成原生 Map/Set 即可(只有 windows 需要 $state)。备选是 untrack(...) 包住读取,或把重置改成显式函数在 sandboxId 变化处调用,但都不如换原生集合干净。
这也说明为什么 npm run check:ui 拦不住它——svelte-check 只做类型检查,环形失效是运行时语义,只有真跑页面才能发现。
🟠 合并前必须补的验证
- 描述里写了没跑 e2e,而这次新增的断言(
tests/e2e/ui.spec.ts:2314-2325)在当前代码下必然失败;.github/workflows/ci.yml里也没有 Playwright 步骤,所以流水线绿了不代表能用。修完 blocker 之后,至少要把 tail 首屏 / 「加载更早日志」/ 「下载原始日志」这三条路径端到端跑一遍。 data-log-pending那条断言对「最新 run 仍在运行」的 sandbox 会必然超时:follow: true的readLogs不会返回,patchWindow(loaded: true)(:114)永远到不了,pending就一直为 true。断言本身需要重新设计,或明确限定只在已结束 run 的场景使用。
🟠 一次对每个 run 开一条流
:96-103 对所有 run 直接 void loadTail(run)。workbench 最多会传 200 个 run,等于打开 tab 就同时开最多 200 条长连接,服务端同时起 200 个 250ms 的 follow 循环。gRPC-web 在 HTTP/1.1 上不能多路复用,浏览器同源并发只有 ~6,用户正在看的那条 tail 会排在其余请求后面。旧代码对已结束的 run 是串行拉取的,这里是一个明确的并发回归。建议只对选中/最近 N 个 run 开 tail,其余滚到或点开再拉。
🟡 建议一起处理
- controller 键冲突:
readLogs:155的键是${options.follow ? 'tail' : 'earlier'}:${runId},而已结束的 run 走的正是follow=false,于是它的 tail 流和loadEarlierRun抢同一个earlier:<runId>。在 tail 还在推的时候点「加载更早日志」,tail 的 controller 会被挤掉,之后切 sandbox 时abort()(:84)再也够不到它 → 流泄漏。加一个独立前缀即可。 - 两个用户可见的行为变更没写进描述:查询从「过滤」变成「高亮 + 上一处/下一处」,但输入框文案仍是「筛选日志」;日志从自动换行变成不换行 + 横向滚动。虚拟化需要固定行高,我理解不换行是有意取舍,但这是产品可见改动,建议确认后写进描述。
sessions.ts:219的tailSet: true是行为变更:它修掉了「tailLines: 1000没带tailSet,服务端实际按全量返回」这个隐藏问题(赞),但同时把对话回退从全量日志变成最后 1000 行,会影响老 sandbox「对话」tab 能显示的历史长度。建议单独说明/确认。lines的派生方式可能达不到本 PR 的目标:每 150ms 的 flush 会改windows→lines重算 → 对所有 run 重跑runLines()→eventLines()里的events.filter+formatRunEvent()格式化。也就是每秒 6~7 次全量重算,和「减少卡顿」的目标相反。建议把每个 run 的 event lines 单独缓存(只依赖events/activeStream)。
⚪ 小问题(可以合并后跟进)
- 滚动锚定多算 2 行:
prependLines:221的addedRows = earlier.length + runHeadingRows(runId),但窗口还没 flush 时runLines渲染的是[heading, placeholder]两行,prepend N 行后变成[heading, ...N],实际位移是N-1。 - 多个 run 同时 prepend(
loadEarlier用Promise.all)时,run-log-viewer.svelte:92用anchoredOffset这个只在异步 scroll 事件里更新的值计算scrollTop,后一次会覆盖前一次的补偿,丢掉位移。 download()的 AbortController(:380)没有登记到controllers,卸载/切 sandbox 不会取消。- 虚拟化后外层用
w-max,宽度只由已挂载的行决定,纵向滚动时横向滚动范围会跳动(旧实现所有行都在 DOM 里,宽度稳定)。 :413那个 hidden + aria-hidden、只为 e2e 计数存在的data-log-sectiondiv。
✅ 已核对、确认没问题
loadEarlierRun不会被 metadata 空 chunk 提前打断:后端initialRunLogOffset在tailSet=false, startOffset=0时返回 0,不满足chunk.offset >= windowStart。textBefore的字节截断是安全的:tailRunLogOffset返回的是\n之后的位置(按行对齐),后端也保证每个 chunk 是完整 UTF-8,不会切坏字符或半行。windowStart由首块带正文的 chunk 推导,与 metadata 里的 offset 一致。- 新增 i18n key 与代码里用到的完全对应,没有遗漏。
RunDetail保持全量 + follow,符合描述。
Keep stream bookkeeping in plain maps so opening a tail cannot retrigger the sandbox reset, and load finished runs one at a time.
|
已修。流登记改成普通 Map/Set,重置 effect 只依赖 sandboxId,打开 tail 不会再把自己标脏。已结束的 run 改回串行拉取,正在运行的才保持 follow。tail 和「加载更早日志」使用不同的 controller 键,下载在切换执行环境时会取消。 e2e 不再用 |
摘要
Webhook 事件详情和执行环境详情的运行日志不再一次拉取并渲染全部历史。首屏只请求最近 2000 行,用虚拟行列表显示;更早内容按需加载,完整日志只在下载时读取。
测试
npm run check:uinpx eslint覆盖本次改动文件