Skip to content

Commit 6e511e8

Browse files
Merge pull request #704 from square/sedwards/699-benchmark-sample
699: Add simulated performance config to Poetry
2 parents 8677f21 + 5763982 commit 6e511e8

File tree

22 files changed

+888
-186
lines changed

22 files changed

+888
-186
lines changed

.buildscript/binary-validation.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ apiValidation {
1313
// Only leaf project name is valid configuration, and every project must be individually ignored.
1414
// See https://github.com/Kotlin/binary-compatibility-validator/issues/3
1515
ignoredProjects += project('samples').subprojects.collect { it.name }
16+
ignoredProjects += project('benchmarks').subprojects.collect { it.name }
1617
}
18+

benchmarks/performancepoetry/api/performancepoetry.api

Lines changed: 0 additions & 16 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.squareup.benchmarks.performance.poetry
2+
3+
import com.squareup.benchmarks.performance.poetry.views.LoaderSpinner
4+
import com.squareup.benchmarks.performance.poetry.views.MayBeLoadingScreen
5+
import com.squareup.sample.container.overviewdetail.OverviewDetailScreen
6+
import com.squareup.workflow1.Snapshot
7+
import com.squareup.workflow1.StatefulWorkflow
8+
import com.squareup.workflow1.Workflow
9+
import com.squareup.workflow1.action
10+
import com.squareup.workflow1.asWorker
11+
import com.squareup.workflow1.runningWorker
12+
import kotlinx.coroutines.flow.Flow
13+
14+
typealias IsLoading = Boolean
15+
16+
class MaybeLoadingGatekeeperWorkflow<T : Any>(
17+
private val childWithLoading: Workflow<T, Any, OverviewDetailScreen>,
18+
private val childProps: T,
19+
private val isLoading: Flow<Boolean>
20+
) : StatefulWorkflow<Unit, IsLoading, Unit, MayBeLoadingScreen>() {
21+
override fun initialState(
22+
props: Unit,
23+
snapshot: Snapshot?
24+
): IsLoading = false
25+
26+
override fun render(
27+
renderProps: Unit,
28+
renderState: IsLoading,
29+
context: RenderContext
30+
): MayBeLoadingScreen {
31+
context.runningWorker(isLoading.asWorker()) {
32+
action {
33+
state = it
34+
}
35+
}
36+
return MayBeLoadingScreen(
37+
baseScreen = context.renderChild(childWithLoading, childProps) {
38+
action { setOutput(Unit) }
39+
},
40+
loaders = if (renderState) listOf(LoaderSpinner) else emptyList()
41+
)
42+
}
43+
44+
override fun snapshotState(state: IsLoading): Snapshot? = null
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package com.squareup.benchmarks.performance.poetry
2+
3+
import com.squareup.benchmarks.performance.poetry.PerformancePoemWorkflow.Action.ClearSelection
4+
import com.squareup.benchmarks.performance.poetry.PerformancePoemWorkflow.Action.HandleStanzaListOutput
5+
import com.squareup.benchmarks.performance.poetry.PerformancePoemWorkflow.Action.SelectNext
6+
import com.squareup.benchmarks.performance.poetry.PerformancePoemWorkflow.Action.SelectPrevious
7+
import com.squareup.benchmarks.performance.poetry.PerformancePoemWorkflow.State
8+
import com.squareup.benchmarks.performance.poetry.PerformancePoemWorkflow.State.ComplexCall
9+
import com.squareup.benchmarks.performance.poetry.PerformancePoemWorkflow.State.Initializing
10+
import com.squareup.benchmarks.performance.poetry.PerformancePoemWorkflow.State.Selected
11+
import com.squareup.benchmarks.performance.poetry.views.BlankScreen
12+
import com.squareup.sample.container.overviewdetail.OverviewDetailScreen
13+
import com.squareup.sample.poetry.PoemWorkflow
14+
import com.squareup.sample.poetry.PoemWorkflow.ClosePoem
15+
import com.squareup.sample.poetry.StanzaListWorkflow
16+
import com.squareup.sample.poetry.StanzaListWorkflow.NO_SELECTED_STANZA
17+
import com.squareup.sample.poetry.StanzaRendering
18+
import com.squareup.sample.poetry.StanzaWorkflow
19+
import com.squareup.sample.poetry.StanzaWorkflow.Output.CloseStanzas
20+
import com.squareup.sample.poetry.StanzaWorkflow.Output.ShowNextStanza
21+
import com.squareup.sample.poetry.StanzaWorkflow.Output.ShowPreviousStanza
22+
import com.squareup.sample.poetry.StanzaWorkflow.Props
23+
import com.squareup.sample.poetry.model.Poem
24+
import com.squareup.workflow1.Snapshot
25+
import com.squareup.workflow1.StatefulWorkflow
26+
import com.squareup.workflow1.Worker
27+
import com.squareup.workflow1.WorkflowAction
28+
import com.squareup.workflow1.WorkflowAction.Companion.noAction
29+
import com.squareup.workflow1.action
30+
import com.squareup.workflow1.runningWorker
31+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
32+
import com.squareup.workflow1.ui.backstack.BackStackScreen
33+
import com.squareup.workflow1.ui.backstack.toBackStackScreen
34+
import kotlinx.coroutines.delay
35+
import kotlinx.coroutines.flow.MutableStateFlow
36+
37+
/**
38+
* Version of [PoemWorkflow] that takes in a [SimulatedPerfConfig] to control the performance
39+
* behavior of the Workflow.
40+
*
41+
* @param [simulatedPerfConfig] specifies whether to make the Workflow more 'complex' by
42+
* introducing some asynchronous delays. See [SimulatedPerfConfig] for more details.
43+
*
44+
* @param [isLoading] will be set to true while this workflow is 'loading'. This is so that another
45+
* component, such as a [MaybeLoadingGatekeeperWorkflow] can overlay the screen with a visual
46+
* loading state. N.B. that whether or not this is loading could be included in the
47+
* RenderingT if the interface [PoemWorkflow] had been left more flexible.
48+
*
49+
* ** Also note that raw mutable state sharing like this will almost always be a smell. It would
50+
* be better to inject an interface of a 'Loading' service that could trigger this and likely
51+
* break ties/conflicts with a token in the start/stop requests. We leave that complexity out
52+
* here. **
53+
*/
54+
class PerformancePoemWorkflow(
55+
private val simulatedPerfConfig: SimulatedPerfConfig = SimulatedPerfConfig.NO_SIMULATED_PERF,
56+
private val isLoading: MutableStateFlow<Boolean>
57+
) : PoemWorkflow, StatefulWorkflow<Poem, State, ClosePoem, OverviewDetailScreen>() {
58+
59+
sealed class State {
60+
// N.B. This state is a smell. We include it to be able to mimic smells
61+
// we encounter in real life. Best practice would be to fold it
62+
// into [Selected(NO_SELECTED_STANZA)] at the very least.
63+
object Initializing : State()
64+
data class ComplexCall(
65+
val payload: Int
66+
) : State()
67+
68+
data class Selected(val stanzaIndex: Int) : State()
69+
}
70+
71+
override fun initialState(
72+
props: Poem,
73+
snapshot: Snapshot?
74+
): State {
75+
return if (simulatedPerfConfig.useInitializingState) Initializing else Selected(
76+
NO_SELECTED_STANZA
77+
)
78+
}
79+
80+
@OptIn(WorkflowUiExperimentalApi::class)
81+
override fun render(
82+
renderProps: Poem,
83+
renderState: State,
84+
context: RenderContext
85+
): OverviewDetailScreen {
86+
return when (renderState) {
87+
Initializing -> {
88+
// Again, then entire `Initializing` state is a smell, which is most obvious from the
89+
// use of `Worker.from { Unit }`. A Worker doing no work and only shuttling the state
90+
// along is usually the sign you have an extraneous state that can be collapsed!
91+
// Don't try this at home.
92+
context.runningWorker(Worker.from { Unit }, "initializing") {
93+
isLoading.value = true
94+
action {
95+
isLoading.value = false
96+
state = Selected(NO_SELECTED_STANZA)
97+
}
98+
}
99+
OverviewDetailScreen(overviewRendering = BackStackScreen(BlankScreen))
100+
}
101+
else -> {
102+
val (stanzaIndex, currentStateIsLoading) = when (renderState) {
103+
is ComplexCall -> Pair(renderState.payload, true)
104+
is Selected -> Pair(renderState.stanzaIndex, false)
105+
Initializing -> throw IllegalStateException("No longer initializing.")
106+
}
107+
108+
if (currentStateIsLoading) {
109+
context.runningWorker(
110+
Worker.from {
111+
isLoading.value = true
112+
delay(simulatedPerfConfig.complexityDelay)
113+
// No Output for Worker is necessary because the selected index
114+
// is already in the state.
115+
}
116+
) {
117+
action {
118+
isLoading.value = false
119+
(state as? ComplexCall)?.let { currentState ->
120+
state = Selected(currentState.payload)
121+
}
122+
}
123+
}
124+
}
125+
126+
val previousStanzas: List<StanzaRendering> =
127+
if (stanzaIndex == NO_SELECTED_STANZA) emptyList()
128+
else renderProps.stanzas.subList(0, stanzaIndex)
129+
.mapIndexed { index, _ ->
130+
context.renderChild(StanzaWorkflow, Props(renderProps, index), "$index") {
131+
noAction()
132+
}
133+
}
134+
135+
val visibleStanza =
136+
if (stanzaIndex == NO_SELECTED_STANZA) {
137+
null
138+
} else {
139+
context.renderChild(
140+
StanzaWorkflow, Props(renderProps, stanzaIndex), "$stanzaIndex"
141+
) {
142+
when (it) {
143+
CloseStanzas -> ClearSelection(simulatedPerfConfig)
144+
ShowPreviousStanza -> SelectPrevious(simulatedPerfConfig)
145+
ShowNextStanza -> SelectNext(simulatedPerfConfig)
146+
}
147+
}
148+
}
149+
150+
val stackedStanzas = visibleStanza?.let {
151+
(previousStanzas + visibleStanza).toBackStackScreen<Any>()
152+
}
153+
154+
val stanzaListOverview =
155+
context.renderChild(
156+
StanzaListWorkflow,
157+
renderProps
158+
) { selected ->
159+
HandleStanzaListOutput(simulatedPerfConfig, selected)
160+
}
161+
.copy(selection = stanzaIndex)
162+
163+
stackedStanzas
164+
?.let {
165+
OverviewDetailScreen(
166+
overviewRendering = BackStackScreen(stanzaListOverview),
167+
detailRendering = it
168+
)
169+
} ?: OverviewDetailScreen(
170+
overviewRendering = BackStackScreen(stanzaListOverview),
171+
selectDefault = {
172+
context.actionSink.send(HandleStanzaListOutput(simulatedPerfConfig, 0))
173+
}
174+
)
175+
}
176+
}
177+
}
178+
179+
override fun snapshotState(state: State): Snapshot? = null
180+
181+
internal sealed class Action : WorkflowAction<Poem, State, ClosePoem>() {
182+
abstract val simulatedPerfConfig: SimulatedPerfConfig
183+
184+
class ClearSelection(override val simulatedPerfConfig: SimulatedPerfConfig) : Action()
185+
class SelectPrevious(override val simulatedPerfConfig: SimulatedPerfConfig) : Action()
186+
class SelectNext(override val simulatedPerfConfig: SimulatedPerfConfig) : Action()
187+
class HandleStanzaListOutput(
188+
override val simulatedPerfConfig: SimulatedPerfConfig,
189+
val selection: Int
190+
) : Action()
191+
192+
class ExitPoem(override val simulatedPerfConfig: SimulatedPerfConfig) : Action()
193+
194+
override fun Updater.apply() {
195+
val currentIndex: Int = when (val solidState = state) {
196+
is ComplexCall -> solidState.payload
197+
Initializing -> NO_SELECTED_STANZA
198+
is Selected -> solidState.stanzaIndex
199+
}
200+
when (this@Action) {
201+
is ClearSelection ->
202+
state =
203+
if (simulatedPerfConfig.isComplex) {
204+
ComplexCall(NO_SELECTED_STANZA)
205+
} else {
206+
Selected(
207+
NO_SELECTED_STANZA
208+
)
209+
}
210+
is SelectPrevious ->
211+
state =
212+
if (simulatedPerfConfig.isComplex) {
213+
ComplexCall(currentIndex - 1)
214+
} else {
215+
Selected(
216+
currentIndex - 1
217+
)
218+
}
219+
is SelectNext ->
220+
state =
221+
if (simulatedPerfConfig.isComplex) {
222+
ComplexCall(currentIndex + 1)
223+
} else {
224+
Selected(
225+
currentIndex + 1
226+
)
227+
}
228+
is HandleStanzaListOutput -> {
229+
if (selection == NO_SELECTED_STANZA) setOutput(ClosePoem)
230+
state = if (simulatedPerfConfig.isComplex) {
231+
ComplexCall(selection)
232+
} else {
233+
Selected(selection)
234+
}
235+
}
236+
is ExitPoem -> setOutput(ClosePoem)
237+
}
238+
}
239+
}
240+
}

0 commit comments

Comments
 (0)