feat: 全新 Korom 资源管理服务组件,优化内存占用

新增完整的程序本体资源管理与内存优化功能:
1. 新增背景图预处理服务,主进程将大图降采样至屏幕适配尺寸,避免全分辨率大图占用渲染进程大量内存
2. 实现资源注册表系统,支持引用计数、LRU自动逐出与释放回调
3. 新增资源与内存调试面板,可查看渲染进程JS堆、Electron进程内存与资源缓存状态
4. 修复进程内存信息API的单位与字段映射错误
5. 优化背景层缓存键避免过长dataURL,为Silk动画添加窗口隐藏时停帧逻辑减少GPU占用
6. 修复资源注册表类型适配问题并更新项目文档
This commit is contained in:
2026-09-04 18:56:43 +08:00
parent 7d8b7bd433
commit cd4578de5a
15 changed files with 452 additions and 15 deletions
+2
View File
@@ -24,6 +24,7 @@ import { VersionCardDebug } from "./pages/debug/version-card-debug";
import { UpdateDebug } from "./pages/debug/update-debug";
import { TaskDebug } from "./pages/debug/task-debug";
import { CrashDebug } from "./pages/debug/crash-debug";
import { ResourceDebug } from "./pages/debug/resource-debug";
import { Oobe } from "./pages/oobe";
import { OobeLanguage } from "./pages/oobe/step-language";
import { OobeAgreement } from "./pages/oobe/step-agreement";
@@ -74,6 +75,7 @@ const pageMap = {
"debug-update": UpdateDebug,
"debug-task": TaskDebug,
"debug-crash": CrashDebug,
"debug-resource": ResourceDebug,
} as const;
function App() {
+3 -3
View File
@@ -21,10 +21,10 @@ export interface ProcessMemoryMetric {
peakWorkingSetSize: number; // KB
}
/** process.getProcessMemoryInfo()(主进程);单位为字节 */
/** process.getProcessMemoryInfo()(主进程);单位为 KB */
export interface MainProcessMemory {
workingSetSize: number; // bytes
privateBytes: number; // bytes
workingSetSize: number; // KBresidentSet
privateBytes: number; // KBprivate
}
export interface SystemMemorySnapshot {
@@ -37,7 +37,8 @@ export function BackgroundLayer() {
// 背景切换时旧条目被释放丢弃,大 dataURL 字符串不再被缓存层额外持有。
const trackedKey = useMemo(() => {
if (!bgImage || !bgImage.startsWith("data:")) return null;
return `background:current:${bgImage.slice(0, 96)}`;
// 只取前缀 + 长度作为 key,避免把整段数 MB 的 dataURL 重复存进 Map key
return `background:current:${bgImage.length}:${bgImage.slice(0, 96)}`;
}, [bgImage]);
useEffect(() => {
+11 -1
View File
@@ -1,6 +1,6 @@
/* eslint-disable react/no-unknown-property */
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { forwardRef, useRef, useMemo, useLayoutEffect } from "react";
import { forwardRef, useRef, useMemo, useLayoutEffect, useEffect } from "react";
import { Color, type Mesh, type ShaderMaterial } from "three";
const hexToNormalizedRGB = (hex: string) => {
@@ -83,7 +83,17 @@ const SilkPlane = forwardRef(function SilkPlane({ uniforms }: SilkPlaneProps, re
invalidate();
}, [ref, viewport, invalidate]);
// 窗口隐藏/最小化时停帧(无可见画面,零视觉影响);恢复可见后立即刷新一帧重启动画
useEffect(() => {
const onVisibilityChange = () => {
if (!document.hidden) invalidate();
};
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
}, [invalidate]);
useFrame((_, delta) => {
if (document.hidden) return; // 不可见时不再推进 uTime / 请求帧,避免 GPU 空转
if (ref && typeof ref === "object" && ref.current) {
(ref.current.material as ShaderMaterial).uniforms.uTime.value += 0.1 * delta;
invalidate();
+9 -1
View File
@@ -1,5 +1,5 @@
import { useRouteStore } from "@/stores/routeStore";
import { Monitor, Paintbrush, CreditCard, ListTodo, ChevronRight, FlaskConical, Rocket, AlertTriangle, RefreshCw, SquareTerminal } from "lucide-react";
import { Monitor, Paintbrush, CreditCard, ListTodo, ChevronRight, FlaskConical, Rocket, AlertTriangle, RefreshCw, SquareTerminal, MemoryStick } from "lucide-react";
const debugPages = [
{
@@ -50,6 +50,14 @@ const debugPages = [
color: "text-cyan-500",
bg: "bg-cyan-500/10",
},
{
key: "debug-resource" as const,
icon: MemoryStick,
title: "资源与内存",
desc: "监控渲染进程 JS 堆、进程工作集与资源注册表占用,验证内存优化",
color: "text-violet-500",
bg: "bg-violet-500/10",
},
{
key: "oobe" as const,
icon: Rocket,
+262
View File
@@ -0,0 +1,262 @@
import { useEffect, useRef, useState } from "react";
import { RefreshCw, Eraser, RotateCcw } from "lucide-react";
import { GlassCard, PageHeader, SettingRow } from "./components";
import { getMemorySnapshot, type ProcessMemoryMetric, type SystemMemorySnapshot } from "@/api/system";
import { useResourceStore } from "@/resources/store";
import type { ResourceKind } from "@/resources/types";
interface JsHeapInfo {
used: number; // bytes
total: number; // bytes
limit: number; // bytes
}
function readJsHeap(): JsHeapInfo | null {
const m = (performance as unknown as { memory?: { usedJSHeapSize: number; totalJSHeapSize: number; jsHeapSizeLimit: number } }).memory;
if (!m) return null;
return { used: m.usedJSHeapSize, total: m.totalJSHeapSize, limit: m.jsHeapSizeLimit };
}
const fmtMB = (bytes: number, fraction = 1): string => `${(bytes / 1024 / 1024).toFixed(fraction)} MB`;
const KIND_LABELS: Record<ResourceKind, string> = {
background: "背景图",
image: "图片/图标",
blob: "Blob",
text: "文本",
};
export function ResourceDebug() {
const [proc, setProc] = useState<SystemMemorySnapshot | null>(null);
const [heap, setHeap] = useState<JsHeapInfo | null>(null);
const [domNodes, setDomNodes] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const snapshot = useResourceStore((s) => s.snapshot);
const budgets = useResourceStore((s) => s.budgets);
const clearFree = useResourceStore((s) => s.clearFree);
const resetCounters = useResourceStore((s) => s.resetCounters);
const sample = async () => {
setHeap(readJsHeap());
setDomNodes(document.querySelectorAll("*").length);
try {
const data = await getMemorySnapshot();
setProc(data);
} catch {
setProc(null);
}
};
useEffect(() => {
sample();
timerRef.current = setInterval(sample, 1000);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const byKind = snapshot.stats.byKind;
const kinds = Object.keys(KIND_LABELS) as ResourceKind[];
const processes = [...(proc?.metrics ?? [])].sort((a, b) => b.workingSetSize - a.workingSetSize);
return (
<div className="max-w-3xl mx-auto p-8">
<PageHeader
title="资源与内存"
desc="启动器程序本体资源/内存监控(仅调试页可见):JS 堆、进程工作集、资源注册表占用"
/>
<div className="space-y-6">
{/* 渲染进程堆 */}
<div>
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
JS 1s
</h3>
<div className="space-y-3">
<GlassCard>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<div>
<p className="text-[12px] text-muted-foreground"></p>
<p className="text-lg font-semibold text-foreground tabular-nums">
{heap ? fmtMB(heap.used) : "不可用"}
</p>
</div>
<div>
<p className="text-[12px] text-muted-foreground"></p>
<p className="text-lg font-semibold text-foreground tabular-nums">
{heap ? fmtMB(heap.limit) : "不可用"}
</p>
</div>
<div>
<p className="text-[12px] text-muted-foreground">DOM </p>
<p className="text-lg font-semibold text-foreground tabular-nums">{domNodes}</p>
</div>
<div>
<p className="text-[12px] text-muted-foreground"></p>
<p className="text-lg font-semibold text-foreground tabular-nums">
{proc ? new Date(proc.timestamp).toLocaleTimeString() : "--"}
</p>
</div>
</div>
</GlassCard>
</div>
</div>
{/* 进程工作集 */}
<div>
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
Electron KB
</h3>
<GlassCard>
{proc === null ? (
<p className="text-[13px] text-muted-foreground"> Electron </p>
) : processes.length === 0 ? (
<p className="text-[13px] text-muted-foreground"></p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-[13px]">
<thead>
<tr className="text-left text-muted-foreground/70">
<th className="py-1 pr-4 font-medium"></th>
<th className="py-1 pr-4 font-medium text-right">PID</th>
<th className="py-1 pr-4 font-medium text-right"></th>
<th className="py-1 font-medium text-right"></th>
</tr>
</thead>
<tbody>
{processes.map((p: ProcessMemoryMetric) => (
<tr key={`${p.type}-${p.pid}`} className="border-t border-foreground/5">
<td className="py-1.5 pr-4 text-foreground">{p.type}</td>
<td className="py-1.5 pr-4 text-right text-muted-foreground tabular-nums">{p.pid}</td>
<td className="py-1.5 pr-4 text-right tabular-nums">{fmtMB(p.workingSetSize * 1024)}</td>
<td className="py-1.5 text-right text-muted-foreground tabular-nums">
{fmtMB(p.peakWorkingSetSize * 1024)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</GlassCard>
</div>
{/* 资源注册表 */}
<div>
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
background:current:* = dataURL
</h3>
<div className="space-y-3">
<GlassCard>
<div className="flex flex-wrap gap-x-8 gap-y-2 mb-3">
<div>
<p className="text-[12px] text-muted-foreground"></p>
<p className="text-lg font-semibold text-foreground tabular-nums">{snapshot.stats.entries}</p>
</div>
<div>
<p className="text-[12px] text-muted-foreground"></p>
<p className="text-lg font-semibold text-foreground tabular-nums">{fmtMB(snapshot.stats.totalBytes)}</p>
</div>
<div>
<p className="text-[12px] text-muted-foreground"> / </p>
<p className="text-lg font-semibold text-foreground tabular-nums">
{snapshot.stats.hits} / {snapshot.stats.misses}
</p>
</div>
<div>
<p className="text-[12px] text-muted-foreground">LRU </p>
<p className="text-lg font-semibold text-foreground tabular-nums">{snapshot.stats.evictions}</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{kinds.map((kind) => {
const group = byKind[kind];
return (
<span
key={kind}
className="inline-flex items-center gap-1.5 rounded-full border border-foreground/10 px-3 py-1 text-[12px]"
>
<span className="text-muted-foreground">{KIND_LABELS[kind]}</span>
<span className="tabular-nums font-medium">
{group ? `${group.count} / ${fmtMB(group.bytes)}` : "0 / 0 MB"}
</span>
<span className="text-muted-foreground/50"> {fmtMB(budgets[kind])}</span>
</span>
);
})}
</div>
</GlassCard>
<GlassCard>
<div className="flex items-center justify-between gap-2 mb-2">
<p className="text-[13px] font-medium text-foreground"></p>
<div className="flex items-center gap-2">
<button
onClick={() => useResourceStore.getState().refresh()}
className="inline-flex items-center gap-1 rounded-md border border-foreground/10 px-2 py-1 text-[12px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
>
<RefreshCw className="w-3 h-3" />
</button>
<button
onClick={clearFree}
className="inline-flex items-center gap-1 rounded-md border border-foreground/10 px-2 py-1 text-[12px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
>
<Eraser className="w-3 h-3" />
</button>
<button
onClick={resetCounters}
className="inline-flex items-center gap-1 rounded-md border border-foreground/10 px-2 py-1 text-[12px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
>
<RotateCcw className="w-3 h-3" />
</button>
</div>
</div>
{snapshot.entries.length === 0 ? (
<p className="text-[13px] text-muted-foreground"> background:current:*</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-[13px]">
<thead>
<tr className="text-left text-muted-foreground/70">
<th className="py-1 pr-4 font-medium">Key</th>
<th className="py-1 pr-4 font-medium"></th>
<th className="py-1 pr-4 font-medium text-right"></th>
<th className="py-1 font-medium text-right"></th>
</tr>
</thead>
<tbody>
{snapshot.entries.slice(0, 40).map((e) => (
<tr key={e.key} className="border-t border-foreground/5">
<td className="py-1.5 pr-4 font-mono text-[12px] text-foreground/80 max-w-[420px] truncate">
{e.key}
</td>
<td className="py-1.5 pr-4 text-muted-foreground">{KIND_LABELS[e.kind] ?? e.kind}</td>
<td className="py-1.5 pr-4 text-right tabular-nums">{fmtMB(e.bytes)}</td>
<td className="py-1.5 text-right tabular-nums">{e.refs}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</GlassCard>
</div>
</div>
<GlassCard>
<SettingRow
label="优化效果验证指引"
desc="在首页空闲时记录 JS 堆与进程工作集 → 在设置里选择一张 ≥3MB 的大图作背景 → 观察 background:current 估算字节与 JS 堆;窗口最小化 Silk 动画停帧(GPU 占用回落)。"
>
<span aria-hidden="true" />
</SettingRow>
</GlassCard>
</div>
</div>
);
}
+2 -1
View File
@@ -99,9 +99,10 @@ class ResourceRegistry {
payload: null,
inFlight: null,
cache: opts.cache ?? true,
onRelease: opts.onRelease,
};
this.entries.set(key, entry);
// 用桥接闭包把调用方的 (payload: T) => void 适配为内部 (payload: unknown) => void
entry.onRelease = opts.onRelease ? (payload: unknown): void => opts.onRelease?.(payload as T) : undefined;
const run = async (): Promise<T | null> => {
let value: T | null = null;
+2 -1
View File
@@ -32,8 +32,8 @@ export type RouteKey =
| "debug-version-card"
| "debug-update"
| "debug-task"
| "debug-resource"
| "debug-crash";
export type TitleBarMode = "default" | "sub" | "window" | "oobe";
export type TransitionDirection = "forward" | "backward";
@@ -82,6 +82,7 @@ export const allRoutes: RouteItem[] = [
{ key: "debug-version-card", label: "版本卡片调试", path: "/debug/version-card", hidden: true },
{ key: "debug-update", label: "更新功能测试", path: "/debug/update", hidden: true },
{ key: "debug-task", label: "任务队列调试", path: "/debug/task", hidden: true },
{ key: "debug-resource", label: "资源与内存", path: "/debug/resource", hidden: true },
];
const topLevelKeys = new Set(routes.map((r) => r.key));