IE11 및 Chromium Edge에서 메모리 힙 스냅샷 찍기
본문의 주제를 처음 준비하던 2018년 후반만 해도 본문은 'IE11을 이용한 JavaScript 디버깅' 시리즈의 일부분으로 구성되어 몇 편에 걸쳐 나뉘어 제공될 예정이었습니다. 그러나 공교롭게도 거의 동일한 시기에 Microsoft가 Chromium Edge에 대한 구체적인 계획을 발표함으로써 시리즈 자체의 필요성이 크게 줄어들었고, 결과적으로 다루고자 계획했던 내용을 끝까지 진행하지 못한 체 시리즈는 사실상 중단된 상태입니다.
비록 'IE11을 이용한 JavaScript 디버깅' 시리즈의 내용이 IE11의 F12 개발자 도구를 활용하는 방법에 중점을 두고 있기는 하지만, 최종 목표는 단순히 그에 그치지 않고 상대적으로 UI 및 기능이 단순한 IE11의 F12 개발자 도구에 친숙해진 다음, 거의 동일한 UI와 기능을 제공하지만 대폭 개선된 Classic Edge를 다루고, 또다시 보다 풍부한 기능을 제공하는 Chrome 등의 최신 브라우저로 접근을 확대해나가는 것이었습니다. 그러나 중간 징검다리 역할을 해줄 Classic Edge의 존재 위치가 불확실해짐에 따라 시리즈의 전반적인 맥락이 끊어졌다는 판단이었습니다.
드디어 2020년 초인 현재, 정식으로 배포되기 시작한 Microsoft의 Chromium Edge는 나름대로 순조롭게 시장에 안착하고 있는 것으로 보입니다. F12 개발자 도구라는 관점에서만 본다면 Chrome과 거의 동일한 UI 및 기능을 제공하고 있어서 기본적인 수준에서는 둘 중 어떤 브라우저를 선택하더라도 별다른 차이가 없을 듯합니다. 개인적으로 과거 IE11/Classic Edge 계열과 Chrome 계열로 나눴던 F12 개발자 도구 그룹을 이제는 IE11 계열과 Chromium Edge/Chrome 계열로 분류해도 무방할 것 같습니다. (FireFox까지 다루고 싶은 욕심도 있었지만 현실적으로 제 역량으로는 무리라고 생각되어 본문에서는 다루지 않습니다.)
본문에서는 IE11과 Chromium Edge의 F12 개발자 도구를 활용한 메모리 힙 스냅샷의 비교/분석을 통해 웹 브라우저의 메모리 누수를 감지하는 가장 기본적인 방법을 살펴봅니다.
시리즈 목차
- IE11 및 Chromium Edge에서 메모리 힙 스냅샷 찍기: 메모리 힙 스냅샷 찍기, 용어 및 기본적인 화면 구성 이해
- IE11 및 Chromium Edge에서 메모리 힙 스냅샷 비교하기: 메모리 힙 스냅샷 간 비교/분석 방법, 분리된 DOM 트리 검토 및 실습
- Chromium Edge의 메모리 힙 스냅샷 분석을 위한 V8 엔진의 이해 1: V8 엔진 실행 파이프라인, 인라이닝, 히든 클래스, 인라인 캐싱, 추적 로그 검토 방법
- Chromium Edge의 메모리 힙 스냅샷 분석을 위한 V8 엔진의 이해 2: 동일한 히든 클래스를 유지하기 위한 다양한 조건, SMI, 명명된 속성, 정수 인덱스 속성
- Visual Studio Community로 V8 엔진 다운로드 및 빌드하고 간단히 살펴보기: V8 엔진 소스 다운로드 및 빌드, depot_tools 설치, 기초적인 D8 옵션 플래그
- IE11 및 Chromium Edge에서 가상의 메모리 누수 상황 재현 및 해결하기: 잘못된 jQuery 사용 패턴, 전역 jQuery 캐시, Performance 창, Allocation instrumentation on timeline 프로파일링
문서 목차
웹 브라우저의 메모리 누수
대부분의 페이지가 빈번하게 새로 고침 되거나 다른 페이지로 이동하는 구조를 가진 전통적인 형태의 웹 응용 프로그램 개발에 있어, 특히나 JavaScript 및 HTML DOM을 주로 다루는 웹 클라이언트 개발자에게 지금까지 메모리 누수라는 개념은 그다지 주의 깊게 다뤄야만 하는 주제는 아니었습니다. 그러나 점차 Ajax 기반의 개발 방식과 SPA(Single Page Application) 프레임워크를 사용하는 웹 응용 프로그램이 부상하면서 더 이상 메모리 누수 관리를 다른 영역의 얘기로만 미뤄둘 수 없게 되었습니다.
굳이 어려운 개념을 들먹이지 않더라도 응용 프로그램의 메모리 사용에 관한 이상적인 목표는 간단하면서도 명백합니다. 메모리의 사용이 끝나면 다시 시스템으로 반환되어야 한다는 것, 그것뿐입니다.
대부분의 웹 클라이언트 개발자들이 이미 알고 있는 것처럼, C# 또는 Java와 같은 다른 많은 프로그래밍 언어들과 마찬가지로 웹 브라우저 환경의 JavaScript 역시 가비지 컬렉터가 메모리의 반환을 책임져주기 때문에 많은 수고를 덜 수 있습니다. 그럼에도 불구하고 실제 업무에서 메모리 누수는 여전히 빈번하게 발생하는 현상이며, 그 중 상당수는 매우 비슷한 코드 패턴을 갖고 있습니다.
이런 메모리 누수 문제를 해결하거나 사전에 검토하고자 할 때 활용할 수 있는 대표적인 도구가 바로 F12 개발자 도구의 메모리 창입니다. 많은 최신 웹 브라우저들이 대부분 이와 비슷한 기능을 제공해줍니다. 본문에서는 그중에서도 IE11과 Chromium Edge의 F12 개발자 도구를 활용한 메모리 힙 스냅샷의 비교/분석을 통해 웹 브라우저의 메모리 누수를 감지하는 가장 기본적인 방법을 살펴봅니다.
F12 개발자 도구 메모리 창
IE11 vs. Chromium Edge
IE11의 메모리 창은 상대적으로 단순한 기능을 제공합니다. 더구나 IE11은 이미 오래전에 개발이 중단된 상태로 더 이상의 기능 개선을 기대할 수가 없습니다. 그 대신 간단명료한 UI를 갖고 있으며 일반적인 웹 개발자가 '기대하는 수준의 정보'를 충분히 '예상할 수 있는 형태'로 보여줍니다. 따라서 초보 개발자가 접근하기가 비교적 용이합니다.
반면 Chromium Edge의 메모리 창은 상당히 방대한 분량의 정보를 제공하며 그 수준도 낮지 않아서 V8 엔진의 일부 정보를 결과에 함께 포함하고 있습니다. 보여주는 정보량이 너무 많아서 원하는 정보를 찾기가 쉽지 않고 강력한 대신 초보 개발자가 접근하기에는 부담스러운 면이 존재합니다. 또한 V8 엔진의 특징인 내부 최적화 작업으로 인한 결과라고 생각되는데, 웹 개발자가 '기대하는 것과는 조금 다른 방식'으로 결과를 보여주어 분석이 쉽지 않습니다. 아마도 Chromium Edge의 메모리 창을 효과적으로 분석하기 위해서는 V8 엔진의 동작 방식까지도 어느 정도는 이해하고 있어야 할 것으로 생각됩니다. (이는 어디까지나 제 개인적으로 결론으로 별다른 근거가 있는 얘기는 아닙니다.)
그렇다고 해서 IE11의 메모리 창이 제공하는 기능이 문제를 해결하기에 부족하다는 뜻은 절대로 아닙니다. 어디까지나 Chromium 계열에 비해 제공되는 정보량이 상대적으로 적다는 뜻일 뿐이며, 초보 개발자가 활용하기 위한 용도로는 여전히 차고 넘칩니다.
또한 국내에서는 정부기관, 관공서, 학교 및 일부 기업체에서 여전히 IE11을 메인 브라우저로 사용하고 있는 경우가 많기 때문에, 안타까운 일이지만 업무 환경에 따라서는 여전히 IE11의 메모리 창을 활용하여 문제를 해결해야만 하는 경우도 많습니다. 당연한 얘기지만 IE11에서만 메모리 누수가 발생하는 경우에는 아무리 기능이 뛰어나다고 한들 Chromium 계열의 F12 개발자 도구로는 문제를 해결할 수 없을 테니 말입니다.
F12 개발자 도구 메모리 창 실행하기
먼저 IE11과 Chromium Edge가 제공하는 F12 개발자 도구 메모리 창의 기본적인 사용 방법과 이에 관한 몇 가지 관련 용어부터 차근차근 살펴보도록 하겠습니다. 다음은 이를 위해 사용할 예제 HTML 파일의 내용으로, 극히 최소한의 태그만 사용하고 있습니다. (새 창에서 보기)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>메모리 누수 - 빈 문서</title>
</head>
<body>
<strong>[빈 문서]</strong>
</body>
</html>
IE11에서 메모리 힙 스냅샷 찍기
IE11에서 예제 페이지를 실행한 다음, F12 키를 눌러서 F12 개발자 도구를 실행합니다. 그리고 메모리 창을 선택하면 다음과 같은 초기 화면이 나타납니다. 그러나 아직 이 상태에서는 얻을 수 있는 정보가 아무것도 없습니다.
화면의 지시대로 Ctrl+E
단축키를 누르거나 메모리 창 좌상단의 프로파일링 세션 시작 버튼을 클릭하여 프로파일링 세션을 시작합니다.
그러면 다음과 같이 프로파일링 세션이 시작되고 총 메모리 사용량 그래프가 기록되기 시작합니다.
이제 Ctrl+Shift+T
단축키를 누르거나 메모리 창 상단의 힙 스냅숏 만들기 버튼을 클릭해서 스냅샷을 찍습니다.
총 메모리 사용량 그래프 하단의 사진기 아이콘이 위치한 타일을 마우스로 클릭해도 동일하게 스냅샷을 찍을 수 있습니다.
그러면 해당 시점의 메모리 힙 스냅샷이 얻어지고, 계속해서 또다시 스냅샷을 찍거나 프로파일링 세션을 중지할 수 있습니다. 스냅샷을 찍는 시점의 총 메모리 사용량 그래프를 가만히 살펴보면 갑자기 메모리 사용량이 증가하는 것을 확인할 수 있는데, 이처럼 총 메모리 사용량은 스냅샷을 찍는 행위만으로도 증가하기 때문에 절대적인 메모리 사용량의 기준으로 삼을 수는 없으므로 참고하시기 바랍니다. 또한 이번 예제는 화면 내용이 단순해서 잘 드러나지 않지만, 각 스냅샷 타일에는 해당 시점의 화면 캡처가 포함됩니다.
다시 Ctrl+E
단축키를 한번 더 누르거나 메모리 창 좌상단의 프로파일링 세션 중지 버튼을 클릭하면 프로파일링 세션이 중지되어 다음과 같이 최종 스냅샷을 얻을 수 있습니다.
마지막으로 스냅샷 타일을 마우스 오른쪽 버튼으로 클릭한 다음, 스냅숏 정보 보기 메뉴를 선택하거나, 스냅샷 타일 상단의 힙 크기 링크 또는 개체 수 링크를 클릭하면 다음과 같이 세부 정보를 볼 수 있는 화면이 나타납니다. 이 상태에서 메모리 창 좌상단의 요약 링크를 클릭하면 다시 이전 화면으로 돌아갈 수 있습니다.
이 세부 정보 화면에 대해서는 잠시 후에 다시 자세하게 살펴보도록 하겠습니다.
Chromium Edge에서 메모리 힙 스냅샷 찍기
지금까지 살펴본 내용과 거의 동일한 작업을 이번에는 Chromium Edge에서 수행해보겠습니다. Chromium Edge에서 예제 페이지를 실행한 다음, F12 키를 눌러서 F12 개발자 도구를 실행합니다. 그리고 Memory 창을 선택하면 다음과 같은 초기 화면이 나타납니다.
IE11과는 달리 모두 세 가지 종류의 프로필을 선택할 수 있는데 본문에서 살펴보고자 하는 프로필은 오직 Heap snapshot 프로필뿐입니다. 이 상태에서 스냅샷을 찍으려면 Memory 창 좌상단의 Take heap snapshot 버튼을 클릭하거나 하단의 Take snapshot 버튼을 클릭하면 됩니다.
그러면 다음과 같이 바로 세부 정보가 화면에 나타납니다.
두 브라우저의 메모리 힙 스냅샷 세부 정보를 대략 훑어보면 제공되는 정보량에 차이가 많다는 점을 알 수 있습니다. 무엇보다 우측의 스크롤바 길이만 비교해봐도 목록의 개수 차이가 매우 큽니다. 가장 큰 이유는 IE11의 기본 설정이 JavaScript가 기본으로 제공하는 개체들을 숨기도록 지정되어 있기 때문입니다. 이에 대해서는 잠시 후에 다시 살펴보도록 하겠습니다.
노트
만약 Chromium Edge나 Chrome에서 사용 중인 확장 프로그램이 많다면 가급적 영향을 최소화하기 위해 새로운 InPrivate 창에서 분석 작업을 수행하는 것이 좋습니다. 많은 확장 프로그램들이 페이지 내부의 DOM을 조작하는 등 영향을 주기 때문에 정확한 분석에 방해가 될 수 있습니다. 본문의 후반부에서 실제로 스냅샷을 비교해보도록 하겠습니다.
메모리 힙 스냅샷 세부 정보 살펴보기
IE11의 메모리 힙 스냅샷 세부 정보
IE11에서는 하나의 메모리 힙 스냅샷을 유형, 루트, 도미네이터의 세 가지 관점에서 살펴볼 수 있습니다.
유형 보기
먼저 한 가지 지적하고 넘어갈 점은 이 보기의 이름으로 사용되고 있는 '유형'이라는 단어의 원문은 'Types'로, 평소 프로그래밍 작업과 관련하여 데이터 형식을 거론할 때 사용하는 바로 그 'Types'을 말한다는 사실입니다. 따라서 '유형'보다는 '형식'으로 번역하는 것이 더 적합할 것 같습니다만, 아무튼 이 점만 이해하고 나면 유형 보기의 관점을 어렵지 않게 파악할 수 있습니다. 다시 말해서 유형 보기는 말 그대로 메모리 힙 내의 개체들을 그 유형(형식) 별로 그룹핑하여 보여줍니다.
가령 이번 예제의 경우, 아래 스냅샷 세부 정보를 살펴보면 (Global) 개체, Window 개체, HTMLDocument 개체 등이 각각 1개씩 생성되었으며, 오직 Function 개체만 3개 생성되었음을 확인할 수 있습니다. 참고로 목록 가장 우측의 개수 칼럼의 숫자가 바로 해당 유형의 생성된 개체 개수를 뜻합니다.
특정 유형을 선택해서 트리를 확장해보면 그 하위 노드로 해당 유형의 실제 인스턴스 노드들이 나타납니다(예, HTMLBodyElement
→ <body>
).
그리고 다시 두 번째 하위 노드부터는 해당 노드가 참조하고 있는 자식 개체들이 계층적으로 나타납니다(예, <body>
→ <strong>
).
목록을 가만히 살펴보면 유지된 크기 칼럼의 크기를 기준으로 역순으로 기본 정렬된 것을 볼 수 있습니다. 메모리 힙 스냅샷의 세부 정보를 정확하게 이해하기 위해서는 이 유지된 크기와 그 좌측의 크기 개념을 정확하게 이해하는 것이 중요합니다.
크기는 참조하고 있는 어떠한 다른 개체의 크기도 포함하지 않은 해당 개체 자체만의 크기를 의미합니다.
반면 유지된 크기는 해당 개체 자체뿐만 아니라 다른 부모가 존재하지 않는 모든 자식 개체들의 크기를 더한 값입니다.
여기서 중요한 부분은 '다른 부모가 존재하지 않는 모든 자식 개체들'이라는 표현입니다.
결론적으로 유지된 크기는 해당 개체가 메모리에서 제거되면 가비지 컬렉터에 의해서 반환될 수 있는 모든 메모리의 크기라는 말과 동일합니다.
가령 다소 비현실적인 예이기는 하지만 이론적으로는 <body>
개체가 제거되면 <strong>
개체도 가비지 컬렉터에 의해 정리될 것입니다.
목록에서 실제 인스턴스 노드 중 하나를 마우스로 선택하면 하단의 개체 참조 목록에 다음과 같이 공유된 참조 목록이 나타납니다.
이 목록에 나타나는 개체들은 어떤 면에서 보면 유지된 크기와 정 반대 경우의 개체입니다. 다시 말해서 이 목록의 개체들은 현재 선택한 개체를 참조하고 있는 개체들입니다. 따라서 현재 선택한 개체가 가비지 컬렉터에 의해서 제거되기 위해서는 이 목록의 개체들이 먼저 모두 정리되어야만 합니다.
루트 보기
루트 보기는 메모리 내 개체들의 참조 관계를 (Global)
개체를 루트로 하는 계층적 구조로 보여줍니다.
DOM을 이해하고 있는 개발자라면 아래의 구조가 너무나 당연할 것입니다.
도미네이터 보기
도미네이터 보기는 힙 메모리 내의 개체들을 도미네이터에 해당하는 개체들을 중심으로 정리하여 보여줍니다.
당연한 얘기지만 이 설명을 이해하기 위해서는 먼저 도미네이터(Dominators)가 무엇인지부터 이해해야 합니다. 사전적인 의미에서 도미네이터란 '지배자', '통솔자', '지배력' 등을 뜻합니다. 또한 Microsoft의 문서에서는 도미네이터를 다른 개체에 대해 배타적/독점적 참조를 갖고 있는 힙 상의 개체라고 설명하고 있습니다. 이 얘기를 다르게 풀어보자면, 특정 개체를 제거했을 때 그 개체가 독점적으로 참조하고 있던 하위의 개체들이 가비지 컬렉터에 의해서 반환된다면 그 개체가 바로 도미네이터입니다.
가령, 개체 A와 개체 B가 개체 C를 동시에 참조하고 있다면 개체 A와 개체 B 중 어떤 개체도 개체 C의 도미네이터가 아닙니다. 왜냐하면 개체 A도, 개체 B도 서로 상대방 때문에 개체 C를 독점적으로 참조하지 못하기 때문입니다. 두 개체 중 한 개체가 해제된 이후에야 남은 다른 개체가 개체 C의 도미네이터가 됩니다. 반대로 개체 C가 개체 A와 개체 B를 모두 참조하고 있다면, 그리고 개체 A와 개체 B를 참조하고 있는 개체가 개체 C 외에는 아무것도 없다면 개체 C는 개체 A와 개체 B의 도미네이터입니다.
여기까지 이해했다면 도미네이터라는 개념이 매우 간단하게 느껴질 수도 있을 것입니다. 그러나 상황에 따라서는 특정 개체가 다른 어떤 개체의 도미네이터인지 아닌지 혼란스러운 경우도 많습니다. 도미네이터에 대한 보다 자세한 정보와 사례들은 Google의 다음 문서를 참고하시기 바랍니다.
기타: 도미네이터별 개체 접기
유형 보기 및 도미네이터 보기 상태에서 메모리 창 우상단의 도미네이터별 개체 접기 버튼을 클릭해서 필터를 토글 할 수 있습니다. 유형 보기에서 이 필터를 활성화시키면 도미네이터 개체만 최상위 수준 보기에 표시됩니다. 도미네이터 보기에서 이 필터를 활성화시키면 최상위 도미네이터 개체만 보기에 표시됩니다.
기타: 기본 제공 표시 및 개체 ID 표시
메모리 창 우상단의 개체 자세히 보기의 표시 설정 변경 버튼을 클릭하면 기본 제공 표시 및 개체 ID 표시 필터를 토글 할 수 있습니다.
잠시 앞에서 설명했던 것처럼 기본 제공 표시를 활성화시키면 JavaScript가 기본으로 제공하는 개체들까지 모두 목록에 표시됩니다.
그리고 개체 ID 표시를 활성화시키면 각 개체의 고유한 ID값이 목록에 함께 표시됩니다.
가끔 개체 ID가 유용한 경우가 있는데 아래 그림과 같은 경우 개체 ID가 제공되지 않는다면 set
함수나 get
함수들을 서로 식별하는 것은 거의 불가능했을 것입니다.
가장 현실적인 사례로 익명 함수들을 서로 구분해야 한다고 가정해보면 무슨 말인지 이해가 될 것입니다.
Chromium Edge의 메모리 힙 스냅샷 세부 정보
Chromium Edge에서는 하나의 메모리 힙 스냅샷을 Summary 또는 Containment라는 두 가지 관점에서 살펴볼 수 있습니다. 기본적으로 Summary 보기는 IE11의 유형 보기와 비슷한 방식으로 정보를 제공해주고 Containment 보기는 IE11의 루트 보기와 비슷한 방식으로 정보를 제공해줍니다. (추가적으로 스냅샷끼리 서로 비교할 때 사용하는 Comparison이라는 보기도 제공되지만 이 보기에 관해서는 다음 글에서 IE11의 해당 기능과 함께 다시 자세하게 살펴보도록 하겠습니다.)
그러나 이미 언급한 것처럼 제공되는 정보량이 질릴 정도로 너무 많아서 당황스러운 수준입니다. 극히 단순한 HTML 페이지로 구성된 본문의 예제만으로도 엄청난 분량의 개체 정보가 쏟아져 나옵니다. 솔직히 말해서 일반적인 웹 클라이언트 개발자에게는 필요 없는 정보까지 너무 많이 제공되어 접근을 어렵게 만드는 진입 장벽이 되고 있습니다. 더군다나 IE11의 경우도 마찬가지지만 관련 정보나 문서가 너무 적고 그나마도 대부분이 오래된 문서들 뿐이라서 체계적인 학습이 어렵습니다.
Summary 보기
Summary 보기는 메모리 힙 내의 개체들을 Constructor, 즉 생성자 별로 그룹핑해서 보여줍니다. 사실상 IE11의 유형 보기와 동일한 관점에서 접근한다고 볼 수 있을 것입니다.
제공되는 항목들도 사용하는 용어만 다를 뿐 대동소이합니다. 가령 Shallow size 칼럼과 Retained size 칼럼은 각각 앞에서 살펴본 IE11의 크기 칼럼과 유지된 크기 칼럼에 해당하는데, 비율을 포함한 조금 더 상세한 정보를 보여줍니다. 이 두 가지 용어에 관해서는 Chromium 계열의 브라우저의 관점에서 보다 잘 정리된 Google의 다음 문서를 참고하시기 바랍니다.
- Shallow size | 메모리 용어 | Tools for Web Developers | Google Developers
- Retained size | 메모리 용어 | Tools for Web Developers | Google Developers
반면 Distance 칼럼은 IE11에는 존재하지 않던 항목으로, V8 엔진의 GC 루트(사실상 전역 Window 개체)에서부터 계산한 최단 유지 경로(Shortest Retaining Path)의 속성 참조 개수 중 가장 작은 값을 보여줍니다.
또한 특정 생성자로 생성된 인스턴스의 개수는 별도의 칼럼으로 제공되지 않고 인스턴스가 2개 이상인 경우에만 생성자 이름 우측에 x{Count}
와 같은 형태로 나타납니다.
(설명의 편의를 위해 Constructor 칼럼을 기준으로 목록을 다시 정렬했습니다.)
가령 위의 목록을 살펴보면 Window
생성자로 생성된 개체의 인스턴스는 모두 9개가 존재하며 그룹핑된 로우에는 해당 인스턴스들의 Distance 값 중 최소 값인 2
가 나타나 있음을 확인할 수 있습니다.
Google의 문서에서는 동일한 생성자로 생성된 인스턴스들 중 다른 인스턴스들 보다 유독 Distance 값이 큰 개체가 있다면 검토해볼 만한 가치가 있다고 지적합니다.
또한 V8 엔진에는 여러 가지 유형의 GC 루트가 존재하는데 그중 대부분은 엔진 자체의 내부 구현 세부 사항으로 웹 클라이언트 개발자가 직접 접근할 수 없습니다.
따라서 웹 응용 프로그램 개발 환경에서는 전역 Window 개체를 관리 가능한 GC 루트로 간주하고 메모리 힙 스냅샷을 분석하는 것이 적절합니다.
그러나 아쉽게도 말처럼 그렇게 쉽게만 이해가 가능한 것은 아닙니다.
단적으로 이 목록을 가만히 살펴보면 뭔가 이상한 점을 발견할 수 있습니다.
무엇보다도 Window
의 인스턴스가 무려 9개나 됩니다.
뿐만 아니라 Window /
의 인스턴스와 Window / {테스트 주소}
의 인스턴스도 각각 하나씩 존재합니다.
더 재미있는 점은 Chromium Edge를 실행한 직후 어떠한 다른 작업도 수행하지 않고 바로 스냅샷을 찍어보면 Window
의 인스턴스 개수가 더 적고 Window /
의 인스턴스는 아예 존재하지도 않습니다.
그런데 스냅샷을 두 번 이상 찍어보면 대부분 위와 같이 변경된 결과를 얻게 됩니다.
페이지를 대상으로 한 어떠한 추가적인 동작도 수행하지 않았음에도 불구하고 스냅샷의 내용이 변하는 것입니다.
(실제로 생성되는 인스턴스의 개수 등은 자신의 환경에 따라서 거의 확실하게 달라집니다. 단적으로 저 같은 경우에는 업무용 노트북과 개인용 노트북에서 매번 다른 결과를 얻었습니다.)
그렇다면 이제 자연스럽게 한 가지 궁금한 점이 생깁니다.
일반적으로 우리가 생각하는 전역 Window 개체는 과연 저 Window
관련 인스턴스들 중 어떤 개체일까요?
결론부터 말하자면, 잠시 뒤에 Containment 보기를 살펴볼 때 다시 언급하겠지만, Window / {테스트 주소}
의 인스턴스가 바로 전역 Window 개체입니다.
마우스 커서를 특정 개체 위에 가만히 올려보면 다음과 같이 'Preview is not available'이라는 문구가 나타나는 개체가 있습니다. 이런 개체들에서는 Preview 정보를 얻을 수 없습니다.
반면 Window / {테스트 주소}
개체에 마우스 커서를 올리면 다음과 같이 어딘가 익숙한 정보들이 나타납니다.
그리고 다시 Preview 팝업의 document 속성 값 링크 위에 마우스 커서를 올려보면 다음과 같이 렌더링 된 문서(즉 document 개체)가 강조되어 하이라이트 됩니다.
그다지 효율적인 방법은 아니지만 Window / {테스트 주소}
의 인스턴스가 바로 우리가 생각하는 전역 Window 개체라는 간접적인 증거입니다.
참고로 해당 링크를 클릭하면 곧바로 F12 개발자 도구의 Elements 창으로 이동합니다.
또한 (...)
로 표시된 평가가 지연된 속성 값을 클릭하면 그 즉시 평가가 수행되어 결과 값이 제공됩니다.
계속해서 목록의 인스턴스 정보를 가만히 살펴보면 각 항목 우측에 @{일련번호}
형태의 숫자 값과 물음표(??
) 기호가 표시되어 있는 것을 볼 수 있습니다.
짐작하시는 것처럼 이 @{일련번호}
형태의 숫자 값은 고유한 개체 ID값으로 브라우저를 새로 열거나 페이지를 새로 고침 하지 않는 이상 스냅샷을 여러 차례 찍더라도 계속 유지되는 값입니다.
(본문의 경우 여러 차례의 수정과 편집을 거쳐서 작성되기 때문에 캡처된 이미지들 간에 이 값이 종종 달라집니다.)
반면 물음표(??
) 기호의 경우 표시되어 있는 개체도 있고 없는 개체도 있는데 사실 이는 Chromium Edge의 버그로 전역 Window 개체로부터 접근할 수 있는 개체인지 여부를 표시하는 아이콘입니다.
비슷한 개체를 Chrome으로 살펴보면 다음과 같이 정상적으로 아이콘이 표시됩니다.
이 문제는 피드백을 제출한 상태이므로 조만간 수정될 것으로 예상됩니다.
Chromium Edge에서도 Summary 보기 목록에서 실제 인스턴스 중 하나를 마우스로 선택하면 하단의 Retainers 목록에 해당 개체를 참조하고 있는 개체들의 목록이 나타납니다.
가령 다음은 Window / {테스트 주소} @4313
개체를 선택한 결과입니다.
예를 들어서 Retainers 목록의 두 번째 항목을 보면 [4] in Window @4315
개체가 Window / {테스트 주소} @4313
개체를 참조하고 있는 개체들 중 하나임을 알 수 있습니다.
그런데 이 @4315
개체는 Window
생성자로 만들어진 인스턴스들 중 하나이고, 따라서 다시 상단 목록에서 @4315
개체를 확장해보면 다음과 같은 연결을 확인할 수 있습니다.
이처럼 Retainers 목록을 활용하면 스냅샷의 메모리 그래프를 추적해나갈 수 있습니다. 참고로 하단 Retainers 목록의 특정 개체를 상단 목록에서 찾고 싶다면, 수작업으로 스크롤바를 이동해가면서 번거롭게 찾을 필요 없이 Retainers 목록에서 해당 개체를 마우스 오른쪽 버튼으로 클릭한 다음, 메뉴에서 Reveal In Summary view 항목을 선택하면 손쉽게 원하는 개체로 이동할 수 있습니다.
Containment 보기
Containment 보기는 IE11의 루트 보기와 비슷하게 메모리 내 개체들의 참조 관계를 (GC Roots)
를 루트로 하는 계층적 구조로 보여줍니다.
그런데 문제는 이 계층적 구조의 형태 자체가 기본적으로 몇 가지 전제조건을 깔고 있어서 처음 접하는 개발자는 이해하기가 매우 어렵다는 점입니다.
먼저 이 목록을 보면 (GC Roots)
노드 외에도 최상위 노드가 여섯 개나 더 존재합니다.
직접 테스트해보면 약간 다른 결과가 나올 수도 있겠지만 어쨌거나 최상위 노드가 여러 개 존재하는 것은 마찬가지일 것입니다.
또한 각 노드명 앞에 [1] :: {노드명}
, 2 :: {노드명}
등과 같이 번호가 붙여져 있는데 과연 그 의미가 무엇인지, 그리고 대괄호가 있는 번호와 없는 번호의 차이는 무엇인지에 대한 자료를 저도 처음에는 그 어디에서도 찾을 수가 없었습니다.
결국 Chrome DevTools 뉴스 그룹 등을 뒤져서 찾아낸 정보를 정리해보면 다음과 같습니다.
우선 (GC Roots)
노드는 실제로 존재하는 개체가 아닙니다.
(GC Roots)
노드는 유형별로 그룹핑된 실제 GC 루트의 여러 그룹에 접근하기 위해 편의를 목적으로 제공되는 진입점 역할을 하는 가상의 노드입니다.
그리고 그 이외의 다른 모든 최상위 노드들은 (GC Roots)
노드 하위에 존재하는 실제 노드들 중에서 관심을 가질만하다고 생각되는 몇몇 특정 노드에 대한 단축 참조일 뿐입니다.
가령 이번 예제에서 4 :: Window / {테스트 주소} @55541
노드는 실제로는 다음과 같은 경로를 통해서 접근 가능한 노드입니다.
[1] :: (GC Roots) @3
⇒ [13] :: (Global handles) @29
⇒ 87 :: Window / {테스트 주소} @55541
익히 알려진 것처럼 V8 엔진은 C++로 만들어졌고 C++ 개발자들에게 핸들(Handles)은 무척이나 친숙한 개념입니다. 그러나 일반적인 웹 클라이언트 개발자들에게는 혼란을 일으킬 수 있는 사항이라고 생각했는지 전역 Window 개체 등에 바로 접근할 수 있는 최상위 노드를 제공하려고 의도한 것 같다는 개인적인 생각입니다. 비록 낮은 우선순위로 인해 이슈가 현재 상태 그대로 종결되기는 했지만 Chromium 버그 리포트에서도 단축 참조의 존재가 너무 혼란스럽지 않냐는 논의가 진행되었던 것을 확인할 수 있습니다.
또한 각 노드명 앞에 붙어있는 번호는 단지 개발자 도구가 각 개체를 순회하면서 채번되는 일련번호일 뿐입니다.
단 (GC Roots)
노드는 항상 [1]
이며 그 이외의 노드들의 값은 고정되지 않습니다.
다음은 Object 칼럼을 기준으로 목록을 다시 정렬한 모습입니다.
마지막으로 대괄호가 있는 번호와 없는 번호의 차이는 V8 엔진에서 내부적으로 사용되는 개체 형식에 따른 것입니다. 즉 v8::HeapGraphEdge::kElement 형식의 개체를 참조하면 대괄호가 존재하고 v8::HeapGraphEdge::kShortcut 형식의 개체를 참조하면 대괄호 없이 표시되는 식입니다. 그러나 짐작하시는 것처럼 이는 웹 클라이언트 개발자들에게는 거의 아무런 의미도 없는 정보입니다.
확장 프로그램 활성 여부에 따른 메모리 힙 스냅샷 비교
다음은 각각 본문의 예제 페이지를 세 가지 다른 조건에서 실행한 다음, 메모리 힙 스냅샷을 찍고 Containment 보기에서 살펴본 모습입니다. 참고로 Chromium Edge에는 Google 번역 확장 프로그램 하나만 설치되어 있는 상태입니다.
먼저 확장 프로그램을 활성시킨 상태에서 예제 페이지를 새 InPrivate 창으로 실행한 결과입니다. 그러나 대부분의 확장 프로그램이 InPrivate 창 모드에서는 기본적으로 비활성되기 때문에 확장 프로그램이 로드되지 않았습니다.
다음은 확장 프로그램을 비활성시킨 상태로 예제 페이지를 일반적인 새 창에서 실행한 결과입니다. 이 두 가지 조건에서는 확장 프로그램이 비활성되기 때문에 단지 개체 ID만 변경되었을 뿐 스냅샷의 구조에는 큰 변화가 없습니다.
마지막으로 다음은 확장 프로그램을 활성시킨 상태로 일반적인 새 창에서 예제 페이지를 실행한 결과입니다.
확장 프로그램으로 인해 Window / {확장 프로그램 주소}
개체가 추가된 것을 확인할 수 있습니다.
Google 번역 확장 프로그램 같은 경우,
작업 내용에 따라서는 페이지 내부에 다량의 <div>
태그를 생성하기도 하기 때문에 스냅샷 분석 시 부작용을 일으킬 수 있습니다.
이와 같은 확장 프로그램의 영향을 가급적 최소화하기 위해서는 새 InPrivate 창에서 분석 작업을 수행하는 것이 좋습니다.
또한 새 InPrivate 창을 사용하는 경우에도 다음과 같은 확장 프로그램 설정이 존재한다는 점을 염두에 두고 있는 것이 좋습니다.
메모리 힙 스냅샷 분석 실습
메모리 힙 스냅샷에서 <strong>
태그에 대응하는 개체 찾기
본문에서 사용하는 예제 페이지의 마크업 구조를 살펴보면 <body>
태그 내부에 단 하나의 <strong>
태그만 존재합니다.
지금까지 살펴본 IE11과 Chromium Edge의 기능을 활용하여 이 <strong>
태그에 대응하는 개체를 메모리 힙 스냅샷에서 찾아보도록 하겠습니다.
IE11에서 <strong>
태그에 대응하는 개체 찾기
IE11에서는 그다지 어렵지 않게 대응하는 개체를 찾을 수 있습니다.
가장 간단한 방법은 <strong>
태그가 <body>
태그 하위에 존재한다는 당연한 사실을 활용하는 것입니다.
메모리 스냅샷을 찍은 다음, 루트 보기에서 <strong>
태그까지 찾아들어가기만 하면 됩니다.
또는 루트 보기를 통해서 <strong>
태그의 형식이 HTMLPhraseElement라는 사실을 쉽게 알 수 있으므로 유형 보기에서 필터링 기능을 이용해도 됩니다.
이렇게 IE11은 서두에 언급했던 것처럼 웹 개발자가 '기대하는 수준의 정보'를 충분히 '예상할 수 있는 형태'로 보여줍니다. 그러나 Chromium 계열 브라우저들의 사정은 이와는 조금 다릅니다.
Chromium Edge에서 <strong>
태그에 대응하는 개체 찾기
이번에도 방금 IE11에서 접근했던 논리와 비슷한 방식으로 <strong>
태그에 대응하는 개체를 찾아보겠습니다.
Containment 보기에서 <strong>
태그까지 찾아서 들어가 봅니다.
결론부터 말하자면 저는 <strong>
태그를 찾는데 실패했습니다.
한참을 뒤진 끝에 HTMLBodyElement
개체까지는 찾았지만 그래도 정작 <strong>
태그에 대응하는 개체는 찾을 수가 없었습니다.
애초에 올바른 HTMLBodyElement
개체를 찾은 건지도 확신할 수 없습니다.
가령 위의 트리 구조에서 [10]
번과 [23]
번은 개체 ID까지 동일한 같은 HTMLHtmlElement
개체로 보입니다.
그러면 이번에는 Summary 보기에서 생성자 이름을 이용해서 필터링하는 두 번째 방법으로 접근해보겠습니다.
그런데 여러분은 Chromium 계열의 브라우저에서 사용하는 <strong>
태그의 생성자 이름이 무엇인지 알고 계십니까?
다음은 정확한 생성자 이름을 모르기 때문에 일단 접두어 HTML
로 필터링을 건 모습니다.
특정 태그와 명확한 연결성을 가진 생성자 이름 외에 범용적인 생성자 이름들을 하이라이트 하여 표시해 놓았습니다.
그러나 결론부터 말하자면 이번에도 저는 <strong>
태그를 찾는데 실패했습니다.
결국 어쩔 수 없이 <strong>
태그의 생성자 이름은 무엇인지, 관련된 조그만 단서라도 얻을 수 없는지 궁리해봐야만 했습니다.
그래서 제가 생각해낸 가장 간단한 방법은 바로 JavaScript를 이용하는 것입니다.
F12 개발자 도구에서 ESC 키를 사용하면 손쉽게 Console 창을 토글 할 수 있습니다.
Memory 창 하단에 Console 창이 나타나 있지 않다면 ESC 키를 눌러서 Console 창을 엽니다.
그리고 다음과 같은 명령들을 입력합니다.
단서를 한 가지 얻었습니다(사실은 만들어냈습니다).
HTMLCollection
개체를 찾아보는 것이 도움이 될 것 같습니다.
두 번째 스냅샷을 찍고 HTMLCollection
개체들을 살펴보겠습니다.
드디어 <strong>
태그에 접근할 수 있는 개체를 찾았습니다.
팝업에 가려져서 노드들이 잘 안보이기는 하지만 이 결과를 자세히 살펴보면 실제 <strong>
태그의 생성자는 HTMLElement
이고 @75939
임을 알 수 있습니다.
따라서 이제 HTMLElement
개체들을 집중적으로 살펴보겠습니다.
그리고 그 결과는 다음과 같습니다.
이 개체가 우리가 찾는 대상이 확실함을 알 수 있습니다. 그런데 이 결과가 만족스러우신가요? 실습을 마무리하기 전에 Snapshot 1과 Snapshot 2를 한 번 더 비교해보도록 하겠습니다.
비록 HTMLCollection
개체를 단초로 <strong>
태그에 대응하는 개체를 찾기는 했지만 <strong>
태그의 실제 형식은 HTMLElement
입니다.
따라서 스냅샷끼리 비교해보려면 HTMLElement
개체들을 비교하는 것이 적합할 것입니다.
다음은 이해하기 쉽도록 Snapshot 1과 Snapshot 2의 HTMLElement
개체들을 비교하기 편하게 정리한 결과입니다.
좌측이 Snapshot 1이고 우측이 Snapshot 2입니다.
기존 개체가 하나 사라졌고, 새로운 개체가 여섯 개 생성되었음을 알 수 있습니다.
그리고 생성된 개체들 중 하나가 바로 그렇게 찾고 있던 <strong>
태그에 대응하는 개체입니다.
결국 우리는 Snapshot 1에는 존재하지도 않던 개체를 찾아 헤맸던 것입니다.
또한 예상했던 것보다 훨씬 더 많은 개체들이 생성되었지만 제 역량으로는 각 개체들의 역할이나 생성된 이유는 파악할 수 없었습니다.
이를 이해하기 위해서는 V8 엔진 자체에 대한 깊은 이해가 선행되어야 할 것으로 보입니다.
이 결과로부터 추측해보건대 Chromium 계열의 브라우저는 페이지가 로딩된 직후 모든 DOM 요소를 JavaScript의 메모리 힙에 올리지는 않는 것으로 보입니다. V8 엔진이 해당 DOM 요소에 접근하고 난 이후에야 메모리 힙에서도 접근이 가능해집니다. 뿐만 아니라 일단 생성된 개체도 상황에 따라 계속 다른 개체로 대체되는데 이 현상에 대해서는 다음 글에서 다시 살펴보겠습니다.
노트
이번 섹션에서 살펴본 내용 중 스냅샷끼리 비교하는 작업은 다음 글에서 살펴볼 Comparison 보기를 사용하는 것이 더 편리합니다. 그러나 아직 Comparison 보기를 설명하지 않았기 때문에 수작업으로 내용을 진행했습니다.
정리
본문에서는 IE11 및 Chromium Edge의 F12 개발자 도구를 활용하여 메모리 힙 스냅샷을 찍는 기초적인 방법과 여러가지 관점에서 스냅샷을 살펴볼 수 있는 다양한 보기들, 그리고 사용되는 용어들에 관해서 알아봤습니다. IE11의 메모리 창은 간단명료한 UI와 초보 개발자도 접근하기 쉬운 기능을 제공합니다. 반면 Chromium Edge의 메모리 창은 강력한 기능과 대량의 정보를 제공하지만 V8 엔진이 동작하는 방식을 이해하지 않으면 활용하기가 어렵습니다.
이어지는 글에서는 IE11의 비교 보기와 Chromium Edge의 Comparison 보기에 관해서 알아보고 그 활용 방법을 살펴보도록 하겠습니다.
- IE11을 이용한 JavaScript 디버깅 01, F12 개발자 도구 콘솔 창 2019-01-01 08:00
- IE11을 이용한 JavaScript 디버깅 02, console 개체 2019-01-08 08:00
- IE11을 이용한 JavaScript 디버깅 03, F12 개발자 도구 DOM 탐색기 창 Part 1 2019-01-15 08:00
- IE11을 이용한 JavaScript 디버깅 04, F12 개발자 도구 DOM 탐색기 창 Part 2 2019-01-22 08:00
- IE11을 이용한 JavaScript 디버깅 05, F12 개발자 도구 디버거 창 Part 1 2019-01-29 08:00
- IE11을 이용한 JavaScript 디버깅 06, F12 개발자 도구 디버거 창 Part 2 2019-02-12 08:00
- IE11을 이용한 JavaScript 디버깅 07, F12 개발자 도구 디버거 창 Part 3 2019-02-19 08:00
- IE11을 이용한 JavaScript 디버깅 08, F12 개발자 도구 디버거 창 Part 4 2019-02-26 08:00
- IE11을 이용한 JavaScript 디버깅 09, F12 개발자 도구 디버거 창 Part 5 2019-03-05 08:00
- IE11을 이용한 JavaScript 디버깅 10, F12 개발자 도구 네트워크 창 Part 1 2019-03-19 08:00
- IE11 및 Chromium Edge에서 메모리 힙 스냅샷 찍기 2020-02-25 08:00
- IE11 및 Chromium Edge에서 메모리 힙 스냅샷 비교하기 2020-03-10 08:00
- Chromium Edge의 메모리 힙 스냅샷 분석을 위한 V8 엔진의 이해 1. 2020-03-24 08:00
- Chromium Edge의 메모리 힙 스냅샷 분석을 위한 V8 엔진의 이해 2. 2020-04-07 08:00
- Visual Studio Community로 V8 엔진 다운로드 및 빌드하고 간단히 살펴보기 2020-04-21 08:00
- IE11 및 Chromium Edge에서 가상의 메모리 누수 상황 재현 및 해결하기 2020-05-05 08:00