Google은 Chrome Dev Summit 2020에서 처음으로 웹에서 WebAssembly 애플리케이션에 대한 Chrome의 디버깅 지원을 시연했습니다. 그 이후로 팀은 대규모 애플리케이션은 물론 초대규모 애플리케이션에서도 개발자 환경을 확장할 수 있도록 많은 노력을 기울여 왔습니다. 이 게시물에서는 다양한 도구에 추가했거나 작동하도록 만든 노브와 이를 사용하는 방법을 보여줍니다.
확장 가능한 디버깅
2020년 게시물에서 중단한 부분부터 다시 살펴보겠습니다. 당시 살펴본 예는 다음과 같습니다.
#include <SDL2/SDL.h>
#include <complex>
int main() {
// Init SDL.
int width = 600, height = 600;
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window;
SDL_Renderer* renderer;
SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
&renderer);
// Generate a palette with random colors.
enum { MAX_ITER_COUNT = 256 };
SDL_Color palette[MAX_ITER_COUNT];
srand(time(0));
for (int i = 0; i < MAX_ITER_COUNT; ++i) {
palette[i] = {
.r = (uint8_t)rand(),
.g = (uint8_t)rand(),
.b = (uint8_t)rand(),
.a = 255,
};
}
// Calculate and draw the Mandelbrot set.
std::complex<double> center(0.5, 0.5);
double scale = 4.0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
std::complex<double> point((double)x / width, (double)y / height);
std::complex<double> c = (point - center) * scale;
std::complex<double> z(0, 0);
int i = 0;
for (; i < MAX_ITER_COUNT - 1; i++) {
z = z * z + c;
if (abs(z) > 2.0)
break;
}
SDL_Color color = palette[i];
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
SDL_RenderDrawPoint(renderer, x, y);
}
}
// Render everything we've drawn to the canvas.
SDL_RenderPresent(renderer);
// SDL_Quit();
}
이것은 여전히 꽤 작은 예이며 매우 큰 애플리케이션에서 볼 수 있는 실제 문제를 볼 수는 없지만 새로운 기능이 무엇인지는 보여줄 수 있습니다. 빠르고 간편하게 설정하고 직접 사용해 볼 수 있습니다.
지난 게시물에서는 이 예시를 컴파일하고 디버그하는 방법을 알아봤습니다. 다시 한번 실행해 보면서 //performance//도 살펴보겠습니다.
$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH
이 명령어는 3MB의 wasm 바이너리를 생성합니다. 아시다시피 이 정보의 대부분은 디버그 정보입니다. llvm-objdump
도구 [1]를 사용하여 이를 확인할 수 있습니다. 예를 들면 다음과 같습니다.
$ llvm-objdump -h mandelbrot.wasm
mandelbrot.wasm: file format wasm
Sections:
Idx Name Size VMA Type
0 TYPE 0000026f 00000000
1 IMPORT 00001f03 00000000
2 FUNCTION 0000043e 00000000
3 TABLE 00000007 00000000
4 MEMORY 00000007 00000000
5 GLOBAL 00000021 00000000
6 EXPORT 0000014a 00000000
7 ELEM 00000457 00000000
8 CODE 0009308a 00000000 TEXT
9 DATA 0000e4cc 00000000 DATA
10 name 00007e58 00000000
11 .debug_info 000bb1c9 00000000
12 .debug_loc 0009b407 00000000
13 .debug_ranges 0000ad90 00000000
14 .debug_abbrev 000136e8 00000000
15 .debug_line 000bb3ab 00000000
16 .debug_str 000209bd 00000000
이 출력은 생성된 wasm 파일에 있는 모든 섹션을 보여줍니다. 대부분은 표준 WebAssembly 섹션이지만 이름이 .debug_
로 시작하는 맞춤 섹션도 여러 개 있습니다. 바이너리에 디버그 정보가 포함되어 있습니다. 모든 크기를 합산하면 디버그 정보가 3MB 파일의 약 2.3MB를 차지하는 것을 알 수 있습니다. emcc
명령어도 time
하면 머신에서 실행하는 데 약 1.5초가 걸렸습니다. 이 수치는 좋은 기준이 되지만 너무 작아서 아무도 신경 쓰지 않을 것입니다. 하지만 실제 애플리케이션에서는 디버그 바이너리가 GB 단위의 크기에 쉽게 도달할 수 있으며 빌드하는 데 몇 분 정도 걸릴 수 있습니다.
Binaryen 건너뛰기
Emscripten으로 wasm 애플리케이션을 빌드할 때 최종 빌드 단계 중 하나는 Binaryen 최적화 도구를 실행하는 것입니다. Binaryen은 WebAssembly(유사) 바이너리를 최적화하고 합법화하는 컴파일러 도구 키트입니다. 빌드의 일부로 Binaryen을 실행하는 것은 상당히 비용이 많이 들지만 특정 조건에서만 필요합니다. 디버그 빌드의 경우 Binaryen 패스를 사용하지 않으면 빌드 시간을 크게 단축할 수 있습니다. 가장 일반적으로 필요한 Binaryen 패스는 64비트 정수 값이 포함된 함수 서명을 합법화하는 것입니다. -sWASM_BIGINT
를 사용하여 WebAssembly BigInt 통합을 선택하면 이 문제를 방지할 수 있습니다.
$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK
안전을 위해 -sERROR_ON_WASM_CHANGES_AFTER_LINK
플래그를 추가했습니다. Binaryen이 실행 중일 때 이를 감지하여 예기치 않게 바이너리를 재작성하는 데 도움을 줍니다. 이렇게 하면 빠른 속도로 진행할 수 있습니다.
예시가 상당히 작지만 Binaryen을 건너뛰는 효과를 확인할 수 있습니다. time
에 따르면 이 명령어는 1초 미만으로 실행되므로 이전보다 0.5초 더 빨라졌습니다.
고급 조정
입력 파일 스캔 건너뛰기
일반적으로 Emscripten 프로젝트를 연결할 때 emcc
는 모든 입력 객체 파일 및 라이브러리를 스캔합니다. 이는 프로그램에서 JavaScript 라이브러리 함수와 네이티브 기호 간의 정확한 종속 항목을 구현하기 위함입니다. 대규모 프로젝트의 경우 입력 파일(llvm-nm
사용)을 추가로 스캔하면 연결 시간이 크게 늘어날 수 있습니다.
대신 emcc
에 JavaScript 함수의 가능한 모든 네이티브 종속 항목을 포함하도록 지시하는 -sREVERSE_DEPS=all
를 사용하여 실행할 수 있습니다. 이는 코드 크기의 오버헤드는 작지만 링크 시간을 단축할 수 있으며 디버그 빌드에 유용할 수 있습니다.
이 예시처럼 작은 프로젝트에서는 실질적인 차이가 없지만 프로젝트에 수백, 수천 개의 객체 파일이 있다면 링크 시간을 크게 개선할 수 있습니다.
'이름' 섹션 제거
대규모 프로젝트, 특히 C++ 템플릿을 많이 사용하는 프로젝트에서는 WebAssembly 'name' 섹션이 매우 클 수 있습니다. 이 예에서는 전체 파일 크기의 일부에 불과하지만(위의 llvm-objdump
출력 참고) 경우에 따라 매우 클 수 있습니다. 애플리케이션의 'name' 섹션이 매우 크고 dwarf 디버그 정보가 디버깅 요구사항에 충분한 경우 다음과 같이 'name' 섹션을 삭제하는 것이 유리할 수 있습니다.
$ emstrip --no-strip-all --remove-section=name mandelbrot.wasm
이렇게 하면 DWARF 디버그 섹션은 유지하면서 WebAssembly '이름' 섹션이 제거됩니다.
분열 디버그
디버그 데이터가 많은 바이너리는 빌드 시간뿐만 아니라 디버깅 시간에도 부담을 줍니다. 디버거는 '로컬 변수 x의 유형은 무엇인가요?'와 같은 쿼리에 빠르게 응답할 수 있도록 데이터를 로드하고 색인을 생성해야 합니다.
디버그 분할을 사용하면 바이너리의 디버그 정보를 바이너리에 남아 있는 부분과 별도의 DWARF 객체(.dwo
) 파일에 포함된 부분으로 나눌 수 있습니다. -gsplit-dwarf
플래그를 Emscripten에 전달하여 사용 설정할 수 있습니다.
$ emcc -sUSE_SDL=2 -g -gsplit-dwarf -gdwarf-5 -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK
아래에서는 다양한 명령어와 디버그 데이터 없이 컴파일하여 어떤 파일이 생성되는지 보여줍니다. 디버그 데이터를 사용하여 마지막으로 디버그 데이터와 디버그 분열을 모두 사용하여 컴파일하면 됩니다.
DWARF 데이터를 분할할 때 디버그 데이터의 일부는 바이너리와 함께 저장되지만 대부분은 mandelbrot.dwo
파일에 저장됩니다(위 그림 참고).
mandelbrot
의 경우 소스 파일이 하나만 있지만 일반적으로 프로젝트는 이보다 크며 두 개 이상의 파일을 포함합니다. 디버그 분할은 각각의 분할에 대해 .dwo
파일을 생성합니다. 디버거의 현재 베타 버전(0.1.6.1615)에서 이 분할 디버그 정보를 로드할 수 있으려면 다음과 같이 모든 정보를 소위 DWARF 패키지(.dwp
)로 번들로 묶어야 합니다.
$ emdwp -e mandelbrot.wasm -o mandelbrot.dwp
개별 객체로 DWARF 패키지를 빌드하면 파일을 하나만 추가로 제공하면 되므로 이점이 있습니다. 현재는 향후 릴리스에서 모든 개별 객체도 로드하기 위해 노력하고 있습니다.
DWARF 5의 특징
위의 emcc
명령어에 -gdwarf-5
라는 다른 플래그가 추가된 것을 눈치채셨을 것입니다. 현재 기본값이 아닌 DWARF 기호의 버전 5를 사용 설정하는 것도 디버깅을 더 빠르게 시작하는 데 도움이 되는 또 다른 방법입니다. 이를 통해 기본 버전 4에서 누락된 특정 정보가 기본 바이너리에 저장됩니다. 특히 기본 바이너리에서만 전체 소스 파일 세트를 확인할 수 있습니다. 이렇게 하면 디버거가 전체 기호 데이터를 로드하고 파싱하지 않고도 전체 소스 트리를 표시하고 브레이크포인트를 설정하는 등의 기본 작업을 실행할 수 있습니다. 이렇게 하면 분할 기호를 사용한 디버깅이 훨씬 빨라지므로 항상 -gsplit-dwarf
및 -gdwarf-5
명령줄 플래그를 함께 사용합니다.
DWARF5 디버그 형식을 사용하면 다른 유용한 기능도 사용할 수 있습니다. -gpubnames
플래그를 전달할 때 생성되는 디버그 데이터에 이름 색인을 도입합니다.
$ emcc -sUSE_SDL=2 -g -gdwarf-5 -gsplit-dwarf -gpubnames -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK
디버깅 세션 중에 이름으로 항목을 검색하여 기호 조회가 발생하는 경우가 많습니다(예: 변수 또는 유형을 찾을 때). 이름 색인은 이름을 정의하는 컴파일 단위를 직접 가리킴으로써 이 검색을 가속합니다. 이름 색인이 없으면 찾고 있는 이름이 지정된 항목을 정의하는 올바른 컴파일 단위를 찾기 위해 전체 디버그 데이터를 철저히 검색해야 합니다.
궁금한 점이 있으면 디버그 데이터를 살펴보세요.
llvm-dwarfdump
를 사용하여 DWARF 데이터를 살펴볼 수 있습니다. 다음을 시도해 보세요.
llvm-dwarfdump mandelbrot.wasm
이는 디버그 정보가 있는 '컴파일 단위'(대략 소스 파일)에 대한 개요를 제공합니다. 이 예시에서는 mandelbrot.cc
의 디버그 정보만 있습니다. 일반 정보에서 스켈레톤 단위가 있음을 알 수 있습니다. 즉, 이 파일에 불완전한 데이터가 있고 나머지 디버그 정보가 포함된 별도의 .dwo
파일이 있음을 의미합니다.
이 파일 내의 다른 테이블(예: wasm 바이트 코드와 C++ 행의 매핑을 보여주는 행 테이블)도 확인할 수 있습니다(llvm-dwarfdump -debug-line
사용해 보세요).
별도의 .dwo
파일에 포함된 디버그 정보도 확인할 수 있습니다.
llvm-dwarfdump mandelbrot.dwo
요약: 디버그 분열을 사용하면 어떤 이점이 있나요?
대규모 애플리케이션으로 작업할 때 디버그 정보를 분할하면 다음과 같은 몇 가지 이점이 있습니다.
더 빠른 연결: 더 이상 링커가 전체 디버그 정보를 파싱할 필요가 없음 일반적으로 링커는 바이너리에 있는 전체 DWARF 데이터를 파싱해야 합니다. 디버그 정보의 상당 부분을 별도의 파일로 제거하면 링커가 더 작은 바이너리를 처리하므로 연결 시간이 단축됩니다(특히 대규모 애플리케이션에 해당).
더 빠른 디버깅: 디버거가 일부 기호 조회 시
.dwo
/.dwp
파일의 추가 기호 파싱을 건너뛸 수 있습니다. 일부 조회(예: wasm-to-C++ 파일의 줄 매핑에 관한 요청)의 경우 추가 디버그 데이터를 확인할 필요가 없습니다. 이렇게 하면 추가 디버그 데이터를 로드하고 파싱할 필요가 없어 시간을 절약할 수 있습니다.
1: 시스템에 최신 버전의 llvm-objdump
가 없고 emsdk
를 사용하는 경우 emsdk/upstream/bin
디렉터리에서 찾을 수 있습니다.
미리보기 채널 다운로드
Chrome Canary, Dev 또는 베타를 기본 개발 브라우저로 사용하는 것이 좋습니다. 이러한 미리보기 채널을 통해 최신 DevTools 기능에 액세스할 수 있고, 최첨단 웹 플랫폼 API를 테스트할 수 있으며, 사용자보다 먼저 사이트에서 문제를 발견할 수 있습니다.
Chrome DevTools팀에 문의하기
다음 옵션을 사용하여 새로운 기능, 업데이트 또는 DevTools와 관련된 다른 내용을 논의하세요.
- crbug.com에서 의견 및 기능 요청을 제출하세요.
- DevTools에서 옵션 더보기 > 도움말 > DevTools 문제 신고를 사용하여 DevTools 문제를 신고합니다.
- @ChromeDevTools에 트윗하세요.
- DevTools의 새로운 기능 YouTube 동영상 또는 DevTools 도움말 YouTube 동영상에 댓글을 남겨주세요.