perf(code-queue): optimize first-load latency with skipTrace and caching

- Frontend: initial task list load passes skipTrace=1 to skip trace stats,
  then fetches full data in background and merges into state
- Code-queue: respect skipTrace=1 param to skip traceStatsForTasks call
- Code-queue: add 8s in-memory cache for OA trace stats (per scope ID)
- Code-queue: add 10s in-memory cache for database statistics summary

These changes reduce first-paint time by eliminating the blocking trace
stats HTTP call on initial load. Trace stats populate asynchronously
after the task list renders.
This commit is contained in:
UniDesk
2026-05-15 18:16:38 +00:00
parent 8b636e2c11
commit 354660c797
3 changed files with 69 additions and 11 deletions
+17 -4
View File
@@ -317,14 +317,14 @@ function queueRunnableTaskId(queueRows: any[], queueId: string, rows: any[]): st
async function loadTaskList(apiBaseUrl: string, queueId = allQueuesId, searchQuery = ""): Promise<any> {
return requestJson(codexApi(
apiBaseUrl,
`/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=1&compact=1&selected=0${taskListQuerySuffix(queueId, searchQuery)}`,
`/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=1&compact=1&selected=0&skipTrace=1${taskListQuerySuffix(queueId, searchQuery)}`,
), codexNoCacheOptions());
}
async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 0, queueId = allQueuesId, searchQuery = ""): Promise<any> {
async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 0, queueId = allQueuesId, searchQuery = "", skipTrace = false): Promise<any> {
return requestJson(codexApi(
apiBaseUrl,
`/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0, afterSeq)))}&preferId=${encodeURIComponent(preferId)}${taskListQuerySuffix(queueId, searchQuery)}`,
`/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0, afterSeq)))}&preferId=${encodeURIComponent(preferId)}${skipTrace ? "&skipTrace=1" : ""}${taskListQuerySuffix(queueId, searchQuery)}`,
), codexNoCacheOptions());
}
@@ -2528,7 +2528,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
const requestedTranscript = Array.isArray(requestedCached?.task?.transcript) ? requestedCached.task.transcript : [];
const overviewAfterSeq = transcriptResumeSeq(requestedTranscript);
let tasksResult = null;
tasksResult = await loadTaskOverview(apiBaseUrl, requestedId, overviewAfterSeq, queueFilterId);
tasksResult = await loadTaskOverview(apiBaseUrl, requestedId, overviewAfterSeq, queueFilterId, "", true);
if (token !== queueLoadTokenRef.current) {
if (trackLoad) trackedLoadInFlightRef.current = false;
return;
@@ -2608,6 +2608,19 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
// cards are shown for historical multi-attempt sessions too.
void ensureTraceSummary(nextId, false, trackLoad ? startedAt : undefined, trackLoad ? queueMs : undefined)
.catch((err) => setError(errorText(err, "加载 Codex Trace Summary 失败")));
// Background refresh with trace stats to populate stepCount in task list
void loadTaskOverview(apiBaseUrl, requestedId, overviewAfterSeq, queueFilterId, "", false)
.then((full: any) => {
if (token !== queueLoadTokenRef.current) return;
const fullRows = taskRows(full);
if (fullRows.length > 0) {
setTasksData((previous: any) => {
const mergedRows = mergeTaskRowsPreferLatest([taskRows(previous), fullRows], activeSortId);
return { ...previous, tasks: applyLocalReadStateToRows(mergedRows) };
});
}
})
.catch(() => {});
return;
}
if (trackLoad) {
@@ -433,14 +433,37 @@ export function publishCodeQueueJudgeEvent(task: QueueTask, queueId: string, typ
});
}
const traceStatsCache = new Map<string, { expiresAt: number; stats: OaTraceStats }>();
let traceStatsCacheLastPrune = 0;
function pruneTraceStatsCache(): void {
const now = Date.now();
if (now - traceStatsCacheLastPrune < 10_000) return;
traceStatsCacheLastPrune = now;
for (const [key, entry] of traceStatsCache) {
if (entry.expiresAt <= now) traceStatsCache.delete(key);
}
}
export async function readOaTraceStatsForScopeIds(scopeIds: string[]): Promise<Map<string, OaTraceStats>> {
const result = new Map<string, OaTraceStats>();
const uniqueScopeIds = Array.from(new Set(scopeIds.map((id) => String(id || "").trim()).filter(Boolean)));
if (uniqueScopeIds.length === 0) return result;
const now = Date.now();
const uncachedIds: string[] = [];
for (const id of uniqueScopeIds) {
const cached = traceStatsCache.get(id);
if (cached !== undefined && cached.expiresAt > now) {
result.set(cached.stats.scopeId, cached.stats);
} else {
uncachedIds.push(id);
}
}
if (uncachedIds.length === 0) { pruneTraceStatsCache(); return result; }
const runtime = ctx();
const url = new URL(`${runtime.baseUrl}/api/stats/trace`);
url.searchParams.set("scopeIds", uniqueScopeIds.join(","));
url.searchParams.set("limit", String(Math.max(100, uniqueScopeIds.length)));
url.searchParams.set("scopeIds", uncachedIds.join(","));
url.searchParams.set("limit", String(Math.max(100, uncachedIds.length)));
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 2500);
try {
@@ -448,17 +471,20 @@ export async function readOaTraceStatsForScopeIds(scopeIds: string[]): Promise<M
if (!response.ok) throw new Error(`status=${response.status}`);
const body = await response.json() as Record<string, unknown>;
const stats = Array.isArray(body.stats) ? body.stats : [];
const cacheTtl = 8_000;
for (const item of stats) {
if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
const record = item as OaTraceStats;
if (typeof record.scopeId !== "string") continue;
result.set(record.scopeId, record);
traceStatsCache.set(record.scopeId, { expiresAt: now + cacheTtl, stats: record });
}
} catch (error) {
runtime.logger("warn", "oa_trace_stats_read_failed", { scopeCount: uniqueScopeIds.length, error: error instanceof Error ? error.message : String(error) });
runtime.logger("warn", "oa_trace_stats_read_failed", { scopeCount: uncachedIds.length, error: error instanceof Error ? error.message : String(error) });
} finally {
clearTimeout(timer);
}
pruneTraceStatsCache();
return result;
}
@@ -733,9 +733,15 @@ function databaseStatsRange(url: URL): { days: number; startDate: string; endDat
};
}
const statisticsCache = new Map<string, { expiresAt: number; value: JsonValue }>();
async function databaseTaskStatisticsSummary(queueId: string | null, url: URL): Promise<JsonValue> {
const generatedAt = new Date().toISOString();
const range = databaseStatsRange(url);
const cacheKey = `${queueId ?? "all"}:${range.days}:${range.startDate}`;
const now = Date.now();
const cached = statisticsCache.get(cacheKey);
if (cached !== undefined && cached.expiresAt > now) return cached.value;
const generatedAt = new Date().toISOString();
const buckets = new Map<string, ReturnType<typeof emptyDatabaseStatsBucket>>();
for (let offset = 0; offset < range.days; offset += 1) {
const date = shiftStatsDateKey(range.startDate, offset);
@@ -853,7 +859,7 @@ async function databaseTaskStatisticsSummary(queueId: string | null, url: URL):
totalDurationMs: 0,
durationSamples: 0,
});
return {
const result = {
generatedAt,
timezone: statsTimeZone,
days: range.days,
@@ -864,6 +870,13 @@ async function databaseTaskStatisticsSummary(queueId: string | null, url: URL):
},
daily,
} as unknown as JsonValue;
statisticsCache.set(cacheKey, { expiresAt: now + 10_000, value: result });
if (statisticsCache.size > 20) {
for (const [key, entry] of statisticsCache) {
if (entry.expiresAt <= now) statisticsCache.delete(key);
}
}
return result;
}
async function databaseTaskTotal(queueId: string | null): Promise<number> {
@@ -1062,7 +1075,10 @@ async function databaseTasksOverviewResponse(url: URL): Promise<Response | null>
} as unknown as JsonValue;
}
const queueContextTasks = [...ctx().tasks(), ...rowsSource];
const traceStats = await ctx().traceStatsForTasks(taskIdsForStats(rowsSource, selectedTask));
const includeTrace = url.searchParams.get("skipTrace") !== "1";
const traceStats = includeTrace
? await ctx().traceStatsForTasks(taskIdsForStats(rowsSource, selectedTask))
: new Map<string, OaTraceStats>();
if (selectedTask !== null && selected !== null && typeof selected === "object" && !Array.isArray(selected)) {
const selectedRecord = selected as Record<string, JsonValue>;
selectedRecord.task = applyStats(selectedRecord.task, traceStats, selectedTask);
@@ -1130,7 +1146,10 @@ async function tasksOverviewResponse(url: URL): Promise<Response> {
maxSeq,
} as unknown as JsonValue;
}
const traceStats = await ctx().traceStatsForTasks(taskIdsForStats(rowsSource, selectedTask));
const includeTrace = url.searchParams.get("skipTrace") !== "1";
const traceStats = includeTrace
? await ctx().traceStatsForTasks(taskIdsForStats(rowsSource, selectedTask))
: new Map<string, OaTraceStats>();
if (selectedTask !== null && selected !== null && typeof selected === "object" && !Array.isArray(selected)) {
const selectedRecord = selected as Record<string, JsonValue>;
selectedRecord.task = applyStats(selectedRecord.task, traceStats, selectedTask);