Skip to content

Commit 1409cea

Browse files
authored
Merge pull request #113 from cheonjaeung/cheon/bench
Add benchmark
2 parents 89574a5 + cd38def commit 1409cea

7 files changed

Lines changed: 272 additions & 2 deletions

File tree

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,39 @@ VerticalGrid(
110110

111111
For more information, please visit [documentation](https://cheonjaeung.github.io/gridlayout-compose/) site.
112112

113+
## Performance
114+
115+
Benchmarks measure first-composition cost across three grid implementations.
116+
117+
> [!NOTE]
118+
> Measured on Galaxy S23 (SM-S911N), Android 16, CPU locked at 3.36 GHz.
119+
>
120+
> The measured library version is 2.7.2
121+
122+
### First Composition Time (Median)
123+
124+
| | 9 items | 30 items | 60 items |
125+
|-----------------------------|----------------|----------------|----------------|
126+
| **VerticalGrid (Baseline)** | 33.4 ms | 33.6 ms | 41.4 ms |
127+
| **RowColumn** | 33.4 ms (+0%) | 33.4 ms (−1%) | 41.5 ms (+0%) |
128+
| **LazyVerticalGrid** | 46.7 ms (+40%) | 43.8 ms (+30%) | 49.8 ms (+20%) |
129+
130+
### Allocation Count (Median)
131+
132+
| | 9 items | 30 items | 60 items |
133+
|-----------------------------|--------------|---------------|---------------|
134+
| **VerticalGrid (Baseline)** | 3,079 | 3,735 | 5,287 |
135+
| **RowColumn** | 3,499 (1.1×) | 4,512 (1.2×) | 7,058 (1.3×) |
136+
| **LazyVerticalGrid** | 6,682 (2.2×) | 16,522 (4.4×) | 19,125 (3.6×) |
137+
138+
- **VerticalGrid ≈ RowColumn**: No performance overhead over manually written `Row`+`Column` code, meaning the simpler API comes for free.
139+
- **VerticalGrid is faster than LazyVerticalGrid**: 20 ~ 40% faster on first composition for fully-visible grids, meaning this library is more efficient than LazyGrid for simple grids.
140+
- **VerticalGrid allocates less than LazyVerticalGrid**: 2 ~ 4 times fewer allocations than LazyVerticalGrid, meaning lower GC pressure in allocation-sensitive scenarios.
141+
142+
> [!NOTE]
143+
> This benchmark shows only the first composition cost.
144+
> If the dataset is large, `LazyVerticalGrid` is the right choice.
145+
113146
## Building
114147

115148
This project is kotlin multiplatform project.

benchmark/build.gradle.kts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
3+
plugins {
4+
alias(libs.plugins.android.library)
5+
alias(libs.plugins.kotlin.android)
6+
alias(libs.plugins.compose.compiler)
7+
}
8+
9+
android {
10+
namespace = "com.cheonjaeung.compose.grid.benchmark"
11+
compileSdk = 36
12+
13+
defaultConfig {
14+
minSdk = 23
15+
16+
testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
17+
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR"
18+
}
19+
20+
// Run instrumented tests against the release build type for accurate benchmark numbers.
21+
testBuildType = "release"
22+
23+
buildTypes {
24+
release {
25+
isMinifyEnabled = true
26+
// Benchmarks are not shipped to production. Reuse the debug signing key.
27+
signingConfig = signingConfigs.getByName("debug")
28+
}
29+
}
30+
31+
buildFeatures {
32+
compose = true
33+
}
34+
35+
compileOptions {
36+
sourceCompatibility = JavaVersion.VERSION_11
37+
targetCompatibility = JavaVersion.VERSION_11
38+
}
39+
40+
kotlin {
41+
compilerOptions {
42+
jvmTarget.set(JvmTarget.JVM_11)
43+
}
44+
}
45+
}
46+
47+
dependencies {
48+
androidTestImplementation(libs.androidx.benchmark.junit4)
49+
androidTestImplementation(libs.androidx.test.runner)
50+
androidTestImplementation(libs.compose.android.foundation)
51+
androidTestImplementation(libs.compose.android.ui.test.junit4)
52+
androidTestReleaseImplementation(libs.compose.android.ui.test.manifest)
53+
54+
androidTestImplementation(project(":grid"))
55+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package com.cheonjaeung.compose.grid.benchmark
2+
3+
import androidx.benchmark.junit4.BenchmarkRule
4+
import androidx.benchmark.junit4.measureRepeated
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.height
11+
import androidx.compose.foundation.lazy.grid.GridCells
12+
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
13+
import androidx.compose.foundation.lazy.grid.items
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.getValue
16+
import androidx.compose.runtime.mutableStateOf
17+
import androidx.compose.runtime.setValue
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.test.junit4.createComposeRule
20+
import androidx.compose.ui.unit.dp
21+
import androidx.test.ext.junit.runners.AndroidJUnit4
22+
import com.cheonjaeung.compose.grid.SimpleGridCells
23+
import com.cheonjaeung.compose.grid.VerticalGrid
24+
import org.junit.Rule
25+
import org.junit.Test
26+
import org.junit.runner.RunWith
27+
28+
private const val COLUMNS = 3
29+
private val ITEM_HEIGHT = 50.dp
30+
private val SPACING = 8.dp
31+
32+
/**
33+
* Benchmarks for first-composition cost: [VerticalGrid] vs [LazyVerticalGrid] vs [Row]+[Column].
34+
*
35+
* It can answer to two questions:
36+
* - How does [VerticalGrid] compare to [Row]+[Column] grid?
37+
* - How does [VerticalGrid] compare to [LazyVerticalGrid] when both must compose all items?
38+
*
39+
* [LazyVerticalGrid] is given an exact height that fits every item (all-visible),
40+
* so it cannot virtualize and the comparison is like-for-like on composition + measure cost.
41+
*
42+
* All item counts are multiples of [COLUMNS] so no placeholder Spacers are needed
43+
* in [RowColumnCase] — both contestants see exactly the same number of composables.
44+
*
45+
* Run with:
46+
* ```
47+
* ./gradlew :benchmark:connectedAndroidTest
48+
* ```
49+
*
50+
* Results are saved to:
51+
* ```
52+
* benchmark/build/outputs/connected_android_test_additional_output/releaseAndroidTest/connected/<device>/
53+
* ```
54+
*/
55+
@RunWith(AndroidJUnit4::class)
56+
class GridBenchmark {
57+
58+
@get:Rule(order = 1)
59+
val benchmarkRule = BenchmarkRule()
60+
61+
@get:Rule(order = 2)
62+
val composeTestRule = createComposeRule()
63+
64+
@Test
65+
fun verticalGrid_9() {
66+
benchmarkContent { VerticalGridCase(itemCount = 9) }
67+
}
68+
69+
@Test
70+
fun verticalGrid_30() {
71+
benchmarkContent { VerticalGridCase(itemCount = 30) }
72+
}
73+
74+
@Test
75+
fun verticalGrid_60() {
76+
benchmarkContent { VerticalGridCase(itemCount = 60) }
77+
}
78+
79+
@Test
80+
fun lazyVerticalGrid_9() {
81+
benchmarkContent { LazyVerticalGridCase(itemCount = 9) }
82+
}
83+
84+
@Test
85+
fun lazyVerticalGrid_30() {
86+
benchmarkContent { LazyVerticalGridCase(itemCount = 30) }
87+
}
88+
89+
@Test
90+
fun lazyVerticalGrid_60() {
91+
benchmarkContent { LazyVerticalGridCase(itemCount = 60) }
92+
}
93+
94+
@Test
95+
fun rowColumn_9() {
96+
benchmarkContent { RowColumnCase(itemCount = 9) }
97+
}
98+
99+
@Test
100+
fun rowColumn_30() {
101+
benchmarkContent { RowColumnCase(itemCount = 30) }
102+
}
103+
104+
@Test
105+
fun rowColumn_60() {
106+
benchmarkContent { RowColumnCase(itemCount = 60) }
107+
}
108+
109+
private fun benchmarkContent(content: @Composable () -> Unit) {
110+
var isVisible by mutableStateOf(false)
111+
112+
composeTestRule.setContent {
113+
if (isVisible) content()
114+
}
115+
composeTestRule.waitForIdle()
116+
117+
benchmarkRule.measureRepeated {
118+
isVisible = true
119+
composeTestRule.waitForIdle()
120+
121+
runWithMeasurementDisabled {
122+
isVisible = false
123+
composeTestRule.waitForIdle()
124+
}
125+
}
126+
}
127+
}
128+
129+
@Composable
130+
private fun VerticalGridCase(itemCount: Int) {
131+
VerticalGrid(
132+
columns = SimpleGridCells.Fixed(COLUMNS),
133+
modifier = Modifier.fillMaxWidth(),
134+
horizontalArrangement = Arrangement.spacedBy(SPACING),
135+
verticalArrangement = Arrangement.spacedBy(SPACING),
136+
) {
137+
repeat(itemCount) { Box(modifier = Modifier.height(ITEM_HEIGHT)) }
138+
}
139+
}
140+
141+
@Composable
142+
private fun LazyVerticalGridCase(itemCount: Int) {
143+
val rows = itemCount / COLUMNS
144+
val gridHeight = ITEM_HEIGHT * rows + SPACING * (rows - 1)
145+
LazyVerticalGrid(
146+
columns = GridCells.Fixed(COLUMNS),
147+
modifier = Modifier.height(gridHeight),
148+
horizontalArrangement = Arrangement.spacedBy(SPACING),
149+
verticalArrangement = Arrangement.spacedBy(SPACING),
150+
) {
151+
items(List(itemCount) { it }) { Box(modifier = Modifier.height(ITEM_HEIGHT)) }
152+
}
153+
}
154+
155+
@Composable
156+
private fun RowColumnCase(itemCount: Int) {
157+
val rowCount = itemCount / COLUMNS
158+
Column(
159+
modifier = Modifier.fillMaxWidth(),
160+
verticalArrangement = Arrangement.spacedBy(SPACING),
161+
) {
162+
(0 until rowCount).forEach { _ ->
163+
Row(
164+
modifier = Modifier.fillMaxWidth(),
165+
horizontalArrangement = Arrangement.spacedBy(SPACING),
166+
) {
167+
(0 until COLUMNS).forEach { _ ->
168+
Box(modifier = Modifier.weight(1f).height(ITEM_HEIGHT))
169+
}
170+
}
171+
}
172+
}
173+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
</manifest>

build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ subprojects {
3333
}
3434

3535
apiValidation {
36-
// :samples:android, :samples:shared
37-
ignoredProjects.addAll(listOf("android", "shared"))
36+
// :samples:android, :samples:shared, :benchmark
37+
ignoredProjects.addAll(listOf("android", "shared", "benchmark"))
3838

3939
nonPublicMarkers.add("kotlin.PublishedApi")
4040
}

gradle/libs.versions.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ compose-android-runtime = { module = "androidx.compose.runtime:runtime", version
2525
compose-android-foundation = { module = "androidx.compose.foundation:foundation", version = "1.10.3" }
2626
compose-android-ui = { module = "androidx.compose.ui:ui", version = "1.10.3" }
2727
compose-android-material3 = { module = "androidx.compose.material3:material3", version = "1.4.0" }
28+
29+
# Benchmark dependencies
30+
androidx-benchmark-junit4 = { module = "androidx.benchmark:benchmark-junit4", version = "1.4.1" }
31+
androidx-test-runner = { module = "androidx.test:runner", version = "1.7.0" }
32+
compose-android-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version = "1.10.6" }
33+
compose-android-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version = "1.10.6" }

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ pluginManagement {
99
rootProject.name = "gridlayout-compose"
1010

1111
include(":grid")
12+
include(":benchmark")
1213
include(":samples:shared")
1314
include(":samples:android")

0 commit comments

Comments
 (0)