The Android Developer Challenge is back! Submit your idea before December 2.

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 객체에 추가한 모든 코드에 적용됩니다. 이러한 코드의 예로는 Drawable 클래스의 서브클래스에 속하는 onDraw(), dispatchDraw() 및 다양한 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()
    }
    

자바

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

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

Kotlin

    canvas.drawPoints(thousandPointArray)
    

자바

    canvas.drawPoints(thousandPointArray);
    

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

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

버퍼 처리 및 전환

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

이 세그먼트가 큰 경우

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

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

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

기타

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

이 세그먼트가 큰 경우

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