From ee96b8803ceab4a2911d250074bacfc09c2d223f Mon Sep 17 00:00:00 2001 From: lingniu Date: Thu, 16 Jul 2026 06:14:32 +0800 Subject: [PATCH] perf(history): reduce export polling load --- .../apps/web/src/v2/domain/history.test.ts | 10 +++++++++- .../apps/web/src/v2/domain/history.ts | 13 ++++++++++++- .../apps/web/src/v2/pages/HistoryPage.test.tsx | 17 ++++++++++++++++- .../apps/web/src/v2/pages/HistoryPage.tsx | 10 ++++++++-- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/domain/history.test.ts b/vehicle-data-platform/apps/web/src/v2/domain/history.test.ts index 20a1d9b0..86329a4b 100644 --- a/vehicle-data-platform/apps/web/src/v2/domain/history.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/domain/history.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildHistoryChartSeries, buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, parseHistoryKeywords } from './history'; +import { buildHistoryChartSeries, buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, HISTORY_EXPORT_POLL_MS, historyExportPollInterval, parseHistoryKeywords } from './history'; describe('history domain', () => { it('parses and bounds multi-vehicle input', () => { @@ -41,4 +41,12 @@ describe('history domain', () => { expect(formatExportFileSize(1536)).toBe('1.5 KB'); expect(formatExportFileSize(5 * 1024 * 1024)).toBe('5.0 MB'); }); + + it('polls active export jobs conservatively and stops after terminal states', () => { + expect(historyExportPollInterval([{ status: 'running' }])).toBe(HISTORY_EXPORT_POLL_MS.running); + expect(historyExportPollInterval([{ status: 'queued' }])).toBe(HISTORY_EXPORT_POLL_MS.queued); + expect(historyExportPollInterval([{ status: 'queued' }, { status: 'running' }])).toBe(HISTORY_EXPORT_POLL_MS.running); + expect(historyExportPollInterval([{ status: 'completed' }, { status: 'failed' }])).toBe(false); + expect(historyExportPollInterval()).toBe(false); + }); }); diff --git a/vehicle-data-platform/apps/web/src/v2/domain/history.ts b/vehicle-data-platform/apps/web/src/v2/domain/history.ts index 0f62d6c5..aa5d9182 100644 --- a/vehicle-data-platform/apps/web/src/v2/domain/history.ts +++ b/vehicle-data-platform/apps/web/src/v2/domain/history.ts @@ -1,4 +1,15 @@ -import type { HistoryDataRow, HistoryMetricDefinition, HistorySeries, HistorySeriesResponse } from '../../api/types'; +import type { HistoryDataRow, HistoryExportJob, HistoryMetricDefinition, HistorySeries, HistorySeriesResponse } from '../../api/types'; + +export const HISTORY_EXPORT_POLL_MS = { + running: 3_000, + queued: 5_000 +} as const; + +export function historyExportPollInterval(jobs?: Array>) { + if (jobs?.some((job) => job.status === 'running')) return HISTORY_EXPORT_POLL_MS.running; + if (jobs?.some((job) => job.status === 'queued')) return HISTORY_EXPORT_POLL_MS.queued; + return false; +} export function parseHistoryKeywords(value: string) { const seen = new Set(); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx index 5026ea4b..6437db47 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx @@ -41,7 +41,8 @@ function historySeries(vin: string, plate: string, asOf: string): HistorySeriesR function renderPage() { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - return render(); + const view = render(); + return { ...view, client }; } test('clears stale rows, charts, and evidence as soon as the history scope changes', async () => { @@ -76,3 +77,17 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang expect(await screen.findByText('NEWVIN-evidence')).toBeInTheDocument(); await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument()); }); + +test('releases export job state immediately after leaving the history page', async () => { + mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] }); + mocks.historyData.mockResolvedValue(historyData('OLDVIN', '旧车牌', 'old-as-of')); + mocks.historySeries.mockResolvedValue(historySeries('OLDVIN', '旧车牌', 'old-as-of')); + mocks.historyExports.mockResolvedValue([{ id: 'export-running', name: '运行中导出', status: 'running', progress: 30, format: 'csv', category: 'location', keywords: ['OLDVIN'], rowCount: 0, totalRows: 100, processedRows: 30, fileSizeBytes: 0, createdAt: '2026-07-16T04:00:00Z', updatedAt: '2026-07-16T04:00:01Z', evidence: 'test export' }]); + + const { client, unmount } = renderPage(); + expect(await screen.findByText('运行中导出')).toBeInTheDocument(); + expect(client.getQueryCache().find({ queryKey: ['history-exports'] })).toBeDefined(); + + unmount(); + await waitFor(() => expect(client.getQueryCache().find({ queryKey: ['history-exports'] })).toBeUndefined()); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx index 4dfb5d92..6bb8349f 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -4,7 +4,7 @@ import { FormEvent, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { HistoryDataResponse, HistoryDataRow, HistoryExportRequest, HistoryMetricDefinition, HistorySeriesResponse } from '../../api/types'; -import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, parseHistoryKeywords } from '../domain/history'; +import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history'; import { InlineError } from '../shared/AsyncState'; import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; @@ -42,7 +42,13 @@ function CreateExportButton({ request, disabled }: { request: HistoryExportReque } function ExportJobsPanel() { - const query = useQuery({ queryKey: ['history-exports'], queryFn: ({ signal }) => api.historyExports(signal), refetchInterval: (current) => current.state.data?.some((job) => job.status === 'queued' || job.status === 'running') ? 1000 : false }); + const query = useQuery({ + queryKey: ['history-exports'], + queryFn: ({ signal }) => api.historyExports(signal), + refetchInterval: (current) => historyExportPollInterval(current.state.data), + refetchIntervalInBackground: false, + gcTime: QUERY_MEMORY.highVolumeGcTime + }); const jobs = query.data ?? []; return
导出任务{jobs.length}
{jobs.slice(0, 6).map((job) =>
{job.name}{job.status === 'queued' ? '等待单并发执行' : job.status === 'running' ? `${job.processedRows.toLocaleString('zh-CN')} / ${job.totalRows.toLocaleString('zh-CN')} 行 · ${job.progress}%` : job.status === 'completed' ? `${job.rowCount.toLocaleString('zh-CN')} 行 · ${formatExportFileSize(job.fileSizeBytes)} · 已完成` : job.error || '失败'}
{job.downloadUrl ? 下载 : {job.status === 'running' ? `${job.progress}%` : '—'}}
)}{query.isError ?
导出任务加载失败
: !jobs.length ?
尚未创建导出任务
: null}
; }