Compare commits

...
4 Commits
Author SHA1 Message Date
KimiSwitch Dev 803c346854 fix(dashboard): useDashboard 加请求世代校验 — 快速连续切换时间范围时并发 get_summary 响应可能乱序返回导致标签与数据错位;参照 useUsageQuery 的 genRef 模式丢弃过期响应(data/loadStats/error/loading 全部受保护),卸载时使世代失效 2026-10-08 00:28:39 +08:00
KimiSwitch Dev f9f9af0fc9 perf(dashboard): 扫描输入缓存 + 归档合并零浪费 + 锁加固 — resolve_scan_inputs 结果按 config.toml/备份文件 (mtime,len) stat 指纹缓存(命中跳过 15 个配置文件 TOML 解析与 SQLite 读写,set_setting_pub 仅在值变化时写);归档合并先合成后克隆,合成结果为空时原 Arc 直接返回(跳过 6.3 万条 clone+sort,已排序输入的稳定排序为恒等变换);SCAN_CACHE/SCAN_INPUTS_CACHE 改 into_inner 防 Mutex 中毒;新增 4 个测试(Arc::ptr_eq 正反例、配置失效链、跨 home 隔离)并修复测试间 KIMI_SWITCH_DB_PATH 全局 env 并行污染;聚合口径零变化 2026-10-08 00:28:22 +08:00
KimiSwitch Dev 9f51ea5e84 perf(dashboard): 加载统计去掉对整个返回值的 JSON.stringify(数 MB 大对象在 webview 主线程序列化造成卡顿),loadStats 仅保留耗时 ms,中英文案同步 2026-10-07 23:35:30 +08:00
KimiSwitch Dev d190a55bf2 perf(dashboard): 用量统计性能优化 — get_summary/get_day_detail 改异步执行不再阻塞 UI;range_totals 单遍统计(每次调用 7 次全量聚合 → 2 次);扫描缓存改 Arc 共享消除整表深拷贝;wire.jsonl 按 (mtime,len) 逐文件增量解析替换 8s TTL 整表缓存(920MB/6.3万条实测:冷扫描 ~17.7s → ~1.0s,任意间隔切换范围 ~1.5s 且 UI 全程可交互);新增单遍等价性与增量行为测试(含混合增删改 vs 强制全量重扫的全字段指纹比对),聚合口径零变化 2026-10-07 23:35:17 +08:00
5 changed files with 991 additions and 99 deletions

No files matched your search

File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -179,7 +179,7 @@ export function DashboardPage() {
</div> </div>
{loadStats && !loading && ( {loadStats && !loading && (
<span className="hidden text-[10px] text-content-muted sm:inline"> <span className="hidden text-[10px] text-content-muted sm:inline">
{t("loadStats", { ms: loadStats.ms, kb: loadStats.kb })} {t("loadStats", { ms: loadStats.ms })}
</span> </span>
)} )}
</div> </div>
+16 -7
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { SummaryResult } from "../types/dashboard"; import type { SummaryResult } from "../types/dashboard";
@@ -23,9 +23,12 @@ export function useDashboard() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
/** Round-trip timing for the last get_summary call (user-perceived lag). */ /** Round-trip timing for the last get_summary call (user-perceived lag). */
const [loadStats, setLoadStats] = useState<{ ms: number; kb: number } | null>(null); const [loadStats, setLoadStats] = useState<{ ms: number } | null>(null);
// Generation counter: stale responses (superseded range / unmounted) are dropped.
const genRef = useRef(0);
const refresh = useCallback(async (force = false) => { const refresh = useCallback(async (force = false) => {
const gen = ++genRef.current;
setLoading(true); setLoading(true);
setError(null); setError(null);
const start = performance.now(); const start = performance.now();
@@ -34,16 +37,15 @@ export function useDashboard() {
range, range,
refresh: force, refresh: force,
}); });
if (genRef.current !== gen) return;
setData(result); setData(result);
setLoadStats({ setLoadStats({ ms: Math.round(performance.now() - start) });
ms: Math.round(performance.now() - start),
kb: Math.round(JSON.stringify(result).length / 1024),
});
} catch (err) { } catch (err) {
if (genRef.current !== gen) return;
const msg = err instanceof Error ? err.message : String(err); const msg = err instanceof Error ? err.message : String(err);
setError(msg); setError(msg);
} finally { } finally {
setLoading(false); if (genRef.current === gen) setLoading(false);
} }
}, [range]); }, [range]);
@@ -51,6 +53,13 @@ export function useDashboard() {
refresh(); refresh();
}, [refresh]); }, [refresh]);
// Ignore late responses after unmount.
useEffect(() => {
return () => {
genRef.current += 1;
};
}, []);
const changeRange = useCallback((next: DashboardRange) => { const changeRange = useCallback((next: DashboardRange) => {
setRange(next); setRange(next);
try { try {
+1 -1
View File
@@ -257,7 +257,7 @@ export const enTranslations: Record<TranslationKey, string> = {
scanning: "Scanning…", scanning: "Scanning…",
refresh: "Refresh", refresh: "Refresh",
refreshing: "Refreshing…", refreshing: "Refreshing…",
loadStats: "Loaded in {ms} ms · {kb} KB payload", loadStats: "Loaded in {ms} ms",
refreshed: "Refreshed", refreshed: "Refreshed",
cancel: "Cancel", cancel: "Cancel",
confirmDeleteTitle: "Delete session forever?", confirmDeleteTitle: "Delete session forever?",
+1 -1
View File
@@ -249,7 +249,7 @@ export const zhTranslations = {
scanning: "扫描中…", scanning: "扫描中…",
refresh: "刷新", refresh: "刷新",
refreshing: "刷新中…", refreshing: "刷新中…",
loadStats: "加载 {ms} ms · 数据 {kb} KB", loadStats: "加载 {ms} ms",
refreshed: "已刷新", refreshed: "已刷新",
cancel: "取消", cancel: "取消",
confirmDeleteTitle: "永久删除会话?", confirmDeleteTitle: "永久删除会话?",