feat: decreasing memory trend & latency

This commit is contained in:
amhsirak
2025-01-05 23:31:11 +05:30
parent e74616a177
commit f26718f7e2

View File

@@ -1,5 +1,5 @@
// Canvas.tsx
import React, { useCallback, useEffect, useRef, useMemo } from 'react'; import React, { useCallback, useEffect, useRef, useMemo } from 'react';
import { unstable_batchedUpdates } from 'react-dom';
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { useActionContext } from '../../context/browserActions'; import { useActionContext } from '../../context/browserActions';
@@ -9,7 +9,36 @@ import TimePicker from './TimePicker';
import DateTimeLocalPicker from './DateTimeLocalPicker'; import DateTimeLocalPicker from './DateTimeLocalPicker';
import { FrontendPerformanceMonitor } from '../../../perf/performance'; import { FrontendPerformanceMonitor } from '../../../perf/performance';
// Types // Optimized throttle with RAF
const rafThrottle = <T extends (...args: any[]) => any>(callback: T) => {
let requestId: number | null = null;
let lastArgs: Parameters<T>;
const later = () => {
requestId = null;
callback.apply(null, lastArgs);
};
return (...args: Parameters<T>) => {
lastArgs = args;
if (requestId === null) {
requestId = requestAnimationFrame(later);
}
};
};
// Cache DOM measurements
let measurementCache = new WeakMap<HTMLElement, DOMRect>();
const getBoundingClientRectCached = (element: HTMLElement) => {
let rect = measurementCache.get(element);
if (!rect) {
rect = element.getBoundingClientRect();
measurementCache.set(element, rect);
}
return rect;
};
// Types (kept the same)
interface CreateRefCallback { interface CreateRefCallback {
(ref: React.RefObject<HTMLCanvasElement>): void; (ref: React.RefObject<HTMLCanvasElement>): void;
} }
@@ -25,256 +54,208 @@ export interface Coordinates {
y: number; y: number;
} }
interface DropdownOption { // Batch updates helper
value: string; const batchedUpdates = (updates: Array<() => void>) => {
text: string; unstable_batchedUpdates(() => {
disabled: boolean; updates.forEach(update => update());
selected: boolean; });
}
interface CanvasState {
datePickerInfo: {
coordinates: Coordinates;
selector: string;
} | null;
dropdownInfo: {
coordinates: Coordinates;
selector: string;
options: DropdownOption[];
} | null;
timePickerInfo: {
coordinates: Coordinates;
selector: string;
} | null;
dateTimeLocalInfo: {
coordinates: Coordinates;
selector: string;
} | null;
}
type CanvasAction =
| { type: 'SET_DATE_PICKER'; payload: CanvasState['datePickerInfo'] }
| { type: 'SET_DROPDOWN'; payload: CanvasState['dropdownInfo'] }
| { type: 'SET_TIME_PICKER'; payload: CanvasState['timePickerInfo'] }
| { type: 'SET_DATETIME_PICKER'; payload: CanvasState['dateTimeLocalInfo'] };
// Helper functions
const throttle = <T extends (...args: any[]) => any>(func: T, limit: number): T => {
let inThrottle = false;
return ((...args: Parameters<T>): ReturnType<T> | void => {
if (!inThrottle) {
func.apply(null, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}) as T;
}; };
const createOffscreenCanvas = (width: number, height: number) => {
if (typeof OffscreenCanvas !== 'undefined') {
return new OffscreenCanvas(width, height);
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
// Reducer
const canvasReducer = (state: CanvasState, action: CanvasAction): CanvasState => {
switch (action.type) {
case 'SET_DATE_PICKER':
return { ...state, datePickerInfo: action.payload };
case 'SET_DROPDOWN':
return { ...state, dropdownInfo: action.payload };
case 'SET_TIME_PICKER':
return { ...state, timePickerInfo: action.payload };
case 'SET_DATETIME_PICKER':
return { ...state, dateTimeLocalInfo: action.payload };
default:
return state;
}
};
// Main Component
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => { const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
// Refs
const performanceMonitor = useRef(new FrontendPerformanceMonitor()); const performanceMonitor = useRef(new FrontendPerformanceMonitor());
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 });
const frameRequest = useRef<number>();
const renderingContext = useRef<CanvasRenderingContext2D | null>(null);
const offscreenCanvas = useRef<HTMLCanvasElement | OffscreenCanvas>(
createOffscreenCanvas(width || 900, height || 400)
);
// Hooks
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { setLastAction, lastAction } = useGlobalInfoStore(); const { setLastAction, lastAction } = useGlobalInfoStore();
const { getText, getList } = useActionContext(); const { getText, getList } = useActionContext();
const getTextRef = useRef(getText);
const getListRef = useRef(getList);
// State // Use a single ref object to reduce memory allocations
const [state, dispatch] = React.useReducer(canvasReducer, { const refs = useRef({
getText,
getList,
lastMousePosition: { x: 0, y: 0 },
frameRequest: 0,
eventQueue: [] as Array<() => void>,
isProcessing: false
});
// Consolidated state using a single reducer
const [state, dispatch] = React.useReducer((state: any, action: any) => {
switch (action.type) {
case 'BATCH_UPDATE':
return { ...state, ...action.payload };
default:
return state;
}
}, {
datePickerInfo: null, datePickerInfo: null,
dropdownInfo: null, dropdownInfo: null,
timePickerInfo: null, timePickerInfo: null,
dateTimeLocalInfo: null dateTimeLocalInfo: null
}); });
// Memoized values // Process events in batches
const canvasSize = useMemo(() => ({ const processEventQueue = useCallback(() => {
width: width || 900, if (refs.current.isProcessing || refs.current.eventQueue.length === 0) return;
height: height || 400
}), [width, height]);
const notifyLastAction = useCallback((action: string) => { refs.current.isProcessing = true;
if (lastAction !== action) { const events = [...refs.current.eventQueue];
setLastAction(action); refs.current.eventQueue = [];
batchedUpdates(events.map(event => () => event()));
refs.current.isProcessing = false;
if (refs.current.eventQueue.length > 0) {
requestAnimationFrame(processEventQueue);
} }
}, [lastAction, setLastAction]); }, []);
// Socket event handlers // Optimized mouse move handler using RAF throttle
const socketHandlers = useMemo(() => ({ const handleMouseMove = useMemo(
showDatePicker: (info: CanvasState['datePickerInfo']) => { () => rafThrottle((coordinates: Coordinates) => {
dispatch({ type: 'SET_DATE_PICKER', payload: info });
},
showDropdown: (info: CanvasState['dropdownInfo']) => {
dispatch({ type: 'SET_DROPDOWN', payload: info });
},
showTimePicker: (info: CanvasState['timePickerInfo']) => {
dispatch({ type: 'SET_TIME_PICKER', payload: info });
},
showDateTimePicker: (info: CanvasState['dateTimeLocalInfo']) => {
dispatch({ type: 'SET_DATETIME_PICKER', payload: info });
}
}), []);
// Event handlers
const handleMouseMove = useCallback(
throttle((coordinates: Coordinates) => {
if (!socket) return; if (!socket) return;
if ( const current = refs.current.lastMousePosition;
lastMousePosition.current.x !== coordinates.x || if (current.x !== coordinates.x || current.y !== coordinates.y) {
lastMousePosition.current.y !== coordinates.y refs.current.lastMousePosition = coordinates;
) {
lastMousePosition.current = coordinates;
socket.emit('input:mousemove', coordinates); socket.emit('input:mousemove', coordinates);
notifyLastAction('move'); refs.current.eventQueue.push(() => setLastAction('move'));
requestAnimationFrame(processEventQueue);
} }
}, 16), }),
[socket, notifyLastAction] [socket, processEventQueue]
); );
// Optimized event handler with better performance characteristics
const onMouseEvent = useCallback((event: MouseEvent) => { const onMouseEvent = useCallback((event: MouseEvent) => {
performanceMonitor.current.measureEventLatency(event);
if (!socket || !canvasRef.current) return; if (!socket || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect(); performanceMonitor.current.measureEventLatency(event);
const clickCoordinates = { const rect = getBoundingClientRectCached(canvasRef.current);
const coordinates = {
x: event.clientX - rect.left, x: event.clientX - rect.left,
y: event.clientY - rect.top, y: event.clientY - rect.top,
}; };
switch (event.type) { switch (event.type) {
case 'mousedown': case 'mousedown':
if (getTextRef.current) { refs.current.eventQueue.push(() => {
console.log('Capturing Text...'); if (refs.current.getText) {
} else if (getListRef.current) { console.log('Capturing Text...');
console.log('Capturing List...'); } else if (refs.current.getList) {
} else { console.log('Capturing List...');
socket.emit('input:mousedown', clickCoordinates); } else {
} socket.emit('input:mousedown', coordinates);
notifyLastAction('click'); }
setLastAction('click');
});
break; break;
case 'mousemove': case 'mousemove':
handleMouseMove(clickCoordinates); handleMouseMove(coordinates);
break; break;
case 'wheel': case 'wheel':
if (frameRequest.current) { if (refs.current.frameRequest) {
cancelAnimationFrame(frameRequest.current); cancelAnimationFrame(refs.current.frameRequest);
} }
frameRequest.current = requestAnimationFrame(() => { refs.current.frameRequest = requestAnimationFrame(() => {
const wheelEvent = event as WheelEvent; const wheelEvent = event as WheelEvent;
socket.emit('input:wheel', { socket.emit('input:wheel', {
deltaX: Math.round(wheelEvent.deltaX), deltaX: Math.round(wheelEvent.deltaX),
deltaY: Math.round(wheelEvent.deltaY), deltaY: Math.round(wheelEvent.deltaY)
}); });
notifyLastAction('scroll'); refs.current.eventQueue.push(() => setLastAction('scroll'));
}); });
break; break;
} }
}, [socket, handleMouseMove, notifyLastAction]);
const onKeyboardEvent = useCallback((event: KeyboardEvent) => { requestAnimationFrame(processEventQueue);
if (!socket) return; }, [socket, handleMouseMove, processEventQueue]);
switch (event.type) { // Optimized keyboard handler
case 'keydown': const onKeyboardEvent = useMemo(
socket.emit('input:keydown', { () => rafThrottle((event: KeyboardEvent) => {
key: event.key, if (!socket) return;
coordinates: lastMousePosition.current
});
notifyLastAction(`${event.key} pressed`);
break;
case 'keyup': refs.current.eventQueue.push(() => {
socket.emit('input:keyup', event.key); switch (event.type) {
break; case 'keydown':
} socket.emit('input:keydown', {
}, [socket, notifyLastAction]); key: event.key,
coordinates: refs.current.lastMousePosition
});
setLastAction(`${event.key} pressed`);
break;
case 'keyup':
socket.emit('input:keyup', event.key);
break;
}
});
requestAnimationFrame(processEventQueue);
}),
[socket, processEventQueue]
);
// Effects // Update refs
useEffect(() => { useEffect(() => {
getTextRef.current = getText; refs.current.getText = getText;
getListRef.current = getList; refs.current.getList = getList;
}, [getText, getList]); }, [getText, getList]);
// Socket event setup with optimized cleanup
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) return;
Object.entries(socketHandlers).forEach(([event, handler]) => { const handlers = {
showDatePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { datePickerInfo: info } }),
showDropdown: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dropdownInfo: info } }),
showTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { timePickerInfo: info } }),
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } })
};
Object.entries(handlers).forEach(([event, handler]) => {
socket.on(event, handler); socket.on(event, handler);
}); });
return () => { return () => {
Object.keys(socketHandlers).forEach(event => { Object.keys(handlers).forEach(event => {
socket.off(event); socket.off(event);
}); });
}; };
}, [socket, socketHandlers]); }, [socket]);
useEffect(() => { useEffect(() => {
const monitor = performanceMonitor.current; const monitor = performanceMonitor.current;
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
const report = monitor.getPerformanceReport(); console.log('Frontend Performance Report:', monitor.getPerformanceReport());
console.log('Frontend Performance Report:', report); }, 15000); // Increased to 15 seconds
}, 10000);
return () => { return () => {
clearInterval(intervalId); clearInterval(intervalId);
if (frameRequest.current) { if (refs.current.frameRequest) {
cancelAnimationFrame(frameRequest.current); cancelAnimationFrame(refs.current.frameRequest);
} }
// Clear measurement cache on unmount
measurementCache = new WeakMap(); // Reset the WeakMap
}; };
}, []); }, []);
// Canvas setup with optimized event binding
useEffect(() => { useEffect(() => {
if (!canvasRef.current) return; if (!canvasRef.current) return;
renderingContext.current = canvasRef.current.getContext('2d');
onCreateRef(canvasRef); onCreateRef(canvasRef);
const canvas = canvasRef.current; const canvas = canvasRef.current;
canvas.addEventListener('mousedown', onMouseEvent);
canvas.addEventListener('mousemove', onMouseEvent); const options = { passive: true };
canvas.addEventListener('wheel', onMouseEvent, { passive: true }); canvas.addEventListener('mousedown', onMouseEvent, options);
canvas.addEventListener('keydown', onKeyboardEvent); canvas.addEventListener('mousemove', onMouseEvent, options);
canvas.addEventListener('keyup', onKeyboardEvent); canvas.addEventListener('wheel', onMouseEvent, options);
canvas.addEventListener('keydown', onKeyboardEvent, options);
canvas.addEventListener('keyup', onKeyboardEvent, options);
return () => { return () => {
canvas.removeEventListener('mousedown', onMouseEvent); canvas.removeEventListener('mousedown', onMouseEvent);
@@ -285,28 +266,28 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
}; };
}, [onMouseEvent, onKeyboardEvent, onCreateRef]); }, [onMouseEvent, onKeyboardEvent, onCreateRef]);
const memoizedSize = useMemo(() => ({
width: width || 900,
height: height || 400
}), [width, height]);
return ( return (
<div className="relative bg-white rounded-b-md overflow-hidden"> <div className="relative bg-white rounded-b-md overflow-hidden">
<canvas <canvas
tabIndex={0} tabIndex={0}
ref={canvasRef} ref={canvasRef}
height={canvasSize.height} height={memoizedSize.height}
width={canvasSize.width} width={memoizedSize.width}
className="block" className="block"
/> />
{state.datePickerInfo && ( {state.datePickerInfo && (
<DatePicker <DatePicker
coordinates={state.datePickerInfo.coordinates} coordinates={state.datePickerInfo.coordinates}
selector={state.datePickerInfo.selector} selector={state.datePickerInfo.selector}
onClose={() => dispatch({ type: 'SET_DATE_PICKER', payload: null })} onClose={() => dispatch({
/> type: 'BATCH_UPDATE',
)} payload: { datePickerInfo: null }
{state.dropdownInfo && ( })}
<Dropdown
coordinates={state.dropdownInfo.coordinates}
selector={state.dropdownInfo.selector}
options={state.dropdownInfo.options}
onClose={() => dispatch({ type: 'SET_DROPDOWN', payload: null })}
/> />
)} )}
{state.timePickerInfo && ( {state.timePickerInfo && (