Skip to content

Commit a59d66c

Browse files
committed
fix: separate auth-file cache identity from fingerprint
1 parent 927ee27 commit a59d66c

4 files changed

Lines changed: 145 additions & 34 deletions

File tree

docs-linhay/memory/2026-05-27.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828

2929
## Auth-file metadata cache
3030

31-
- 修复方向:桌面客户端普通展示场景不再反复下载同一 auth-file 原文;`ListAuthFiles` 首次 fresh 推断后按 `name + size + modified` 缓存展示元数据。
31+
- 修复方向:桌面客户端普通展示场景不再反复下载同一 auth-file 原文;`ListAuthFiles` 首次 fresh 推断后按 canonical `name` 归一缓存展示元数据,`size + modified` 只作为 freshness fingerprint。
32+
- 归一化边界:auth-file 身份仍是 `auth-file:<name>``size` 不是账号身份、不能参与前端选择态/禁用态/排序/modal detail 的 ID 归一,只用于判断缓存是否过期。
33+
- 验证:构造 10,000 次同名 auth-file fingerprint churn 的 benchmark 中,新实现约 `16.9 KB/op / 164 allocs/op`,旧 `name|size|modified` composite-key 基线约 `4.68 MB/op / 30070 allocs/op`
3234

33-
## Codex live sessions 单指标 ECG 切换
34-
- 决策:`#frame=codex&workspace=live-sessions` 的请求耗时趋势图不再把多个 timing 维度同时画进同一张图里,而是改成“一个指标一条 ECG 线”;最终图表类型定为 `heartbeat strip chart / rolling strip chart`,最新 request 锚在右侧,按稳定时间密度向左回看
35-
- 实现:`SessionDetail` 维护当前 `selectedTimingMetric``TimingMetrics` 把可绘图项变成可点击按钮并用 `aria-pressed` 标示选中态,`TimingTrendChart` 只按当前 metric 计算 y 轴和单条波形;图表去掉横向滚动,按容器宽度决定可见窗口,宽屏回看更多、窄屏只显示更近样本。动画改为“指标切换时短淡入 + live 点光圈呼吸”,不再对整条线做 sweep 重扫。
35+
## Codex live sessions 单指标音频波形图
36+
- 决策:`#frame=codex&workspace=live-sessions` 的请求耗时趋势图不再把多个 timing 维度同时画进同一张图里,而是改成“一个指标一组音频波形条”;最终图表类型定为 forward-moving audio waveform chart,一柱一请求,x 轴是请求序号而不是真实时间间隔
37+
- 实现:`SessionDetail` 维护当前 `selectedTimingMetric``TimingMetrics` 把可绘图项变成可点击按钮并用 `aria-pressed` 标示选中态,`TimingTrendChart` 只按当前 metric 计算 y 轴和单组垂直振幅条;图表去掉横向滚动,按容器宽度决定可见请求数,宽屏显示更多最近请求、窄屏显示更少,最新 request 靠右密集推进。动画改为“指标切换时短淡入 + live 点光圈呼吸”,不再对整条波形做 sweep 重扫。
3638
- 补充:`请求时间线` 是扫描区而非完整历史列表,页面内只展示排序后的最近 15 条 request,标题行数按实际可见行数展示。
3739
- 验证:`node --test frontend/src/features/codex-live-sessions/model.test.mjs``npm --prefix frontend run typecheck``npm --prefix frontend run build` 通过;早前 Playwright 复核确认点击 `TTFT 562ms` 后图表切到蓝色单线,点击 `流式` 后切到绿色单线。本轮最终浏览器复核被 Browser URL policy 拦截,未绕过执行。
3840
- 失效边界:上传、删除、启停状态修改成功后按文件名清理缓存;文件大小或修改时间变化会自然触发重新下载与缓存刷新。

docs-linhay/spaces/20260526-auth-file-metadata-cache/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949

5050
### 2026-05-27
5151

52-
-`internal/wailsapp` 增加 auth-file 元数据进程缓存,缓存键为 `name + size + modified`
52+
-`internal/wailsapp` 增加 auth-file 元数据进程缓存;缓存身份按 canonical `name` 归一,`size + modified` 只作为 freshness fingerprint
5353
- `ListAuthFiles` 对普通列表场景优先使用缓存;首次 fresh 下载后只缓存展示元数据,不缓存完整 auth 原文。
5454
- 上传、删除、启停状态修改成功后按文件名失效缓存;文件大小或修改时间变化会自然绕过旧缓存。
5555
- 补充 sidecar mock 测试,覆盖重复列表不重复下载、元数据仍不完整也不重复下载、fingerprint 变化后重新下载。
56+
- 补充同名 fingerprint churn 构造测试与 benchmark,确认同一 auth-file 名称不会因 `size/modified` 变化累积多条 cache entry。

internal/wailsapp/auth_files_cache.go

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,69 @@
11
package wailsapp
22

33
import (
4-
"fmt"
54
"strings"
65
)
76

8-
type authFileMetadataCacheEntry struct {
9-
Name string
7+
type authFileMetadataFingerprint struct {
108
Size int64
119
Modified int64
12-
Type string
13-
Provider string
14-
Priority int
15-
Email string
16-
PlanType string
1710
}
1811

19-
func authFileMetadataCacheKey(name string, size int64, modified int64) string {
20-
return fmt.Sprintf("%s|%d|%d", strings.TrimSpace(name), size, modified)
12+
type authFileMetadataCacheEntry struct {
13+
Name string
14+
Fingerprint authFileMetadataFingerprint
15+
Type string
16+
Provider string
17+
Priority int
18+
Email string
19+
PlanType string
20+
}
21+
22+
func authFileMetadataCacheName(name string) string {
23+
return strings.TrimSpace(name)
24+
}
25+
26+
func authFileMetadataFingerprintFor(file AuthFileItem) authFileMetadataFingerprint {
27+
return authFileMetadataFingerprint{
28+
Size: file.Size,
29+
Modified: file.Modified,
30+
}
2131
}
2232

2333
func (a *App) cachedAuthFileMetadata(file AuthFileItem) (authFileMetadataCacheEntry, bool) {
24-
name := strings.TrimSpace(file.Name)
34+
name := authFileMetadataCacheName(file.Name)
2535
if name == "" {
2636
return authFileMetadataCacheEntry{}, false
2737
}
28-
key := authFileMetadataCacheKey(name, file.Size, file.Modified)
38+
fingerprint := authFileMetadataFingerprintFor(file)
2939
a.authFileCacheMu.RLock()
30-
entry, ok := a.authFileMetadataCache[key]
40+
entry, ok := a.authFileMetadataCache[name]
3141
a.authFileCacheMu.RUnlock()
32-
if !ok {
42+
if !ok || entry.Fingerprint != fingerprint {
3343
return authFileMetadataCacheEntry{}, false
3444
}
3545
return entry, true
3646
}
3747

3848
func (a *App) storeAuthFileMetadata(file AuthFileItem) {
39-
name := strings.TrimSpace(file.Name)
49+
name := authFileMetadataCacheName(file.Name)
4050
if name == "" {
4151
return
4252
}
4353
entry := authFileMetadataCacheEntry{
44-
Name: name,
45-
Size: file.Size,
46-
Modified: file.Modified,
47-
Type: strings.TrimSpace(file.Type),
48-
Provider: strings.TrimSpace(file.Provider),
49-
Priority: file.Priority,
50-
Email: strings.TrimSpace(file.Email),
51-
PlanType: strings.TrimSpace(file.PlanType),
54+
Name: name,
55+
Fingerprint: authFileMetadataFingerprintFor(file),
56+
Type: strings.TrimSpace(file.Type),
57+
Provider: strings.TrimSpace(file.Provider),
58+
Priority: file.Priority,
59+
Email: strings.TrimSpace(file.Email),
60+
PlanType: strings.TrimSpace(file.PlanType),
5261
}
53-
key := authFileMetadataCacheKey(name, file.Size, file.Modified)
5462
a.authFileCacheMu.Lock()
5563
if a.authFileMetadataCache == nil {
5664
a.authFileMetadataCache = map[string]authFileMetadataCacheEntry{}
5765
}
58-
a.authFileMetadataCache[key] = entry
66+
a.authFileMetadataCache[name] = entry
5967
a.authFileCacheMu.Unlock()
6068
}
6169

@@ -94,16 +102,16 @@ func (a *App) invalidateAuthFileMetadataCache(names ...string) {
94102
}
95103
targets := map[string]struct{}{}
96104
for _, name := range names {
97-
if trimmed := strings.TrimSpace(name); trimmed != "" {
105+
if trimmed := authFileMetadataCacheName(name); trimmed != "" {
98106
targets[trimmed] = struct{}{}
99107
}
100108
}
101109
if len(targets) == 0 {
102110
return
103111
}
104-
for key, entry := range a.authFileMetadataCache {
105-
if _, ok := targets[entry.Name]; ok {
106-
delete(a.authFileMetadataCache, key)
112+
for name := range targets {
113+
if _, ok := a.authFileMetadataCache[name]; ok {
114+
delete(a.authFileMetadataCache, name)
107115
}
108116
}
109117
}

internal/wailsapp/auth_files_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"mime/multipart"
99
"net/http"
1010
"net/url"
11+
"strconv"
1112
"strings"
1213
"testing"
1314
)
@@ -227,6 +228,105 @@ func TestListAuthFilesRefreshesMetadataCacheWhenFingerprintChanges(t *testing.T)
227228
}
228229
}
229230

231+
func TestAuthFileMetadataCacheUsesNameForIdentityAndFingerprintForFreshness(t *testing.T) {
232+
app := New("", "", "")
233+
first := AuthFileItem{
234+
Name: " codex-team.json ",
235+
Size: 91,
236+
Modified: 1760000000,
237+
Type: "codex",
238+
Provider: "codex",
239+
Email: "team@example.com",
240+
PlanType: "plus",
241+
Priority: 7,
242+
}
243+
second := first
244+
second.Size = 92
245+
second.Modified = 1760000001
246+
second.PlanType = "pro"
247+
second.Priority = 9
248+
249+
app.storeAuthFileMetadata(first)
250+
if _, ok := app.cachedAuthFileMetadata(AuthFileItem{Name: "codex-team.json", Size: 91, Modified: 1760000000}); !ok {
251+
t.Fatal("expected original fingerprint to hit the metadata cache")
252+
}
253+
254+
app.storeAuthFileMetadata(second)
255+
if len(app.authFileMetadataCache) != 1 {
256+
t.Fatalf("metadata cache entries = %d, want 1 keyed by auth-file name", len(app.authFileMetadataCache))
257+
}
258+
if _, ok := app.cachedAuthFileMetadata(AuthFileItem{Name: "codex-team.json", Size: 91, Modified: 1760000000}); ok {
259+
t.Fatal("old fingerprint should miss after the same auth-file name is refreshed")
260+
}
261+
cached, ok := app.cachedAuthFileMetadata(AuthFileItem{Name: "codex-team.json", Size: 92, Modified: 1760000001})
262+
if !ok {
263+
t.Fatal("expected refreshed fingerprint to hit the metadata cache")
264+
}
265+
if cached.PlanType != "pro" || cached.Priority != 9 {
266+
t.Fatalf("cached metadata = %#v, want refreshed pro priority 9", cached)
267+
}
268+
}
269+
270+
func BenchmarkAuthFileMetadataCacheSameNameFingerprintChurn(b *testing.B) {
271+
files := buildAuthFileMetadataCacheBenchmarkFiles(10_000)
272+
b.ReportAllocs()
273+
274+
for i := 0; i < b.N; i++ {
275+
app := New("", "", "")
276+
for _, file := range files {
277+
app.storeAuthFileMetadata(file)
278+
}
279+
if got := len(app.authFileMetadataCache); got != 1 {
280+
b.Fatalf("metadata cache entries = %d, want 1 keyed by auth-file name", got)
281+
}
282+
}
283+
}
284+
285+
func BenchmarkAuthFileMetadataCacheCompositeKeyFingerprintChurnBaseline(b *testing.B) {
286+
files := buildAuthFileMetadataCacheBenchmarkFiles(10_000)
287+
b.ReportAllocs()
288+
289+
for i := 0; i < b.N; i++ {
290+
cache := map[string]authFileMetadataCacheEntry{}
291+
for _, file := range files {
292+
name := authFileMetadataCacheName(file.Name)
293+
cache[authFileMetadataCompositeKeyBaseline(name, file.Size, file.Modified)] = authFileMetadataCacheEntry{
294+
Name: name,
295+
Fingerprint: authFileMetadataFingerprintFor(file),
296+
Type: file.Type,
297+
Provider: file.Provider,
298+
Priority: file.Priority,
299+
Email: file.Email,
300+
PlanType: file.PlanType,
301+
}
302+
}
303+
if got := len(cache); got != len(files) {
304+
b.Fatalf("baseline metadata cache entries = %d, want %d", got, len(files))
305+
}
306+
}
307+
}
308+
309+
func buildAuthFileMetadataCacheBenchmarkFiles(count int) []AuthFileItem {
310+
files := make([]AuthFileItem, 0, count)
311+
for index := 0; index < count; index++ {
312+
files = append(files, AuthFileItem{
313+
Name: "codex-team.json",
314+
Size: int64(91 + index),
315+
Modified: int64(1760000000 + index),
316+
Type: "codex",
317+
Provider: "codex",
318+
Email: "team@example.com",
319+
PlanType: "plus",
320+
Priority: 7,
321+
})
322+
}
323+
return files
324+
}
325+
326+
func authFileMetadataCompositeKeyBaseline(name string, size int64, modified int64) string {
327+
return name + "|" + strconv.FormatInt(size, 10) + "|" + strconv.FormatInt(modified, 10)
328+
}
329+
230330
func TestUniqueAuthFileUploadName(t *testing.T) {
231331
existing := map[string]struct{}{
232332
"auth.json": {},

0 commit comments

Comments
 (0)