fix: stabilize background operations and large embedding results

This commit is contained in:
2026-09-06 16:26:17 +08:00
parent 874e916106
commit 3b9490e3fb
73 changed files with 3847 additions and 149 deletions
+2
View File
@@ -97,3 +97,5 @@
- [Markdown 语法预设与外部文件刷新](development/Markdown语法预设与外部文件刷新.md)
- [长文渲染优化与压测报告](development/长文渲染优化与压测报告.md)
- [Agent 与任务压测报告](development/Agent与任务压测报告.md)
- [后台运行日志与压力问题修复](development/后台运行日志与压力问题修复.md)
@@ -204,6 +204,17 @@ RunCancelled
- `POST /api/workspace/open` 返回可使用的 WorkspaceSnapshot,不等待向量推理。
- `PATCH /api/notes/{note_id}` 成功代表正文、元数据和 FTS 已保存;后台向量失败不撤销这次保存。
- `GET /api/index/status` 新增 `vector_refresh_required: boolean`,表示工作区或笔记存在向量待处理标记。该字段不是进度百分比;任务失败时也可为 true。
- `pending_jobs` 返回真实未完成索引任务数(包含运行中),不再固定为 0。全库待重建或运行中的全量重建计一个任务;逐笔记刷新按待处理标记计数。`running_jobs` 返回当前运行数。失败后保留的待重建标记仍计入未完成数。
- `active_searches``completed_searches``failed_searches``cancelled_searches` 分别表示向量/混合检索的进行中、完成、失败、取消次数,覆盖搜索、对话和 Agent 经统一检索引擎发起的调用,排除纯 FTS;混合检索回退 FTS 后成功仍计完成。计数仅保存在当前服务进程内,重启归零,与索引队列互相独立。
- 设置与搜索页每秒轮询状态,其他页面空闲时每 5 秒轮询;不是事件推送。短检索可能无法观察到进行中状态,但完成/失败计数会保留。设置页与底部状态栏复用状态标签,未知字段显示“未获取”。
- `POST /api/index/rebuild` 仍仅支持全量重建,并等待结果;不要将上述异步语义推广到所有索引 API。
状态、恢复限制与验证见 [工作区后台索引与保存开发说明](../development/工作区后台索引与保存开发说明.md)。
## 2026-09-06:统一运行日志
`GET /api/logs` 返回独立持久化的后台操作日志,无需打开 Vault。参数:`limit` 默认 50、最大 200`before` 为上一页 next_cursor`level` 为 INFO/WARNING/ERROR/CRITICAL 或空;`source` 按模块精确匹配;`q` 在事件名和脱敏元数据中做字面搜索。
返回 `items: [{id,timestamp,level,source,event,details}]``next_cursor`(无后续页时 null)、`sources``pending``dropped``write_failures``retention`。日志按 ID 倒序,保留最近 20,000 条。响应头 `X-Request-ID` 与后台日志关联。不得依赖日志记录笔记正文、工具参数、凭据或原始异常消息。
队列、错误处理、字段白名单和验证方法见 [后台运行日志与压力问题修复](../development/后台运行日志与压力问题修复.md)。
@@ -0,0 +1,156 @@
# Agent 与任务压测报告
> 日期:2026-09-06。范围:当前第二阶段 Agent Runtime、任务 API、任务列表及 Trace 组件。测试在 Windows 11、Python 3.12.6、独立无头 Chrome 下执行,前端为 Vite 开发模式,纸间时光版本 1.8.1。
## 1. 结论
功能正确性检查通过:Agent 工具调用、事件序号与回放、终态持久化恢复、容量拒绝、取消、权限等待、故障隔离,以及任务增删改查和分页均得到预期结果。性能仍有三项明确问题:
| 优先级 | 问题 | 本轮证据 | 建议方向 |
| --- | --- | --- | --- |
| P1 | 任务列表未加载后续页;Agent 历史列表也没有后续分页入口 | 100、1000 条任务的实际页面均只请求 `/api/tasks` 并显示 50 条;Agent store 同样仅调用默认分页一次,接口默认 50 | 增加服务端筛选与分页/加载更多,显示总量,防止统计仅基于已加载项 |
| P1 | 高并发 Agent 阻塞事件循环 | 200 并发的完成延迟 P95 约 10.55 秒;进程内心跳最大延迟约 3.56 秒 | 分析同步持久化,设计有界执行调度及顺序写入,保留事件落盘、终态与回放一致性 |
| P1 | 大 Trace 的树形筛选长时间阻塞 | 2000 事件树形搜索约 483617 ms10000 事件约 25.91 秒 | 为匹配的 sequence/tool/model ID 建 Set,避免逐节点重复遍历事件;再评估分段显示及增量更新 |
本次新增的是可复现压测与报告,没有把上述生产问题标记为已修复。页面滚动与树形搜索是不同瓶颈:10000 事件的滚动 P95 约 40 ms,并不能说明搜索交互也流畅。
## 2. 隔离与方法
- 后端脚本在导入任何 app 模块前,将 `APP_DATA_DIR``APP_DB_PATH``APP_VAULT_PATH` 指向临时目录,结束后清理;不读写用户 Vault、现有任务及 Provider 凭据。
- Agent 使用内置 Mock Provider,每轮模型调用注入 50 ms 异步等待,正常样本包含一次 `system.echo` 工具调用和两轮模型调用;不调用真实厂商,也不衡量回答质量或真实模型吞吐。
- 进程内运行直接调用真实 Runtime、SQLite 和任务 ASGI 路由。任务 HTTP 复核另起独立 Uvicorn 进程,用真实回环连接执行,避免将 ASGI 驱动的连续无让出执行误解为网络服务延迟。
- 前端挂载真实 TasksView、TraceTimeline 和 Pinia store。压测页拦截全部 fetch,不向真实后端发出请求。任务接口夹具保留默认 50 条分页,先测实际加载条数,再注入完整列表测试渲染上限,两者分开记录。
- Trace 由确定性模型开始/完成、工具调用/结果事件组成,带调用与父节点 ID;测量首次渲染、时间线过滤、树形切换、匹配全部节点的树形搜索。
- 滚动用 CDP 派发 120 次滚轮事件,记录真实滚动容器及 `maxScrollTop`,防止对未滚动页面统计帧间隔。Trace 测试容器显式解除应用根节点的裁剪,由独立滚动区承担真实 Agent 页滚动容器的职责。
本轮每个前端配置测一次;开发编译、后台负载、缓存和浏览器调度都会影响结果,数据用于发现瓶颈,不作为生产环境 SLA。首次渲染计时不包含模块下载,包含 Vue 更新及两个 animation frame。
## 3. Agent 后端
### 3.1 正常工具调用与回放
| 并发数 | 运行数 | 完成延迟中位数(ms) | 完成延迟 P95(ms) | 场景心跳最大延迟(ms) |
| ---: | ---: | ---: | ---: | ---: |
| 1 | 20 | 146.20 | 170.13 | 26.84 |
| 10 | 20 | 405.75 | 505.74 | 136.71 |
| 50 | 50 | 2234.45 | 2304.42 | 798.84 |
| 200 | 200 | 10262.47 | 10554.52 | 3559.97 |
290 个正常运行全部完成,工具返回内容逐项一致;实时订阅事件序号连续,`after_sequence=2` 回放与原事件后缀一致。新的 Runtime 实例可以从 SQLite 恢复这些终态运行。订阅者集合最终清空,进程内保留记录不超过 200。
“并发”是同时提交的协程数,不代表独立 CPU 工作线程。当前 queued 是启动前状态,Runtime 并没有把超额活跃运行无限排入执行队列。场景心跳还包含提交、持久化回放与恢复校验,不能将其全部归为模型执行耗时;50、200 并发场景的心跳采样数仅 8 个,表中因此列最大值而非宣称稳定分位数。
代码定位:`AgentRuntime._publish()` 同步调用 `AgentTraceRepository.append_event()`,后者逐事件连接 SQLite 并提交事务;`connect()` 还加载扩展和检查迁移。这是需要进一步拆分测量的阻塞路径,本轮未完成各子步骤 CPU/磁盘成本归因。
### 3.2 容量、取消与故障注入
- 保持 200 个模型调用活跃,第 201 次创建得到 `AgentCapacityError`;随后取消全部 200 个运行,全部进入 cancelled,后台任务均结束。
- 20 个运行等待权限:10 个允许后完成,10 个等待中取消,订阅者均释放。
- 20 个混合运行:5 个正常完成、5 个 Provider 异常、5 个模型超时、5 个工具超时,均获得预期状态或错误码;工具执行器剩余数量为 0。
- 正常回放和故障场景合计创建 530 个运行,临时数据库最终约 2.95 MB。工具超时被记录为 ToolResult 错误,后续 Mock 模型仍可能完成运行,不将它误算为整个 Run 必然失败。
这里的“恢复”是新 Runtime 读取终态持久化数据,未模拟 OS 强杀时的写入中断。SSE 通过 Runtime 的事件生成器验证序号及断点回放,没有进行真实网络慢读者/断网压力测试。
## 4. 任务 API
进程内分别对 100、1000 条任务执行创建、逐页读取、更新为 done、删除,所有 ID 完整,最终总量为 0。默认接口每页 50 条,显式每页 100 条遍历可以取回全部数据。
ASGITransport 的 1000 条突发操作中,心跳出现约 14 秒延迟:测试客户端与同步路由处于同一事件循环,连续就绪协程缺少网络等待,不能将其视作真实 HTTP 健康检查延迟。因此增加独立 Uvicorn + 回环 HTTP 复核:
| 操作 | 数量 | P95ms | 最大值(ms |
| --- | ---: | ---: | ---: |
| 创建 | 1000 | 116.45 | 134.91 |
| 更新 | 1000 | 159.29 | 193.37 |
| 删除 | 1000 | 156.43 | 190.10 |
| 分页/收尾查询 | 11 | 20.96 | 20.96 |
| 并行健康检查 | 117 | 157.00 | 195.07 |
HTTP 客户端并发 20,总耗时约 18.31 秒;健康检查零失败,分页无重复/遗漏,最终任务数为 0。此处是本机单次结果,未覆盖跨网络、长期持久负载或多进程同时写同一数据库。
## 5. 前端
### 5.1 任务列表
100、1000 条数据的实际加载阶段都只有一次 `/api/tasks` 请求,store 中均为 50 条。以下是额外注入完整 1000 条数据后的结果,不是生产页面当前可以加载 1000 条的证明。
| 主题 | 完整列表渲染(ms) | 完成状态筛选(ms) |
| --- | ---: | ---: |
| 默认浅色 | 117.4 | 44.1 |
| 默认深色 | 137.8 | 47.8 |
| 纸间时光 1.8.1 | 150.0 | 59.5 |
1000 条列表约 11007 个 DOM 节点,实际滚动最远达到 81000 px;筛选 done 得到 334 条,与夹具期望一致。100 条样本筛选得到 34 条,也一致。默认浅色的 1000 条滚动 P95 约 30.2 ms,纸间时光约 50.1 ms;样本数少,暂不据此认定新的单一 CSS 根因。
### 5.2 Agent Trace
| 事件数/主题 | 首次渲染(ms) | 时间线搜索(ms) | 树形切换(ms) | 树形全匹配搜索(ms) |
| --- | ---: | ---: | ---: | ---: |
| 2000 / 默认浅色 | 283.8 | 97.4 | 64.0 | 617.2 |
| 2000 / 默认深色 | 289.0 | 74.5 | 62.9 | 483.0 |
| 2000 / 纸间时光 | 337.3 | 86.3 | 62.1 | 540.1 |
| 10000 / 纸间时光 | 1803.9 | 582.8 | 317.5 | 25914.8 |
200 事件的树形搜索约 20–29 ms。2000 事件时间线约 20542 个 DOM 节点,10000 事件约 102542 个。查询“压力测试 1”分别命中 444、4444 条事件,与直接检查输入数据的结果一致;树形搜索“压力测试”匹配所有事件,会自动展开匹配子树。
代码定位:`TraceTimeline.filteredTree` 为每个节点调用 `filteredEvents.some(...)`。节点数和匹配事件数一起增加时,会产生近似二次增长的匹配工作,随后还需构造并渲染展开子树。建议先用匹配 ID 集合消除嵌套扫描,再测 DOM 更新成本;不要仅用防抖隐藏单次 25 秒阻塞。
10000 是扩大数据规模的诊断测试,高于 Runtime 默认内存事件保留量 2000;它直接向组件传入事件,不代表当前单次 API 页或实时 store 默认会收到这一数量。也未覆盖逐事件 SSE 增量刷新的端到端成本。
## 6. 复现与原始数据
```powershell
# 后端:独立临时数据,离线 Mock
backend/.venv/Scripts/python.exe backend/scripts/agent-task-stress.py --output .local-plans/agent-task-backend.json
# 真实 HTTP:独立 Uvicorn、随机回环端口
backend/.venv/Scripts/python.exe backend/scripts/task-http-stress.py --count 1000 --concurrency 20 --output .local-plans/task-http.json
# 前端:先启动独立 Vite,另一个终端执行后续命令
npm --prefix frontend run dev -- --port 5175 --strictPort
backend/.venv/Scripts/python.exe frontend/tests/performance/run-stress.py --url 'http://127.0.0.1:5175/tests/performance/agent-task.html?kind=tasks&theme=paper-moments' --scroll --sizes 100 1000 --runs 1 --output .local-plans/task-ui.json
backend/.venv/Scripts/python.exe frontend/tests/performance/run-stress.py --url 'http://127.0.0.1:5175/tests/performance/agent-task.html?kind=trace&theme=paper-moments' --scroll --sizes 200 2000 10000 --runs 1 --output .local-plans/trace-ui.json
```
URL 的 theme 可换为 light、dark。测试应串行运行,避免不同负载相互争用;10k Trace 树形筛选可能长时间占用测试浏览器。浏览器使用临时用户目录,不接管用户当前浏览器。
- [Agent 与进程内任务 API 数据](performance/2026-09-06-agent-task-backend.json)
- [真实 HTTP 任务数据](performance/2026-09-06-task-http.json)
- [前端 13 组样本数据](performance/2026-09-06-agent-task-ui.json)
- [前端压测工具](../../frontend/tests/performance/README.md)
附加回归:`test_agent_core.py``test_api.py` 共 26 项通过。脚本中的状态、序号、输出、分页与筛选断言均通过。未提交或推送本次材料。
## 7. 修复后复测(2026-09-06
以上保留首次压测基线。本节对应后台 Trace 批量写入、任务后台写入、完整列表加载、Trace 集合匹配/分页和统一日志接入后的实现。样本开启新操作日志,仍只调用隔离 Mock。
| 项目 | 修复前 | 修复后 |
| --- | ---: | ---: |
| 200 并发 Agent 完成 P95 | 10,554.52 ms | 1,233.12 ms |
| 200 并发测量区间事件循环最大延迟 | 3,559.97 ms | 509.00 ms |
| 纸间时光 10k Trace 首次渲染 | 1,803.9 ms | 94.4 ms |
| 纸间时光 10k Trace 树形全匹配筛选 | 25,914.8 ms | 189.2 ms |
| 纸间时光 10k Trace 首屏 DOM 节点 | 约 102,542 | 1,852 |
| 1,000 条任务首次实际加载 | 50 条 | 1,000 条(10 次 API 请求) |
Agent 测量区间还包含同步读回、校验和重启恢复,最大心跳延迟不是纯执行阶段指标。290 次普通运行、200 个容量/取消场景、20 个权限场景、20 个失败/超时场景全部通过,订阅者和工具执行器释放。不能声称消除了所有主线程工作。
Trace 搜索检查完整数据,只有渲染分页。10k 样本滚动 frame gap P95 为 10.1 ms,无 longtask;时间线筛选 284.9 ms,树形切换 100.9 ms。这是单次本机样本,不代表所有硬件与实时 SSE 输入。
1,000 条任务界面每页 100 条;筛选后数据总数 334,当前页 100。筛选 18.7 ms,滚动 frame gap P95 为 30 ms,无 longtask。断言同时检查完整数据量和分页渲染数量。
真实 HTTP 测试 1,000 条任务、20 并发,CRUD 全部成功,分页完整,最终任务数 0:
| 指标 | 修复前 | 修复后 |
| --- | ---: | ---: |
| health P95 | 157.00 ms | 38.64 ms |
| 创建 P95 | 116.45 ms | 157.68 ms |
| 更新 P95 | 159.29 ms | 208.68 ms |
| 删除 P95 | 156.43 ms | 205.45 ms |
| 总耗时 | 18.31 s | 24.51 s |
后台排队和操作记录改善了无关请求的响应,但这组样本写入吞吐下降,不能描述为 CRUD 全面提速。后续可以评估任务事务批量化与连接初始化开销;本次没有降低 SQLite 持久化级别。
- [Agent / 进程内任务复测](performance/2026-09-06-agent-task-backend-fixed.json)
- [真实 HTTP 任务复测](performance/2026-09-06-task-http-fixed.json)
- [Trace 浏览器复测](performance/2026-09-06-agent-trace-ui-fixed.json)
- [任务浏览器复测](performance/2026-09-06-task-ui-fixed.json)
- [日志架构与验收说明](后台运行日志与压力问题修复.md)
@@ -0,0 +1,230 @@
{
"python": "3.12.6",
"platform": "Windows-11-10.0.26100-SP0",
"provider": "mock with 50 ms injected delay per model turn; no network",
"results": [
{
"scenario": "agent_tool_runs",
"concurrency": 1,
"runs": 20,
"latency": {
"count": 20,
"median_ms": 150.79,
"p95_ms": 169.57,
"max_ms": 178.52
},
"completed": 20,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 20,
"elapsed_ms": 3047.59,
"event_loop_lag": {
"count": 995,
"median_ms": 0,
"p95_ms": 5.65,
"max_ms": 21.83
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 10,
"runs": 20,
"latency": {
"count": 20,
"median_ms": 187.48,
"p95_ms": 214.02,
"max_ms": 214.58
},
"completed": 20,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 40,
"elapsed_ms": 514.15,
"event_loop_lag": {
"count": 192,
"median_ms": 0,
"p95_ms": 4.82,
"max_ms": 20.02
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 50,
"runs": 50,
"latency": {
"count": 50,
"median_ms": 364.9,
"p95_ms": 366.99,
"max_ms": 367.3
},
"completed": 50,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 90,
"elapsed_ms": 526.56,
"event_loop_lag": {
"count": 172,
"median_ms": 0,
"p95_ms": 4.57,
"max_ms": 69.89
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 200,
"runs": 200,
"latency": {
"count": 200,
"median_ms": 1118.63,
"p95_ms": 1233.12,
"max_ms": 1237.31
},
"completed": 200,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 200,
"elapsed_ms": 1839.2,
"event_loop_lag": {
"count": 470,
"median_ms": 0,
"p95_ms": 2.6,
"max_ms": 509.0
}
},
{
"scenario": "capacity_and_cancel",
"active_limit": 200,
"overflow_rejected": true,
"cancelled": 200,
"cancel_latency": {
"count": 200,
"median_ms": 7.71,
"p95_ms": 10.8,
"max_ms": 17.37
},
"elapsed_ms": 3613.48,
"event_loop_lag": {
"count": 1613,
"median_ms": 0,
"p95_ms": 0,
"max_ms": 7.86
}
},
{
"scenario": "permission_wait",
"runs": 20,
"approved_completed": 10,
"cancelled_waiting_permission": 10,
"subscribers_released": true,
"elapsed_ms": 388.83,
"event_loop_lag": {
"count": 72,
"median_ms": 0,
"p95_ms": 9.9,
"max_ms": 15.27
}
},
{
"scenario": "failure_and_timeout_isolation",
"runs": 20,
"success": 5,
"provider_errors": 5,
"model_timeouts": 5,
"tool_timeouts": 5,
"remaining_tool_executors": 0,
"elapsed_ms": 1295.29,
"event_loop_lag": {
"count": 152,
"median_ms": 0,
"p95_ms": 7.2,
"max_ms": 17.24
}
},
{
"scenario": "task_api_crud",
"tasks": 100,
"client_concurrency": 20,
"latencies": {
"create": {
"count": 100,
"median_ms": 214.09,
"p95_ms": 264.51,
"max_ms": 309.99
},
"update": {
"count": 100,
"median_ms": 221.48,
"p95_ms": 403.74,
"max_ms": 495.1
},
"list": {
"count": 1,
"median_ms": 7.92,
"p95_ms": 7.92,
"max_ms": 7.92
},
"delete": {
"count": 100,
"median_ms": 232.9,
"p95_ms": 468.48,
"max_ms": 475.63
}
},
"default_page_count": 50,
"default_total": 100,
"pagination_complete": true,
"final_total": 0,
"elapsed_ms": 3673.22,
"event_loop_lag": {
"count": 2337,
"median_ms": 0,
"p95_ms": 0,
"max_ms": 118.92
}
},
{
"scenario": "task_api_crud",
"tasks": 1000,
"client_concurrency": 20,
"latencies": {
"create": {
"count": 1000,
"median_ms": 200.87,
"p95_ms": 237.75,
"max_ms": 376.8
},
"update": {
"count": 1000,
"median_ms": 220.04,
"p95_ms": 260.19,
"max_ms": 300.07
},
"list": {
"count": 10,
"median_ms": 7.89,
"p95_ms": 9.61,
"max_ms": 9.61
},
"delete": {
"count": 1000,
"median_ms": 218.1,
"p95_ms": 266.23,
"max_ms": 316.14
}
},
"default_page_count": 50,
"default_total": 1000,
"pagination_complete": true,
"final_total": 0,
"elapsed_ms": 32562.93,
"event_loop_lag": {
"count": 24893,
"median_ms": 0,
"p95_ms": 0,
"max_ms": 91.85
}
}
],
"database_bytes": 2981888,
"complete": true
}
@@ -0,0 +1,230 @@
{
"python": "3.12.6",
"platform": "Windows-11-10.0.26100-SP0",
"provider": "mock with 50 ms injected delay per model turn; no network",
"results": [
{
"scenario": "agent_tool_runs",
"concurrency": 1,
"runs": 20,
"latency": {
"count": 20,
"median_ms": 146.2,
"p95_ms": 170.13,
"max_ms": 171.23
},
"completed": 20,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 20,
"elapsed_ms": 3041.03,
"event_loop_lag": {
"count": 190,
"median_ms": 5.64,
"p95_ms": 16.81,
"max_ms": 26.84
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 10,
"runs": 20,
"latency": {
"count": 20,
"median_ms": 405.75,
"p95_ms": 505.74,
"max_ms": 539.28
},
"completed": 20,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 40,
"elapsed_ms": 1045.19,
"event_loop_lag": {
"count": 15,
"median_ms": 57.63,
"p95_ms": 136.71,
"max_ms": 136.71
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 50,
"runs": 50,
"latency": {
"count": 50,
"median_ms": 2234.45,
"p95_ms": 2304.42,
"max_ms": 2307.54
},
"completed": 50,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 90,
"elapsed_ms": 2552.49,
"event_loop_lag": {
"count": 8,
"median_ms": 345.24,
"p95_ms": 798.84,
"max_ms": 798.84
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 200,
"runs": 200,
"latency": {
"count": 200,
"median_ms": 10262.47,
"p95_ms": 10554.52,
"max_ms": 10584.03
},
"completed": 200,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 200,
"elapsed_ms": 11442.28,
"event_loop_lag": {
"count": 8,
"median_ms": 1647.87,
"p95_ms": 3559.97,
"max_ms": 3559.97
}
},
{
"scenario": "capacity_and_cancel",
"active_limit": 200,
"overflow_rejected": true,
"cancelled": 200,
"cancel_latency": {
"count": 200,
"median_ms": 4.32,
"p95_ms": 5.36,
"max_ms": 8.27
},
"elapsed_ms": 3645.04,
"event_loop_lag": {
"count": 3,
"median_ms": 14.04,
"p95_ms": 3610.52,
"max_ms": 3610.52
}
},
{
"scenario": "permission_wait",
"runs": 20,
"approved_completed": 10,
"cancelled_waiting_permission": 10,
"subscribers_released": true,
"elapsed_ms": 971.64,
"event_loop_lag": {
"count": 11,
"median_ms": 49.09,
"p95_ms": 314.46,
"max_ms": 314.46
}
},
{
"scenario": "failure_and_timeout_isolation",
"runs": 20,
"success": 5,
"provider_errors": 5,
"model_timeouts": 5,
"tool_timeouts": 5,
"remaining_tool_executors": 0,
"elapsed_ms": 1531.14,
"event_loop_lag": {
"count": 83,
"median_ms": 5.17,
"p95_ms": 27.82,
"max_ms": 255.96
}
},
{
"scenario": "task_api_crud",
"tasks": 100,
"client_concurrency": 20,
"latencies": {
"create": {
"count": 100,
"median_ms": 5.37,
"p95_ms": 34.16,
"max_ms": 61.99
},
"update": {
"count": 100,
"median_ms": 7.22,
"p95_ms": 35.0,
"max_ms": 96.33
},
"list": {
"count": 1,
"median_ms": 3.73,
"p95_ms": 3.73,
"max_ms": 3.73
},
"delete": {
"count": 100,
"median_ms": 5.44,
"p95_ms": 14.57,
"max_ms": 36.43
}
},
"default_page_count": 50,
"default_total": 100,
"pagination_complete": true,
"final_total": 0,
"elapsed_ms": 2532.22,
"event_loop_lag": {
"count": 4,
"median_ms": 776.23,
"p95_ms": 1712.81,
"max_ms": 1712.81
}
},
{
"scenario": "task_api_crud",
"tasks": 1000,
"client_concurrency": 20,
"latencies": {
"create": {
"count": 1000,
"median_ms": 5.18,
"p95_ms": 32.23,
"max_ms": 43.29
},
"update": {
"count": 1000,
"median_ms": 6.51,
"p95_ms": 14.72,
"max_ms": 90.27
},
"list": {
"count": 10,
"median_ms": 5.48,
"p95_ms": 6.18,
"max_ms": 6.18
},
"delete": {
"count": 1000,
"median_ms": 4.76,
"p95_ms": 8.42,
"max_ms": 97.64
}
},
"default_page_count": 50,
"default_total": 1000,
"pagination_complete": true,
"final_total": 0,
"elapsed_ms": 21242.37,
"event_loop_lag": {
"count": 4,
"median_ms": 6932.7,
"p95_ms": 14260.46,
"max_ms": 14260.46
}
}
],
"database_bytes": 2949120,
"complete": true
}
@@ -0,0 +1,706 @@
[
{
"kind": "tasks",
"theme": "light",
"size": 100,
"initialRenderMs": 33.099999994039536,
"requests": [
"/api/tasks"
],
"initialTaskCount": 50,
"fullListRenderMs": 79.09999999403954,
"fullListIsInjected": true,
"domNodes": 1107,
"frameGapsMs": {
"median": 10,
"p95": 10.100000000000364,
"max": 69.49999999403951
},
"frames": 407,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 11702,
"maxScrollTop": 10845,
"filteredCount": 34,
"expectedFilteredCount": 34,
"filterMs": 16.299999982118607,
"repeat": 1
},
{
"kind": "tasks",
"theme": "light",
"size": 1000,
"initialRenderMs": 4.4000000059604645,
"requests": [
"/api/tasks"
],
"initialTaskCount": 50,
"fullListRenderMs": 117.39999997615814,
"fullListIsInjected": true,
"domNodes": 11007,
"frameGapsMs": {
"median": 20.099999999999454,
"p95": 30.199999999999818,
"max": 50
},
"frames": 353,
"longTasks": [],
"scrollTop": 54000,
"scrollHeight": 115652,
"maxScrollTop": 81000,
"filteredCount": 334,
"expectedFilteredCount": 334,
"filterMs": 44.10000002384186,
"repeat": 1
},
{
"kind": "tasks",
"theme": "dark",
"size": 100,
"initialRenderMs": 30.900000005960464,
"requests": [
"/api/tasks"
],
"initialTaskCount": 50,
"fullListRenderMs": 78.2999999821186,
"fullListIsInjected": true,
"domNodes": 1107,
"frameGapsMs": {
"median": 10,
"p95": 10.100000000000364,
"max": 49.69999998807907
},
"frames": 411,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 11702,
"maxScrollTop": 10845,
"filteredCount": 34,
"expectedFilteredCount": 34,
"filterMs": 16.099999994039536,
"repeat": 1
},
{
"kind": "tasks",
"theme": "dark",
"size": 1000,
"initialRenderMs": 5,
"requests": [
"/api/tasks"
],
"initialTaskCount": 50,
"fullListRenderMs": 137.7999999821186,
"fullListIsInjected": true,
"domNodes": 11007,
"frameGapsMs": {
"median": 20,
"p95": 30.199999999999818,
"max": 40.19999999999982
},
"frames": 360,
"longTasks": [],
"scrollTop": 54000,
"scrollHeight": 115652,
"maxScrollTop": 81000,
"filteredCount": 334,
"expectedFilteredCount": 334,
"filterMs": 47.79999998211861,
"repeat": 1
},
{
"kind": "tasks",
"theme": "paper-moments",
"size": 100,
"initialRenderMs": 38.099999994039536,
"requests": [
"/api/tasks"
],
"initialTaskCount": 50,
"fullListRenderMs": 116.09999999403954,
"fullListIsInjected": true,
"domNodes": 1107,
"frameGapsMs": {
"median": 10,
"p95": 20,
"max": 59.900000000000034
},
"frames": 387,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 11764,
"maxScrollTop": 10907,
"filteredCount": 34,
"expectedFilteredCount": 34,
"filterMs": 11.600000023841858,
"repeat": 1
},
{
"kind": "tasks",
"theme": "paper-moments",
"size": 1000,
"initialRenderMs": 5.800000011920929,
"requests": [
"/api/tasks"
],
"initialTaskCount": 50,
"fullListRenderMs": 150,
"fullListIsInjected": true,
"domNodes": 11007,
"frameGapsMs": {
"median": 10.100000000000364,
"p95": 50.100000000000364,
"max": 80
},
"frames": 368,
"longTasks": [],
"scrollTop": 54000,
"scrollHeight": 115714,
"maxScrollTop": 81000,
"filteredCount": 334,
"expectedFilteredCount": 334,
"filterMs": 59.5,
"repeat": 1
},
{
"kind": "trace",
"theme": "light",
"size": 200,
"initialRenderMs": 75.10000002384186,
"requests": [],
"domNodes": 2092,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 18853,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 18853,
"scrollHeight": 18853,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 18805,
"scrollHeight": 18805,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 10,
"p95": 10.100000000000364,
"max": 40.00000000000003
},
"frames": 411,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 18853,
"maxScrollTop": 17948,
"filteredDomNodes": 436,
"filteredCount": 44,
"expectedFilteredCount": 44,
"filterMs": 16,
"treeSwitchMs": 32.099999994039536,
"treeFilterMs": 27.900000005960464,
"treeFilteredDomNodes": 689,
"repeat": 1
},
{
"kind": "trace",
"theme": "light",
"size": 2000,
"initialRenderMs": 283.7999999821186,
"requests": [],
"domNodes": 20542,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 184678,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 184678,
"scrollHeight": 184678,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 184630,
"scrollHeight": 184630,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 166992,
"scrollHeight": 166992,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 166992,
"scrollHeight": 166992,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 10,
"p95": 10.100000000000364,
"max": 20.100000000000023
},
"frames": 491,
"longTasks": [],
"scrollTop": 54000,
"scrollHeight": 184678,
"maxScrollTop": 81000,
"filteredDomNodes": 4036,
"filteredCount": 444,
"expectedFilteredCount": 444,
"filterMs": 97.40000000596046,
"treeSwitchMs": 64,
"treeFilterMs": 617.1999999880791,
"treeFilteredDomNodes": 6539,
"repeat": 1
},
{
"kind": "trace",
"theme": "dark",
"size": 200,
"initialRenderMs": 81.5,
"requests": [],
"domNodes": 2092,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 18853,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 18853,
"scrollHeight": 18853,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 18805,
"scrollHeight": 18805,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 10,
"p95": 10.100000000000364,
"max": 30
},
"frames": 415,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 18853,
"maxScrollTop": 17948,
"filteredDomNodes": 436,
"filteredCount": 44,
"expectedFilteredCount": 44,
"filterMs": 16.69999998807907,
"treeSwitchMs": 31.099999994039536,
"treeFilterMs": 28.900000005960464,
"treeFilteredDomNodes": 689,
"repeat": 1
},
{
"kind": "trace",
"theme": "dark",
"size": 2000,
"initialRenderMs": 289,
"requests": [],
"domNodes": 20542,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 184678,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 184678,
"scrollHeight": 184678,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 184630,
"scrollHeight": 184630,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 166992,
"scrollHeight": 166992,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 166992,
"scrollHeight": 166992,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 10,
"p95": 10.100000000000364,
"max": 20.200000000000045
},
"frames": 487,
"longTasks": [],
"scrollTop": 54000,
"scrollHeight": 184678,
"maxScrollTop": 81000,
"filteredDomNodes": 4036,
"filteredCount": 444,
"expectedFilteredCount": 444,
"filterMs": 74.5,
"treeSwitchMs": 62.900000005960464,
"treeFilterMs": 483,
"treeFilteredDomNodes": 6539,
"repeat": 1
},
{
"kind": "trace",
"theme": "paper-moments",
"size": 200,
"initialRenderMs": 119.59999999403954,
"requests": [],
"domNodes": 2092,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 18853,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 18853,
"scrollHeight": 18853,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 18805,
"scrollHeight": 18805,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 10,
"p95": 20,
"max": 89.99999999999997
},
"frames": 394,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 18853,
"maxScrollTop": 17948,
"filteredDomNodes": 436,
"filteredCount": 44,
"expectedFilteredCount": 44,
"filterMs": 19.700000017881393,
"treeSwitchMs": 40.10000002384186,
"treeFilterMs": 19.899999976158142,
"treeFilteredDomNodes": 689,
"repeat": 1
},
{
"kind": "trace",
"theme": "paper-moments",
"size": 2000,
"initialRenderMs": 337.2999999821186,
"requests": [],
"domNodes": 20542,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 184678,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 184678,
"scrollHeight": 184678,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 184630,
"scrollHeight": 184630,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 166992,
"scrollHeight": 166992,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 166992,
"scrollHeight": 166992,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 10,
"p95": 20.100000000000023,
"max": 30.100000000000023
},
"frames": 416,
"longTasks": [],
"scrollTop": 54000,
"scrollHeight": 184678,
"maxScrollTop": 81000,
"filteredDomNodes": 4036,
"filteredCount": 444,
"expectedFilteredCount": 444,
"filterMs": 86.2999999821186,
"treeSwitchMs": 62.099999994039536,
"treeFilterMs": 540.0999999940395,
"treeFilteredDomNodes": 6539,
"repeat": 1
},
{
"kind": "trace",
"theme": "paper-moments",
"size": 10000,
"initialRenderMs": 1803.9000000059605,
"requests": [],
"domNodes": 102542,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 921678,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 921678,
"scrollHeight": 921678,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 921630,
"scrollHeight": 921630,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 834992,
"scrollHeight": 834992,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 834992,
"scrollHeight": 834992,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 20.100000000000364,
"p95": 40,
"max": 100
},
"frames": 357,
"longTasks": [
82,
53
],
"scrollTop": 54000,
"scrollHeight": 921678,
"maxScrollTop": 81000,
"filteredDomNodes": 40036,
"filteredCount": 4444,
"expectedFilteredCount": 4444,
"filterMs": 582.7999999821186,
"treeSwitchMs": 317.5,
"treeFilterMs": 25914.80000001192,
"treeFilteredDomNodes": 32539,
"repeat": 1
}
]
@@ -0,0 +1,156 @@
[
{
"kind": "trace",
"theme": "paper-moments",
"size": 2000,
"initialRenderMs": 117.19999998807907,
"requests": [],
"domNodes": 1852,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 17214,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 17214,
"scrollHeight": 17214,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 17166,
"scrollHeight": 17166,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 10,
"p95": 10.100000000000364,
"max": 80.09999999999997
},
"frames": 408,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 17214,
"maxScrollTop": 16309,
"filteredDomNodes": 1844,
"filteredCount": 200,
"expectedFilteredCount": 200,
"filterMs": 71.90000000596046,
"treeSwitchMs": 51.400000005960464,
"treeFilterMs": 48.70000001788139,
"treeFilteredDomNodes": 1343,
"repeat": 1
},
{
"kind": "trace",
"theme": "paper-moments",
"size": 10000,
"initialRenderMs": 94.40000000596046,
"requests": [],
"domNodes": 1852,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 17214,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 17214,
"scrollHeight": 17214,
"overflow": "visible",
"position": "static"
},
{
"node": "trace-visualization",
"height": 17166,
"scrollHeight": 17166,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline-view",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "static"
},
{
"node": "timeline",
"height": 16692,
"scrollHeight": 16692,
"overflow": "visible",
"position": "relative"
}
],
"frameGapsMs": {
"median": 10,
"p95": 10.100000000000364,
"max": 50.09999999999991
},
"frames": 414,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 17214,
"maxScrollTop": 16309,
"filteredDomNodes": 1844,
"filteredCount": 200,
"expectedFilteredCount": 200,
"filterMs": 284.90000000596046,
"treeSwitchMs": 100.90000000596046,
"treeFilterMs": 189.19999998807907,
"treeFilteredDomNodes": 1343,
"repeat": 1
}
]
@@ -0,0 +1,150 @@
{
"light": {
"requestedHan": 120000,
"theme": "light",
"variant": "default",
"frameGapsMs": {
"median": 10,
"p95": 30,
"max": 90.09999999999991
},
"frames": 403,
"framesOver25ms": 29,
"longTasks": [
89
],
"scrollTop": 54000,
"scrollHeight": 168515,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
"dark": {
"requestedHan": 120000,
"theme": "dark",
"variant": "default",
"frameGapsMs": {
"median": 10,
"p95": 30,
"max": 90
},
"frames": 408,
"framesOver25ms": 27,
"longTasks": [
88
],
"scrollTop": 54000,
"scrollHeight": 168515,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
"fixed": [
{
"requestedHan": 120000,
"theme": "paper-moments",
"variant": "default",
"frameGapsMs": {
"median": 10,
"p95": 30.100000000000136,
"max": 100
},
"frames": 422,
"framesOver25ms": 31,
"longTasks": [
99
],
"scrollTop": 54037,
"scrollHeight": 160247,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 120000,
"theme": "paper-moments",
"variant": "default",
"frameGapsMs": {
"median": 10,
"p95": 30,
"max": 90
},
"frames": 427,
"framesOver25ms": 31,
"longTasks": [
84,
58
],
"scrollTop": 54000,
"scrollHeight": 160211,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
},
{
"requestedHan": 120000,
"theme": "paper-moments",
"variant": "default",
"frameGapsMs": {
"median": 10,
"p95": 30.09999999999991,
"max": 90.09999999999991
},
"frames": 422,
"framesOver25ms": 29,
"longTasks": [
85
],
"scrollTop": 54037,
"scrollHeight": 160247,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 3
}
],
"outlineDisabled": [
{
"requestedHan": 120000,
"theme": "paper-moments",
"variant": "no-outline",
"frameGapsMs": {
"median": 10,
"p95": 30.099999999999454,
"max": 100
},
"frames": 418,
"framesOver25ms": 31,
"longTasks": [
101
],
"scrollTop": 54037,
"scrollHeight": 160247,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 120000,
"theme": "paper-moments",
"variant": "no-outline",
"frameGapsMs": {
"median": 10,
"p95": 30,
"max": 99.89999999999964
},
"frames": 413,
"framesOver25ms": 32,
"longTasks": [
87,
50,
52,
52
],
"scrollTop": 53965,
"scrollHeight": 160174,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
}
]
}
@@ -0,0 +1,66 @@
[
{
"requestedHan": 120000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 70.29999999999927,
"p95": 100.20000000000073,
"max": 210.20000000000073
},
"frames": 308,
"framesOver25ms": 286,
"longTasks": [
86,
54,
56,
62,
50,
58,
53,
52,
58,
100,
107,
96,
60,
99,
89,
136,
101,
98,
101,
90,
66,
120,
74,
99,
58
],
"scrollTop": 53127,
"scrollHeight": 159554,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 120000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 70,
"p95": 90,
"max": 150
},
"frames": 261,
"framesOver25ms": 235,
"longTasks": [
84,
67,
52
],
"scrollTop": 53233,
"scrollHeight": 159554,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
}
]
@@ -0,0 +1,36 @@
{
"transport": "real loopback HTTP, separate Uvicorn process",
"tasks": 1000,
"concurrency": 20,
"elapsed_ms": 24509.81,
"latencies": {
"create": {
"count": 1000,
"p95_ms": 157.68,
"max_ms": 186.7
},
"update": {
"count": 1000,
"p95_ms": 208.68,
"max_ms": 259.91
},
"list": {
"count": 11,
"p95_ms": 23.39,
"max_ms": 23.39
},
"delete": {
"count": 1000,
"p95_ms": 205.45,
"max_ms": 279.12
},
"health": {
"count": 375,
"p95_ms": 38.64,
"max_ms": 164.76
}
},
"health_errors": [],
"pagination_complete": true,
"final_total": 0
}
@@ -0,0 +1,36 @@
{
"transport": "real loopback HTTP, separate Uvicorn process",
"tasks": 1000,
"concurrency": 20,
"elapsed_ms": 18312.92,
"latencies": {
"create": {
"count": 1000,
"p95_ms": 116.45,
"max_ms": 134.91
},
"update": {
"count": 1000,
"p95_ms": 159.29,
"max_ms": 193.37
},
"list": {
"count": 11,
"p95_ms": 20.96,
"max_ms": 20.96
},
"delete": {
"count": 1000,
"p95_ms": 156.43,
"max_ms": 190.1
},
"health": {
"count": 117,
"p95_ms": 157.0,
"max_ms": 195.07
}
},
"health_errors": [],
"pagination_complete": true,
"final_total": 0
}
@@ -0,0 +1,70 @@
[
{
"kind": "tasks",
"theme": "paper-moments",
"size": 1000,
"initialRenderMs": 173,
"requests": [
"/api/tasks?limit=100&offset=0",
"/api/tasks?limit=100&offset=100",
"/api/tasks?limit=100&offset=200",
"/api/tasks?limit=100&offset=300",
"/api/tasks?limit=100&offset=400",
"/api/tasks?limit=100&offset=500",
"/api/tasks?limit=100&offset=600",
"/api/tasks?limit=100&offset=700",
"/api/tasks?limit=100&offset=800",
"/api/tasks?limit=100&offset=900"
],
"initialTaskCount": 1000,
"fullListRenderMs": 85.39999997615814,
"fullListIsInjected": true,
"renderedTasks": 100,
"domNodes": 1111,
"scrollContainers": [
{
"node": "HTML",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "BODY",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
},
{
"node": "viewport",
"height": 905,
"scrollHeight": 905,
"overflow": "auto",
"position": "static"
},
{
"node": "app",
"height": 905,
"scrollHeight": 905,
"overflow": "hidden",
"position": "static"
}
],
"frameGapsMs": {
"median": 10,
"p95": 30,
"max": 50.10000000000002
},
"frames": 384,
"longTasks": [],
"scrollTop": 0,
"scrollHeight": 11798,
"maxScrollTop": 10941,
"filteredCount": 100,
"totalFilteredCount": 334,
"expectedFilteredCount": 100,
"filterMs": 18.69999998807907,
"repeat": 1
}
]
@@ -0,0 +1,101 @@
# 后台运行日志与压力问题修复
## 1. 范围与入口
本次针对 Agent 与任务压测发现的历史记录截断、同步数据库写入阻塞事件循环、长 Trace 树形筛选过慢进行修复,并增加统一运行日志。
主导航的「日志」打开 `/logs`。知识库选择页也有入口;无需成功打开 Vault 即可查看。需要 AI Core 正常提供 HTTP 服务。后台未启动时,页面显示连接错误,不伪造历史结果。
页面提供级别、模块、事件名/错误码/关联 ID 筛选及详情展开。日志列表高度受限,按时间从旧到新显示,默认每 5 秒获取新日志并跟随到底部。向上滚动时暂停跟随,抵达顶部自动加载更早日志并保持阅读位置;可一键回到最新。隐藏页面和卸载后停止轮询。界面最多保留当前浏览窗口的 500 条,继续读取旧日志时移出另一端的记录,后台保留窗口不受影响。所有组件使用已有主题变量、卡片、按钮、下拉框及 `ui-disclosure` 展开样式。
## 2. 日志覆盖
| 模块 | 记录内容 | 定位信息 |
| --- | --- | --- |
| vectors | 后台索引状态、向量调用失败、是否使用本地索引回退 | job_id、错误码、异常类型、调用位置、批次数量 |
| models | 本地模型/远程模型运行诊断、CUDA/CPU、失败与回退 | 模型、设备、状态、错误码、耗时 |
| agent | 创建运行、模型轮次、工具调用与结果、权限决策、完成/失败/取消 | run_id、Provider、模型、步骤、事件序号、工具名 |
| tasks | 创建、修改、删除及不存在的删除目标 | task_id、关联 note_id、修改字段名、状态 |
| providers / chat | 模型请求完成或未完整结束、流式模型错误、对话失败 | Provider、模型、request_id、run_id、错误码 |
| http / api | HTTP 写操作、失败请求、超过一秒的请求及业务错误 | HTTP 方法、路由模板、状态码、耗时、request_id、资源 ID |
| system / Python logger | 服务启动与停止,以及应用、服务器和依赖库的 warning/error | 模块、代码位置、异常类型 |
`X-Request-ID` 响应头可与日志中的 request_id 对照。请求上下文通过 ContextVar 传播到后台任务和线程;Agent 内部工具触发的日志还带 run_id。查询日志自身以及正常短轮询不生成 HTTP 操作日志,避免轮询放大日志量。
这里只记录操作诊断,不替代完整 Agent Trace、Token 用量台账和模型诊断表。旧代码 logger 的自由文本可能包含笔记或厂商返回,因此桥接时只记录级别、模块、代码位置和异常类型;需要业务错误码的地方使用结构化接口。
## 3. 存储、容量与隐私
- 位置:`APP_DATA_DIR/logs/operations.sqlite3`,与业务数据库独立,应用重启后可继续查看。
- 保留最近 20,000 条;每批写入时淘汰更早的记录。不是按天永久归档。
- 后台线程批量写入,队列最多 4,096 条,一批最多 128 条。队列满时丢弃新诊断记录并计数,绝不阻塞保存或模型推理。
- 日志页面显示待写入数量、队列溢出和写入失败数量。计数属于当前进程;重启后重置。存储不可读时显示请求失败。
- 正常退出先停止 Agent 和其他后台工作,再排空日志队列;强制终止进程可能丢失尚未写入的队列内容。
- 不记录请求/响应正文、系统提示词、工具参数/输出、笔记标题或正文、凭据、原始异常消息、查询字符串。元数据按白名单筛选并限长,Bearer 和 `sk-` 凭据形式额外脱敏。
- API 仅返回保留窗口内的诊断数据,不提供任意文件读取和日志删除接口。
## 4. 开发接口
```python
from app.operation_logs import log_event
log_event('vectors', 'embedding.failed', level='ERROR', error=exc,
model=model_id, job_id=job_id, error_code='EMBEDDING_UNAVAILABLE',
fallback='local_index')
```
第一个参数是模块名。`source` 是可选的元数据字段,例如 `local``api`,不要与模块名混淆。事件名应使用开发时定义的短常量。不要把正文、URL、异常消息或用户输入拼入事件名。
`GET /api/logs` 参数:
| 参数 | 规则 |
| --- | --- |
| limit | 默认 50,范围 1200 |
| before | 上页 next_cursor;正整数,返回更早的 ID |
| level | 空或 INFO / WARNING / ERROR / CRITICAL |
| source | 模块名精确匹配,最多 100 字符 |
| q | 在事件名及脱敏元数据中做字面子串搜索,最多 200 字符 |
返回 `items``next_cursor``sources``pending``dropped``write_failures``retention`。按递增 ID 的倒序分页,插入新日志不改变已经翻到的旧页边界。没有匹配记录时 items 为空、next_cursor 为 null。
## 5. 压力问题的修复方式
Agent Trace 写入由独立异步队列调度到线程,最多合并 64 个写入为一个 SQLite 事务。提交成功后才更新对外可见快照和广播 SSE。创建记录在第一次 await 前占用运行名额,避免并发创建突破 200 个活动运行上限。取消等待正在提交的快照结束,再写终态;服务关闭会等待 Agent 和 Trace 队列。数据库失败不会被当成成功提交。
任务 HTTP 写操作在协程中排队,然后在线程执行;读操作也在线程执行,减少文件锁争用和主事件循环阻塞。SQLite 仍是单写者,不意味着任务写入支持无限并发。
前端读取任务和 Agent 列表的后续 API 页,不再只显示第一页的 50 条。任务筛选和数量基于已获取的完整列表,界面每页显示 100 个任务;运行侧栏每页 50 条。列表刷新失败保留上次结果。列表读取使用现有 offset 协议,不是跨多页的事务快照;其他客户端同时修改数据时可以刷新重新获取。
Trace 搜索先生成事件序号、模型调用 ID、工具调用 ID 的集合,再一次遍历调用树。匹配子节点保留父节点。搜索作用于完整已加载的历史,每页只渲染 200 个事件或树节点。工具统计按工具名和状态汇总,避免在底部再次生成数千个调用卡片。
## 6. 验证方法
后端回归覆盖日志持久化、重启、保留窗口、筛选与游标、正文排除、HTTP 关联 ID,以及后台 Trace 写入同时取消时仍释放等待方。业务回归覆盖 Agent/任务、模型路由、本地模型、用量统计和对话。
```powershell
cd backend
.venv/Scripts/python.exe -m pytest tests/test_agent_core.py tests/test_api.py tests/test_operation_logs.py tests/test_model_routing.py tests/test_local_models.py tests/test_usage_overrides.py tests/test_chat_history.py tests/test_chat_context.py -q
```
运行测试前应将 APP_DATA_DIR、APP_DB_PATH 和 APP_VAULT_PATH 指向临时目录,避免模块导入时初始化真实扩展。pytest 的 fixture 会继续为每个测试隔离数据。
前端执行 `npm --prefix frontend test``npm --prefix frontend run build`。新增用例验证后续任务/Agent 页完整加载、Trace 全历史搜索与分页、日志筛选/游标、自动跟随与上滚暂停、顶部历史加载、存储异常提示及卸载后停止刷新。
2026-09-06 实测:后端全量 627 项通过;随后增加的 3 项写入失败/取消回归也通过,最后相关接口回归 21 项通过。前端全量 413 项通过,生产构建通过。构建仍提示已有部分 Markdown/Mermaid 依赖块大于 500 kB;后端测试保留已有 Starlette TestClient 弃用提示。纸间时光日志页已用隔离合成数据检查截图,运行中的本机 `/api/logs` 也已确认可返回结果。
离线并发压测与真实本机 HTTP 压测复现方式、前后数据见 [Agent 与任务压测报告](Agent与任务压测报告.md)。压测只使用隔离数据和 Mock Provider,不消耗用户厂商额度。
手工验收建议:
1. 创建、修改和删除一个测试任务,按返回的 task_id 或 X-Request-ID 筛选日志。
2. 运行含工具调用的 Agent,批准或取消权限请求;按 run_id 检查创建、权限、工具结果和终态。
3. 在隔离配置中请求未安装的本地模型,查看 models/vectors 的错误码与回退信息,不应出现输入文本。
4. 重启 AI Core,确认已提交日志仍可查看;退出 Vault 后从入口页打开日志。
5. 切换默认浅色、深色、纸间时光主题,检查筛选栏、错误徽标和详情展开;窄窗口下应换行且详情不撑出页面。
# 2026-09-06:大批量本地向量传输修复
全量重建包含 2111 个文本块时,本地模型可能成功完成推理,但整批向量被写成一行 JSON。接收端单行限制为 16 MiBasyncio 管道拒绝超长行,Windows 线程管道则读到不完整 JSON,最终被包装成 `LOCAL_MODEL_INVALID_RESPONSE`。这类错误不表示 CUDA 显存不足,也不应通过切换 CPU 处理。
Embedding 响应改为每 128 条向量一个 JSON 帧,结束帧携带总数。接收端检查偏移连续性、请求数量和结束标记;缺块时明确失败,不把部分向量写入索引。模型仍只加载一次,推理批大小不变。语音等其他响应保持原协议。
验证方法:在隔离数据目录运行 `tests/test_local_models.py`,分别使用 asyncio 与 Windows 线程管道传输 2111 × 384 的结果,确认旧格式超过 16 MiB,而分帧可完整接收。该文件 7 项测试通过。另使用本机 Bekko 权重、CUDA 对 2111 条合成文本做真实推理:返回 2111 条 384 维向量,设备 `cuda:0`,耗时约 12.55 秒;旧格式 JSON 为 17,886,180 字节。该验收不修改用户笔记、索引或运行配置;用户原始长文档的全库重建仍需在服务加载修复后重试。
@@ -77,3 +77,34 @@ backend/.venv/Scripts/python.exe frontend/tests/performance/run-stress.py --url
下一步需要评估代码块按交互初始化或编辑器级虚拟化,并同时验证输入、选区、搜索定位和折叠;当前不将 12 万字滚动问题标记为完成。
本轮前端 405 项测试通过,涵盖折叠定位、语言标签增量同步、菜单祖先滚动定位和卸载清理。
## 7. CodeMirror 高亮复用实验
在上轮提交 `874e916` 的基础上继续尝试:同一语言及主题的 CodeMirror LanguageSupport 内缓存高亮装饰,代码块离屏销毁后重新创建时,相同正文不再重复分词及构建装饰。缓存最多 32 项、64000 个源字符,超过 16000 字符的单块不缓存,使用 LRU 淘汰;语言支持释放时缓存一并释放。编辑后的内容按新正文计算,不复用旧位置。
回归测试通过统计分词调用验证跨视图复用、正文编辑重新计算和超限淘汰。原有深浅主题切换与编辑测试继续验证显示内容。
12 万字继续测量两次,帧间隔 P95 分别约 100.2 ms、90 ms,二者中位数约 95.1 ms;上一轮约 100 ms。样本重复代码较多,结果仍在明显波动范围内,不能宣称解决滚动卡顿。此优化减少确定可重复的高亮工作,但下一步仍须处理创建 CodeMirror 和布局测量的成本。
- [本轮滚轮原始数据](performance/2026-09-06-scroll-token-cache.json)
## 8. 主题对照与纸间时光 1.8.1 修复
用户补充:同一份 12 万字文档在默认浅色、深色下不卡顿。因此重新进行主题对照,之前将 CodeMirror 作为主要优化方向的判断不足;CPU 采样中的布局及选区测量耗时不能单独证明编辑器是主要原因。
保持相同文档和滚轮脚本,默认浅色、深色各测一次,P95 均约 30 ms。纸间时光 1.8.0 仅禁用整篇 `.ProseMirror` 的虚线 outline 后,两次 P95 约 30.1、30 ms;相比此前约 90–100 ms,构成明确的样式消融证据。主题与 CodeMirror 的交互仍可能影响布局,但不需要先用编辑器虚拟化解决这个差异。
修复将贯穿长文的虚线轮廓改为四条小尺寸渐变平铺背景,保留纸张、缝线、胶带、段落横线和叠纸阴影。独立伪元素虚线 border 的实验反而更慢,已撤回。正式主题版本升为 1.8.1。
| 主题或实验 | 次数 | 帧间隔 P95(ms) |
| --- | ---: | --- |
| 默认浅色 | 1 | 30 |
| 默认深色 | 1 | 30 |
| 1.8.0 仅禁用整篇 outline | 2 | 30.1、30 |
| 1.8.1 平铺缝线 | 3 | 30.1、30、30.1 |
1.8.1 三次测试的帧间隔中位数均为 10 ms,全部折叠后的 scrollTop 均为 0,光标位置均为 1。已检查视口截图,手账装饰保留。这里的结果说明本轮样本与环境下,纸间时光恢复到接近默认主题的滚动表现,不代表所有设备、所有复杂文档均无长任务。
已安装主题保存在本地,不会自动被新版资源覆盖。刷新前端后,到“主题 → 社区主题 → 纸间时光”点击“更新”,确认版本 1.8.1 再进行手动复测。
- [主题对比原始数据](performance/2026-09-06-scroll-theme-comparison.json)