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
13 changes: 12 additions & 1 deletion benchmarks/multiplatform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
- `./gradlew :benchmarks:run`

## Run native on iOS
Open the project in Fleet or Android Studio with KMM plugin installed and
Open the project in Fleet or Android Studio with KMM plugin installed and
choose `iosApp` run configuration. Make sure that you build the app in `Release` configuration.
Alternatively you may open `iosApp/iosApp` project in XCode and run the app from there.

## Run automated iOS benchmarks
1. To run on device, open `iosApp/iosApp.xcodeproj` and properly configure the Signing section on the Signing & Capabilities project tab.
2. Use the following command to get list of all iOS devices:
- `xcrun xctrace list devices`
3. From the benchmarks directory run:
- `./iosApp/run_ios_benchmarks.sh <DEVICE ID>`
4. Results are saved as `.txt` files in `benchmarks_result/`.

## Run native on MacOS
- `./gradlew :benchmarks:runReleaseExecutableMacosArm64` (Works on Arm64 processors)
- `./gradlew :benchmarks:runReleaseExecutableMacosX64` (Works on Intel processors)
Expand Down Expand Up @@ -48,3 +56,6 @@ Please run your browser with manual GC enabled before running the benchmark, lik
| LazyList | [benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt](benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt) | Tests the performance of a complex LazyColumn implementation with features like pull-to-refresh, loading more items, and continuous scrolling. |
| MultipleComponents | [benchmarks/src/commonMain/kotlin/benchmarks/example1/Example1.kt](benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt) | Tests the performance of a comprehensive UI that showcases various Compose components including layouts, animations, and styled text. |
| MultipleComponents-NoVectorGraphics | [benchmarks/src/commonMain/kotlin/benchmarks/example1/Example1.kt](benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt) | Same as MultipleComponents but skips the Composables with vector graphics rendering. |
| TextLayout | [benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt](benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt) | Tests text layout and rendering performance by continuously scrolling column with big number of heady to layout items. |
| CanvasDrawing | [benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt](benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt) | Tests Canvas drawing performance by scrolling items with massive amount of graphic shapes. |
| HeavyShader | [benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt](benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt) | Tests GPU shader performance by scrolling items with a complex GPU shader. |
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import benchmarks.complexlazylist.components.MainUiNoImageUseModel
import benchmarks.multipleComponents.MultipleComponentsExample
import benchmarks.lazygrid.LazyGrid
import benchmarks.visualeffects.NYContent
import benchmarks.textlayout.TextLayout
import benchmarks.canvasdrawing.CanvasDrawing
import benchmarks.heavyshader.HeavyShader
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -298,8 +301,11 @@ fun getBenchmarks(): List<Benchmark> = listOf(
Benchmark("MultipleComponents") { MultipleComponentsExample() },
Benchmark("MultipleComponents-NoVectorGraphics") {
MultipleComponentsExample(isVectorGraphicsSupported = false)
}
)
},
Benchmark("TextLayout") { TextLayout() },
Benchmark("CanvasDrawing") { CanvasDrawing() },
Benchmark("HeavyShader") { HeavyShader() }
).sortedBy { it.name }

suspend fun runBenchmark(
benchmark: Benchmark,
Expand Down Expand Up @@ -403,7 +409,7 @@ fun BenchmarkRunner(
val stats = BenchmarkResult(
name = benchmark.name,
frameBudget = nanosPerFrame.nanoseconds,
conditions = BenchmarkConditions(benchmark.frameCount, 0),
conditions = BenchmarkConditions(benchmark.frameCount, warmupCount = Config.warmupCount),
averageFrameInfo = FrameInfo(duration / benchmark.frameCount, Duration.ZERO),
averageFPSInfo = FPSInfo(benchmark.frameCount.toDouble() / duration.toDouble(DurationUnit.SECONDS)),
frames = frames
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/

package benchmarks.canvasdrawing

import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameMillis
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.unit.dp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlin.random.Random
import kotlinx.coroutines.isActive

private const val ITEM_COUNT = 1200

@Composable
fun CanvasDrawing() {
val listState = rememberLazyListState()
var scrollForward by remember { mutableStateOf(true) }

LaunchedEffect(Unit) {
while (isActive) {
withFrameMillis { }
val currentItem = listState.firstVisibleItemIndex
if (currentItem == 0) scrollForward = true
if (currentItem > ITEM_COUNT - 100) scrollForward = false
listState.scrollBy(if (scrollForward) 33f else -33f)
}
}

LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize().background(Color.Black)
) {
items(ITEM_COUNT) { index ->
CanvasDrawingItem(index)
}
}
}

@Composable
private fun CanvasDrawingItem(index: Int) {
val animatedValue by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)

Canvas(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.padding(8.dp)
) {
val seed = index * 12345L
val random = Random(seed)

drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()),
Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt())
)
)
)

repeat(200) { i ->
val angle = animatedValue + i * 7.2f
val radius = size.minDimension / 4f
val x = size.width / 2 + cos(angle * PI / 180.0).toFloat() * radius * random.nextFloat()
val y = size.height / 2 + sin(angle * PI / 180.0).toFloat() * radius * random.nextFloat()

val path = Path().apply {
moveTo(x, y)
repeat(40) { j ->
val pointAngle = angle + j * 45f + animatedValue
val pointRadius = 20f + random.nextFloat() * 30f
val px = x + cos(pointAngle * PI / 180.0).toFloat() * pointRadius
val py = y + sin(pointAngle * PI / 180.0).toFloat() * pointRadius
if (j % 2 == 0) {
lineTo(px, py)
} else {
quadraticBezierTo(
x + random.nextFloat() * 50f - 25f,
y + random.nextFloat() * 50f - 25f,
px, py
)
}
}
close()
}

drawPath(
path = path,
brush = Brush.radialGradient(
colors = listOf(
Color(random.nextInt(0xFFFFFF) or 0x80000000.toInt()),
Color(random.nextInt(0xFFFFFF) or 0x40000000.toInt())
),
center = Offset(x, y)
)
)
}

repeat(70) { i ->
val lineY = i * (size.height / 30)
drawLine(
color = Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()),
start = Offset(0f, lineY),
end = Offset(size.width, lineY + sin(animatedValue * i).toFloat() * 20f),
strokeWidth = 2f
)
}

repeat(70) { i ->
val circleX = (i % 8) * (size.width / 8) + (size.width / 16)
val circleY = (i / 8) * (size.height / 5) + (size.height / 10)
val circleRadius = 15f + sin(animatedValue + i).toFloat() * 10f

drawCircle(
color = Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()),
radius = circleRadius,
center = Offset(circleX, circleY),
alpha = 0.7f
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/

package benchmarks.heavyshader

import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameMillis
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlinx.coroutines.isActive

private const val ITEM_COUNT = 800

@Composable
fun HeavyShader() {
val listState = rememberLazyListState()
var scrollForward by remember { mutableStateOf(true) }

LaunchedEffect(Unit) {
while (isActive) {
withFrameMillis { }
val currentItem = listState.firstVisibleItemIndex
if (currentItem == 0) scrollForward = true
if (currentItem > ITEM_COUNT - 100) scrollForward = false
listState.scrollBy(if (scrollForward) 33f else -33f)
}
}

LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize().background(Color.Black)
) {
items(ITEM_COUNT) {
HeavyShaderItem()
}
}
}

@Composable
private fun HeavyShaderItem() {
val time by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = 100f,
animationSpec = infiniteRepeatable(
animation = tween(5000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)

Canvas(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.padding(8.dp)
) {
val layers = 60

repeat(layers) { layer ->
val offset = layer * 10f
val alpha = 1f - (layer.toFloat() / layers)

drawRect(
brush = Brush.radialGradient(
colors = listOf(
Color(0xFF00FF00).copy(alpha = alpha),
Color(0xFF0000FF).copy(alpha = alpha * 0.7f),
Color(0xFFFF0000).copy(alpha = alpha * 0.5f),
Color(0xFFFFFF00).copy(alpha = alpha * 0.3f),
Color(0xFF00FFFF).copy(alpha = alpha * 0.1f)
),
center = Offset(
size.width / 2 + cos(time + layer).toFloat() * 150f,
size.height / 2 + sin(time + layer).toFloat() * 150f
),
radius = size.minDimension / 2 + offset
),
blendMode = when (layer % 3) {
0 -> BlendMode.Screen
1 -> BlendMode.Overlay
else -> BlendMode.Multiply
}
)
}

repeat(60) { i ->
val angle = i * 7.2f + time * 10
val distance = size.minDimension / 2
val x = size.width / 2 + cos(angle * PI / 180.0).toFloat() * distance
val y = size.height / 2 + sin(angle * PI / 180.0).toFloat() * distance

drawCircle(
brush = Brush.radialGradient(
colors = listOf(
Color.White.copy(alpha = 0.8f),
Color.Transparent
),
center = Offset(x, y),
radius = 30f
),
radius = 30f,
center = Offset(x, y),
blendMode = BlendMode.Plus
)
}
}
}
Loading