Skip to content

Commit 15ecc74

Browse files
WIP added some actual benchmarks
1 parent ff4ee48 commit 15ecc74

File tree

5 files changed

+158
-8
lines changed

5 files changed

+158
-8
lines changed

benchmarks/compose-workflow/build.gradle.kts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
plugins {
2-
id("com.android.library")
2+
id("com.android.application")
33
id("kotlin-android")
44
id("android-defaults")
5+
alias(libs.plugins.compose.compiler)
56
}
67

78
// Note: We are not including our defaults from .buildscript as we do not need the base Workflow
@@ -15,8 +16,7 @@ android {
1516

1617
buildTypes {
1718
debug {
18-
// TODO why isn't this available?
19-
// isDebuggable = false
19+
isDebuggable = false
2020
}
2121
}
2222

@@ -30,4 +30,6 @@ dependencies {
3030
androidTestImplementation(libs.androidx.test.espresso.core)
3131
androidTestImplementation(libs.androidx.test.junit)
3232
androidTestImplementation(libs.androidx.test.uiautomator)
33+
androidTestImplementation(libs.kotlin.test.jdk)
34+
androidTestImplementation(libs.kotlinx.coroutines.test)
3335
}
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,140 @@
1+
@file:OptIn(WorkflowExperimentalApi::class)
2+
13
package com.squareup.benchmark.composeworkflow.benchmark
24

35
import androidx.benchmark.junit4.BenchmarkRule
46
import androidx.benchmark.junit4.measureRepeated
7+
import androidx.compose.runtime.Composable
58
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import com.squareup.workflow1.RuntimeConfigOptions
10+
import com.squareup.workflow1.Workflow
11+
import com.squareup.workflow1.WorkflowExperimentalApi
12+
import com.squareup.workflow1.WorkflowExperimentalRuntime
13+
import com.squareup.workflow1.compose.ComposeWorkflow
14+
import com.squareup.workflow1.compose.composable
15+
import com.squareup.workflow1.compose.renderChild
16+
import com.squareup.workflow1.renderChild
17+
import com.squareup.workflow1.renderWorkflowIn
18+
import com.squareup.workflow1.stateless
19+
import kotlinx.coroutines.Job
20+
import kotlinx.coroutines.flow.MutableStateFlow
21+
import kotlinx.coroutines.job
22+
import kotlinx.coroutines.plus
23+
import kotlinx.coroutines.test.runTest
624
import org.junit.Rule
725
import org.junit.Test
826
import org.junit.runner.RunWith
27+
import kotlin.test.assertEquals
928

29+
@OptIn(WorkflowExperimentalRuntime::class)
1030
@RunWith(AndroidJUnit4::class)
1131
class ComposeWorkflowMicroBenchmark {
1232

1333
@get:Rule val benchmarkRule = BenchmarkRule()
1434

15-
@Test fun foo() {
35+
@Test fun tradRoot_tradChildren_initialRender() {
36+
benchmarkSimpleTreeInitialRender(
37+
maxChildCount = 100,
38+
composeRoot = false,
39+
composeChildren = false
40+
)
41+
}
42+
43+
@Test fun tradRoot_composeChildren_initialRender() {
44+
benchmarkSimpleTreeInitialRender(
45+
maxChildCount = 100,
46+
composeRoot = false,
47+
composeChildren = true
48+
)
49+
}
50+
51+
@Test fun composeRoot_tradChildren_initialRender() {
52+
benchmarkSimpleTreeInitialRender(
53+
maxChildCount = 100,
54+
composeRoot = true,
55+
composeChildren = false
56+
)
57+
}
58+
59+
@Test fun composeRoot_composeChildren_initialRender() {
60+
benchmarkSimpleTreeInitialRender(
61+
maxChildCount = 100,
62+
composeRoot = true,
63+
composeChildren = true
64+
)
65+
}
66+
67+
private fun benchmarkSimpleTreeInitialRender(
68+
maxChildCount: Int,
69+
composeRoot: Boolean,
70+
composeChildren: Boolean
71+
) = runTest {
72+
val props =
73+
MutableStateFlow(RootWorkflowProps(childCount = 0, composeChildren = composeChildren))
74+
val workflowJob = Job(parent = coroutineContext.job)
75+
val renderings = renderWorkflowIn(
76+
workflow = if (composeRoot) {
77+
composeSimpleRoot
78+
} else {
79+
traditionalSimpleRoot
80+
},
81+
props = props,
82+
scope = this + workflowJob,
83+
runtimeConfig = RuntimeConfigOptions.ALL,
84+
onOutput = {}
85+
)
86+
1687
benchmarkRule.measureRepeated {
17-
Thread.sleep(100)
88+
runWithTimingDisabled {
89+
props.value = RootWorkflowProps(childCount = 0, composeChildren = composeChildren)
90+
testScheduler.runCurrent()
91+
assertEquals(0, renderings.value.rendering)
92+
}
93+
94+
props.value = RootWorkflowProps(childCount = maxChildCount, composeChildren = composeChildren)
95+
testScheduler.runCurrent()
96+
assertEquals(maxChildCount, renderings.value.rendering)
1897
}
98+
99+
workflowJob.cancel()
100+
}
101+
}
102+
103+
private data class RootWorkflowProps(
104+
val childCount: Int,
105+
val composeChildren: Boolean
106+
)
107+
108+
private val traditionalSimpleRoot = Workflow.stateless<RootWorkflowProps, Nothing, Int> { props ->
109+
var rendering = 0
110+
repeat(props.childCount) { child ->
111+
rendering += renderChild(
112+
key = child.toString(),
113+
child = if (props.composeChildren) {
114+
composeSimpleLeaf
115+
} else {
116+
traditionalSimpleLeaf
117+
}
118+
)
19119
}
120+
rendering
20121
}
122+
123+
private val composeSimpleRoot = Workflow.composable<RootWorkflowProps, Nothing, Int> { props, _ ->
124+
var rendering = 0
125+
repeat(props.childCount) {
126+
rendering += renderChild(
127+
workflow = if (props.composeChildren) {
128+
composeSimpleLeaf
129+
} else {
130+
traditionalSimpleLeaf
131+
},
132+
props = Unit,
133+
onOutput = null
134+
)
135+
}
136+
rendering
137+
}
138+
139+
private val traditionalSimpleLeaf = Workflow.stateless<Unit, Nothing, Int> { 1 }
140+
private val composeSimpleLeaf = Workflow.composable<Unit, Nothing, Int> { _, _ -> 1 }
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
5+
<uses-permission
6+
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
7+
tools:ignore="ScopedStorage"/>
38
</manifest>

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkflow.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,17 @@ private fun <PropsT, OutputT, RenderingT> ChildWorkflowRecomposeIsolator(
304304
) {
305305
renderingHolder.value = renderChild(workflow, props, onOutput)
306306
}
307+
308+
@WorkflowExperimentalApi
309+
public inline fun <PropsT, OutputT, RenderingT> Workflow.Companion.composable(
310+
crossinline produceRendering: @Composable (
311+
props: PropsT,
312+
emitOutput: (OutputT) -> Unit
313+
) -> RenderingT
314+
): Workflow<PropsT, OutputT, RenderingT> = object : ComposeWorkflow<PropsT, OutputT, RenderingT>() {
315+
@Composable
316+
override fun produceRendering(
317+
props: PropsT,
318+
emitOutput: (OutputT) -> Unit
319+
): RenderingT = produceRendering(props, emitOutput)
320+
}

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/compose/runtime/SynchronizedMolecule.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,15 @@ private class RealSynchronizedMolecule(
186186
} else {
187187
// Hard-code unchanging frame time since there's no actual frame time code shouldn't rely on
188188
// this value.
189-
frameRequest.execute(0L)
189+
val frameResult = frameRequest.execute(0L)
190+
191+
// If the composition threw an exception, re-throw it ourselves now instead of waiting for the
192+
// scope to get it, since lastResult may have not been initialized in this case and we'd throw
193+
// below and get supppressed.
194+
frameResult.exceptionOrNull()?.let {
195+
throw RuntimeException("ComposeWorkflow composition threw an exception", it)
196+
}
197+
190198
// If the composition threw an exception, we want it to cancel the coroutine scope before
191199
// getOrThrow below does so.
192200
dispatcher.advanceUntilIdle()
@@ -284,9 +292,10 @@ private class RealSynchronizedMolecule(
284292
private val onFrame: (frameTimeNanos: Long) -> R,
285293
private val continuation: CancellableContinuation<R>
286294
) {
287-
fun execute(frameTimeNanos: Long) {
295+
fun execute(frameTimeNanos: Long): Result<R> {
288296
val frameResult = runCatching { onFrame(frameTimeNanos) }
289297
continuation.resumeWith(frameResult)
298+
return frameResult
290299
}
291300
}
292301
}

0 commit comments

Comments
 (0)