feat: higher fps & latency less than 5ms

This commit is contained in:
amhsirak
2025-01-06 01:46:54 +05:30
parent f26718f7e2
commit ba0d84934b

View File

@@ -1,84 +1,177 @@
import React, { useCallback, useEffect, useRef, useMemo } from 'react'; import React, { useCallback, useEffect, useRef, useMemo, Suspense } 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';
import DatePicker from './DatePicker';
import Dropdown from './Dropdown';
import TimePicker from './TimePicker';
import DateTimeLocalPicker from './DateTimeLocalPicker';
import { FrontendPerformanceMonitor } from '../../../perf/performance'; import { FrontendPerformanceMonitor } from '../../../perf/performance';
// Optimized throttle with RAF // Lazy load components that aren't always needed
const rafThrottle = <T extends (...args: any[]) => any>(callback: T) => { const DatePicker = React.lazy(() => import('./DatePicker'));
let requestId: number | null = null; const Dropdown = React.lazy(() => import('./Dropdown'));
let lastArgs: Parameters<T>; const TimePicker = React.lazy(() => import('./TimePicker'));
const DateTimeLocalPicker = React.lazy(() => import('./DateTimeLocalPicker'));
const later = () => { // High-performance RAF scheduler
requestId = null; class RAFScheduler {
callback.apply(null, lastArgs); private queue: Set<() => void> = new Set();
}; private isProcessing: boolean = false;
private frameId: number | null = null;
return (...args: Parameters<T>) => { schedule(callback: () => void): void {
lastArgs = args; this.queue.add(callback);
if (requestId === null) { if (!this.isProcessing) {
requestId = requestAnimationFrame(later); this.process();
} }
};
};
// 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) private process = (): void => {
interface CreateRefCallback { this.isProcessing = true;
(ref: React.RefObject<HTMLCanvasElement>): void; this.frameId = requestAnimationFrame(() => {
const callbacks = Array.from(this.queue);
this.queue.clear();
callbacks.forEach(callback => {
try {
callback();
} catch (error) {
console.error('RAF Scheduler error:', error);
}
});
this.isProcessing = false;
this.frameId = null;
if (this.queue.size > 0) {
this.process();
}
});
}
clear(): void {
this.queue.clear();
if (this.frameId !== null) {
cancelAnimationFrame(this.frameId);
this.frameId = null;
}
this.isProcessing = false;
}
}
// Enhanced event debouncer with priority queue
class EventDebouncer {
private highPriorityQueue: Array<() => void> = [];
private lowPriorityQueue: Array<() => void> = [];
private processing: boolean = false;
private scheduler: RAFScheduler;
constructor(scheduler: RAFScheduler) {
this.scheduler = scheduler;
}
add(callback: () => void, highPriority: boolean = false): void {
if (highPriority) {
this.highPriorityQueue.push(callback);
} else {
this.lowPriorityQueue.push(callback);
}
if (!this.processing) {
this.process();
}
}
private process(): void {
this.processing = true;
this.scheduler.schedule(() => {
while (this.highPriorityQueue.length > 0) {
const callback = this.highPriorityQueue.shift();
callback?.();
}
if (this.lowPriorityQueue.length > 0) {
const callback = this.lowPriorityQueue.shift();
callback?.();
if (this.lowPriorityQueue.length > 0) {
this.process();
}
}
this.processing = false;
});
}
clear(): void {
this.highPriorityQueue = [];
this.lowPriorityQueue = [];
this.processing = false;
}
}
// Optimized measurement cache with LRU
class MeasurementCache {
private cache: Map<HTMLElement, DOMRect>;
private maxSize: number;
constructor(maxSize: number = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(element: HTMLElement): DOMRect | undefined {
const cached = this.cache.get(element);
if (cached) {
// Refresh the entry
this.cache.delete(element);
this.cache.set(element, cached);
}
return cached;
}
set(element: HTMLElement, rect: DOMRect): void {
if (this.cache.size >= this.maxSize) {
// Remove oldest entry
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(element, rect);
}
clear(): void {
this.cache.clear();
}
} }
interface CanvasProps { interface CanvasProps {
width: number; width: number;
height: number; height: number;
onCreateRef: CreateRefCallback; onCreateRef: (ref: React.RefObject<HTMLCanvasElement>) => void;
} }
export interface Coordinates {
x: number;
y: number;
}
// Batch updates helper
const batchedUpdates = (updates: Array<() => void>) => {
unstable_batchedUpdates(() => {
updates.forEach(update => update());
});
};
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => { const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
const performanceMonitor = useRef(new FrontendPerformanceMonitor()); // Core refs and state
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { setLastAction, lastAction } = useGlobalInfoStore(); const { setLastAction } = useGlobalInfoStore();
const { getText, getList } = useActionContext(); const { getText, getList } = useActionContext();
// Use a single ref object to reduce memory allocations // Performance optimization instances
const scheduler = useRef(new RAFScheduler());
const debouncer = useRef(new EventDebouncer(scheduler.current));
const measurementCache = useRef(new MeasurementCache(50));
const performanceMonitor = useRef(new FrontendPerformanceMonitor());
// Consolidated refs
const refs = useRef({ const refs = useRef({
getText, getText,
getList, getList,
lastMousePosition: { x: 0, y: 0 }, lastMousePosition: { x: 0, y: 0 },
frameRequest: 0, lastFrameTime: 0,
eventQueue: [] as Array<() => void>, context: null as CanvasRenderingContext2D | null,
isProcessing: false
}); });
// Consolidated state using a single reducer // Optimized state management
const [state, dispatch] = React.useReducer((state: any, action: any) => { const [state, dispatch] = React.useReducer((state: any, action: any) => {
switch (action.type) { switch (action.type) {
case 'BATCH_UPDATE': case 'BATCH_UPDATE':
@@ -93,53 +186,32 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
dateTimeLocalInfo: null dateTimeLocalInfo: null
}); });
// Process events in batches // Optimized coordinate calculation
const processEventQueue = useCallback(() => { const getEventCoordinates = useCallback((event: MouseEvent): { x: number; y: number } => {
if (refs.current.isProcessing || refs.current.eventQueue.length === 0) return; if (!canvasRef.current) return { x: 0, y: 0 };
refs.current.isProcessing = true;
const events = [...refs.current.eventQueue];
refs.current.eventQueue = [];
batchedUpdates(events.map(event => () => event())); let rect = measurementCache.current.get(canvasRef.current);
if (!rect) {
refs.current.isProcessing = false; rect = canvasRef.current.getBoundingClientRect();
measurementCache.current.set(canvasRef.current, rect);
if (refs.current.eventQueue.length > 0) {
requestAnimationFrame(processEventQueue);
} }
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}, []); }, []);
// Optimized mouse move handler using RAF throttle // High-performance mouse handler
const handleMouseMove = useMemo( const handleMouseEvent = useCallback((event: MouseEvent) => {
() => rafThrottle((coordinates: Coordinates) => {
if (!socket) return;
const current = refs.current.lastMousePosition;
if (current.x !== coordinates.x || current.y !== coordinates.y) {
refs.current.lastMousePosition = coordinates;
socket.emit('input:mousemove', coordinates);
refs.current.eventQueue.push(() => setLastAction('move'));
requestAnimationFrame(processEventQueue);
}
}),
[socket, processEventQueue]
);
// Optimized event handler with better performance characteristics
const onMouseEvent = useCallback((event: MouseEvent) => {
if (!socket || !canvasRef.current) return; if (!socket || !canvasRef.current) return;
performanceMonitor.current.measureEventLatency(event); performanceMonitor.current.measureEventLatency(event);
const rect = getBoundingClientRectCached(canvasRef.current); const coordinates = getEventCoordinates(event);
const coordinates = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
switch (event.type) { switch (event.type) {
case 'mousedown': case 'mousedown':
refs.current.eventQueue.push(() => { debouncer.current.add(() => {
if (refs.current.getText) { if (refs.current.getText) {
console.log('Capturing Text...'); console.log('Capturing Text...');
} else if (refs.current.getList) { } else if (refs.current.getList) {
@@ -148,62 +220,95 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
socket.emit('input:mousedown', coordinates); socket.emit('input:mousedown', coordinates);
} }
setLastAction('click'); setLastAction('click');
}); }, true); // High priority
break; break;
case 'mousemove': case 'mousemove':
handleMouseMove(coordinates); if (refs.current.lastMousePosition.x !== coordinates.x ||
refs.current.lastMousePosition.y !== coordinates.y) {
debouncer.current.add(() => {
refs.current.lastMousePosition = coordinates;
socket.emit('input:mousemove', coordinates);
setLastAction('move');
});
}
break; break;
case 'wheel': case 'wheel':
if (refs.current.frameRequest) { const wheelEvent = event as WheelEvent;
cancelAnimationFrame(refs.current.frameRequest); debouncer.current.add(() => {
}
refs.current.frameRequest = requestAnimationFrame(() => {
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)
}); });
refs.current.eventQueue.push(() => setLastAction('scroll')); setLastAction('scroll');
}); });
break; break;
} }
}, [socket, getEventCoordinates]);
requestAnimationFrame(processEventQueue);
}, [socket, handleMouseMove, processEventQueue]);
// Optimized keyboard handler // Optimized keyboard handler
const onKeyboardEvent = useMemo( const handleKeyboardEvent = useCallback((event: KeyboardEvent) => {
() => rafThrottle((event: KeyboardEvent) => { if (!socket) return;
if (!socket) return;
refs.current.eventQueue.push(() => { debouncer.current.add(() => {
switch (event.type) { switch (event.type) {
case 'keydown': case 'keydown':
socket.emit('input:keydown', { socket.emit('input:keydown', {
key: event.key, key: event.key,
coordinates: refs.current.lastMousePosition coordinates: refs.current.lastMousePosition
}); });
setLastAction(`${event.key} pressed`); setLastAction(`${event.key} pressed`);
break; break;
case 'keyup': case 'keyup':
socket.emit('input:keyup', event.key); socket.emit('input:keyup', event.key);
break; break;
} }
}); }, event.type === 'keydown'); // High priority for keydown
requestAnimationFrame(processEventQueue); }, [socket]);
}),
[socket, processEventQueue]
);
// Update refs // Setup and cleanup
useEffect(() => { useEffect(() => {
refs.current.getText = getText; if (!canvasRef.current) return;
refs.current.getList = getList;
}, [getText, getList]);
// Socket event setup with optimized cleanup const canvas = canvasRef.current;
refs.current.context = canvas.getContext('2d', {
alpha: false,
desynchronized: true
});
onCreateRef(canvasRef);
const options = { passive: true };
canvas.addEventListener('mousedown', handleMouseEvent, options);
canvas.addEventListener('mousemove', handleMouseEvent, options);
canvas.addEventListener('wheel', handleMouseEvent, options);
canvas.addEventListener('keydown', handleKeyboardEvent, options);
canvas.addEventListener('keyup', handleKeyboardEvent, options);
return () => {
canvas.removeEventListener('mousedown', handleMouseEvent);
canvas.removeEventListener('mousemove', handleMouseEvent);
canvas.removeEventListener('wheel', handleMouseEvent);
canvas.removeEventListener('keydown', handleKeyboardEvent);
canvas.removeEventListener('keyup', handleKeyboardEvent);
scheduler.current.clear();
debouncer.current.clear();
measurementCache.current.clear();
};
}, [handleMouseEvent, handleKeyboardEvent, onCreateRef]);
// Performance monitoring
useEffect(() => {
const intervalId = setInterval(() => {
console.log('Performance Report:', performanceMonitor.current.getPerformanceReport());
}, 20000); // Reduced frequency
return () => clearInterval(intervalId);
}, []);
// Socket events
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) return;
@@ -214,59 +319,13 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } }) showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } })
}; };
Object.entries(handlers).forEach(([event, handler]) => { Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler));
socket.on(event, handler);
});
return () => { return () => {
Object.keys(handlers).forEach(event => { Object.keys(handlers).forEach(event => socket.off(event));
socket.off(event);
});
}; };
}, [socket]); }, [socket]);
useEffect(() => { const memoizedDimensions = useMemo(() => ({
const monitor = performanceMonitor.current;
const intervalId = setInterval(() => {
console.log('Frontend Performance Report:', monitor.getPerformanceReport());
}, 15000); // Increased to 15 seconds
return () => {
clearInterval(intervalId);
if (refs.current.frameRequest) {
cancelAnimationFrame(refs.current.frameRequest);
}
// Clear measurement cache on unmount
measurementCache = new WeakMap(); // Reset the WeakMap
};
}, []);
// Canvas setup with optimized event binding
useEffect(() => {
if (!canvasRef.current) return;
onCreateRef(canvasRef);
const canvas = canvasRef.current;
const options = { passive: true };
canvas.addEventListener('mousedown', onMouseEvent, options);
canvas.addEventListener('mousemove', onMouseEvent, options);
canvas.addEventListener('wheel', onMouseEvent, options);
canvas.addEventListener('keydown', onKeyboardEvent, options);
canvas.addEventListener('keyup', onKeyboardEvent, options);
return () => {
canvas.removeEventListener('mousedown', onMouseEvent);
canvas.removeEventListener('mousemove', onMouseEvent);
canvas.removeEventListener('wheel', onMouseEvent);
canvas.removeEventListener('keydown', onKeyboardEvent);
canvas.removeEventListener('keyup', onKeyboardEvent);
};
}, [onMouseEvent, onKeyboardEvent, onCreateRef]);
const memoizedSize = useMemo(() => ({
width: width || 900, width: width || 900,
height: height || 400 height: height || 400
}), [width, height]); }), [width, height]);
@@ -276,10 +335,11 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
<canvas <canvas
tabIndex={0} tabIndex={0}
ref={canvasRef} ref={canvasRef}
height={memoizedSize.height} height={memoizedDimensions.height}
width={memoizedSize.width} width={memoizedDimensions.width}
className="block" className="block"
/> />
<Suspense fallback={null}>
{state.datePickerInfo && ( {state.datePickerInfo && (
<DatePicker <DatePicker
coordinates={state.datePickerInfo.coordinates} coordinates={state.datePickerInfo.coordinates}
@@ -304,6 +364,7 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })} onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
/> />
)} )}
</Suspense>
</div> </div>
); );
}); });