perf(logs): 日志事件按窗口合并推送,抽屉改为虚拟滚动
- 后端 LogSink 把逐行事件按 16 KiB / 100 ms 合并成一条 log 事件,文件写入保持即时;后台 tick 与析构 flush 保证低输出和收尾都不丢尾部 - 前端新增 LogViewer:固定行高虚拟滚动、跟随底部/回到最新,增量按 rAF 合并,长日志 DOM 节点数恒定 - 日志抽屉改为行缓冲(lib/logLines)+ 可见窗口计算(lib/logWindow) - 详情区改 flex 布局,日志面板吃掉剩余高度,回到最新按钮不再被裁到面板外 - 测试:Rust 单元 52→57,Vitest 13→28,Playwright 4→5(1500 行虚拟滚动) - 文档:README 补充合并推送与虚拟滚动,新增 ADR-0008
This commit is contained in:
@@ -264,3 +264,63 @@ test('renders without external network access', async ({ page }) => {
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('keeps a long run log scrollable without rendering every line', async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const headers = await authHeaders(request)
|
||||
await request.delete('/api/v1/tasks/e2e_long_log', { headers })
|
||||
const response = await request.post('/api/v1/tasks', {
|
||||
headers,
|
||||
data: {
|
||||
...taskPayload,
|
||||
id: 'e2e_long_log',
|
||||
name: 'Long log task',
|
||||
timeout_secs: 60,
|
||||
action: {
|
||||
...taskPayload.action,
|
||||
args: ['for /L %i in (1,1,1500) do @echo line-%i'],
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(response.status()).toBe(201)
|
||||
|
||||
await page.goto('/')
|
||||
await page
|
||||
.getByRole('row', { name: /Long log task/ })
|
||||
.getByRole('button', { name: '运行' })
|
||||
.click()
|
||||
const drawer = page.getByRole('dialog', { name: 'Long log task' })
|
||||
await expect(drawer).toBeVisible()
|
||||
await expect(drawer.locator('.status-succeeded').first()).toBeVisible({
|
||||
timeout: 60_000,
|
||||
})
|
||||
// 自动跟随到底部,最后几行在视口内。
|
||||
await expect(drawer.getByText('line-1500')).toBeVisible()
|
||||
|
||||
// 虚拟滚动:DOM 中只保留视口附近的少量行,而不是 1500 行全量渲染。
|
||||
const rendered = await drawer.locator('.log-line').count()
|
||||
expect(rendered).toBeLessThan(200)
|
||||
expect(rendered).toBeGreaterThan(5)
|
||||
|
||||
await drawer.locator('.log-viewer').evaluate((node) => {
|
||||
node.scrollTop = node.scrollHeight / 2
|
||||
})
|
||||
const jump = drawer.getByRole('button', { name: '回到最新 ↓' })
|
||||
await expect(jump).toBeVisible()
|
||||
const logBox = await drawer.locator('.log-viewer').boundingBox()
|
||||
const jumpBox = await jump.boundingBox()
|
||||
expect(logBox).not.toBeNull()
|
||||
expect(jumpBox).not.toBeNull()
|
||||
if (logBox && jumpBox) {
|
||||
// 按钮必须落在日志面板可视范围内,而不是被面板裁剪到外面。
|
||||
expect(jumpBox.y + jumpBox.height).toBeLessThanOrEqual(
|
||||
logBox.y + logBox.height + 1,
|
||||
)
|
||||
}
|
||||
await page.screenshot({
|
||||
path: 'test-results/dashboard-log-drawer-long.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user