GPU 렌더링 프로파일링으로 분석

GPU 렌더링 프로파일링 도구는 렌더링 파이프라인의 각 단계에서 이전 프레임을 렌더링하는 데 걸리는 상대적인 시간을 나타냅니다. 이 정보는 파이프라인의 병목 현상을 식별하는 데 도움이 되므로 앱의 렌더링 성능을 향상하기 위해 최적화해야 하는 항목을 파악할 수 있습니다.

이 페이지에서는 각 파이프라인 단계에서 발생하는 상황을 간략하게 설명하고 병목 현상을 일으킬 수 있는 문제에 관해 논의합니다. 이 페이지를 읽기 전에 GPU 렌더링 프로파일링에 있는 정보를 숙지해야 합니다. 또한 모든 단계가 유기적으로 잘 작동하는 방식을 이해하려면 렌더링 파이프라인의 작동 방식을 검토하는 것이 좋습니다.

시각적 표현

GPU 렌더링 프로파일링 도구는 단계와 각 단계의 상대적인 시간을 그래프(색상으로 구분된 히스토그램) 형태로 표시합니다. 그림 1은 이러한 표시의 예를 보여줍니다.

그림 1. GPU 렌더링 프로파일링 그래프

GPU 렌더링 프로파일링 그래프에 표시된 세로 막대의 각 세그먼트는 파이프라인의 단계를 나타내며 막대그래프에서 특정 색상을 사용하여 강조표시됩니다. 그림 2는 표시된 각 색상의 핵심 의미를 보여줍니다.

그림 2. GPU 렌더링 프로파일링 그래프 범례

각 색상의 의미를 이해한 후에는 앱의 특정 측면을 타겟팅하여 렌더링 성능을 최적화하도록 시도할 수 있습니다.

단계와 단계별 의미

이 섹션에서는 그림 2의 색상에 상응하는 각 단계에서 발생하는 상황과 주의해야 하는 병목 현상의 원인을 설명합니다.

입력 처리

파이프라인의 입력 처리 단계는 앱이 입력 이벤트를 처리하는 데 걸린 시간을 측정합니다. 이 측정항목은 앱이 입력 이벤트 콜백의 결과로 호출된 코드를 실행하는 데 걸린 시간을 나타냅니다.

이 세그먼트가 큰 경우

일반적으로 이 영역의 값이 큰 것은 입력-핸들러 이벤트 콜백 내에서 발생하는 작업이 너무 많거나 너무 복잡하기 때문입니다. 이러한 콜백은 항상 기본 스레드에서 발생하므로 이 문제를 해결하려면 작업을 직접 최적화하거나 다른 스레드로 작업을 오프로드하는 것에 초점을 두어야 합니다.

또한 RecyclerView 스크롤이 이 단계에 표시될 수도 있다는 점에 주목해야 합니다. RecyclerView는 터치 이벤트를 사용할 때 즉시 스크롤됩니다. 그 결과 새 항목 뷰를 확장하거나 채울 수 있습니다. 따라서 최대한 빨리 이 작업을 실행하는 것이 중요합니다. Traceview 또는 Systrace와 같은 프로파일링 도구를 사용하면 더 자세히 조사할 수 있습니다.

애니메이션

애니메이션 단계는 프레임에서 실행 중인 모든 애니메이터를 평가하는 데 걸린 시간을 보여줍니다. 가장 일반적인 애니메이터는 ObjectAnimator, ViewPropertyAnimator, 전환입니다.

이 세그먼트가 큰 경우

일반적으로 이 영역의 값이 큰 것은 애니메이션의 일부 속성 변경으로 인해 실행 중인 작업 때문입니다. 예를 들어 ListView 또는 RecyclerView를 스크롤하는 플링 애니메이션은 뷰를 크게 확장시키거나 채웁니다.

측정/레이아웃

Android에서 화면에 뷰 항목을 그리려면 뷰 계층 구조의 레이아웃과 뷰에 걸쳐 두 가지 특정 작업을 실행합니다.

먼저 시스템이 뷰 항목을 측정합니다. 모든 뷰와 레이아웃에는 화면의 객체 크기를 설명하는 특정 데이터가 있습니다. 일부 뷰는 특정 크기를 가질 수 있으며 그 외에는 상위 요소 레이아웃 컨테이너의 크기에 맞게 조정한 크기를 갖습니다.

둘째로 시스템은 뷰 항목을 배치합니다. 시스템이 하위 뷰의 크기를 계산하면 시스템은 레이아웃을 사용하여 뷰의 크기를 조정하고 화면에 위치를 지정할 수 있습니다.

시스템은 그리려는 뷰뿐만 아니라 루트 뷰까지 이러한 뷰의 상위 계층 구조를 측정하고 배치합니다.

이 세그먼트가 큰 경우

앱이 이 영역에서 프레임당 많은 시간을 소비한다면 일반적으로 배치되어야 하는 뷰의 양이 많거나 계층 구조의 잘못된 위치에서 이중 과세 같은 문제가 발생하기 때문입니다. 두 경우 모두 성능 문제를 해결하면 뷰 계층 구조의 성능이 향상됩니다.

onLayout(boolean, int, int, int, int) 또는 onMeasure(int, int)에 추가한 코드로 인해 성능 문제가 발생할 수도 있습니다. TraceviewSystrace를 사용하면 호출 스택을 확인하여 코드에서 발생할 수 있는 문제를 파악할 수 있습니다.

그리기

그리기 단계는 백그라운드 그리기 또는 텍스트 그리기와 같은 뷰의 렌더링 작업을 기본 그리기 명령어 시퀀스로 변환합니다. 시스템은 이러한 명령어를 표시 목록에 캡처합니다.

그리기 막대는 이 프레임의 화면에서 업데이트해야 하는 모든 뷰와 관련해 명령어를 표시 목록에 캡처하는 데 걸린 시간을 기록합니다. 측정된 시간은 앱의 UI 객체에 추가한 모든 코드에 적용됩니다. onDraw(), dispatchDraw()Drawable 클래스의 서브클래스에 속하는 다양한 draw ()methods가 그러한 코드의 예에 해당할 수 있습니다.

이 세그먼트가 큰 경우

간단히 말해 이 측정항목은 무효화된 각 뷰에서 모든 onDraw() 호출을 실행하는 데 걸린 시간을 보여줍니다. 이 측정에는 표시될 수 있는 하위 요소 및 드로어블로 그리기 명령어를 전달하는 데 걸리는 모든 시간이 포함됩니다. 이런 이유로, 막대가 급증한 경우 갑자기 많은 뷰가 무효가 된 것이 원인일 수 있습니다. 무효가 되면 뷰의 표시 목록을 다시 생성해야 합니다. 또는 onDraw() 메서드에 매우 복잡한 로직을 가진 몇몇 맞춤설정 뷰로 인해 시간이 길어질 수도 있습니다.

동기화/업로드

동기화 및 업로드 측정항목은 현재 프레임에서 비트맵 객체를 CPU 메모리에서 GPU 메모리로 전송하는 데 걸리는 시간을 나타냅니다.

CPU와 GPU는 서로 다른 프로세서로 처리 전용 RAM 영역이 다릅니다. Android에서 비트맵을 그릴 때 시스템은 GPU가 화면에 렌더링하기 전에 GPU 메모리로 비트맵을 전송합니다. 그런 다음, 텍스처가 GPU 텍스처 캐시에서 제거되지 않는 한 시스템이 데이터를 다시 전송할 필요가 없도록 GPU는 비트맵을 캐시합니다.

참고: Lollipop 기기에서 이 단계는 보라색입니다.

이 세그먼트가 큰 경우

프레임의 모든 리소스는 프레임을 그리는 데 사용되기 전에 GPU 메모리에 있어야 합니다. 즉, 이 측정항목의 값이 크면 작은 리소스 로드가 많이 있거나 적지만 매우 큰 리소스가 있다는 의미입니다. 일반적으로 앱이 화면 크기에 가까운 단일 비트맵을 표시하는 경우입니다. 또 다른 경우로 앱이 다수의 썸네일을 표시하는 것을 들 수 있습니다.

이 막대를 축소하려면 다음과 같은 기법을 사용하면 됩니다.

  • 비트맵 해상도가 표시되는 크기보다 많이 큰 것은 아닌지 확인합니다. 예를 들어, 앱은 1024x1024 이미지를 48x48 이미지로 표시하는 것을 피해야 합니다.
  • prepareToDraw()를 활용하여 다음 동기화 단계 전에 비트맵을 비동기식으로 미리 업로드합니다.

명령어 실행

명령어 실행 세그먼트는 표시 목록을 화면에 그리기 위해 필요한 모든 명령어를 실행하는 데 걸리는 시간을 나타냅니다.

시스템은 화면에 표시 목록을 그리도록 GPU에 필요한 명령어를 전송합니다. 일반적으로 OpenGL ES API를 통해 이 작업을 실행합니다.

이 절차는 시스템이 GPU로 명령어를 보내기 전에 각 명령어에 최종 변환 및 클리핑을 실행하기 때문에 어느 정도 시간이 걸립니다. 그런 다음에는 최종 명령어를 계산하는 GPU 측에서 추가 오버헤드가 발생합니다. 이러한 명령어는 최종 변환 및 추가 클리핑을 포함합니다.

이 세그먼트가 큰 경우

이 단계에서 소요되는 시간은 시스템이 특정 프레임에서 렌더링하는 디스플레이 목록의 복잡성과 수량을 직접 측정한 것입니다. 예를 들어 그리기 작업이 많을 경우, 특히 각 그리기 프리미티브에 적지만 내재된 비용이 있는 경우에는 이 시간이 증가할 수 있습니다. 예:

Kotlin

for (i in 0 until 1000) {
    canvas.drawPoint()
}

Java

for (int i = 0; i < 1000; i++) {
    canvas.drawPoint()
}

위의 예는 아래 코드보다 실행하는 데 훨씬 큰 비용이 발생합니다.

Kotlin

canvas.drawPoints(thousandPointArray)

Java

canvas.drawPoints(thousandPointArray);

명령어를 실행하는 것과 실제로 표시 목록을 그리는 것 사이에 항상 1:1의 상관관계가 있는 것은 아닙니다. GPU에 그리기 명령어를 보내는 데 걸리는 시간을 캡처하는 명령어 실행과 달리 그리기 측정항목은 표시 목록에 실행된 명령어를 캡처하는 데 걸린 시간을 나타냅니다.

디스플레이 목록은 가능한 한 시스템에서 캐시하므로 이러한 차이가 발생합니다. 결과적으로 스크롤, 변환 또는 애니메이션을 위해 시스템이 표시 목록을 다시 보내야 하지만 실제로 처음부터 다시 빌드할 필요는 없는(즉, 그리기 명령어를 다시 캡처) 상황이 있습니다. 그 결과 높은 그리기 명령어 막대는 표시되지 않고 높은 '명령어 실행' 막대가 표시됩니다.

버퍼 처리 및 전환

Android가 모든 표시 목록을 GPU에 제출하면 시스템은 마지막 명령어를 실행하여 그래픽 드라이버에 현재 프레임이 완료되었음을 알립니다. 이 시점에서 드라이버는 최종적으로 업데이트된 이미지를 화면에 표시할 수 있습니다.

이 세그먼트가 큰 경우

GPU는 CPU와 병렬로 작업을 실행한다는 점을 알아야 합니다. Android 시스템은 GPU에 그리기 명령어를 실행한 후 다음 작업으로 넘어갑니다. GPU는 이러한 그리기 명령어를 큐에서 읽고 처리합니다.

CPU가 GPU에서 처리하는 것보다 빠르게 명령어를 실행하면 프로세서 간의 통신 큐가 가득 찰 수 있습니다. 이 경우 CPU는 작업을 중단하고 큐에 다음 명령어를 넣을 공간이 생길 때까지 대기합니다. 이렇게 큐가 가득 차는 상태는 버퍼 전환 단계에서 자주 발생하는데 그 이유는 이 단계에서 전체 프레임 분량의 명령어가 제출되었기 때문입니다.

이 문제를 해결하기 위한 핵심은 '명령어 실행' 단계에서 하는 것과 비슷한 방식으로 GPU에서 발생하는 작업의 복잡성을 줄이는 것입니다.

기타

렌더링 시스템이 작업을 실행하는 데 걸리는 시간 외에 기본 스레드에서 발생하며 렌더링과는 관계없는 일련의 추가 작업이 있습니다. 이 작업이 소비하는 시간은 기타 시간으로 보고됩니다. 일반적으로 기타 시간은 렌더링의 연속된 두 프레임 간의 UI 스레드에서 발생할 수 있는 작업을 나타냅니다.

이 세그먼트가 큰 경우

이 값이 높으면 앱에 다른 스레드에서 발생해야 하는 콜백, 인텐트 또는 기타 작업이 있을 수 있습니다. 메서드 트레이스 또는 Systrace와 같은 도구를 사용하면 기본 스레드에서 실행 중인 작업을 확인할 수 있습니다. 이 정보는 타겟 성능을 향상하는 데 도움이 됩니다.