시스템 추적 구성

시스템 추적을 구성하여 단기간에 걸친 앱의 CPU 및 스레드 프로필을 캡처할 수 있습니다. 그러면 시스템 트레이스의 출력 보고서를 사용하여 게임 성능을 개선할 수 있습니다.

게임 기반 시스템 트레이스 설정

Systrace 도구는 다음 두 가지 방법으로 사용할 수 있습니다.

Systrace는 다음과 같은 하위 수준 도구입니다.

  • 실측을 제공합니다. Systrace는 커널에서 직접 출력을 캡처하므로, 여기서 캡처되는 측정항목은 일련의 시스템 호출에서 보고되는 측정항목과 거의 동일합니다.
  • 리소스를 거의 소비하지 않습니다. Systrace는 데이터를 메모리 내 버퍼에 스트리밍하기 때문에 기기에 미치는 오버헤드가 대체로 1% 미만으로 아주 낮습니다.

최적의 설정

다음과 같이 도구에 적절한 인수 집합을 제공하는 것이 중요합니다.

  • 카테고리: 게임 기반 시스템 트레이스를 위해 사용할 최상의 카테고리 집합은 다음과 같습니다. {sched, freq, idle, am, wm, gfx, view, sync, binder_driver, hal, dalvik}
  • 버퍼 사이즈: CPU 코어당 버퍼 사이즈가 10MB이면 길이가 약 20초인 추적이 가능하다는 것이 일반적인 규칙입니다. 예를 들어 기기에 쿼드 코어 CPU 2개(총 코어 8개)가 있는 경우 systrace 프로그램에 전달할 적절한 값은 80,000KB(80MB)입니다.

    게임에서 컨텍스트 전환을 상당히 많이 실행하는 경우 버퍼를 CPU 코어당 15MB로 늘리세요.

  • 맞춤 이벤트: 게임 내에서 캡처하도록 맞춤 이벤트를 정의하는 경우 -a 플래그를 사용 설정합니다. 그러면 Systrace의 출력 보고서에 이 맞춤 이벤트가 포함될 수 있습니다.

systrace 명령줄 프로그램을 사용하는 경우 다음 명령어를 사용하여 카테고리 집합, 버퍼 사이즈, 맞춤 이벤트와 관련한 권장사항을 적용하는 시스템 트레이스를 캡처합니다.

python systrace.py -a com.example.myapp -b 80000 -o my_systrace_report.html \
  sched freq idle am wm gfx view sync binder_driver hal dalvik

기기에서 Systrace 시스템 앱을 사용하는 경우에는 다음 단계를 완료하여 카테고리 집합, 버퍼 사이즈, 맞춤 이벤트와 관련한 권장사항을 적용하는 시스템 트레이스를 캡처합니다.

  1. Trace debuggable applications 옵션을 사용 설정합니다.

    이 설정을 사용하기 위해서는 기기에 256MB나 512MB의 가용 메모리(CPU의 코어가 4개인지 8개인지에 따라 결정됨)가 있어야 하며 64MB 크기의 각 메모리를 인접한 청크로 사용할 수 있어야 합니다.

  2. Categories를 선택한 후 다음 목록의 카테고리를 사용 설정합니다.

    • am: 활동 관리자
    • binder_driver: 바인더 커널 드라이버
    • dalvik: Dalvik VM
    • freq: CPU 주파수
    • gfx: 그래픽
    • hal: 하드웨어 모듈
    • idle: CPU Idle
    • sched: CPU 예약
    • sync: 동기화
    • view: 뷰 시스템
    • wm: 창 관리자
  3. Record tracing을 사용 설정합니다.

  4. 게임을 로드합니다.

  5. 게임에서 기기 성능을 측정할 게임플레이에 상응하는 상호작용을 실행합니다.

  6. 게임에서 바람직하지 않은 동작을 발견하면 즉시 시스템 추적을 사용 중지합니다.

문제를 추가로 분석하는 데 필요한 성능 통계를 캡처했습니다.

디스크 공간을 절약하기 위해 기기 내 시스템 추적은 압축한 트레이스 형식(*.ctrace)으로 파일을 저장합니다. 보고서를 생성할 때 이 파일을 압축 해제하려면 명령줄 프로그램을 사용하고 --from-file 옵션을 포함합니다.

python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \
  -o my_systrace_report.html

특정 성능 영역 개선하기

이 섹션에서는 모바일 게임에서 발견되는 일반적인 성능 문제 몇 가지에 중점을 두고 설명하고 게임의 이러한 측면을 파악하여 개선하는 방법을 설명합니다.

로드 속도

플레이어는 최대한 빨리 게임 액션에 돌입하고 싶어 합니다. 따라서 게임의 로드 시간을 최대한 많이 개선하는 것이 중요합니다. 일반적으로 로드 시간에 도움이 되는 다음 조치는 다음과 같습니다.

  • 지연 로드를 수행합니다. 게임에서 연속되는 장면이나 레벨에 동일한 애셋을 사용하는 경우 이 애셋을 한 번만 로드합니다.
  • 애셋 크기를 줄입니다. 그러면 이 애셋의 미압축 버전을 게임 APK와 번들로 묶을 수 있습니다.
  • 디스크에 효율적인 압축 방법을 사용합니다. 이런 방법의 예로는 zlib를 들 수 있습니다.
  • 모노 대신 IL2CPP를 사용합니다. (Unity를 사용하는 경우에만 적용됩니다.) IL2CPP를 사용하면 C# 스크립트 실행 성능이 더 좋습니다.
  • 게임을 멀티스레드로 만듭니다. 자세한 내용은 프레임 속도 일관성 섹션을 참고하세요.

프레임 속도 일관성

게임플레이 환경에서 가장 중요한 요소 중 하나는 일관된 프레임 속도를 확보하는 것입니다. 이 목표를 더 쉽게 달성하려면 이 섹션에서 설명하는 최적화 기법을 따르세요.

멀티스레드

여러 플랫폼용으로 개발할 때는 게임 내의 모든 활동을 단일 스레드에 배치하는 것이 당연합니다. 이 실행 방법은 많은 게임 엔진에서 간단히 구현할 수 있지만 Android 기기에서 실행하는 경우 최적의 방법이 아닙니다. 결과적으로, 단일 스레드 게임은 로드 속도가 느리고 프레임 속도가 일관되지 않은 경우가 많습니다.

그림 1의 Systrace는 한 번에 한 CPU에서만 실행되는 게임에서 일반적으로 나타나는 동작을 보여줍니다.

시스템 트레이스 내의 스레드 다이어그램

그림 1. 단일 스레드 게임의 Systrace 보고서

게임 성능을 개선하려면 게임을 멀티스레드로 만드세요. 일반적으로 최상의 모델은 다음 두 스레드로 만드는 것입니다.

  • 게임 스레드: 게임의 기본 모듈을 포함하며 렌더링 명령어를 전송합니다.
  • 렌더링 스레드: 렌더링 명령어를 수신하여 기기 GPU가 장면을 표시하는 데 사용할 수 있는 그래픽 명령어로 변환합니다.

Vulkan API는 이 모델을 기반으로 확장된 것으로, 일반적인 버퍼 2개를 병렬로 푸시할 수 있습니다. 이 기능을 사용하면 여러 개의 렌더링 스레드를 여러 CPU에 분산하여 특정 장면의 렌더링 시간을 더 개선할 수 있습니다.

엔진별 변경사항을 적용하여 게임의 멀티스레드 성능을 향상할 수도 있습니다.

  • Unity 게임 엔진을 사용하여 게임을 개발하는 경우 Multithreaded Rendering 옵션과 GPU Skinning 옵션을 사용 설정합니다.
  • 맞춤 렌더링 엔진을 사용하는 경우 렌더링 명령어 파이프라인과 그래픽 명령어 파이프라인을 올바르게 정렬해야 합니다. 그러지 않으면 게임의 장면이 표시될 때 지연이 발생할 수 있습니다.

이러한 변경사항을 적용하고 나면 그림 2와 같이 게임이 CPU 2개 이상을 동시에 사용 중임을 확인할 수 있습니다.

시스템 트레이스 내의 스레드 다이어그램

그림 2. 멀티스레드 게임의 Systrace 보고서

UI 요소 로드

시스템 트레이스 내의 프레임 스택 다이어그램
그림 3. UI 요소 수십 개를 동시에 렌더링하는 게임의 Systrace 보고서

풍부한 기능을 갖춘 게임을 만들 때는 다양한 옵션과 액션을 동시에 플레이어에게 표시하려는 경향이 있을 수 있습니다. 하지만 일관된 프레임 속도를 유지하려면 비교적 크기가 작은 모바일 디스플레이를 고려하고 UI를 최대한 단순하게 유지하는 것이 중요합니다.

그림 3의 Systrace 보고서는 휴대기기의 기능에 비해 렌더링하려는 요소가 너무 많은 UI 프레임의 예입니다.

UI 업데이트 시간을 2~3밀리초로 단축하는 것을 목표로 하는 것이 좋습니다. 이렇게 빠른 업데이트를 달성하려면 다음과 비슷하게 최적화를 수행하면 됩니다.

  • 화면에서 이동한 요소만 업데이트합니다.
  • UI 텍스처와 레이어의 수를 제한합니다. 동일한 소재를 사용하는 그래픽 호출(예: 셰이더, 텍스처)을 결합해 보세요.
  • 요소 애니메이션 작업을 GPU에서 처리하도록 지연합니다.
  • 더 공격적인 절두체 및 오클루전 컬링을 수행합니다.
  • 가능하면 Vulkan API를 사용하여 그리기 작업을 수행합니다. 그리기 호출 오버헤드는 Vulkan에서 더 낮습니다.

전력 소모량

이전 섹션에서 설명한 대로 최적화한 후에도 게임플레이의 첫 45~50분 이내에 게임의 프레임 속도가 떨어질 수 있습니다. 그뿐만 아니라 기기가 발열하기 시작하고 시간이 지남에 따라 배터리 소모량이 늘어날 수 있습니다.

많은 경우 이런 바람직하지 않은 발열과 전력 소모는 게임의 워크로드가 기기의 CPU 전체에 분산되는 방식과 관련 있습니다. 게임의 전략 소모 효율성을 높이려면 다음 섹션에서 설명하는 권장사항을 적용합니다.

메모리를 많이 사용하는 스레드를 단일 CPU에 유지하기

많은 휴대기기에서 L1 캐시는 특정 CPU에 있고 L2 캐시는 클록을 공유하는 CPU 집합에 있습니다. L1 캐시 적중을 최대화하려면 일반적으로 메모리를 많이 사용하는 다른 스레드와 함께 게임 기본 스레드를 단일 CPU에서 실행되도록 유지하는 것이 가장 좋습니다.

단기 작업을 전력 소모가 적은 CPU에서 처리하도록 지연하기

Unity를 포함한 대부분의 게임 엔진은 작업자 스레드 작업을 게임 메인 스레드와 관련된 다른 CPU에서 처리되도록 지연할 것을 인지하고 있지만 기기의 특정 아키텍처를 인식하지 않으며 게임의 워크로드를 개발자만큼 잘 예측할 수 없습니다.

대부분의 단일 칩 시스템 기기에는 공유 클록이 2개 이상 있으며, 클록 하나는 기기의 빠른 CPU에 해당하고 하나는 기기의 느린 CPU에 해당합니다. 이런 아키텍처를 사용하면 빠른 CPU 하나가 최대 속도로 작동해야 하는 경우 나머지 빠른 CPU도 모두 최대 속도로 작동하는 결과가 발생합니다.

그림 4의 보고서 예에서는 빠른 CPU를 활용하는 게임을 보여줍니다. 하지만 이렇게 높은 수준의 활동은 빠른 시간 내에 상당히 많은 전력 소모와 발열을 야기합니다.

시스템 트레이스 내의 스레드 다이어그램

그림 4. 기기 CPU를 대상으로 한 최적이 아닌 스레드 할당을 보여주는 Systrace 보고서

전체 전력 소모량을 줄이려면 오디오 로드, 작업자 스레드 실행, choreographer 실행과 같이 비교적 기간이 짧은 작업을 기기의 느린 CPU 집합에서 처리되도록 지연하도록 스케줄러에 제안하는 것이 가장 좋습니다. 원하는 프레임 속도를 유지하면서 이러한 작업을 느린 CPU로 최대한 많이 이전하세요.

대부분의 기기에서 느린 CPU가 빠른 CPU보다 먼저 나열되지만, 기기의 SOC에서 이 순서대로 사용된다고 가정할 수는 없습니다. 확인하려면 GitHub에 나온 CPU 토폴로지 검색 코드와 비슷한 명령어를 실행하세요.

기기의 CPU 중 느린 CPU가 어느 것인지 알고 나면 단기 스레드의 관련성을 선언할 수 있습니다. 그러면 기기의 스케줄러가 이에 따라 일정을 예약합니다. 선언하려면 각 스레드 내에 다음 코드를 추가합니다.

#include <sched.h>
#include <sys/types.h>
#include <unistd.h>

pid_t my_pid; // PID of the process containing your thread.

// Assumes that cpu0, cpu1, cpu2, and cpu3 are the "slow CPUs".
cpu_set_t my_cpu_set;
CPU_ZERO(&my_cpu_set);
CPU_SET(0, &my_cpu_set);
CPU_SET(1, &my_cpu_set);
CPU_SET(2, &my_cpu_set);
CPU_SET(3, &my_cpu_set);
sched_setaffinity(my_pid, sizeof(cpu_set_t), &my_cpu_set);

열 응력

기기가 너무 뜨거워지면 CPU 또는 GPU가 제한될 수 있으며 이로 인해 예상치 못한 방식으로 게임에 영향을 줄 수 있습니다. 복잡한 그래픽, 과도한 계산 또는 지속적인 네트워크 활동이 포함된 게임에서 문제가 발생할 가능성이 높습니다.

Thermal API를 사용하여 기기의 온도 변화를 모니터링하고 낮은 전력 사용과 낮은 기기 온도를 유지하기 위해 조치를 취합니다. 기기에서 열 응력을 보고하는 경우 진행 중인 활동을 중단하여 전력 사용을 줄입니다. 예를 들어 프레임 속도 또는 다각형 테셀레이션을 줄입니다.

먼저, PowerManager 객체를 선언한 다음 onCreate() 메서드에서 초기화합니다. 객체에 열 상태 리스너를 추가합니다.

Kotlin

class MainActivity : AppCompatActivity() {
    lateinit var powerManager: PowerManager

    override fun onCreate(savedInstanceState: Bundle?) {
        powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
        powerManager.addThermalStatusListener(thermalListener)
    }
}

Java

public class MainActivity extends AppCompatActivity {
    PowerManager powerManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        powerManager.addThermalStatusListener(thermalListener);
    }
}

리스너가 상태 변경을 감지할 때 실행할 작업을 정의합니다. 게임에서 C/C++를 사용하는 경우 onThermalStatusChanged()의 열 상태 수준에 코드를 추가하여 JNI를 통해 네이티브 게임 코드를 호출하거나 네이티브 Thermal API를 사용합니다.

Kotlin

val thermalListener = object : PowerManager.OnThermalStatusChangedListener() {
    override fun onThermalStatusChanged(status: Int) {
        when (status) {
            PowerManager.THERMAL_STATUS_NONE -> {
                // No thermal status, so no action necessary
            }

            PowerManager.THERMAL_STATUS_LIGHT -> {
                // Add code to handle light thermal increase
            }

            PowerManager.THERMAL_STATUS_MODERATE -> {
                // Add code to handle moderate thermal increase
            }

            PowerManager.THERMAL_STATUS_SEVERE -> {
                // Add code to handle severe thermal increase
            }

            PowerManager.THERMAL_STATUS_CRITICAL -> {
                // Add code to handle critical thermal increase
            }

            PowerManager.THERMAL_STATUS_EMERGENCY -> {
                // Add code to handle emergency thermal increase
            }

            PowerManager.THERMAL_STATUS_SHUTDOWN -> {
                // Add code to handle immediate shutdown
            }
        }
    }
}

Java

PowerManager.OnThermalStatusChangedListener thermalListener =
    new PowerManager.OnThermalStatusChangedListener () {

    @Override
    public void onThermalStatusChanged(int status) {

        switch (status)
        {
            case PowerManager.THERMAL_STATUS_NONE:
                // No thermal status, so no action necessary
                break;

            case PowerManager.THERMAL_STATUS_LIGHT:
                // Add code to handle light thermal increase
                break;

            case PowerManager.THERMAL_STATUS_MODERATE:
                // Add code to handle moderate thermal increase
                break;

            case PowerManager.THERMAL_STATUS_SEVERE:
                // Add code to handle severe thermal increase
                break;

            case PowerManager.THERMAL_STATUS_CRITICAL:
                // Add code to handle critical thermal increase
                break;

            case PowerManager.THERMAL_STATUS_EMERGENCY:
                // Add code to handle emergency thermal increase
                break;

            case PowerManager.THERMAL_STATUS_SHUTDOWN:
                // Add code to handle immediate shutdown
                break;
        }
    }
};

터치 후 표시까지의 지연 시간

프레임을 최대한 빨리 렌더링하는 게임의 경우 프레임 버퍼가 과도하게 채워지는 GPU 바인드 시나리오가 생깁니다. CPU가 GPU를 위해 대기해야 하며, 이로 인해 플레이어의 입력 시점과 이 입력이 화면에 적용되는 시점 사이에 눈에 띄는 지연이 발생합니다.

게임의 프레임 속도를 개선할 수 있을지 여부를 판단하려면 다음 단계를 완료하세요.

  1. gfxinput 카테고리를 포함하는 Systrace 보고서를 생성합니다. 이러한 카테고리는 터치 후 표시까지의 지연 시간을 결정하는 데 특히 유용한 측정치를 구성합니다.
  2. Systrace 보고서의 SurfaceView 섹션을 확인합니다. 버퍼가 과도하게 채워지면 그림 5와 같이 대기 중인 버퍼 그리기의 수가 1과 2 사이에서 조정됩니다.

    시스템 트레이스 내의 버퍼 큐 다이어그램

    그림 5. 과도하게 채워져 주기적으로 그리기 명령어를 수락할 수 없을 정도로 꽉 찬 버퍼를 보여주는 Systrace 보고서

프레임 속도의 이러한 불일치를 완화하려면 다음 섹션에서 설명하는 작업을 완료하세요.

게임에 Android Frame Pacing API 통합하기

Android Frame Pacing API를 사용하면 프레임 전환을 수행하고 게임의 프레임 속도를 더 일관되게 유지하도록 전환 간격을 정의할 수 있습니다.

비 UI 애셋의 해상도 낮추기

최신 휴대기기의 디스플레이에 있는 픽셀은 플레이어가 처리할 수 있는 것보다 더 많습니다. 픽셀 5개 또는 심지어 10개를 실행할 때도 모두 단일 색상을 포함하도록 다운샘플링해도 괜찮습니다. 디스플레이 캐시 대부분의 구조를 고려할 때 1차원만 따라 해상도를 낮추는 것이 가장 좋습니다.

하지만 게임 UI 요소의 해상도를 낮추지는 마세요. 이러한 요소의 선 두께를 보존하여 모든 플레이어에게 충분한 터치 영역 크기를 유지하는 것이 중요합니다.

원활한 렌더링

SurfaceFlinger가 디스플레이 버퍼에 연결되어 게임 장면을 표시할 때 CPU 활동이 잠깐 증가합니다. 이렇게 CPU 활동이 급격히 증가하는 현상이 균일하지 않게 발생한다면 게임에서 끊김 문제가 발생할 수 있습니다. 그림 6의 다이어그램은 이 문제가 발생하는 이유를 보여줍니다.

프레임이 그리기 시작한 시점이 너무 늦어서 Vsync 창이 누락된 프레임 다이어그램

그림 6. 프레임에 Vsync가 누락될 수 있는 이유를 보여주는 Systrace 보고서

프레임이 그리기 시작하는 시점이 너무 늦으면 지연 시간이 몇 밀리초라도 다음 디스플레이 창이 누락될 수 있습니다. 그러면 프레임은 다음 Vsync가 표시될 때까지(30FPS로 게임을 실행 중인 경우 33밀리초) 대기해야 하고 이로 인해 플레이어의 시점에서는 눈에 띄는 지연이 발생합니다.

이 문제를 해결하려면 Android Frame Pacing API를 사용하여 항상 새 프레임을 VSync 웨이브프런트에 제공하세요.

메모리 상태

게임을 오랫동안 실행할 때는 기기에서 메모리 부족 오류가 발생할 수 있습니다.

이런 경우에는 Systrace 보고서에서 CPU 활동을 확인하여 시스템이 kswapd 데몬을 호출하는 빈도를 검토합니다. 게임 실행 중에 발생한 호출이 많은 경우에는 게임이 메모리를 어떻게 관리하고 정리하는지 면밀하게 살펴보는 것이 가장 좋습니다.

자세한 내용은 효과적인 게임 메모리 관리를 참고하세요.

스레드 상태

Systrace 보고서에서 일반적인 요소를 탐색할 때 그림 7과 같이 보고서 내의 스레드를 선택하여 특정 스레드가 가능한 각 스레드 상태를 유지했던 시간을 확인할 수 있습니다.

Systrace 보고서 다이어그램

그림 7. 스레드를 선택하여 이 스레드의 상태 요약이 표시된 것을 보여주는 Systrace 보고서

그림 7과 같이 게임의 스레드가 '실행 중' 상태나 '실행 가능' 상태를 유지한 빈도가 부족하다는 것을 발견할 수도 있습니다. 특정 스레드가 주기적으로 비정상적인 상태로 전환될 수 있는 이유는 다음과 같습니다.

  • 스레드가 오랫동안 중지 중인 경우는 스레드에서 잠금 경합이 발생했거나 스레드가 GPU 활동을 대기하고 있을 수 있습니다.
  • 스레드가 지속적으로 I/O에서 차단되는 경우는 디스크에서 한 번에 읽는 데이터가 너무 많거나 게임 스래싱이 발생했을 수 있습니다.

추가 리소스

게임의 성능을 개선하는 방법을 자세히 알아보려면 다음과 같은 추가 리소스를 참고하세요.

동영상