perf(web): pause hidden live polling
This commit is contained in:
@@ -28,6 +28,10 @@ test('rapid viewport changes retain one workspace payload and avoid duplicate fl
|
||||
});
|
||||
|
||||
await waitFor(() => expect(monitorWorkspace).toHaveBeenCalledTimes(1));
|
||||
const workspaceQuery = client.getQueryCache().findAll({ queryKey: ['monitor', 'workspace'] })[0];
|
||||
const liveOptions = workspaceQuery?.options as { refetchIntervalInBackground?: boolean; refetchOnWindowFocus?: boolean };
|
||||
expect(liveOptions.refetchIntervalInBackground).toBe(false);
|
||||
expect(liveOptions.refetchOnWindowFocus).toBe(true);
|
||||
expect(monitorSummary).not.toHaveBeenCalled();
|
||||
expect(vehicleRealtime).not.toHaveBeenCalled();
|
||||
for (let index = 1; index <= 8; index += 1) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
import type { MonitorMapResponse, MonitorSummary, MonitorWorkspaceResponse, Page, VehicleRealtimeRow } from '../../api/types';
|
||||
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||
|
||||
export type MonitorFilters = {
|
||||
keyword: string;
|
||||
@@ -143,7 +143,8 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor
|
||||
placeholderData: retainPreviousPageWithinScope<MonitorWorkspaceResponse>(filterScope, 2),
|
||||
staleTime: 5_000,
|
||||
gcTime: MONITOR_CACHE.mapGcTime,
|
||||
refetchInterval: mapEnabled ? MONITOR_REFRESH.fleet : false
|
||||
refetchInterval: mapEnabled ? MONITOR_REFRESH.fleet : false,
|
||||
...LIVE_QUERY_POLICY
|
||||
});
|
||||
const summaryQuery = useQuery({
|
||||
queryKey: ['monitor', 'summary', params.toString()],
|
||||
@@ -151,7 +152,8 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor
|
||||
enabled: !mapEnabled,
|
||||
placeholderData: workspace.data?.summary,
|
||||
refetchInterval: !mapEnabled ? MONITOR_REFRESH.summary : false,
|
||||
gcTime: QUERY_MEMORY.summaryGcTime
|
||||
gcTime: QUERY_MEMORY.summaryGcTime,
|
||||
...LIVE_QUERY_POLICY
|
||||
});
|
||||
const vehiclesQuery = useQuery({
|
||||
queryKey: ['monitor', 'vehicles', params.toString()],
|
||||
@@ -159,7 +161,8 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor
|
||||
enabled: vehicleEnabled && !mapEnabled,
|
||||
placeholderData: workspace.data?.vehicles,
|
||||
refetchInterval: vehicleEnabled && !mapEnabled ? MONITOR_REFRESH.fleet : false,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime,
|
||||
...LIVE_QUERY_POLICY
|
||||
});
|
||||
const summary = mapEnabled ? projectWorkspaceQuery<MonitorSummary>(workspace, (data) => data.summary) : summaryQuery;
|
||||
const vehicles = mapEnabled ? projectWorkspaceQuery<Page<VehicleRealtimeRow>>(workspace, (data) => data.vehicles) : vehiclesQuery;
|
||||
@@ -171,7 +174,8 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor
|
||||
enabled: Boolean(selectedVin),
|
||||
staleTime: 5_000,
|
||||
refetchInterval: selectedVin ? MONITOR_REFRESH.selected : false,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime,
|
||||
...LIVE_QUERY_POLICY
|
||||
});
|
||||
|
||||
return { summary, vehicles, map, selectedVehicle };
|
||||
@@ -196,7 +200,8 @@ export function useMonitorVehicleCard(vin: string, vehicle?: VehicleRealtimeRow,
|
||||
enabled,
|
||||
staleTime: 10_000,
|
||||
refetchInterval: enabled && activelyTracked ? MONITOR_REFRESH.alerts : false,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime,
|
||||
...LIVE_QUERY_POLICY
|
||||
});
|
||||
const address = useQuery({
|
||||
queryKey: ['monitor', 'vehicle-card', 'address', addressPoint?.vin, addressPoint?.key],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { api } from '../../api/client';
|
||||
import type { HistoryDataResponse, HistoryDataRow, HistoryExportRequest, HistoryMetricDefinition, HistorySeriesResponse } from '../../api/types';
|
||||
import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history';
|
||||
import { InlineError } from '../shared/AsyncState';
|
||||
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||
import { usePlatformSession } from '../auth/AuthGate';
|
||||
import { canOperate } from '../auth/session';
|
||||
|
||||
@@ -50,7 +50,7 @@ function ExportJobsPanel() {
|
||||
queryKey: ['history-exports'],
|
||||
queryFn: ({ signal }) => api.historyExports(signal),
|
||||
refetchInterval: (current) => historyExportPollInterval(current.state.data),
|
||||
refetchIntervalInBackground: false,
|
||||
...LIVE_QUERY_POLICY,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
});
|
||||
const jobs = query.data ?? [];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FleetMap } from '../map/FleetMap';
|
||||
import { EmptyState, InlineError } from '../shared/AsyncState';
|
||||
import { formatNumber, relativeFreshness, statusLabel, vehicleStatus } from '../domain/monitor';
|
||||
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData';
|
||||
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||
|
||||
const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT'];
|
||||
const statuses = ['', 'online', 'offline', 'driving', 'idle'];
|
||||
@@ -251,7 +251,8 @@ export default function MonitorPage() {
|
||||
queryKey: ['monitor', 'vehicle-list', listScope, listLimit, listOffset],
|
||||
queryFn: ({ signal }) => api.vehicleRealtime(listParams, signal),
|
||||
enabled: mode === 'list', placeholderData: retainPreviousPageWithinScope<Page<VehicleRealtimeRow>>(listScope, 2), refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime,
|
||||
...LIVE_QUERY_POLICY
|
||||
});
|
||||
const rows = useMemo(() => {
|
||||
const data = vehicles.data?.items ?? [];
|
||||
|
||||
@@ -22,6 +22,12 @@ test('reconciles service identities with bound and identity-required vehicles',
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
|
||||
expect(await screen.findByText('1035 / 234')).toBeInTheDocument();
|
||||
for (const key of [['ops-health-v2'], ['ops-source-readiness-v2']]) {
|
||||
const query = client.getQueryCache().find({ queryKey: key });
|
||||
const liveOptions = query?.options as { refetchIntervalInBackground?: boolean; refetchOnWindowFocus?: boolean };
|
||||
expect(liveOptions.refetchIntervalInBackground).toBe(false);
|
||||
expect(liveOptions.refetchOnWindowFocus).toBe(true);
|
||||
}
|
||||
expect(screen.getByText('服务身份 / 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument();
|
||||
expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument();
|
||||
|
||||
@@ -2,14 +2,15 @@ import { IconRefresh } from '@douyinfe/semi-icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import { InlineError } from '../shared/AsyncState';
|
||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
|
||||
|
||||
function statusLabel(status: string) {
|
||||
return { ok: '正常', warning: '关注', error: '异常' }[status] ?? status;
|
||||
}
|
||||
|
||||
export default function OperationsPage() {
|
||||
const health = useQuery({ queryKey: ['ops-health-v2'], queryFn: ({ signal }) => api.opsHealth(signal), refetchInterval: 15_000, staleTime: 8_000 });
|
||||
const readiness = useQuery({ queryKey: ['ops-source-readiness-v2'], queryFn: ({ signal }) => api.sourceReadiness(signal), refetchInterval: 30_000, staleTime: 15_000 });
|
||||
const health = useQuery({ queryKey: ['ops-health-v2'], queryFn: ({ signal }) => api.opsHealth(signal), refetchInterval: 15_000, staleTime: 8_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY });
|
||||
const readiness = useQuery({ queryKey: ['ops-source-readiness-v2'], queryFn: ({ signal }) => api.sourceReadiness(signal), refetchInterval: 30_000, staleTime: 15_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY });
|
||||
const data = health.data; const sources = readiness.data;
|
||||
const refresh = () => Promise.all([health.refetch(), readiness.refetch()]);
|
||||
return <div className="v2-ops-page">
|
||||
|
||||
@@ -9,7 +9,7 @@ import { api } from '../../api/client';
|
||||
import type { LatestTelemetryResponse, QualityIssueRow, VehicleDetail, VehicleProfileSyncItem, VehicleProfileSyncResult } from '../../api/types';
|
||||
import { usePlatformSession } from '../auth/AuthGate';
|
||||
import { canAdminister } from '../auth/session';
|
||||
import { QUERY_MEMORY } from '../queryPolicy';
|
||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
|
||||
import { formatTelemetryTime, formatTelemetryValue, telemetryQualityLabel } from '../domain/telemetry';
|
||||
import { formatZhNumber } from '../domain/formatters';
|
||||
import { parseVehicleProfileSyncCSV, vehicleProfileSyncCSVHeader } from '../domain/profileSync';
|
||||
@@ -198,7 +198,7 @@ function VehicleRecord({ detail, telemetry, telemetryPending, telemetryError, on
|
||||
export default function VehiclePage() {
|
||||
const { vin } = useParams();
|
||||
const query = useQuery({ queryKey: ['vehicle-detail', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin!, limit: '20' }), signal), gcTime: QUERY_MEMORY.summaryGcTime });
|
||||
const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, refetchIntervalInBackground: false });
|
||||
const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, ...LIVE_QUERY_POLICY });
|
||||
if (!vin) return <VehicleSearch />;
|
||||
if (query.isPending) return <PageLoading />;
|
||||
if (query.isError) return <div className="v2-page-error"><InlineError message={query.error instanceof Error ? query.error.message : '车辆档案加载失败'} onRetry={() => query.refetch()} /></div>;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from './queryPolicy';
|
||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from './queryPolicy';
|
||||
|
||||
describe('query memory policy', () => {
|
||||
it('pauses live polling in hidden tabs and refreshes immediately on return', () => {
|
||||
expect(LIVE_QUERY_POLICY).toEqual({
|
||||
refetchIntervalInBackground: false,
|
||||
refetchOnWindowFocus: true
|
||||
});
|
||||
});
|
||||
it('drops high-volume route results immediately and bounds reusable small results', () => {
|
||||
expect(QUERY_MEMORY.highVolumeGcTime).toBe(0);
|
||||
expect(QUERY_MEMORY.optionGcTime).toBeLessThanOrEqual(30_000);
|
||||
|
||||
@@ -4,6 +4,14 @@ export const QUERY_MEMORY = {
|
||||
summaryGcTime: 60_000
|
||||
} as const;
|
||||
|
||||
// Live views must not spend traffic while the browser tab is hidden, but they
|
||||
// should reconcile immediately when an operator returns instead of waiting for
|
||||
// the next interval tick.
|
||||
export const LIVE_QUERY_POLICY = {
|
||||
refetchIntervalInBackground: false,
|
||||
refetchOnWindowFocus: true
|
||||
} as const;
|
||||
|
||||
export function retainPreviousPageWithinScope<T>(scope: string, scopeIndex = 1) {
|
||||
return (previous: T | undefined, previousQuery?: { queryKey: readonly unknown[] }) => previousQuery?.queryKey[scopeIndex] === scope ? previous : undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user