diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index fce7f61f0..b6c929b22 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -131,7 +131,7 @@ jobs : name : Check with Gradle with : arguments : | - allTests test apiCheck checkVersionIsSnapshot lint lintKotlin jvmWorkflowNodeBenchmarkJar --continue + allTests test apiCheck checkVersionIsSnapshot lint lintKotlin --continue cache-read-only : false # Report as Github Pull Request Check. @@ -217,7 +217,7 @@ jobs : report_paths : '**/build/test-results/test/TEST-*.xml' jvm-conflate-stateChange-runtime-test : - name : Render On State Change Only Runtime JVM Tests + name : Render On State Change Only and Conflate Stale Runtime JVM Tests runs-on : ubuntu-latest timeout-minutes : 20 steps : @@ -342,7 +342,7 @@ jobs : if : ${{ always() }} uses : actions/upload-artifact@v3 with : - name : instrumentation-test-results-${{ matrix.api-level }} + name : renderpass-counting-results-${{ matrix.api-level }} path : ./**/build/reports/androidTests/connected/** instrumentation-tests : @@ -436,7 +436,7 @@ jobs : if : ${{ always() }} uses : actions/upload-artifact@v3 with : - name : instrumentation-test-results-${{ matrix.api-level }} + name : conflate-instrumentation-test-results-${{ matrix.api-level }} path : ./**/build/reports/androidTests/connected/** stateChange-runtime-instrumentation-tests : @@ -484,11 +484,11 @@ jobs : if : ${{ always() }} uses : actions/upload-artifact@v3 with : - name : instrumentation-test-results-${{ matrix.api-level }} + name : stateChange-instrumentation-test-results-${{ matrix.api-level }} path : ./**/build/reports/androidTests/connected/** conflate-stateChange-runtime-instrumentation-tests : - name : Render on State Change Only Instrumentation tests + name : Render on State Change Only and Conflate Stale Renderings Instrumentation tests runs-on : macos-latest timeout-minutes : 45 strategy : @@ -532,81 +532,9 @@ jobs : if : ${{ always() }} uses : actions/upload-artifact@v3 with : - name : instrumentation-test-results-${{ matrix.api-level }} + name : conflate-stateChange-instrumentation-test-results-${{ matrix.api-level }} path : ./**/build/reports/androidTests/connected/** - upload-to-mobiledev : - name : mobile.dev | Build & Upload - runs-on : ubuntu-latest - timeout-minutes : 20 - steps : - - uses : actions/checkout@v3 - - - name : set up JDK 11 - if : env.MOBILE_DEV_API_KEY != null - uses : actions/setup-java@v3 - with : - distribution : 'zulu' - java-version : 11 - env : - MOBILE_DEV_API_KEY : ${{ secrets.MOBILE_DEV_API_KEY }} - - - ## Build artifact for upload with cache - - uses : gradle/gradle-build-action@v2 - name : Build Performance Poetry APK - if : env.MOBILE_DEV_API_KEY != null - with : - arguments : | - benchmarks:performance-poetry:complex-poetry:assembleRelease - cache-read-only : false - env : - MOBILE_DEV_API_KEY : ${{ secrets.MOBILE_DEV_API_KEY }} - - ## Upload with POST - - name : Upload Poetry to mobile.dev - if : env.MOBILE_DEV_API_KEY != null - id : upload_apk - run : | - #!/usr/bin/env bash - set -e - set -x - RESPONSE_ID=$(curl -X POST \ - -H 'Content-Type: multipart/form-data' \ - -H "Authorization: Bearer $MOBILE_DEV_API_KEY" \ - --data-binary "@$APP_FILE" \ - https://api.mobile.dev/apk | jq -r .id) - echo "::set-output name=apk_id::$RESPONSE_ID" - env : - MOBILE_DEV_API_KEY : ${{ secrets.MOBILE_DEV_API_KEY }} - APP_FILE : benchmarks/performance-poetry/complex-poetry/build/outputs/apk/release/complex-poetry-release.apk - - ## Start analysis - - name : Start analysis on mobile.dev - if : env.MOBILE_DEV_API_KEY != null - run : | - #!/usr/bin/env bash - set -e - set -x - GIT_HASH=$(git log --pretty=format:'%h' -n 1) - BENCHMARK_NAME="$GIT_HASH" - REPO_BASE_NAME=$(basename "$REPO_NAME") - if [[ ! -z "$PULL_REQUEST_ID" ]]; then - PR_DATA=", \"repoOwner\":\"$REPO_OWNER\", \"repoName\":\"$REPO_BASE_NAME\", \"pullRequestId\":\"$PULL_REQUEST_ID\"" - fi - curl -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $MOBILE_DEV_API_KEY" \ - https://api.mobile.dev/analysis \ - --data "{\"benchmarkName\": \"$BENCHMARK_NAME\", \"apkId\": \"$APP_ID\", \"branch\": \"$BRANCH_NAME\"$PR_DATA}" - env : - APP_ID : ${{ steps.upload_apk.outputs.apk_id }} - MOBILE_DEV_API_KEY : ${{ secrets.MOBILE_DEV_API_KEY }} - REPO_OWNER : ${{ github.repository_owner }} - REPO_NAME : ${{ github.repository }} - BRANCH_NAME : ${{ github.head_ref || github.ref_name }} - PULL_REQUEST_ID : ${{ github.event.pull_request.number }} - all-green : if : always() runs-on : ubuntu-latest @@ -614,15 +542,18 @@ jobs : - artifacts-check - check - conflate-renderings-instrumentation-tests + - conflate-stateChange-runtime-instrumentation-tests - dependency-guard - dokka - instrumentation-tests - ios-tests - js-tests - jvm-conflate-runtime-test + - jvm-conflate-stateChange-runtime-test + - jvm-stateChange-runtime-test - performance-tests + - stateChange-runtime-instrumentation-tests - tutorials - - upload-to-mobiledev steps : - name : require that all other jobs have passed diff --git a/build.gradle.kts b/build.gradle.kts index aa928b9f1..be7d96655 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,6 @@ import java.net.URL buildscript { dependencies { classpath(libs.android.gradle.plugin) - classpath(libs.kotlinx.benchmark.gradle.plugin) classpath(libs.dokka.gradle.plugin) classpath(libs.kotlin.serialization.gradle.plugin) classpath(libs.kotlinx.binaryCompatibility.gradle.plugin) diff --git a/buildSrc/src/main/java/com/squareup/workflow1/buildsrc/KotlinCommonSettings.kt b/buildSrc/src/main/java/com/squareup/workflow1/buildsrc/KotlinCommonSettings.kt index ad75f6c06..3f347e17e 100644 --- a/buildSrc/src/main/java/com/squareup/workflow1/buildsrc/KotlinCommonSettings.kt +++ b/buildSrc/src/main/java/com/squareup/workflow1/buildsrc/KotlinCommonSettings.kt @@ -53,7 +53,6 @@ private fun Project.maybeEnableExplicitApi(compileTask: KotlinCompile) { path.startsWith(":samples") -> return path.startsWith(":benchmarks") -> return compileTask.name.contains("test", ignoreCase = true) -> return - compileTask.name.contains("jmh", ignoreCase = true) -> return else -> compileTask.kotlinOptions { // TODO this should be moved to `kotlin { explicitApi() }` once that's working for android // projects, see https://youtrack.jetbrains.com/issue/KT-37652. diff --git a/dependencies/classpath.txt b/dependencies/classpath.txt index 500230345..dc056beaa 100644 --- a/dependencies/classpath.txt +++ b/dependencies/classpath.txt @@ -74,7 +74,6 @@ com.squareup.retrofit2:converter-moshi:2.9.0 com.squareup.retrofit2:retrofit:2.9.0 com.squareup:javapoet:1.10.0 com.squareup:javawriter:2.5.0 -com.squareup:kotlinpoet:1.3.0 com.sun.activation:javax.activation:1.2.0 com.sun.istack:istack-commons-runtime:3.0.8 com.sun.xml.fastinfoset:FastInfoset:1.2.16 @@ -110,7 +109,6 @@ net.java.dev.jna:jna:5.6.0 net.sf.jopt-simple:jopt-simple:4.9 net.sf.kxml:kxml2:2.3.0 org.apache.commons:commons-compress:1.20 -org.apache.commons:commons-math3:3.2 org.apache.httpcomponents:httpclient:4.5.13 org.apache.httpcomponents:httpcore:4.4.13 org.apache.httpcomponents:httpmime:4.5.6 @@ -153,10 +151,8 @@ org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21 org.jetbrains.kotlin:kotlin-stdlib:1.6.21 org.jetbrains.kotlin:kotlin-tooling-core:1.7.20 org.jetbrains.kotlin:kotlin-util-io:1.7.20 -org.jetbrains.kotlin:kotlin-util-klib-metadata:1.6.0 org.jetbrains.kotlin:kotlin-util-klib:1.7.20 org.jetbrains.kotlinx:binary-compatibility-validator:0.11.1 -org.jetbrains.kotlinx:kotlinx-benchmark-plugin:0.4.2 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3 @@ -167,7 +163,6 @@ org.jetbrains:markdown:0.3.1 org.json:json:20180813 org.jsoup:jsoup:1.15.3 org.jvnet.staxex:stax-ex:1.8.1 -org.openjdk.jmh:jmh-core:1.21 org.ow2.asm:asm-analysis:9.2 org.ow2.asm:asm-commons:9.2 org.ow2.asm:asm-tree:9.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37d617ce4..f92fe0d1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,14 +47,12 @@ google-material = "1.4.0" groovy = "3.0.9" jUnit = "4.13.2" javaParser = "3.24.0" -jmh = "1.34" kotest = "5.1.0" kotlin = "1.7.20" kotlinx-binary-compatibility = "0.11.1" kotlinx-coroutines = "1.6.4" kotlinx-serialization-json = "1.3.2" -kotlinx-benchmark = "0.4.2" kotlinx-atomicfu = "0.17.2" kotlinter = "3.12.0" @@ -100,7 +98,6 @@ google-ksp = { id = "com.google.devtools.ksp", version.ref = "google-ksp" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinter" } kotlinx-apiBinaryCompatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "kotlinx-binary-compatibility" } -kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinx-benchmark" } mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-publish" } [libraries] @@ -211,8 +208,6 @@ kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test-common = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } -kotlinx-benchmark-gradle-plugin = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-plugin", version.ref = "kotlinx-benchmark" } -kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark" } kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinx-atomicfu" } ktlint-core = { module = "com.pinterest.ktlint:ktlint-core", version.ref = "ktlint" } diff --git a/workflow-runtime/build.gradle.kts b/workflow-runtime/build.gradle.kts index 59dd04aff..e184fa47c 100644 --- a/workflow-runtime/build.gradle.kts +++ b/workflow-runtime/build.gradle.kts @@ -1,11 +1,8 @@ import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64 -import kotlinx.benchmark.gradle.JvmBenchmarkTarget -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation plugins { `kotlin-multiplatform` published - id("org.jetbrains.kotlinx.benchmark") } kotlin { @@ -14,28 +11,7 @@ kotlin { iosWithSimulatorArm64() } if (targets == "kmp" || targets == "jvm") { - jvm { - compilations { - val main by getting - - create("workflowNode") { - val workflowNodeCompilation: KotlinJvmCompilation = this - kotlinOptions { - // Associating compilations allows us to access declarations with `internal` visibility. - // It's the new version of the "-Xfriend-paths=___" compiler argument. - // https://youtrack.jetbrains.com/issue/KTIJ-7662/IDE-support-internal-visibility-introduced-by-associated-compilations - workflowNodeCompilation.associateWith(main) - } - defaultSourceSet { - dependencies { - implementation(libs.kotlinx.benchmark.runtime) - - implementation(main.compileDependencyFiles + main.output.classesDirs) - } - } - } - } - } + jvm {} } if (targets == "kmp" || targets == "js") { js { browser() } @@ -49,12 +25,3 @@ dependencies { commonTestImplementation(libs.kotlinx.coroutines.test.common) commonTestImplementation(libs.kotlin.test.jdk) } - -benchmark { - targets { - register("jvmWorkflowNode") { - this as JvmBenchmarkTarget - jmhVersion = libs.versions.jmh.get() - } - } -} diff --git a/workflow-runtime/dependencies/jvmWorkflowNodeRuntimeClasspath.txt b/workflow-runtime/dependencies/jvmWorkflowNodeRuntimeClasspath.txt deleted file mode 100644 index 2b9587c07..000000000 --- a/workflow-runtime/dependencies/jvmWorkflowNodeRuntimeClasspath.txt +++ /dev/null @@ -1,16 +0,0 @@ -:workflow-core -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 -net.sf.jopt-simple:jopt-simple:5.0.4 -org.apache.commons:commons-math3:3.2 -org.jetbrains.kotlin:kotlin-bom:1.7.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20 -org.jetbrains.kotlin:kotlin-stdlib:1.7.20 -org.jetbrains.kotlinx:kotlinx-benchmark-runtime-jvm:0.4.2 -org.jetbrains.kotlinx:kotlinx-benchmark-runtime:0.4.2 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 -org.jetbrains:annotations:13.0 -org.openjdk.jmh:jmh-core:1.34 diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt index 5c8021266..8eb13cff0 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt @@ -158,46 +158,46 @@ internal class SubtreeManagerTest { assertEquals("initialState:props", composeState) } - @Test fun tick_children_handles_child_output() = runTest { + @Test fun onNextAction_for_children_handles_child_output() = runTest { val manager = subtreeManagerForTest() val workflow = TestWorkflow() val handler: StringHandler = { output -> action { setOutput("case output:$output") } } - // Initialize the child so tickAction has something to work with, and so that we can send + // Initialize the child so applyNextAction has something to work with, and so that we can send // an event to trigger an output. val (_, _, eventHandler) = manager.render(workflow, "props", key = "", handler = handler) manager.commitRenderedChildren() - val tickOutput = async { manager.tickAction() } - assertFalse(tickOutput.isCompleted) + val appliedActionResult = async { manager.applyNextAction() } + assertFalse(appliedActionResult.isCompleted) eventHandler("event!") - val update = tickOutput.await().output!!.value!! + val update = appliedActionResult.await().output!!.value!! val (_, result) = update.applyTo("props", "state") assertEquals("case output:workflow output:event!", result.output!!.value) assertFalse(result.stateChanged) } - @Test fun tick_children_handles_no_child_output() = runTest { + @Test fun onNextAction_for_children_handles_no_child_output() = runTest { val manager = subtreeManagerForTest() val workflow = TestWorkflow() val handler: StringHandler = { _ -> WorkflowAction.noAction() } - // Initialize the child so tickAction has something to work with, and so that we can send + // Initialize the child so applyNextAction has something to work with, and so that we can send // an event to trigger an output. val (_, _, eventHandler) = manager.render(workflow, "props", key = "", handler = handler) manager.commitRenderedChildren() - val tickOutput = async { manager.tickAction() } - assertFalse(tickOutput.isCompleted) + val appliedActionResult = async { manager.applyNextAction() } + assertFalse(appliedActionResult.isCompleted) eventHandler("event!") - val update = tickOutput.await().output!!.value!! + val update = appliedActionResult.await().output!!.value!! val (_, result) = update.applyTo("props", "state") assertEquals(null, result.output) @@ -211,11 +211,11 @@ internal class SubtreeManagerTest { manager.render(workflow, "props", key = "", handler = handler) .also { manager.commitRenderedChildren() } - // First render + tick pass – uninteresting. + // First render + apply action pass – uninteresting. render { action { setOutput("initial handler: $it") } } .let { rendering -> rendering.eventHandler("initial output") - val initialAction = manager.tickAction().output!!.value + val initialAction = manager.applyNextAction().output!!.value val (_, initialResult) = initialAction!!.applyTo("", "") assertEquals( expected = "initial handler: workflow output:initial output", @@ -224,7 +224,7 @@ internal class SubtreeManagerTest { assertFalse(initialResult.stateChanged) } - // Do a second render + tick, but with a different handler function. + // Do a second render + apply action, but with a different handler function. render { action { state = "New State" @@ -233,7 +233,7 @@ internal class SubtreeManagerTest { } .let { rendering -> rendering.eventHandler("second output") - val secondAction = manager.tickAction().output!!.value + val secondAction = manager.applyNextAction().output!!.value val (secondState, secondResult) = secondAction!!.applyTo("", "") assertEquals( expected = "second handler: workflow output:second output", @@ -300,7 +300,7 @@ internal class SubtreeManagerTest { } @Suppress("UNCHECKED_CAST") - private suspend fun SubtreeManager.tickAction() = + private suspend fun SubtreeManager.applyNextAction() = select { onNextChildAction(this) } as ActionApplied?> diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt index 7ef7f8300..8afede8ac 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt @@ -175,7 +175,11 @@ internal class WorkflowNodeTest { "", null, context, - emitAppliedActionToParent = { it.copy(output = WorkflowOutput("tick:${it.output!!.value}")) } + emitAppliedActionToParent = { + it.copy( + output = WorkflowOutput("applyActionOutput:${it.output!!.value}") + ) + } ) node.render(workflow, "")("event") @@ -185,7 +189,7 @@ internal class WorkflowNodeTest { node.onNextAction(this) } as ActionApplied } - assertEquals("tick:event", result.output!!.value) + assertEquals("applyActionOutput:event", result.output!!.value) } } @@ -213,7 +217,11 @@ internal class WorkflowNodeTest { "", null, context, - emitAppliedActionToParent = { it.copy(output = WorkflowOutput("tick:${it.output!!.value}")) } + emitAppliedActionToParent = { + it.copy( + output = WorkflowOutput("applyActionOutput:${it.output!!.value}") + ) + } ) val sink = node.render(workflow, "") @@ -228,7 +236,10 @@ internal class WorkflowNodeTest { } as ActionApplied } } - assertEquals(listOf("tick:event", "tick:event2"), result.map { it.output!!.value }) + assertEquals( + listOf("applyActionOutput:event", "applyActionOutput:event2"), + result.map { it.output!!.value } + ) } } diff --git a/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/RenderBenchmarkResults.txt b/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/RenderBenchmarkResults.txt deleted file mode 100644 index eb1e23004..000000000 --- a/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/RenderBenchmarkResults.txt +++ /dev/null @@ -1,27 +0,0 @@ - -SHA 85718c3d8e81458c8e8cb5c8c3c0f4edd8ad941a -February 15, 2022 -SPHardwareDataType -Model Name: MacBook Pro - Model Identifier: MacBookPro18,4 - Chip: Apple M1 Max - Total Number of Cores: 10 (8 performance and 2 efficiency) - Memory: 64 GB - System Firmware Version: 7429.81.3 - OS Loader Version: 7429.81.3 - -Benchmark (treeShape) Mode Cnt Score Error Units -WorkflowNodeBenchmark.renderPassAlternateLeaves DEEP avgt 3 36.191 ± 1.108 us/op -WorkflowNodeBenchmark.renderPassAlternateLeaves BUSHY avgt 3 1265.783 ± 51.456 us/op -WorkflowNodeBenchmark.renderPassAlternateLeaves SQUARE avgt 3 109.455 ± 1.902 us/op -WorkflowNodeBenchmark.renderPassAlternateOneLeaf DEEP avgt 3 43.423 ± 1.664 us/op -WorkflowNodeBenchmark.renderPassAlternateOneLeaf BUSHY avgt 3 2253.134 ± 542.016 us/op -WorkflowNodeBenchmark.renderPassAlternateOneLeaf SQUARE avgt 3 51.600 ± 0.735 us/op -WorkflowNodeBenchmark.renderPassAlternateWorkers DEEP avgt 3 37.337 ± 4.621 us/op -WorkflowNodeBenchmark.renderPassAlternateWorkers BUSHY avgt 3 1300.935 ± 98.725 us/op -WorkflowNodeBenchmark.renderPassAlternateWorkers SQUARE avgt 3 108.605 ± 1.095 us/op -WorkflowNodeBenchmark.renderPassAlwaysLeaves DEEP avgt 3 40.768 ± 4.702 us/op -WorkflowNodeBenchmark.renderPassAlwaysLeaves BUSHY avgt 3 2246.697 ± 96.847 us/op -WorkflowNodeBenchmark.renderPassAlwaysLeaves SQUARE avgt 3 59.566 ± 23.736 us/op - - diff --git a/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/WorkflowNodeBenchmark.kt b/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/WorkflowNodeBenchmark.kt deleted file mode 100644 index 345d7541c..000000000 --- a/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/WorkflowNodeBenchmark.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.squareup.workflow1 - -import com.squareup.workflow1.FractalWorkflow.Props -import com.squareup.workflow1.FractalWorkflow.Props.DO_NOT_RENDER_LEAVES -import com.squareup.workflow1.FractalWorkflow.Props.RENDER_LEAVES -import com.squareup.workflow1.FractalWorkflow.Props.RUN_WORKERS -import com.squareup.workflow1.FractalWorkflow.Props.SKIP_FIRST_LEAF -import com.squareup.workflow1.WorkflowAction.Companion.noAction -import com.squareup.workflow1.internal.WorkflowNode -import com.squareup.workflow1.internal.id -import kotlinx.coroutines.Dispatchers.Unconfined -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.suspendCancellableCoroutine -import org.openjdk.jmh.annotations.Benchmark -import org.openjdk.jmh.annotations.BenchmarkMode -import org.openjdk.jmh.annotations.Fork -import org.openjdk.jmh.annotations.Measurement -import org.openjdk.jmh.annotations.Mode.AverageTime -import org.openjdk.jmh.annotations.OutputTimeUnit -import org.openjdk.jmh.annotations.Param -import org.openjdk.jmh.annotations.Scope -import org.openjdk.jmh.annotations.Setup -import org.openjdk.jmh.annotations.State -import org.openjdk.jmh.annotations.Warmup -import java.util.concurrent.TimeUnit.MICROSECONDS -import java.util.concurrent.TimeUnit.SECONDS - -@Fork(value = 1) -@State(Scope.Thread) -@BenchmarkMode(AverageTime) -@Warmup(iterations = 3, time = 5, timeUnit = SECONDS) -@Measurement(iterations = 3, time = 5, timeUnit = SECONDS) -@OutputTimeUnit(MICROSECONDS) -internal open class WorkflowNodeBenchmark { - - /** - * Used to parameterize the benchmarks. - * - * @param childCount The number of children _each workflow in the tree_ should have. - * @param depth The depth of the workflow tree. - */ - enum class TreeShape( - val childCount: Int, - val depth: Int - ) { - // A tree with childCount = 4 and depth = 4 has 341 nodes, so keep the node count consistent - // across all benchmarks. - DEEP(childCount = 1, depth = 340), - BUSHY(childCount = 340, depth = 1), - SQUARE(childCount = 4, depth = 4); - - override fun toString(): String = "$name(childCount=$childCount, depth=$depth)" - } - - private val context = Unconfined - - @Param - @JvmField - var treeShape = TreeShape.SQUARE - - private lateinit var workflow: FractalWorkflow - private lateinit var node: WorkflowNode - - @Setup open fun setUp() { - println("Tree shape: $treeShape") - workflow = FractalWorkflow(treeShape.childCount, treeShape.depth) - node = workflow.createNode() - } - - /** - * Always renders leaves. After the first render pass, the tree structure never changes, so this - * measures no-op render passes. - */ - @Benchmark open fun renderPassAlwaysLeaves() { - node.render(workflow, RENDER_LEAVES) - // Second render to be consistent with other benchmarks that need two render passes. - node.render(workflow, RENDER_LEAVES) - } - - /** - * Alternates between rendering leaves and not rendering leaves. The tree structure is always - * changing, as well as the props all the way down the tree, so this prevents props caching - * and measures setup/teardown cost for [WorkflowNode]s. - */ - @Benchmark open fun renderPassAlternateLeaves() { - node.render(workflow, RENDER_LEAVES) - node.render(workflow, DO_NOT_RENDER_LEAVES) - } - - /** - * Alternates between having every leaf run a worker, and having no leaves run workers. - * Measures setup/teardown cost for [Worker]s. - */ - @Benchmark open fun renderPassAlternateWorkers() { - node.render(workflow, RENDER_LEAVES) - node.render(workflow, RUN_WORKERS) - } - - /** - * This benchmark is equivalent to [renderPassAlternateLeaves] for the [TreeShape.DEEP] case. - */ - @Benchmark open fun renderPassAlternateOneLeaf() { - node.render(workflow, RENDER_LEAVES) - node.render(workflow, SKIP_FIRST_LEAF) - } - - private fun FractalWorkflow.createNode() = WorkflowNode( - id = this.id(), - workflow = this, - initialProps = RENDER_LEAVES, - snapshot = null, - baseContext = context - ) -} - -/** - * A stateless, outputless, rendering-less workflow that will recursively form a tree. - * - * Subclasses [StatefulWorkflow], not `StatelessWorkflow`, to reduce the number of allocations - * because `StatelessWorkflow` allocates a `StatefulWorkflow` under the hood. - */ -private class FractalWorkflow( - private val childCount: Int, - private val depth: Int -) : StatefulWorkflow() { - - enum class Props( - val renderLeaves: Boolean, - val runWorkers: Boolean = false, - val skipFirstLeaf: Boolean = false - ) { - RENDER_LEAVES(renderLeaves = true), - DO_NOT_RENDER_LEAVES(renderLeaves = false), - RUN_WORKERS(renderLeaves = false, runWorkers = true), - SKIP_FIRST_LEAF(renderLeaves = true, skipFirstLeaf = true), - } - - private val childWorkflow = - if (depth > 0 && childCount > 0) { - FractalWorkflow(childCount, depth - 1) - } else { - null - } - - private val areChildrenLeaves = depth == 1 - - override fun initialState( - props: Props, - snapshot: Snapshot? - ) = Unit - - override fun render( - renderProps: Props, - renderState: Unit, - context: RenderContext - ) { - if (childWorkflow != null && (renderProps.renderLeaves || !areChildrenLeaves)) { - for (i in 0 until childCount) { - if (renderProps.skipFirstLeaf) { - // Don't render the first child if it's a leaf, otherwise render children using props that - // will fractally result in the first leaf being skipped. - if (!areChildrenLeaves || i > 0) { - val childProps = if (i == 0) renderProps else RENDER_LEAVES - context.renderChild(childWorkflow, childProps, key = i.toString()) - } - } else { - context.renderChild(childWorkflow, renderProps, key = i.toString()) - } - } - } - - if (renderProps.runWorkers && depth == 0) { - context.runningWorker(NeverWorker) { noAction() } - } - } - - override fun snapshotState(state: Unit): Snapshot = throw NotImplementedError() -} - -private object NeverWorker : Worker { - override fun run(): Flow = flow { - suspendCancellableCoroutine { - // Never emit, suspend indefinitely. - } - } -}