feat: Perfetto 风格的本地缓存链接(local_cache_key) - #65
Conversation
ui.perfetto.dev keeps every opened trace in browser storage under a UUID and puts that UUID in the URL, so a reload or a bookmark reopens the same trace without re-uploading it. Do the same for log analyses. Each analysis (files + parsed metrics + the parsing config that produced them) is snapshotted to IndexedDB under a cache key, and the key is mirrored in the hash as `#!/viewer?local_cache_key=<uuid>`. Reload, a bookmark, a second tab or the new cache list all reopen it; the bare URL still restores the most recent analysis, so nothing regresses for users who never touch a link. Links are local to the browser by design — the modal and the miss-toast say so — and nothing is uploaded anywhere. - idb.js: one shared DB open/upgrade, since fileStorage and the session cache live in the same database and must agree on its version - sessionCache.js: snapshot store + lightweight summary index (listing the cache never deserializes log text), LRU eviction by age (30d), count (20) and bytes (192MB), and quota-exceeded recovery - cacheUrl.js: key parse/build/validate + history rewriting - SessionsModal.jsx: cached analyses with open / copy link / delete - Restore order on mount: URL key → most recent snapshot → legacy working set (adopted into a cache entry on its first save) Tests cover the URL helpers, the store against fake-indexeddb, and the App-level flow (mint a key, restore from it, warn on an unknown link). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167bNHXjLvWKocE6AXUJz3A
Three gaps in the cache as first shipped, all of them local-experience: - Switching analyses cancelled the 600ms debounced write, so an edit made just before opening another snapshot was silently dropped. The pending payload is now flushed before the switch. - The only route to an empty canvas was "clear all", which deletes the current snapshot — so the cache could never hold more than one entry and the list had nothing to list. Added "New analysis", which keeps the current snapshot and mints a fresh key on the next upload. - The URL changing on its own needed explaining: a one-time toast after the first key is minted, plus inline rename in the list (renameSession existed but nothing called it) so a history of analyses stays readable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167bNHXjLvWKocE6AXUJz3A
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:fabae606cb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else { | ||
| console.warn('Failed to cache analysis', err); | ||
| } |
There was a problem hiding this comment.
Fall back when cache writes fail
When indexedDB exists but openDB()/the cache write rejects for a non-quota reason, such as a blocked/private IndexedDB or a version error, this branch only logs and the effect never reaches the legacy saveFilesToStorage serialization below. The previous persistence layer fell back to localStorage for exactly these contexts, so small logs in those browsers now silently stop persisting across refreshes; set the cache path disabled and save through the legacy path, or otherwise call the fallback here.
Useful? React with 👍 / 👎.
| sessionIdRef.current = null; | ||
| setCacheKey(null); | ||
| clearCacheKey(); | ||
| deleteCachedSession(staleId).catch(() => { /* best effort */ }); |
There was a problem hiding this comment.
Clear the legacy working set when files are removed
For upgraded browsers that still have the pre-cache files/all IndexedDB working-set record, this empty-files path deletes only the current session. After the user clicks the FilesPanel clear-all action, the URL is cleared and there are no sessions left, so the next bare reload falls through to loadWorkingSet() and resurrects the supposedly deleted old logs; clear fileStorage in this branch as well.
Useful? React with 👍 / 👎.
Measured in Chromium against the previous layout, which held the raw logs
and every parsed point inside one session record:
20MB / 10 files: first save 718ms, re-save on any state change 752ms
200MB / 10 files: first save 1503ms, re-save 1532ms
...and the raw logs were silently dropped at 64MB
Every checkbox toggle, range edit or metric rename rewrote the whole
snapshot, so the cost scaled with everything cached rather than with what
changed. The 64MB per-session ceiling also degraded all-or-nothing: one
byte over and every file lost its content, which 30 x 2MB already tripped
because parsed points counted toward it.
Now content and metrics live in their own records, one per file, written
only when they actually change; the session record holds names, enabled
flags, configs and references. Same machine, same scenarios:
20MB / 10 files: first save 177ms, toggle 3ms, restore 73ms
60MB / 30 files: first save 505ms, toggle 4ms, restore 190ms
200MB / 10 files: first save 2728ms, toggle 5ms, restore 1295ms
...with all raw logs kept
Also:
- Points pack into Float64Array pairs rather than {x, y} objects: 1M
points write in 106ms instead of 779ms, read in 191ms instead of 931ms.
- Content is stored as UTF-8 bytes taken from the text already in memory,
not as the original File. A File is a reference to the file on disk, so
the next training run overwriting train.log would leave the cache
serving content that no longer matches the metrics parsed from it.
- The byte ceiling is now min(2GB, half the origin's quota) instead of a
flat 192MB, and storage.persist() is requested so the browser is less
likely to evict us.
- Out of space: evict other analyses first, then keep the parsed series
and drop the raw logs, so charts still render.
The lazy-encode rule has its own test: encoding content before deciding
whether it needs writing costs exactly as much as writing it, and put
seconds back into every toggle once already.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167bNHXjLvWKocE6AXUJz3AJavaZeroo
commented
Aug 29, 2026
补一个 commit( 原方案的问题(Chromium 实测)原来一次分析的所有内容(原文 + 所有解析点)塞在一条 session 记录里:
两个问题:
改法内容和解析结果各自独占记录,按文件分开,只在真正变化时才写;session 记录只留文件名、启用状态、配置和引用。
交互期的写入变成与数据量无关的常数。首次保存仍随数据量增长,但那一次本来就和 FileReader 读取 + worker 解析同一量级,而且是防抖异步的。 其他几处:
一个容易踩回去的点"先编码再判断要不要写"和"直接写"成本一样 —— 我自己就先踩了一次,勾选框重写从 4ms 回到 1763ms。所以 测试 155 个全过;另外用 Chromium 对 build 产物跑了完整链路,确认刷新后原文确实回来了(不出现"需要重新上传"标记)。 Generated by Claude Code |
背景
ui.perfetto.dev把打开过的 trace 存在浏览器里、用一个 UUID 索引它,并把 UUID 写进 URL:刷新、收藏、开第二个标签页都能回到同一份 trace,不用重新上传。这个 PR 把本地这一半搬过来给日志分析用。跨机分享(Perfetto 靠上传 GCS 换短链)不做 —— 那会让日志离开浏览器,和项目"数据不上传"的定位冲突。
做了什么
每次分析(文件内容 + 解析出的 metrics + 产生它们的解析配置)快照进 IndexedDB,地址栏自动变成
#!/viewer?local_cache_key=<uuid>。顺带一个收益:旧逻辑对 >5MB 的文件会丢掉
content只留metricsData(标记 needsReupload),缓存快照把上限提到 64MB,大日志现在也能整份存下来。改动
src/utils/idb.jsfileStorage和缓存同库,版本必须统一,否则老的open(v1)会 VersionErrorsrc/utils/sessionCache.jssrc/utils/cacheUrl.jshistory.replaceState/pushStatesrc/components/SessionsModal.jsxsrc/App.jsxURL key → 最近快照 → 旧 working set的顺序恢复;防抖 600ms 写入;解析中不写(避免快照到空 metrics)第二个 commit 是本地体验的三处修补:
train.log +2攒几条以后认不出来)。设计取舍
vite preview都不用改 rewrite 规则。sessions存完整 payload,sessionIndex只存摘要,所以列缓存时不会把几十 MB 日志正文读进内存。验证
npm run lint干净(只剩既有的 react-refresh warning)npm run test:24 文件 / 147 用例全过。新增覆盖 URL helper、fake-indexeddb 下的存储层(含 LRU 淘汰、超大快照降级、重命名)、以及 App 级流程(生成 key → 刷新恢复 → 未知 key 告警 → 新建分析后两份快照并存)新增一个 devDependency:
fake-indexeddb(仅测试用,jsdom 没有 IndexedDB)。Generated by Claude Code