首屏性能:/api/stats 缓存化 + /api/events 首拉截尾 (#34)#35
Merged
Conversation
功能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>
There was a problem hiding this comment.
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/statsto serve only from the cached snapshot, returning{"warming": true}before first warm-up completes. - Change
/api/events?since=0to return only the last 50 events while preservingnext_seqbehavior 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 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 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 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)做。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,改前):
本 PR(改后):
新增测试单独跑(先验证未实现时失败 → 实现后全绿):
连跑 3 次无 flaky:
22 passed(4.40s / 4.53s / 4.92s)。Before / after
/api/stats重查询/api/events?since=0next_seq不变Non-goals(未做,符合 issue)
agents/、contracts/、DB schema;不删 tab/功能;不引前端框架。线上实测(需重部署后复核,非单测)
验收 5–8(
curlp95<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