Skip to content

Commit 36f07f1

Browse files
committed
reply performance setup
1 parent 8f311fa commit 36f07f1

15 files changed

Lines changed: 29769 additions & 17 deletions

File tree

Reply/app/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ android {
6363
"proguard-rules.pro",
6464
)
6565
}
66+
67+
create("benchmark") {
68+
initWith(getByName("debug"))
69+
isDebuggable = false
70+
signingConfig = signingConfigs.getByName("debug")
71+
matchingFallbacks.add("release")
72+
matchingFallbacks.add("debug")
73+
}
6674
}
6775

6876
testOptions {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright 2026 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
18+
<!-- Required for macrobenchmark to write trace files on some API levels -->
19+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
20+
<application>
21+
<profileable android:shell="true" />
22+
</application>
23+
</manifest>

Reply/app/src/main/baseline-prof.txt

Lines changed: 29405 additions & 0 deletions
Large diffs are not rendered by default.

Reply/app/src/main/java/com/example/reply/data/Account.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
package com.example.reply.data
1818

1919
import androidx.annotation.DrawableRes
20+
import androidx.compose.runtime.Immutable
2021

2122
/**
2223
* An object which represents an account which can belong to a user. A single user can have
2324
* multiple accounts.
2425
*/
26+
@Immutable
2527
data class Account(
2628
val id: Long,
2729
val uid: Long,
@@ -30,7 +32,7 @@ data class Account(
3032
val email: String,
3133
val altEmail: String,
3234
@DrawableRes val avatar: Int,
33-
var isCurrentAccount: Boolean = false,
35+
val isCurrentAccount: Boolean = false,
3436
) {
3537
val fullName: String = "$firstName $lastName"
3638
}

Reply/app/src/main/java/com/example/reply/data/Email.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@
1616

1717
package com.example.reply.data
1818

19+
import androidx.compose.runtime.Immutable
20+
1921
/**
2022
* A simple data class to represent an Email.
2123
*/
24+
@Immutable
2225
data class Email(
2326
val id: Long,
2427
val sender: Account,
2528
val recipients: List<Account> = emptyList(),
2629
val subject: String,
2730
val body: String,
2831
val attachments: List<EmailAttachment> = emptyList(),
29-
var isImportant: Boolean = false,
30-
var isStarred: Boolean = false,
31-
var mailbox: MailboxType = MailboxType.INBOX,
32+
val isImportant: Boolean = false,
33+
val isStarred: Boolean = false,
34+
val mailbox: MailboxType = MailboxType.INBOX,
3235
val createdAt: String,
3336
val threads: List<Email> = emptyList(),
3437
)

Reply/app/src/main/java/com/example/reply/data/EmailAttachment.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ import androidx.annotation.DrawableRes
2121
/**
2222
* An object class to define an attachment to email object.
2323
*/
24+
import androidx.compose.runtime.Immutable
25+
26+
@Immutable
2427
data class EmailAttachment(@DrawableRes val resId: Int, val contentDesc: String)

Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass
2626
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
2727
import androidx.compose.runtime.Composable
2828
import androidx.compose.runtime.getValue
29+
import androidx.compose.runtime.remember
2930
import androidx.compose.ui.tooling.preview.Preview
3031
import androidx.compose.ui.unit.DpSize
3132
import androidx.compose.ui.unit.dp
@@ -49,19 +50,27 @@ class MainActivity : ComponentActivity() {
4950
val displayFeatures = calculateDisplayFeatures(this)
5051
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
5152

53+
val closeDetailScreen = remember(viewModel) {
54+
{ viewModel.closeDetailScreen() }
55+
}
56+
val navigateToDetail = remember(viewModel) {
57+
{ emailId: Long, pane: com.example.reply.ui.utils.ReplyContentType ->
58+
viewModel.setOpenedEmail(emailId, pane)
59+
}
60+
}
61+
val toggleSelectedEmail = remember(viewModel) {
62+
{ emailId: Long ->
63+
viewModel.toggleSelectedEmail(emailId)
64+
}
65+
}
66+
5267
ReplyApp(
5368
windowSize = windowSize,
5469
displayFeatures = displayFeatures,
5570
replyHomeUIState = uiState,
56-
closeDetailScreen = {
57-
viewModel.closeDetailScreen()
58-
},
59-
navigateToDetail = { emailId, pane ->
60-
viewModel.setOpenedEmail(emailId, pane)
61-
},
62-
toggleSelectedEmail = { emailId ->
63-
viewModel.toggleSelectedEmail(emailId)
64-
},
71+
closeDetailScreen = closeDetailScreen,
72+
navigateToDetail = navigateToDetail,
73+
toggleSelectedEmail = toggleSelectedEmail,
6574
)
6675
}
6776
}

Reply/app/src/main/java/com/example/reply/ui/ReplyListContent.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import androidx.compose.material3.MaterialTheme
3838
import androidx.compose.material3.Text
3939
import androidx.compose.runtime.Composable
4040
import androidx.compose.runtime.LaunchedEffect
41+
import androidx.compose.runtime.remember
4142
import androidx.compose.ui.Alignment
4243
import androidx.compose.ui.Modifier
4344
import androidx.compose.ui.res.painterResource
@@ -168,6 +169,12 @@ fun ReplyEmailList(
168169
modifier: Modifier = Modifier,
169170
navigateToDetail: (Long, ReplyContentType) -> Unit,
170171
) {
172+
val onNavigateToDetail = remember(navigateToDetail) {
173+
{ emailId: Long ->
174+
navigateToDetail(emailId, ReplyContentType.SINGLE_PANE)
175+
}
176+
}
177+
171178
Box(modifier = modifier.windowInsetsPadding(WindowInsets.statusBars)) {
172179
ReplyDockedSearchBar(
173180
emails = emails,
@@ -188,9 +195,7 @@ fun ReplyEmailList(
188195
items(items = emails, key = { it.id }) { email ->
189196
ReplyEmailListItem(
190197
email = email,
191-
navigateToDetail = { emailId ->
192-
navigateToDetail(emailId, ReplyContentType.SINGLE_PANE)
193-
},
198+
navigateToDetail = onNavigateToDetail,
194199
toggleSelection = toggleEmailSelection,
195200
isOpened = openedEmail?.id == email.id,
196201
isSelected = selectedEmailIds.contains(email.id),

Reply/benchmark/build.gradle.kts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
plugins {
18+
alias(libs.plugins.android.test)
19+
}
20+
21+
android {
22+
compileSdk = libs.versions.compileSdk.get().toInt()
23+
namespace = "com.example.reply.benchmark"
24+
25+
defaultConfig {
26+
minSdk = 23
27+
targetSdk = libs.versions.targetSdk.get().toInt()
28+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
29+
}
30+
31+
buildTypes {
32+
// This benchmark build type is matching the type in the target application
33+
create("benchmark") {
34+
signingConfig = signingConfigs.getByName("debug")
35+
matchingFallbacks.add("release")
36+
}
37+
}
38+
39+
targetProjectPath = ":app"
40+
experimentalProperties["android.experimental.self-instrumenting"] = true
41+
}
42+
43+
dependencies {
44+
implementation(libs.androidx.test.rules)
45+
implementation(libs.androidx.test.ext.junit)
46+
implementation(libs.androidx.test.uiautomator)
47+
implementation(libs.androidx.benchmark.macro.junit4)
48+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.reply.benchmark
18+
19+
import android.content.Intent
20+
import androidx.benchmark.macro.CompilationMode
21+
import androidx.benchmark.macro.StartupMode
22+
import androidx.benchmark.macro.StartupTimingMetric
23+
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
24+
import androidx.test.ext.junit.runners.AndroidJUnit4
25+
import androidx.test.platform.app.InstrumentationRegistry
26+
import androidx.test.uiautomator.By
27+
import androidx.test.uiautomator.Until
28+
import org.junit.Rule
29+
import org.junit.Test
30+
import org.junit.runner.RunWith
31+
32+
@RunWith(AndroidJUnit4::class)
33+
class ExampleStartupBenchmark {
34+
@get:Rule
35+
val benchmarkRule = MacrobenchmarkRule()
36+
37+
private val targetPackageName = "com.example.reply"
38+
39+
@Test
40+
fun startupCold() = benchmarkRule.measureRepeated(
41+
packageName = targetPackageName,
42+
metrics = listOf(StartupTimingMetric()),
43+
compilationMode = CompilationMode.None(),
44+
startupMode = StartupMode.COLD,
45+
iterations = 5
46+
) {
47+
pressHome()
48+
val context = InstrumentationRegistry.getInstrumentation().context
49+
val intent = context.packageManager.getLaunchIntentForPackage(packageName)!!
50+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
51+
context.startActivity(intent)
52+
device.wait(Until.hasObject(By.pkg(packageName).depth(0)), 15000)
53+
}
54+
}

0 commit comments

Comments
 (0)