feat(web): 设置缓存分区显示命中率
缓存区表加「命中率」列(近 7 天、三个进程合计)与内存 / 数据库 / 旧值 / 未命中明细; 缓存有效期卡片补 TMDB 人物与图片、豆瓣 / Bangumi 搜索与条目。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
1535d8664e
commit
81dca5d664
2 files changed
+93
-4
No files matched your search
@@ -0,0 +1,45 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { type CacheRegionList, DEFAULT_SETTINGS } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { CacheSection, hitDetailText, hitRateText } from "./cache.tsx";
|
||||
|
||||
// 设置 → 缓存:缓存区表显示统一元数据缓存的命中率(最近 7 天、三个进程合计)。
|
||||
|
||||
const hits = { l1Hits: 50, l2Hits: 30, staleHits: 4, misses: 12, failures: 1, refreshes: 4, hitRate: 84 / 96 };
|
||||
|
||||
describe("缓存区命中率", () => {
|
||||
it("命中率按百分比显示一位小数;没有统计的区显示「—」,没读取过显示「暂无读取」", () => {
|
||||
expect(hitRateText(hits)).toBe("87.5%");
|
||||
expect(hitRateText(undefined)).toBe("—");
|
||||
expect(hitRateText({ ...hits, hitRate: null })).toBe("暂无读取");
|
||||
expect(hitDetailText(hits)).toBe("内存 50 · 数据库 30 · 旧值 4 · 未命中 12 · 回源失败 1 · 后台刷新 4");
|
||||
expect(hitDetailText({ ...hits, staleHits: 0, failures: 0, refreshes: 0 })).toBe("内存 50 · 数据库 30 · 未命中 12");
|
||||
});
|
||||
|
||||
it("缓存区表有「命中率」列,元数据区显示命中率与明细", () => {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false, enabled: false } } });
|
||||
qc.setQueryData(qk.settings, DEFAULT_SETTINGS);
|
||||
const regions: CacheRegionList = {
|
||||
items: [
|
||||
{ region: "tmdb.request", entries: 120, expired: 0, bytes: 4096, lastWriteAt: null, hits },
|
||||
{ region: "site.detail", entries: 3, expired: 0, bytes: 100, lastWriteAt: null },
|
||||
],
|
||||
};
|
||||
qc.setQueryData(["cache", "regions"], regions);
|
||||
const html = renderToString(
|
||||
createElement(
|
||||
QueryClientProvider,
|
||||
{ client: qc },
|
||||
createElement(MemoryRouter, { initialEntries: ["/console/settings#cache"] }, createElement(CacheSection)),
|
||||
),
|
||||
);
|
||||
expect(html).toContain("命中率");
|
||||
expect(html).toContain("87.5%");
|
||||
expect(html).toContain("内存 50 · 数据库 30");
|
||||
expect(html).toContain("最近 7 天");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cacheRegionLabel } from "@umt/contracts";
|
||||
import { type CacheHitStats, cacheRegionLabel } from "@umt/contracts";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -32,6 +32,8 @@ export type CacheRow = {
|
||||
expired: number;
|
||||
bytes: number;
|
||||
lastWriteAt: string | null;
|
||||
/** 统一元数据缓存的区才有:最近 7 天命中统计 */
|
||||
hits?: CacheHitStats | undefined;
|
||||
};
|
||||
|
||||
/** 区名 → 行:有中文名的用中文名,没有的原样显示 region */
|
||||
@@ -39,6 +41,23 @@ export function cacheRow(r: Omit<CacheRow, "label">): CacheRow {
|
||||
return { ...r, label: cacheRegionLabel(r.region) ?? r.region };
|
||||
}
|
||||
|
||||
/** 命中率文字:没有统计的区显示「—」,有统计没读取显示「暂无读取」 */
|
||||
export function hitRateText(h: CacheHitStats | undefined): string {
|
||||
if (!h) return "—";
|
||||
if (h.hitRate === null) return "暂无读取";
|
||||
return `${Math.round(h.hitRate * 1000) / 10}%`;
|
||||
}
|
||||
|
||||
/** 命中明细:内存 / 数据库 / 旧值 / 未命中,有失败与后台刷新时附上 */
|
||||
export function hitDetailText(h: CacheHitStats): string {
|
||||
const parts = [`内存 ${h.l1Hits}`, `数据库 ${h.l2Hits}`];
|
||||
if (h.staleHits) parts.push(`旧值 ${h.staleHits}`);
|
||||
parts.push(`未命中 ${h.misses}`);
|
||||
if (h.failures) parts.push(`回源失败 ${h.failures}`);
|
||||
if (h.refreshes) parts.push(`后台刷新 ${h.refreshes}`);
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
function CacheRegionsBlock() {
|
||||
const regions = useCacheRegions();
|
||||
const images = useImageCache();
|
||||
@@ -85,6 +104,19 @@ function CacheRegionsBlock() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "hitRate",
|
||||
header: "命中率",
|
||||
align: "right",
|
||||
cell: (r) => (
|
||||
<div className="tabular-nums">
|
||||
{hitRateText(r.hits)}
|
||||
{r.hits && r.hits.hitRate !== null ? (
|
||||
<p className="text-meta text-muted-foreground">{hitDetailText(r.hits)}</p>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "bytes",
|
||||
header: "占用",
|
||||
@@ -121,7 +153,7 @@ function CacheRegionsBlock() {
|
||||
section="cache"
|
||||
anchor="regions"
|
||||
title="缓存区"
|
||||
description="清空后下次用到时重新请求,不影响作品、资源和文件。已过期的条目由定时任务清理。"
|
||||
description="清空后下次用到时重新请求,不影响作品、资源和文件。已过期的条目由定时任务清理。命中率按最近 7 天、三个进程合计。"
|
||||
plain
|
||||
>
|
||||
<DataTable<CacheRow>
|
||||
@@ -224,7 +256,16 @@ export const IMAGES_CARD: CardSpec = {
|
||||
}),
|
||||
};
|
||||
|
||||
const TTL_KEYS = ["tmdbSearchDays", "tmdbDetailDays", "tmdbMissDays", "siteDetailDays", "webSearchDays"] as const;
|
||||
const TTL_KEYS = [
|
||||
"tmdbSearchDays",
|
||||
"tmdbDetailDays",
|
||||
"tmdbMissDays",
|
||||
"imageDays",
|
||||
"metaSearchDays",
|
||||
"metaDetailDays",
|
||||
"siteDetailDays",
|
||||
"webSearchDays",
|
||||
] as const;
|
||||
const ttlField = (k: string, label: string, hint?: string) => ({
|
||||
name: `storage.cacheTtl.${k}`,
|
||||
label,
|
||||
@@ -234,11 +275,14 @@ const ttlField = (k: string, label: string, hint?: string) => ({
|
||||
export const TTL_CARD: CardSpec = {
|
||||
anchor: "ttl",
|
||||
title: "缓存有效期",
|
||||
description: "单位天。追踪中的作品命中 TMDB 详情缓存时自动续期。",
|
||||
description: "单位天。过期后先用旧值、后台刷新;关注中的作品命中详情缓存时自动续期,不会因条数上限被清掉。",
|
||||
fields: [
|
||||
ttlField("tmdbSearchDays", "TMDB 搜索与识别结果"),
|
||||
ttlField("tmdbDetailDays", "TMDB 详情"),
|
||||
ttlField("tmdbMissDays", "TMDB 空结果", "搜索无结果、未识别时的负缓存"),
|
||||
ttlField("imageDays", "TMDB 人物与图片"),
|
||||
ttlField("metaSearchDays", "豆瓣 / Bangumi 搜索"),
|
||||
ttlField("metaDetailDays", "豆瓣 / Bangumi 条目"),
|
||||
ttlField("siteDetailDays", "站点详情页内容"),
|
||||
ttlField("webSearchDays", "网页搜索结果"),
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user