Jetpack Compose ekran geçişlerini Macrobenchmark ve Perfetto ile tekrar üretilebilir biçimde ölçün. Ana thread'deki liste dönüşümlerini bularak p90 frame overrun değerini önce-sonra karşılaştırın.
Jetpack Compose'da Macrobenchmark ile Ekran Geçişlerini Ölçmek
Jetpack Compose için tekrar üretilebilir Macrobenchmark senaryosu
Ekran geçişini Android Studio'da elle gözlemlemek, cihazın termal durumu ve derleme modu değiştiği için karşılaştırılabilir bir ölçüm değildir. Ayrı bir benchmark modülünde release'e yakın hedef uygulamayı 15 kez çalıştırın; soğuk açılış yerine yalnızca feed-detail geçişini ölçmek için StartupMode.WARM kullanın. Bu ayrım önemlidir: cold start'ta dex yükleme ve process oluşturma maliyeti, navigation composable'ının frame maliyetini maskeleyebilir.
// benchmark/build.gradle.kts
plugins {
id("com.android.test")
id("androidx.benchmark")
}
android {
namespace = "com.opendart.sample.benchmark"
targetProjectPath = ":app"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation("androidx.benchmark:benchmark-macro-junit4:<uyumlu-surum>")
androidTestImplementation("androidx.test.uiautomator:uiautomator:<uyumlu-surum>")
}UIAutomator'ın Compose semantics düğümünü bulabilmesi için geçiş düğümüne sabit bir content description verin. Görünen metne göre seçim yapmak lokalizasyon değişince testi kırar; By.desc("open_detail") ise çeviriden bağımsızdır. Hedef uygulamanın manifest dosyasındaki profileable etiketi, shell aracılığıyla trace toplanmasına izin verir; Macrobenchmark çalıştırmadan önce bunun benchmark aldığınız varyantta bulunduğunu doğrulayın.
<!-- app/src/main/AndroidManifest.xml -->
<application ...>
<profileable android:shell="true" />
</application>
@Composable
fun FeedRoute(onOpenDetail: () -> Unit) {
Button(
onClick = onOpenDetail,
modifier = Modifier.semantics {
contentDescription = "open_detail"
}
) {
Text("Detayı aç")
}
}Asıl testte FrameTimingMetric() render edilen frame'lerin overrun dağılımını, TraceSectionMetric("feed_map_rows") ise uygulamanın işaretlediği CPU bloğunu toplar. Her iterasyonda feed'e dönmek zorunludur; aksi halde ikinci iterasyonda detail ekranı zaten composition'da veya navigation back stack'te kalabilir ve ilk geçiş maliyetini ölçmezsiniz.
@RunWith(AndroidJUnit4::class)
class FeedNavigationBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun feedToDetail() = benchmarkRule.measureRepeated(
packageName = "com.opendart.sample",
metrics = listOf(
FrameTimingMetric(),
TraceSectionMetric("feed_map_rows")
),
compilationMode = CompilationMode.Partial(),
startupMode = StartupMode.WARM,
iterations = 15,
setupBlock = {
pressHome()
startActivityAndWait()
}
) {
device.findObject(By.desc("open_detail")).click()
device.waitForIdle()
pressBack()
device.waitForIdle()
}
}Perfetto ile frame overrun ve Compose trace'ini ayırmak
Benchmark çıktısındaki tek bir medyan sayı kök nedeni vermez. Testi aşağıdaki komutla çalıştırın, üretilen .perfetto-trace dosyasını Perfetto UI'da açın ve UI Thread üzerindeki uzun dilimleri frame timeline ile hizalayın. 16.6 ms ekran yenileme hedefi olan bir cihazda 16.6 ms'yi aşan her frame overrun üretir; ancak 120 Hz cihazda bütçe 8.3 ms olduğu için cihaz modelini sonuç dosyasına mutlaka kaydedin.
./gradlew :benchmark:connectedBenchmarkAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.opendart.sample.benchmark.FeedNavigationBenchmark
find benchmark/build/outputs -name '*.perfetto-trace'Uygulama tarafında yalnızca şüpheli dönüşümü işaretleyin. Buradaki amaç bütün composable ağacını trace ile doldurmak değil, frame içinde CPU tüketen saf dönüşümün süresini ayrı görmektir. Trace.beginSection ile açılan bölüm ana thread'de 7 ms sürüyor, RenderThread boş kalıyor ve GPU dilimleri kısa görünüyorsa sorun çizim değil Kotlin koleksiyon işlemidir.
private fun List<Message>.toRows(filter: Filter): List<FeedRowUi> {
Trace.beginSection("feed_map_rows")
return try {
asSequence()
.filter { filter.accepts(it) }
.sortedByDescending { it.createdAt }
.map { FeedRowUi(it.id, it.title, it.createdAt) }
.toList()
} finally {
Trace.endSection()
}
}Örnek bir başlangıç koşusunda p90 frameOverrunMs 22.4 ms, feed_map_rows p90 değeri 8.1 ms çıktıysa, önce bu iki sayıyı aynı cihaz, aynı veri kümesi ve aynı CompilationMode.Partial() ile kaydedin. Farklı compilation mode'ları karşılaştırmak yanıltıcıdır: JIT/AOT derleme farkı, kod değişikliğiniz olmadan method maliyetini değiştirebilir. Perfetto'da uzun dilim Choreographer#doFrame altında değilse, örneğin ağ callback'inde ise navigation benchmark'ı yerine o iş akışını tetikleyen ayrı bir senaryo yazın.
Android MVVM mimarisi içinde ana thread dönüşümünü taşımak
Sık rastlanan hata, Room veya repository'den gelen ham listeyi composable gövdesinde sıralamaktır. Aşağıdaki kodda scroll state, tema veya başka bir state değişimi feed composable'ını yeniden çalıştırdığında sortedByDescending yeniden çağrılabilir. remember(messages) yalnızca liste referansı değişmediğinde yardım eder; birçok repository her emisyona yeni bir List ürettiğinden referans anahtarı bu vakada koruma sağlamaz.
@Composable
fun FeedScreen(messages: List<Message>, listState: LazyListState) {
val rows = messages
.filter { !it.archived }
.sortedByDescending { it.createdAt }
.map { FeedRowUi(it.id, it.title, it.createdAt) }
LazyColumn(state = listState) {
items(rows) { row -> FeedRow(row) }
}
}Android MVVM mimarisi sınırında UI için hazır satır modelleri üretin ve CPU ağırlıklı sıralama ile map işlemini Dispatchers.Default üzerinde çalıştırın. flowOn operatörü kendisinden önceki upstream zincire etki eder; bu nedenle burada stateIn'den önce konumlanmalıdır. WhileSubscribed(5_000), kısa konfigürasyon değişiminde upstream'i hemen kapatmaz ama kullanıcı ekrandan ayrıldıktan sonra sürekli Room emisyonu işlemekten kaçınır.
class FeedViewModel(
repository: MessageRepository
) : ViewModel() {
private val filter = MutableStateFlow(Filter.Active)
val rows: StateFlow<List<FeedRowUi>> = combine(
repository.observeMessages(),
filter
) { messages, selectedFilter ->
messages.toRows(selectedFilter)
}
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
}
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val rows by viewModel.rows.collectAsStateWithLifecycle()
LazyColumn {
items(
items = rows,
key = { it.id },
contentType = { "feed_row" }
) { row ->
FeedRow(row)
}
}
}Buradaki key salt performans ayrıntısı değildir. Liste sıralaması değiştiğinde key verilmezse LazyColumn pozisyona göre composition state'i yeniden kullanabilir; örneğin satır içindeki checkbox state'i başka mesaja taşınabilir. Sabit kimlik ve tek tip contentType, Compose'un uygun item composition'larını geri dönüştürmesine izin verir. Bu değişiklikten sonra aynı benchmark'ta p90 feed_map_rows değerini 8.1 ms'den 1.3 ms'ye, p90 frameOverrunMs değerini 22.4 ms'den 6.8 ms'ye indirdiyseniz, yalnızca ortalamayı değil p90 değerini raporlayın; kullanıcıların gördüğü kısa takılmalar dağılımın kuyruğunda bulunur.
Jetpack Compose benchmark sonucunu CI ve release artefaktında doğrulamak
CI'da emülatör snapshot'ı, animasyon ölçekleri ve veri hacmi sabit değilse benchmark karşılaştırması anlamlı değildir. Test başlangıcında sabit 5000 kayıt içeren yerel bir Room veritabanı kurun, ağ çağrısını fake repository ile kapatın ve cihazı her işten önce yeniden başlatın. Ayrıca benchmark artefaktını PR bazında saklayın; Gradle'ın ürettiği JSON özeti hızlı kıyas için, Perfetto trace ise regresyonun nedenini incelemek için gereklidir.
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
./gradlew :benchmark:connectedBenchmarkAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.opendart.sample.benchmark.FeedNavigationBenchmark --no-build-cachePlay Store yayınlama öncesinde benchmark'ın debug APK yerine yayınlayacağınız minify edilmiş artefakta yakın bir varyantı hedeflediğini kontrol edin. R8 bazen lambda ve inline akışını değiştirdiği için debug trace'inde görünmeyen bir maliyet release'te ortaya çıkabilir. Bundle manifestini bundletool ile incelemek, benchmark için gerekli profileable ayarının hangi artefakta girdiğini denetlemenin somut yoludur.
java -jar bundletool-all.jar dump manifest --bundle=app-release.aab --module=base | grep -n "profileable"Bir android eğitimi veya android eğitim planında bu çalışma, yalnızca Compose API ezberinden daha değerlidir: android studio eğitimi sırasında Macrobenchmark çıktısını açıp Perfetto'da doğrulamak gerekir. Aynı pratik android kursu, kotlin eğitimi ve kotlin kursu katılımcılarına Flow operatör bağlamının etkisini gösterir; mobil uygulama geliştirme eğitimi için de ölçüm senaryosu, veri seti ve p90 eşiği kod inceleme maddesi haline getirilebilir.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
Android Studio eğitimi sırasında Macrobenchmark trace dosyasını nerede bulurum?
Bağlı cihaz testinden sonra `benchmark/build/outputs` altında `.perfetto-trace` dosyasını `find benchmark/build/outputs -name '*.perfetto-trace'` ile bulun. Dosyayı ui.perfetto.dev üzerinde açın; UI Thread track'inde `feed_map_rows` dilimini ve aynı zaman aralığındaki FrameTimeline dilimlerini birlikte inceleyin.
Android MVVM mimarisi ve Kotlin eğitimi için Flow dönüşümünü neden composable yerine ViewModel'de yapmalıyım?
Composable gövdesi state değişimlerinde tekrar çalışabilir. Sıralama ve map işlemini ViewModel'deki Flow zincirine taşıyıp `flowOn(Dispatchers.Default)` kullanırsanız CPU işi ana thread frame'i içinde yapılmaz. Ancak `flowOn` mutlaka dönüşümden sonra, `stateIn` çağrısından önce olmalıdır; aksi halde beklediğiniz upstream bağlam değişimi gerçekleşmez.
Android kursu kapsamında Jetpack Compose ekran geçişi için kaç benchmark iterasyonu kullanmalıyım?
Başlangıç için 15 iterasyon kullanılabilir, fakat tek sayı yeterli değildir. Aynı fiziksel cihazda 3 ayrı koşu alın, p50 ve p90 `frameOverrunMs` değerlerini karşılaştırın. Koşular arası p90 farkı büyükse termal throttling, arka plan işlemleri veya değişken veri seti vardır; kod değişikliğine sonuç atfetmeden önce bu koşulları sabitleyin.
Mobil uygulama geliştirme eğitimi ve play store yayınlama öncesinde benchmark hangi build ile çalışmalı?
Debug APK yerine R8 ve release ayarlarına yakın bir benchmark hedefi kullanın. Yayın AAB'sinin manifestini `bundletool dump manifest --bundle=app-release.aab --module=base` komutuyla denetleyin ve benchmarkın packageName değerinin gerçekten ölçmek istediğiniz uygulama varyantına ait olduğunu doğrulayın.
AI / LLM Discovery
Bu makale Opendart Akademi Android eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


