Visual Studio Community로 V8 엔진 다운로드 및 빌드하고 간단히 살펴보기

등록일시: 2020-04-21 08:00,  수정일시: 2020-04-21 08:00
조회수: 9,674

본문의 주제를 처음 준비하던 2018년 후반만 해도 본문은 'IE11을 이용한 JavaScript 디버깅' 시리즈의 일부분으로 구성되어 몇 편에 걸쳐 나뉘어 제공될 예정이었습니다. 그러나 공교롭게도 거의 동일한 시기에 Microsoft가 Chromium Edge에 대한 구체적인 계획을 발표함으로써 시리즈 자체의 필요성이 크게 줄어들었고, 결과적으로 다루고자 계획했던 내용을 끝까지 진행하지 못한 체 시리즈는 사실상 중단된 상태입니다.

비록 'IE11을 이용한 JavaScript 디버깅' 시리즈의 내용이 IE11F12 개발자 도구를 활용하는 방법에 중점을 두고 있기는 하지만, 최종 목표는 단순히 그에 그치지 않고 상대적으로 UI 및 기능이 단순한 IE11F12 개발자 도구에 친숙해진 다음, 거의 동일한 UI와 기능을 제공하지만 대폭 개선된 Classic Edge를 다루고, 또다시 보다 풍부한 기능을 제공하는 Chrome 등의 최신 브라우저로 접근을 확대해나가는 것이었습니다. 그러나 중간 징검다리 역할을 해줄 Classic Edge의 존재 위치가 불확실해짐에 따라 시리즈의 전반적인 맥락이 끊어졌다는 판단이었습니다.

드디어 2020년 초인 현재, 정식으로 배포되기 시작한 Microsoft의 Chromium Edge는 나름대로 순조롭게 시장에 안착하고 있는 것으로 보입니다. F12 개발자 도구라는 관점에서만 본다면 Chrome과 거의 동일한 UI 및 기능을 제공하고 있어서 기본적인 수준에서는 둘 중 어떤 브라우저를 선택하더라도 별다른 차이가 없을 듯합니다. 개인적으로 과거 IE11/Classic Edge 계열과 Chrome 계열로 나눴던 F12 개발자 도구 그룹을 이제는 IE11 계열과 Chromium Edge/Chrome 계열로 분류해도 무방할 것 같습니다. (FireFox까지 다루고 싶은 욕심도 있었지만 현실적으로 제 역량으로는 무리라고 생각되어 본문에서는 다루지 않습니다.)

본문에서는 IE11Chromium EdgeF12 개발자 도구를 활용한 메모리 힙 스냅샷의 비교/분석을 통해 웹 브라우저의 메모리 누수를 감지하는 가장 기본적인 방법을 살펴봅니다.

시리즈 목차

문서 목차

들어가기 전에

본문에서는 Windows 환경에서 V8 엔진의 소스를 다운로드 받고 Visual Studio Community 2019를 사용하여 빌드하는 방법을 살펴봅니다. 그리고 그 결과물 중 하나로 얻어지는 V8 엔진의 디버그 쉘인 D8을 이용해서 JavaScript 코드를 분석하는 방법을 간단히 알아보도록 하겠습니다. 본문의 내용은 Windows 환경에서 Chromium을 빌드하는 방법을 설명하고 있는 다음 문서의 내용을 V8 엔진을 대상으로 적용하여 간추린 결과를 담고 있습니다. 보다 자세한 정보는 이 문서를 참고하시기 바랍니다.

반면 Windows 환경에만 국한되지 않고 Linux 환경이나 macOS 환경을 포괄하는 보다 근본적인 정보가 필요하다면 다음 링크의 문서들을 참고하시기 바랍니다. 바로 위의 문서도 다름 아닌 아래의 문서들을 탐색하여 얻어진 결과입니다.

본문은 D8을 활용하는 방법에는 관심을 갖고 있으나, V8 엔진의 세부적인 코드 구현에는 신경 쓰고 싶지 않은, 다음과 같은 조건에 해당하는 웹 클라이언트 개발자를 주로 염두에 두고 작성된 문서입니다.

  • JavaScript로 작성된 코드의 이해에 문제가 없고 직접 작성할 수 있습니다.
  • 분산 버전 관리 시스템인 Git을 능숙하게 다루지는 못하지만, Git이 무엇인지, 어떤 용도로 사용되는지 알고 있습니다.
  • Visual Studio 또는 비슷한 IDE를 사용해본 경험이 있거나 최소한 지시를 따라 하는데 어려움이 없습니다.

비록 이 조건을 만족하지 않더라도 내용을 따라 시도해 볼 수는 있지만 예상하지 못한 문제가 발생했을 때 유연한 대처가 어려울 수 있음을 감안하시기 바랍니다. 경우에 따라서는 진행 도중 작업을 포기하고 취소하기엔 이미 너무 많은 노력을 쏟았을 수도 있습니다. V8 엔진의 개발에 사용되는 프로그래밍 언어인 C/C++를 알고 있어야 할 필요는 없습니다. 저 역시 전문적인 C/C++ 개발자는 아닙니다. V8 엔진의 오픈 소스 프로젝트에 기여하는 방법이나 그와 관련된 정보도 여기에서는 다루지 않습니다. 이 주제와 관련된 더 자세한 정보는 다음 링크의 문서를 참고하시기 바랍니다.

본문의 과정은 어렵다기보다는 시간이 오래 걸리고 지루한 유형의 작업입니다. 네트워크 전송 속도 및 작업 머신의 성능에 따라서 모든 과정을 마치기까지 반나절 이상 걸리는 경우도 있으며, HDD 공간도 100GB 이상 넉넉하게 준비된 상태에서 작업을 시작하는 것이 좋습니다. 참고로 본문은 Hyper-V에 새로운 가상 컴퓨터를 구성하고 기본 운영 체제만 설치한 상태에서 지금부터 설명할 내용과 동일한 과정의 검토를 통해서 작성되었습니다.

V8 엔진 다운로드 및 빌드하기

단계 1: Visual Studio 및 Windows 10 SDK 설치하기

가장 먼저 해야 할 일은 Visual StudioWindows 10 SDK를 설치하는 것입니다.

만약 Visual Studio의 다양한 상업용 에디션에 대한 적절한 라이선스를 보유하고 있지 않다면 일정 조건 하에서 무료로 사용 가능Visual Studio Community 에디션을 사용해도 무방합니다. 특히 이 에디션은 개인 사용자에게는 완벽하게 무료나 마찬가지이기 때문에 지금과 같은 상황에 매우 적합합니다. 다음 링크에서 최신 설치 프로그램을 다운로드 받을 수 있습니다.

만약 이미 Visual Studio를 설치하여 사용하고 있는 중이라면 버전 요구 사양을 만족하는지 확인하시기 바랍니다. 본문을 시작하면서 언급했던 문서에 지정된 최소 버전은 Visual Studio 2017 15.7.2 이상, 권장 버전은 Visual Studio 2019 16.0.0 이상입니다. Windows 10 SDKVisual Studio를 설치하면서 함께 설치할 수 있기 때문에 별도로 다운로드 받을 필요가 없습니다. 문서에 지정된 Windows 10 SDK의 최소 버전은 10.0.18362입니다.

다운로드 받은 Visual Studio Installer 프로그램을 실행하고 설치 프로그램이 지시하는 대로 과정을 진행하면 다음과 같이 설치할 워크로드를 선택하는 단계가 나타납니다. 이 단계에서 다음과 같이 'C++를 사용한 데스크톱 개발' 워크로드를 선택하면 필요한 모든 구성 요소를 선택한 셈이 됩니다.

Visual Studio Community 2019 설치 - 워크로드

가령 이 상태에서 화면 상단의 개별 구성 요소 탭을 선택한 다음, 스크롤을 가장 아래로 내려보면 다음과 같이 적절한 버전의 Windows 10 SDK가 함께 선택되어 있는 것을 확인할 수 있습니다. 이미 Visual Studio를 설치하여 사용 중인 경우에도 시작 메뉴에서 Visual Studio Installer 프로그램을 실행하여 비슷한 방법으로 추가적인 워크로드개별 구성 요소를 설치할 수 있습니다.

Visual Studio Community 2019 설치 - 개별 구성 요소

마지막으로 설치 버튼을 클릭하면 본격적인 설치가 시작되며 사용 중인 환경에 따라서 약 20분에서 30분 정도가 소요됩니다. 이때 가급적 기본 설치 위치는 변경하지 않는 것을 추천드립니다. 모든 설치가 마무리되고 나면 일단 Visual StudioVisual Studio Installer 프로그램을 닫습니다.

Debugging Tools for Windows 설치하기

아직 설치해야 하는 Windows 10 SDK 관련 하위 도구가 한 가지 더 남아 있습니다. Debugging Tools for Windows가 바로 그것으로 이 도구를 설치하지 않으면 다음 단계에서 오류가 발생하게 됩니다. 이 도구는 다음 단계에 필요한 디버그 인터페이스 액세스 SDK(DIA SDK) 관련 파일을 포함하고 있습니다.

제어판프로그램 및 기능 목록에서 Windows Software Development Kit 항목을 선택한 다음, 변경 버튼을 선택합니다.

Debugging Tools for Windows 설치 - 프로그램 및 기능

그러면 다음과 같은 대화 상자가 나타나는데, Change 항목이 선택된 기본 상태 그대로 Next 버튼을 클릭합니다.

Debugging Tools for Windows 설치 - 설치 변경 대화 상자

그러면 다시 다음과 같은 대화 상자가 나타납니다. Debugging Tools for Windows 항목을 선택하고 Change 버튼을 클릭하면 설치가 시작됩니다.

Debugging Tools for Windows 설치 - 설치 변경 대화 상자

잠시 후 설치가 마무리되면 Close 버튼을 클릭해서 대화 상자를 닫습니다.

물론 Windows 10 SDKVisual Studio와 상관없이 별도로 다운로드 받아서 설치할 수도 있습니다. 다만 본문에서는 이런 형태의 개발 도구에 익숙하지 않은 웹 클라이언트 개발자들을 위해 가장 간단한 설치 방법만 살펴봤습니다. 만약 필요하다면 다음 링크에서 Windows 10 SDK를 비롯한 기타 개발 도구를 직접 다운로드 받을 수도 있습니다.

단계 2: depot_tools 설치하기

이제 본격적으로 V8 엔진을 다운로드 받고 빌드하기 위한 준비를 수행할 차례입니다. 다음 링크를 클릭해서 Chromium 관련 소스 코드 저장소 및 개발 프로세스와의 상호 작용을 관리하는 depot_tools 스크립트 패키지를 다운로드 받습니다.

그리고 적절한 위치에 다운로드 받은 파일의 압축을 풉니다. 가령 본문에서는 다음과 같이 v8_engine이라는 이름으로 새로운 폴더를 생성하고 그 하위에 압축을 풀었습니다. 가급적 폴더 이름에 빈 문자열이나 한글을 사용하지 않는 것을 권장합니다. Windows의 사용자 이름이 한글인 경우에도 문제가 발생할 수 있습니다. 매우 사소하지만 원인을 알 수 없는 예상하지 못한 오류를 미연에 방지할 수 있는 가장 기본적인 습관입니다.

C:\v8_engine\depot_tools

그런 다음 이 경로를 다음과 같이 PATH 환경 변수의 가장 앞부분에 추가합니다. 만약 Administrator 권한을 갖고 있다면 가급적 시스템 변수 영역에 추가하는 것이 안전합니다. 아예 사용자 변수 영역의 PATH에 추가할 수 없는 것은 아니지만, 이 경우 동일한 파일 이름을 가진 프로그램의 호출 순서를 완벽하게 제어할 수 없다는 문제점이 존재합니다.

depot_tools 설치 - 환경 변수 PATH 추가

가령 본문의 테스트 환경은 완벽하게 새로 설치한 가상 컴퓨터에서 작업이 수행되기 때문에 그렇지 않지만, 사용 중인 환경에 이미 별도로 Python이나 Git이 설치되어 있을 수도 있습니다. 따라서 시스템 변수 영역에 이미 해당 프로그램의 경로가 추가되어 있다면, 운이 나쁜 경우 사용자 변수 영역에 PATH를 구성하더라도 depot_tools에 포함되어 있는 해당 프로그램 대신 기존에 설치된 프로그램이 실행될 가능성이 존재합니다. 그렇게 되면 이후의 과정에서 오류가 발생할 확률이 아주 높아집니다.

비슷한 방식으로 DEPOT_TOOLS_WIN_TOOLCHAIN이라는 환경 변수의 값을 0으로 지정하여 시스템 변수 영역에 추가합니다. 이 환경 변수는 depot_tools에게 로컬에 설치된 Visual Studio를 사용하도록 지시합니다(기본적으로 depot_tools는 Google 내부 버전을 사용하려고 시도합니다).

depot_tools 설치 - 환경 변수 DEPOT_TOOLS_WIN_TOOLCHAIN 추가

두 가지 환경 변수의 구성을 마쳤으면 명령 프롬프트(cmd.exe)를 열고 아무런 인자 없이 gclient를 실행합니다. 반드시 이 작업은 일반적으로 'DOS 창'이라고 부르는 명령 프롬프트에서 수행해야 한다는 점에 주의하시기 바랍니다. PowerShell이나 cygwin 등의 다른 CLI에서 이 작업을 수행할 경우, 겉으로 보기에는 정상적으로 처리되는 것처럼 보이지만 msysgit이나 python 같은 도구들이 올바르게 설치되지 않을 수 있습니다. 이처럼 gclient는 최초 실행 시 msysgitpython을 비롯해서 작업에 필요한 모든 Windows 관련 구성 요소를 설치합니다. "Downloading CIPD client for windows-amd-64 form http://..."로 시작하는 메시지가 나타난 다음, 한동안 명령 프롬프트 창이 멈춰있는 것처럼 보이지만 약 3분 정도 기다리면 수행이 완료됩니다.

C:\v8_engine\depot_tools>gclient

다음은 정상적으로 명령이 완료된 명령 프롬프트 창을 보여줍니다.

depot_tools 설치 - gclient 최초 실행

마지막으로 명령 프롬프트에 where python라고 명령어를 입력하여 depot_tools 폴더 하위의 python.bat 파일이 목록의 가장 상단에 나타나는지 확인합니다.

C:\v8_engine\depot_tools>where python

즉 다음과 비슷하게 나타나야 합니다. 만약 기존에 이미 별도로 Python을 설치한 적이 있다면 목록의 내용은 이와 다를 수 있지만 첫 번째 항목이 python.bat 파일이어야 합니다.

depot_tools 설치 - where python 실행

단계 3: V8 엔진 코드 가져오기

드디어 실제로 V8 엔진의 소스 코드를 가져오는 단계입니다. 그전에 먼저 명령 프롬프트에 다음과 같은 명령들을 입력하여 Git의 기본 환경을 설정합니다.

git config --global user.name "[user name]"
git config --global user.email "[email address]"
git config --global core.autocrlf false
git config --global core.filemode false
git config --global branch.autosetuprebase always

그런 다음, V8 엔진의 소스를 다운로드 받을 폴더를 생성하고 해당 폴더로 이동합니다. 가령 본문에서는 v8_engine 폴더 하위에 source라는 이름의 새로운 폴더를 생성했습니다. 결과적으로 현재 관련된 주요 폴더들의 구조는 다음과 같게 됩니다.

C:\v8_engine\depot_tools
            \source

이제 새 폴더에서 다음 명령을 입력하면 소스 다운로드가 시작됩니다.

C:\v8_engine\source>fetch v8

이때 실수로라도 fetch v8 명령 대신 fetch chromium 명령을 입력하지 않도록 주의하시기 바랍니다. V8 엔진의 소스를 다운로드 받는데 걸리는 시간만 계산해도 넉넉잡아 10분 이상이 필요합니다. 네트워크 환경에 따라서 다르지만 Chromium의 전체 소스를 다운로드 받는데 걸리는 시간은 분 단위보다는 시간 단위에 가깝습니다.

만약 다음과 비슷한 오류가 발생한다면 Debugging Tools for Windows의 설치 과정을 빼먹었거나 정상적으로 설치가 마무리되지 않은 것입니다.

... 생략 ...

Downloading https://commondatastorage.googleapis.com/chromium-browser-clang/Win/clang-n345635-5d881dd8-1.tgz .......... Done.
Copying C:\Program Files (x86)/Microsoft Visual Studio/2019/Community\DIA SDK\bin\amd64\msdia140.dll to C:\v8_engine\source\v8\third_party\llvm-build\Release+Asserts\bin
Traceback (most recent call last):
  File "v8/tools/clang/scripts/update.py", line 383, in <module>
    sys.exit(main())
  File "v8/tools/clang/scripts/update.py", line 379, in main
    return UpdatePackage(args.package)
  File "v8/tools/clang/scripts/update.py", line 313, in UpdatePackage
    CopyDiaDllTo(os.path.join(LLVM_BUILD_DIR, 'bin'))
  File "v8/tools/clang/scripts/update.py", line 248, in CopyDiaDllTo
    CopyFile(dia_dll, target_dir)
  File "v8/tools/clang/scripts/update.py", line 242, in CopyFile
    shutil.copy(src, dst)
  File "C:\v8_engine\depot_tools\bootstrap-3_8_0_chromium_8_bin\python\bin\Lib\shutil.py", line 139, in copy
    copyfile(src, dst)
  File "C:\v8_engine\depot_tools\bootstrap-3_8_0_chromium_8_bin\python\bin\Lib\shutil.py", line 96, in copyfile
    with open(src, 'rb') as fsrc:
IOError: [Errno 2] No such file or directory: 'C:\\Program Files (x86)/Microsoft Visual Studio/2019/Community\\DIA SDK\\bin\\amd64\\msdia140.dll'
Error: Command 'vpython.bat v8/tools/clang/scripts/update.py' returned non-zero exit status 1 in C:\v8_engine\source
Hook 'vpython.bat v8/tools/clang/scripts/update.py' took 117.71 secs
Subprocess failed with return code 2.

이와 같이 오류가 발생하는 경우 외에도 다양한 이유로 인해서 소스를 처음부터 다시 받고 싶다면 그냥 소스 폴더 전체를 삭제하고 재생성한 다음, fetch v8 명령을 다시 수행하면 됩니다.

다음은 정상적으로 V8 엔진의 소스를 받은 상태의 명령 프롬프트 창을 보여줍니다.

V8 엔진 코드 가져오기 - fetch v8 실행

단계 4: Visual Studio 솔루션 파일 생성하기

이전 섹션의 작업을 모두 마치고 나면 source 폴더 하위에 몇 개의 폴더 및 파일과 함께 v8이라는 폴더가 생성됩니다. 바로 이 v8 폴더에 V8 엔진의 소스가 위치하게 됩니다. 그러나 Visual Studio Community 2019를 사용하여 이 소스를 빌드하려면 우선 빌드 디렉터리를 만들고 솔루션 파일을 생성해야 합니다.

명령 프롬프트에서 v8 폴더로 이동한 다음, 다음과 같이 명령을 입력합니다.

C:\v8_engine\source\v8>gn gen --ide=vs out\default

다음은 이 명령이 정상적으로 실행된 명령 프롬프트 창을 보여줍니다.

Visual Studio 솔루션 파일 생성하기 - gn gen --ide=vs out\default 실행

파일 탐색기에서 빌드 디렉터리(C:\v8_engine\source\v8\out\default)를 살펴보면 다음과 같이 all.sln이라는 이름으로 Visual Studio의 솔루션 파일이 생성된 것을 확인할 수 있습니다.

Visual Studio 솔루션 파일 생성하기 - all.sln 파일

단계 5: V8 엔진 빌드하기

이제 실제로 소스를 빌드하기만 하면 됩니다. Visual Studio Community 2019를 실행하고 all.sln 파일을 열면 다음과 같이 모두 137개의 프로젝트가 로딩됩니다.

V8 엔진 빌드하기 - 솔루션 탐색기

이 상태에서 솔루션 탐색기에서 솔루션을 마우스 오른쪽 버튼으로 클릭한 다음, 컨텍스트 메뉴에서 솔루션 빌드를 선택하거나 또는 Ctrl+Shift+B 단축키를 누르면 빌드가 시작됩니다. 빌드가 완료되려면 만찬을 즐기고 와도 될 정도로 상당히 많은 시간이 걸리므로 느긋한 마음으로 기다리시기 바랍니다.

V8 엔진 빌드하기 - 솔루션 빌드 실행

다음은 정상적으로 모든 프로젝트의 빌드가 완료된 Visual Studio출력 창 메시지를 보여줍니다. 본문의 가상 컴퓨터 환경에서는 최초 빌드에 3시간이 넘게 걸렸습니다. 일반적인 개발자 노트북 환경에서도 기본적으로 1시간 이상은 걸린다고 간주하시는 것이 좋습니다.

V8 엔진 빌드하기 - 솔루션 빌드 완료

다시 파일 탐색기에서 빌드 디렉터리를 살펴보면 다음과 같이 v8.dll, d8.exe 등의 파일이 정상적으로 생성된 것을 확인할 수 있습니다.

V8 엔진 빌드하기 - v8.dll 파일 외

V8 엔진 빌드하기 - d8.exe 파일 외

D8 디버그 쉘을 이용한 JavaScript 코드 분석

기본 사용 방법

이제 D8을 사용하기 위한 모든 준비가 마무리되었습니다. 명령 프롬프트 창을 열고 빌드 디렉터리로 이동한 다음 아무런 인자도 지정하지 않고 그냥 d8.exe를 실행해봅니다. 그러면 다음과 같이 현재 사용 중인 V8 엔진의 버전 정보가 출력되고 디버그 쉘 모드로 전환됩니다.

C:\v8_engine\source\v8\out\default>d8.exe
V8 version 8.4.0 (candidate)
d8>

이 상태에서 브라우저의 F12 개발자 도구가 제공하는 Console 창을 사용할 때처럼 디버그 쉘에 직접 간단한 JavaScript 코드를 입력할 수 있습니다. 일부 문서에서는 디버그 쉘 모드에서 console 개체가 지원되지 않기 때문에 값을 출력하기 위해서는 console.log() 등의 메서드 대신 print() 함수를 사용해야 한다고 설명하기도 하지만 막상 테스트해보면 문제없이 잘 동작합니다. 또한 매번 코드를 한 줄씩 실행할 때마다 undefined가 출력되는 것을 볼 수 있는데 이미 다른 문서에서 언급했던 것처럼 이는 기본 반환값으로 잘못된 동작이 아닙니다.

C:\v8_engine\source\v8\out\default>d8.exe
V8 version 8.4.0 (candidate)
d8> var i = 10;
undefined
d8> var j = 20;
undefined
d8> console.log(i + j);
30
undefined
d8>

지금처럼 독립적으로 실행 중인 V8 엔진 환경에는 브라우저 환경의 전역 Window 개체나 DOM 요소 등이 당연히 존재하지 않으므로 이에 접근하려고 시도하면 다음과 같이 오류가 발생하게 됩니다.

C:\v8_engine\source\v8\out\default>d8.exe
V8 version 8.4.0 (candidate)
d8> var i = 10;
undefined
d8> var j = 20;
undefined
d8> console.log(i + j);
30
undefined
d8> console.log(window.location.href);
(d8):1: ReferenceError: window is not defined
console.log(window.location.href);
            ^
ReferenceError: window is not defined
    at (d8):1:13

d8>

디버그 쉘을 종료하려면 Ctrl+C 키를 누르면 됩니다.

그러나 일반적으로는 방금 살펴본 것처럼 디버그 쉘의 프롬프트에 코드를 직접 입력하기 보다는 미리 작성한 JavaScript 파일을 지정하여 코드를 실행하는 경우가 더 많을 것입니다. 가령 다음과 같은 코드가 이미 d8_greetings.js라는 파일로 작성되어 있다고 가정해보겠습니다. (새 창에서 보기)

function greet(name) {
  return "Hello, " + name;
}

console.log(greet("John"));

이 파일의 코드를 디버그 쉘로 테스트할 수 있는 방법은 크게 두 가지입니다. 먼저 첫 번째 방법은 디버그 쉘을 실행한 다음, 아래와 같이 load() 함수를 활용하는 것입니다. load() 함수는 매개 변수로 지정한 파일의 코드를 로딩하고 실행합니다.

C:\v8_engine\source\v8\out\default>d8.exe
V8 version 8.4.0 (candidate)
d8> load("d8_greetings.js");
Hello, John
undefined
d8>

이 방법의 장점은 이후 로딩한 파일에 정의되어 있는 모든 구성 요소에 자유롭게 접근이 가능하다는 점입니다.

C:\v8_engine\source\v8\out\default>d8.exe
V8 version 8.4.0 (candidate)
d8> load("d8_greetings.js");
Hello, John
undefined
d8> greet("SONG");
"Hello, SONG"
d8> greet("PARK");
"Hello, PARK"

두 번째 방법은 d8.exe를 실행할 때 파일 이름을 인자로 전달하는 것입니다. 간단하지만 단 한 번만 실행됩니다.

C:\v8_engine\source\v8\out\default>d8.exe d8_greetings.js
Hello, John

디버그 쉘을 실행할 때 다양한 옵션 플래그를 함께 지정할 수도 있습니다. d8.exe의 전체 옵션 플래그 목록은 다음 명령을 입력하여 확인할 수 있습니다.

C:\v8_engine\source\v8\out\default>d8.exe --help

... or ...

C:\v8_engine\source\v8\out\default>d8.exe --help | more

그러나 지원되는 옵션 플래그 목록의 전체 분량이 너무 많기 때문에 명령 프롬프트 창으로는 자세히 살펴보기가 어렵습니다. 대신 다음과 같이 명령을 입력하여 파일로 저장한 다음 살펴보는 것이 편리합니다. (새 창에서 보기)

C:\v8_engine\source\v8\out\default>d8.exe --help >> d8_flags_list.txt

가령 Ignition 인터프리터가 생성하는 바이트 코드를 살펴보려면 다음과 같이 --print-bytecode 옵션 플래그를 지정하면 됩니다. 다만 디버그 쉘 진입 이후 첫 번째 코드를 실행할 때는 기본적으로 대량의 바이트 코드가 생성되므로 의미 없는 엔터 키를 한 번 눌러주는 것이, 이후에 입력하는 각각의 코드에 대응하는 바이트 코드를 살펴보기에 좋습니다.

c:\v8_engine\source\v8\out\default>d8.exe --print-bytecode
V8 version 8.4.0 (candidate)
d8>{↩}
[generated bytecode for function:  (0x026d081cfbfd <SharedFunctionInfo>)]
Parameter count 1
Register count 0
Frame size 0
         0000026D081CFC4E @    0 : 0d                LdaUndefined
         0000026D081CFC4F @    1 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

... 대량의 바이트 코드 ...

undefined
d8>

그런 다음 한 줄씩 코드를 입력해보면서 각 코드에 대한 바이트코드를 검토해 볼 수 있습니다.

c:\v8_engine\source\v8\out\default>d8.exe --print-bytecode
V8 version 8.4.0 (candidate)
d8>{↩}
[generated bytecode for function:  (0x026d081cfbfd <SharedFunctionInfo>)]
Parameter count 1
Register count 0
Frame size 0
         0000026D081CFC4E @    0 : 0d                LdaUndefined
         0000026D081CFC4F @    1 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

... 대량의 바이트 코드 ...

undefined
d8> var i = 10;
[generated bytecode for function:  (0x026d081d09e5 <SharedFunctionInfo>)]
Parameter count 1
Register count 3
Frame size 24
         0000026D081D0A52 @    0 : 12 00             LdaConstant [0]
         0000026D081D0A54 @    2 : 26 fa             Star r1
         0000026D081D0A56 @    4 : 27 fe f9          Mov <closure>, r2
         0000026D081D0A59 @    7 : 61 37 01 fa 02    CallRuntime [DeclareGlobals], r1-r2
         0000026D081D0A5E @   12 : 0c 0a             LdaSmi [10]
         0000026D081D0A60 @   14 : 15 01 00          StaGlobal [1], [0]
         0000026D081D0A63 @   17 : 0d                LdaUndefined
         0000026D081D0A64 @   18 : aa                Return
Constant pool (size = 2)
0000026D081D0A21: [FixedArray] in OldSpace
 - map: 0x026d080404b1 <Map>
 - length: 2
           0: 0x026d081d0a0d <FixedArray[1]>
           1: 0x026d0808a3b1 <String[#1]: i>
Handler Table (size = 0)
Source Position Table (size = 0)
undefined
d8> var j = 20;
[generated bytecode for function:  (0x026d081d0b39 <SharedFunctionInfo>)]
Parameter count 1
Register count 3
Frame size 24
         0000026D081D0BA6 @    0 : 12 00             LdaConstant [0]
         0000026D081D0BA8 @    2 : 26 fa             Star r1
         0000026D081D0BAA @    4 : 27 fe f9          Mov <closure>, r2
         0000026D081D0BAD @    7 : 61 37 01 fa 02    CallRuntime [DeclareGlobals], r1-r2
         0000026D081D0BB2 @   12 : 0c 14             LdaSmi [20]
         0000026D081D0BB4 @   14 : 15 01 00          StaGlobal [1], [0]
         0000026D081D0BB7 @   17 : 0d                LdaUndefined
         0000026D081D0BB8 @   18 : aa                Return
Constant pool (size = 2)
0000026D081D0B75: [FixedArray] in OldSpace
 - map: 0x026d080404b1 <Map>
 - length: 2
           0: 0x026d081d0b61 <FixedArray[1]>
           1: 0x026d081d0b05 <String[#1]: j>
Handler Table (size = 0)
Source Position Table (size = 0)
undefined
d8>

물론 다음과 같이 옵션 플래그와 파일 이름을 함께 인자로 전달할 수도 있습니다.

c:\v8_engine\source\v8\out\default>d8.exe --print-bytecode d8_greetings.js
[generated bytecode for function:  (0x029f081cfc25 <SharedFunctionInfo>)]
Parameter count 1
Register count 5
Frame size 40
         0000029F081CFCDA @    0 : 12 00             LdaConstant [0]
         0000029F081CFCDC @    2 : 26 fa             Star r1
         0000029F081CFCDE @    4 : 27 fe f9          Mov <closure>, r2
         0000029F081CFCE1 @    7 : 61 37 01 fa 02    CallRuntime [DeclareGlobals], r1-r2
         0000029F081CFCE6 @   12 : 13 01 00          LdaGlobal [1], [0]
         0000029F081CFCE9 @   15 : 26 f9             Star r2
         0000029F081CFCEB @   17 : 28 f9 02 02       LdaNamedProperty r2, [2], [2]
         0000029F081CFCEF @   21 : 26 fa             Star r1
         0000029F081CFCF1 @   23 : 13 03 04          LdaGlobal [3], [4]
         0000029F081CFCF4 @   26 : 26 f8             Star r3
         0000029F081CFCF6 @   28 : 12 04             LdaConstant [4]
         0000029F081CFCF8 @   30 : 26 f7             Star r4
         0000029F081CFCFA @   32 : 5d f8 f7 06       CallUndefinedReceiver1 r3, r4, [6]
         0000029F081CFCFE @   36 : 26 f8             Star r3
         0000029F081CFD00 @   38 : 59 fa f9 f8 08    CallProperty1 r1, r2, r3, [8]
         0000029F081CFD05 @   43 : 26 fb             Star r0
         0000029F081CFD07 @   45 : aa                Return
Constant pool (size = 5)
0000029F081CFC9D: [FixedArray] in OldSpace
 - map: 0x029f080404b1 <Map>
 - length: 5
           0: 0x029f081cfc4d <FixedArray[2]>
           1: 0x029f081468b5 <String[#7]: console>
           2: 0x029f08146929 <String[#3]: log>
           3: 0x029f081cfbdd <String[#5]: greet>
           4: 0x029f081cfbf1 <String[#4]: John>
Handler Table (size = 0)
Source Position Table (size = 0)
[generated bytecode for function: greet (0x029f081cfc5d <SharedFunctionInfo greet>)]
Parameter count 2
Register count 1
Frame size 8
         0000029F081CFE46 @    0 : 12 00             LdaConstant [0]
         0000029F081CFE48 @    2 : 26 fb             Star r0
         0000029F081CFE4A @    4 : 25 02             Ldar a0
         0000029F081CFE4C @    6 : 34 fb 00          Add r0, [0]
         0000029F081CFE4F @    9 : aa                Return
Constant pool (size = 1)
0000029F081CFE19: [FixedArray] in OldSpace
 - map: 0x029f080404b1 <Map>
 - length: 1
           0: 0x029f081cfdd5 <String[#7]: Hello, >
Handler Table (size = 0)
Source Position Table (size = 0)
Hello, John

그 밖의 D8 디버그 쉘에 대한 보다 자세한 사용 방법은 다음 링크의 문서를 참고하시기 바랍니다.

--trace-opt, --trace-opt-verbose, --trace-deopt 옵션 플래그

이번 섹션을 비롯한 이어지는 섹션들에서는 몇 가지 유용한 옵션 플래그들을 살펴봅니다. 가장 먼서 살펴볼 옵션 플래그는 기본적인 최적화 추적 로그와 관련된 것들입니다. 다음과 같은 코드가 d8_optimizations.js라는 파일로 작성되어 있다고 가정해보겠습니다. (새 창에서 보기)

var _position1 = { x: 10, y: 20 };
var _position2 = { x: 10, y: 20, z: 30 };

function multiply(position) {
  if (position.z) {
    return position.x * position.y * position.z;
  } else {
    return position.x * position.y;
  }
}

for (var i = 0; i < 100000; i++) {
  multiply(i % 90000 == 0 ? _position1 : _position2);
}

이 코드의 경우 루프문에서 지속적으로 multiply() 함수에 동일한 히든 클래스를 가진 개체가 전달되다가 9만 번째 루프 직후 갑자기 다른 히든 클래스를 가진 개체가 전달됩니다.

다음과 같이 이 파일을 --trace-opt 옵션 플래그를 함께 지정하여 D8에 전달하면 V8 엔진의 최적화 추적 로그를 살펴볼 수 있습니다.

c:\v8_engine\source\v8\out\default>d8.exe --trace-opt d8_optimizations.js
[marking 0x0346081cfe71 <JSFunction multiply (sfi = 00000346081CFCB9)> for optimized recompilation, reason: small function]
[compiling method 0x0346081cfe71 <JSFunction multiply (sfi = 00000346081CFCB9)> using TurboFan]
[optimizing 0x0346081cfe71 <JSFunction multiply (sfi = 00000346081CFCB9)> - took 0.000, 15.000, 0.000 ms]
[completed optimizing 0x0346081cfe71 <JSFunction multiply (sfi = 00000346081CFCB9)>]
[marking 0x0346081cfe05 <JSFunction (sfi = 00000346081CFC75)> for optimized recompilation, reason: hot and stable]
[compiling method 0x0346081cfe05 <JSFunction (sfi = 00000346081CFC75)> using TurboFan OSR]
[optimizing 0x0346081cfe05 <JSFunction (sfi = 00000346081CFC75)> - took 0.000, 0.000, 0.000 ms]
[marking 0x0346081cfe05 <JSFunction (sfi = 00000346081CFC75)> for optimized recompilation, reason: hot and stable]
[compiling method 0x0346081cfe05 <JSFunction (sfi = 00000346081CFC75)> using TurboFan OSR]
[optimizing 0x0346081cfe05 <JSFunction (sfi = 00000346081CFC75)> - took 0.000, 15.000, 0.000 ms]

또는 이 옵션 플래그 대신 --trace-opt-verbose 옵션 플래그를 지정하여 조금 더 상세한 최적화 정보를 살펴볼 수도 있습니다.

c:\v8_engine\source\v8\out\default>d8.exe --trace-opt-verbose d8_optimizations.js
[not yet optimizing , not enough ticks: 0/2 and ICs changed]
[not yet optimizing , not enough ticks: 1/2 and  too large for small function optimization: 105/90]
[marking 0x01a3081cfe71 <JSFunction multiply (sfi = 000001A3081CFCB9)> for optimized recompilation, reason: small function]
[compiling method 0x01a3081cfe71 <JSFunction multiply (sfi = 000001A3081CFCB9)> using TurboFan]
[optimizing 0x01a3081cfe71 <JSFunction multiply (sfi = 000001A3081CFCB9)> - took 0.000, 0.000, 0.000 ms]
[completed optimizing 0x01a3081cfe71 <JSFunction multiply (sfi = 000001A3081CFCB9)>]
[marking 0x01a3081cfe05 <JSFunction (sfi = 000001A3081CFC75)> for optimized recompilation, reason: hot and stable]
[compiling method 0x01a3081cfe05 <JSFunction (sfi = 000001A3081CFC75)> using TurboFan OSR]
[optimizing 0x01a3081cfe05 <JSFunction (sfi = 000001A3081CFC75)> - took 0.000, 0.000, 0.000 ms]
[not yet optimizing , not enough ticks: 0/2 and ICs changed]
[not yet optimizing , not enough ticks: 1/2 and  too large for small function optimization: 105/90]
[marking 0x01a3081cfe05 <JSFunction (sfi = 000001A3081CFC75)> for optimized recompilation, reason: hot and stable]
[compiling method 0x01a3081cfe05 <JSFunction (sfi = 000001A3081CFC75)> using TurboFan OSR]
[optimizing 0x01a3081cfe05 <JSFunction (sfi = 000001A3081CFC75)> - took 0.000, 0.000, 0.000 ms]

그러나 코드가 한번 최적화됐다고 해서 마지막까지 그대로 최적화가 유지된다고 장담할 수는 없습니다. 상황에 따라서는 최적화됐던 코드가 다시 역최적화 되는 경우도 당연히 존재합니다. 이러한 역최적화 추적 로그는 --trace-deopt 옵션 플래그를 지정하여 살펴볼 수 있는데, 앞에서 살펴봤던 --trace-opt 옵션 플래그와 함께 사용되는 경우가 대부분입니다.

c:\v8_engine\source\v8\out\default>d8.exe --trace-opt --trace-deopt d8_optimizations.js
[marking 0x0184081cfee9 <JSFunction multiply (sfi = 00000184081CFCF9)> for optimized recompilation, reason: small function]
[compiling method 0x0184081cfee9 <JSFunction multiply (sfi = 00000184081CFCF9)> using TurboFan]
[optimizing 0x0184081cfee9 <JSFunction multiply (sfi = 00000184081CFCF9)> - took 0.000, 0.000, 0.000 ms]
[completed optimizing 0x0184081cfee9 <JSFunction multiply (sfi = 00000184081CFCF9)>]
[marking 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> for optimized recompilation, reason: hot and stable]
[compiling method 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> using TurboFan OSR]
[optimizing 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> - took 0.000, 0.000, 0.000 ms]
[deoptimizing (DEOPT eager): begin 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> (opt #1) @2, FP to SP delta: 72, caller sp: 0x009e343fe948]
            ;;; deoptimize at <d8_optimizations.js:5:16> inlined at <d8_optimizations.js:13:3>, wrong map
  reading input frame  => bytecode_offset=85, args=1, height=3, retval=0(#0); inputs:
      0: 0x0184081cfe7d ;  [fp -  16]  0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)>
      1: 0x0184080c09a9 ;  [fp +  16]  0x0184080c09a9 <JSGlobal Object>
      2: 0x0184081c0da1 ;  [fp -  64]  0x0184081c0da1 <NativeContext[262]>
      3: 0x018408040815 ; (literal  4) 0x018408040815 <Odd Oddball: optimized_out>
      4: 0x018408040815 ; (literal  4) 0x018408040815 <Odd Oddball: optimized_out>
      5: 0x018408040815 ; (literal  4) 0x018408040815 <Odd Oddball: optimized_out>
      6: 0x018408040815 ; (literal  4) 0x018408040815 <Odd Oddball: optimized_out>
  reading input frame multiply => bytecode_offset=0, args=2, height=1, retval=0(#0); inputs:
      0: 0x0184081cfee9 ; (literal  5) 0x0184081cfee9 <JSFunction multiply (sfi = 00000184081CFCF9)>
      1: 0x0184080c09a9 ; (literal  6) 0x0184080c09a9 <JSGlobal Object>
      2: 0x0184080c55c9 ; rax 0x0184080c55c9 <Object map = 0000018408204AB9>
      3: 0x0184081c0da1 ; (literal  7) 0x0184081c0da1 <NativeContext[262]>
      4: 0x018408040815 ; (literal  4) 0x018408040815 <Odd Oddball: optimized_out>
      5: 0x018408040815 ; (literal  4) 0x018408040815 <Odd Oddball: optimized_out>
  translating interpreted frame  => bytecode_offset=85, variable_frame_size=24, frame_size=80
    0x009e343fe940: [top +  72] <- 0x0184080c09a9 <JSGlobal Object> ;  stack parameter (input #1)
    -------------------------
    0x009e343fe938: [top +  64] <- 0x7ffd8ea536be ;  caller's pc
    0x009e343fe930: [top +  56] <- 0x009e343fe958 ;  caller's fp
    0x009e343fe928: [top +  48] <- 0x0184081c0da1 <NativeContext[262]> ;  context (input #2)
    0x009e343fe920: [top +  40] <- 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> ;  function (input #0)
    0x009e343fe918: [top +  32] <- 0x0184081cfd9d <BytecodeArray[105]> ;  bytecode array
    0x009e343fe910: [top +  24] <- 0x0000000000ec <Smi 118> ;  bytecode offset
    -------------------------
    0x009e343fe908: [top +  16] <- 0x018408040815 <Odd Oddball: optimized_out> ;  stack parameter (input #3)
    0x009e343fe900: [top +   8] <- 0x018408040815 <Odd Oddball: optimized_out> ;  stack parameter (input #4)
    0x009e343fe8f8: [top +   0] <- 0x018408040815 <Odd Oddball: optimized_out> ;  stack parameter (input #5)
  translating interpreted frame multiply => bytecode_offset=0, variable_frame_size=16, frame_size=80
    0x009e343fe8f0: [top +  72] <- 0x0184080c09a9 <JSGlobal Object> ;  stack parameter (input #1)
    0x009e343fe8e8: [top +  64] <- 0x0184080c55c9 <Object map = 0000018408204AB9> ;  stack parameter (input #2)
    -------------------------
    0x009e343fe8e0: [top +  56] <- 0x7ffd8ea5d540 ;  caller's pc
    0x009e343fe8d8: [top +  48] <- 0x009e343fe930 ;  caller's fp
    0x009e343fe8d0: [top +  40] <- 0x0184081c0da1 <NativeContext[262]> ;  context (input #3)
    0x009e343fe8c8: [top +  32] <- 0x0184081cfee9 <JSFunction multiply (sfi = 00000184081CFCF9)> ;  function (input #0)
    0x009e343fe8c0: [top +  24] <- 0x0184081cffd5 <BytecodeArray[43]> ;  bytecode array
    0x009e343fe8b8: [top +  16] <- 0x000000000042 <Smi 33> ;  bytecode offset
    -------------------------
    0x009e343fe8b0: [top +   8] <- 0x018408040815 <Odd Oddball: optimized_out> ;  stack parameter (input #4)
    0x009e343fe8a8: [top +   0] <- 0x018408040815 <Odd Oddball: optimized_out> ;  accumulator (input #5)
[deoptimizing (eager): end 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> @2 => node=0, pc=0x7ffd8ea5d660, caller sp=0x009e343fe948, took 31.000 ms]
[marking 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> for optimized recompilation, reason: hot and stable]
[compiling method 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> using TurboFan OSR]
[optimizing 0x0184081cfe7d <JSFunction (sfi = 00000184081CFCB5)> - took 0.000, 16.000, 0.000 ms]

이 추적 로그에서 역최적화 관련 추적 로그를 자세히 살펴보면 wrong map, 즉 히든 클래스의 변경으로 인해서 역최적화가 수행되었음을 확인할 수 있습니다.

--trace-turbo-inlining, --max-inlined-bytecode-size-small 옵션 플래그

이번 섹션에서 살펴볼 --trace-turbo-inlining 옵션 플래그는 TurboFan 컴파일러에 의해서 수행되는 인라이닝 최적화와 관련된 추적 로그를 제공합니다. 기억하실지 모르겠지만 사실 이 플래그는 본 시리즈의 지난 문서 중 'Chromium Edge에서 V8 엔진의 인라이닝 정보 살펴보기' 섹션에서 이미 살펴본 바가 있습니다. 당시 해당 섹션에서는 디버그 쉘을 사용하는 대신 브라우저 실행 파일의 PE 헤더를 편집하여 다소 일반적이지 않은 방법으로 추적 로그를 살펴봤었습니다. 그러나 이제는 디버그 쉘을 자유롭게 활용하여 테스트할 수 있는 환경을 갖췄으므로 동일한 작업을 보다 간편하게 수행할 수 있습니다.

가령 다음과 같은 코드가 d8_inlining_normal.js라는 파일로 작성되어 있다고 가정해보겠습니다. (새 창에서 보기)

var list = [
  { x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 }, { x: 4, y: 4 }, 
  { x: 5, y: 5 }, { x: 6, y: 6 }, { x: 7, y: 7 }, { x: 8, y: 8 }, { x: 9, y: 9 }
];

function multiply(item) {
  return item.x * item.y;
}

for (var i = 0; i < 5000; i++) {
  for (var j = 0; j < list.length; j++) {
    multiply(list[j]);
  }
}

이 파일을 다음과 같이 --trace-turbo-inlining 옵션 플래그와 함께 D8에 전달하면 인라이닝 최적화 정보를 살펴볼 수 있습니다.

c:\v8_engine\source\v8\out\default>d8.exe --trace-turbo-inlining d8_inlining_normal.js
Considering 00000191203502F8 {0x01400824fc99 <SharedFunctionInfo multiply>} for inlining with 0000019120350910 {0x014008250121 <FeedbackVector[5]>}
Inlining small function(s) at call site #93:JSCall
Inlining 00000191203502F8 {0x01400824fc99 <SharedFunctionInfo multiply>} into 0000019120316FD8 {0x01400824fc55 <SharedFunctionInfo>}

이번에는 코드를 일부 변경한 다음, 동일한 방식으로 인라이닝 추적 로그의 변화를 살펴보겠습니다. 코드 내용 중 크게 변경된 부분은 없으며 단지 multiply() 함수 내부에 별다른 의미 없이 try ... catch 문만 추가하여 d8_inlining_try_catch.js라는 파일로 다시 저장했습니다. (새 창에서 보기)

var list = [
  { x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 }, { x: 4, y: 4 }, 
  { x: 5, y: 5 }, { x: 6, y: 6 }, { x: 7, y: 7 }, { x: 8, y: 8 }, { x: 9, y: 9 }
];

function multiply(item) {
  try {
    return item.x * item.y;
  } catch (e) {
    console.log(e);
  }
}

for (var i = 0; i < 5000; i++) {
  for (var j = 0; j < list.length; j++) {
    multiply(list[j]);
  }
}

다음은 이 변경된 코드 파일을 사용하여 디버그 쉘에서 인라이닝 최적화 정보를 살펴본 결과입니다.

c:\v8_engine\source\v8\out\default>d8.exe --trace-turbo-inlining d8_inlining_try_catch.js
Considering 0000020386C00328 {0x02ad0824fca9 <SharedFunctionInfo multiply>} for inlining with 0000020386C00940 {0x02ad08250195 <FeedbackVector[11]>}
1 candidate(s) for inlining:
- candidate: JSCall node #93 with frequency 9991, 1 target(s):
  - target: 0000020386C00328 {0x02ad0824fca9 <SharedFunctionInfo multiply>}, bytecode size: 54
Inlining 0000020386C00328 {0x02ad0824fca9 <SharedFunctionInfo multiply>} into 0000020386BDDD68 {0x02ad0824fc65 <SharedFunctionInfo>}

여전히 인라이닝 최적화가 적용되기는 하지만 그 사유가 달라졌습니다. 첫 번째 테스트에서는 small function, 즉 함수의 크기가 작은 것이 인라이닝 최적화가 적용된 가장 주된 이유였습니다. 반면 두 번째 테스트에서는 이 조건을 만족하지 못하는 대신, 높은 호출 빈도로 인해서 인라이닝 최적화가 적용된 것임을 알 수 있습니다. 이번 예제 코드의 경우 multiply() 함수의 바이트 코드 크기는 54 바이트입니다. 그렇다면 인라이닝 최적화의 기준이 되는 바이트 코드 크기는 얼마일까요? 그 답은 d8.exe의 옵션 플래그 목록에서 찾을 수 있습니다. 이전 섹션에서 살펴봤던 전체 옵션 플래그 목록을 찾아보면 다음과 같은 옵션 플래그를 발견할 수 있습니다.

...
--reserve-inline-budget-scale-factor (maximum cumulative size of bytecode considered for inlining)
      type: float  default: 1.2
--max-inlined-bytecode-size-small (maximum size of bytecode considered for small function inlining)
      type: int  default: 30
--max-optimized-bytecode-size (maximum bytecode size to be considered for optimization; too high values may cause the compiler to hit (release) assertions)
      type: int  default: 61440
--min-inlining-frequency (minimum frequency for inlining)
      type: float  default: 0.15
...

결국 인라이닝 최적화에서 small function 조건을 만족하는 바이트 코드의 최대 크기 기본값은 30 바이트인 반면, multiply() 함수의 바이트 코드 크기는 54 바이트로 이 값을 초과했기 때문에 해당 조건을 만족하지 못했던 것입니다. 따라서 다음과 같이 --max-inlined-bytecode-size-small 옵션 플래그를 함께 지정하면 조정된 기준값을 적용할 수 있습니다.

c:\v8_engine\source\v8\out\default>d8.exe --trace-turbo-inlining --max-inlined-bytecode-size-small=60 d8_inlining_try_catch.js
Considering 000001E0BBC555B8 {0x01aa0824fca9 <SharedFunctionInfo multiply>} for inlining with 000001E0BBC55BD0 {0x01aa08250195 <FeedbackVector[11]>}
Inlining small function(s) at call site #93:JSCall
Inlining 000001E0BBC555B8 {0x01aa0824fca9 <SharedFunctionInfo multiply>} into 000001E0BBC7AD98 {0x01aa0824fc65 <SharedFunctionInfo>}

이번 테스트에서는 --max-inlined-bytecode-size-small 옵션 플래그를 지정하여 기준값을 60 바이트로 늘렸기 때문에 다시 small function 조건을 만족하는 것을 확인할 수 있습니다.

--allow-natives-syntax 옵션 플래그

내부적으로 V8 엔진에는 매우 유용한 기본 제공 함수(Built-in Functions)들이 존재합니다. 가령 기본 제공 함수들 중 하나인 HaveSameMap() 함수를 사용하면 두 개체가 동일한 히든 클래스를 참조하는지 여부를 확인할 수 있습니다. 그러나 일반적인 상황에서는 이 함수들을 사용할 수 없으며, 대신 --allow-natives-syntax 옵션 플래그를 지정하는 경우에만 % 접두사를 추가하여 디버깅 목적으로 사용 가능합니다.

다음의 코드를 작성하여 d8_have_same_map.js라는 이름의 파일로 저장합니다. (새 창에서 보기)

function _Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

var _person1 = new _Person("John", "Doe");
var _person2 = new _Person("Jane", "Doe");

console.log("_person1 == _person2: " + %HaveSameMap(_person1, _person2));

_person2.age = "unknown";

console.log("_person1 == _person2: " + %HaveSameMap(_person1, _person2));

그런 다음 아래와 같이 디버그 쉘에서 코드를 실행하여 그 결과를 확인할 수 있습니다.

c:\v8_engine\source\v8\out\default>d8.exe --allow-natives-syntax d8_have_same_map.js
_person1 == _person2: true
_person1 == _person2: false

이런 방법을 사용하면 지난 글에서처럼 매번 불편하게 메모리 힙 스냅샷을 찍어서 히든 클래스를 비교할 필요가 없습니다. 그러나 모든 웹 클라이언트 개발자가 V8 엔진의 소스를 다운로드 받아서 빌드할 수 있는 것은 아니기 때문에 F12 개발자 도구를 사용해서 히든 클래스를 비교하는 방법을 알아보는 것도 결코 의미 없는 일은 아니라는 생각입니다. 지원되는 기본 제공 함수들의 목록은 다음 소스에서 확인하실 수 있습니다.

--trace-ic 옵션 플래그

이번 섹션에서 살펴볼 --trace-ic 옵션 플래그 역시, 본 시리즈의 지난 문서 중 'D8 추적 로그로 인라인 캐싱 전환 추이 살펴보기' 섹션에서 간단히 알아본 바가 있습니다. 따라서 이 자리에서는 가볍게 다뤄보기만 하겠습니다.

지금까지 살펴본 다른 옵션 플래그들과는 달리 --trace-ic 옵션 플래그는 추적 로그를 화면에 출력하지 않고 v8.log라는 이름으로 인라인 캐싱 상태 전환 추적 로그 파일을 생성합니다. 가령 다음과 같은 명령어를 입력하면 빌드 디렉터리에 추적 로그 파일이 만들어집니다.

c:\v8_engine\source\v8\out\default>d8.exe --trace-ic d8_optimizations.js

이렇게 생성된 v8.log 파일은 애초부터 사람이 직접 눈으로 확인하기 보다는 tools 폴더(본문의 경우 C:\v8_engine\source\v8\tools)에 위치한 ic-processor 도구를 이용해서 확인하기 위한 용도입니다. 직접 추적 로그 파일을 살펴볼 수 없는 것은 아니지만 내용이 너무 장황하고 분량이 많기 때문에 분석하기가 쉽지 않습니다. 가령 위의 명령으로 생성된 추적 로그 파일의 내용을 살펴보면 1,600 줄을 살짝 넘는 분량입니다.

v8-version,8,4,0,0,1
code-creation,Builtin,3,15000,0x7ff855df4120,1768,RecordWrite
code-creation,Builtin,3,15000,0x7ff855df4820,516,EphemeronKeyBarrier
code-creation,Builtin,3,15000,0x7ff855df4a40,492,AdaptorWithBuiltinExitFrame
code-creation,Builtin,3,15000,0x7ff855df4c40,301,ArgumentsAdaptorTrampoline

... 대량의 로그 ...

code-deopt,93000,992,0x2e9000c2b60,0,127,eager,<d8_optimizations.js:5:16> inlined at <d8_optimizations.js:13:3>,wrong map
LoadIC,0x2e9081cfff6,5,16,1,P,0x02e908204ab9,z,,
LoadIC,0x2e9081d0013,8,21,1,P,0x02e908204ab9,x,,
LoadIC,0x2e9081d0019,8,34,1,P,0x02e908204ab9,y,,
code-creation,LazyCompile,0,125000,0x2e9000c31e0,2390, d8_optimizations.js:1:1,0x2e9081cfcb4,*

물론 위의 추적 로그를 잘 살펴보면 속성 x, y, zMONOMORPHIC 상태(1)에서 POLYMORPHIC 상태(P)로 전환됐다는 등의 단편적인 정보는 알아낼 수 있지만 1,600 줄이 넘는 로그를 일일이 살펴보면서 전체적인 윤곽을 얻어내기는 어렵습니다. 따라서 지난 글에서 살펴본 것처럼 tools 폴더에 위치한 ic-explorer.html 도구를 사용하여 다음과 같이 보다 시각적으로 분석하는 편이 유용합니다.

V8 IC Explorer - v8.log 분석

정리

본문에서는 Windows 환경에서 V8 엔진의 소스를 다운로드 받고 Visual Studio Community 2019를 사용하여 빌드하는 방법을 살펴봤습니다. 그리고 그 결과물 중 하나로 얻어진 V8 엔진의 디버그 쉘인 D8을 이용해서 JavaScript 코드를 분석하는 방법을 간단히 알아봤습니다.

이어지는 글에서는 지금까지 살펴본 여러 내용들을 바탕으로 F12 개발자 도구메모리 창을 활용하여 실제 업무 환경에서 발생할 수 있는 가상의 메모리 누수 상황을 직접 해결해보도록 하겠습니다.