perf(monitor): reuse point animation buffers

This commit is contained in:
lingniu
2026-07-16 10:27:36 +08:00
parent 59f220885b
commit 77f12be63e
2 changed files with 84 additions and 38 deletions

View File

@@ -22,7 +22,9 @@ const CLUSTER_VISUAL_CACHE_LIMIT = 256;
const DEFAULT_POINT_MOVE_DURATION_MS = 15_000;
const MIN_POINT_MOVE_DURATION_MS = 1_000;
const MAX_POINT_MOVE_DURATION_MS = 120_000;
const POINT_MOVE_FRAME_MS = 80;
const MIN_POINT_MOVE_FRAME_MS = 100;
const MAX_POINT_MOVE_FRAME_MS = 220;
const POINT_MOVE_EPSILON = 0.000003;
const MAP_INTERACTION_PAN_DURATION_MS = 650;
const clusterVisualCache = new Map<number, { diameter: number; url: string }>();
@@ -201,21 +203,35 @@ export function pointMoveDurationMs(reportIntervalMs?: number) {
return Math.min(MAX_POINT_MOVE_DURATION_MS, Math.max(MIN_POINT_MOVE_DURATION_MS, Math.round(Number(reportIntervalMs))));
}
export function interpolateMassPointsByElapsed(previous: AMapMassPoint[], target: AMapMassPoint[], elapsedMs: number, durationByID: Map<string, number>, eligibleIDs?: Set<string>) {
return interpolateMassPointsFrom(
new Map(previous.map((point) => [point.id, point])),
target,
(id) => elapsedMs / (durationByID.get(id) ?? DEFAULT_POINT_MOVE_DURATION_MS),
eligibleIDs
);
export type MutablePointMotion = {
point: AMapMassPoint;
start: [number, number];
target: [number, number];
durationMs: number;
};
export function advancePointMotions(motions: MutablePointMotion[], elapsedMs: number) {
for (const motion of motions) {
const progress = Math.min(1, Math.max(0, elapsedMs / motion.durationMs));
motion.point.lnglat[0] = motion.start[0] + (motion.target[0] - motion.start[0]) * progress;
motion.point.lnglat[1] = motion.start[1] + (motion.target[1] - motion.start[1]) * progress;
}
}
export function pointMoveFrameMs(totalPoints: number, movingPoints: number) {
if (totalPoints <= 2) return MIN_POINT_MOVE_FRAME_MS;
const pressure = Math.max(totalPoints / 800, movingPoints / 120);
return Math.round(Math.min(MAX_POINT_MOVE_FRAME_MS, MIN_POINT_MOVE_FRAME_MS + pressure * 120));
}
function movingPointIDs(previous: AMapMassPoint[], target: AMapMassPoint[]) {
const previousByID = new Map(previous.map((point) => [point.id, point.lnglat]));
return new Set(target.flatMap((point) => {
const moving = new Set<string>();
for (const point of target) {
const start = previousByID.get(point.id);
return start && (start[0] !== point.lnglat[0] || start[1] !== point.lnglat[1]) ? [point.id] : [];
}));
if (start && (Math.abs(start[0] - point.lnglat[0]) >= POINT_MOVE_EPSILON || Math.abs(start[1] - point.lnglat[1]) >= POINT_MOVE_EPSILON)) moving.add(point.id);
}
return moving;
}
function selectionStatus(target: PlateLabelPoint & { status?: string; online?: boolean; speedKmh?: number }) {
@@ -273,6 +289,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
const massStyleSignatureRef = useRef('__unset__');
const renderedMassDataRef = useRef<AMapMassPoint[]>([]);
const pointAnimationFrameRef = useRef<number | undefined>(undefined);
const pointAnimationTimerRef = useRef<number | undefined>(undefined);
const movingPointIDsRef = useRef(new Set<string>());
const onSelectRef = useRef(onSelect);
const onSelectVinRef = useRef(onSelectVin);
@@ -409,7 +426,9 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
window.clearTimeout(resizeTimer);
window.clearTimeout(viewportTimerRef.current);
if (pointAnimationFrameRef.current !== undefined) window.cancelAnimationFrame(pointAnimationFrameRef.current);
window.clearTimeout(pointAnimationTimerRef.current);
pointAnimationFrameRef.current = undefined;
pointAnimationTimerRef.current = undefined;
movingPointIDsRef.current.clear();
if (selectMassPoint) massInstance?.off?.('click', selectMassPoint);
if (notifyViewport) {
@@ -470,43 +489,58 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
.map((point) => [point.vin, pointMoveDurationMs(point.reportIntervalMs)]));
massDataSignatureRef.current = dataSignature;
if (pointAnimationFrameRef.current !== undefined) window.cancelAnimationFrame(pointAnimationFrameRef.current);
window.clearTimeout(pointAnimationTimerRef.current);
pointAnimationFrameRef.current = undefined;
pointAnimationTimerRef.current = undefined;
const previousData = renderedMassDataRef.current;
const previousDataByID = new Map(previousData.map((point) => [point.id, point]));
const vehiclePointIDs = new Set(monitorMode ? mapPoints.map((point) => point.vin) : fallbackPoints.map((point) => point.vin));
const movingIDs = new Set([...movingPointIDs(previousData, data)].filter((id) => vehiclePointIDs.has(id)));
movingPointIDsRef.current = movingIDs;
if (!movingIDs.size || previousData.length === 0 || typeof window.requestAnimationFrame !== 'function') {
if (!movingIDs.size || previousData.length === 0 || typeof window.requestAnimationFrame !== 'function' || document.visibilityState === 'hidden') {
mass.setData(data);
renderedMassDataRef.current = data;
movingPointIDsRef.current.clear();
return;
}
const dataByID = new Map(data.map((point) => [point.id, point]));
const motions: MutablePointMotion[] = [];
for (const id of movingIDs) {
const point = dataByID.get(id);
const previous = previousDataByID.get(id);
if (!point || !previous) continue;
const target: [number, number] = [point.lnglat[0], point.lnglat[1]];
point.lnglat = [previous.lnglat[0], previous.lnglat[1]];
motions.push({ point, start: [previous.lnglat[0], previous.lnglat[1]], target, durationMs: moveDurationByID.get(id) ?? DEFAULT_POINT_MOVE_DURATION_MS });
}
let startedAt: number | undefined;
let lastPaintAt = -Infinity;
const longestMoveDuration = Math.max(...[...movingIDs].map((id) => moveDurationByID.get(id) ?? DEFAULT_POINT_MOVE_DURATION_MS));
const paint = (frameData: AMapMassPoint[]) => {
mass.setData(frameData);
renderedMassDataRef.current = frameData;
for (const point of frameData) {
if (!movingIDs.has(point.id)) continue;
const label = labelMarkersRef.current.get(point.id);
label?.marker.setPosition?.(point.lnglat);
if (label) label.position = point.lnglat;
if (selectedVinRef.current === point.id) selectionRef.current?.setPosition?.(point.lnglat);
const frameMs = pointMoveFrameMs(data.length, motions.length);
const paint = (elapsed: number) => {
advancePointMotions(motions, elapsed);
mass.setData(data);
renderedMassDataRef.current = data;
for (const motion of motions) {
const label = labelMarkersRef.current.get(motion.point.id);
label?.marker.setPosition?.(motion.point.lnglat);
if (label) label.position = motion.point.lnglat;
if (selectedVinRef.current === motion.point.id) selectionRef.current?.setPosition?.(motion.point.lnglat);
}
};
const schedule = () => {
pointAnimationTimerRef.current = window.setTimeout(() => {
pointAnimationTimerRef.current = undefined;
pointAnimationFrameRef.current = window.requestAnimationFrame(animate);
}, frameMs);
};
const animate = (timestamp: number) => {
pointAnimationFrameRef.current = undefined;
startedAt ??= timestamp;
const elapsed = timestamp - startedAt;
const complete = elapsed >= longestMoveDuration;
if (timestamp - lastPaintAt >= POINT_MOVE_FRAME_MS || complete) {
paint(interpolateMassPointsFrom(previousDataByID, data, (id) => elapsed / (moveDurationByID.get(id) ?? DEFAULT_POINT_MOVE_DURATION_MS), movingIDs));
lastPaintAt = timestamp;
}
if (!complete) pointAnimationFrameRef.current = window.requestAnimationFrame(animate);
const complete = elapsed >= longestMoveDuration || document.visibilityState === 'hidden';
paint(complete ? longestMoveDuration : elapsed);
if (!complete) schedule();
else {
pointAnimationFrameRef.current = undefined;
movingPointIDsRef.current.clear();
}
};