측정할 수 없다면 개선도 불가능합니다.
켈빈 로드
HTML5 게임을 더 빠르게 실행하려면 먼저 성능 병목 현상을 정확히 찾아내야 하지만 이는 어려울 수 있습니다. 초당 프레임 수 (FPS) 데이터 평가는 시작이지만 전체 그림을 보려면 Chrome 활동의 미묘한 차이를 파악해야 합니다.
about:tracing 도구는 본질적으로 의도적인 추측으로만 성능 향상을 목표로 하는 성급한 해결 방법을 피할 수 있도록 도와주는 유용한 정보를 제공합니다. 많은 시간과 에너지를 절약하고, Chrome이 각 프레임에서 어떤 작업을 하는지 더 명확하게 파악하며, 이 정보를 사용하여 게임을 최적화할 수 있습니다.
about:tracing 소개
Chrome의 about:tracing 도구를 사용하면 일정 기간 동안의 모든 Chrome 활동을 아주 세부적으로 들여다볼 수 있어 처음에는 복잡하게 느껴질 수 있습니다. Chrome의 많은 함수는 기본적으로 추적하도록 구현되어 있으므로 수동 계측을 하지 않고도 about:tracing을 사용하여 실적을 추적할 수 있습니다. (JS 수동으로 계측하기에 대한 이후 섹션 참조)
추적 뷰를 보려면 'about:tracing'을 입력하면 됩니다. 을 입력합니다.
<ph type="x-smartling-placeholder">추적 도구에서 기록을 시작하고 몇 초 동안 게임을 실행한 후 트레이스 데이터를 확인할 수 있습니다. 다음은 데이터가 어떻게 표시되는지 보여주는 예입니다.
<ph type="x-smartling-placeholder">네, 약간 혼란스럽네요. 읽는 방법에 관해 이야기해 보겠습니다.
각 행은 프로파일링 중인 프로세스를 나타내고, 왼쪽-오른쪽 축은 시간을 나타내고, 색상이 지정된 각 상자는 계측 함수 호출입니다. 다양한 종류의 리소스에 대한 행이 있습니다. 게임 프로파일링에서 가장 흥미로운 것은 GPU (그래픽 처리 장치)가 수행하는 작업을 보여주는 CrGpuMain과 CrRendererMain입니다. 각 트레이스에는 트레이스 기간 동안 열린 각 탭의 CrRendererMain 행이 포함됩니다 (about:tracing 탭 자체 포함).
트레이스 데이터를 읽을 때 가장 먼저 할 일은 어떤 CrRendererMain 행이 게임에 해당하는지 확인하는 것입니다.
<ph type="x-smartling-placeholder">이 예에서 두 개의 후보는 2216과 6516입니다. 안타깝게도 현재 애플리케이션을 선택할 수 있는 효과적인 방법은 없습니다. 주기적인 업데이트를 많이 수행하는 줄을 찾는 것 (또는 트레이스 데이터가 포함된 코드를 수동으로 계측한 코드를 수동으로 계측한 경우 트레이스 데이터가 포함된 줄을 찾는 것)을 찾는 것뿐입니다. 이 예에서는 6516이 업데이트 빈도에서 메인 루프를 실행하는 것으로 보입니다. 추적을 시작하기 전에 다른 탭을 모두 닫으면 올바른 CrRendererMain을 더 쉽게 찾을 수 있습니다. 하지만 게임 이외의 프로세스에 대한 CrRendererMain 행이 있을 수도 있습니다.
프레임 찾기
게임의 추적 도구에서 올바른 행을 찾았다면 다음 단계는 기본 루프를 찾는 것입니다. 메인 루프는 추적 데이터에서 반복되는 패턴처럼 보입니다. W, A, S, D 키를 사용하여 추적 데이터를 탐색할 수 있습니다. A와 D는 왼쪽이나 오른쪽으로 이동 (시간을 앞뒤로)하고 W와 S로 데이터를 확대/축소합니다. 게임이 60Hz에서 실행되는 경우 메인 루프는 16밀리초마다 반복되는 패턴일 것으로 예상됩니다.
<ph type="x-smartling-placeholder">게임의 하트비트를 찾으면 코드가 각 프레임에서 정확히 무엇을 하고 있는지 살펴볼 수 있습니다. W, A, S, D를 사용하여 함수 상자의 텍스트를 읽을 수 있을 때까지 확대합니다.
<ph type="x-smartling-placeholder">이 상자 모음은 각 호출이 색상이 지정된 상자로 표시된 일련의 함수 호출을 보여줍니다. 각 함수는 위에 있는 상자에 의해 호출되었으므로 이 경우에는 RenderWidget::OnSwapBuffersComplete라는 MessageLoop::RunTask가 표시되며 RenderWidget::DoDeferredUpdate 등으로 호출됩니다. 이 데이터를 읽으면 각 실행에 걸린 시간과 소요 시간에 대한 전체적인 보기를 얻을 수 있습니다.
하지만 여기가 조금 끈적해지죠. about:tracing을 통해 공개된 정보는 Chrome 소스 코드의 원시 함수 호출입니다. 이름을 보고 각 함수가 어떤 역할을 하는지 충분히 추측할 수 있지만 정보가 사용자 친화적이지 않습니다. 프레임의 전반적인 흐름을 확인하는 것은 유용하지만 실제로 무슨 일이 일어나고 있는지 파악하려면 좀 더 사람이 읽을 수 있는 것이 필요합니다.
트레이스 태그 추가
다행히 코드에 수동 계측을 추가하여 트레이스 데이터를 만드는 편리한 방법(console.time
및 console.timeEnd
)이 있습니다.
console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");
위의 코드는 지정된 태그를 사용하여 추적 뷰 이름에 새 상자를 만듭니다. 따라서 앱을 다시 실행하면 'update'가 표시됩니다. 'render'를 사용하여 체크박스에 표시됩니다.
<ph type="x-smartling-placeholder">이를 통해 사람이 읽을 수 있는 추적 데이터를 만들어 코드에서 핫스팟을 추적할 수 있습니다.
GPU와 CPU 중 무엇을 사용해야 하나요?
하드웨어 가속 그래픽을 사용하는 경우 프로파일링 중에 물어볼 수 있는 가장 중요한 질문 중 하나는 이 코드가 GPU에 바인딩되었는지 아니면 CPU에 바인딩되었는지 여부입니다. 각 프레임에서 GPU에서 렌더링 작업을 실행하고 CPU에서 일부 로직을 실행합니다. 게임을 느려지게 만드는 요인을 이해하려면 두 리소스 간에 작업이 어떻게 균형을 이루는지 확인해야 합니다.
먼저 CrGPUMain이라는 추적 뷰에서 GPU가 특정 시간에 사용 중인지 나타내는 줄을 찾습니다.
게임의 모든 프레임으로 인해 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");
이제 다음과 같은 트레이스가 표시됩니다.
이 trace는 무엇을 알 수 있나요? 사진에서 프레임이 약 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);
}
}
이 셰이더를 사용하는 코드의 트레이스는 어떤 모습일까요?
<ph type="x-smartling-placeholder">프레임의 지속 시간을 다시 한 번 살펴보겠습니다. 여기서 반복 패턴은 약 2750ms에서 2950ms 사이이며, 200ms의 지속 시간 (약 5Hz의 프레임 속도)입니다. CrRendererMain 행은 거의 비어 있습니다. 즉, GPU가 과부하된 상태에서 CPU는 대부분 유휴 상태입니다. 이는 셰이더가 너무 무겁다는 확실한 신호입니다.
프레임 속도가 낮은 원인을 정확히 파악하지 못했다면 5Hz 업데이트를 관찰하고 게임 코드로 이동하여 게임 로직을 최적화하거나 제거하려고 시도할 수 있습니다. 이 경우에는 게임 루프의 로직이 시간을 소모하지 않으므로 전혀 도움이 되지 않습니다. 실제로 이 트레이스에서 나타내는 것은 각 프레임에 더 많은 CPU 작업을 실행하면 기본적으로 '사용 가능'합니다. CPU가 유휴 상태에 있다는 점에서 더 많은 작업을 해도 프레임이 걸리는 시간에 영향을 미치지 않습니다.
실제 사례
이제 실제 게임의 데이터를 추적하는 방법을 살펴보겠습니다. 개방형 웹 기술로 구축된 게임의 장점 중 하나는 좋아하는 제품에서 어떤 일이 일어나고 있는지 확인할 수 있다는 것입니다. 프로파일링 도구를 테스트하려면 Chrome 웹 스토어에서 좋아하는 WebGL 타이틀을 선택하고 about:tracing으로 프로파일링할 수 있습니다. 다음은 뛰어난 WebGL 게임인 Skid Racer에서 가져온 트레이스 예입니다.
<ph type="x-smartling-placeholder">각 프레임에 약 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 게임의 성능 최적화에 관한 프레젠테이션입니다.