Skip to content

perf: tail and virtualize webhook event logs - #50

Merged
winterfx merged 6 commits into
mainfrom
perf/webhook-event-log-tail
Sep 26, 2026
Merged

winterfx merged 6 commits into
mainfrom
perf/webhook-event-log-tail

Conversation

@yorkew-east8

@yorkew-east8 yorkew-east8 commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

摘要

Webhook 事件详情和执行环境详情的运行日志不再一次拉取并渲染全部历史。首屏只请求最近 2000 行,用虚拟行列表显示;更早内容按需加载,完整日志只在下载时读取。

  • 日志分片先缓冲再刷新,避免每个 64KB 分片拷贝整段字符串并触发重渲染。
  • 窗口起点记在第一块带正文的日志上,元数据空块不会隐藏「加载更早日志」。向前加载必须读到尾部窗口起点才拼接;中途断开保留按钮和已显示的尾部。
  • 事件时间线超过 240 字默认截断,可按条展开。完整消息仍保留给无 run 的日志回退。该展示也用于没有执行环境时的执行概览。
  • 工作台仍等执行环境、run 列表、历史和 execution events 都返回后才显示。有对话时默认停在「对话」,否则停在「运行日志」。
  • 运行详情页仍订阅完整日志。

测试

  • npm run check:ui
  • npx eslint 覆盖本次改动文件
  • 未在浏览器中用上万行会话做端到端点击验证。Playground 当前未启动,受影响 e2e 依赖保留的执行环境。

王越 added 3 commits September 26, 2026 13:34
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.
@yorkew-east8

Copy link
Copy Markdown
Collaborator Author

@monkeyscan 评审下这个PR

@monkeyscan

monkeyscan Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

PR Title: perf: tail and virtualize webhook event logs

Commit: 1737dcd

整体评估

本次变更重构了运行日志查看链路:RunLogViewer 改为按行虚拟滚动并新增搜索计数/跳转,sandbox-log-runs 改为按 run 拉取 tail 分片并支持向前补载更早日志,followRunLogs 改为结构化 chunk 与 options 参数,另含事件时间线展开与文案补充。整体方向一致,新契约与各调用方基本同步更新,未发现数据完整性或安全层面的高风险问题,问题主要集中在日志检索与预插日志的交互行为上。

总结

整体可合入,但没有阻断性缺陷。最需要留意的是新增日志检索“日志搜索没有任何匹配高亮,用户看不到匹配位置”(只有计数,匹配行与普通行外观相同)以及“点击“加载更早日志”后视口会跳到其它内容”(预插更早日志未补偿滚动位置)。这两点都出现在新特性的核心使用路径上,建议合并前补齐。

Findings (2)

  • 🟡 [MEDIUM] src/lib/components/run-log-viewer.svelte:156 — 日志搜索没有任何匹配高亮,用户看不到匹配位置
  • 🟡 [MEDIUM] src/lib/components/sandbox-log-runs.svelte:222 — 点击“加载更早日志”后视口会跳到其它内容
Prompt To Fix All With AI
# Review 1

This is a comment left during a code review.
Repository: https://github.com/chaitin/agent-compose-ui
Path: src/lib/components/run-log-viewer.svelte
Line: 155-156

Comment:
日志搜索没有任何匹配高亮,用户看不到匹配位置

在 src/lib/components/run-log-viewer.svelte 中,模板只给当前匹配行设置 data-log-match="active",其余匹配行与普通行完全相同,而且工程内(含 src/app.css)没有任何针对 data-log-match 的样式;同时输入关键字后视图也不会自动滚动到首个匹配。结果是用户输入筛选词后仅能看到 "1 / N" 计数,日志里看不到任何可见的高亮或定位(点击“下一处”还会直接跳到第 2 个匹配,第 1 个匹配始终不被展示),在长日志里无法定位匹配内容。

# Review 2

This is a comment left during a code review.
Repository: https://github.com/chaitin/agent-compose-ui
Path: src/lib/components/sandbox-log-runs.svelte
Line: 215-222

Comment:
点击“加载更早日志”后视口会跳到其它内容

在 src/lib/components/sandbox-log-runs.svelte 的 prependLines 中,加载到的更早日志被直接插到 lines 顶部,但 run-log-viewer.svelte 的滚动位置以像素(scrollTop)记录,插入后没有任何补偿。当用户已经向上滚动(followsLatest 为 false,正是想查看更早日志的场景)时点击“加载更早日志”,滚动偏移不变而全部内容整体下移,视口会跳到与刚才不同的位置,用户丢失原本的阅读位置。
---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/lib/components/run-log-viewer.svelte Outdated
Comment thread src/lib/components/sandbox-log-runs.svelte
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.
@yorkew-east8

Copy link
Copy Markdown
Collaborator Author

@monkeyscan 评审下这个PR的最新提交

@monkeyscan

monkeyscan Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

PR Title: perf: tail and virtualize webhook event logs

Commit: 6054377

整体评估

本次改动把沙箱/运行日志查看从“一次性渲染整段文本”改为窗口化加虚拟滚动:sandbox-log-runs.svelte 先拉取日志尾部(2000 行)并支持按字节偏移惰性加载更早内容,run-log-viewer.svelte 改为按行虚拟渲染并新增匹配定位与“加载更早日志”,api/runs.ts 把 followRunLogs 改为选项对象并暴露带偏移的 RunLogChunk,RunDetail、sessions、workbench、event-timeline、i18n 与 e2e 同步适配。整体方向合理,大日志的渲染与内存压力明显改善,各调用方的签名迁移基本自洽。

总结

整体可用,主要风险集中在新增的搜索与滚动保持逻辑:实时跟随的日志下搜索会被反复重置回第一处匹配,多运行并行“加载更早日志”时共享的行数补偿可能把视口推到错误位置。这两处建议在合并前修正。

Findings (2)

  • 🟡 [MEDIUM] src/lib/components/run-log-viewer.svelte:75 — 实时日志下搜索会不断被重置回第一处匹配
  • 🟡 [MEDIUM] src/lib/components/sandbox-log-runs.svelte:227 — 多个运行同时加载更早日志时共享补偿量会把视口推到错误位置
Prompt To Fix All With AI
# Review 1

This is a comment left during a code review.
Repository: https://github.com/chaitin/agent-compose-ui
Path: src/lib/components/run-log-viewer.svelte
Line: 68-75

Comment:
实时日志下搜索会不断被重置回第一处匹配

run-log-viewer.svelte 第 68-75 行的查询 $effect 在 matchIndexes 变化时都会把 activeMatch 置 0 并调用 revealMatch(0),而 matchIndexes 是由 lines 派生的;RUNNING 运行的日志分片每约 150ms 追加一次并重建 lines/matchIndexes,从而反复触发该 effect。后果:在仍在输出的运行里使用搜索时,视口和“当前第几处”会被不停拉回第一处匹配,上一处/下一处实际无法使用。

# Review 2

This is a comment left during a code review.
Repository: https://github.com/chaitin/agent-compose-ui
Path: src/lib/components/sandbox-log-runs.svelte
Line: 217-227

Comment:
多个运行同时加载更早日志时共享补偿量会把视口推到错误位置

sandbox-log-runs.svelte 的 loadEarlier 会并行地为所有 hasEarlier 的运行加载更早内容,prependLines(约 217-227 行)把每个运行新增的行数累加进同一个 preserveLine,而 run-log-viewer.svelte 把这个总和当作唯一的滚动补偿量。当被补偿的某个运行位于当前视口下方(例如用户在读最早的那个运行)时,它新增的行并不会使正在阅读的内容下移,补偿却仍按总行数上推 scrollTop,视口会跳离原位置。
---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/lib/components/run-log-viewer.svelte
Comment thread src/lib/components/sandbox-log-runs.svelte
Reset a log search only when the query changes, and compensate earlier-log inserts only for sections above the anchored line.

@winterfx winterfx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论

方向认可: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),于是形成闭环:

  1. 订阅 effect 给每个 run 开 tail,controllers.set 写入新 key;
  2. 重置 effect 被标脏重跑 → abort 全部、clear()、requested.clear()、windows = {};
  3. 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-section div。

✅ 已核对、确认没问题

  • 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.
@yorkew-east8

Copy link
Copy Markdown
Collaborator Author

已修。流登记改成普通 Map/Set,重置 effect 只依赖 sandboxId,打开 tail 不会再把自己标脏。已结束的 run 改回串行拉取,正在运行的才保持 follow。tail 和「加载更早日志」使用不同的 controller 键,下载在切换执行环境时会取消。

e2e 不再用 data-log-pending=false 判断加载完成,那条对仍在 follow 的 run 不会结束。浏览器端的上万行点击验证这轮还没跑。

@winterfx
winterfx merged commit 5042d51 into main Sep 26, 2026
1 check passed
@winterfx
winterfx deleted the perf/webhook-event-log-tail branch September 26, 2026 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants