feat(dashboard): 最近请求缓存命中列 + 中文模式人民币计价 + 价格解析记忆化

- 最近请求表新增缓存命中率列(缓存读取/总输入)
- useCurrency:中文界面费用按最新 USD→CNY 汇率显示 ¥,英文保持 $(open.er-api.com,缓存 12h,失败兜底 7.2)
- match_price 记忆化缓存:O(记录数×5910) → O(模型种类×5910),扫描从秒级降到百毫秒级
- 仪表盘右上角显示加载耗时/数据量(get_summary 往返埋点);Rust 侧分段计时
This commit is contained in:
KimiSwitch Dev committed 2026-08-02 00:18:19 +08:00
1 parent de132ffb3a
commit 38a11666ac
9 files changed
+173 -28

No files matched your search

+30
View File
@@ -610,7 +610,26 @@ fn strip_context_suffix(model_name: &str) -> Option<String> {
})
}
/// Memoized price resolution. `match_price` is called once per usage record
/// during the scan, and the models.dev suffix scan is O(5910) per miss — with
/// thousands of records sharing a handful of model names, memoizing the result
/// turns the whole scan from O(records × 5910) into O(unique_models × 5910).
static MATCH_PRICE_MEMO: OnceLock<Mutex<HashMap<String, (String, f64, f64, f64, bool)>>> =
OnceLock::new();
/// Resolve a model's price, caching per-model results (hits and misses).
fn match_price(model_name: &str) -> (String, f64, f64, f64, bool) {
let key = model_name.to_ascii_lowercase();
let memo = MATCH_PRICE_MEMO.get_or_init(|| Mutex::new(HashMap::new()));
if let Some(hit) = memo.lock().unwrap().get(&key) {
return hit.clone();
}
let result = match_price_inner(model_name);
memo.lock().unwrap().insert(key, result.clone());
result
}
fn match_price_inner(model_name: &str) -> (String, f64, f64, f64, bool) {
let bare = match model_name.rsplit_once('/') {
Some((_, b)) => b,
None => model_name,
@@ -1537,6 +1556,7 @@ pub fn get_prices() -> PricesResult {
#[tauri::command]
pub fn get_summary(home_override: Option<String>, range: Option<String>, refresh: Option<bool>) -> SummaryResult {
let t0 = std::time::Instant::now();
let refresh = refresh.unwrap_or(false);
let home = resolve_kimi_home(home_override);
let now_ms = std::time::SystemTime::now()
@@ -1544,6 +1564,7 @@ pub fn get_summary(home_override: Option<String>, range: Option<String>, refresh
let r = range.unwrap_or_else(|| "30d".into());
let (records, meta) = scan_usage_cached(&home, refresh);
let t1 = std::time::Instant::now();
let stats = aggregate(&records, &r, now_ms);
let all_stats = aggregate(&records, "all", now_ms);
let heatmap = build_heatmap(&records, now_ms);
@@ -1568,6 +1589,15 @@ pub fn get_summary(home_override: Option<String>, range: Option<String>, refresh
name, provider: std::env::var("KIMI_MODEL_PROVIDER").ok(), model: std::env::var("KIMI_MODEL_ID").ok(),
});
let t2 = std::time::Instant::now();
let scan_ms = t1.duration_since(t0).as_secs_f64() * 1000.0;
let aggregate_ms = t2.duration_since(t1).as_secs_f64() * 1000.0;
let total_ms = t2.duration_since(t0).as_secs_f64() * 1000.0;
eprintln!(
"[dashboard] get_summary range={} records={} scan={:.0}ms aggregate={:.0}ms total={:.0}ms",
r, records.len(), scan_ms, aggregate_ms, total_ms
);
SummaryResult {
home: home.to_string_lossy().to_string(),
valid: is_kimi_home(&home),