Skip to content

首屏性能:/api/stats 缓存化 + /api/events 首拉截尾 (#34)#35

Merged
Protocol-zero-0 merged 1 commit into
masterfrom
feat/firstpaint-perf
Jun 9, 2026
Merged

首屏性能:/api/stats 缓存化 + /api/events 首拉截尾 (#34)#35
Protocol-zero-0 merged 1 commit into
masterfrom
feat/firstpaint-perf

Conversation

@Protocol-zero-0

Copy link
Copy Markdown
Contributor

Closes #34.

首屏指标卡十几秒才出数,根因是两个首屏必经接口太重:/api/stats 每请求串行跑 ~30 个 COUNT(*)(并发被放大到 18.7s),/api/events?since=0 回全量 1000 条 / 463KB 但前端只留 50 条。本 PR 修这两个,不碰统计口径。

Changed files

  • web/stats_cache.py(新增):进程内 TTL 缓存 StatsCache,纯读 get() + 同步 prewarm() + 单后台刷新线程。
  • web/app.py:/api/stats 改为只读缓存;/api/events?since=0 首拉截尾;新增 prewarm_stats_cache()
  • main.py:启动时后台线程预热缓存并启动刷新。
  • tests/test_web_app.py:新增 6 条测试(additive,未改动现有测试)。

功能 1 · /api/stats 缓存化

  • COUNT(*) 查询从不在请求线程里跑:get() 是纯读,只返回快照;预热在启动后台线程里做,刷新由单独一条后台线程每 TTL(30s)做。
  • 不改任何 COUNT 的 SQL / 口径,数字保持正确,只允许 ≤TTL 秒滞后。
  • import web.app / 建 app 不触发重查询(缓存惰性构造)。
  • 冷启动未预热时返回 {"warming": true} 标记,不返回伪造数字

功能 2 · /api/events?since=0 首拉截尾

  • since=0 只回最近 50 条;next_seq 仍为 max(seq)+1,后续 ?since=next_seq 增量轮询行为不变
  • since>0 增量路径完全不动;前端无需改(本就截 50)。

Tests added(先加失败测试,再改代码)

功能1:test_api_stats_served_from_cache_not_recomputed_each_request(mock 计数证明 ≤1 次重算)、test_api_stats_returns_correct_fields(字段/值与底层一致)、test_import_web_app_does_not_block_on_heavy_stats(import 不触发重查询)。
功能2:test_api_events_since_zero_returns_tail_only(≤50 且 next_seq 正确)、test_api_events_incremental_since_unchanged(增量语义不变)。
E2E:test_first_paint_endpoints_are_fast_and_bounded(首屏 5 接口全 200 + stats 缓存命中 ≤1 + events≤50 + 字段完整,mock 计数不依赖墙钟)。

Tests run(实际命令与结果,在装了 flask 的 .venv 里跑)

基线(master,改前):

$ .venv/bin/python -m pytest tests/test_web_app.py -q
16 passed in 4.14s

本 PR(改后):

$ .venv/bin/python -c "import web.app; print('OK')"
OK

$ .venv/bin/python -m pytest tests/test_web_app.py -q
22 passed, 80 warnings in 5.16s

新增测试单独跑(先验证未实现时失败 → 实现后全绿):

$ .venv/bin/python -m pytest tests/test_web_app.py -q -k "StatsCache or EventsTail or FirstPaint"
# 实现前:4 failed, 2 passed   实现后:6 passed

连跑 3 次无 flaky:22 passed(4.40s / 4.53s / 4.92s)。

Before / after

Before After
/api/stats 重查询 每请求串行 ~30 COUNT,并发 18.7s 请求线程 0 查询,只读缓存;后台每 30s 刷新一次
/api/events?since=0 全量 1000 条 / 463KB 最近 ≤50 条;next_seq 不变
import / 建 app 不触发重查询

Non-goals(未做,符合 issue)

线上实测(需重部署后复核,非单测)

验收 5–8(curl p95<1s、首屏<3s、events≤25KB、9 卡 1–2s 出数)属重部署后线上项,本 PR 无法在 CI 内完成;按 issue「重部署」步骤在 host 上 git checkout master && git pull && sudo systemctl restart deepgraph-web 后用 curl + 浏览器复核。

🤖 Generated with Claude Code

功能1:新增 web/stats_cache.py(进程内 TTL 缓存),/api/stats 只读缓存,
重 COUNT(*) 查询从不在请求线程里跑。启动时后台预热一次 + 后台线程每 TTL(30s)
刷新;import / 建 app 不触发重查询。不改任何 COUNT 口径,数字保持正确,只允许
≤TTL 秒滞后;冷启动未预热时返回 {"warming": true} 标记,不返回伪造数字。

功能2:/api/events?since=0 只回最近 50 条(前端本就只留 50),next_seq 仍为
max(seq)+1,后续 ?since=next_seq 增量轮询行为不变。

测试(先加失败测试再改代码,additive):
- 功能1 三条:命中缓存不重算(mock 计数 ≤1)、字段/值与底层一致、import 不被重查询拖慢。
- 功能2 两条:since=0 截尾 ≤50、增量语义不变。
- E2E 一条:首屏全部接口 200 + stats 缓存命中 ≤1 + events≤50 + 字段完整。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 9, 2026 07:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses dashboard first-paint latency by ensuring /api/stats is served from an in-process snapshot cache (so request threads never run the heavy ~30 COUNT(*) queries) and by truncating the initial /api/events?since=0 response to only the most recent 50 events.

Changes:

  • Add StatsCache (process-internal TTL cache) and wire /api/stats to serve only from the cached snapshot, returning {"warming": true} before first warm-up completes.
  • Change /api/events?since=0 to return only the last 50 events while preserving next_seq behavior for subsequent incremental polling.
  • Add unit + E2E tests validating caching behavior, import-time behavior (no heavy stats on import), and first-paint endpoint boundedness; start cache prewarm on server startup.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
web/stats_cache.py Introduces StatsCache with synchronous prewarm and a single background refresh thread.
web/app.py Routes /api/stats through the cache; truncates /api/events for since=0; adds prewarm helper/constants.
main.py Starts a daemon thread at startup to prewarm stats cache and begin background refresh.
tests/test_web_app.py Adds targeted tests for stats caching, events tail behavior, and first-paint endpoint boundedness.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread web/stats_cache.py
Comment on lines +31 to +35
def __init__(self, compute, ttl: float = 30.0, time_func=time.monotonic):
self._compute = compute
self._ttl = float(ttl)
self._time = time_func
self._lock = threading.Lock()
Comment thread web/app.py
Comment on lines +38 to +42
"""Warm the stats cache once and start its background refresher. Call from
server startup (in a thread) so the first browser paint is served from a
warm cache, not a cold ~30-COUNT(*) query, and stays fresh thereafter."""
_stats_cache.prewarm()
_stats_cache.start_background_refresh()
Comment thread web/app.py
Comment on lines +669 to +671
# Serve from the in-process TTL cache; the heavy COUNT(*) query never runs
# in this request thread (issue #34). Stale entries trigger a background
# refresh inside the cache.
@Protocol-zero-0 Protocol-zero-0 merged commit 8f8fa0c into master Jun 9, 2026
3 checks passed
@Protocol-zero-0 Protocol-zero-0 deleted the feat/firstpaint-perf branch June 9, 2026 08:00
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.

Dashboard 首屏性能:/api/stats 缓存化 + /api/events 首拉截尾

2 participants