Skip to content

Commit 8991cda

Browse files
authored
Merge pull request #724 from square/ray/catchup-1.7.0
Merge main into ray/ui-update
2 parents ec37f4d + 5e72bed commit 8991cda

File tree

48 files changed

+1094
-383
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1094
-383
lines changed

.buildscript/binary-validation.gradle

+2
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+

RELEASING.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
1. Make sure you're on the `main` branch (or fix branch, e.g. `v0.1-fixes`).
77

88
1. Confirm that the kotlin build is green before committing any changes
9+
(Note we exclude benchmarks, but you can check those too!)
910
```bash
10-
./gradlew build connectedCheck
11+
./gradlew build && ./gradlew connectedCheck -x :benchmarks:dungeon-benchmark:connectedCheck
1112
```
1213

1314
1. Update your tags.

benchmarks/README.md

+24-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
# benchmark
1+
# benchmarks
22

33
This module contains benchmarks. Used to measure and help improve Workflow performance. These tests
44
should be run on physical devices.
55

66
## dungeon-benchmark
77

8-
Currently this is the only benchmark included here. It is for the /samples/dungeon/app. This
9-
includes a macrobenchmark [test](benchmarks/dungeon-benchmark/src/main/java/com/squareup/sample/dungeon/benchmark/WorkflowBaselineProfiles.kt)
10-
that exercises this sample app (with UiAutomator) to collect a [baseline profile](https://developer.android.com/studio/profile/baselineprofiles).
11-
After running this the `profile.txt` file can be taken off of the device and used directly in the
12-
/src/main directory of the application. This will include the profile into the APK for guided
13-
optimization at install time.
8+
This is for the /samples/dungeon/app. This includes a macrobenchmark
9+
[test](dungeon-benchmark/src/main/java/com/squareup/sample/dungeon/benchmark/WorkflowBaselineProfiles.kt)
10+
that exercises this sample app (with UiAutomator) to collect
11+
a [baseline profile](https://developer.android.com/studio/profile/baselineprofiles). After running
12+
this the `profile.txt` file can be taken off of the device and used directly in the /src/main
13+
directory of the application. This will include the profile into the APK for guided optimization at
14+
install time.
1415

1516
Better yet, the profile can be split up and added into each Workflow module's src directory so that
1617
it will be included with all APKs built using Workflow (including 3rd party). To do this a java
@@ -28,8 +29,23 @@ This will create an output file separated by module and then also by package as
2829
profile for each module can be added into its /src/main directory as `baseline-prof.txt`. Then on a
2930
release build this will be included with the resulting APK/binary.
3031

31-
The other [test](benchmarks/dungeon-benchmark/src/main/java/com/squareup/sample/dungeon/benchmark/WorkflowBaselineBenchmark.kt)
32+
The other [test](dungeon-benchmark/src/main/java/com/squareup/sample/dungeon/benchmark/WorkflowBaselineBenchmark.kt)
3233
is used to verify the results of including the baseline profiles on the startup time. This runs the
3334
same scenario with and without forcing the use of the profiles. To force the use of profiles, the
3435
`libs.androidx.profileinstaller` dependency is included into the app under profile (dungeon in this
3536
case) for side-loading the profiles.
37+
38+
## performance-poetry
39+
40+
Module of code for performance testing related to poetry applications.
41+
42+
### complex-poetry
43+
44+
This application is a modification of the samples/containers/app-poetry app which uses also the
45+
common components in samples/containers/common and samples/containers/poetry. It modifies this
46+
application to pass the Workflow
47+
a [SimulatedPerfConfig](performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/poetry/SimulatedPerfConfig.kt).
48+
49+
In this case we specify that the app should be more 'complex' which adds delays into each of the
50+
selections that are run by Worker's which then trigger a loading state that is handled by the
51+
[MaybeLoadingGatekeeperWorkflow](performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/poetry/MaybeLoadingGatekeeperWorkflow.kt).

benchmarks/performancepoetry/build.gradle.kts renamed to benchmarks/performance-poetry/complex-poetry/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ apply(from = rootProject.file(".buildscript/android-ui-tests.gradle"))
88

99
android {
1010
defaultConfig {
11-
applicationId = "com.squareup.benchmarks.performance.poetry"
11+
applicationId = "com.squareup.benchmarks.performance.complex.poetry"
1212
}
1313
}
1414

benchmarks/performancepoetry/src/main/AndroidManifest.xml renamed to benchmarks/performance-poetry/complex-poetry/src/main/AndroidManifest.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:tools="http://schemas.android.com/tools"
4-
package="com.squareup.benchmarks.performance.poetry">
4+
package="com.squareup.benchmarks.performance.complex.poetry">
55

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

0 commit comments

Comments
 (0)