perf: reduce background overhead and improve UI responsiveness
- Move blocking usage queries and core lifecycle operations to background workers, limit concurrency, and prevent overlapping core operations - Coalesce statistics refreshes and consolidate category aggregations, reducing unfiltered analysis queries from 11 to 4 - Cache bundled model prices, reuse management API connections, and reduce redundant checks in the usage collector - Throttle download progress and guide positioning updates, and avoid redundant renders when core status is unchanged - Bound and coalesce configuration watcher events, and reuse a single model catalog refresh worker - Stop scheduling remaining health checks when the dialog closes and limit backend health-check concurrency - Replace overlapping OAuth polling with sequential requests, retry backoff, timeouts, and stale callback protection - Add regression tests and a statistics benchmark script, and move performance verification documentation to agent_docs
This commit is contained in:
1 parent
45ecd37ce5
commit
9cf3878b28
19 files changed
+1050
-300
No files matched your search
+20
-12
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
@@ -26,24 +26,32 @@ const CoreRuntimeContext = createContext<CoreRuntimeContextValue | null>(null);
|
||||
export function CoreRuntimeProvider({ children }: { children: ReactNode }) {
|
||||
const [status, setStatus] = useState<CoreStatus | null>(null);
|
||||
const [statusError, setStatusError] = useState('');
|
||||
const refreshRequest = useRef<Promise<void> | null>(null);
|
||||
const statusRevision = useRef(0);
|
||||
|
||||
const publishStatus = useCallback((nextStatus: CoreStatus | null) => {
|
||||
setStatus(nextStatus);
|
||||
++statusRevision.current;
|
||||
setStatus((previous) => JSON.stringify(previous) === JSON.stringify(nextStatus) ? previous : nextStatus);
|
||||
if (nextStatus) {
|
||||
setStatusError('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
try {
|
||||
const nextStatus = await invoke<CoreStatus>('get_core_status');
|
||||
setStatus(nextStatus);
|
||||
setStatusError('');
|
||||
} catch (error) {
|
||||
setStatus(null);
|
||||
setStatusError(String(error));
|
||||
}
|
||||
}, []);
|
||||
const refreshStatus = useCallback(() => {
|
||||
if (refreshRequest.current) return refreshRequest.current;
|
||||
const revision = statusRevision.current;
|
||||
refreshRequest.current = invoke<CoreStatus>('get_core_status')
|
||||
.then((nextStatus) => {
|
||||
if (revision === statusRevision.current) publishStatus(nextStatus);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (revision !== statusRevision.current) return;
|
||||
setStatus(null);
|
||||
setStatusError(String(error));
|
||||
})
|
||||
.finally(() => { refreshRequest.current = null; });
|
||||
return refreshRequest.current;
|
||||
}, [publishStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
|
||||
@@ -1338,6 +1338,13 @@ function ProviderHealthDialog({ row, onClose }: ProviderHealthDialogProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [results, setResults] = useState<Record<string, ProviderModelHealthState>>({});
|
||||
const [checkingAll, setCheckingAll] = useState(false);
|
||||
const healthControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
healthControllerRef.current = controller;
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
const healthOptions = useMemo<ProviderHealthCheckOptions>(() => ({
|
||||
provider: providerModelType(row.section),
|
||||
@@ -1393,22 +1400,26 @@ function ProviderHealthDialog({ row, onClose }: ProviderHealthDialogProps) {
|
||||
};
|
||||
|
||||
const checkOneModel = async (model: ModelOption) => {
|
||||
const signal = healthControllerRef.current?.signal;
|
||||
if (!signal || signal.aborted) return;
|
||||
const key = model.name.toLowerCase();
|
||||
setResults((current) => ({ ...current, [key]: { status: 'checking' } }));
|
||||
saveResult(await checkProviderModelHealth(healthOptions, model.name));
|
||||
const result = await checkProviderModelHealth(healthOptions, model.name);
|
||||
if (!signal.aborted) saveResult(result);
|
||||
};
|
||||
|
||||
const checkAllModels = async () => {
|
||||
if (models.length === 0) return;
|
||||
const signal = healthControllerRef.current?.signal;
|
||||
if (models.length === 0 || checkingAll || !signal || signal.aborted) return;
|
||||
setCheckingAll(true);
|
||||
setResults(Object.fromEntries(models.map((model) => [
|
||||
model.name.toLowerCase(),
|
||||
{ status: 'checking' } satisfies ProviderModelHealthState,
|
||||
])));
|
||||
try {
|
||||
await checkProviderModelsHealth(healthOptions, models, saveResult);
|
||||
await checkProviderModelsHealth(healthOptions, models, saveResult, undefined, signal);
|
||||
} finally {
|
||||
setCheckingAll(false);
|
||||
if (!signal.aborted) setCheckingAll(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+57
-16
@@ -125,6 +125,7 @@ export function EasyModePage({
|
||||
message: string;
|
||||
} | null>(null);
|
||||
const oauthPollTimer = useRef<number | null>(null);
|
||||
const oauthGeneration = useRef(0);
|
||||
|
||||
const [selectedApiSection, setSelectedApiSection] = useState<ApiSection>("openai-compatibility");
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState("");
|
||||
@@ -200,7 +201,8 @@ export function EasyModePage({
|
||||
useEffect(() => {
|
||||
void refreshSourceStatus();
|
||||
return () => {
|
||||
if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current);
|
||||
++oauthGeneration.current;
|
||||
if (oauthPollTimer.current !== null) window.clearTimeout(oauthPollTimer.current);
|
||||
};
|
||||
}, [refreshSourceStatus]);
|
||||
|
||||
@@ -248,7 +250,9 @@ export function EasyModePage({
|
||||
setGuideOAuthProvider(provider);
|
||||
setGuideOAuthCompleted(false);
|
||||
}
|
||||
if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current);
|
||||
const generation = ++oauthGeneration.current;
|
||||
if (oauthPollTimer.current !== null) window.clearTimeout(oauthPollTimer.current);
|
||||
oauthPollTimer.current = null;
|
||||
setOauthLoggingIn(provider);
|
||||
setOauthNotice(null);
|
||||
|
||||
@@ -263,6 +267,8 @@ export function EasyModePage({
|
||||
browser: "default",
|
||||
});
|
||||
|
||||
if (generation !== oauthGeneration.current) return;
|
||||
|
||||
if (!result.state) {
|
||||
setOauthNotice({
|
||||
tone: "error",
|
||||
@@ -273,15 +279,24 @@ export function EasyModePage({
|
||||
}
|
||||
|
||||
const stateKey = result.state;
|
||||
oauthPollTimer.current = window.setInterval(async () => {
|
||||
const deadline = Date.now() + 10 * 60_000;
|
||||
let failures = 0;
|
||||
const poll = async () => {
|
||||
if (generation !== oauthGeneration.current) return;
|
||||
if (Date.now() >= deadline) {
|
||||
setOauthLoggingIn(null);
|
||||
setOauthNotice({ tone: "error", message: "授权等待超时,请重新发起登录" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const pollRes = await invoke<{ status: string; error?: string }>(
|
||||
"get_oauth_status",
|
||||
{ state: stateKey },
|
||||
);
|
||||
if (generation !== oauthGeneration.current) return;
|
||||
failures = 0;
|
||||
const status = (pollRes.status || "").toLowerCase();
|
||||
if (status === "ok") {
|
||||
if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current);
|
||||
setOauthLoggingIn(null);
|
||||
setOauthNotice({
|
||||
tone: "success",
|
||||
@@ -290,16 +305,29 @@ export function EasyModePage({
|
||||
setGuideOAuthCompleted(true);
|
||||
void refreshSourceStatus();
|
||||
} else if (status === "error") {
|
||||
if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current);
|
||||
setOauthLoggingIn(null);
|
||||
setOauthNotice({
|
||||
tone: "error",
|
||||
message: pollRes.error ? `授权失败: ${pollRes.error}` : "授权失败,请重试",
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}, 1500);
|
||||
if (status === "ok" || status === "error") return;
|
||||
} catch (error) {
|
||||
if (generation !== oauthGeneration.current) return;
|
||||
failures += 1;
|
||||
if (failures >= 3) {
|
||||
setOauthLoggingIn(null);
|
||||
setOauthNotice({ tone: "error", message: String(error) });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (generation === oauthGeneration.current) {
|
||||
oauthPollTimer.current = window.setTimeout(poll, Math.min(1500 * 2 ** failures, 10_000));
|
||||
}
|
||||
};
|
||||
oauthPollTimer.current = window.setTimeout(poll, 1500);
|
||||
} catch (err) {
|
||||
if (generation !== oauthGeneration.current) return;
|
||||
setOauthLoggingIn(null);
|
||||
setOauthNotice({
|
||||
tone: "error",
|
||||
@@ -451,28 +479,41 @@ export function EasyModePage({
|
||||
const el = document.getElementById(currentTargetId);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
setSpotlightRect({
|
||||
const next = {
|
||||
top: Math.max(0, rect.top),
|
||||
left: Math.max(0, rect.left),
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
el.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
};
|
||||
setSpotlightRect((previous) => previous && previous.top === next.top && previous.left === next.left
|
||||
&& previous.width === next.width && previous.height === next.height ? previous : next);
|
||||
} else {
|
||||
setSpotlightRect(null);
|
||||
}
|
||||
}, [guideActive, currentTargetId]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(updateSpotlightPosition, 100);
|
||||
window.addEventListener("resize", updateSpotlightPosition);
|
||||
window.addEventListener("scroll", updateSpotlightPosition, true);
|
||||
let frame: number | null = null;
|
||||
const schedulePosition = () => {
|
||||
if (frame !== null) return;
|
||||
frame = window.requestAnimationFrame(() => {
|
||||
frame = null;
|
||||
updateSpotlightPosition();
|
||||
});
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
if (currentTargetId) document.getElementById(currentTargetId)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
schedulePosition();
|
||||
}, 100);
|
||||
window.addEventListener("resize", schedulePosition);
|
||||
window.addEventListener("scroll", schedulePosition, { capture: true, passive: true });
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
window.removeEventListener("resize", updateSpotlightPosition);
|
||||
window.removeEventListener("scroll", updateSpotlightPosition, true);
|
||||
if (frame !== null) window.cancelAnimationFrame(frame);
|
||||
window.removeEventListener("resize", schedulePosition);
|
||||
window.removeEventListener("scroll", schedulePosition, true);
|
||||
};
|
||||
}, [updateSpotlightPosition]);
|
||||
}, [currentTargetId, updateSpotlightPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
setGuideCardPosition(null);
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getCurrentLocale, useI18n } from '../i18n';
|
||||
import type { MessageKey } from '../i18n/resources';
|
||||
import { formatCacheReadRate, formatGenerationSpeed } from '../services/usageMetrics';
|
||||
import { formatUsageNumber } from '../services/usageNumber';
|
||||
import { createRefreshScheduler } from '../services/refreshScheduler';
|
||||
|
||||
type UsageTab = 'overview' | 'analysis' | 'events' | 'pricing' | 'data-management';
|
||||
type UsageRange = '4h' | '24h' | 'today' | '7d' | '30d' | 'all' | 'custom';
|
||||
@@ -292,6 +293,8 @@ export function UsageRecordsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const requestIdRef = useRef(0);
|
||||
const schedulerRef = useRef<ReturnType<typeof createRefreshScheduler> | null>(null);
|
||||
if (!schedulerRef.current) schedulerRef.current = createRefreshScheduler();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -325,7 +328,7 @@ export function UsageRecordsPage() {
|
||||
};
|
||||
}, [apiKeyHash, customEnd, customStart, model, provider, range, result, source]);
|
||||
|
||||
const loadData = useCallback(
|
||||
const executeLoadData = useCallback(
|
||||
async (quiet = false) => {
|
||||
const requestId = ++requestIdRef.current;
|
||||
const { timeQuery, query } = buildQueries();
|
||||
@@ -348,7 +351,9 @@ export function UsageRecordsPage() {
|
||||
statusRequest,
|
||||
optionsRequest,
|
||||
invoke<UsageOverview>('get_usage_overview', { query }),
|
||||
invoke<UsageAnalysis>('get_usage_analysis', { query }),
|
||||
model || provider || source || apiKeyHash || result !== 'all'
|
||||
? invoke<UsageAnalysis>('get_usage_analysis', { query })
|
||||
: optionsRequest,
|
||||
]);
|
||||
if (requestId !== requestIdRef.current) return;
|
||||
setStatus(nextStatus);
|
||||
@@ -390,11 +395,23 @@ export function UsageRecordsPage() {
|
||||
if (requestId === requestIdRef.current) setLoading(false);
|
||||
}
|
||||
},
|
||||
[activeTab, buildQueries, page, pageSize]
|
||||
[activeTab, buildQueries, page, pageSize, model, provider, source, apiKeyHash, result]
|
||||
);
|
||||
|
||||
const loadData = useCallback(
|
||||
(quiet = false) => {
|
||||
if (!quiet) setLoading(true);
|
||||
return schedulerRef.current!.schedule(() => executeLoadData(quiet), !quiet);
|
||||
},
|
||||
[executeLoadData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
return () => {
|
||||
++requestIdRef.current;
|
||||
schedulerRef.current?.cancelPending();
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -273,25 +273,27 @@ export async function runProviderModelHealthChecks(
|
||||
checkModel: (model: ModelOption, index: number) => Promise<ProviderModelHealthResult>,
|
||||
onModelChecked?: (result: ProviderModelHealthResult, index: number) => void,
|
||||
concurrency = PROVIDER_HEALTH_CONCURRENCY,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ProviderModelHealthResult[]> {
|
||||
const results: ProviderModelHealthResult[] = new Array(models.length);
|
||||
let nextIndex = 0;
|
||||
const workerCount = Math.min(models.length, Math.max(1, Math.floor(concurrency)));
|
||||
|
||||
const runWorker = async () => {
|
||||
while (nextIndex < models.length) {
|
||||
while (nextIndex < models.length && !signal?.aborted) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
const model = models[index];
|
||||
if (!model) continue;
|
||||
const result = await checkModel(model, index);
|
||||
if (signal?.aborted) break;
|
||||
results[index] = result;
|
||||
onModelChecked?.(result, index);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(Array.from({ length: workerCount }, runWorker));
|
||||
return results;
|
||||
return results.filter((result) => result !== undefined);
|
||||
}
|
||||
|
||||
export async function checkProviderModelsHealth(
|
||||
@@ -299,11 +301,13 @@ export async function checkProviderModelsHealth(
|
||||
models: ModelOption[],
|
||||
onModelChecked?: (result: ProviderModelHealthResult, index: number) => void,
|
||||
concurrency = PROVIDER_HEALTH_CONCURRENCY,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ProviderModelHealthResult[]> {
|
||||
return runProviderModelHealthChecks(
|
||||
models,
|
||||
(model) => checkProviderModelHealth(options, model.name),
|
||||
onModelChecked,
|
||||
concurrency,
|
||||
signal,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
export function createRefreshScheduler(minIntervalMs = 1_000) {
|
||||
type Task = () => Promise<void>;
|
||||
type Completion = { promise: Promise<void>; resolve: () => void; reject: (error: unknown) => void };
|
||||
let running = false;
|
||||
let completedAt = -Infinity;
|
||||
let pending: Task | null = null;
|
||||
let urgent = false;
|
||||
let completion: Completion | null = null;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const pump = () => {
|
||||
if (running || !pending) return;
|
||||
if (timer !== null) clearTimeout(timer);
|
||||
timer = null;
|
||||
const delay = urgent ? 0 : Math.max(0, minIntervalMs - (performance.now() - completedAt));
|
||||
if (delay > 0) {
|
||||
timer = setTimeout(pump, delay);
|
||||
return;
|
||||
}
|
||||
const task = pending;
|
||||
const currentCompletion = completion!;
|
||||
pending = null;
|
||||
completion = null;
|
||||
urgent = false;
|
||||
running = true;
|
||||
void (async () => {
|
||||
try {
|
||||
await task();
|
||||
currentCompletion.resolve();
|
||||
} catch (error) {
|
||||
currentCompletion.reject(error);
|
||||
} finally {
|
||||
running = false;
|
||||
completedAt = performance.now();
|
||||
pump();
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
return {
|
||||
schedule(task: Task, immediate = false): Promise<void> {
|
||||
pending = task;
|
||||
urgent ||= immediate;
|
||||
if (!completion) {
|
||||
let resolve!: () => void;
|
||||
let reject!: (error: unknown) => void;
|
||||
const promise = new Promise<void>((onResolved, onRejected) => {
|
||||
resolve = onResolved;
|
||||
reject = onRejected;
|
||||
});
|
||||
completion = { promise, resolve, reject };
|
||||
}
|
||||
const result = completion.promise;
|
||||
pump();
|
||||
return result;
|
||||
},
|
||||
cancelPending() {
|
||||
if (timer !== null) clearTimeout(timer);
|
||||
timer = null;
|
||||
pending = null;
|
||||
urgent = false;
|
||||
completion?.resolve();
|
||||
completion = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user