Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Jetchat/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@
.cxx
local.properties
.kotlin/

# Macrobenchmark results and Perfetto traces from local performance runs
/benchmark_reports
18 changes: 18 additions & 0 deletions Jetchat/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose)
alias(libs.plugins.androidx.baselineprofile)
}

android {
Expand Down Expand Up @@ -63,6 +64,13 @@ android {
"proguard-rules.pro",
)
}

// Non-debuggable, non-minified build type for Macrobenchmark and composition tracing
create("benchmark") {
initWith(getByName("debug"))
isDebuggable = false
signingConfig = signingConfigs.getByName("debug")
}
}

kotlin {
Expand All @@ -88,6 +96,11 @@ android {
}
}

baselineProfile {
// Put the generated profile in src/main so every build type, including benchmark, ships it
mergeIntoMain = true
}

dependencies {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
Expand Down Expand Up @@ -115,8 +128,13 @@ dependencies {
implementation(libs.androidx.compose.ui.util)
implementation(libs.androidx.compose.ui.viewbinding)
implementation(libs.androidx.compose.ui.googlefonts)
// Adds composable function names to Perfetto traces
implementation(libs.androidx.compose.runtime.tracing)
implementation(libs.androidx.profileinstaller)
baselineProfile(project(":benchmark"))

implementation(libs.androidx.media3.exoplayer)
implementation(libs.coil.kt.compose)
implementation(libs.androidx.media3.ui.compose)

debugImplementation(libs.androidx.compose.ui.test.manifest)
Expand Down
23 changes: 23 additions & 0 deletions Jetchat/app/src/benchmark/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required for macrobenchmark to write trace files on some API levels -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
<profileable android:shell="true" />
</application>
</manifest>
24,342 changes: 24,342 additions & 0 deletions Jetchat/app/src/main/generated/baselineProfiles/baseline-prof.txt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.example.compose.jetchat.R
import com.example.compose.jetchat.data.colleagueProfile
import com.example.compose.jetchat.data.meProfile
Expand Down Expand Up @@ -191,8 +192,9 @@ private fun ProfileItem(text: String, @DrawableRes profilePic: Int?, selected: B
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)
.size(24.dp)
if (profilePic != null) {
Image(
painter = painterResource(id = profilePic),
// Profile photos are large rasters shown at 24dp: decode off the main thread at that size
AsyncImage(
model = profilePic,
modifier = paddingSizeModifier.then(Modifier.clip(CircleShape)),
contentScale = ContentScale.Crop,
contentDescription = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.example.compose.jetchat.FunctionalityNotAvailablePopup
import com.example.compose.jetchat.R
import com.example.compose.jetchat.blur.backdropBlur
Expand Down Expand Up @@ -785,13 +786,9 @@ fun ChatItemBubble(
if (hasText) {
Spacer(modifier = Modifier.height(4.dp))
}
val painter = painterResource(imageRes)
val intrinsicSize = painter.intrinsicSize
val aspectRatio = if (intrinsicSize.width > 0f && intrinsicSize.height > 0f) {
intrinsicSize.width / intrinsicSize.height
} else {
1f
}
// Message images are large photos: decode them off the main thread at display size
// instead of with painterResource, which decodes the full bitmap during composition.
val aspectRatio = rememberImageAspectRatio(imageRes)
Surface(
color = if (isLiked) Color.Transparent else backgroundBubbleColor,
shape = bubbleShape,
Expand All @@ -814,8 +811,8 @@ fun ChatItemBubble(
},
),
) {
Image(
painter = painter,
AsyncImage(
model = imageRes,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(if (isLiked) 8.dp else 0.dp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.compose.jetchat.conversation

import android.content.res.Resources
import android.graphics.BitmapFactory
import androidx.annotation.DrawableRes
import androidx.collection.mutableIntFloatMapOf
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalResources

private val aspectRatioCache = mutableIntFloatMapOf()

/**
* Width / height of a raster drawable, read from the image header only. Lets message bubbles size
* themselves before the image is decoded asynchronously, so the layout matches a synchronous load.
*/
@Composable
fun rememberImageAspectRatio(@DrawableRes imageRes: Int): Float {
val resources = LocalResources.current
return remember(imageRes, resources) {
aspectRatioCache.getOrPut(imageRes) { readAspectRatio(resources, imageRes) }
}
}

private fun readAspectRatio(resources: Resources, @DrawableRes imageRes: Int): Float {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeResource(resources, imageRes, options)
return if (options.outWidth > 0 && options.outHeight > 0) {
options.outWidth.toFloat() / options.outHeight
} else {
1f
}
}
65 changes: 65 additions & 0 deletions Jetchat/benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.androidx.baselineprofile)
}

android {
namespace = "com.example.compose.jetchat.benchmark"
compileSdk = libs.versions.compileSdk.get().toInt()
compileSdkMinor = 2

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
compilerOptions {
jvmTarget = JvmTarget.fromTarget("17")
}
}

defaultConfig {
minSdk = 28
targetSdk = libs.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

targetProjectPath = ":app"
// Enable the benchmark to run separately from the app process
experimentalProperties["android.experimental.self-instrumenting"] = true

buildTypes {
create("benchmark") {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("debug")
// Target the app's non-debuggable, non-minified benchmark build type
matchingFallbacks += listOf("benchmark", "release")
}
}
}

dependencies {
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.compose.jetchat.benchmark

import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()

@Test
fun generate() = baselineProfileRule.collect(packageName = TARGET_PACKAGE) {
pressHome()
launchJetchat()
val list = waitForConversation()
scrollConversation(list)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.compose.jetchat.benchmark

import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.TraceSectionMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalMetricApi::class)
@RunWith(AndroidJUnit4::class)
class ConversationScrollBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()

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

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

private fun scroll(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = listOf(
FrameTimingMetric(),
// Mode.Sum reports both Compose:recomposeSumMs and Compose:recomposeCount
TraceSectionMetric("Compose:recompose", TraceSectionMetric.Mode.Sum),
),
compilationMode = compilationMode,
startupMode = StartupMode.WARM,
iterations = 10,
setupBlock = {
pressHome()
launchJetchat()
},
) {
val list = waitForConversation()
scrollConversation(list)
}
}
Loading
Loading