diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt index 45b3e934ff..d69c3adefe 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt @@ -4,6 +4,7 @@ import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowse import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow.State.ComplexCall import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow.State.Initializing import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow.State.NoSelection +import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow.State.Recurse import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow.State.Selected import com.squareup.benchmarks.performance.complex.poetry.instrumentation.ActionHandlingTracingInterceptor import com.squareup.benchmarks.performance.complex.poetry.instrumentation.SimulatedPerfConfig @@ -50,9 +51,11 @@ class PerformancePoemsBrowserWorkflow( private val isLoading: MutableStateFlow, ) : PoemsBrowserWorkflow, - StatefulWorkflow, State, Unit, OverviewDetailScreen>() { + StatefulWorkflow, List>, State, Unit, OverviewDetailScreen>() { sealed class State { + object Recurse : State() + // N.B. This state is a smell. We include it to be able to mimic smells // we encounter in real life. Best practice would be to fold it // into [NoSelection] at the very least. @@ -66,36 +69,57 @@ class PerformancePoemsBrowserWorkflow( } override fun initialState( - props: List, + props: Pair, List>, snapshot: Snapshot? ): State { - return if (simulatedPerfConfig.useInitializingState) Initializing else NoSelection + return if (props.first.first > 0 && props.first.second == simulatedPerfConfig.recursionGraph.second) { + Recurse + } else if (simulatedPerfConfig.useInitializingState) { + Initializing + } else { + NoSelection + } } @OptIn(WorkflowUiExperimentalApi::class) override fun render( - renderProps: List, + renderProps: Pair, List>, renderState: State, context: RenderContext ): OverviewDetailScreen { - if (simulatedPerfConfig.simultaneousActions > 0) { - repeat(simulatedPerfConfig.simultaneousActions) { index -> - context.runningWorker( - worker = isLoading.asTraceableWorker("SimultaneousSubscribeBrowser-$index"), - key = "Browser-$index" + when (renderState) { + is Recurse -> { + val recursiveChild = PerformancePoemsBrowserWorkflow( + simulatedPerfConfig, + poemWorkflow, + isLoading, + ) + repeat(renderProps.first.second) { breadth -> + val nextProps = renderProps.copy( + first = renderProps.first.first - 1 to breadth + ) + // ignore the siblings. + context.renderChild( + child = recursiveChild, + props = nextProps, + key = "${nextProps.first},${nextProps.second}", + ) { + noAction() + } + } + val nextProps = renderProps.copy( + first = renderProps.first.first - 1 to renderProps.first.second + ) + return context.renderChild( + child = recursiveChild, + props = nextProps, + key = "${nextProps.first},${nextProps.second}", ) { - noAction() + action { + setOutput(it) + } } } - } - val poemListProps = Props( - poems = renderProps, - eventHandlerTag = ActionHandlingTracingInterceptor::keyForTrace - ) - val poemListRendering = context.renderChild(PoemListWorkflow, poemListProps) { selected -> - choosePoem(selected) - } - when (renderState) { // Again, then entire `Initializing` state is a smell, which is most obvious from the // use of `Worker.from { Unit }`. A Worker doing no work and only shuttling the state // along is usually the sign you have an extraneous state that can be collapsed! @@ -110,58 +134,86 @@ class PerformancePoemsBrowserWorkflow( } return OverviewDetailScreen(overviewRendering = BackStackScreen(BlankScreen)) } - is NoSelection -> { - return OverviewDetailScreen( - overviewRendering = BackStackScreen( - poemListRendering.copy(selection = NO_POEM_SELECTED) - ) + + is ComplexCall, is NoSelection, is Selected -> { + if (simulatedPerfConfig.simultaneousActions > 0) { + repeat(simulatedPerfConfig.simultaneousActions) { index -> + context.runningWorker( + worker = isLoading.asTraceableWorker("SimultaneousSubscribeBrowser-$index"), + key = "Browser-$index" + ) { + noAction() + } + } + } + val poemListProps = Props( + poems = renderProps.second, + eventHandlerTag = ActionHandlingTracingInterceptor::keyForTrace ) - } - is ComplexCall -> { - context.runningWorker( - TraceableWorker.from("ComplexCallBrowser(${renderState.payload})") { - isLoading.value = true - delay(simulatedPerfConfig.complexityDelay) - // No Output for Worker is necessary because the selected index - // is already in the state. + val poemListRendering = context.renderChild(PoemListWorkflow, poemListProps) { selected -> + choosePoem(selected) + } + when (renderState) { + is NoSelection -> { + return OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = NO_POEM_SELECTED) + ) + ) } - ) { - action { - isLoading.value = false - (state as? ComplexCall)?.let { currentState -> - state = if (currentState.payload != NO_POEM_SELECTED) { - Selected(currentState.payload) - } else { - NoSelection + + is ComplexCall -> { + context.runningWorker( + TraceableWorker.from("ComplexCallBrowser(${renderState.payload})") { + isLoading.value = true + delay(simulatedPerfConfig.complexityDelay) + // No Output for Worker is necessary because the selected index + // is already in the state. + } + ) { + action { + isLoading.value = false + (state as? ComplexCall)?.let { currentState -> + state = if (currentState.payload != NO_POEM_SELECTED) { + Selected(currentState.payload) + } else { + NoSelection + } + } } } + var poems = OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = renderState.payload) + ) + ) + if (renderState.payload != NO_POEM_SELECTED) { + val poem: OverviewDetailScreen = context.renderChild( + poemWorkflow, + renderProps.second[renderState.payload] + ) { clearSelection } + poems += poem + } + return poems + } + + is Selected -> { + val poems = OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = renderState.poemIndex) + ) + ) + val poem: OverviewDetailScreen = context.renderChild( + poemWorkflow, + renderProps.second[renderState.poemIndex] + ) { clearSelection } + return poems + poem + } + + else -> { + throw IllegalStateException("State can't change while rendering.") } } - var poems = OverviewDetailScreen( - overviewRendering = BackStackScreen( - poemListRendering.copy(selection = renderState.payload) - ) - ) - if (renderState.payload != NO_POEM_SELECTED) { - val poem: OverviewDetailScreen = context.renderChild( - poemWorkflow, - renderProps[renderState.payload] - ) { clearSelection } - poems += poem - } - return poems - } - is Selected -> { - val poems = OverviewDetailScreen( - overviewRendering = BackStackScreen( - poemListRendering.copy(selection = renderState.poemIndex) - ) - ) - val poem: OverviewDetailScreen = context.renderChild( - poemWorkflow, - renderProps[renderState.poemIndex] - ) { clearSelection } - return poems + poem } } } diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryActivity.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryActivity.kt index 12f67635dc..b616f3401a 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryActivity.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryActivity.kt @@ -59,11 +59,15 @@ class PerformancePoetryActivity : AppCompatActivity() { setupMainLooperTracing() + val recursiveDepth = intent.getIntExtra(EXTRA_PERF_CONFIG_RECURSION_DEPTH, 0) + val recursiveBreadth = intent.getIntExtra(EXTRA_PERF_CONFIG_RECURSION_BREADTH, 0) + // Default is just to have the basic 'delay' complexity. val simulatedPerfConfig = SimulatedPerfConfig( isComplex = true, complexityDelay = intent.getLongExtra(EXTRA_PERF_CONFIG_DELAY, 200L), useInitializingState = intent.getBooleanExtra(EXTRA_SCENARIO_CONFIG_INITIALIZING, false), + recursionGraph = recursiveDepth to recursiveBreadth, repeatOnNext = intent.getIntExtra(EXTRA_PERF_CONFIG_REPEAT, 0), simultaneousActions = intent.getIntExtra(EXTRA_PERF_CONFIG_SIMULTANEOUS, 0), traceFrameLatency = intent.getBooleanExtra(EXTRA_TRACE_FRAME_LATENCY, false), @@ -242,6 +246,10 @@ class PerformancePoetryActivity : AppCompatActivity() { const val EXTRA_PERF_CONFIG_REPEAT = "complex.poetry.performance.config.repeat.amount" const val EXTRA_PERF_CONFIG_DELAY = "complex.poetry.performance.config.delay.length" const val EXTRA_PERF_CONFIG_SIMULTANEOUS = "complex.poetry.performance.config.simultaneous" + const val EXTRA_PERF_CONFIG_RECURSION_DEPTH = + "complex.poetry.performance.config.recursion.depth" + const val EXTRA_PERF_CONFIG_RECURSION_BREADTH = + "complex.poetry.performance.config.recursion.breadth" const val SELECT_ON_TIMEOUT_LOG_NAME = "kotlinx.coroutines.selects.SelectBuilderImpl\$onTimeout\$\$inlined\$Runnable" @@ -257,7 +265,7 @@ class PerformancePoetryActivity : AppCompatActivity() { class PoetryModel( savedState: SavedStateHandle, - workflow: MaybeLoadingGatekeeperWorkflow>, + workflow: MaybeLoadingGatekeeperWorkflow, List>>, interceptor: WorkflowInterceptor?, runtimeConfig: RuntimeConfig ) : ViewModel() { @@ -274,7 +282,7 @@ class PoetryModel( class Factory( owner: SavedStateRegistryOwner, - private val workflow: MaybeLoadingGatekeeperWorkflow>, + private val workflow: MaybeLoadingGatekeeperWorkflow, List>>, private val workflowInterceptor: WorkflowInterceptor?, private val runtimeConfig: RuntimeConfig ) : AbstractSavedStateViewModelFactory(owner, null) { diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryComponent.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryComponent.kt index 766a99fc89..d8b71a5d1f 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryComponent.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryComponent.kt @@ -34,7 +34,7 @@ class PerformancePoetryComponent( private val loadingGatekeeperForPoems = MaybeLoadingGatekeeperWorkflow( childWithLoading = poemsBrowserWorkflow, - childProps = Poem.allPoems, + childProps = Pair(simulatedPerfConfig.recursionGraph, Poem.allPoems), browserIsLoading.combine(poemIsLoading) { one, two -> one || two } ) diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/SimulatedPerfConfig.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/SimulatedPerfConfig.kt index 968712a04c..55fbf51e75 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/SimulatedPerfConfig.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/SimulatedPerfConfig.kt @@ -16,6 +16,7 @@ data class SimulatedPerfConfig( val isComplex: Boolean, val complexityDelay: Long, val useInitializingState: Boolean, + val recursionGraph: Pair = 0 to 0, val repeatOnNext: Int = 0, val simultaneousActions: Int = 0, val traceRenderingPasses: Boolean = false, @@ -27,6 +28,7 @@ data class SimulatedPerfConfig( isComplex = false, complexityDelay = 0, useInitializingState = false, + recursionGraph = 0 to 0, repeatOnNext = 0, simultaneousActions = 0, traceRenderingPasses = false, diff --git a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt index ac81d8025c..cfd27146d4 100644 --- a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt +++ b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt @@ -52,7 +52,7 @@ class PoetryModel(savedState: SavedStateHandle) : ViewModel() { renderWorkflowIn( workflow = RealPoemsBrowserWorkflow(RealPoemWorkflow()), scope = viewModelScope, - prop = Poem.allPoems, + prop = 0 to 0 to Poem.allPoems, savedStateHandle = savedState, runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() ) diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemsBrowserWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemsBrowserWorkflow.kt index 39e4205508..a85df05174 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemsBrowserWorkflow.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemsBrowserWorkflow.kt @@ -10,4 +10,4 @@ import com.squareup.workflow1.Workflow * (Defining this as an interface allows us to use other implementations * in other contexts -- check out our :benchmarks module!) */ -interface PoemsBrowserWorkflow : Workflow, Unit, OverviewDetailScreen> +interface PoemsBrowserWorkflow : Workflow, List>, Unit, OverviewDetailScreen> diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/RealPoemsBrowserWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/RealPoemsBrowserWorkflow.kt index 09b579868d..3e062db0be 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/RealPoemsBrowserWorkflow.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/RealPoemsBrowserWorkflow.kt @@ -23,10 +23,10 @@ typealias SelectedPoem = Int class RealPoemsBrowserWorkflow( private val poemWorkflow: PoemWorkflow ) : PoemsBrowserWorkflow, - StatefulWorkflow, SelectedPoem, Unit, OverviewDetailScreen>() { + StatefulWorkflow, List>, SelectedPoem, Unit, OverviewDetailScreen>() { override fun initialState( - props: List, + props: Pair, List>, snapshot: Snapshot? ): SelectedPoem { return snapshot?.bytes?.parse { source -> @@ -36,12 +36,12 @@ class RealPoemsBrowserWorkflow( @OptIn(WorkflowUiExperimentalApi::class) override fun render( - renderProps: List, + renderProps: Pair, List>, renderState: SelectedPoem, context: RenderContext ): OverviewDetailScreen { val poems: OverviewDetailScreen = - context.renderChild(PoemListWorkflow, Props(poems = renderProps)) { selected -> + context.renderChild(PoemListWorkflow, Props(poems = renderProps.second)) { selected -> choosePoem( selected ) @@ -53,7 +53,7 @@ class RealPoemsBrowserWorkflow( poems } else { val poem: OverviewDetailScreen = - context.renderChild(poemWorkflow, renderProps[renderState]) { clearSelection } + context.renderChild(poemWorkflow, renderProps.second[renderState]) { clearSelection } poems + poem } }