perf(monitor): bound map refresh fingerprints

This commit is contained in:
lingniu
2026-07-16 08:38:41 +08:00
parent 4c54cfa6ff
commit 91b5d76657
2 changed files with 85 additions and 10 deletions

View File

@@ -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'],

View File

@@ -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<number, { diameter: number; url: string }>();
function dotDataUrl(color: string) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" viewBox="0 0 18 18"><circle cx="9" cy="9" r="6.5" fill="${color}" stroke="white" stroke-width="2.5"/></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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${diameter * 3}" height="${diameter * 3}" viewBox="0 0 ${diameter} ${diameter}"><circle cx="${center}" cy="${center}" r="${center - 2}" fill="#1268f3" stroke="white" stroke-width="2.5"/><circle cx="${center}" cy="${center}" r="${center - 5}" fill="none" stroke="rgba(255,255,255,.22)" stroke-width="1"/><text x="${center}" y="${center + fontSize * 0.34}" text-anchor="middle" font-family="Inter,Arial,sans-serif" font-size="${fontSize}" font-weight="800" fill="white">${label}</text></svg>`;
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 ? [