perf(monitor): skip metadata-only map work
This commit is contained in:
@@ -248,6 +248,33 @@ test('skips unchanged refresh redraws and replaces only a moved vehicle label',
|
||||
expect((replacementLabels[0].options.text as { content: string }).content).toBe('粤A12345');
|
||||
});
|
||||
|
||||
test('does not traverse unchanged point arrays for an asOf-only refresh', async () => {
|
||||
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
||||
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
||||
let pointReads = 0;
|
||||
const observedPoints = new Proxy(pointMap.points, {
|
||||
get(target, property, receiver) {
|
||||
if (typeof property === 'string' && /^\d+$/.test(property)) pointReads += 1;
|
||||
return Reflect.get(target, property, receiver);
|
||||
}
|
||||
});
|
||||
const observedMap = { ...pointMap, points: observedPoints };
|
||||
const view = render(<FleetMap vehicles={[]} monitorMap={observedMap} onSelect={() => undefined} />);
|
||||
await waitFor(() => expect(addLabels).toHaveBeenCalled());
|
||||
pointReads = 0;
|
||||
|
||||
await act(async () => {
|
||||
view.rerender(<FleetMap
|
||||
vehicles={[]}
|
||||
monitorMap={{ ...observedMap, asOf: '2026-07-14T01:00:15Z' }}
|
||||
onSelect={() => undefined}
|
||||
/>);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(pointReads).toBe(0);
|
||||
});
|
||||
|
||||
test('converts AMap GCJ-02 bounds back to WGS-84 before requesting monitor data', async () => {
|
||||
const [west, south] = wgs84ToGcj02(113, 22);
|
||||
const [east, north] = wgs84ToGcj02(114, 24);
|
||||
|
||||
@@ -90,11 +90,18 @@ type LabelMarkerEntry = {
|
||||
signature: string;
|
||||
};
|
||||
|
||||
function massPointSignature(monitorMap: MonitorMapResponse | undefined, points: VehicleRealtimeRow[]) {
|
||||
if (monitorMap) return [
|
||||
`mode:${monitorMap.mode}`,
|
||||
...monitorMap.clusters.map((cluster) => `c:${cluster.id}:${cluster.longitude}:${cluster.latitude}:${cluster.count}`),
|
||||
...monitorMap.points.map((point) => `p:${point.vin}:${point.longitude}:${point.latitude}:${point.status}:${point.plate || ''}`)
|
||||
const NO_VEHICLE_POINTS: VehicleRealtimeRow[] = [];
|
||||
|
||||
function massPointSignature(
|
||||
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('|');
|
||||
}
|
||||
@@ -166,11 +173,15 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
const [followSelected, setFollowSelected] = useState(true);
|
||||
const [mapZoom, setMapZoom] = useState(5);
|
||||
const points = useMemo(() => vehicles.filter((vehicle) => isValidAMapCoordinate(vehicle.longitude, vehicle.latitude)), [vehicles]);
|
||||
const monitorMode = monitorMap?.mode;
|
||||
const monitorPoints = monitorMap?.points;
|
||||
const monitorClusters = monitorMap?.clusters;
|
||||
const fallbackPoints = monitorMode ? NO_VEHICLE_POINTS : points;
|
||||
const selectedTarget = useMemo(() => selectedVin
|
||||
? monitorMap?.points.find((item) => item.vin === selectedVin) ?? points.find((item) => item.vin === selectedVin)
|
||||
: undefined, [monitorMap, points, selectedVin]);
|
||||
const renderedPointCount = monitorMap ? monitorMap.points.length : points.length;
|
||||
const renderedClusterCount = monitorMap?.clusters.length ?? 0;
|
||||
? monitorPoints?.find((item) => item.vin === selectedVin) ?? points.find((item) => item.vin === selectedVin)
|
||||
: undefined, [monitorPoints, points, selectedVin]);
|
||||
const renderedPointCount = monitorPoints?.length ?? fallbackPoints.length;
|
||||
const renderedClusterCount = monitorClusters?.length ?? 0;
|
||||
const mapComposition = monitorMap && renderedClusterCount > 0
|
||||
? `${renderedClusterCount} 个聚合 · ${renderedPointCount} 个车辆点 · ${monitorMap.total} 辆`
|
||||
: `${renderedPointCount} 个有效点位`;
|
||||
@@ -309,8 +320,10 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
const mass = massRef.current;
|
||||
const AMap = amapRef.current;
|
||||
if (!mass || !AMap) return;
|
||||
clustersRef.current = new Map((monitorMap?.clusters ?? []).map((cluster) => [cluster.id, cluster]));
|
||||
const clusterCounts = [...new Set((monitorMap?.clusters ?? []).map((cluster) => cluster.count))].sort((a, b) => a - b);
|
||||
const clusters = monitorClusters ?? [];
|
||||
const mapPoints = monitorPoints ?? [];
|
||||
clustersRef.current = new Map(clusters.map((cluster) => [cluster.id, cluster]));
|
||||
const clusterCounts = [...new Set(clusters.map((cluster) => cluster.count))].sort((a, b) => a - b);
|
||||
const styleSignature = clusterCounts.join(',');
|
||||
if (massStyleSignatureRef.current !== styleSignature) {
|
||||
const baseStyles = COLORS.map((color) => ({ url: dotDataUrl(color), anchor: new AMap.Pixel(9, 9), size: new AMap.Size(18, 18) }));
|
||||
@@ -321,18 +334,18 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
mass.setStyle?.([...baseStyles, ...clusterStyles]);
|
||||
massStyleSignatureRef.current = styleSignature;
|
||||
}
|
||||
const dataSignature = massPointSignature(monitorMap, points);
|
||||
const dataSignature = massPointSignature(monitorMode, monitorClusters, monitorPoints, fallbackPoints);
|
||||
if (massDataSignatureRef.current === dataSignature) return;
|
||||
const clusterStyleIndexes = new Map(clusterCounts.map((count, index) => [count, COLORS.length + index]));
|
||||
const data: AMapMassPoint[] = monitorMap ? [
|
||||
...monitorMap.clusters.map((cluster) => ({ lnglat: wgs84ToGcj02(cluster.longitude, cluster.latitude), style: clusterStyleIndexes.get(cluster.count) ?? COLORS.length, id: cluster.id, label: `${cluster.count} 辆` })),
|
||||
...monitorMap.points.map((point) => ({ lnglat: wgs84ToGcj02(point.longitude, point.latitude), style: point.status === 'driving' ? 2 : point.status === 'idle' ? 0 : point.status === 'offline' ? 1 : 3, id: point.vin, label: point.plate || point.vin }))
|
||||
] : points.map((vehicle) => ({
|
||||
const data: AMapMassPoint[] = monitorMode ? [
|
||||
...clusters.map((cluster) => ({ lnglat: wgs84ToGcj02(cluster.longitude, cluster.latitude), style: clusterStyleIndexes.get(cluster.count) ?? COLORS.length, id: cluster.id, label: `${cluster.count} 辆` })),
|
||||
...mapPoints.map((point) => ({ lnglat: wgs84ToGcj02(point.longitude, point.latitude), style: point.status === 'driving' ? 2 : point.status === 'idle' ? 0 : point.status === 'offline' ? 1 : 3, id: point.vin, label: point.plate || point.vin }))
|
||||
] : fallbackPoints.map((vehicle) => ({
|
||||
lnglat: wgs84ToGcj02(vehicle.longitude, vehicle.latitude), style: styleIndex(vehicle), id: vehicle.vin, label: vehicle.plate || vehicle.vin
|
||||
}));
|
||||
mass.setData(data);
|
||||
massDataSignatureRef.current = dataSignature;
|
||||
}, [monitorMap, points, state]);
|
||||
}, [fallbackPoints, monitorClusters, monitorMode, monitorPoints, state]);
|
||||
|
||||
useEffect(() => {
|
||||
const labels = labelsRef.current;
|
||||
@@ -340,9 +353,9 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
const AMap = amapRef.current;
|
||||
const map = mapRef.current;
|
||||
if (!labels || !denseLabels || !AMap?.LabelMarker || !map) return;
|
||||
const mapLabelPoints = (monitorMap
|
||||
? monitorMap.points
|
||||
: points.map((vehicle) => ({ ...vehicle, status: vehicleStatus(vehicle) })));
|
||||
const mapLabelPoints = (monitorPoints
|
||||
? monitorPoints
|
||||
: fallbackPoints.map((vehicle) => ({ ...vehicle, status: vehicleStatus(vehicle) })));
|
||||
const allLabelPoints = selectedTarget && !mapLabelPoints.some((point) => point.vin === selectedTarget.vin)
|
||||
? [...mapLabelPoints, selectedTarget]
|
||||
: mapLabelPoints;
|
||||
@@ -407,7 +420,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
if (markersToRemove.length) activeLabels.remove(markersToRemove);
|
||||
if (markersToAdd.length) activeLabels.add(markersToAdd);
|
||||
labelMarkersRef.current = nextMarkers;
|
||||
}, [mapZoom, monitorMap, points, selectedTarget, selectedVin, showLabels, state]);
|
||||
}, [fallbackPoints, mapZoom, monitorPoints, selectedTarget, selectedVin, showLabels, state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedVin || !mapRef.current || !amapRef.current) {
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
This document records verified risks, the production controls that address them, and the next evidence to collect. It is intentionally operational: a passing build alone is not proof that the browser application is production-ready.
|
||||
|
||||
## 2026-07-16: render-scoped fleet refresh
|
||||
|
||||
The incremental map update already avoided AMap mutations when a 15-second response changed only its `asOf` timestamp, but the effects still depended on the complete response object. Each metadata-only refresh therefore traversed all visible points to rebuild signatures, cluster counts and label diffs before discovering that there was nothing to draw. The cost was hidden from mutation-count tests and could still create a periodic main-thread spike in a dense viewport.
|
||||
|
||||
FleetMap now binds rendering work to the response fields that can change pixels: `mode`, `points` and `clusters`. TanStack Query's JSON structural sharing preserves those nested references when their content is unchanged, and React compares effect dependencies with `Object.is`; an `asOf`-only root-object replacement therefore no longer enters either the MassMarks or LabelsLayer pipeline. Fallback vehicle points are also detached while a monitor-map payload is active, so unrelated list-prop identity changes cannot wake the map renderer. Real point-array changes still run the existing incremental marker replacement path.
|
||||
|
||||
This follows React's guidance to remove unnecessary object dependencies and TanStack Query's structural-sharing contract:
|
||||
|
||||
- <https://react.dev/reference/react/useEffect>
|
||||
- <https://react.dev/learn/removing-effect-dependencies>
|
||||
- <https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults>
|
||||
|
||||
A proxy-backed regression test counts numeric array-index reads and proves that a metadata-only refresh performs zero point traversal. The existing movement regression still proves that replacing one vehicle point refreshes MassMarks once and replaces exactly one plate label.
|
||||
|
||||
## 2026-07-16: search-scope route error recovery
|
||||
|
||||
The route boundary correctly isolated render failures, but its error state outlived search-parameter navigation inside the same pathname. If one alert, history or mileage scope triggered a render exception, correcting the filter or following a same-module link changed the URL while leaving the previous fallback permanently mounted. The operator had to reload the entire application or leave the module even when the next scope was valid.
|
||||
|
||||
Reference in New Issue
Block a user