앱을 설치하거나 업데이트한 후에 처음 실행할 때 앱의 코드는 JIT될 때까지 인터프리트 모드(interpreted mode)로 실행된다.
APK에서는 Java 및 Kotlin 코드가 dex 바이트 코드로 컴파일 되지만, 완전히 컴파일된 앱을 저장하고 로드하는 비용 때문에 (Android 6 이후로는) 기계어 코드로 완전히 컴파일되지 않는다.
앱에서 자주 사용되는 클래스와 메서드뿐 아니라 앱 시작에 사용되는 클래스와 메서드도 프로필 파일에 기록된다.
기기가 유휴 모드(idle mode)에 들어가면 ART는 이러한 프로필을 기반으로 앱을 컴파일하고 이에 따라 다음번에는 앱이 더 빠르게 시작된다.
Google Play는 Android 9(API 수준 28)부터 Cloud Profile도 제공한다. (wow..)
앱이 기기에서 실행될 때 ART에서 생성된 프로필은 Play Store앱에 의해 업로드되고 클라우드에서 집계된다.
애플리케이션에 대해 업로드된 프로필이 충분히 많아지면, Play앱은 후속 설치에 집계된 프로필을 사용한다.
Cloud Profile은 분명히 유용하지만 앱 설치 시 언제든 사용할 수 있는 것은 아님.
프로필의 수집 및 집계에 보통 며칠이 걸리는데, 많은 앱이 매주 업데이트 되기 때문!
Cloud Profile을 사용할 수 있게 되기 전에 많은 사용자가 업데이트를 설치할 것이고, 이러한 문제들 때문에 나온 것이 Baseline Profile!
Baseline Profile을 사용하면 포함된 코드 경로의 해석과 JIT(Just-in-time) 컴파일 단계를 피하여 최초 실행 후 코드 실행 속도가 약 30% 향상된다.
앱이나 라이브러리에 Baseline Profile을 제공하면 Android 런타임(ART)이 AOT(Ahead-Of-Time) 컴파일을 통해 포함된 코드 경롤르 최적화하여 모든 신규 사용자와 앱 업데이트에 향상된 성능을 제공함.
이 프로필 기반 최적화(PGO)는 앱이 시작을 최적화하고, 상호작용으로 인한 버벅거림을 줄이고, 첫 실행부터최종 사용자가 경험하는 전반적인 런타임 성능을 개선할 수 있도록 해준다.
Baseline Profile을 사용하면 앱 시작 뿐 아니라 화면 간 이동, 컨텐츠 스크롤과 같은 모든 사용자 상호작용이 처음 실행될 때부터 더 원활해진다.
앱의 속도와 반응성을 높이면 DAU를 늘리고 리텐션을 높일 수 있을 것이다.
Baseline Profile은 첫 실행부터 앱 런타임을 개선하는 일반적인 사용자 인터렉션을 제공하여 앱 시작뿐 아니라 그 이후로도 최적화를 적용하는 데 도움을 준다.
Baseline Profile을 사용하지 않는 경우 모든 앱 코드는 해석된 후에 메모리에 또는 기기가 유휴 상태일 때 백그라운드의 odex 파일에 JIT로 컴파일된다.
이로인해 새 경로가 최적화되기 전에 처음 앱을 설치하거나 업데이트한 후 앱을 실행할 때 사용자 환경이 저하될 수 있다.
Baseline Profile 모듈을 생성하면 BaselineProfileGenerator
, StartupBenchmarks
파일이 자동 생성된다.
@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() {
rule.collect("com.example.baselineprofiles_codelab") {
// This block defines the app's critical user journey. This is where you
// optimize for app startup. You can also navigate and scroll
// through your most important UI.
// Start default activity for your app.
pressHome()
startActivityAndWait()
// TODO Write more interactions to optimize advanced journeys of your app.
// Check UiAutomator documentation for more information about how to interact with the app.
// https://d.android.com/training/testing/other-components/ui-automator
}
}
}
BaselineProfileGenerator 클래스는 BaselineProfileRule 테스트 규칙을 사용하며 프로필 생성을 위한 하나의 테스트 메서드를 포함한다.
프로필을 생성하는 진입점은 collect()함수이며 다음 두 가지 매개변수만 있으면 된다.
profileBlock 람다에서 앱의 일반적인 사용자 경험을 다루는 상호작용을 지정함.
라이브러리는 profileBlock을 여러 번 실행하고, 호출된 클래스와 함수를 수집하고, 최적화할 코드로 기기에서 Baseline Profile을 생성한다.
기본적으로 BaselineProfileGenerator는 기본 Activity를 시작하는 상호작용을 포함하고 startActivityAndWait() 메서드를 사용하여 앱의 첫 번째 프레임이 렌더링될 때까지 기다린다.
BaselineProfileGenerator 클래스에는 앱 내 구현된 기능을 최적화하기 위해 더 많은 상호작용을 작성하는 TODO도 포함되어 있다.
설명을 위한 예시가 있다.
// ...
rule.collect("com.example.baselineprofiles_codelab") {
// This block defines the app's critical user journey. This is where you
// optimize for app startup. You can also navigate and scroll
// through your most important UI.
// Start default activity for your app.
pressHome()
startActivityAndWait()
// TODO Write more interactions to optimize advanced journeys of your app.
// For example:
// 1. Wait until the content is asynchronously loaded.
waitForAsyncContent()
// 2. Scroll the feed content.
scrollSnackListJourney()
// 3. Navigate to detail screen.
goToSnackDetailJourney()
// Check UiAutomator documentation for more information about how to interact with the app.
// https://d.android.com/training/testing/other-components/ui-automator
}
// ...
@Immutable
data class SnackCollection(
val id: Long,
val name: String,
val snacks: List<Snack>,
val type: CollectionType = CollectionType.Normal
)
@Composable
private fun SnackCollectionList(
snackCollections: List<SnackCollection>,
filters: List<Filter>,
onSnackClick: (Long) -> Unit,
modifier: Modifier = Modifier
) {
var filtersVisible by rememberSaveable { mutableStateOf(false) }
Box(modifier) {
LazyColumn(
modifier = Modifier.testTag("snack_list"),
) {
...
}
}
}
많은 앱에는 컨텐츠가 로드되고 렌더링될 때를 시스템에 알리고 사용자가 컨텐츠와 사용할 수 있는 앱 시작(TTFD) 시 일종의 비동기 로드가 있다.
waitForAsyncContent()에서 상태를 기다린다.
fun MacrobenchmarkScope.waitForAsyncContent() {
device.wait(Until.hasObject(By.res("snack_list")), 5_000)
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)
}
스크롤 스낵 리스트의 경우를 보자.
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()
}
Baseline Profile을 생성하려면 Gradle 관리 기기와 같은 에뮬레이터를 사용하거나 Android 13(API 33) 이상을 실행하는 기기를 사용하는 것이 좋다.
Baseline Profile Generator 모듈 생성시 Use Gradle Managed Device를 체크하면 기기 정의가 자동으로 된다.
그게 아니라면 baselineprofile 모듈의 build.gradle 파일에 아래 내용들을 추가한다.
android {
// ...
testOptions.managedDevices.devices {
create<ManagedVirtualDevice>("pixel6Api31") {
device = "Pixel 6"
apiLevel = 31
systemImageSource = "aosp"
}
}
}
baselineProfile {
managedDevices += "pixel6Api31"
useConnectedDevices = false
}
dependencies {
// ...
}
위 내용을 좀 더 구체화하자면 아래와 같다.
create<ManagedVirtualDevice>("에뮬레이터명") {
device = "에뮬레이터 모델명"
apiLevel = 에뮬레이터 API level
systemImageSource = "aosp"
}
baselineProfile {
managedDevices += "에뮬레이터명"
useConnectedDevices = false
}
만약 에뮬레이터를 Pixel7 API 33을 사용한다고 하면 다음과 같이 수정하면 된다.
create<ManagedVirtualDevice>("pixel7Api33") {
device = "Pixel 7"
apiLevel = 33
systemImageSource = "aosp"
}
baselineProfile {
managedDevices += "pixel7Api33"
useConnectedDevices = false
}
기기가 준비되면 Baseline Profile을 만들 수 있음
Baseline Profile Gradle 플러그인은 Gradle 작업을 만들어 Generator 테스트 클래스를 실행하고 생성된 Baseline Profile을 앱에 적용하는 전체 프로세스를 자동화한다.
Generate Baseline Profile for app 실행 Configuration을 찾아서 실행하면 된다.
생성이 완료되면 아래와 같이 app 모듈에 자동으로 생성된다.
Baseline Profile이 생성되고 앱의 소스 코드에 복사되면 평소처럼 앱의 프로덕션 버전을 빌드하면 된다.
사용자에게 Baseline Profile을 배포하기 위해 우리가 따로 해야 할 추가 작업은 없음
빌드 중에 Android Gradle 플러그인에서 선택하며 AAB나 APK에 포함된다.
Baseline Profile을 추가하였으니 얼마나 성능이 개선되었는지 확인해볼 수 있다.
전에 자동으로 생성된 StartupBenchmarks 클래스를 보자.
@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmarks {
@get:Rule
val rule = MacrobenchmarkRule()
@Test
fun startupCompilationNone() =
benchmark(CompilationMode.None())
@Test
fun startupCompilationBaselineProfiles() =
benchmark(CompilationMode.Partial(BaselineProfileMode.Require))
private fun benchmark(compilationMode: CompilationMode) {
rule.measureRepeated(
packageName = "com.example.baselineprofiles_codelab",
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = 10,
setupBlock = {
pressHome()
},
measureBlock = {
startActivityAndWait()
// TODO Add interactions to wait for when your app is fully drawn.
// The app is fully drawn when Activity.reportFullyDrawn is called.
// For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter
// from the AndroidX Activity library.
// Check the UiAutomator documentation for more information on how to
// interact with the app.
// https://d.android.com/training/testing/other-components/ui-automator
}
)
}
}
packageName:
측정할 애플리케이션입니다.metrics
: 벤치마크 중에 측정하려는 정보 유형입니다.iterations
: 벤치마크가 반복되는 횟수입니다.startupMode
: 벤치마크 시작 시 원하는 애플리케이션 시작 방식입니다.setupBlock
: 측정 전에 발생해야 하는 앱과의 상호작용입니다.measureBlock
: 벤치마크 중에 측정하려는 앱과의 상호작용입니다.CompilationMode 매개변수는 애플리케이션이 기계어 코드로 사전 컴파일되는 방식을 정의하고 다음과 같은 옵션들이 있음
애플리케이션 성능을 최적화하려는 경우 DEFAULT 컴파일 모드를 선택하면 된다.
성능이 Google Play에서 앱을 설치할 때와 비슷하기 때문이다.
Baseline Profile을 통해 제공되는 성능 이점을 비교하려면 컴파일 모드 None과 Partial의 결과를 비교하면 됨
titmeToInitalDisplay(ms) | titmeToFullDisplay(ms) | |
---|---|---|
BaselineProfiles | 241.2 | 582.3 |
None | 257.7 | 613.5 |
개선사항 | 약 6% | 약 5% |
startupCompilationNone는 앱이 시작되는 동안 기기에서 대부분의 JIT 컴파일을 실행해야 하므로 성능이 좋지 않다.
하지만 startupCompilationBaselineProfiles는 성능이 더 나은데 이는 Baseline Profile이 있는 부분 컴파일이 사용자가 사용할 가능성이 가장 높은 코드를 AOT 컴파일하고 중요하지 않은 코드는 사전 컴파일되지 않은 채로 두므로 즉시 로드하지 않아도 되기 때문이다.
https://developers-kr.googleblog.com/2022/03/improving-app-performance-with-baseline.html
https://developer.android.com/codelabs/android-baseline-profiles-improve?hl=ko#3