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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user