[251125] memory monitoring

This commit is contained in:
2025-11-25 22:47:00 +09:00
parent 7baeca9432
commit c88c0cebc8
3 changed files with 294 additions and 0 deletions

View File

@@ -66,10 +66,12 @@ import TReactPlayer from './TReactPlayer';
import Video from './Video'; import Video from './Video';
import css from './VideoPlayer.module.less'; import css from './VideoPlayer.module.less';
import { updateVideoPlayState } from '../../actions/playActions'; import { updateVideoPlayState } from '../../actions/playActions';
import createMemoryMonitor from '../../utils/memoryMonitor';
// 디버그 헬퍼 설정 // 디버그 헬퍼 설정
const DEBUG_MODE = false; const DEBUG_MODE = false;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
const memoryMonitor = createMemoryMonitor();
const isEnter = is('enter'); const isEnter = is('enter');
const isLeft = is('left'); const isLeft = is('left');
@@ -826,6 +828,7 @@ const VideoPlayerBase = class extends React.Component {
} }
componentDidMount() { componentDidMount() {
memoryMonitor.logMemory('[VideoPlayer] componentDidMount');
on('mousemove', this.activityDetected); on('mousemove', this.activityDetected);
if (platform.touch) { if (platform.touch) {
on('touchmove', this.activityDetected); on('touchmove', this.activityDetected);
@@ -1021,6 +1024,7 @@ const VideoPlayerBase = class extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
memoryMonitor.logMemory('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src });
// console.log('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src }); // console.log('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src });
off('mousemove', this.activityDetected); off('mousemove', this.activityDetected);
if (platform.touch) { if (platform.touch) {
@@ -1128,6 +1132,12 @@ const VideoPlayerBase = class extends React.Component {
} }
// 레퍼런스도 해제해 GC 대상이 되도록 함 // 레퍼런스도 해제해 GC 대상이 되도록 함
this.video = null; this.video = null;
// 메모리 모니터링 인터벌 정리
if (this.memoryMonitoringInterval) {
clearInterval(this.memoryMonitoringInterval);
this.memoryMonitoringInterval = null;
}
memoryMonitor.logMemory('[VideoPlayer] componentWillUnmount - cleanup done');
// console.log('[VideoPlayer] componentWillUnmount - cleanup done', { src: this.props?.src }); // console.log('[VideoPlayer] componentWillUnmount - cleanup done', { src: this.props?.src });
if (this.floatingLayerController) { if (this.floatingLayerController) {
this.floatingLayerController.unregister(); this.floatingLayerController.unregister();
@@ -1564,6 +1574,14 @@ const VideoPlayerBase = class extends React.Component {
handleEvent = (ev) => { handleEvent = (ev) => {
const el = this.video; const el = this.video;
// 재생 종료 또는 오류 시 메모리 모니터링 타이머 정리
if (ev.type === 'ended' || ev.type === 'error') {
if (this.memoryMonitoringInterval) {
clearInterval(this.memoryMonitoringInterval);
this.memoryMonitoringInterval = null;
}
}
const updatedState = { const updatedState = {
// Standard media properties // Standard media properties
currentTime: 0, currentTime: 0,
@@ -1798,6 +1816,11 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
play = () => { play = () => {
console.log('[TEST] play() method called');
memoryMonitor.logMemory('[VideoPlayer] play() called', {
currentTime: this.state.currentTime,
duration: this.state.duration,
});
dlog('🟢 [PlayerPanel][VideoPlayer] play() called', { dlog('🟢 [PlayerPanel][VideoPlayer] play() called', {
currentTime: this.state.currentTime, currentTime: this.state.currentTime,
duration: this.state.duration, duration: this.state.duration,
@@ -1819,6 +1842,21 @@ const VideoPlayerBase = class extends React.Component {
this.send('play'); this.send('play');
this.announce($L('Play')); this.announce($L('Play'));
this.startDelayedMiniFeedbackHide(5000); this.startDelayedMiniFeedbackHide(5000);
// 재생 시작 시 정기적 메모리 모니터링 시작
if (!this.memoryMonitoringInterval) {
this.memoryMonitoringInterval = setInterval(() => {
try {
const mediaState = this.getMediaState();
memoryMonitor.logMemory('[VideoPlayer] Playing', {
currentTime: (mediaState?.currentTime ?? 0).toFixed(2),
duration: (mediaState?.duration ?? 0).toFixed(2),
buffered: (this.state?.proportionLoaded ?? 0).toFixed(2),
});
} catch (err) {
// 타이머 실행 중 오류 발생 시 무시
}
}, 30000); // 30초마다 메모리 확인
}
// Redux 상태 업데이트 - 재생 상태로 변경 // Redux 상태 업데이트 - 재생 상태로 변경
if (this.props.dispatch) { if (this.props.dispatch) {
@@ -1842,6 +1880,10 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
pause = () => { pause = () => {
memoryMonitor.logMemory('[VideoPlayer] pause() called', {
currentTime: this.state.currentTime.toFixed(2),
duration: this.state.duration.toFixed(2),
});
dlog('🔴 [VideoPlayer] pause() called', { dlog('🔴 [VideoPlayer] pause() called', {
currentTime: this.state.currentTime, currentTime: this.state.currentTime,
duration: this.state.duration, duration: this.state.duration,
@@ -1863,6 +1905,11 @@ const VideoPlayerBase = class extends React.Component {
this.send('pause'); this.send('pause');
this.announce($L('Pause')); this.announce($L('Pause'));
this.stopDelayedMiniFeedbackHide(); this.stopDelayedMiniFeedbackHide();
// 재생 일시정지 시 정기적 메모리 모니터링 중지
if (this.memoryMonitoringInterval) {
clearInterval(this.memoryMonitoringInterval);
this.memoryMonitoringInterval = null;
}
// Redux 상태 업데이트 - 일시정지 상태로 변경 // Redux 상태 업데이트 - 일시정지 상태로 변경
if (this.props.dispatch) { if (this.props.dispatch) {

View File

@@ -0,0 +1,191 @@
/**
* 메모리 모니터링 유틸리티
* [Memory] 태그를 붙인 로그로 메모리 사용량을 추적합니다
*/
let memoryMonitorInstance = null;
let initialized = false;
export const createMemoryMonitor = (enableInitLog = true) => {
// 싱글톤 패턴: 이미 생성된 인스턴스가 있으면 재사용
if (memoryMonitorInstance) {
return memoryMonitorInstance;
}
if (enableInitLog && !initialized) {
initialized = true;
const timestamp = new Date().toISOString();
console.log(`[Memory Monitor Initialized] ${timestamp}`);
if (typeof performance !== 'undefined' && performance.memory) {
console.log(`[Memory] API Support: YES - performance.memory available`);
} else {
console.log(`[Memory] API Support: NO - performance.memory NOT available (webOS TV 또는 제한된 브라우저)`);
}
}
const getMemoryInfo = () => {
if (typeof performance !== 'undefined' && performance.memory) {
return {
usedJSHeapSize: (performance.memory.usedJSHeapSize / 1048576).toFixed(2),
totalJSHeapSize: (performance.memory.totalJSHeapSize / 1048576).toFixed(2),
jsHeapSizeLimit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2),
};
}
return null;
};
const formatMemoryLog = (usedMB, totalMB, limitMB) => {
const percentage = ((usedMB / limitMB) * 100).toFixed(1);
return `[Memory] Used: ${usedMB}MB / Total: ${totalMB}MB / Limit: ${limitMB}MB (${percentage}%)`;
};
return {
/**
* 현재 메모리 상태를 로깅
* @param {string} context - 컨텍스트 설명
* @param {object} additionalInfo - 추가 정보
*/
logMemory: (context = '', additionalInfo = {}) => {
const mem = getMemoryInfo();
if (mem) {
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
console.log(`${logMsg} | ${context} ${info}`);
} else {
const timestamp = new Date().toISOString();
console.log(`[Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`);
}
},
/**
* 메모리 사용량 변화를 추적
* @param {string} context - 컨텍스트 설명
* @param {number} previousMB - 이전 메모리 사용량 (MB)
* @returns {number} 현재 메모리 사용량 (MB)
*/
trackMemoryDelta: (context = '', previousMB = 0) => {
const mem = getMemoryInfo();
if (mem) {
const currentMB = parseFloat(mem.usedJSHeapSize);
const delta = (currentMB - previousMB).toFixed(2);
const deltaSign = delta > 0 ? '+' : '';
console.log(
`[Memory] ${context} | Current: ${currentMB}MB (${deltaSign}${delta}MB) | Total: ${mem.totalJSHeapSize}MB / Limit: ${mem.jsHeapSizeLimit}MB`
);
return currentMB;
}
return previousMB;
},
/**
* 정기적으로 메모리를 모니터링
* @param {number} intervalMs - 모니터링 간격 (기본값: 10000ms)
* @param {string} label - 모니터링 라벨
* @returns {function} cleanup 함수
*/
startPeriodicMonitoring: (intervalMs = 10000, label = 'Periodic') => {
let lastMemory = 0;
const mem = getMemoryInfo();
if (mem) lastMemory = parseFloat(mem.usedJSHeapSize);
const intervalId = setInterval(() => {
lastMemory = this.trackMemoryDelta(`${label}:`, lastMemory);
}, intervalMs);
return () => clearInterval(intervalId);
},
/**
* 버퍼 관련 메모리 정보 로깅
* @param {string} context - 컨텍스트
* @param {object} bufferInfo - 버퍼 정보 { bufferedSegments, totalDuration, etc }
*/
logBufferMemory: (context = '', bufferInfo = {}) => {
const mem = getMemoryInfo();
if (mem) {
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
const bufferStr = JSON.stringify(bufferInfo);
console.log(`${logMsg} | Buffer: ${context} | Info: ${bufferStr}`);
}
},
/**
* HLS 상태에 따른 메모리 로깅
* @param {string} context - 컨텍스트
* @param {object} hlsState - HLS 상태 정보
*/
logHlsMemory: (context = '', hlsState = {}) => {
const mem = getMemoryInfo();
if (mem) {
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
const hlsStr = JSON.stringify(hlsState);
console.log(`${logMsg} | HLS: ${context} | State: ${hlsStr}`);
}
},
/**
* 메모리 정보만 반환 (로깅 없음)
* @returns {object} 메모리 정보 객체
*/
getMemory: () => getMemoryInfo(),
};
// 싱글톤 인스턴스 저장
memoryMonitorInstance = {
logMemory: (context = '', additionalInfo = {}) => {
const mem = getMemoryInfo();
if (mem) {
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
console.log(`${logMsg} | ${context} ${info}`);
} else {
const timestamp = new Date().toISOString();
console.log(`[Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`);
}
},
trackMemoryDelta: (context = '', previousMB = 0) => {
const mem = getMemoryInfo();
if (mem) {
const currentMB = parseFloat(mem.usedJSHeapSize);
const delta = (currentMB - previousMB).toFixed(2);
const deltaSign = delta > 0 ? '+' : '';
console.log(
`[Memory] ${context} | Current: ${currentMB}MB (${deltaSign}${delta}MB) | Total: ${mem.totalJSHeapSize}MB / Limit: ${mem.jsHeapSizeLimit}MB`
);
return currentMB;
}
return previousMB;
},
startPeriodicMonitoring: (intervalMs = 30000, label = 'Periodic') => {
let lastMemory = 0;
const mem = getMemoryInfo();
if (mem) lastMemory = parseFloat(mem.usedJSHeapSize);
const intervalId = setInterval(() => {
this.trackMemoryDelta(`${label}:`, lastMemory);
}, intervalMs);
return () => clearInterval(intervalId);
},
logBufferMemory: (context = '', bufferInfo = {}) => {
const mem = getMemoryInfo();
if (mem) {
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
const bufferStr = JSON.stringify(bufferInfo);
console.log(`${logMsg} | Buffer: ${context} | Info: ${bufferStr}`);
}
},
logHlsMemory: (context = '', hlsState = {}) => {
const mem = getMemoryInfo();
if (mem) {
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
const hlsStr = JSON.stringify(hlsState);
console.log(`${logMsg} | HLS: ${context} | State: ${hlsStr}`);
}
},
getMemory: () => getMemoryInfo(),
};
return memoryMonitorInstance;
};
export default createMemoryMonitor;

View File

@@ -62,6 +62,7 @@ import css from './PlayerPanel.module.less';
import PlayerTabButton from './PlayerTabContents/TabButton/PlayerTabButton'; import PlayerTabButton from './PlayerTabContents/TabButton/PlayerTabButton';
import TabContainer from './PlayerTabContents/TabContainer'; import TabContainer from './PlayerTabContents/TabContainer';
import TabContainerV2 from './PlayerTabContents/v2/TabContainer.v2'; import TabContainerV2 from './PlayerTabContents/v2/TabContainer.v2';
import createMemoryMonitor from '../../utils/memoryMonitor';
// import LiveShowContainer from './PlayerTabContents/v2/LiveShowContainer'; // import LiveShowContainer from './PlayerTabContents/v2/LiveShowContainer';
// import ShopNowContainer from './PlayerTabContents/v2/ShopNowContainer'; // import ShopNowContainer from './PlayerTabContents/v2/ShopNowContainer';
// import ShopNowButton from './PlayerTabContents/v2/ShopNowButton'; // import ShopNowButton from './PlayerTabContents/v2/ShopNowButton';
@@ -179,6 +180,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const focusReturnRef = useRef(null); const focusReturnRef = useRef(null);
const modalPrevRef = useRef(panelInfo?.modal); const modalPrevRef = useRef(panelInfo?.modal);
const prevIsTopPanelDetailFromPlayerRef = useRef(false); const prevIsTopPanelDetailFromPlayerRef = useRef(false);
const memoryMonitor = useRef(null);
const [playListInfo, setPlayListInfo] = USE_STATE('playListInfo', ''); const [playListInfo, setPlayListInfo] = USE_STATE('playListInfo', '');
const [shopNowInfo, setShopNowInfo] = USE_STATE('shopNowInfo'); const [shopNowInfo, setShopNowInfo] = USE_STATE('shopNowInfo');
const [backupInitialIndex, setBackupInitialIndex] = USE_STATE('backupInitialIndex', 0); const [backupInitialIndex, setBackupInitialIndex] = USE_STATE('backupInitialIndex', 0);
@@ -370,6 +372,14 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
panelInfoRef.current = panelInfo; panelInfoRef.current = panelInfo;
}, [panelInfo]); }, [panelInfo]);
// memoryMonitor 초기화 (마운트 시 한 번만)
useEffect(() => {
if (!memoryMonitor.current) {
memoryMonitor.current = createMemoryMonitor(false); // 초기 로그 비활성화
console.log('[PlayerPanel] Memory monitor initialized');
}
}, []);
// PlayerPanel.jsx의 라인 313-327 useEffect 수정 - detailPanelClosed flag 감지 추가 // PlayerPanel.jsx의 라인 313-327 useEffect 수정 - detailPanelClosed flag 감지 추가
useEffect(() => { useEffect(() => {
dlog('[PlayerPanel] 🔍 isOnTop useEffect 호출:', { dlog('[PlayerPanel] 🔍 isOnTop useEffect 호출:', {
@@ -1733,6 +1743,24 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime); setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime);
break; break;
} }
case 'onBuffer': {
// 버퍼링 시작 시 메모리 상태 로깅
memoryMonitor.current.logMemory('[Video Buffer Start]', {
currentTime: videoPlayer.current?.getMediaState()?.currentTime?.toFixed(2),
duration: videoPlayer.current?.getMediaState()?.duration?.toFixed(2),
proportionLoaded: videoPlayer.current?.getMediaState()?.proportionLoaded?.toFixed(2),
});
break;
}
case 'onBufferEnd': {
// 버퍼링 종료 시 메모리 상태 로깅
memoryMonitor.current.logMemory('[Video Buffer End]', {
currentTime: videoPlayer.current?.getMediaState()?.currentTime?.toFixed(2),
duration: videoPlayer.current?.getMediaState()?.duration?.toFixed(2),
proportionLoaded: videoPlayer.current?.getMediaState()?.proportionLoaded?.toFixed(2),
});
break;
}
case 'error': { case 'error': {
dispatch( dispatch(
sendBroadCast({ sendBroadCast({
@@ -1746,6 +1774,23 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const mediaId = videoPlayer.current?.video?.media?.mediaId; const mediaId = videoPlayer.current?.video?.media?.mediaId;
setMediaId(mediaId); setMediaId(mediaId);
setVideoLoaded(true); setVideoLoaded(true);
// HLS 인스턴스 정보 로깅
try {
const hlsInstance = videoPlayer.current?.video?.getInternalPlayer?.('hls');
if (hlsInstance) {
memoryMonitor.current.logHlsMemory('[Video Loaded] HLS Instance', {
hlsVersion: hlsInstance.version,
config: {
maxBufferLength: hlsInstance.config?.maxBufferLength,
maxMaxBufferLength: hlsInstance.config?.maxMaxBufferLength,
backBufferLength: hlsInstance.config?.backBufferLength,
maxBufferSize: hlsInstance.config?.maxBufferSize,
},
});
}
} catch (e) {
// HLS 정보 수집 실패는 무시
}
dlog( dlog(
'[PlayerPanel] 🎬 Video Loaded - shptmBanrTpNm:', '[PlayerPanel] 🎬 Video Loaded - shptmBanrTpNm:',
panelInfoRef.current?.shptmBanrTpNm panelInfoRef.current?.shptmBanrTpNm
@@ -1998,6 +2043,17 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setVideoLoaded(false); setVideoLoaded(false);
}, [currentPlayingUrl]); }, [currentPlayingUrl]);
// 메모리 모니터링: 비디오 URL 변경 시
useEffect(() => {
if (currentPlayingUrl) {
memoryMonitor.current.logMemory(`[Video Change] New URL loaded`, {
url: currentPlayingUrl.substring(0, 50),
isHLS: currentPlayingUrl.includes('.m3u8'),
isDASH: currentPlayingUrl.includes('.mpd'),
});
}
}, [currentPlayingUrl]);
// 비디오가 새로 선택될 때 타이머 초기화 // 비디오가 새로 선택될 때 타이머 초기화
useEffect(() => { useEffect(() => {
if (currentPlayingUrl) { if (currentPlayingUrl) {