From f1a6cb509b85ced175cd026dcabf784eab6d45d8 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 16 May 2026 04:34:25 +0000 Subject: [PATCH] perf: speed up code queue first paint --- src/components/frontend/src/code-queue.tsx | 67 +++++++++++++++-- .../microservices/code-queue/src/queue-api.ts | 72 ++++++++++++------- 2 files changed, 107 insertions(+), 32 deletions(-) diff --git a/src/components/frontend/src/code-queue.tsx b/src/components/frontend/src/code-queue.tsx index ea89f33a..5e5e2e92 100644 --- a/src/components/frontend/src/code-queue.tsx +++ b/src/components/frontend/src/code-queue.tsx @@ -14,7 +14,7 @@ const h = React.createElement; const { useEffect, useMemo, useRef } = React; const useState: any = React.useState; const codexTranscriptChunkLimit = 120; -const codexInitialTaskLimit = 24; +const codexInitialTaskLimit = 12; const codexMoreTaskLimit = 48; const queueErrorPreviewLength = 1200; @@ -317,14 +317,18 @@ function queueRunnableTaskId(queueRows: any[], queueId: string, rows: any[]): st async function loadTaskList(apiBaseUrl: string, queueId = allQueuesId, searchQuery = ""): Promise { return requestJson(codexApi( apiBaseUrl, - `/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=1&compact=1&selected=0&skipTrace=1${taskListQuerySuffix(queueId, searchQuery)}`, + `/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=1&compact=1&selected=0&includeActive=0&stats=0&skipTrace=1${taskListQuerySuffix(queueId, searchQuery)}`, ), codexNoCacheOptions()); } -async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 0, queueId = allQueuesId, searchQuery = "", skipTrace = false): Promise { +async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 0, queueId = allQueuesId, searchQuery = "", skipTrace = false, options: AnyRecord = {}): Promise { + const selectedParam = options.selected === false ? "&selected=0" : ""; + const includeActiveParam = options.includeActive === false ? "&includeActive=0" : ""; + const statsParam = options.stats === false ? "&stats=0" : ""; + const limit = Number.isInteger(options.limit) && options.limit > 0 ? Math.min(500, options.limit) : codexInitialTaskLimit; return requestJson(codexApi( apiBaseUrl, - `/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)}`, + `/api/tasks/overview?limit=${encodeURIComponent(String(limit))}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0, afterSeq)))}&preferId=${encodeURIComponent(preferId)}${selectedParam}${includeActiveParam}${statsParam}${skipTrace ? "&skipTrace=1" : ""}${taskListQuerySuffix(queueId, searchQuery)}`, ), codexNoCacheOptions()); } @@ -2528,7 +2532,10 @@ 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, "", true); + const firstPaintListOnly = trackLoad && requestedId.length === 0; + tasksResult = firstPaintListOnly + ? await loadTaskList(apiBaseUrl, queueFilterId, "") + : await loadTaskOverview(apiBaseUrl, requestedId, overviewAfterSeq, queueFilterId, "", true, { stats: false }); if (token !== queueLoadTokenRef.current) { if (trackLoad) trackedLoadInFlightRef.current = false; return; @@ -2574,6 +2581,56 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi const cached = sessionCacheRef.current.get(nextId); if (cached?.task) sessionCacheRef.current.set(nextId, { ...cached, task: { ...row, ...cached.task, status: row.status, updatedAt: row.updatedAt } }); } + if (firstPaintListOnly && row) { + publishCachedTask(nextId, { + ...row, + _summaryLoaded: false, + transcript: [], + _detailLoaded: false, + _transcriptComplete: false, + _transcriptPreview: true, + }, detailLoadTokenRef.current); + setSelectedDetailLoading(true); + setLoadStats({ + phase: "complete", + taskId: nextId, + queueMs, + detailMs: 0, + totalMs: performance.now() - startedAt, + chunks: 0, + transcriptRows: 0, + partial: true, + completedAt: new Date(), + }); + setRefreshedAt(new Date()); + if (trackLoad) trackedLoadInFlightRef.current = false; + void ensureTraceSummary(nextId, true) + .catch((err) => setError(errorText(err, "加载 Codex Trace Summary 失败"))); + void loadTaskOverview(apiBaseUrl, nextId, 0, queueFilterId, "", false) + .then((full: any) => { + if (token !== queueLoadTokenRef.current) return; + const fullRows = taskRows(full); + const selected = overviewSelectedTask(full); + if (fullRows.length > 0) { + setTasksData((previous: any) => { + const mergedRows = mergeTaskRowsPreferLatest([taskRows(previous), fullRows], activeSortId); + return { ...previous, statistics: full?.statistics || previous?.statistics, tasks: applyLocalReadStateToRows(mergedRows) }; + }); + } + if (selected?.id === selectedIdRef.current) { + const transcript = Array.isArray(selected.transcript) ? selected.transcript : []; + publishCachedTask(selected.id, { + ...selected, + transcript, + _summaryLoaded: true, + _detailLoaded: transcript.length > 0, + _transcriptPreview: Boolean(full?.selected?.preview), + }, detailLoadTokenRef.current); + } + }) + .catch(() => {}); + return; + } if (overviewTask?.id === nextId && overviewTranscript !== null) { const cached = sessionCacheRef.current.get(nextId); const existingTranscript = Array.isArray(cached?.task?.transcript) ? cached.task.transcript : []; diff --git a/src/components/microservices/code-queue/src/queue-api.ts b/src/components/microservices/code-queue/src/queue-api.ts index 70ffa213..38f3afd5 100644 --- a/src/components/microservices/code-queue/src/queue-api.ts +++ b/src/components/microservices/code-queue/src/queue-api.ts @@ -457,29 +457,40 @@ async function queueSummaryForResponse(includeDevReady = true, tasks?: QueueTask async function queueSummaryForHealth(includeDevReady = true): Promise { const summary = queueSummary(includeDevReady, ctx().tasks()) as Record; if (!ctx().databaseReady()) return summary; - const [totalRows, statusRows, queueStatusRows, unreadRows] = await Promise.all([ - ctx().sql>`SELECT COUNT(*) AS total FROM unidesk_code_queue_tasks`, - ctx().sql>` - SELECT status, COUNT(*) AS count - FROM unidesk_code_queue_tasks - GROUP BY status - `, - ctx().sql>` - SELECT queue_id, status, COUNT(*) AS count - FROM unidesk_code_queue_tasks - GROUP BY queue_id, status - `, - ctx().sql>` - SELECT queue_id, COUNT(*) AS count - FROM unidesk_code_queue_tasks - WHERE status IN ('succeeded', 'failed', 'canceled') - AND read_at IS NULL - GROUP BY queue_id - `, - ]); + const aggregateRows = await ctx().sql` + SELECT + (SELECT COUNT(*) FROM unidesk_code_queue_tasks) AS total, + COALESCE(( + SELECT jsonb_object_agg(status, count) + FROM ( + SELECT status, COUNT(*) AS count + FROM unidesk_code_queue_tasks + GROUP BY status + ) AS status_counts + ), '{}'::jsonb) AS status_counts, + COALESCE(( + SELECT jsonb_agg(jsonb_build_object('queueId', queue_id, 'status', status, 'count', count)) + FROM ( + SELECT queue_id, status, COUNT(*) AS count + FROM unidesk_code_queue_tasks + GROUP BY queue_id, status + ) AS queue_status_counts + ), '[]'::jsonb) AS queue_status_counts, + COALESCE(( + SELECT jsonb_agg(jsonb_build_object('queueId', queue_id, 'count', count)) + FROM ( + SELECT queue_id, COUNT(*) AS count + FROM unidesk_code_queue_tasks + WHERE status IN ('succeeded', 'failed', 'canceled') + AND read_at IS NULL + GROUP BY queue_id + ) AS unread_counts + ), '[]'::jsonb) AS unread_counts + `; + const aggregate = aggregateRows[0] ?? { total: ctx().tasks().length, status_counts: {}, queue_status_counts: [], unread_counts: [] }; const counts: Record = {}; - for (const row of statusRows) counts[row.status] = Number(row.count); - const unreadByQueue = new Map(unreadRows.map((row) => [ctx().safeQueueId(row.queue_id), Number(row.count)])); + for (const [status, count] of Object.entries(aggregate.status_counts ?? {})) counts[status] = Number(count); + const unreadByQueue = new Map((aggregate.unread_counts ?? []).map((row) => [ctx().safeQueueId(String(row.queueId ?? row.queue_id ?? "")), Number(row.count ?? 0)])); const summaries = new Map updatedAt: queue.updatedAt, }); } - for (const row of queueStatusRows) { - const queueId = ctx().safeQueueId(row.queue_id); + for (const row of aggregate.queue_status_counts ?? []) { + const queueId = ctx().safeQueueId(String(row.queueId ?? row.queue_id ?? "")); let queue = summaries.get(queueId); if (queue === undefined) { queue = { @@ -518,8 +529,9 @@ async function queueSummaryForHealth(includeDevReady = true): Promise }; summaries.set(queueId, queue); } - const count = Number(row.count); - queue.counts[row.status] = count; + const count = Number(row.count ?? 0); + const status = String(row.status || ""); + queue.counts[status] = count; queue.total += count; } for (const [queueId, queue] of summaries) { @@ -540,7 +552,7 @@ async function queueSummaryForHealth(includeDevReady = true): Promise createdAt: queue.createdAt, updatedAt: queue.updatedAt, })) as unknown as JsonValue[]; - summary.total = Number(totalRows[0]?.total ?? ctx().tasks().length); + summary.total = Number(aggregate.total ?? ctx().tasks().length); summary.counts = counts as unknown as JsonValue; summary.unreadTerminal = Array.from(unreadByQueue.values()).reduce((total, count) => total + count, 0); summary.queues = queues; @@ -657,6 +669,12 @@ function taskPageRows(filteredTasks: QueueTask[], url: URL, limit: number): { type TaskIdRow = { id: string; created_at?: Date | string }; type CountRow = { count: string | number }; +type QueueSummaryAggregateRow = { + total: string | number; + status_counts: Record | null; + queue_status_counts: Array<{ queueId?: string; queue_id?: string; status?: string; count?: string | number }> | null; + unread_counts: Array<{ queueId?: string; queue_id?: string; count?: string | number }> | null; +}; type DailyCountRow = { date: string; count: string | number }; type DailyCompletedRow = { date: string;