기준 프로필 디버그

이 문서에서는 문제를 진단하고 기준 프로필이 최대한의 이점을 제공하도록 올바르게 작동하는지 확인하는 데 도움이 되는 권장사항을 보여줍니다.

빌드 문제

Now in Android 샘플 앱에서 기준 프로필 예시를 복사했다면 기준 프로필 작업 중에 에뮬레이터에서 테스트를 실행할 수 없다는 테스트 실패가 발생할 수 있습니다.

./gradlew assembleDemoRelease
Starting a Gradle Daemon (subsequent builds will be faster)
Calculating task graph as no configuration cache is available for tasks: assembleDemoRelease
Type-safe project accessors is an incubating feature.

> Task :benchmarks:pixel6Api33DemoNonMinifiedReleaseAndroidTest
Starting 14 tests on pixel6Api33

com.google.samples.apps.nowinandroid.foryou.ScrollForYouFeedBenchmark > scrollFeedCompilationNone[pixel6Api33] FAILED
        java.lang.AssertionError: ERRORS (not suppressed): EMULATOR
        WARNINGS (suppressed):
        ...

이 실패는 Now in Android가 기준 프로필 생성에 Gradle 관리 기기를 사용하기 때문에 발생합니다. 일반적으로 성능 벤치마크는 에뮬레이터에서 실행하면 안 되므로 이 실패가 예상되는 것입니다. 그러나 기준 프로필을 생성할 때는 성능 측정항목을 수집하지 않으므로 편의를 위해 에뮬레이터에서 기준 프로필 수집을 실행할 수 있습니다. 에뮬레이터에서 기준 프로필을 사용하려면 명령줄에서 빌드 및 설치를 수행하고, 기준 프로필 규칙을 사용 설정하도록 인수를 설정하세요.

installDemoRelease -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile

또는 Android 스튜디오에서 Run > Edit Configurations를 선택하여 에뮬레이터에서 기준 프로필을 사용 설정하도록 맞춤 실행 구성을 만들 수 있습니다.

Now in Android에서 기준 프로필을 만들기 위한 맞춤 실행 구성 추가
그림 1. Now in Android에서 기준 프로필을 만들기 위한 맞춤 실행 구성을 추가합니다.

설치 문제

빌드 중인 APK 또는 AAB가 기준 프로필을 포함하는 빌드 변형에서 시작된 것인지 확인합니다. 이를 확인하는 가장 쉬운 방법은 Android 스튜디오에서 Build > Analyze APK를 선택하여 APK를 연 다음 /assets/dexopt/baseline.prof에서 프로필을 찾는 것입니다.

Android 스튜디오에서 APK 뷰어를 사용하여 기준 프로필 확인
그림 2. Android 스튜디오에서 APK 뷰어를 사용하여 기준 프로필을 확인합니다.

기준 프로필은 앱을 실행하는 기기에서 컴파일해야 합니다. 앱 스토어 설치와 PackageInstaller를 사용하여 설치된 앱 모두 앱 설치 프로세스의 일부로 온디바이스 컴파일이 실행됩니다. 그러나 앱이 Android 스튜디오에서 또는 명령줄 도구를 사용하여 사이드로드된 경우 JetpackProfileInstaller 라이브러리가 다음번 백그라운드 DEX 최적화 프로세스 중에 컴파일할 프로필을 대기열에 추가합니다. 이 경우 기준 프로필이 사용되도록 하려면 기준 프로필을 강제 컴파일해야 할 수 있습니다. 다음 예와 같이 ProfileVerifier를 사용하면 프로필 설치 및 컴파일 상태를 쿼리할 수 있습니다.

Kotlin

private const val TAG = "MainActivity"

class MainActivity : ComponentActivity() {
  ...
  override fun onResume() {
    super.onResume()
    lifecycleScope.launch {
      logCompilationStatus()
    }
  }

  private suspend fun logCompilationStatus() {
     withContext(Dispatchers.IO) {
        val status = ProfileVerifier.getCompilationStatusAsync().await()
        when (status.profileInstallResultCode) {
            RESULT_CODE_NO_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Baseline Profile not found")
            RESULT_CODE_COMPILED_WITH_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Compiled with profile")
            RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
                Log.d(TAG, "ProfileInstaller: App was installed through Play store")
            RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST ->
                Log.d(TAG, "ProfileInstaller: PackageName not found")
            RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ ->
                Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read")
            RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE ->
                Log.d(TAG, "ProfileInstaller: Can't write cache file")
            RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            else ->
                Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued")
        }
    }
}

Java


public class MainActivity extends ComponentActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onResume() {
        super.onResume();

        logCompilationStatus();
    }

    private void logCompilationStatus() {
         ListeningExecutorService service = MoreExecutors.listeningDecorator(
                Executors.newSingleThreadExecutor());
        ListenableFuture<ProfileVerifier.CompilationStatus> future =
                ProfileVerifier.getCompilationStatusAsync();
        Futures.addCallback(future, new FutureCallback<>() {
            @Override
            public void onSuccess(CompilationStatus result) {
                int resultCode = result.getProfileInstallResultCode();
                if (resultCode == RESULT_CODE_NO_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Baseline Profile not found");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Compiled with profile");
                } else if (resultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING) {
                    Log.d(TAG, "ProfileInstaller: App was installed through Play store");
                } else if (resultCode == RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST) {
                    Log.d(TAG, "ProfileInstaller: PackageName not found");
                } else if (resultCode == RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ) {
                    Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read");
                } else if (resultCode
                        == RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE) {
                    Log.d(TAG, "ProfileInstaller: Can't write cache file");
                } else if (resultCode == RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else {
                    Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued");
                }
            }

            @Override
            public void onFailure(Throwable t) {
                Log.d(TAG,
                        "ProfileInstaller: Error getting installation status: " + t.getMessage());
            }
        }, service);
    }
}

다음 결과 코드는 일부 문제의 원인을 알려주는 힌트를 제공합니다.

RESULT_CODE_COMPILED_WITH_PROFILE
프로필이 설치 및 컴파일되어 앱이 실행될 때마다 사용됩니다. 이는 바람직한 결과입니다.
RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
실행 중인 APK 또는 AAB에서 프로필을 찾을 수 없습니다. 이 오류가 표시되면 기준 프로필이 포함된 빌드 변형을 사용 중이며 APK에 프로필이 포함되어 있는지 확인하세요.
RESULT_CODE_NO_PROFILE
앱 스토어 또는 패키지 관리자를 통해 앱을 설치할 때 이 앱의 프로필이 설치되지 않았습니다. 이 오류 코드의 주된 이유는 ProfileInstallerInitializer가 사용 중지되어 프로필 설치 프로그램이 실행되지 않았기 때문입니다. 이 오류가 보고되었을 때는 애플리케이션 APK에 여전히 삽입된 프로필이 있습니다. 삽입된 프로필을 찾을 수 없는 경우 반환되는 오류 코드는 RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED입니다.
RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
프로필이 APK 또는 AAB에 있고 컴파일을 위해 대기열에 추가됩니다. ProfileInstaller에서 프로필을 설치하면 다음번에 시스템에서 백그라운드 DEX 최적화를 실행할 때 컴파일을 위해 대기열에 추가됩니다. 프로필은 컴파일이 완료될 때까지 활성화되지 않습니다. 컴파일이 완료되기 전까지는 기준 프로필의 벤치마킹을 시도하지 마세요. 기준 프로필을 강제 컴파일해야 할 수 있습니다. Android 9(API 28) 및 이후 버전을 실행하는 기기의 앱 스토어 또는 패키지 관리자에서 앱이 설치된 경우에는 설치 중에 컴파일이 실행되기 때문에 이 오류가 발생하지 않습니다.
RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING
일치하지 않는 프로필이 설치되었고 앱이 이 프로필을 사용하여 컴파일되었습니다. 이는 Google Play 스토어 또는 패키지 관리자를 통해 설치한 결과입니다. 일치하지 않는 프로필은 프로필과 앱 간에 아직 공유되는 메서드만 컴파일하기 때문에 이 결과는 RESULT_CODE_COMPILED_WITH_PROFILE과 다릅니다. 프로필은 예상보다 크기가 작으며, 기준 프로필에 포함된 것보다 적은 개수의 메서드가 컴파일됩니다.
RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE
ProfileVerifier가 확인 결과 캐시 파일을 쓸 수 없습니다. 이는 앱 폴더 권한에 문제가 있거나 기기의 디스크 여유 공간이 충분하지 않은 경우에 발생할 수 있습니다.
RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
ProfileVerifieris running on an unsupported API version of Android. ProfileVerifier는 Android 9(API 수준 28) 및 이후 버전만 지원합니다.
RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST
PackageManager.NameNotFoundException은 앱 패키지의 PackageManager가 쿼리될 때 발생합니다. 이는 매우 드물게 발생합니다. 앱을 모두 제거한 후 다시 설치해 보세요.
RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ
이전 확인 결과 캐시 파일이 존재하지만 읽을 수 없습니다. 이는 매우 드물게 발생합니다. 앱을 모두 제거한 후 다시 설치해 보세요.

프로덕션에서 ProfileVerifier 사용

프로덕션에서 Firebase용 Google 애널리틱스와 같은 애널리틱스 보고 라이브러리와 함께 ProfileVerifier를 사용하여 프로필 상태를 나타내는 분석 이벤트를 생성할 수 있습니다. 예를 들어, 기준 프로필이 포함되지 않은 새 앱 버전이 출시되면 빠르게 알려 줍니다.

기준 프로필 강제 컴파일

기준 프로필의 컴파일 상태가 RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION이면 adb를 사용하여 즉시 컴파일을 강제할 수 있습니다.

adb shell cmd package compile -r bg-dexopt PACKAGE_NAME

ProfileVerifier 없이 컴파일 상태 확인

ProfileVerifier를 사용하지 않는 경우 adb를 사용하여 컴파일 상태를 확인할 수 있습니다. 단, adb는 ProfileVerifier만큼 심층적인 통계를 제공하지 않습니다.

adb shell dumpsys package dexopt | grep -A 2 PACKAGE_NAME

adb를 사용하면 다음과 같은 결과가 생성됩니다.

  [com.google.samples.apps.nowinandroid.demo]
    path: /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/base.apk
      arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]
        [location is /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/oat/arm64/base.odex]

상태 값은 프로필 컴파일 상태를 나타내며 다음 값 중 하나를 갖습니다.

컴파일 상태 의미
speed‑profile 컴파일된 프로필이 존재하며 사용 중입니다.
verify 컴파일된 프로필이 없습니다.

verify 상태는 APK 또는 AAB에 프로필이 포함되어 있지 않음을 의미하지 않습니다. 다음번 백그라운드 DEX 최적화 작업에 의한 컴파일을 위해 대기열에 추가될 수 있기 때문입니다.

이유 값은 프로필 컴파일을 트리거하는 요소를 나타내며 다음 값 중 하나를 갖습니다.

이유 의미
install‑dm 기준 프로필이 앱이 설치될 때 수동으로 또는 Google Play에 의해 컴파일되었습니다.
bg‑dexopt 기기가 유휴 상태일 때 프로필이 컴파일되었습니다. 이는 기준 프로필이거나 앱 사용 중에 수집된 프로필일 수 있습니다.
cmdline adb를 사용하여 컴파일이 트리거되었습니다. 이는 기준 프로필이거나 앱 사용 중에 수집된 프로필일 수 있습니다.

성능 문제

이 섹션에서는 기준 프로필을 올바르게 정의하고 벤치마킹하여 최대한의 이점을 얻기 위한 몇 가지 권장사항을 보여줍니다.

시작 측정항목의 올바른 벤치마킹

시작 측정항목이 잘 정의되어 있으면 기준 프로필이 더 효과적으로 기능합니다. 두 가지 주요 측정항목은 처음 표시하는 데 걸린 시간(TTID)완전히 표시하는 데 걸린 시간(TTFD)입니다.

TTID는 앱이 첫 프레임을 그리기까지 걸린 시간입니다. 무언가를 표시하면 사용자가 앱이 실행 중이라는 것을 알 수 있으므로 TTID를 최대한 짧게 유지하는 것이 중요합니다. 미확정 진행률 표시기를 표시하여 앱이 반응하고 있음을 나타낼 수도 있습니다.

TTFD는 앱과 실제로 상호작용할 수 있기까지 걸린 시간입니다. 사용자가 불만을 느끼지 않도록 최대한 짧게 유지하는 것이 중요합니다. TTFD에 올바르게 신호를 보내면 TTFD에 도달하는 과정에서 실행되는 코드가 앱 시작의 일부임을 시스템에 알리는 것입니다. 결과적으로 시스템이 이 코드를 프로필에 배치할 가능성이 커집니다.

앱이 뛰어난 반응성을 갖는 것처럼 보일 수 있도록 TTID와 TTFD를 가능한 한 낮게 유지하세요.

시스템은 TTID를 감지하여 Logcat에 표시하고 시작 벤치마크의 일부로 보고할 수 있습니다. 그러나 TTFD는 확인할 수 없으며 앱이 완전히 그려진 상호작용 상태에 도달했을 때 이를 보고할 책임은 앱에 있습니다. 이렇게 하려면 reportFullyDrawn()을 호출하거나 Jetpack Compose를 사용하는 경우 ReportDrawn을 호출하면 됩니다. 앱이 완전히 그려진 것으로 간주되려면 모두 완료되어야 하는 여러 개의 백그라운드 작업이 있다면 시작 시간 정확성 개선에 설명된 대로 FullyDrawnReporter를 사용할 수 있습니다.

라이브러리 프로필 및 맞춤 프로필

프로필의 영향을 벤치마킹할 때 앱 프로필의 이점을 Jetpack 라이브러리와 같은 라이브러리에서 제공하는 프로필과 분리하기 어려울 수 있습니다. APK를 빌드할 때 Android Gradle 플러그인은 맞춤 프로필과 라이브러리 종속 항목에 프로필을 추가합니다. 이는 전반적인 성능을 최적화하는 데 유용하며 출시 빌드에 권장됩니다. 그러나 맞춤 프로필에서 추가로 성능이 얼마나 향상되는지 측정하기가 어렵습니다.

맞춤 프로필에서 제공하는 추가 최적화를 수동으로 확인하는 빠른 방법은 이를 삭제하고 벤치마크를 실행하는 것입니다. 그런 다음 이를 바꾸고 벤치마크를 다시 실행하세요. 두 프로필을 비교하면 라이브러리 프로필만 통해 제공되는 최적화와 라이브러리 프로필과 맞춤 프로필을 함께 확인할 수 있습니다.

프로필을 자동으로 비교하는 방법은 맞춤 프로필이 아닌 라이브러리 프로필만 포함하는 새 빌드 변형을 생성하는 것입니다. 이 변형의 벤치마크를 라이브러리 프로필과 맞춤 프로필을 모두 포함하는 출시 변형과 비교합니다. 다음 예는 라이브러리 프로필만 포함하는 변형을 설정하는 방법을 보여줍니다. 프로필 소비자 모듈(일반적으로 앱 모듈)에 releaseWithoutCustomProfile라는 새 변형을 추가합니다.

Kotlin

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    create("releaseWithoutCustomProfile") {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile(project(":baselineprofile"))
}

baselineProfile {
  variants {
    create("release") {
      from(project(":baselineprofile"))
    }
  }
}

Groovy

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    releaseWithoutCustomProfile {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile ':baselineprofile"'
}

baselineProfile {
  variants {
    release {
      from(project(":baselineprofile"))
    }
  }
}

위의 코드 예에서는 모든 변형에서 baselineProfile 종속 항목을 삭제하고 release 변형에만 선택적으로 적용합니다. 프로필 제작자 모듈의 종속 항목이 삭제될 때 라이브러리 프로필이 계속 추가되고 있는 것이 이상하게 보일 수 있습니다. 그러나 이 모듈은 커스텀 프로필 생성만 담당합니다. Android Gradle 플러그인은 모든 변형에서 계속 실행되며 라이브러리 프로필을 포함합니다.

또한 새 변형을 프로필 생성기 모듈에 추가해야 합니다. 이 예에서 생산자 모듈의 이름은 :baselineprofile입니다.

Kotlin

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      create("releaseWithoutCustomProfile") {}
      ...
    }
  ...
}

Groovy

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      releaseWithoutCustomProfile {}
      ...
    }
  ...
}

라이브러리 프로필로만 벤치마킹하려면 다음을 실행합니다.

./gradlew :baselineprofile:connectedBenchmarkReleaseWithoutCustomProfileAndroidTest

라이브러리 프로필과 맞춤 프로필을 모두 사용하여 벤치마킹하려면 다음을 실행합니다.

./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest

Macrobenchmark 샘플 앱에서 위 코드를 실행하면 두 변형 간에 성능 차이가 있음을 알 수 있습니다. 라이브러리 프로필만 있는 경우 웜 startupCompose 벤치마크는 다음과 같은 결과를 표시합니다.

SmallListStartupBenchmark_startupCompose[mode=COLD]
timeToInitialDisplayMs   min  70.8,   median  79.1,   max 126.0
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

많은 Jetpack Compose 라이브러리에는 라이브러리 프로필이 있으므로 기준 프로필 Gradle 플러그인을 사용하기만 하면 몇 가지 최적화가 가능합니다. 그러나 커스텀 프로필을 사용할 때 추가 최적화가 있습니다.

SmallListStartupBenchmark_startupCompose[mode=COLD]
timeToInitialDisplayMs   min 57.9,   median 73.5,   max 92.3
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

I/O 바운드 앱 시작 방지

앱이 시작 중에 많은 I/O 호출 또는 네트워크 호출을 실행하면 앱 시작 시간과 시작 벤치마킹 정확성에 부정적인 영향을 미칠 수 있습니다. 이러한 고강도 호출에는 시간이 흐름에 따라 그리고 동일한 벤치마크에서도 각 반복마다 달라질 수 불특정한 시간이 걸릴 수 있습니다. I/O 호출은 일반적으로 네트워크 호출보다 닛습니다. 네트워크 호출은 기기 외부 요소와 기기 자체 요소에 영향을 받을 수 있기 때문입니다. 시작 중에는 네트워크 호출을 피하세요. 둘 중 하나를 사용해야 한다면 I/O를 사용하세요.

시작을 벤치마킹할 때만이라도 앱 아키텍처가 네트워크 또는 I/O 호출 없이 앱 시작을 지원하도록 하는 것이 좋습니다. 이렇게 하면 벤치마크의 각 반복 간에 최대한 낮은 변동성을 보장할 수 있습니다.

앱에서 Hilt를 사용하는 경우 Microbenchmark 및 Hilt에서 벤치마킹할 때 가짜 I/O 바운드 구현을 제공할 수 있습니다.

중요한 사용자 여정 모두 포함

기준 프로필 생성에서 중요한 사용자 여정을 모두 정확하게 포함하는 것이 중요합니다. 포함되지 않은 사용자 여정은 기준 프로필로 개선되지 않습니다. 가장 효과적인 기준 프로필에는 일반적인 시작 사용자 여정과 스크롤 목록과 같이 성능에 민감한 인앱 사용자 여정이 포함됩니다.