Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/local.properties
.idea/
.DS_Store
/build
**/build/
/captures
.externalNativeBuild
.cxx
Expand Down
8 changes: 8 additions & 0 deletions apps/flipcash/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ android {
)
}
}
create("benchmark") {
initWith(getByName("debug"))
isDebuggable = false
signingConfig = signingConfigs.getByName("contributors")
matchingFallbacks += listOf("debug")
}
}

compileOptions {
Expand Down Expand Up @@ -272,6 +278,8 @@ dependencies {
implementation(libs.timber)
implementation(libs.bugsnag)

implementation(libs.androidx.profileinstaller)

testImplementation(libs.junit)
testImplementation(libs.kotlin.test.junit)
}
43 changes: 43 additions & 0 deletions apps/flipcash/app/src/main/baseline-prof.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Baseline profile for Flipcash — covers cold start and Compose rendering.
# Generated profiles from :apps:flipcash:benchmark will replace this file.

# Application + Activity startup
HSPLcom/flipcash/app/FlipcashApp;->onCreate()V
HSPLcom/flipcash/app/MainActivity;->onCreate(Landroid/os/Bundle;)V

# Hilt dependency injection (startup-critical)
HSPLdagger/hilt/android/internal/**;->**(**)**
HSPLcom/flipcash/app/FlipcashApp_GeneratedInjector;->**(**)**
HSPLcom/flipcash/app/MainActivity_GeneratedInjector;->**(**)**

# Jetpack Compose runtime (critical for first frame)
HSPLandroidx/compose/runtime/**;->**(**)**
HSPLandroidx/compose/ui/**;->**(**)**
HSPLandroidx/compose/foundation/**;->**(**)**
HSPLandroidx/compose/material/**;->**(**)**
HSPLandroidx/compose/animation/**;->**(**)**

# Compose layout and drawing
HSPLandroidx/compose/ui/platform/AndroidComposeView;->**(**)**
HSPLandroidx/compose/ui/node/**;->**(**)**

# Navigation
HSPLandroidx/navigation/**;->**(**)**

# Lifecycle
HSPLandroidx/lifecycle/**;->**(**)**
HSPLandroidx/activity/**;->**(**)**

# Kotlinx coroutines (startup flows)
HSPLkotlinx/coroutines/**;->**(**)**

# Kotlinx serialization (used in session/config deserialization)
HSPLkotlinx/serialization/**;->**(**)**

# Coil image loading
HSPLcoil3/**;->**(**)**

# App theme and UI setup
HSPLcom/getcode/theme/**;->**(**)**
HSPLcom/getcode/ui/core/**;->**(**)**
HSPLcom/getcode/ui/components/**;->**(**)**
52 changes: 52 additions & 0 deletions apps/flipcash/benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
plugins {
id("com.android.test")
}

val contributorsSigningConfig = ContributorsSignatory(rootProject)

android {
namespace = "com.flipcash.benchmark"
compileSdk = libs.versions.android.compileSdk.get().toInt()

defaultConfig {
minSdk = 29
targetSdk = libs.versions.android.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR"
}

signingConfigs {
create("contributors") {
storeFile = contributorsSigningConfig.keystore
storePassword = contributorsSigningConfig.keystorePassword
keyAlias = contributorsSigningConfig.keyAlias
keyPassword = contributorsSigningConfig.keyPassword
}
}

buildTypes {
create("benchmark") {
isDebuggable = true
signingConfig = signingConfigs.getByName("contributors")
matchingFallbacks += listOf("debug")
}
}

compileOptions {
sourceCompatibility(libs.versions.android.java.get())
targetCompatibility(libs.versions.android.java.get())
}

kotlin {
jvmToolchain(libs.versions.android.java.get().toInt())
}

targetProjectPath = ":apps:flipcash:app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}

dependencies {
implementation(libs.androidx.benchmark.macro.junit4)
implementation(libs.androidx.test.runner)
implementation(libs.androidx.junit)
}
3 changes: 3 additions & 0 deletions apps/flipcash/benchmark/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package com.flipcash.benchmark

import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
* Generates a baseline profile by exercising critical user journeys.
*
* Pass a seed phrase via instrumentation args to enable authenticated flows:
* ```
* -Pandroid.testInstrumentationRunnerArguments.SEED_PHRASE="word1 word2 ..."
* ```
*
* Without a seed phrase, only the pre-auth startup path is profiled.
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {

@get:Rule
val rule = BaselineProfileRule()

private val seedPhrase: String?
get() = InstrumentationRegistry.getArguments().getString("SEED_PHRASE")

@Test
fun generateBaselineProfile() {
rule.collect(
packageName = PACKAGE_NAME,
) {
// Cold start — app init, Hilt DI, Compose runtime bootstrap
pressHome()
startActivityAndWait()
device.waitForIdle()

// After login, app data persists across iterations so subsequent
// iterations launch directly into the scanner (already authenticated).
val onScanner = device.hasObject(By.res("scanner_view"))

if (onScanner) {
// Already logged in from a prior iteration
scannerJourney()
walletJourney()
giveJourney()
menuJourney()
} else {
val seed = seedPhrase
if (seed.isNullOrBlank()) {
preAuthJourney()
} else {
login(seed)
scannerJourney()
walletJourney()
giveJourney()
menuJourney()
}
}
}
}

private fun MacrobenchmarkScope.preAuthJourney() {
// Navigate to seed input — exercises nav transitions, text input composables
device.wait(Until.findObject(By.res("login_button")), TIMEOUT)?.click()
device.waitForIdle()

// Back to onboarding — exercises pop transition
device.pressBack()
device.waitForIdle()
}

private fun MacrobenchmarkScope.login(seed: String) {
// Tap "Log in"
device.wait(Until.findObject(By.res("login_button")), TIMEOUT)?.click()

// Wait for seed input field and tap to focus
device.wait(Until.findObject(By.res("seed_input_field")), TIMEOUT)?.click()
device.waitForIdle()

// Type seed phrase via IME — UiObject2.text doesn't work with Compose TextField
val escaped = seed.replace(" ", "%s")
device.executeShellCommand("input text $escaped")
device.waitForIdle()

// Wait for seed validation to enable the button, then confirm
device.wait(Until.hasObject(By.res("login_confirm_button").enabled(true)), TIMEOUT)
device.findObject(By.res("login_confirm_button"))?.click()

// Wait for scanner screen
device.wait(Until.findObject(By.res("scanner_view")), LOGIN_TIMEOUT)
device.waitForIdle()
}

private fun MacrobenchmarkScope.scannerJourney() {
// Scanner is the home screen — let it fully render
device.wait(Until.findObject(By.res("scanner_view")), TIMEOUT)
device.waitForIdle()
}

private fun MacrobenchmarkScope.walletJourney() {
// Open wallet sheet
device.wait(Until.findObject(By.text("Wallet")), TIMEOUT)?.click()
device.wait(Until.findObject(By.res("wallet_screen")), TIMEOUT)
device.waitForIdle()

// Open token info
device.wait(Until.findObject(By.text("Float")), TIMEOUT)?.click()
device.wait(Until.findObject(By.res("token_info_screen")), TIMEOUT)
device.waitForIdle()

// Back to wallet
device.pressBack()
device.waitForIdle()

// Close sheet — swipe down to return to scanner
dismissSheet()
}

private fun MacrobenchmarkScope.giveJourney() {
// Open give/cash screen
device.wait(Until.findObject(By.text("Give")), TIMEOUT)?.click()
device.wait(Until.findObject(By.res("cash_screen")), TIMEOUT)
device.waitForIdle()

// Close sheet
dismissSheet()
}

private fun MacrobenchmarkScope.menuJourney() {
// Open menu
device.wait(Until.findObject(By.res("menu_button")), TIMEOUT)?.click()
device.wait(Until.findObject(By.res("menu_screen")), TIMEOUT)
device.waitForIdle()

// Close sheet
dismissSheet()
}

private fun MacrobenchmarkScope.dismissSheet() {
// Swipe from mid-screen downward to dismiss bottom sheet.
// Avoid starting near the top to prevent pulling the notification panel.
device.swipe(
device.displayWidth / 2,
device.displayHeight / 3,
device.displayWidth / 2,
device.displayHeight * 3 / 4,
10,
)
device.wait(Until.findObject(By.res("scanner_view")), TIMEOUT)
device.waitForIdle()
}

companion object {
private const val PACKAGE_NAME = "com.flipcash.app.android"
private const val TIMEOUT = 5_000L
private const val LOGIN_TIMEOUT = 15_000L
}
}

private typealias MacrobenchmarkScope = androidx.benchmark.macro.MacrobenchmarkScope
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.flipcash.benchmark

import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingLegacyMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmark {

@get:Rule
val rule = MacrobenchmarkRule()

@Test
fun startupNoCompilation() = benchmark(CompilationMode.None())

@Test
fun startupPartialCompilation() = benchmark(
CompilationMode.Partial(
baselineProfileMode = BaselineProfileMode.Disable,
warmupIterations = 3,
)
)

@Test
fun startupBaselineProfile() = benchmark(
CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require)
)

@Test
fun startupFullCompilation() = benchmark(CompilationMode.Full())

private fun benchmark(compilationMode: CompilationMode) {
rule.measureRepeated(
packageName = "com.flipcash.app.android",
metrics = listOf(StartupTimingLegacyMetric()),
compilationMode = compilationMode,
iterations = 5,
startupMode = StartupMode.COLD,
) {
pressHome()
startActivityAndWait()
}
}
}
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ lib-phone-number-port = "9.0.31"
lib-phone-number-google = "9.0.31"
zxing = "3.5.4"

androidx-benchmark-macro = "1.4.0"
androidx-profileinstaller = "1.4.1"
androidx-test-runner = "1.7.0"
junit = "4.13.2"
androidx-junit = "1.3.0"
Expand Down Expand Up @@ -270,6 +272,10 @@ rinku = { module = "dev.theolm:rinku", version.ref = "rinku" }
rinku-compose = { module = "dev.theolm:rinku-compose-ext", version.ref = "rinku" }
event-bus = { module = "io.github.hoc081098:channel-event-bus", version.ref = "event-bus" }

# Baseline Profile / Benchmark
androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark-macro" }
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "androidx-profileinstaller" }

# Testing
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
Expand Down
Loading
Loading