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

Lilli Thompson
Lilli Thompson

측정할 수 없다면 개선할 수 없습니다.

켈빈 경

HTML5 게임을 더 빠르게 실행하려면 먼저 성능 병목 현상을 파악해야 하지만 쉽지 않을 수 있습니다. 프레임 속도 (FPS) 데이터를 평가하는 것이 시작이지만 전체적인 상황을 파악하려면 Chrome 활동의 미묘한 차이를 파악해야 합니다.

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

about:tracing, 안녕하세요.

Chrome의 about:tracing 도구는 일정 기간 동안의 모든 Chrome 활동을 매우 상세하게 보여주므로 처음에는 다소 부담스러울 수 있습니다. Chrome의 많은 함수는 기본적으로 추적을 위해 계측되므로 수동 계측을 수행하지 않고도 about:tracing를 사용하여 성능을 추적할 수 있습니다. (JS를 수동으로 계측하는 방법에 관한 뒷부분 섹션 참고)

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

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

추적 도구에서 녹화를 시작하고 게임을 몇 초 동안 실행한 다음 트레이스 데이터를 볼 수 있습니다. 다음은 데이터가 표시되는 방식의 예입니다.

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

네, 혼란스러우실 수 있습니다. 읽는 방법을 알아보겠습니다.

각 행은 프로파일링되는 프로세스를 나타내고 좌우 축은 시간을 나타내며 각 색상 상자는 계측된 함수 호출입니다. 다양한 종류의 리소스에 관한 행이 있습니다. 게임 프로파일링에 가장 흥미로운 것은 그래픽 처리 장치 (GPU)가 실행 중인 작업을 보여주는 CrGpuMain과 CrRendererMain입니다. 각 트레이스에는 트레이스 기간 동안 열린 각 탭 (about:tracing 탭 자체 포함)의 CrRendererMain 라인이 포함됩니다.

트레이스 데이터를 읽을 때 가장 먼저 해야 할 일은 게임에 해당하는 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::OnSwapBuffersComplete가 RenderWidget::DoDeferredUpdate를 호출하는 등 계속해서 호출된 것을 확인할 수 있습니다. 이 데이터를 읽으면 무엇이 무엇을 호출했는지, 각 실행에 걸린 시간을 전체적으로 확인할 수 있습니다.

하지만 여기서 약간 문제가 발생합니다. about:tracing에서 노출하는 정보는 Chrome 소스 코드의 원시 함수 호출입니다. 이름을 통해 각 함수가 하는 일을 추측할 수는 있지만, 이 정보는 사용자 친화적이지 않습니다. 프레임의 전반적인 흐름을 확인하는 데는 유용하지만 실제로 무슨 일이 일어나고 있는지 파악하려면 좀 더 사람이 읽을 수 있는 것이 필요합니다.

추적 태그 추가

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

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

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

수동으로 추가된 태그
태그를 직접 추가했습니다

이를 사용하여 사람이 읽을 수 있는 추적 데이터를 만들어 코드의 핫스팟을 추적할 수 있습니다.

GPU 또는 CPU?

하드웨어 가속 그래픽을 사용할 때 프로파일링 중에 물어볼 수 있는 가장 중요한 질문 중 하나는 이 코드가 GPU 제약 조건이 적용되는지 CPU 제약 조건이 적용되는지 여부입니다. 각 프레임에서 GPU에서 렌더링 작업을 하고 CPU에서 로직 작업을 실행합니다. 게임이 느려지는 원인을 파악하려면 두 리소스 간에 작업의 균형이 어떻게 유지되는지 확인해야 합니다.

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

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 라인이 거의 완전히 비어 있으므로 CPU는 대부분 유휴 상태이고 GPU는 과부하 상태입니다. 이는 셰이더가 너무 무겁다는 확실한 신호입니다.

프레임 속도가 느려지는 원인을 정확히 파악하지 못한 경우 5Hz 업데이트를 관찰하고 게임 코드로 이동하여 게임 로직을 최적화하거나 삭제하려고 할 수 있습니다. 이 경우 게임 루프의 로직이 시간을 소모하는 것이 아니므로 전혀 도움이 되지 않습니다. 실제로 이 트레이스는 CPU가 유휴 상태로 대기하고 있으므로 프레임마다 더 많은 CPU 작업을 실행하는 것이 본질적으로 '무료'라는 것을 나타냅니다. 따라서 더 많은 작업을 할당해도 프레임이 걸리는 시간에 영향을 미치지 않습니다.

실제 예시

이제 실제 게임의 추적 데이터가 어떤 모습인지 확인해 보겠습니다. 개방형 웹 기술로 빌드된 게임의 멋진 점 중 하나는 좋아하는 제품에서 어떤 일이 일어나고 있는지 확인할 수 있다는 것입니다. 프로파일링 도구를 테스트하려면 Chrome 웹 스토어에서 좋아하는 WebGL 타이틀을 선택하고 about:tracing로 프로파일링하면 됩니다. 다음은 훌륭한 WebGL 게임인 Skid Racer에서 가져온 추적의 예입니다.

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

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

결론

게임을 60Hz로 실행하려면 모든 프레임에서 모든 작업이 16ms의 CPU 시간과 16ms의 GPU 시간에 맞아야 합니다. 동시에 활용할 수 있는 두 가지 리소스가 있으며, 성능을 극대화하기 위해 두 리소스 간에 작업을 전환할 수 있습니다. Chrome의 about:tracing 보기는 코드가 실제로 실행하는 작업을 파악하는 데 매우 유용한 도구이며, 적절한 문제를 해결하여 개발 시간을 최대화하는 데 도움이 됩니다.

다음 단계

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

웹 게임 개발자인 경우 아래 동영상을 시청하세요. GDC 2012에서 Google의 게임 개발자 어드보킷팀이 Chrome 게임의 성능 최적화에 대해 발표한 내용입니다.