기준 프로필을 사용하여 앱 성능 개선

1. 시작하기 전에

이 Codelab에서는 기준 프로필을 생성하여 애플리케이션 성능을 최적화하는 방법과 기준 프로필 사용의 성능 이점을 확인하는 방법을 알아봅니다.

기본 요건

이 Codelab은 Macrobenchmark 라이브러리로 앱 성능을 측정하는 방법을 보여주는 Macrobenchmark로 앱 성능 검사 Codelab을 기반으로 합니다.

필요한 항목

실행할 작업

  • 기준 프로필을 생성하여 성능 최적화
  • Macrobenchmark 라이브러리를 사용하여 성능 향상 확인

학습할 내용

  • 기준 프로필 생성
  • 기준 프로필의 성능 이점 이해

2. 설정

시작하려면 다음 명령어를 사용하여 명령줄에서 GitHub 저장소를 클론합니다.

$ git clone --branch baselineprofiles-main  https://github.com/googlecodelabs/android-performance.git
$ cd android-macrobenchmark-codelab

또는 ZIP 파일 두 개를 다운로드해도 됩니다.

Android 스튜디오로 프로젝트 열기

  1. 'Welcome to Android Studio' 창에서 c01826594f360d94.png Open an Existing Project를 선택합니다.
  2. [Download Location]/android-performance-codelabs/benchmarking 폴더를 선택합니다(도움말: build.gradle이 포함된 benchmarking 디렉터리를 선택해야 함).
  3. Android 스튜디오에서 프로젝트를 가져오면 app 모듈을 실행하여 벤치마킹할 샘플 애플리케이션을 빌드할 수 있는지 확인합니다.

3. 기준 프로필이란 무엇인가요?

기준 프로필Android 런타임(ART)에서 앱 설치 중에 중요한 경로를 기계어 코드로 사전 컴파일하는 데 사용하는 APK에 포함된 클래스 및 메서드 목록입니다. 기준 프로필은 앱이 시작을 최적화하고, 버벅거림을 줄이고, 최종 사용자가 경험하는 성능을 개선할 수 있도록 해 주는 일종의 프로필 기반 최적화(PGO)입니다.

기준 프로필 작동 방식

프로필 규칙은 assets/dexopt/baseline.prof에서 APK의 바이너리 형식으로 컴파일된 후 APK와 함께 Google Play를 통해 사용자에게 직접 제공됩니다.

앱 설치가 진행될 때 ART가 프로필에서 메서드의 AOT(Ahead-Of-Time) 컴파일을 실행하여 메서드의 실행 속도를 높입니다. 프로필이 앱 실행 시 또는 프레임 렌더링 중에 앱에서 사용하는 메서드를 포함하는 경우, 사용자가 경험하는 실행 시간이 빨라지고 버벅거림이 줄어듭니다.

4. 벤치마킹 모듈 업데이트

앱 개발자는 Jetpack Macrobenchmark 라이브러리를 사용하여 기준 프로필을 자동으로 생성할 수 있습니다. 기준 프로필을 생성하려면 추가 변경사항으로 애플리케이션을 벤치마킹하기 위해 만든 동일한 모듈을 사용하면 됩니다.

기준 프로필 난독화 사용 중지

앱에 난독화가 사용 설정되어 있다면 벤치마크에 관해 난독화를 사용 중지해야 합니다.

:app 모듈에 추가 proguard 파일을 추가하고 거기서 난독화를 사용 중지한 후 proguard 파일을 benchmark buildType에 추가하면 됩니다.

:app 모듈에서 benchmark-rules.pro라는 새 파일을 만듭니다. 파일은 모듈별 build.gradle 파일 옆의 /app/ 폴더에 배치해야 합니다. 27bd3b1881011d06.png

이 파일에서 다음 스니펫과 같이 -dontobfuscate를 추가하여 난독화를 사용 중지하세요.

# Disables obfuscation for benchmark builds.
-dontobfuscate

그런 다음 :app 모듈별 build.gradle에서 benchmark buildType을 수정하고 직접 만든 파일을 추가합니다. initWith 출시 buildType을 사용하고 있으므로 이 줄로 인해 출시 proguard 파일에 benchmark-rules.pro proguard 파일이 추가됩니다.

buildTypes {
   release {
      // ...
   }

   benchmark {
      initWith buildTypes.release
      // ...
      proguardFiles('benchmark-rules.pro')
   }
}

이제 기준 프로필 생성기 클래스를 작성해 보겠습니다.

5. 기준 프로필 생성기 클래스 작성

대개 앱의 일반적인 사용자 경험에 관한 기준 프로필을 생성합니다.

이 예에서는 다음 세 가지 경험을 확인할 수 있습니다.

  1. 애플리케이션 시작(대부분의 애플리케이션에 필수)
  2. 스낵 목록 스크롤
  3. 스낵 세부정보로 이동

기준 프로필을 생성하기 위해 :macrobenchmark 모듈에 새 테스트 클래스 BaselineProfileGenerator를 추가합니다. 이 클래스는 BaselineProfileRule 테스트 규칙을 사용하며 프로필을 생성하기 위한 테스트 메서드를 하나 포함합니다. 프로필을 생성하는 진입점은 collectBaselineProfile 함수입니다. 다음 두 가지 매개변수만 있으면 됩니다.

  • packageName(앱의 패키지)
  • profileBlock(마지막 람다 매개변수)
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

   @get:Rule
   val rule = BaselineProfileRule()

   @Test
   fun generate() {
       rule.collectBaselineProfile("com.example.macrobenchmark_codelab") {
           // TODO Add interactions for the typical user journeys
       }
   }
}

profileBlock 람다에서 앱의 일반적인 사용자 경험을 처리하는 상호작용을 지정합니다. 라이브러리는 profileBlock을 여러 번 실행하고 최적화할 호출된 클래스와 함수를 수집하여 기기에서 기준 프로필을 생성합니다.

다음 스니펫에서 일반적인 경험을 처리하는 기준 프로필 생성기를 개략적으로 확인할 수 있습니다.

@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

   @get:Rule
   val rule = BaselineProfileRule()

   @Test
   fun generate() {
       rule.collectBaselineProfile("com.example.macrobenchmark_codelab") {
           startApplicationJourney() // TODO Implement
           scrollSnackListJourney() // TODO Implement
           goToSnackDetailJourney() // TODO Implement
       }
   }
}

이제 언급된 각 경험의 상호작용을 작성해 보겠습니다. MacrobenchmarkScope의 확장 함수로 작성하므로 제공된 매개변수와 함수에 액세스할 수 있습니다. 이러한 방식으로 작성하면 벤치마크와의 상호작용을 재사용하여 성능 향상을 확인할 수 있습니다.

애플리케이션 경험 시작

앱 시작 경험(startApplicationJourney)의 경우 다음 상호작용을 처리해야 합니다.

  1. 홈을 눌러 앱 상태가 다시 시작되었는지 확인합니다.
  2. 기본 활동을 시작하고 첫 프레임이 렌더링될 때까지 기다립니다.
  3. 콘텐츠가 로드 및 렌더링되어 사용자가 상호작용할 수 있을 때까지 기다립니다.
fun MacrobenchmarkScope.startApplicationJourney() {
   pressHome()
   startActivityAndWait()
   val contentList = device.findObject(By.res("snack_list"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(Until.hasObject(By.res("snack_collection")), 5_000)
}

스크롤 목록 경험

스크롤 스낵 목록 경험(scrollSnackListJourney)의 경우 다음 상호작용을 따를 수 있습니다.

  1. 스낵 목록 UI 요소를 찾습니다.
  2. 동작 여백이 시스템 탐색을 트리거하지 않도록 설정합니다.
  3. 목록을 스크롤하고 UI가 안정될 때까지 기다립니다.
fun MacrobenchmarkScope.scrollSnackListJourney() {
   val snackList = device.findObject(By.res("snack_list"))
   // Set gesture margin to avoid triggering gesture navigation
   snackList.setGestureMargin(device.displayWidth / 5)
   snackList.fling(Direction.DOWN)
   device.waitForIdle()
}

세부정보 경험으로 이동

마지막 경험(goToSnackDetailJourney)은 다음 상호작용을 구현합니다.

  1. 스낵 목록을 찾아 작업할 수 있는 모든 스낵 항목을 찾습니다.
  2. 목록에서 항목을 선택합니다.
  3. 항목을 클릭하고 세부정보 화면이 로드될 때까지 기다립니다. 스낵 목록이 더 이상 화면에 표시되지 않는다는 사실을 활용할 수 있습니다.
fun MacrobenchmarkScope.goToSnackDetailJourney() {
   val snackList = device.findObject(By.res("snack_list"))
   val snacks = snackList.findObjects(By.res("snack_item"))
   // Select random snack from the list
   snacks[Random.nextInt(snacks.size)].click()
   // Wait until the screen is gone = the detail is shown
   device.wait(Until.gone(By.res("snack_list")), 5_000)
}

지금까지 기준 프로필 생성기를 실행할 준비를 하는 데 필요한 모든 상호작용을 정의했지만 이제 이를 실행할 기기를 우선 정의해야 합니다.

6. Gradle 관리 기기 준비

기준 프로필을 생성하려면 먼저 userdebug 에뮬레이터를 준비해야 합니다. 기준 프로필을 만드는 프로세스를 자동화하려면 Gradle 관리 기기를 사용하면 됩니다. Gradle 관리 기기에 관한 자세한 내용은 문서를 참고하세요.

먼저 다음 스니펫과 같이 :macrobenchmark 모듈 build.gradle 파일에서 Gradle 관리 기기를 정의합니다.

testOptions {
    managedDevices {
        devices {
            pixel2Api31(com.android.build.api.dsl.ManagedVirtualDevice) {
                device = "Pixel 2"
                apiLevel = 31
                systemImageSource = "aosp"
            }
        }
    }
}

기준 프로필을 생성하려면 루팅된 Android 9(API 28) 이상을 사용해야 합니다.

여기서는 Android 11(API 수준 31)을 사용하며 aosp 시스템 이미지는 루팅된 액세스를 지원합니다.

Gradle 관리 기기를 사용하면 수동으로 실행하고 해체하지 않고도 Android Emulator에서 테스트를 실행할 수 있습니다. build.gradle에 정의를 추가하면 새 pixel2Api31[BuildVariant]AndroidTest 작업을 실행할 수 있습니다. 다음 단계에서 이 작업을 사용하여 기준 프로필을 생성합니다.

7. 기준 프로필 생성기 테스트 실행

Gradle 관리 기기가 준비되면 생성기 테스트를 시작할 수 있습니다.

실행 구성에서 생성기 실행

Gradle 관리 기기를 사용하려면 테스트를 Gradle 작업으로 실행해야 합니다. 빠르게 시작할 수 있도록 실행하는 데 필요한 모든 매개변수로 작업을 지정하는 실행 구성을 만들었습니다.

실행하려면 generateBaselineProfile 실행 구성을 찾아 Run 버튼 229e32fcbe68452f.png을 클릭합니다.

8f6b7c9a5da6585.png

테스트에서는 앞에서 정의한 에뮬레이터 이미지를 만들고 상호작용을 여러 번 실행한 후 에뮬레이터를 해체하고 Android 스튜디오에 출력을 제공합니다.

4b5b2d0091b4518c.png

(선택사항) 명령줄에서 생성기 실행

명령줄에서 생성기를 실행하려면 Gradle 관리 기기에서 만든 작업(:macrobenchmark:pixel2Api31BenchmarkAndroidTest)을 활용하면 됩니다.

이 명령어로 프로젝트의 모든 테스트가 실행되지만 실패합니다. 모듈에는 나중에 성능 향상을 확인할 수 있는 벤치마크도 포함되어 있기 때문입니다.

이를 위해 -P android.testInstrumentationRunnerArguments.class 매개변수를 사용하여 실행할 클래스를 필터링하고 이전에 작성한 com.example.macrobenchmark.BaselineProfileGenerator를 지정할 수 있습니다.

전체 명령어는 다음과 같습니다.

./gradlew :macrobenchmark:pixel2Api31BenchmarkAndroidTest -P android.testInstrumentationRunnerArguments.class=com.example.macrobenchmark.BaselineProfileGenerator

8. 생성된 기준 프로필 적용

생성기가 성공적으로 완료되면 앱에서 기준 프로필이 작동하도록 몇 가지 작업을 실행해야 합니다.

생성된 기준 프로필 파일을 AndroidManifest.xml 옆의 src/main 폴더에 배치해야 합니다. 파일을 가져오려면 다음 스크린샷과 같이 /macrobenchmark/build/outputs/에 있는 managed_device_android_test_additional_output/ 폴더에서 파일을 복사하면 됩니다.

b104f315f06b3578.png

또는 Android 스튜디오 출력에서 results 링크를 클릭하여 콘텐츠를 저장하거나 ​​출력에 인쇄된 adb pull 명령어를 사용할 수 있습니다.

이제 파일 이름을 baseline-prof.txt로 변경해야 합니다.

8973f012921669f6.png

그런 다음 profileinstaller 종속 항목을 :app 모듈에 추가합니다.

dependencies {
  implementation("androidx.profileinstaller:profileinstaller:1.2.0-rc01")
}

이 종속 항목을 추가하면 다음 작업을 할 수 있습니다.

  • 기준 프로필을 로컬에서 벤치마킹합니다.
  • 클라우드 프로필을 지원하지 않는 Android 7(API 수준 24) 및 Android 8(API 수준 26)에서 기준 프로필을 사용합니다.
  • Google Play 서비스가 없는 기기에서 기준 프로필을 사용합니다.

마지막으로 1079605eb7639c75.png 아이콘을 클릭하여 프로젝트를 Gradle 파일과 동기화합니다.

40cb2ba3d0b88dd6.png

다음 단계에서는 기준 프로필을 사용하면 앱 성능이 얼마나 향상되는지 확인하는 방법을 알아봅니다.

9. 시작 성능 개선 확인

이제 기준 프로필을 생성하여 앱에 추가했습니다. 앱의 성능에 원하는 효과를 내는지 확인해 보겠습니다.

앱 시작을 측정하는 벤치마크가 포함된 ExampleStartupBenchmark 클래스로 돌아가겠습니다. 다른 컴파일 모드에서 재사용되도록 startup() 테스트를 약간 변경해야 합니다. 이를 통해 기준 프로필을 사용할 때 차이를 비교할 수 있습니다.

CompilationMode

CompilationMode 매개변수는 애플리케이션이 기계어 코드로 사전 컴파일되는 방식을 정의합니다. 다음과 같은 옵션이 있습니다.

  • DEFAULT: 사용 가능한 경우 기준 프로필을 사용하여 앱을 부분적으로 사전 컴파일합니다(compilationMode 매개변수가 적용되지 않은 경우 사용됨).
  • None(): 앱 컴파일 상태를 재설정하고 앱을 사전 컴파일하지 않습니다. JIT(Just-In-Time) 컴파일은 앱 실행 중에 계속 사용 설정됩니다.
  • Partial(): 기준 프로필 또는 준비 실행을 통해 앱을 사전 컴파일합니다.
  • Full(): 전체 애플리케이션 코드를 사전 컴파일합니다. 이는 Android 6(API 23) 이하에서 유일하게 지원됩니다.

애플리케이션 성능을 최적화하려는 경우 DEFAULT 컴파일 모드를 선택하면 됩니다. 성능이 Google Play에서 앱을 설치할 때와 비슷하기 때문입니다. 기준 프로필을 통해 제공되는 성능 이점을 비교하려면 컴파일 모드 NonePartial의 결과를 비교하면 됩니다.

다른 CompilationMode로 시작 테스트 수정

먼저 startup 메서드에서 @Test 주석을 삭제하고(JUnit 테스트에는 매개변수를 포함할 수 없으므로) compilationMode 매개변수를 추가한 후 measureRepeated 함수에서 사용해 보겠습니다.

// Remove @Test annotation and add the compilationMode parameter.
fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
   packageName = "com.google.samples.apps.sunflower",
   metrics = listOf(StartupTimingMetric()),
   iterations = 5,
   compilationMode = compilationMode, // Set the compilation mode
   startupMode = StartupMode.COLD
) {
   pressHome()
   startActivityAndWait()
}

이제 CompilationMode가 다른 테스트 함수를 두 개 추가해 보겠습니다. 첫 번째는 CompilationMode.None을 사용합니다. 즉, 각 벤치마크 전에 앱의 상태가 재설정되고 앱에는 사전 컴파일된 코드가 없습니다.

@Test
fun startupCompilationNone() = startup(CompilationMode.None())

두 번째 테스트에서는 기준 프로필을 로드하고 벤치마크를 실행하기 전에 프로필에서 지정된 클래스와 함수를 사전 컴파일하는 CompilationMode.Partial을 활용합니다.

@Test
fun startupCompilationPartial() = startup(CompilationMode.Partial())

원하는 경우 CompilationMode.Full을 사용하여 전체 애플리케이션을 사전 컴파일하는 세 번째 메서드를 추가할 수 있습니다. 이는 Android 6(API 23) 이하에서 유일한 옵션입니다. 시스템이 완전히 사전 컴파일된 앱만 실행하기 때문입니다.

@Test
fun startupCompilationFull() = startup(CompilationMode.Full())

이제 실제 기기에서와 마찬가지로 벤치마크를 실행하고 Macrobenchmark에서 다양한 컴파일 모드를 사용하여 시작 시간을 측정할 때까지 기다립니다.

벤치마크가 완료되면 다음 스크린샷과 같이 Android 스튜디오 출력에서 시간을 확인할 수 있습니다.

cbbc9660374a438.png

스크린샷을 보면 앱 시작 시간이 CompilationMode마다 다릅니다. 중앙값은 다음 표에 나와 있습니다.

timeToInitialDisplay [ms]

timeToFullDisplay [ms]

None

364.4

846.5

Full

325.8

739.1

Partial

296.1

708.1

직관적으로 None 컴파일은 최악의 성능을 보입니다. 앱이 시작되는 동안 기기에서 대부분의 JIT 컴파일이 실행되어야 하기 때문입니다. Full 컴파일의 성능이 최고가 아닌 점은 직관적이지 않을 수 있습니다. 이 경우에는 모든 항목이 컴파일되므로 앱의 odex 파일이 매우 큽니다. 따라서 시스템이 일반적으로 앱 시작 중에 훨씬 더 많은 IO를 실행해야 합니다. 가장 좋은 성능은 기준 프로필을 사용하는 Partial 사례에서 발휘됩니다. 부분 컴파일은 사용자가 사용할 가능성이 가장 높은 코드의 컴파일 간에 균형을 유지하지만 중요하지 않은 코드를 사전 컴파일되지 않은 상태로 두므로 즉시 로드할 필요가 없기 때문입니다.

10. 스크롤 성능 확인

이전 단계에서 했던 것처럼 스크롤 벤치마크를 측정하고 확인할 수 있습니다. 이전과 마찬가지로 ScrollBenchmarks 클래스를 수정해 보겠습니다. scroll 테스트에 매개변수를 추가하고 다른 컴파일 모드 매개변수로 테스트를 더 추가합니다.

ScrollBenchmarks.kt 파일을 열고 scroll() 함수를 수정하여 compilationMode 매개변수를 추가합니다.

fun scroll(compilationMode: CompilationMode) {
        benchmarkRule.measureRepeated(
            compilationMode = compilationMode, // Set the compilation mode
            // ...

이제 다른 매개변수를 사용하는 테스트를 여러 개 정의합니다.

@Test
fun scrollCompilationNone() = scroll(CompilationMode.None())

@Test
fun scrollCompilationPartial() = scroll(CompilationMode.Partial())

벤치마크를 전처럼 실행하여 다음 스크린샷과 같은 결과를 확인합니다. 249af52e917a4fcf.png

결과에서 CompilationMode.Partial은 평균 프레임 시간이 0.5ms 더 짧습니다. 이는 사용자에게 눈에 띄지 않을 수 있지만 다른 백분위수에서는 결과가 더 명확합니다. P90의 경우 차이는 11.7ms입니다. 즉, 프레임을 생성하도록 지정된 시간의 ~70%입니다(Pixel 3의 경우 약 ~16ms).

11. 축하합니다

축하합니다. 이 Codelab을 완료하고 기준 프로필을 사용하여 앱 성능을 개선했습니다.

다음 단계

Macrobenchmark 및 기타 성능 샘플이 포함된 성능 샘플 GitHub 저장소를 확인하세요. 벤치마킹 및 기준 프로필을 사용하여 성능을 개선하는 실제 애플리케이션인 Now In Android 샘플 앱도 확인하세요.

참조 문서