From 91b5d76657bef7e6e006b7dd7ec329289392dc79 Mon Sep 17 00:00:00 2001 From: lingniu Date: Thu, 16 Jul 2026 08:38:41 +0800 Subject: [PATCH] perf(monitor): bound map refresh fingerprints --- .../apps/web/src/v2/map/FleetMap.test.tsx | 19 ++++- .../apps/web/src/v2/map/FleetMap.tsx | 76 ++++++++++++++++--- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/map/FleetMap.test.tsx b/vehicle-data-platform/apps/web/src/v2/map/FleetMap.test.tsx index ca60f5cd..c9a50ac5 100644 --- a/vehicle-data-platform/apps/web/src/v2/map/FleetMap.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/map/FleetMap.test.tsx @@ -2,7 +2,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-libra import { afterEach, expect, test, vi } from 'vitest'; import type { MonitorMapResponse } from '../../api/types'; import { wgs84ToGcj02, type AMapLike, type AMapMap, type AMapMassPoint } from '../../integrations/amap'; -import { FleetMap } from './FleetMap'; +import { FleetMap, massPointFingerprint } from './FleetMap'; const setData = vi.fn<(data: AMapMassPoint[]) => void>(); const setStyle = vi.fn(); @@ -124,6 +124,23 @@ const pointMap: MonitorMapResponse = { }] }; +test('keeps large fleet redraw fingerprints constant-size and sensitive to point changes', () => { + const manyPoints = Array.from({ length: 10_000 }, (_, index) => ({ + ...pointMap.points[index % pointMap.points.length], + vin: `LARGE-FLEET-${index}`, + longitude: 113.26 + index / 1_000_000 + })); + const fingerprint = massPointFingerprint('points', [], manyPoints, []); + const changed = massPointFingerprint('points', [], [ + ...manyPoints.slice(0, -1), + { ...manyPoints[manyPoints.length - 1], longitude: 114.5 } + ], []); + + expect(fingerprint.length).toBeLessThan(40); + expect(changed).not.toBe(fingerprint); + expect(massPointFingerprint('points', [], manyPoints, [])).toBe(fingerprint); +}); + function amapMock(): AMapLike { return { Map: TestMap as unknown as AMapLike['Map'], diff --git a/vehicle-data-platform/apps/web/src/v2/map/FleetMap.tsx b/vehicle-data-platform/apps/web/src/v2/map/FleetMap.tsx index 01237188..6d374ad0 100644 --- a/vehicle-data-platform/apps/web/src/v2/map/FleetMap.tsx +++ b/vehicle-data-platform/apps/web/src/v2/map/FleetMap.tsx @@ -18,6 +18,8 @@ import { vehicleStatus } from '../domain/monitor'; import type { MonitorViewport } from '../hooks/useMonitorData'; const COLORS = ['#12a46f', '#9aa6b7', '#1677ff', '#f59e0b', '#ef4444']; +const CLUSTER_VISUAL_CACHE_LIMIT = 256; +const clusterVisualCache = new Map(); function dotDataUrl(color: string) { const svg = ``; @@ -25,12 +27,24 @@ function dotDataUrl(color: string) { } function clusterVisual(count: number) { + const cached = clusterVisualCache.get(count); + if (cached) { + clusterVisualCache.delete(count); + clusterVisualCache.set(count, cached); + return cached; + } const label = count.toLocaleString('en-US'); const diameter = Math.min(54, 34 + Math.max(0, label.length - 1) * 4); const center = diameter / 2; const fontSize = label.length >= 6 ? 9 : label.length >= 4 ? 10 : 11; const svg = `${label}`; - return { diameter, url: `data:image/svg+xml,${encodeURIComponent(svg)}` }; + const visual = { diameter, url: `data:image/svg+xml,${encodeURIComponent(svg)}` }; + if (clusterVisualCache.size >= CLUSTER_VISUAL_CACHE_LIMIT) { + const oldest = clusterVisualCache.keys().next().value; + if (oldest !== undefined) clusterVisualCache.delete(oldest); + } + clusterVisualCache.set(count, visual); + return visual; } function viewportFromMap(map: AMapMap): MonitorViewport | null { @@ -92,18 +106,62 @@ type LabelMarkerEntry = { const NO_VEHICLE_POINTS: VehicleRealtimeRow[] = []; -function massPointSignature( +function createPointFingerprint() { + let left = 0x811c9dc5; + let right = 0x9e3779b9; + let fields = 0; + const add = (value: unknown) => { + const text = String(value ?? ''); + for (let index = 0; index < text.length; index += 1) { + const code = text.charCodeAt(index); + left = Math.imul(left ^ code, 0x01000193); + right = Math.imul(right ^ code, 0x85ebca6b); + } + left = Math.imul(left ^ 31, 0x01000193); + right = Math.imul(right ^ 127, 0x85ebca6b); + fields += 1; + }; + const value = () => `${fields}:${(left >>> 0).toString(36)}:${(right >>> 0).toString(36)}`; + return { add, value }; +} + +export function massPointFingerprint( mode: MonitorMapResponse['mode'] | undefined, clusters: MonitorMapResponse['clusters'] | undefined, mapPoints: MonitorMapResponse['points'] | undefined, points: VehicleRealtimeRow[] ) { - if (mode) return [ - `mode:${mode}`, - ...(clusters ?? []).map((cluster) => `c:${cluster.id}:${cluster.longitude}:${cluster.latitude}:${cluster.count}`), - ...(mapPoints ?? []).map((point) => `p:${point.vin}:${point.longitude}:${point.latitude}:${point.status}:${point.plate || ''}`) - ].join('|'); - return ['vehicles', ...points.map((point) => `p:${point.vin}:${point.longitude}:${point.latitude}:${vehicleStatus(point)}:${point.plate || ''}`)].join('|'); + const fingerprint = createPointFingerprint(); + fingerprint.add(mode ?? 'vehicles'); + if (mode) { + const clusterRows = clusters ?? []; + const pointRows = mapPoints ?? []; + fingerprint.add(clusterRows.length); + for (const cluster of clusterRows) { + fingerprint.add(cluster.id); + fingerprint.add(cluster.longitude); + fingerprint.add(cluster.latitude); + fingerprint.add(cluster.count); + } + fingerprint.add(pointRows.length); + for (const point of pointRows) { + fingerprint.add(point.vin); + fingerprint.add(point.longitude); + fingerprint.add(point.latitude); + fingerprint.add(point.status); + fingerprint.add(point.plate || ''); + } + } else { + fingerprint.add(points.length); + for (const point of points) { + fingerprint.add(point.vin); + fingerprint.add(point.longitude); + fingerprint.add(point.latitude); + fingerprint.add(vehicleStatus(point)); + fingerprint.add(point.plate || ''); + } + } + return fingerprint.value(); } function labelMarkerSignature(point: PlateLabelPoint, selected: boolean, placement?: { direction: 'left' | 'right'; textOffset: [number, number] }) { @@ -335,7 +393,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect mass.setStyle?.([...baseStyles, ...clusterStyles]); massStyleSignatureRef.current = styleSignature; } - const dataSignature = massPointSignature(monitorMode, monitorClusters, monitorPoints, fallbackPoints); + const dataSignature = massPointFingerprint(monitorMode, monitorClusters, monitorPoints, fallbackPoints); if (massDataSignatureRef.current === dataSignature) return; const clusterStyleIndexes = new Map(clusterCounts.map((count, index) => [count, COLORS.length + index])); const data: AMapMassPoint[] = monitorMode ? [