Skip to content

Commit d2ec11c

Browse files
committed
feat(perf): add user journey flows to baseline profile generator
Exercise scanner, wallet, give, and menu paths when a SEED_PHRASE instrumentation arg is provided. Falls back to pre-auth only when no seed is available. Mirrors the existing Maestro subflows.
1 parent 92adf7e commit d2ec11c

2 files changed

Lines changed: 188 additions & 4 deletions

File tree

apps/flipcash/benchmark/src/main/kotlin/com/flipcash/benchmark/BaselineProfileGenerator.kt

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,147 @@ package com.flipcash.benchmark
33
import androidx.benchmark.macro.junit4.BaselineProfileRule
44
import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.filters.LargeTest
6+
import androidx.test.platform.app.InstrumentationRegistry
7+
import androidx.test.uiautomator.By
8+
import androidx.test.uiautomator.Until
69
import org.junit.Rule
710
import org.junit.Test
811
import org.junit.runner.RunWith
912

13+
/**
14+
* Generates a baseline profile by exercising critical user journeys.
15+
*
16+
* Pass a seed phrase via instrumentation args to enable authenticated flows:
17+
* ```
18+
* -Pandroid.testInstrumentationRunnerArguments.SEED_PHRASE="word1 word2 ..."
19+
* ```
20+
*
21+
* Without a seed phrase, only the pre-auth startup path is profiled.
22+
*/
1023
@RunWith(AndroidJUnit4::class)
1124
@LargeTest
1225
class BaselineProfileGenerator {
1326

1427
@get:Rule
1528
val rule = BaselineProfileRule()
1629

30+
private val seedPhrase: String?
31+
get() = InstrumentationRegistry.getArguments().getString("SEED_PHRASE")
32+
1733
@Test
1834
fun generateBaselineProfile() {
1935
rule.collect(
20-
packageName = "com.flipcash.app.android",
36+
packageName = PACKAGE_NAME,
2137
) {
22-
// Cold start the app — this covers the critical startup path
38+
// 1. Cold start app init, Hilt DI, Compose runtime bootstrap
2339
pressHome()
2440
startActivityAndWait()
41+
device.waitForIdle()
2542

26-
// TODO: Add user journey interactions here to cover hot paths
27-
// e.g., navigating to balance, scanning, etc.
43+
val seed = seedPhrase
44+
if (seed.isNullOrBlank()) {
45+
// Pre-auth only: exercise login screen navigation
46+
preAuthJourney()
47+
} else {
48+
// Full journey: login then exercise main app screens
49+
login(seed)
50+
scannerJourney()
51+
walletJourney()
52+
giveJourney()
53+
menuJourney()
54+
}
2855
}
2956
}
57+
58+
private fun MacrobenchmarkScope.preAuthJourney() {
59+
// Navigate to seed input — exercises nav transitions, text input composables
60+
device.wait(Until.findObject(By.res("login_button")), TIMEOUT)?.click()
61+
device.waitForIdle()
62+
63+
// Back to onboarding — exercises pop transition
64+
device.pressBack()
65+
device.waitForIdle()
66+
}
67+
68+
private fun MacrobenchmarkScope.login(seed: String) {
69+
// Tap "Log in"
70+
device.wait(Until.findObject(By.res("login_button")), TIMEOUT)?.click()
71+
device.wait(Until.findObject(By.res("seed_input_field")), TIMEOUT)?.click()
72+
73+
// Enter seed phrase
74+
device.wait(Until.findObject(By.res("seed_input_field")), TIMEOUT)?.text = seed
75+
device.waitForIdle()
76+
77+
// Confirm login
78+
device.wait(Until.findObject(By.res("login_confirm_button")), TIMEOUT)?.click()
79+
80+
// Wait for scanner screen
81+
device.wait(Until.findObject(By.res("scanner_view")), LOGIN_TIMEOUT)
82+
device.waitForIdle()
83+
}
84+
85+
private fun MacrobenchmarkScope.scannerJourney() {
86+
// Scanner is the home screen — let it fully render
87+
device.wait(Until.findObject(By.res("scanner_view")), TIMEOUT)
88+
device.waitForIdle()
89+
}
90+
91+
private fun MacrobenchmarkScope.walletJourney() {
92+
// Open wallet sheet
93+
device.wait(Until.findObject(By.text("Wallet")), TIMEOUT)?.click()
94+
device.wait(Until.findObject(By.res("wallet_screen")), TIMEOUT)
95+
device.waitForIdle()
96+
97+
// Open token info
98+
device.wait(Until.findObject(By.text("Float")), TIMEOUT)?.click()
99+
device.wait(Until.findObject(By.res("token_info_screen")), TIMEOUT)
100+
device.waitForIdle()
101+
102+
// Back to wallet
103+
device.pressBack()
104+
device.waitForIdle()
105+
106+
// Close sheet — swipe down to return to scanner
107+
dismissSheet()
108+
}
109+
110+
private fun MacrobenchmarkScope.giveJourney() {
111+
// Open give/cash screen
112+
device.wait(Until.findObject(By.text("Give")), TIMEOUT)?.click()
113+
device.wait(Until.findObject(By.res("cash_screen")), TIMEOUT)
114+
device.waitForIdle()
115+
116+
// Close sheet
117+
dismissSheet()
118+
}
119+
120+
private fun MacrobenchmarkScope.menuJourney() {
121+
// Open menu
122+
device.wait(Until.findObject(By.res("menu_button")), TIMEOUT)?.click()
123+
device.wait(Until.findObject(By.res("menu_screen")), TIMEOUT)
124+
device.waitForIdle()
125+
126+
// Close sheet
127+
dismissSheet()
128+
}
129+
130+
private fun MacrobenchmarkScope.dismissSheet() {
131+
device.swipe(
132+
device.displayWidth / 2,
133+
device.displayHeight / 4,
134+
device.displayWidth / 2,
135+
device.displayHeight,
136+
10,
137+
)
138+
device.wait(Until.findObject(By.res("scanner_view")), TIMEOUT)
139+
device.waitForIdle()
140+
}
141+
142+
companion object {
143+
private const val PACKAGE_NAME = "com.flipcash.app.android"
144+
private const val TIMEOUT = 5_000L
145+
private const val LOGIN_TIMEOUT = 15_000L
146+
}
30147
}
148+
149+
private typealias MacrobenchmarkScope = androidx.benchmark.macro.MacrobenchmarkScope

scripts/benchmark.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
echo "Usage: scripts/benchmark.sh [-t TestClass] [seed phrase words...]"
6+
echo ""
7+
echo " -t CLASS Run a specific test class (e.g. StartupBenchmark, BaselineProfileGenerator)"
8+
echo " seed ... Seed phrase for authenticated user journeys"
9+
echo ""
10+
echo "Examples:"
11+
echo " scripts/benchmark.sh # all benchmarks, no auth"
12+
echo " scripts/benchmark.sh word1 word2 word3 ... # all benchmarks, with auth"
13+
echo " scripts/benchmark.sh -t StartupBenchmark # startup only, no auth"
14+
echo " scripts/benchmark.sh -t BaselineProfileGenerator word1 word2 ... # profile gen with auth"
15+
exit 1
16+
}
17+
18+
TEST_CLASS=""
19+
20+
while getopts "t:h" opt; do
21+
case $opt in
22+
t) TEST_CLASS="$OPTARG" ;;
23+
h) usage ;;
24+
*) usage ;;
25+
esac
26+
done
27+
shift $((OPTIND - 1))
28+
29+
SEED_PHRASE="$*"
30+
31+
GRADLE_ARGS=(
32+
:apps:flipcash:benchmark:connectedBenchmarkAndroidTest
33+
--no-configuration-cache
34+
-x lint
35+
)
36+
37+
if [[ -n "$TEST_CLASS" ]]; then
38+
GRADLE_ARGS+=("-Pandroid.testInstrumentationRunnerArguments.class=com.flipcash.benchmark.$TEST_CLASS")
39+
fi
40+
41+
if [[ -n "$SEED_PHRASE" ]]; then
42+
GRADLE_ARGS+=("-Pandroid.testInstrumentationRunnerArguments.SEED_PHRASE=$SEED_PHRASE")
43+
fi
44+
45+
./gradlew "${GRADLE_ARGS[@]}"
46+
47+
# Print results if available
48+
RESULTS_DIR="apps/flipcash/benchmark/build/outputs/connected_android_test_additional_output"
49+
BENCHMARK_JSON=$(find "$RESULTS_DIR" -name "*benchmarkData.json" 2>/dev/null | head -1)
50+
51+
if [[ -n "$BENCHMARK_JSON" ]]; then
52+
echo ""
53+
echo "=== Benchmark Results ==="
54+
python3 -c "
55+
import json, sys
56+
data = json.load(open('$BENCHMARK_JSON'))
57+
for b in data.get('benchmarks', []):
58+
m = b['metrics']
59+
startup = m.get('startupMs', {})
60+
fd = m.get('fullyDrawnMs', {})
61+
print(f\"{b['name']}:\")
62+
if startup: print(f\" TTID: min={startup['minimum']:.0f}ms median={startup['median']:.0f}ms max={startup['maximum']:.0f}ms\")
63+
if fd: print(f\" TTFD: min={fd['minimum']:.0f}ms median={fd['median']:.0f}ms max={fd['maximum']:.0f}ms\")
64+
" 2>/dev/null || cat "$BENCHMARK_JSON"
65+
fi

0 commit comments

Comments
 (0)