about:tracing 플래그를 사용하여 WebGL 게임 프로파일링

릴리 톰슨
릴리 톰슨

측정할 수 없다면 개선도 불가능합니다.

로드 켈빈

HTML5 게임을 더 빠르게 실행하려면 먼저 성능 병목 현상을 파악해야 하지만 이 작업은 어려울 수 있습니다. FPS (초당 프레임 수) 데이터 평가는 시작에 있지만 전체적인 그림을 이해하려면 Chrome 활동의 미묘한 차이를 이해해야 합니다.

about:tracing 도구는 성능 개선을 목표로 하는 성급한 해결 방법을 피하는 데 도움이 되는 통찰력을 제공하지만 근본적으로 선의의 추측입니다. 많은 시간과 에너지를 절약하고 각 프레임에서 Chrome이 수행하는 작업을 더 명확하게 파악하며 이 정보를 사용하여 게임을 최적화할 수 있습니다.

추적 소개

Chrome의 about:tracing 도구는 일정 기간 동안의 모든 Chrome 활동을 볼 수 있는 창을 제공하므로 처음에는 복잡하게 느껴질 수 있습니다. Chrome의 많은 기능은 즉시 추적할 수 있도록 계측되므로 수동 계측을 사용하지 않아도 about:tracing을 사용하여 성능을 추적할 수 있습니다. (JS에서 수동으로 계측하는 방법은 이후 섹션을 참조하세요.)

추적 보기를 확인하려면 Chrome의 검색주소창 (주소 표시줄)에 'about:tracing'을 입력하면 됩니다.

Chrome 검색주소창
Chrome의 검색창에 'about:tracing'을 입력합니다.

추적 도구에서 기록을 시작하고 몇 초 동안 게임을 실행한 다음 트레이스 데이터를 볼 수 있습니다. 다음은 데이터가 어떻게 표시되는지 보여주는 예입니다.

간단한 추적 결과
간단한 추적 결과

혼란스럽습니다. 읽는 방법에 관해 이야기해 봅시다.

각 행은 프로파일링 중인 프로세스를 나타내고, 왼쪽-오른쪽 축은 시간을 나타내며, 색상이 지정된 각 상자는 계측 함수 호출입니다. 다양한 종류의 리소스에 대한 행이 있습니다. 게임 프로파일링에서 가장 흥미로운 것은 GPU (Graphics Processing Unit)가 수행하는 작업을 보여주는 CrGpuMain과 CrRendererMain입니다. 각 트레이스에는 트레이스 기간 동안 열려 있는 각 탭의 CrRendererMain 행이 포함됩니다 (about:tracing 탭 자체 포함).

트레이스 데이터를 읽을 때 첫 번째 작업은 게임에 해당하는 CrRendererMain 행을 결정하는 것입니다.

강조 표시된 간단한 추적 결과
강조표시된 간단한 추적 결과

이 예에서 두 후보는 2216과 6516입니다. 안타깝게도 현재 정기적으로 많은 업데이트를 실행하는 줄을 찾는 것 (또는 트레이스 포인트를 사용하여 코드를 수동으로 계측한 경우 트레이스 데이터가 포함된 줄을 찾는 일) 외에는 애플리케이션을 선택할 수 있는 세련된 방법이 없습니다. 이 예에서는 6516이 업데이트 빈도에 따라 기본 루프를 실행하는 것처럼 보입니다. 트레이스를 시작하기 전에 다른 탭을 모두 닫으면 올바른 CrRendererMain을 더 쉽게 찾을 수 있습니다. 하지만 게임이 아닌 프로세스에 관한 CrRendererMain 행이 여전히 있을 수 있습니다.

프레임 찾기

게임의 추적 도구에서 올바른 행을 찾았다면 다음 단계는 기본 루프를 찾는 것입니다. 메인 루프는 추적 데이터에서 반복 패턴처럼 보입니다. W, A, S, D 키를 사용하여 추적 데이터를 탐색할 수 있습니다. A와 D는 왼쪽 또는 오른쪽으로 (시간 앞뒤로), W와 S를 사용하여 데이터를 확대/축소합니다. 게임이 60Hz에서 실행되는 경우 기본 루프는 16밀리초마다 반복되는 패턴일 것으로 예상할 수 있습니다.

실행 프레임이 3개 같음
실행 프레임이 3개 있음

게임의 하트비트를 찾았으면 코드가 각 프레임에서 정확히 어떤 역할을 하는지 자세히 파악할 수 있습니다. 함수 상자의 텍스트를 읽을 수 있을 때까지 W, A, S, D를 사용하여 확대합니다.

실행 프레임 심층 분석
실행 프레임 심층 분석

이 상자 모음은 일련의 함수 호출을 보여주며 각 호출은 색상이 지정된 상자로 표시됩니다. 각 함수는 그 위에 있는 상자에 의해 호출되었습니다. 따라서 이 경우에는 MessageLoop::RunTask가 RenderWidget::OnSwapBuffersComplete라고 했으며 RenderWidget::DoDeferredUpdate 등으로 호출된 것을 확인할 수 있습니다. 이 데이터를 읽으면 무엇이 호출되었는지, 각 실행에 얼마나 걸렸는지 전체적으로 파악할 수 있습니다.

하지만 여기서 조금 더 어려워집니다. about:tracing으로 노출되는 정보는 Chrome 소스 코드의 원시 함수 호출입니다. 이름을 보고 각 함수가 하는 일을 추측할 수 있지만 정보가 정확히 사용자 친화적이지 않습니다. 프레임의 전반적인 흐름을 확인하는 것은 유용하지만, 실제 상황을 파악하기 위해서는 좀 더 사람이 읽을 수 있는 내용이 필요합니다.

트레이스 태그 추가

다행히 코드에 수동 계측을 추가하여 트레이스 데이터를 만드는 친숙한 방법이 있습니다. console.timeconsole.timeEnd입니다.

console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");

위의 코드는 지정된 태그로 추적 뷰 이름에 새 상자를 만듭니다. 따라서 앱을 다시 실행하면 각 태그의 시작 호출과 종료 호출 사이에 경과된 시간을 보여주는 'update' 및 'render' 상자가 표시됩니다.

수동으로 추가된 태그
수동으로 추가된 태그

이를 통해 사람이 읽을 수 있는 추적 데이터를 만들어 코드에서 핫스팟을 추적할 수 있습니다.

GPU 또는 CPU?

하드웨어 가속 그래픽의 경우 프로파일링 중에 물어볼 수 있는 가장 중요한 질문 중 하나는 이 코드가 GPU의 제약을 받는지 아니면 CPU의 제약을 받는지입니다. 각 프레임으로 GPU에서 일부 렌더링 작업을 하고 CPU에서 일부 로직을 수행하게 됩니다. 게임을 느리게 만드는 이유를 이해하려면 두 리소스 간에 작업이 어떻게 균형을 이루는지 확인해야 합니다.

먼저 CrGPUMain이라는 추적 뷰에서 GPU가 특정 시간에 사용 중인지 여부를 나타내는 줄을 찾습니다.

GPU 및 CPU 트레이스

게임의 모든 프레임으로 인해 CrRendererMain은 물론 GPU에서도 CPU 작업이 발생하는 것을 확인할 수 있습니다. 위의 트레이스는 대부분의 16ms 프레임에서 CPU와 GPU가 모두 유휴 상태인 매우 간단한 사용 사례를 보여줍니다.

추적 뷰는 느리게 실행되는 게임이 있고 어떤 리소스를 소진하고 있는지 확실하지 않은 경우에 매우 유용합니다. GPU와 CPU 라인의 관계를 살펴보는 것이 디버깅의 핵심입니다. 이전과 동일한 예를 사용하지만 업데이트 루프에 약간의 추가 작업을 추가합니다.

console.time("update");
doExtraWork();
update(Math.min(50, now - time));
console.timeEnd("update");

console.time("render");
render();
console.timeEnd("render");

이제 다음과 같은 트레이스가 표시됩니다.

GPU 및 CPU 트레이스

이 트레이스는 무엇을 알 수 있나요? 사진에 나온 프레임의 속도가 약 2270ms에서 2320ms로 변한 것을 볼 수 있습니다. 즉, 각 프레임에 약 50ms (프레임 속도의 20Hz)가 소요된다는 의미입니다. 업데이트 상자 옆에 렌더링 함수를 나타내는 색상이 지정된 상자가 보이지만 프레임은 전적으로 업데이트 자체에 의해 지배됩니다.

CPU에서 발생하는 상황과 대조적으로 GPU가 대부분의 모든 프레임에서 여전히 유휴 상태에 있는 것을 확인할 수 있습니다. 이 코드를 최적화하려면 셰이더 코드에서 실행할 수 있는 작업을 찾아 GPU로 이동하여 리소스를 최대한 활용할 수 있습니다.

셰이더 코드 자체가 느리고 GPU가 과부하되면 어떻게 해야 할까요? CPU에서 불필요한 작업을 삭제하고 대신 프래그먼트 셰이더 코드에 작업을 추가하면 어떻게 될까요? 다음은 불필요하게 비싼 프래그먼트 셰이더입니다.

#ifdef GL_ES
precision highp float;
#endif
void main(void) {
  for(int i=0; i<9999; i++) {
    gl_FragColor = vec4(1.0, 0, 0, 1.0);
  }
}

이 셰이더를 사용하는 코드 트레이스는 어떻게 되나요?

느린 GPU 코드를 사용하는 경우 GPU 및 CPU 트레이스
느린 GPU 코드를 사용하는 경우 GPU 및 CPU 트레이스

프레임 지속 시간에 주목하세요. 여기서 반복 패턴은 약 2750ms에서 2950ms로, 200ms의 지속 시간 (약 5Hz의 프레임 속도)입니다. CrRendererMain 줄은 거의 완전히 비어 있습니다. GPU가 과부하 상태인 동안 CPU가 대부분의 경우 유휴 상태임을 의미합니다. 이는 셰이더가 너무 무거워졌다는 확실한 신호입니다.

프레임 속도가 느린 원인을 정확히 파악하지 못했다면 5Hz 업데이트를 관찰하고 게임 코드로 이동하여 게임 로직을 최적화하거나 삭제해 보고 싶을 수 있습니다. 이 경우에는 아무런 소용이 없습니다. 게임 루프의 로직이 시간을 소모하는 것이 아니기 때문입니다. 사실, 이 트레이스가 나타내는 것은 CPU가 유휴 상태로 유지되는 상태라는 점에서, 각 프레임에 더 많은 CPU 작업을 하면 본질적으로 '무료'가 되므로 더 많은 작업을 해도 프레임이 걸리는 시간에 영향을 미치지 않습니다.

실제 사례

이제 실제 게임의 데이터 추적 방법을 살펴보겠습니다. 개방형 웹 기술로 빌드된 게임의 한 가지 멋진 점 중 하나는 좋아하는 제품에서 무슨 일이 일어나고 있는지 확인할 수 있다는 것입니다. 프로파일링 도구를 테스트하려면 Chrome 웹 스토어에서 좋아하는 WebGL 제목을 선택하고 about:tracing을 사용하여 프로파일링할 수 있습니다. 다음은 뛰어난 WebGL 게임 Skid Racer에서 가져온 예시 트레이스입니다.

실제 게임 추적
실제 게임 추적

각 프레임에는 약 20ms가 걸리는 것으로 보입니다. 즉, 프레임 속도는 약 50FPS입니다. CPU와 GPU 간에 작업이 균형을 이루고 있지만 GPU 수요가 가장 많은 리소스입니다. WebGL 게임의 실제 예를 프로파일링하는 방법을 알아보고 싶다면 WebGL로 구축된 다음과 같은 Chrome 웹 스토어 타이틀을 사용해 보세요.

결론

게임을 60Hz에서 실행하려면 모든 프레임에 대해 모든 작업이 CPU 16ms와 GPU 시간 16ms에 들어가야 합니다. 병렬로 사용할 수 있는 두 가지 리소스가 있으며 두 리소스 간에 작업을 전환하여 성능을 극대화할 수 있습니다. Chrome의 about:tracing 보기는 코드가 실제로 어떤 작업을 하는지 파악할 수 있는 귀중한 도구이며 적절한 문제를 해결하여 개발 시간을 극대화하는 데 도움이 됩니다.

다음 단계

GPU 외에 Chrome 런타임의 다른 부분도 추적할 수 있습니다. Chrome의 초기 버전인 Chrome Canary는 IO, IndexedDB 및 기타 여러 활동을 추적하도록 계측됩니다. 추적 이벤트의 현재 상태를 자세히 알아보려면 이 Chromium 도움말을 참고하세요.

웹 게임 개발자라면 아래 동영상을 시청하세요. GDC 2012에서 Google의 게임 개발자 지원 팀에서 Chrome 게임의 성능 최적화에 대해 발표한 프레젠테이션입니다.