perf(monitor): bound map refresh fingerprints
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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 ? [
|
||||
|
||||
Reference in New Issue
Block a user