diff --git a/com.twin.app.shoptime/src/utils/memoryMonitor.js b/com.twin.app.shoptime/src/utils/memoryMonitor.js index eebfc3b8..faf529c7 100644 --- a/com.twin.app.shoptime/src/utils/memoryMonitor.js +++ b/com.twin.app.shoptime/src/utils/memoryMonitor.js @@ -33,6 +33,253 @@ export const createMemoryMonitor = (enableInitLog = true) => { return null; }; + // 미디어 리소스 메모리 정보 수집 + const getMediaMemoryInfo = () => { + try { + const mediaElements = document.querySelectorAll('video, audio'); + let totalVideoBuffer = 0; + let totalAudioBuffer = 0; + let videoCount = 0; + let audioCount = 0; + const mediaInfo = []; + + // NodeList를 배열로 변환하여 forEach 사용 + Array.from(mediaElements).forEach((media, index) => { + try { + const buffered = media.buffered; + let totalDuration = 0; + + if (buffered && buffered.length) { + for (let i = 0; i < buffered.length; i++) { + try { + totalDuration += buffered.end(i) - buffered.start(i); + } catch (e) { + // buffered 접근 중 오류 발생 시 무시 + } + } + } + + if (media.tagName === 'VIDEO') { + videoCount++; + } else if (media.tagName === 'AUDIO') { + audioCount++; + } + + // 비디오 메타데이터 수집 + let videoBitrate = 0; + let codecInfo = 'unknown'; + + if (media.tagName === 'VIDEO' && media.videoWidth && media.videoHeight) { + // 해상도 기반 비트레이트 추정 (HLS 스트리밍 기준) + const resolution = media.videoWidth * media.videoHeight; + if (resolution >= 3840 * 2160) { // 4K + videoBitrate = 15000000; // 15Mbps + codecInfo = '4K/HLS'; + } else if (resolution >= 1920 * 1080) { // FHD + videoBitrate = 8000000; // 8Mbps + codecInfo = 'FHD/HLS'; + } else if (resolution >= 1280 * 720) { // HD + videoBitrate = 4000000; // 4Mbps + codecInfo = 'HD/HLS'; + } else { // SD + videoBitrate = 2000000; // 2Mbps + codecInfo = 'SD/HLS'; + } + } + + // HLS 스트리밍 정보 확인 + let hlsInfo = null; + if (media.src && media.src.includes('.m3u8')) { + hlsInfo = { + isHLS: true, + playlistUrl: media.src.substring(0, 100) + '...', + estimatedSegments: Math.ceil((media.duration || 0) / 10), // 10초 세그먼트 기준 + }; + } else if (media.src) { + hlsInfo = { + isHLS: false, + contentType: 'progressive', + format: media.src.includes('.mp4') ? 'MP4' : 'Unknown', + }; + } + + const mediaData = { + index, + type: media.tagName ? media.tagName.toLowerCase() : 'unknown', + src: media.src ? (media.src.length > 50 ? media.src.substring(0, 50) + '...' : media.src) : 'N/A', + duration: media.duration || 0, + bufferedDuration: totalDuration, + currentTime: media.currentTime || 0, + readyState: media.readyState || 0, + networkState: media.networkState || 0, + videoWidth: media.videoWidth || 0, + videoHeight: media.videoHeight || 0, + // 비디오 전용 정보 + bitrate: videoBitrate, + codecInfo: codecInfo, + // HLS/스트리밍 정보 + hlsInfo: hlsInfo, + // 버퍼 효율성 + bufferEfficiency: media.duration > 0 ? (totalDuration / media.duration * 100).toFixed(1) + '%' : '0%', + // 재생 상태 + paused: media.paused, + ended: media.ended, + muted: media.muted, + volume: media.volume || 0, + }; + + mediaInfo.push(mediaData); + + // 실제 버퍼 메모리 계산 + if (media.tagName === 'VIDEO' && media.videoWidth && media.videoHeight) { + // 비디오: 실제 비트레이트 기반 계산 + totalVideoBuffer += totalDuration * (videoBitrate / 8); // bytes + } else if (media.tagName === 'AUDIO') { + // 오디오: 고품질 320kbps로 추정 + totalAudioBuffer += totalDuration * 320000 / 8; // bytes + } + } catch (e) { + // 개별 미디어 요소 처리 중 오류 발생 시 무시 + console.warn('[Memory Monitor] Error processing media element:', e); + } + }); + + return { + mediaCount: mediaElements.length, + videoElements: videoCount, + audioElements: audioCount, + totalVideoBufferMB: (totalVideoBuffer / 1048576).toFixed(2), + totalAudioBufferMB: (totalAudioBuffer / 1048576).toFixed(2), + estimatedMediaMemoryMB: ((totalVideoBuffer + totalAudioBuffer) / 1048576).toFixed(2), + mediaElements: mediaInfo + }; + } catch (e) { + console.warn('[Memory Monitor] Error getting media memory info:', e); + return { + mediaCount: 0, + videoElements: 0, + audioElements: 0, + totalVideoBufferMB: '0.00', + totalAudioBufferMB: '0.00', + estimatedMediaMemoryMB: '0.00', + mediaElements: [] + }; + } + }; + + // 이미지 리소스 메모리 정보 수집 + const getImageMemoryInfo = () => { + try { + const images = document.querySelectorAll('img'); + let totalImageMemory = 0; + const imageInfo = []; + + // NodeList를 배열로 변환하여 forEach 사용 + Array.from(images).forEach((img, index) => { + try { + if (img.naturalWidth && img.naturalHeight) { + // 이미지 메모리 크기 추정 (너비 * 높이 * 4바이트 RGBA) + const estimatedMemory = img.naturalWidth * img.naturalHeight * 4; + totalImageMemory += estimatedMemory; + + imageInfo.push({ + index, + src: img.src ? (img.src.length > 50 ? img.src.substring(0, 50) + '...' : img.src) : 'N/A', + naturalWidth: img.naturalWidth, + naturalHeight: img.naturalHeight, + displayWidth: img.offsetWidth || 0, + displayHeight: img.offsetHeight || 0, + estimatedMemoryMB: (estimatedMemory / 1048576).toFixed(2), + complete: img.complete || false, + loading: img.loading || 'auto' + }); + } + } catch (e) { + // 개별 이미지 요소 처리 중 오류 발생 시 무시 + console.warn('[Memory Monitor] Error processing image element:', e); + } + }); + + return { + imageCount: images.length, + totalImageMemoryMB: (totalImageMemory / 1048576).toFixed(2), + images: imageInfo + }; + } catch (e) { + console.warn('[Memory Monitor] Error getting image memory info:', e); + return { + imageCount: 0, + totalImageMemoryMB: '0.00', + images: [] + }; + } + }; + + // Canvas/WebGL 리소스 메모리 정보 수집 + const getCanvasMemoryInfo = () => { + try { + const canvases = document.querySelectorAll('canvas'); + let totalCanvasMemory = 0; + const canvasInfo = []; + + // NodeList를 배열로 변환하여 forEach 사용 + Array.from(canvases).forEach((canvas, index) => { + try { + const context = canvas.getContext('2d') || canvas.getContext('webgl') || canvas.getContext('webgl2'); + if (context) { + const memory = canvas.width * canvas.height * 4; // 4바이트 per 픽셀 + totalCanvasMemory += memory; + + canvasInfo.push({ + index, + width: canvas.width || 0, + height: canvas.height || 0, + contextType: context.constructor.name || 'unknown', + estimatedMemoryMB: (memory / 1048576).toFixed(2) + }); + } + } catch (e) { + // 개별 캔버스 요소 처리 중 오류 발생 시 무시 + console.warn('[Memory Monitor] Error processing canvas element:', e); + } + }); + + return { + canvasCount: canvases.length, + totalCanvasMemoryMB: (totalCanvasMemory / 1048576).toFixed(2), + canvases: canvasInfo + }; + } catch (e) { + console.warn('[Memory Monitor] Error getting canvas memory info:', e); + return { + canvasCount: 0, + totalCanvasMemoryMB: '0.00', + canvases: [] + }; + } + }; + + // 통합 미디어 메모리 정보 + const getCompleteMediaMemoryInfo = () => { + const mediaMemory = getMediaMemoryInfo(); + const imageMemory = getImageMemoryInfo(); + const canvasMemory = getCanvasMemoryInfo(); + + const totalEstimatedMB = ( + parseFloat(mediaMemory.estimatedMediaMemoryMB) + + parseFloat(imageMemory.totalImageMemoryMB) + + parseFloat(canvasMemory.totalCanvasMemoryMB) + ).toFixed(2); + + return { + totalEstimatedMediaMemoryMB: totalEstimatedMB, + media: mediaMemory, + images: imageMemory, + canvas: canvasMemory, + timestamp: new Date().toISOString() + }; + }; + const getDetailedMemoryInfo = () => { const info = getMemoryInfo(); if (!info) return null; @@ -46,6 +293,8 @@ export const createMemoryMonitor = (enableInitLog = true) => { domNodeCount: document.querySelectorAll('*').length, // 리스너 수 (대략값) eventListenerEstimate: Object.keys(window).filter(key => key.startsWith('on')).length, + // 미디어 리소스 정보 추가 + mediaMemory: getCompleteMediaMemoryInfo(), }; return detailed; @@ -155,7 +404,121 @@ export const createMemoryMonitor = (enableInitLog = true) => { domNodeCount: detailed.domNodeCount, eventListenerEstimate: detailed.eventListenerEstimate, }); - console.log(`${logMsg} | ${context} | Details: ${detailStr} ${info}`); + const mediaMemory = detailed.mediaMemory; + const mediaStr = JSON.stringify({ + totalMediaMemory: mediaMemory.totalEstimatedMediaMemoryMB + 'MB', + videoElements: mediaMemory.media.videoElements, + audioElements: mediaMemory.media.audioElements, + imageCount: mediaMemory.images.imageCount, + imageMemory: mediaMemory.images.totalImageMemoryMB + 'MB', + canvasCount: mediaMemory.canvas.canvasCount, + canvasMemory: mediaMemory.canvas.totalCanvasMemoryMB + 'MB' + }); + + const jsTotal = parseFloat(detailed.usedJSHeapSize); + const mediaTotal = parseFloat(mediaMemory.totalEstimatedMediaMemoryMB); + const estimatedTotal = (jsTotal + mediaTotal).toFixed(2); + + console.log(`${logMsg} | ${context} | Details: ${detailStr} | Media: ${mediaStr} | Est.Total: ${estimatedTotal}MB ${info}`); + } + }, + + /** + * 전체 미디어 리소스 메모리 로깅 + * @param {string} context - 컨텍스트 + * @param {object} additionalInfo - 추가 정보 + */ + logMediaMemory: (context = '', additionalInfo = {}) => { + const jsMem = getMemoryInfo(); + const mediaMem = getCompleteMediaMemoryInfo(); + + if (jsMem && mediaMem) { + const jsTotal = parseFloat(jsMem.usedJSHeapSize); + const mediaTotal = parseFloat(mediaMem.totalEstimatedMediaMemoryMB); + const estimatedTotal = (jsTotal + mediaTotal).toFixed(2); + + const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit); + const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : ''; + + console.log(`${logMsg} | Media: ${context}`); + console.log(`[Media Breakdown] Images: ${mediaMem.images.totalImageMemoryMB}MB (${mediaMem.images.imageCount}개), Video: ${mediaMem.media.estimatedMediaMemoryMB}MB (${mediaMem.media.mediaCount}개), Canvas: ${mediaMem.canvas.totalCanvasMemoryMB}MB (${mediaMem.canvas.canvasCount}개)`); + console.log(`[Total Estimated] JS(${jsTotal}MB) + Media(${mediaTotal}MB) = ${estimatedTotal}MB ${info}`); + } else { + const timestamp = new Date().toISOString(); + console.log(`[Media Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`); + } + }, + + /** + * 비디오 전용 상세 메모리 로깅 + * @param {string} context - 컨텍스트 + * @param {object} additionalInfo - 추가 정보 + */ + logVideoMemory: (context = '', additionalInfo = {}) => { + const jsMem = getMemoryInfo(); + const mediaMem = getMediaMemoryInfo(); + + if (jsMem && mediaMem) { + const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit); + const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : ''; + + console.log(`${logMsg} | Video Memory: ${context}`); + console.log(`[Video Summary] ${mediaMem.videoElements}개 비디오, ${mediaMem.totalVideoBufferMB}MB 버퍼 메모리 사용`); + + // 개별 비디오 정보 상세 출력 + mediaMem.mediaElements.forEach((video, idx) => { + if (video.type === 'video') { + console.log(`[Video ${video.index}] ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Buffered: ${video.bufferedDuration.toFixed(1)}s/${video.duration.toFixed(1)}s (${video.bufferEfficiency}) | ${video.hlsInfo?.isHLS ? 'HLS' : 'Progressive'} | ${video.paused ? 'Paused' : 'Playing'} | Src: ${video.src}`); + } + }); + + console.log(`[Video Estimation] JS Heap: ${jsMem.usedJSHeapSize}MB + Video Buffer: ${mediaMem.totalVideoBufferMB}MB = ${(parseFloat(jsMem.usedJSHeapSize) + parseFloat(mediaMem.totalVideoBufferMB)).toFixed(2)}MB ${info}`); + } else { + const timestamp = new Date().toISOString(); + console.log(`[Video Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`); + } + }, + + /** + * HLS 스트리밍 메모리 전용 로깅 + * @param {string} context - 컨텍스트 + * @param {object} additionalInfo - 추가 정보 + */ + logHLSMemory: (context = '', additionalInfo = {}) => { + const jsMem = getMemoryInfo(); + const mediaMem = getMediaMemoryInfo(); + + if (jsMem && mediaMem) { + const hlsVideos = mediaMem.mediaElements.filter(video => video.hlsInfo && video.hlsInfo.isHLS); + const progressiveVideos = mediaMem.mediaElements.filter(video => video.hlsInfo && !video.hlsInfo.isHLS); + + const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit); + const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : ''; + + console.log(`${logMsg} | HLS Streaming: ${context}`); + console.log(`[Streaming Analysis] HLS: ${hlsVideos.length}개, Progressive: ${progressiveVideos.length}개 | Total Video Memory: ${mediaMem.totalVideoBufferMB}MB`); + + // HLS 비디오 상세 정보 + if (hlsVideos.length > 0) { + console.log(`[HLS Videos]`); + hlsVideos.forEach(video => { + console.log(` ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Segments: ~${video.hlsInfo.estimatedSegments}개 | Buffer: ${video.bufferedDuration.toFixed(1)}s | Efficiency: ${video.bufferEfficiency}`); + }); + } + + // Progressive 비디오 상세 정보 + if (progressiveVideos.length > 0) { + console.log(`[Progressive Videos]`); + progressiveVideos.forEach(video => { + console.log(` ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Format: ${video.hlsInfo.format} | Buffer: ${video.bufferedDuration.toFixed(1)}s`); + }); + } + + const streamingMemoryMB = hlsVideos.reduce((sum, video) => { + return sum + parseFloat(video.bufferedDuration) * (video.bitrate / 8 / 1048576); + }, 0).toFixed(2); + + console.log(`[Streaming Memory] HLS Buffer: ${streamingMemoryMB}MB | Progressive Buffer: ${(parseFloat(mediaMem.totalVideoBufferMB) - parseFloat(streamingMemoryMB)).toFixed(2)}MB ${info}`); } }, @@ -165,6 +528,7 @@ export const createMemoryMonitor = (enableInitLog = true) => { */ getMemory: () => getMemoryInfo(), getDetailedMemory: () => getDetailedMemoryInfo(), + getMediaMemory: () => getCompleteMediaMemoryInfo(), }; // 싱글톤 인스턴스 저장 @@ -230,11 +594,109 @@ export const createMemoryMonitor = (enableInitLog = true) => { domNodeCount: detailed.domNodeCount, eventListenerEstimate: detailed.eventListenerEstimate, }); - console.log(`${logMsg} | ${context} | Details: ${detailStr} ${info}`); + const mediaMemory = detailed.mediaMemory; + const mediaStr = JSON.stringify({ + totalMediaMemory: mediaMemory.totalEstimatedMediaMemoryMB + 'MB', + videoElements: mediaMemory.media.videoElements, + audioElements: mediaMemory.media.audioElements, + imageCount: mediaMemory.images.imageCount, + imageMemory: mediaMemory.images.totalImageMemoryMB + 'MB', + canvasCount: mediaMemory.canvas.canvasCount, + canvasMemory: mediaMemory.canvas.totalCanvasMemoryMB + 'MB' + }); + + const jsTotal = parseFloat(detailed.usedJSHeapSize); + const mediaTotal = parseFloat(mediaMemory.totalEstimatedMediaMemoryMB); + const estimatedTotal = (jsTotal + mediaTotal).toFixed(2); + + console.log(`${logMsg} | ${context} | Details: ${detailStr} | Media: ${mediaStr} | Est.Total: ${estimatedTotal}MB ${info}`); + } + }, + + logMediaMemory: (context = '', additionalInfo = {}) => { + const jsMem = getMemoryInfo(); + const mediaMem = getCompleteMediaMemoryInfo(); + + if (jsMem && mediaMem) { + const jsTotal = parseFloat(jsMem.usedJSHeapSize); + const mediaTotal = parseFloat(mediaMem.totalEstimatedMediaMemoryMB); + const estimatedTotal = (jsTotal + mediaTotal).toFixed(2); + + const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit); + const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : ''; + + console.log(`${logMsg} | Media: ${context}`); + console.log(`[Media Breakdown] Images: ${mediaMem.images.totalImageMemoryMB}MB (${mediaMem.images.imageCount}개), Video: ${mediaMem.media.estimatedMediaMemoryMB}MB (${mediaMem.media.mediaCount}개), Canvas: ${mediaMem.canvas.totalCanvasMemoryMB}MB (${mediaMem.canvas.canvasCount}개)`); + console.log(`[Total Estimated] JS(${jsTotal}MB) + Media(${mediaTotal}MB) = ${estimatedTotal}MB ${info}`); + } else { + const timestamp = new Date().toISOString(); + console.log(`[Media Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`); } }, getMemory: () => getMemoryInfo(), getDetailedMemory: () => getDetailedMemoryInfo(), + getMediaMemory: () => getCompleteMediaMemoryInfo(), + logVideoMemory: (context = '', additionalInfo = {}) => { + const jsMem = getMemoryInfo(); + const mediaMem = getMediaMemoryInfo(); + + if (jsMem && mediaMem) { + const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit); + const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : ''; + + console.log(`${logMsg} | Video Memory: ${context}`); + console.log(`[Video Summary] ${mediaMem.videoElements}개 비디오, ${mediaMem.totalVideoBufferMB}MB 버퍼 메모리 사용`); + + // 개별 비디오 정보 상세 출력 + mediaMem.mediaElements.forEach((video, idx) => { + if (video.type === 'video') { + console.log(`[Video ${video.index}] ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Buffered: ${video.bufferedDuration.toFixed(1)}s/${video.duration.toFixed(1)}s (${video.bufferEfficiency}) | ${video.hlsInfo?.isHLS ? 'HLS' : 'Progressive'} | ${video.paused ? 'Paused' : 'Playing'} | Src: ${video.src}`); + } + }); + + console.log(`[Video Estimation] JS Heap: ${jsMem.usedJSHeapSize}MB + Video Buffer: ${mediaMem.totalVideoBufferMB}MB = ${(parseFloat(jsMem.usedJSHeapSize) + parseFloat(mediaMem.totalVideoBufferMB)).toFixed(2)}MB ${info}`); + } else { + const timestamp = new Date().toISOString(); + console.log(`[Video Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`); + } + }, + logHLSMemory: (context = '', additionalInfo = {}) => { + const jsMem = getMemoryInfo(); + const mediaMem = getMediaMemoryInfo(); + + if (jsMem && mediaMem) { + const hlsVideos = mediaMem.mediaElements.filter(video => video.hlsInfo && video.hlsInfo.isHLS); + const progressiveVideos = mediaMem.mediaElements.filter(video => video.hlsInfo && !video.hlsInfo.isHLS); + + const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit); + const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : ''; + + console.log(`${logMsg} | HLS Streaming: ${context}`); + console.log(`[Streaming Analysis] HLS: ${hlsVideos.length}개, Progressive: ${progressiveVideos.length}개 | Total Video Memory: ${mediaMem.totalVideoBufferMB}MB`); + + // HLS 비디오 상세 정보 + if (hlsVideos.length > 0) { + console.log(`[HLS Videos]`); + hlsVideos.forEach(video => { + console.log(` ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Segments: ~${video.hlsInfo.estimatedSegments}개 | Buffer: ${video.bufferedDuration.toFixed(1)}s | Efficiency: ${video.bufferEfficiency}`); + }); + } + + // Progressive 비디오 상세 정보 + if (progressiveVideos.length > 0) { + console.log(`[Progressive Videos]`); + progressiveVideos.forEach(video => { + console.log(` ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Format: ${video.hlsInfo.format} | Buffer: ${video.bufferedDuration.toFixed(1)}s`); + }); + } + + const streamingMemoryMB = hlsVideos.reduce((sum, video) => { + return sum + parseFloat(video.bufferedDuration) * (video.bitrate / 8 / 1048576); + }, 0).toFixed(2); + + console.log(`[Streaming Memory] HLS Buffer: ${streamingMemoryMB}MB | Progressive Buffer: ${(parseFloat(mediaMem.totalVideoBufferMB) - parseFloat(streamingMemoryMB)).toFixed(2)}MB ${info}`); + } + }, }; return memoryMonitorInstance;