304 lines
7.7 KiB
Markdown
304 lines
7.7 KiB
Markdown
---
|
|
id: android-baseline-profile
|
|
title: Android Baseline Profile — Startup / Scroll 최적화
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [android, performance, baseline-profile, vibe-coding]
|
|
tech_stack: { language: "Kotlin / Macrobenchmark", applicable_to: ["Android"] }
|
|
applied_in: []
|
|
aliases: [Baseline Profile, AOT, Macrobenchmark, startup metric, frame timing]
|
|
---
|
|
|
|
# Android Baseline Profile
|
|
|
|
> Startup / scroll 30% 빠르게. **Macrobenchmark 가 critical user flow trace → AOT compiled**. R8 + Compose 와 결합.
|
|
|
|
## 📖 핵심 개념
|
|
- Baseline Profile: 자주 쓰는 코드 path 미리 AOT compile.
|
|
- Macrobenchmark: 측정 + profile 생성.
|
|
- Default profile: Compose / 일부 lib 자동.
|
|
- Startup Profile: cold start 가속.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### Setup
|
|
```groovy
|
|
// build.gradle (app)
|
|
plugins {
|
|
id 'androidx.baselineprofile'
|
|
}
|
|
|
|
dependencies {
|
|
implementation 'androidx.profileinstaller:profileinstaller:1.3.1'
|
|
baselineProfile project(':baselineprofile')
|
|
}
|
|
|
|
baselineProfile {
|
|
saveInSrc = true // src/main/baseline-prof.txt
|
|
automaticGenerationDuringBuild = false // CI 에서만
|
|
}
|
|
```
|
|
|
|
### baselineprofile module
|
|
```groovy
|
|
// :baselineprofile/build.gradle
|
|
plugins {
|
|
id 'com.android.test'
|
|
id 'androidx.baselineprofile'
|
|
}
|
|
|
|
android {
|
|
targetProjectPath = ':app'
|
|
}
|
|
|
|
dependencies {
|
|
implementation 'androidx.benchmark:benchmark-macro-junit4:1.2.0'
|
|
implementation 'androidx.test.ext:junit:1.1.5'
|
|
implementation 'androidx.test.espresso:espresso-core:3.5.1'
|
|
implementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
|
}
|
|
```
|
|
|
|
### Generate profile (Macrobenchmark)
|
|
```kotlin
|
|
// :baselineprofile/src/main/java/.../BaselineProfileGenerator.kt
|
|
@RunWith(AndroidJUnit4::class)
|
|
class BaselineProfileGenerator {
|
|
@get:Rule val rule = BaselineProfileRule()
|
|
|
|
@Test
|
|
fun generate() = rule.collect(packageName = "com.acme.app") {
|
|
// 1. App 시작 — cold start
|
|
startActivityAndWait()
|
|
|
|
// 2. 핵심 user flow
|
|
device.findObject(By.res("login_button")).click()
|
|
device.wait(Until.hasObject(By.res("home_screen")), 5000)
|
|
|
|
// 3. Scroll
|
|
val list = device.findObject(By.res("feed_list"))
|
|
list.fling(Direction.DOWN)
|
|
list.fling(Direction.DOWN)
|
|
|
|
// 4. 다른 screen
|
|
device.findObject(By.text("Profile")).click()
|
|
device.wait(Until.hasObject(By.res("profile_screen")), 5000)
|
|
}
|
|
}
|
|
```
|
|
|
|
```bash
|
|
./gradlew :baselineprofile:generateBaselineProfile
|
|
# 결과: app/src/main/baseline-prof.txt
|
|
```
|
|
|
|
### Build 안 적용
|
|
```
|
|
- baseline-prof.txt 가 APK 안 포함.
|
|
- 첫 install 시 ProfileInstaller 가 dexopt.
|
|
- 사용자가 처음 launch 부터 빠름.
|
|
```
|
|
|
|
### Macrobenchmark — 측정
|
|
```kotlin
|
|
@RunWith(AndroidJUnit4::class)
|
|
class StartupBenchmark {
|
|
@get:Rule val rule = MacrobenchmarkRule()
|
|
|
|
@Test fun startupCold() = rule.measureRepeated(
|
|
packageName = "com.acme.app",
|
|
metrics = listOf(StartupTimingMetric()),
|
|
iterations = 5,
|
|
startupMode = StartupMode.COLD,
|
|
) {
|
|
startActivityAndWait()
|
|
}
|
|
|
|
@Test fun scrollFeed() = rule.measureRepeated(
|
|
packageName = "com.acme.app",
|
|
metrics = listOf(FrameTimingMetric()),
|
|
iterations = 5,
|
|
startupMode = StartupMode.WARM,
|
|
) {
|
|
startActivityAndWait()
|
|
val list = device.findObject(By.res("feed_list"))
|
|
list.fling(Direction.DOWN)
|
|
list.fling(Direction.DOWN)
|
|
}
|
|
}
|
|
```
|
|
|
|
```bash
|
|
./gradlew :baselineprofile:connectedAndroidTest
|
|
```
|
|
|
|
→ Result: startup time, frame timing, jank %.
|
|
|
|
### 기대 효과
|
|
```
|
|
Startup: 20-30% 빠름
|
|
Scroll: jank ↓
|
|
Compose 첫 frame ↓
|
|
```
|
|
|
|
→ R8 + Baseline Profile + Compose 가 함께 사용.
|
|
|
|
### Compose 최적화 (Baseline 와 별도 + 함께)
|
|
```kotlin
|
|
// 1. Stable / Immutable
|
|
@Stable data class UserState(...)
|
|
|
|
// 2. derivedStateOf
|
|
val isAtTop by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
|
|
|
|
// 3. key + contentType
|
|
items(items, key = { it.id }, contentType = { it.kind }) { ... }
|
|
|
|
// 4. animateItem
|
|
items(items, key = { it.id }) { it ->
|
|
Row(modifier = Modifier.animateItem()) { ... }
|
|
}
|
|
```
|
|
|
|
### Compose Compiler Metrics
|
|
```groovy
|
|
android {
|
|
composeOptions {
|
|
kotlinCompilerExtensionVersion = '1.5.10'
|
|
}
|
|
}
|
|
|
|
tasks.withType(KotlinCompile).configureEach {
|
|
kotlinOptions {
|
|
freeCompilerArgs += [
|
|
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$buildDir/compose_metrics",
|
|
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$buildDir/compose_metrics",
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
→ Reports: 어떤 component 가 unstable / non-skippable.
|
|
|
|
### Startup 측정 in production
|
|
```kotlin
|
|
// Firebase Performance / 자체
|
|
class App : Application() {
|
|
override fun onCreate() {
|
|
val startupTrace = Firebase.performance.newTrace("app_startup")
|
|
startupTrace.start()
|
|
super.onCreate()
|
|
// ...
|
|
startupTrace.stop()
|
|
}
|
|
}
|
|
```
|
|
|
|
### App Startup library
|
|
```kotlin
|
|
class MyInitializer : Initializer<Unit> {
|
|
override fun create(context: Context) {
|
|
// 빠른 init
|
|
}
|
|
override fun dependencies() = emptyList<Class<out Initializer<*>>>()
|
|
}
|
|
```
|
|
|
|
```xml
|
|
<provider android:name="androidx.startup.InitializationProvider" ...>
|
|
<meta-data android:name=".MyInitializer" android:value="androidx.startup" />
|
|
</provider>
|
|
```
|
|
|
|
→ ContentProvider 보다 빠른 init.
|
|
|
|
### CI 안 generation
|
|
```yaml
|
|
# .github/workflows/baseline-profile.yml
|
|
- name: Build + generate
|
|
run: ./gradlew :baselineprofile:generateBaselineProfile
|
|
- name: Commit if changed
|
|
uses: stefanzweifel/git-auto-commit-action@v5
|
|
with:
|
|
file_pattern: 'app/src/main/baseline-prof.txt'
|
|
commit_message: 'chore: update baseline profile'
|
|
```
|
|
|
|
### Profile vs ART AOT
|
|
```
|
|
ART (5.0+): 자동 PGO — 사용 후 점진 빠름.
|
|
Baseline Profile: 첫 사용부터 빠름.
|
|
|
|
→ 둘 다.
|
|
```
|
|
|
|
### Common gotchas
|
|
```
|
|
- emulator 결과 ≠ 실기. 실기에서 측정.
|
|
- Cold start 만 측정 — process 재시작.
|
|
- Macrobenchmark 가 자체 process — system overhead.
|
|
- Profile 갱신 — 매 release 권장.
|
|
- Compose version 변경 시 다시 generate.
|
|
```
|
|
|
|
### Frame timing
|
|
```
|
|
StartupTimingMetric: Activity launch 시간
|
|
FrameTimingMetric: p50/p95/p99 frame time
|
|
TraceSectionMetric: 자체 trace section
|
|
PowerMetric: 전력
|
|
NetworkUsageMetric: bytes
|
|
```
|
|
|
|
### Custom trace
|
|
```kotlin
|
|
// App code
|
|
Trace.beginSection("loadHomeFeed")
|
|
val items = repo.loadFeed()
|
|
Trace.endSection()
|
|
|
|
// Macrobenchmark
|
|
metrics = listOf(TraceSectionMetric("loadHomeFeed"))
|
|
```
|
|
|
|
### Profile size
|
|
```
|
|
일반: 5-50 KB.
|
|
큰 app: 200 KB 까지.
|
|
APK 안 별 영향 X.
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 적용 |
|
|
|---|---|
|
|
| 새 release | Baseline Profile 항상 |
|
|
| Compose UI 무거움 | 매우 효과 |
|
|
| 작은 utility app | 큰 효과 X |
|
|
| Startup 핵심 | 우선순위 |
|
|
| Scroll-heavy | 매우 효과 |
|
|
| Game | Profile 보다 game-specific |
|
|
|
|
## ❌ 안티패턴
|
|
- **Profile 한 번만 + 영원 사용**: 매 release 갱신.
|
|
- **Emulator 만 측정**: 실기와 다름.
|
|
- **모든 path profile**: 큰 file. critical 만.
|
|
- **R8 / minification 없이**: 효과 적음.
|
|
- **App Startup library 안 씀 + 큰 init**: cold start 느림.
|
|
- **Macrobenchmark 없는 측정**: 추측.
|
|
- **Compose Compiler Metrics 무시**: 어떤 게 unstable 모름.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- Macrobenchmark 가 측정 + profile 생성.
|
|
- 매 release CI 갱신.
|
|
- Compose stable / immutable + key + animateItem.
|
|
- App Startup library 로 init 빠르게.
|
|
|
|
## 🔗 관련 문서
|
|
- [[Android_LazyList_Performance]]
|
|
- [[Android_Compose_Recomposition_Pitfalls]]
|
|
- [[Mobile_App_Size_Optimization]]
|