/** * Memory monitoring hook for performance measurement * * Tracks JavaScript heap memory usage using the performance.memory API * where available (Chrome/Android). Falls back gracefully when unavailable. * * @example * ```tsx * const { metrics, start, stop, reset, getSnapshot } = useMemoryMonitor(); * * // Start monitoring * start(); * * // Later, get current metrics * const snapshot = getSnapshot(); * console.log(`Peak heap: ${snapshot.peakHeapMB}MB, Delta: ${snapshot.heapDeltaMB}MB`); * * // Stop and reset * stop(); * reset(); * ``` */ import { useCallback, useRef, useEffect } from 'react'; import type { MemoryMetrics, MemoryMonitorConfig } from '../types'; import { DEFAULT_MEMORY_METRICS, DEFAULT_MEMORY_MONITOR_CONFIG } from '../types'; /** * Extended Performance interface with memory property * Available in Chrome and Chromium-based browsers (including Android WebView/Hermes debug) */ interface PerformanceWithMemory extends Performance { memory?: { usedJSHeapSize: number; totalJSHeapSize: number; jsHeapSizeLimit: number; }; } /** * Convert bytes to megabytes with 2 decimal places */ function bytesToMB(bytes: number): number { return Math.round((bytes / 1024 / 1024) * 100) / 100; } /** * Check if memory API is available */ function isMemoryAvailable(): boolean { if (typeof performance === 'undefined') return false; const perf = performance as PerformanceWithMemory; return perf.memory !== undefined; } /** * Get current heap size in bytes, or 0 if unavailable */ function getCurrentHeapBytes(): number { if (!isMemoryAvailable()) return 0; const perf = performance as PerformanceWithMemory; return perf.memory?.usedJSHeapSize ?? 0; } export interface UseMemoryMonitorResult { /** Current memory metrics */ metrics: MemoryMetrics; /** Start memory monitoring */ start: () => void; /** Stop memory monitoring */ stop: () => void; /** Reset all metrics */ reset: () => void; /** Get a snapshot of current metrics */ getSnapshot: () => MemoryMetrics; /** Whether memory API is available */ isAvailable: boolean; } /** * Hook for monitoring JavaScript heap memory usage * * Uses the performance.memory API to sample memory at configurable intervals. * Falls back gracefully when the API is unavailable. * * @param config - Optional configuration for sampling interval */ export function useMemoryMonitor( config: Partial = {}, ): UseMemoryMonitorResult { const { sampleIntervalMs } = { ...DEFAULT_MEMORY_MONITOR_CONFIG, ...config, }; // Check if memory API is available const memoryAvailable = isMemoryAvailable(); // Use ref for metrics to avoid re-renders const metricsRef = useRef({ ...DEFAULT_MEMORY_METRICS, isAvailable: memoryAvailable, }); // Refs for monitoring state const isActiveRef = useRef(false); const intervalIdRef = useRef | null>(null); // Sample memory and update metrics const sampleMemory = useCallback(() => { if (!memoryAvailable) return; const currentHeapBytes = getCurrentHeapBytes(); const currentHeapMB = bytesToMB(currentHeapBytes); const current = metricsRef.current; // Update metrics current.currentHeapMB = currentHeapMB; current.peakHeapMB = Math.max(current.peakHeapMB, currentHeapMB); current.heapDeltaMB = Math.round((currentHeapMB - current.initialHeapMB) * 100) / 100; current.sampleCount += 1; current.lastSampleTimestamp = Date.now(); }, [memoryAvailable]); // Start monitoring const start = useCallback(() => { if (isActiveRef.current) return; isActiveRef.current = true; // Capture initial heap size if (memoryAvailable) { const initialHeapMB = bytesToMB(getCurrentHeapBytes()); metricsRef.current.initialHeapMB = initialHeapMB; metricsRef.current.currentHeapMB = initialHeapMB; metricsRef.current.peakHeapMB = initialHeapMB; metricsRef.current.heapDeltaMB = 0; metricsRef.current.sampleCount = 1; metricsRef.current.lastSampleTimestamp = Date.now(); } // Start sampling interval intervalIdRef.current = setInterval(sampleMemory, sampleIntervalMs); }, [memoryAvailable, sampleIntervalMs, sampleMemory]); // Stop monitoring const stop = useCallback(() => { if (!isActiveRef.current) return; isActiveRef.current = false; if (intervalIdRef.current !== null) { clearInterval(intervalIdRef.current); intervalIdRef.current = null; } }, []); // Reset metrics const reset = useCallback(() => { metricsRef.current = { ...DEFAULT_MEMORY_METRICS, isAvailable: memoryAvailable, }; }, [memoryAvailable]); // Get snapshot const getSnapshot = useCallback((): MemoryMetrics => { // Sample fresh data before returning snapshot if (memoryAvailable && isActiveRef.current) { sampleMemory(); } return { ...metricsRef.current }; }, [memoryAvailable, sampleMemory]); // Cleanup on unmount useEffect(() => { return () => { if (intervalIdRef.current !== null) { clearInterval(intervalIdRef.current); } }; }, []); return { metrics: metricsRef.current, start, stop, reset, getSnapshot, isAvailable: memoryAvailable, }; }