From 97c0628798101e4951cce1dc14232c9d64dfaf0e Mon Sep 17 00:00:00 2001 From: Honza Rychnovsky Date: Tue, 13 Dec 2022 14:25:09 +0100 Subject: [PATCH] [#789] Benchmarking Demo-app * [#789] Add Benchmark module to Demo-app * Code cleanup * Opti-in experimental coroutines api in tests * Add Testing documentation * Documentation update + ktlint * Check screen on and keyguard unlocked in each test * Introduce UiAutomator extensions * Enhance BenchmarkTrace events definition * Remove unnecessary mutex * Change blocks range * Increase sync blockchain timeout - To always fit in the Balances screen timeout for the selected blocks range * Remove unnecessary fun suspend modifiers * Macrobenchmark lib bump to 1.2.0-alpha8 * Remove duplicate label attr in app/manifest * File and link benchmark on CI task * Add proguard keep rules * Documentation update --- .github/pull_request_template.md | 2 +- ...ark_test_connectedBenchmarkAndroidTest.xml | 56 ++++++ .../zcash-sdk.android-conventions.gradle.kts | 29 ++- darkside-test-lib/build.gradle.kts | 8 + .../test/DarksideTestPrerequisites.kt | 3 +- demo-app-benchmark-test/build.gradle.kts | 48 +++++ demo-app-benchmark-test/proguard-consumer.txt | 4 + .../src/main/AndroidManifest.xml | 4 + .../sdk/demoapp/benchmark/StartupBenchmark.kt | 123 ++++++++++++ .../benchmark/SyncBlockchainBenchmark.kt | 101 ++++++++++ .../z/ecc/android/sdk/demoapp/test/Global.kt | 12 ++ .../sdk/demoapp/test/UiAutomatorExt.kt | 15 ++ .../sdk/demoapp/test/UiTestPrerequisites.kt | 48 +++++ demo-app/build.gradle.kts | 10 + demo-app/src/main/AndroidManifest.xml | 25 ++- .../android/sdk/demoapp/BaseDemoFragment.kt | 17 ++ .../z/ecc/android/sdk/demoapp/MainActivity.kt | 7 + .../android/sdk/demoapp/SharedViewModel.kt | 14 ++ .../demos/getaddress/GetAddressFragment.kt | 13 ++ .../demos/getbalance/GetBalanceFragment.kt | 58 +++++- .../ListTransactionsFragment.kt | 13 ++ .../sdk/demoapp/demos/send/SendFragment.kt | 13 ++ .../sdk/demoapp/util/BenchmarkTrace.kt | 182 ++++++++++++++++++ demo-app/src/main/res/menu/main.xml | 29 ++- demo-app/src/main/res/values/strings.xml | 1 + docs/{tests => }/Build.md | 0 docs/{tests => }/Public APIs.md | 4 +- docs/{tests => testing}/Darkside.md | 0 docs/testing/Testing.md | 109 +++++++++++ .../manual_testing}/Download sapling files.md | 0 .../Move database files to no_backup.md | 0 .../Move sapling files to no_backup.md | 0 gradle.properties | 9 +- sdk-lib/build.gradle.kts | 5 + .../android/sdk/db/DatabaseCoordinatorTest.kt | 7 + .../cash/z/ecc/android/sdk/ext/FileExtTest.kt | 3 + .../sdk/internal/SaplingParamToolBasicTest.kt | 5 + .../SaplingParamToolIntegrationTest.kt | 6 + .../sdk/model/UnifiedSpendingKeyTest.kt | 4 + .../android/sdk/tool/DerivationToolTest.kt | 2 + .../android/sdk/annotation/OpenForTesting.kt | 11 ++ .../cash/z/ecc/android/sdk/SdkSynchronizer.kt | 2 - .../sdk/block/CompactBlockProcessor.kt | 27 ++- .../z/ecc/android/sdk/ext/BenchmarkingExt.kt | 9 + .../android/sdk/fixture/BlockRangeFixture.kt | 24 +++ .../internal/ext/android/ComputableFlow.kt | 2 + .../service/LightWalletGrpcService.kt | 16 +- settings.gradle.kts | 16 +- 48 files changed, 1056 insertions(+), 40 deletions(-) create mode 100644 .idea/runConfigurations/demo_app_benchmark_test_connectedBenchmarkAndroidTest.xml create mode 100644 demo-app-benchmark-test/build.gradle.kts create mode 100644 demo-app-benchmark-test/proguard-consumer.txt create mode 100644 demo-app-benchmark-test/src/main/AndroidManifest.xml create mode 100644 demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/benchmark/StartupBenchmark.kt create mode 100644 demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/benchmark/SyncBlockchainBenchmark.kt create mode 100644 demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/Global.kt create mode 100644 demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/UiAutomatorExt.kt create mode 100644 demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/UiTestPrerequisites.kt create mode 100644 demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/BenchmarkTrace.kt rename docs/{tests => }/Build.md (100%) rename docs/{tests => }/Public APIs.md (91%) rename docs/{tests => testing}/Darkside.md (100%) create mode 100644 docs/testing/Testing.md rename docs/{tests => testing/manual_testing}/Download sapling files.md (100%) rename docs/{tests => testing/manual_testing}/Move database files to no_backup.md (100%) rename docs/{tests => testing/manual_testing}/Move sapling files to no_backup.md (100%) create mode 100644 sdk-lib/src/benchmark/java/cash/z/ecc/android/sdk/annotation/OpenForTesting.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BenchmarkingExt.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/BlockRangeFixture.kt diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4785d87c..98c07c31 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ This code review checklist is intended to serve as a starting point for the auth - [ ] Self-review: Did you review your own code in GitHub's web interface? _Code often looks different when reviewing the diff in a browser, making it easier to spot potential bugs._ - [ ] Automated tests: Did you add appropriate automated tests for any code changes? -- [ ] Manual tests: Did you update the [manual tests](../blob/main/docs/tests) as appropriate? _While we aim for automated testing of the application, some aspects require manual testing. If you had to manually test something during development of this pull request, write those steps down._ +- [ ] Manual tests: Did you update the [manual tests](../blob/main/docs/testing/manual_testing) as appropriate? _While we aim for automated testing of the application, some aspects require manual testing. If you had to manually test something during development of this pull request, write those steps down._ - [ ] Documentation: Did you update documentation as appropriate? (e.g [README.md](../blob/main/README.md), etc.) - [ ] Run the app: Did you run the demo app and try the changes? - [ ] Rebase and squash: Did you pull in the latest changes from the main branch and squash your commits before assigning a reviewer? _Having your code up to date and squashed will make it easier for others to review. Use best judgement when squashing commits, as some changes (such as refactoring) might be easier to review as a separate commit._ diff --git a/.idea/runConfigurations/demo_app_benchmark_test_connectedBenchmarkAndroidTest.xml b/.idea/runConfigurations/demo_app_benchmark_test_connectedBenchmarkAndroidTest.xml new file mode 100644 index 00000000..d381e039 --- /dev/null +++ b/.idea/runConfigurations/demo_app_benchmark_test_connectedBenchmarkAndroidTest.xml @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/build-conventions/src/main/kotlin/zcash-sdk.android-conventions.gradle.kts b/build-conventions/src/main/kotlin/zcash-sdk.android-conventions.gradle.kts index 84d9c4fa..04b35584 100644 --- a/build-conventions/src/main/kotlin/zcash-sdk.android-conventions.gradle.kts +++ b/build-conventions/src/main/kotlin/zcash-sdk.android-conventions.gradle.kts @@ -52,6 +52,32 @@ pluginManager.withPlugin("com.android.library") { } } +pluginManager.withPlugin("com.android.test") { + project.the().apply { + configureBaseExtension() + + defaultConfig { + minSdk = project.property("ANDROID_MIN_BENCHMARK_VERSION").toString().toInt() + targetSdk = project.property("ANDROID_TARGET_SDK_VERSION").toString().toInt() + + // The last two are for support of pseudolocales in debug builds. + // If we add other localizations, they should be included in this list. + // By explicitly setting supported locales, we strip out unused localizations from third party + // libraries (e.g. play services) + resourceConfigurations.addAll(listOf("en", "en-rUS", "en-rGB", "en-rAU", "en_XA", "ar_XB")) + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) { + testInstrumentationRunnerArguments["clearPackageData"] = "true" + } + } + testCoverage { + jacocoVersion = project.property("JACOCO_VERSION").toString() + } + } +} + @Suppress("LongMethod") fun com.android.build.gradle.BaseExtension.configureBaseExtension() { compileSdkVersion(project.property("ANDROID_COMPILE_SDK_VERSION").toString().toInt()) @@ -116,8 +142,7 @@ fun com.android.build.gradle.BaseExtension.configureBaseExtension() { kotlinOptions { jvmTarget = project.property("ANDROID_JVM_TARGET").toString() allWarningsAsErrors = project.property("ZCASH_IS_TREAT_WARNINGS_AS_ERRORS").toString().toBoolean() - freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" + - "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + "-opt-in=kotlinx.coroutines.FlowPreview" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" } } } diff --git a/darkside-test-lib/build.gradle.kts b/darkside-test-lib/build.gradle.kts index f91994c7..19cb02b0 100644 --- a/darkside-test-lib/build.gradle.kts +++ b/darkside-test-lib/build.gradle.kts @@ -12,6 +12,14 @@ android { //targetSdk = 30 //Integer.parseInt(project.property("targetSdkVersion")) multiDexEnabled = true } + + buildTypes { + create("benchmark") { + // We provide the extra benchmark build type just for benchmarking purposes + initWith(buildTypes.getByName("release")) + matchingFallbacks += listOf("release") + } + } } dependencies { diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestPrerequisites.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestPrerequisites.kt index c3e78b4e..ee5b281e 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestPrerequisites.kt +++ b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestPrerequisites.kt @@ -14,7 +14,8 @@ open class DarksideTestPrerequisites { @Before fun verifyEmulator() { require(isProbablyEmulator(ApplicationProvider.getApplicationContext())) { - "Darkside tests are configured to only run on the Android Emulator. Please see https://github.com/zcash/zcash-android-wallet-sdk/blob/master/docs/tests/Darkside.md" + "Darkside tests are configured to only run on the Android Emulator. Please see https://github" + + ".com/zcash/zcash-android-wallet-sdk/blob/master/docs/testing/Darkside.md" } } diff --git a/demo-app-benchmark-test/build.gradle.kts b/demo-app-benchmark-test/build.gradle.kts new file mode 100644 index 00000000..f36d3594 --- /dev/null +++ b/demo-app-benchmark-test/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.test") + kotlin("android") + id("zcash-sdk.android-conventions") +} + +android { + namespace = "cash.z.ecc.android.sdk.demoapp.benchmark" + targetProjectPath = ":${projects.demoApp.name}" + experimentalProperties["android.experimental.self-instrumenting"] = true + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + // To enable benchmarking for emulators, although only a physical device us gives real results + testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR" + // To simplify module variants, we assume to run benchmarking against mainnet only + missingDimensionStrategy("network", "zcashmainnet") + } + + buildTypes { + create("release") { + // To provide compatibility with other modules + } + create("benchmark") { + // We provide the extra benchmark build type for benchmarking. We still need to support debug + // variants to be compatible with debug variants in other modules, although benchmarking does not allow + // not minified build variants - benchmarking with the debug build variants will fail. + isMinifyEnabled = true + isDebuggable = true + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks += listOf("release") + } + } +} + +dependencies { + implementation(libs.bundles.androidx.test) + implementation(libs.androidx.test.macrobenchmark) + implementation(libs.androidx.uiAutomator) + + if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) { + implementation(libs.androidx.test.orchestrator) { + artifact { + type = "apk" + } + } + } +} \ No newline at end of file diff --git a/demo-app-benchmark-test/proguard-consumer.txt b/demo-app-benchmark-test/proguard-consumer.txt new file mode 100644 index 00000000..97bc6fca --- /dev/null +++ b/demo-app-benchmark-test/proguard-consumer.txt @@ -0,0 +1,4 @@ +# This is generated automatically by the Android Gradle plugin. +-dontwarn androidx.test.services.storage.internal.InternalTestStorage +-dontwarn com.google.errorprone.annotations.InlineMe +-dontwarn com.google.errorprone.annotations.MustBeClosed \ No newline at end of file diff --git a/demo-app-benchmark-test/src/main/AndroidManifest.xml b/demo-app-benchmark-test/src/main/AndroidManifest.xml new file mode 100644 index 00000000..44008a43 --- /dev/null +++ b/demo-app-benchmark-test/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/benchmark/StartupBenchmark.kt b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/benchmark/StartupBenchmark.kt new file mode 100644 index 00000000..4a2a2550 --- /dev/null +++ b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/benchmark/StartupBenchmark.kt @@ -0,0 +1,123 @@ +package cash.z.ecc.android.sdk.demoapp.benchmark + +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.ExperimentalMetricApi +import androidx.benchmark.macro.MacrobenchmarkScope +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric +import androidx.benchmark.macro.TraceSectionMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import cash.z.ecc.android.sdk.demoapp.test.UiTestPrerequisites +import cash.z.ecc.android.sdk.demoapp.test.clickAndWaitFor +import cash.z.ecc.android.sdk.demoapp.test.waitFor +import org.junit.Rule +import org.junit.Test +import java.util.regex.Pattern +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Purpose of this class is to provide a basic startup measurements, and captured system traces for investigating the + * app's performance. It navigates to the device's home screen, and launches the default activity. + * + * Run this benchmark from Android Studio only against the Benchmark build type set in all modules. + * + * We ideally run this against a physical device with Android SDK level 29, at least, as profiling is provided by this + * version and later on. + */ + +// TODO [#809]: Enable macrobenchmark on CI +// TODO [#809]: https://github.com/zcash/zcash-android-wallet-sdk/issues/809 +class StartupBenchmark : UiTestPrerequisites() { + + companion object { + private const val APP_TARGET_PACKAGE_NAME = "cash.z.ecc.android.sdk.demoapp.mainnet" // NON-NLS + + private const val ADDRESS_SCREEN_SECTION = "ADDRESS_SCREEN" // NON-NLS + private const val UNIFIED_ADDRESS_SECTION = "UNIFIED_ADDRESS" // NON-NLS + private const val SAPLING_ADDRESS_SECTION = "SAPLING_ADDRESS" // NON-NLS + private const val TRANSPARENT_ADDRESS_SECTION = "TRANSPARENT_ADDRESS" // NON-NLS + } + + private val unifiedAddressPattern = "^[a-z0-9]{141}$".toPattern() // NON-NLS + private val saplingAddressPattern = "^[a-z0-9]{78}$".toPattern() // NON-NLS + private val transparentAddressPattern = "^[a-zA-Z0-9]{35}$".toPattern() // NON-NLS + + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + /** + * This test starts the Demo-app on Home screen and measures its metrics. + */ + @Test + fun appStartup() = benchmarkRule.measureRepeated( + packageName = APP_TARGET_PACKAGE_NAME, + metrics = listOf(StartupTimingMetric()), + iterations = 5, + startupMode = StartupMode.COLD, + setupBlock = { + // Press home button before each run to ensure the starting activity isn't visible + pressHome() + } + ) { + startActivityAndWait() + } + + /** + * Advanced trace events startup test, which starts the Demo-app on the Home screen and then navigates to the + * Get Address screen. Logic for providing addresses from SDK is measured here. + */ + @Test + @OptIn(ExperimentalMetricApi::class) + fun tracesSdkStartup() = benchmarkRule.measureRepeated( + packageName = APP_TARGET_PACKAGE_NAME, + metrics = listOf( + TraceSectionMetric(sectionName = ADDRESS_SCREEN_SECTION, mode = TraceSectionMetric.Mode.First), + TraceSectionMetric(sectionName = UNIFIED_ADDRESS_SECTION, mode = TraceSectionMetric.Mode.First), + TraceSectionMetric(sectionName = SAPLING_ADDRESS_SECTION, mode = TraceSectionMetric.Mode.First), + TraceSectionMetric(sectionName = TRANSPARENT_ADDRESS_SECTION, mode = TraceSectionMetric.Mode.First) + ), + compilationMode = CompilationMode.Full(), + startupMode = StartupMode.COLD, + iterations = 5, + measureBlock = { + startActivityAndWait() + gotoAddressScreen() + waitForAddressScreen() + closeAddressScreen() + } + ) + + private fun MacrobenchmarkScope.closeAddressScreen() { + // To close the Address screen and disconnect from SDK Synchronizer + device.pressBack() + } + + private fun MacrobenchmarkScope.waitForAddressScreen() { + val timeoutSeconds = 5.seconds + check( + waitForAddressAppear(unifiedAddressPattern, timeoutSeconds) && + waitForAddressAppear(saplingAddressPattern, timeoutSeconds) && + waitForAddressAppear(transparentAddressPattern, timeoutSeconds) + ) { + "Some of the addresses didn't appear before $timeoutSeconds seconds timeout." + } + } + + private fun MacrobenchmarkScope.waitForAddressAppear(addressPattern: Pattern, timeout: Duration): Boolean { + return device.waitFor(Until.hasObject(By.text(addressPattern)), timeout) + } + + // TODO [#808]: Add demo-ui-lib module (and reference the hardcoded texts here) + // TODO [#808]: https://github.com/zcash/zcash-android-wallet-sdk/issues/808 + + private fun MacrobenchmarkScope.gotoAddressScreen() { + // Open drawer menu + device.findObject(By.desc("Open navigation drawer")) // NON-NLS + .clickAndWaitFor(Until.newWindow(), 2.seconds) + // Navigate to Addresses screen + device.findObject(By.text("Get Address")).click() // NON-NLS + } +} diff --git a/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/benchmark/SyncBlockchainBenchmark.kt b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/benchmark/SyncBlockchainBenchmark.kt new file mode 100644 index 00000000..d3a7f2cd --- /dev/null +++ b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/benchmark/SyncBlockchainBenchmark.kt @@ -0,0 +1,101 @@ +package cash.z.ecc.android.sdk.demoapp.benchmark + +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.ExperimentalMetricApi +import androidx.benchmark.macro.MacrobenchmarkScope +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.TraceSectionMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import cash.z.ecc.android.sdk.demoapp.test.UiTestPrerequisites +import cash.z.ecc.android.sdk.demoapp.test.clickAndWaitFor +import cash.z.ecc.android.sdk.demoapp.test.waitFor +import org.junit.Rule +import org.junit.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +/** + * This benchmark class provides measurements and captured custom traces for investigating SDK syncing mechanisms + * with restricted blockchain range. It always resets the SDK before the next sync iteration. It uses UIAutomator to + * navigate to the Balances screen, where the whole download -> validate -> scan -> enhance process is performed and + * thus measured by this test. + * + * Run this benchmark from Android Studio only against the Benchmark build type set in all modules. + * + * We ideally run this on a physical device with Android SDK level 29, at least, as profiling is provided by this + * version and later on. + */ + +// TODO [#809]: Enable macrobenchmark on CI +// TODO [#809]: https://github.com/zcash/zcash-android-wallet-sdk/issues/809 +class SyncBlockchainBenchmark : UiTestPrerequisites() { + + companion object { + private const val APP_TARGET_PACKAGE_NAME = "cash.z.ecc.android.sdk.demoapp.mainnet" // NON-NLS + + private const val BALANCE_SCREEN_SECTION = "BALANCE_SCREEN" // NON-NLS + private const val BLOCKCHAIN_SYNC_SECTION = "BLOCKCHAIN_SYNC" // NON-NLS + private const val DOWNLOAD_SECTION = "DOWNLOAD" // NON-NLS + private const val VALIDATION_SECTION = "VALIDATION" // NON-NLS + private const val SCAN_SECTION = "SCAN" // NON-NLS + } + + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + /** + * Advanced trace events test, which starts the Demo-app on the Home screen and then navigates to the Get Balance + * screen. SDK sync phases with restricted blockchain range are measured during the overall sync mechanism here. + */ + @Test + @OptIn(ExperimentalMetricApi::class) + fun tracesSyncBlockchain() = benchmarkRule.measureRepeated( + packageName = APP_TARGET_PACKAGE_NAME, + metrics = listOf( + TraceSectionMetric(sectionName = BALANCE_SCREEN_SECTION, mode = TraceSectionMetric.Mode.First), + TraceSectionMetric(sectionName = BLOCKCHAIN_SYNC_SECTION, mode = TraceSectionMetric.Mode.First), + TraceSectionMetric(sectionName = DOWNLOAD_SECTION, mode = TraceSectionMetric.Mode.First), + TraceSectionMetric(sectionName = VALIDATION_SECTION, mode = TraceSectionMetric.Mode.First), + TraceSectionMetric(sectionName = SCAN_SECTION, mode = TraceSectionMetric.Mode.First) + ), + compilationMode = CompilationMode.Full(), + startupMode = StartupMode.COLD, + iterations = 3, + measureBlock = { + startActivityAndWait() + resetSDK() + gotoBalanceScreen() + waitForBalanceScreen() + closeBalanceScreen() + } + ) + + // TODO [#808]: Add demo-ui-lib module (and reference the hardcoded texts here) + // TODO [#808]: https://github.com/zcash/zcash-android-wallet-sdk/issues/808 + + private fun MacrobenchmarkScope.resetSDK() { + // Open toolbar overflow menu + device.findObject(By.desc("More options")).clickAndWaitFor(Until.newWindow(), 2.seconds) // NON-NLS + // Click on the reset sdk menu item + device.findObject(By.text("Reset SDK")).click() // NON-NLS + device.waitForIdle() + } + + private fun MacrobenchmarkScope.waitForBalanceScreen() { + device.waitFor(Until.hasObject(By.text("Status: SYNCED")), 5.minutes) // NON-NLS + } + + private fun MacrobenchmarkScope.closeBalanceScreen() { + // To close the Balance screen and disconnect from SDK Synchronizer + device.pressBack() + } + + private fun MacrobenchmarkScope.gotoBalanceScreen() { + // Open drawer menu + device.findObject(By.desc("Open navigation drawer")).clickAndWaitFor(Until.newWindow(), 2.seconds) // NON-NLS + // Navigate to Balances screen + device.findObject(By.text("Get Balance")).click() // NON-NLS + } +} diff --git a/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/Global.kt b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/Global.kt new file mode 100644 index 00000000..fac4ba54 --- /dev/null +++ b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/Global.kt @@ -0,0 +1,12 @@ +package cash.z.ecc.android.sdk.demoapp.test + +import android.content.Context +import androidx.annotation.StringRes +import androidx.test.core.app.ApplicationProvider + +fun getAppContext(): Context = ApplicationProvider.getApplicationContext() + +fun getStringResource(@StringRes resId: Int) = getAppContext().getString(resId) + +fun getStringResourceWithArgs(@StringRes resId: Int, vararg formatArgs: String) = + getAppContext().getString(resId, *formatArgs) diff --git a/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/UiAutomatorExt.kt b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/UiAutomatorExt.kt new file mode 100644 index 00000000..c7faa9e2 --- /dev/null +++ b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/UiAutomatorExt.kt @@ -0,0 +1,15 @@ +package cash.z.ecc.android.sdk.demoapp.test + +import androidx.test.uiautomator.EventCondition +import androidx.test.uiautomator.SearchCondition +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject2 +import kotlin.time.Duration + +fun UiDevice.waitFor(condition: SearchCondition, timeout: Duration): Boolean { + return wait(condition, timeout.inWholeMilliseconds) +} + +fun UiObject2.clickAndWaitFor(condition: EventCondition, timeout: Duration): Boolean { + return clickAndWait(condition, timeout.inWholeMilliseconds) +} diff --git a/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/UiTestPrerequisites.kt b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/UiTestPrerequisites.kt new file mode 100644 index 00000000..fe97eee2 --- /dev/null +++ b/demo-app-benchmark-test/src/main/java/cash/z/ecc/android/sdk/demoapp/test/UiTestPrerequisites.kt @@ -0,0 +1,48 @@ +package cash.z.ecc.android.sdk.demoapp.test + +import android.app.KeyguardManager +import android.content.Context +import android.os.PowerManager +import androidx.test.core.app.ApplicationProvider +import org.junit.Before + +/** + * Subclass this in view unit and integration tests. This verifies that + * prerequisites necessary for reliable UI tests are met, and it provides more useful error messages. + */ +open class UiTestPrerequisites { + @Before + fun verifyPrerequisites() { + assertScreenIsOn() + assertKeyguardIsUnlocked() + } + + companion object { + fun assertScreenIsOn() { + if (!isScreenOn()) { + throw AssertionError("Screen must be on for Android UI tests to run") // $NON-NLS + } + } + + private fun isScreenOn(): Boolean { + val powerService = ApplicationProvider.getApplicationContext() + .getSystemService(Context.POWER_SERVICE) as PowerManager + return powerService.isInteractive + } + + fun assertKeyguardIsUnlocked() { + if (isKeyguardLocked()) { + throw AssertionError("Device must be unlocked on for Android UI tests to run") // $NON-NLS + } + } + + private fun isKeyguardLocked(): Boolean { + val keyguardService = ( + ApplicationProvider.getApplicationContext() + .getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + ) + + return keyguardService.isKeyguardLocked + } + } +} diff --git a/demo-app/build.gradle.kts b/demo-app/build.gradle.kts index 304ccd19..8fd3e728 100644 --- a/demo-app/build.gradle.kts +++ b/demo-app/build.gradle.kts @@ -82,6 +82,12 @@ android { signingConfig = signingConfigs.getByName("debug") } } + create("benchmark") { + // We provide the extra benchmark build type just for benchmarking purposes + initWith(buildTypes.getByName("release")) + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks += listOf("release") + } } lint { @@ -103,6 +109,10 @@ dependencies { implementation(libs.androidx.multidex) implementation(libs.androidx.navigation.fragment) implementation(libs.androidx.navigation.ui) + // Just to support profile installation and tracing events needed by benchmark tests + implementation(libs.androidx.profileinstaller) + implementation(libs.androidx.tracing) + implementation(libs.material) androidTestImplementation(libs.bundles.androidx.test) diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml index 1c99debc..19a35a97 100644 --- a/demo-app/src/main/AndroidManifest.xml +++ b/demo-app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ - + @@ -21,6 +20,24 @@ + + + + + + + + + + + - + \ No newline at end of file diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/BaseDemoFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/BaseDemoFragment.kt index cf99c9aa..c2ad02f4 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/BaseDemoFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/BaseDemoFragment.kt @@ -11,8 +11,14 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.viewbinding.ViewBinding +import cash.z.ecc.android.sdk.demoapp.util.BenchmarkTrace +import cash.z.ecc.android.sdk.demoapp.util.ProvideAddressBenchmarkTrace +import cash.z.ecc.android.sdk.demoapp.util.SyncBlockchainBenchmarkTrace import cash.z.ecc.android.sdk.demoapp.util.mainActivity import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch abstract class BaseDemoFragment : Fragment() { @@ -27,6 +33,8 @@ abstract class BaseDemoFragment : Fragment() { val sharedViewModel: SharedViewModel by activityViewModels() lateinit var binding: T + internal val traceScope = CoroutineScope(Dispatchers.Main) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -93,4 +101,13 @@ abstract class BaseDemoFragment : Fragment() { * interface so the base class cannot take care of this behavior without some help. */ abstract fun inflateBinding(layoutInflater: LayoutInflater): T + + internal fun reportTraceEvent(event: BenchmarkTrace.Event?) { + traceScope.launch { + when (event) { + is ProvideAddressBenchmarkTrace.Event -> ProvideAddressBenchmarkTrace.writeEvent(event) + is SyncBlockchainBenchmarkTrace.Event -> SyncBlockchainBenchmarkTrace.writeEvent(event) + } + } + } } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt index 9eb5c017..9211ee1e 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt @@ -9,6 +9,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.inputmethod.InputMethodManager +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.content.getSystemService @@ -36,6 +37,7 @@ class MainActivity : private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var clipboard: ClipboardManager var fabListener: BaseDemoFragment? = null + private val sharedViewModel: SharedViewModel by viewModels() /** * The service to use for all demos that interact directly with the service. Since gRPC channels @@ -102,6 +104,11 @@ class MainActivity : startActivity(newBrowserIntent("https://faucet.zecpages.com/")) } true + } else if (item.itemId == R.id.action_reset_sdk) { + val navController = findNavController(R.id.nav_host_fragment) + navController.navigate(R.id.nav_home) + sharedViewModel.resetSDK() + true } else { super.onOptionsItemSelected(item) } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/SharedViewModel.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/SharedViewModel.kt index cce7a03f..411f632c 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/SharedViewModel.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/SharedViewModel.kt @@ -2,13 +2,16 @@ package cash.z.ecc.android.sdk.demoapp import android.app.Application import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope import cash.z.ecc.android.bip39.Mnemonics +import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ZcashNetwork import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking /** @@ -42,6 +45,17 @@ class SharedViewModel(application: Application) : AndroidViewModel(application) } } + fun resetSDK() { + viewModelScope.launch { + with(getApplication()) { + Synchronizer.erase( + appContext = applicationContext, + network = ZcashNetwork.fromResources(applicationContext) + ) + } + } + } + private fun isValidSeedPhrase(phrase: String?): Boolean { if (phrase.isNullOrEmpty()) { return false diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt index 9f850df6..880bc538 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt @@ -9,6 +9,7 @@ import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext +import cash.z.ecc.android.sdk.demoapp.util.ProvideAddressBenchmarkTrace import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -61,17 +62,23 @@ class GetAddressFragment : BaseDemoFragment() { private fun displayAddress() { viewLifecycleOwner.lifecycleScope.launchWhenStarted { binding.unifiedAddress.apply { + reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_START) val uaddress = synchronizer.getUnifiedAddress() + reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_END) text = uaddress setOnClickListener { copyToClipboard(uaddress) } } binding.saplingAddress.apply { + reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_START) val sapling = synchronizer.getSaplingAddress() + reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_END) text = sapling setOnClickListener { copyToClipboard(sapling) } } binding.transparentAddress.apply { + reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_START) val transparent = synchronizer.getTransparentAddress() + reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_END) text = transparent setOnClickListener { copyToClipboard(transparent) } } @@ -84,6 +91,7 @@ class GetAddressFragment : BaseDemoFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + reportTraceEvent(ProvideAddressBenchmarkTrace.Event.ADDRESS_SCREEN_START) setup() } @@ -92,6 +100,11 @@ class GetAddressFragment : BaseDemoFragment() { displayAddress() } + override fun onDestroy() { + super.onDestroy() + reportTraceEvent(ProvideAddressBenchmarkTrace.Event.ADDRESS_SCREEN_END) + } + // // Base Fragment overrides // diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt index f025ff8d..e085f0c5 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.demoapp.demos.getbalance import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu import android.view.View import androidx.lifecycle.lifecycleScope import cash.z.ecc.android.bip39.Mnemonics @@ -9,12 +10,17 @@ import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment +import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext +import cash.z.ecc.android.sdk.demoapp.util.SyncBlockchainBenchmarkTrace import cash.z.ecc.android.sdk.demoapp.util.fromResources +import cash.z.ecc.android.sdk.ext.BenchmarkingExt import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.collectWith import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString +import cash.z.ecc.android.sdk.fixture.BlockRangeFixture +import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.WalletBalance @@ -38,9 +44,22 @@ class GetBalanceFragment : BaseDemoFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_START) + setHasOptionsMenu(true) setup() } + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + // We rather hide options menu actions while actively using the Synchronizer + menu.setGroupVisible(R.id.main_menu_group, false) + } + + override fun onDestroy() { + super.onDestroy() + reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_END) + } + private fun setup() { // defaults to the value of `DemoConfig.seedWords` but can also be set by the user val seedPhrase = sharedViewModel.seedPhrase.value @@ -55,7 +74,11 @@ class GetBalanceFragment : BaseDemoFragment() { network, lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), seed = seed, - birthday = sharedViewModel.birthdayHeight.value + birthday = if (BenchmarkingExt.isBenchmarking()) { + BlockRangeFixture.new().start + } else { + sharedViewModel.birthdayHeight.value + } ) } @@ -116,7 +139,7 @@ class GetBalanceFragment : BaseDemoFragment() { binding.shield.apply { // TODO [#776]: Support variable fees - // https://github.com/zcash/zcash-android-wallet-sdk/issues/776 + // TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776 visibility = if ((transparentBalance?.available ?: Zatoshi(0)) > ZcashSdk.MINERS_FEE) { View.VISIBLE } else { @@ -126,6 +149,37 @@ class GetBalanceFragment : BaseDemoFragment() { } private fun onStatus(status: Synchronizer.Status) { + twig("Synchronizer status: $status") + // report benchmark event + val traceEvents = when (status) { + Synchronizer.Status.DOWNLOADING -> { + listOf( + SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_START, + SyncBlockchainBenchmarkTrace.Event.DOWNLOAD_START + ) + } + Synchronizer.Status.VALIDATING -> { + listOf( + SyncBlockchainBenchmarkTrace.Event.DOWNLOAD_END, + SyncBlockchainBenchmarkTrace.Event.VALIDATION_START + ) + } + Synchronizer.Status.SCANNING -> { + listOf( + SyncBlockchainBenchmarkTrace.Event.VALIDATION_END, + SyncBlockchainBenchmarkTrace.Event.SCAN_START + ) + } + Synchronizer.Status.SYNCED -> { + listOf( + SyncBlockchainBenchmarkTrace.Event.SCAN_END, + SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_END + ) + } + else -> null + } + traceEvents?.forEach { reportTraceEvent(it) } + binding.textStatus.text = "Status: $status" onOrchardBalance(synchronizer.orchardBalances.value) onSaplingBalance(synchronizer.saplingBalances.value) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index 0b1c45d7..824553ec 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.demoapp.demos.listtransactions import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu import android.view.View import android.view.ViewGroup import androidx.lifecycle.lifecycleScope @@ -11,6 +12,7 @@ import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment +import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext import cash.z.ecc.android.sdk.demoapp.util.fromResources @@ -132,6 +134,11 @@ class ListTransactionsFragment : BaseDemoFragment() { // Android Lifecycle overrides // + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -248,6 +255,12 @@ class SendFragment : BaseDemoFragment() { initSendUi() } + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + // We rather hide options menu actions while actively using the Synchronizer + menu.setGroupVisible(R.id.main_menu_group, false) + } + override fun onResume() { super.onResume() // the lifecycleScope is used to dispose of the synchronizer when the fragment dies diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/BenchmarkTrace.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/BenchmarkTrace.kt new file mode 100644 index 00000000..8d54d250 --- /dev/null +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/BenchmarkTrace.kt @@ -0,0 +1,182 @@ +package cash.z.ecc.android.sdk.demoapp.util + +import android.os.Looper +import androidx.tracing.Trace +import cash.z.ecc.android.sdk.ext.BenchmarkingExt +import cash.z.ecc.android.sdk.internal.twig + +interface BenchmarkTrace { + fun checkMainThread() { + check(Looper.getMainLooper().thread === Thread.currentThread()) { + "Should be called from the main thread, not ${Thread.currentThread()}." + } + } + + interface Event { + val section: String + val cookie: Int + } +} + +object SyncBlockchainBenchmarkTrace : BenchmarkTrace { + fun writeEvent(event: BenchmarkTrace.Event?) { + twig("New SyncBlockchain event: $event arrived.") + if (!BenchmarkingExt.isBenchmarking()) { + return + } + checkMainThread() + when (event) { + Event.BALANCE_SCREEN_START -> { + Trace.beginAsyncSection(Event.BALANCE_SCREEN_START.section, Event.BALANCE_SCREEN_START.cookie) + } + Event.BALANCE_SCREEN_END -> { + Trace.endAsyncSection(Event.BALANCE_SCREEN_END.section, Event.BALANCE_SCREEN_END.cookie) + } + Event.BLOCKCHAIN_SYNC_START -> { + Trace.beginAsyncSection(Event.BLOCKCHAIN_SYNC_START.section, Event.BLOCKCHAIN_SYNC_START.cookie) + } + Event.BLOCKCHAIN_SYNC_END -> { + Trace.endAsyncSection(Event.BLOCKCHAIN_SYNC_END.section, Event.BLOCKCHAIN_SYNC_END.cookie) + } + Event.DOWNLOAD_START -> { + Trace.beginAsyncSection(Event.DOWNLOAD_START.section, Event.DOWNLOAD_START.cookie) + } + Event.DOWNLOAD_END -> { + Trace.endAsyncSection(Event.DOWNLOAD_END.section, Event.DOWNLOAD_END.cookie) + } + Event.VALIDATION_START -> { + Trace.beginAsyncSection(Event.VALIDATION_START.section, Event.VALIDATION_START.cookie) + } + Event.VALIDATION_END -> { + Trace.endAsyncSection(Event.VALIDATION_END.section, Event.VALIDATION_END.cookie) + } + Event.SCAN_START -> { + Trace.beginAsyncSection(Event.SCAN_START.section, Event.SCAN_START.cookie) + } + Event.SCAN_END -> { + Trace.endAsyncSection(Event.SCAN_END.section, Event.SCAN_END.cookie) + } + else -> { /* nothing to write */ } + } + } + + @Suppress("MagicNumber") + enum class Event : BenchmarkTrace.Event { + BALANCE_SCREEN_START { + override val section: String = "BALANCE_SCREEN" // NON-NLS + override val cookie: Int = 100 + }, + BALANCE_SCREEN_END { + override val section: String = "BALANCE_SCREEN" // NON-NLS + override val cookie: Int = 100 + }, + BLOCKCHAIN_SYNC_START { + override val section: String = "BLOCKCHAIN_SYNC" // NON-NLS + override val cookie: Int = 200 + }, + BLOCKCHAIN_SYNC_END { + override val section: String = "BLOCKCHAIN_SYNC" // NON-NLS + override val cookie: Int = 200 + }, + DOWNLOAD_START { + override val section: String = "DOWNLOAD" // NON-NLS + override val cookie: Int = 300 + }, + DOWNLOAD_END { + override val section: String = "DOWNLOAD" // NON-NLS + override val cookie: Int = 300 + }, + VALIDATION_START { + override val section: String = "VALIDATION" // NON-NLS + override val cookie: Int = 400 + }, + VALIDATION_END { + override val section: String = "VALIDATION" // NON-NLS + override val cookie: Int = 400 + }, + SCAN_START { + override val section: String = "SCAN" // NON-NLS + override val cookie: Int = 500 + }, + SCAN_END { + override val section: String = "SCAN" // NON-NLS + override val cookie: Int = 500 + } + } +} + +object ProvideAddressBenchmarkTrace : BenchmarkTrace { + fun writeEvent(event: BenchmarkTrace.Event?) { + twig("New ProvideAddress event: $event arrived.") + if (!BenchmarkingExt.isBenchmarking()) { + return + } + checkMainThread() + when (event) { + Event.ADDRESS_SCREEN_START -> { + Trace.beginAsyncSection(Event.ADDRESS_SCREEN_START.section, Event.ADDRESS_SCREEN_START.cookie) + } + Event.ADDRESS_SCREEN_END -> { + Trace.endAsyncSection(Event.ADDRESS_SCREEN_END.section, Event.ADDRESS_SCREEN_END.cookie) + } + Event.UNIFIED_ADDRESS_START -> { + Trace.beginAsyncSection(Event.UNIFIED_ADDRESS_START.section, Event.UNIFIED_ADDRESS_START.cookie) + } + Event.UNIFIED_ADDRESS_END -> { + Trace.endAsyncSection(Event.UNIFIED_ADDRESS_END.section, Event.UNIFIED_ADDRESS_END.cookie) + } + Event.SAPLING_ADDRESS_START -> { + Trace.beginAsyncSection(Event.SAPLING_ADDRESS_START.section, Event.SAPLING_ADDRESS_START.cookie) + } + Event.SAPLING_ADDRESS_END -> { + Trace.endAsyncSection(Event.SAPLING_ADDRESS_END.section, Event.SAPLING_ADDRESS_END.cookie) + } + Event.TRANSPARENT_ADDRESS_START -> { + Trace.beginAsyncSection( + Event.TRANSPARENT_ADDRESS_START.section, + Event.TRANSPARENT_ADDRESS_START.cookie + ) + } + Event.TRANSPARENT_ADDRESS_END -> { + Trace.endAsyncSection(Event.TRANSPARENT_ADDRESS_END.section, Event.TRANSPARENT_ADDRESS_END.cookie) + } + else -> { /* nothing to write */ } + } + } + + @Suppress("MagicNumber") + enum class Event : BenchmarkTrace.Event { + ADDRESS_SCREEN_START { + override val section: String = "ADDRESS_SCREEN" // NON-NLS + override val cookie: Int = 100 + }, + ADDRESS_SCREEN_END { + override val section: String = "ADDRESS_SCREEN" // NON-NLS + override val cookie: Int = 100 + }, + UNIFIED_ADDRESS_START { + override val section: String = "UNIFIED_ADDRESS" // NON-NLS + override val cookie: Int = 200 + }, + UNIFIED_ADDRESS_END { + override val section: String = "UNIFIED_ADDRESS" // NON-NLS + override val cookie: Int = 200 + }, + SAPLING_ADDRESS_START { + override val section: String = "SAPLING_ADDRESS" // NON-NLS + override val cookie: Int = 300 + }, + SAPLING_ADDRESS_END { + override val section: String = "SAPLING_ADDRESS" // NON-NLS + override val cookie: Int = 300 + }, + TRANSPARENT_ADDRESS_START { + override val section: String = "TRANSPARENT_ADDRESS" // NON-NLS + override val cookie: Int = 400 + }, + TRANSPARENT_ADDRESS_END { + override val section: String = "TRANSPARENT_ADDRESS" // NON-NLS + override val cookie: Int = 400 + } + } +} diff --git a/demo-app/src/main/res/menu/main.xml b/demo-app/src/main/res/menu/main.xml index 2b067540..249c2f40 100644 --- a/demo-app/src/main/res/menu/main.xml +++ b/demo-app/src/main/res/menu/main.xml @@ -2,14 +2,23 @@ - - + + + + + + diff --git a/demo-app/src/main/res/values/strings.xml b/demo-app/src/main/res/values/strings.xml index c5baaff1..2223dbb9 100644 --- a/demo-app/src/main/res/values/strings.xml +++ b/demo-app/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Navigation header Change Seed Phrase Testnet Faucet + Reset SDK Home diff --git a/docs/tests/Build.md b/docs/Build.md similarity index 100% rename from docs/tests/Build.md rename to docs/Build.md diff --git a/docs/tests/Public APIs.md b/docs/Public APIs.md similarity index 91% rename from docs/tests/Public APIs.md rename to docs/Public APIs.md index f84ba55d..2a508200 100644 --- a/docs/tests/Public APIs.md +++ b/docs/Public APIs.md @@ -3,7 +3,7 @@ The SDK has a variety of public APIs that should be kept stable for SDK consumer # Compile Compatibility 1. Publish the SDK to mavenLocal - 1. Bump the SDK version in [gradle.properties](../../gradle.properties) + 1. Bump the SDK version in [gradle.properties](../gradle.properties) 1. Navigate to the root of the SDK checkout 1. Run the Gradle task `./gradlew publishToMavenLocal` 1. Modify the wallet app to build against the new SDK @@ -20,7 +20,7 @@ The SDK has a variety of public APIs that should be kept stable for SDK consumer 1. Build the unmodified version of the wallet app 1. Run the wallet app and create a new wallet 1. Publish the SDK to mavenLocal - 1. Bump the SDK version in [gradle.properties](../../gradle.properties) + 1. Bump the SDK version in [gradle.properties](../gradle.properties) 1. Navigate to the root of the SDK checkout 1. Run the Gradle task `./gradlew publishToMavenLocal` 1. Modify the wallet app to build against the new SDK diff --git a/docs/tests/Darkside.md b/docs/testing/Darkside.md similarity index 100% rename from docs/tests/Darkside.md rename to docs/testing/Darkside.md diff --git a/docs/testing/Testing.md b/docs/testing/Testing.md new file mode 100644 index 00000000..0cd6341d --- /dev/null +++ b/docs/testing/Testing.md @@ -0,0 +1,109 @@ +# Testing +This documentation outlines our approach to testing. By running tests against our app consistently, we verify the +SDK's correctness, functional behavior, and usability before releasing it publicly. + +## Automated testing + +- TBD + + + +## Manual testing + +We aim to automate as much as we possibly can. Still manual testing is really important for Quality Assurance. + +Here you'll find our manual testing scripts. When developing a new feature you can add your own that provide the proper steps to properly test it. + +## Gathering Code Coverage +The app consists of different Gradle module types (e.g. Kotlin Multiplatform, Android). Generating coverage for these different module types requires different command line invocations. + +### Kotlin Multiplatform +Kotlin Multiplatform does not support coverage for all platforms. Most of our code lives under commonMain, with a JVM target. This effectively allows generation of coverage reports with Jacoco. Coverage is enabled by default when running `./gradlew check`. + +### Android +The Android Gradle plugin supports code coverage with Jacoco. This integration can sometimes be buggy. For that reason, coverage is disabled by default and can be enabled on a case-by-case basis, by passing `-PIS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED=true` as a command line argument for Gradle builds. For example: `./gradlew connectedCheck -PIS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED=true`. + +When coverage is enabled, running instrumentation tests will automatically generate coverage reports stored under `$module/build/reports/coverage`. + +## Benchmarking +This section provides information about available benchmark tests integrated in this project as well as how to use them. Currently, we support macrobenchmark tests run locally as described in the Android [documentation](https://developer.android.com/topic/performance/benchmarking/benchmarking-overview). + +We provide dedicated benchmark test module `demo-app-benchmark-test` for this. If you want to run these benchmark +tests against our demo application, make sure you have a physical device connected with Android SDK level 29, at least. +Select `benchmark` build variant for this module. Make sure that other modules are set to benchmark +type too. The benchmark tests can be run with Android Studio run configuration +`demo-app-benchmark-test:connectedBenchmarkAndroidTest`. Running the benchmark test this way automatically +provides benchmarking results in Run panel. Or you can run the tests manually from the terminal with `./gradlew connectedBenchmarkAndroidTest` and analyze results with Android Studio's Profiler or [Perfetto](https://ui.perfetto.dev/) tool, as described in this Android [documentation](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview#access-trace). + +**Note**: We've enabled benchmarking also for emulators, although it's always better to run the tests on a real physical device. Emulator benchmark improvements might not carry over to a real user's experience (or even regress real device performance). + +### Referential benchmark tests results +Every few months, or before a major SDK release, we run and compare benchmark test results to avoid making the SDK's mechanisms significantly slower. + +**Note**: If possible, run the benchmark tests on a physical device with sufficient empty disk space, connected to the +internet and charged or plugged-in to a charger. It's always better to restart the device before approaching to +running the benchmark tests. Also, please, ensure you're running it on the latest main branch +commits of that date. Generate tests results with the Android Studio run configuration +`demo-app-benchmark-test:connectedBenchmarkAndroidTest` and gather results from the Run panel. + +#### Dec 7, 2022: + +- SDK version: `1.10.0-beta01` +- Git branch: `789-benchmark-demo-app` +- Device: + - Pixel 6 - Android 13: + ``` + Starting 3 tests on Pixel 6 - 13 + + StartupBenchmark_appStartup + timeToInitialDisplayMs min 388.8, median 410.9, max 423.0 + Traces: Iteration 0 1 2 3 4 + + StartupBenchmark_tracesSdkStartup + ADDRESS_SCREENMs min 784.1, median 900.4, max 926.9 + SAPLING_ADDRESSMs min 2.4, median 4.3, max 5.9 + TRANSPARENT_ADDRESSMs min 1.7, median 2.6, max 6.0 + UNIFIED_ADDRESSMs min 1.5, median 2.2, max 2.8 + Traces: Iteration 0 1 2 3 4 + + SyncBlockchainBenchmark_tracesSyncBlockchain + BALANCE_SCREENMs min 46,042.2, median 46,233.0, max 46,462.2 + BLOCKCHAIN_SYNCMs min 45,393.5, median 45,578.3, max 45,830.3 + DOWNLOADMs min 34,951.3, median 35,763.0, max 35,870.6 + SCANMs min 9,536.7, median 9,846.5, max 10,501.8 + VALIDATIONMs min 93.1, median 112.5, max 124.3 + Traces: Iteration 0 1 2 + + BUILD SUCCESSFUL in 4m 18s + ``` + - Pixel 3a - Android 12: + ``` + Starting 3 tests on Pixel 3a - 12 + + StartupBenchmark_appStartup + timeToInitialDisplayMs min 545.3, median 565.3, max 607.2 + Traces: Iteration 0 1 2 3 4 + + StartupBenchmark_tracesSdkStartup + ADDRESS_SCREENMs min 897.1, median 955.3, max 1,352.8 + SAPLING_ADDRESSMs min 3.9, median 6.1, max 8.3 + TRANSPARENT_ADDRESSMs min 2.0, median 4.2, max 5.9 + UNIFIED_ADDRESSMs min 2.4, median 2.4, max 5.2 + Traces: Iteration 0 1 2 3 4 + + SyncBlockchainBenchmark_tracesSyncBlockchain + BALANCE_SCREENMs min 63,403.1, median 63,716.7, max 63,993.6 + BLOCKCHAIN_SYNCMs min 62,739.0, median 63,060.5, max 63,345.0 + DOWNLOADMs min 34,317.3, median 34,462.8, max 34,551.3 + SCANMs min 28,279.3, median 28,463.3, max 28,655.8 + VALIDATIONMs min 133.0, median 136.4, max 141.2 + Traces: Iteration 0 1 2 + + BUILD SUCCESSFUL in 6m 12s + ``` + + + + + + diff --git a/docs/tests/Download sapling files.md b/docs/testing/manual_testing/Download sapling files.md similarity index 100% rename from docs/tests/Download sapling files.md rename to docs/testing/manual_testing/Download sapling files.md diff --git a/docs/tests/Move database files to no_backup.md b/docs/testing/manual_testing/Move database files to no_backup.md similarity index 100% rename from docs/tests/Move database files to no_backup.md rename to docs/testing/manual_testing/Move database files to no_backup.md diff --git a/docs/tests/Move sapling files to no_backup.md b/docs/testing/manual_testing/Move sapling files to no_backup.md similarity index 100% rename from docs/tests/Move sapling files to no_backup.md rename to docs/testing/manual_testing/Move sapling files to no_backup.md diff --git a/gradle.properties b/gradle.properties index e08d95ef..65bb8c86 100644 --- a/gradle.properties +++ b/gradle.properties @@ -65,9 +65,11 @@ IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY=false # Versions ANDROID_MIN_SDK_VERSION=21 +ANDROID_MIN_BENCHMARK_VERSION=24 ANDROID_TARGET_SDK_VERSION=33 ANDROID_COMPILE_SDK_VERSION=33 +# TODO[#317]: Update NDK to 24.0.7856742 # TODO[#317]: https://github.com/zcash/zcash-android-wallet-sdk/issues/317 # When changing this, be sure to update .github/actions/setup/action.yml ANDROID_NDK_VERSION=22.1.7171670 @@ -94,11 +96,14 @@ ANDROIDX_LIFECYCLE_VERSION=2.4.1 ANDROIDX_MULTIDEX_VERSION=2.0.1 ANDROIDX_NAVIGATION_VERSION=2.4.2 ANDROIDX_PAGING_VERSION=2.1.2 +ANDROIDX_PROFILE_INSTALLER_VERSION=1.3.0-alpha02 ANDROIDX_ROOM_VERSION=2.4.3 ANDROIDX_TEST_JUNIT_VERSION=1.1.3 -ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.1.0-alpha1 +ANDROIDX_TEST_MACROBENCHMARK_VERSION=1.2.0-alpha08 +ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.4.2 ANDROIDX_TEST_VERSION=1.4.0 -ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1 +ANDROIDX_TRACING_VERSION=1.2.0-alpha01 +ANDROIDX_UI_AUTOMATOR_VERSION=2.3.0-alpha01 BIP39_VERSION=1.0.4 COROUTINES_OKHTTP=1.0 GOOGLE_MATERIAL_VERSION=1.6.1 diff --git a/sdk-lib/build.gradle.kts b/sdk-lib/build.gradle.kts index 7bd780e9..7455cc71 100644 --- a/sdk-lib/build.gradle.kts +++ b/sdk-lib/build.gradle.kts @@ -147,6 +147,11 @@ android { ) ) } + create("benchmark") { + // We provide the extra benchmark build type just for benchmarking purposes + initWith(buildTypes.getByName("release")) + matchingFallbacks += listOf("release") + } } sourceSets.getByName("main") { diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/DatabaseCoordinatorTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/DatabaseCoordinatorTest.kt index fef9e079..0642e371 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/DatabaseCoordinatorTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/DatabaseCoordinatorTest.kt @@ -9,6 +9,7 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.test.getAppContext import cash.z.ecc.fixture.DatabaseNameFixture import cash.z.ecc.fixture.DatabasePathFixture +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceTimeBy @@ -37,6 +38,7 @@ class DatabaseCoordinatorTest { // to test mutex implementation and correct DatabaseCoordinator function call result. @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun mutex_test() = runTest { var testResult: File? = null @@ -77,6 +79,7 @@ class DatabaseCoordinatorTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun database_cache_file_creation_test() = runTest { val directory = File(DatabasePathFixture.new()) val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_CACHE_NAME) @@ -92,6 +95,7 @@ class DatabaseCoordinatorTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun database_data_file_creation_test() = runTest { val directory = File(DatabasePathFixture.new()) val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_DATA_NAME) @@ -107,6 +111,7 @@ class DatabaseCoordinatorTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun database_transactions_file_creation_test() = runTest { val directory = File(DatabasePathFixture.new()) val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_PENDING_TRANSACTIONS_NAME) @@ -122,6 +127,7 @@ class DatabaseCoordinatorTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun database_files_move_test() = runTest { val parentFile = File( DatabasePathFixture.new( @@ -205,6 +211,7 @@ class DatabaseCoordinatorTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun delete_database_files_test() = runTest { val parentFile = File( DatabasePathFixture.new( diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/FileExtTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/FileExtTest.kt index 96ba5087..b7e599c2 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/FileExtTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/FileExtTest.kt @@ -5,6 +5,7 @@ import cash.z.ecc.android.sdk.internal.ext.createNewFileSuspend import cash.z.ecc.android.sdk.internal.ext.existsSuspend import cash.z.ecc.android.sdk.internal.ext.getSha1Hash import cash.z.ecc.android.sdk.test.getAppContext +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before @@ -25,6 +26,7 @@ class FileExtTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun check_empty_file_sha1_result() = runTest { testFile.apply { createNewFileSuspend() @@ -39,6 +41,7 @@ class FileExtTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun check_not_empty_file_sha1_result() = runTest { testFile.apply { createNewFileSuspend() diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SaplingParamToolBasicTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SaplingParamToolBasicTest.kt index fd564dc3..8aa2d836 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SaplingParamToolBasicTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SaplingParamToolBasicTest.kt @@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.internal.ext.listFilesSuspend import cash.z.ecc.android.sdk.test.getAppContext import cash.z.ecc.fixture.SaplingParamToolFixture import cash.z.ecc.fixture.SaplingParamsFixture +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Before @@ -32,6 +33,7 @@ class SaplingParamToolBasicTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun init_sapling_param_tool_test() = runTest { val spendSaplingParams = SaplingParamsFixture.new() val outputSaplingParams = SaplingParamsFixture.new( @@ -60,6 +62,7 @@ class SaplingParamToolBasicTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun init_and_get_params_destination_dir_test() = runTest { val destDir = SaplingParamTool.new(getAppContext()).properties.paramsDirectory @@ -73,6 +76,7 @@ class SaplingParamToolBasicTest { @Test @MediumTest + @OptIn(ExperimentalCoroutinesApi::class) fun move_files_from_legacy_destination_test() = runTest { SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY.mkdirs() val spendFile = File(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY, SaplingParamsFixture.SPEND_FILE_NAME) @@ -124,6 +128,7 @@ class SaplingParamToolBasicTest { @Test @MediumTest + @OptIn(ExperimentalCoroutinesApi::class) fun ensure_params_exception_thrown_test() = runTest { val saplingParamTool = SaplingParamTool( SaplingParamToolFixture.new( diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SaplingParamToolIntegrationTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SaplingParamToolIntegrationTest.kt index 0f7d5da3..03e56931 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SaplingParamToolIntegrationTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SaplingParamToolIntegrationTest.kt @@ -6,6 +6,7 @@ import cash.z.ecc.android.sdk.exception.TransactionEncoderException import cash.z.ecc.android.sdk.internal.ext.listFilesSuspend import cash.z.ecc.android.sdk.test.getAppContext import cash.z.ecc.fixture.SaplingParamsFixture +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Before @@ -146,6 +147,7 @@ class SaplingParamToolIntegrationTest { @Test @LargeTest + @OptIn(ExperimentalCoroutinesApi::class) fun fetch_params_uninitialized_test() = runTest { val saplingParamTool = SaplingParamTool.new(getAppContext()) @@ -160,6 +162,7 @@ class SaplingParamToolIntegrationTest { @Test @LargeTest + @OptIn(ExperimentalCoroutinesApi::class) fun fetch_params_incorrect_hash_test() = runTest { val saplingParamTool = SaplingParamTool.new(getAppContext()) @@ -178,6 +181,7 @@ class SaplingParamToolIntegrationTest { @Test @LargeTest + @OptIn(ExperimentalCoroutinesApi::class) fun fetch_params_incorrect_max_file_size_test() = runTest { val saplingParamTool = SaplingParamTool.new(getAppContext()) @@ -196,6 +200,7 @@ class SaplingParamToolIntegrationTest { @Test @LargeTest + @OptIn(ExperimentalCoroutinesApi::class) fun fetch_param_manual_recover_test_from_fetch_params_exception() = runTest { val saplingParamTool = SaplingParamTool.new(getAppContext()) @@ -225,6 +230,7 @@ class SaplingParamToolIntegrationTest { @Test @LargeTest + @OptIn(ExperimentalCoroutinesApi::class) fun fetch_param_manual_recover_test_from_validate_params_exception() = runTest { val saplingParamTool = SaplingParamTool.new(getAppContext()) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKeyTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKeyTest.kt index f8c27fb6..ca0131a4 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKeyTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKeyTest.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.model import androidx.test.filters.SmallTest import cash.z.ecc.android.sdk.fixture.WalletFixture +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.test.assertContentEquals @@ -10,6 +11,7 @@ import kotlin.test.assertEquals class UnifiedSpendingKeyTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun factory_copies_bytes() = runTest { val spendingKey = WalletFixture.getUnifiedSpendingKey() val expected = spendingKey.copyBytes().copyOf() @@ -23,6 +25,7 @@ class UnifiedSpendingKeyTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun get_copies_bytes() = runTest { val spendingKey = WalletFixture.getUnifiedSpendingKey() @@ -36,6 +39,7 @@ class UnifiedSpendingKeyTest { @Test @SmallTest + @OptIn(ExperimentalCoroutinesApi::class) fun toString_does_not_leak() = runTest { assertEquals( "UnifiedSpendingKey(account=Account(value=0))", diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/DerivationToolTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/DerivationToolTest.kt index 69980c77..bf82256e 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/DerivationToolTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/DerivationToolTest.kt @@ -3,12 +3,14 @@ package cash.z.ecc.android.sdk.tool import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.sdk.fixture.WalletFixture import cash.z.ecc.android.sdk.model.Account +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.test.assertContentEquals class DerivationToolTest { @Test + @OptIn(ExperimentalCoroutinesApi::class) fun create_spending_key_does_not_mutate_passed_bytes() = runTest { val bytesOne = Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy() val bytesTwo = Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy() diff --git a/sdk-lib/src/benchmark/java/cash/z/ecc/android/sdk/annotation/OpenForTesting.kt b/sdk-lib/src/benchmark/java/cash/z/ecc/android/sdk/annotation/OpenForTesting.kt new file mode 100644 index 00000000..05be1354 --- /dev/null +++ b/sdk-lib/src/benchmark/java/cash/z/ecc/android/sdk/annotation/OpenForTesting.kt @@ -0,0 +1,11 @@ +package cash.z.ecc.android.sdk.annotation + +/** + * Used in conjunction with the kotlin-allopen plugin to make any class with this annotation open for extension. + * Typically, we apply this to classes that we want to mock in androidTests because unit tests don't have this problem, + * it's only an issue with JUnit4 Instrumentation tests. This annotation is only leveraged in debug builds. + * + * Note: the counterpart to this annotation in the debug buildType applies the OpenClass annotation but here we do not. + */ +@Target(AnnotationTarget.CLASS) +annotation class OpenForTesting diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index 4f9c3db2..a51d19f5 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -68,7 +68,6 @@ import io.grpc.ManagedChannel import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async @@ -102,7 +101,6 @@ import kotlin.coroutines.EmptyCoroutineContext * @property processor saves the downloaded compact blocks to the cache and then scans those blocks for * data related to this wallet. */ -@FlowPreview @Suppress("TooManyFunctions") class SdkSynchronizer internal constructor( private val storage: DerivedDataRepository, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt index 9fcf7689..218ec140 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt @@ -19,6 +19,7 @@ import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.Mismatche import cash.z.ecc.android.sdk.exception.InitializeException import cash.z.ecc.android.sdk.exception.RustLayerException import cash.z.ecc.android.sdk.ext.BatchMetrics +import cash.z.ecc.android.sdk.ext.BenchmarkingExt import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk.DOWNLOAD_BATCH_SIZE import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL @@ -27,6 +28,7 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk.POLL_INTERVAL import cash.z.ecc.android.sdk.ext.ZcashSdk.RETRIES import cash.z.ecc.android.sdk.ext.ZcashSdk.REWIND_DISTANCE import cash.z.ecc.android.sdk.ext.ZcashSdk.SCAN_BATCH_SIZE +import cash.z.ecc.android.sdk.fixture.BlockRangeFixture import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader import cash.z.ecc.android.sdk.internal.ext.retryUpTo @@ -288,13 +290,26 @@ class CompactBlockProcessor internal constructor( setState(Scanned(currentInfo.lastScanRange)) BlockProcessingResult.NoBlocksToProcess } else { - downloadNewBlocks(currentInfo.lastDownloadRange) - val error = validateAndScanNewBlocks(currentInfo.lastScanRange) - if (error != BlockProcessingResult.Success) { - error + if (BenchmarkingExt.isBenchmarking()) { + // We inject a benchmark test blocks range at this point to process only a restricted range of blocks + // for a more reliable benchmark results. + val benchmarkBlockRange = BlockRangeFixture.new() + downloadNewBlocks(benchmarkBlockRange) + val error = validateAndScanNewBlocks(benchmarkBlockRange) + if (error != BlockProcessingResult.Success) { + error + } else { + enhanceTransactionDetails(benchmarkBlockRange) + } } else { - currentInfo.lastScanRange?.let { enhanceTransactionDetails(it) } - ?: BlockProcessingResult.NoBlocksToProcess + downloadNewBlocks(currentInfo.lastDownloadRange) + val error = validateAndScanNewBlocks(currentInfo.lastScanRange) + if (error != BlockProcessingResult.Success) { + error + } else { + currentInfo.lastScanRange?.let { enhanceTransactionDetails(it) } + ?: BlockProcessingResult.NoBlocksToProcess + } } } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BenchmarkingExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BenchmarkingExt.kt new file mode 100644 index 00000000..4e8e0dd9 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BenchmarkingExt.kt @@ -0,0 +1,9 @@ +package cash.z.ecc.android.sdk.ext + +import cash.z.ecc.android.sdk.BuildConfig + +object BenchmarkingExt { + private const val TARGET_BUILD_TYPE = "benchmark" // NON-NLS + + fun isBenchmarking(): Boolean = TARGET_BUILD_TYPE == BuildConfig.BUILD_TYPE +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/BlockRangeFixture.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/BlockRangeFixture.kt new file mode 100644 index 00000000..47cb92ab --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/BlockRangeFixture.kt @@ -0,0 +1,24 @@ +package cash.z.ecc.android.sdk.fixture + +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.ZcashNetwork + +object BlockRangeFixture { + + // Be aware that changing these bounds values in a broader range may result in a timeout reached in + // SyncBlockchainBenchmark. So if changing these, don't forget to align also the test timeout in + // waitForBalanceScreen() appropriately. + + @Suppress("MagicNumber") + private val BLOCK_HEIGHT_LOWER_BOUND = BlockHeight.new(ZcashNetwork.Mainnet, 1730001L) + + @Suppress("MagicNumber") + private val BLOCK_HEIGHT_UPPER_BOUND = BlockHeight.new(ZcashNetwork.Mainnet, 1730100L) + + fun new( + lowerBound: BlockHeight = BLOCK_HEIGHT_LOWER_BOUND, + upperBound: BlockHeight = BLOCK_HEIGHT_UPPER_BOUND + ): ClosedRange { + return lowerBound..upperBound + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/ext/android/ComputableFlow.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/ext/android/ComputableFlow.kt index c4550b84..b0cdcdb0 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/ext/android/ComputableFlow.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/ext/android/ComputableFlow.kt @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal.ext.android import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -23,6 +24,7 @@ abstract class ComputableFlow(dispatcher: CoroutineDispatcher = Dispatchers.I computationScope.launch { computationFlow.emit(compute()) } } + @OptIn(ExperimentalCoroutinesApi::class) fun cancel() { computationScope.cancel() computationFlow.resetReplayCache() diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt index 999e8bf9..b606aa8d 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt @@ -2,6 +2,8 @@ package cash.z.ecc.android.sdk.internal.service import android.content.Context import cash.z.ecc.android.sdk.annotation.OpenForTesting +import cash.z.ecc.android.sdk.ext.BenchmarkingExt +import cash.z.ecc.android.sdk.fixture.BlockRangeFixture import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.LightWalletEndpoint @@ -50,10 +52,16 @@ class LightWalletGrpcService private constructor( } override fun getLatestBlockHeight(): BlockHeight { - return BlockHeight( - requireChannel().createStub(singleRequestTimeout) - .getLatestBlock(Service.ChainSpec.newBuilder().build()).height - ) + return if (BenchmarkingExt.isBenchmarking()) { + // We inject a benchmark test blocks range at this point to process only a restricted range of blocks + // for a more reliable benchmark results. + BlockRangeFixture.new().endInclusive + } else { + BlockHeight( + requireChannel().createStub(singleRequestTimeout) + .getLatestBlock(Service.ChainSpec.newBuilder().build()).height + ) + } } override fun getServerInfo(): Service.LightdInfo { diff --git a/settings.gradle.kts b/settings.gradle.kts index d18c30ed..1d64f525 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,6 +27,7 @@ pluginManagement { id("com.android.application") version (androidGradlePluginVersion) apply (false) id("com.android.library") version (androidGradlePluginVersion) apply (false) + id("com.android.test") version (androidGradlePluginVersion) apply (false) id("com.github.ben-manes.versions") version (gradleVersionsPluginVersion) apply (false) id("com.google.devtools.ksp") version(kspVersion) apply (false) id("com.google.protobuf") version (protobufVersion) apply (false) @@ -74,10 +75,13 @@ dependencyResolutionManagement { val androidxMultidexVersion = extra["ANDROIDX_MULTIDEX_VERSION"].toString() val androidxNavigationVersion = extra["ANDROIDX_NAVIGATION_VERSION"].toString() val androidxPagingVersion = extra["ANDROIDX_PAGING_VERSION"].toString() + val androidxProfileInstallerVersion = extra["ANDROIDX_PROFILE_INSTALLER_VERSION"].toString() val androidxRoomVersion = extra["ANDROIDX_ROOM_VERSION"].toString() val androidxTestJunitVersion = extra["ANDROIDX_TEST_JUNIT_VERSION"].toString() - val androidxTestOrchestratorVersion = extra["ANDROIDX_ESPRESSO_VERSION"].toString() + val androidxTestMacrobenchmarkVersion = extra["ANDROIDX_TEST_MACROBENCHMARK_VERSION"].toString() + val androidxTestOrchestratorVersion = extra["ANDROIDX_TEST_ORCHESTRATOR_VERSION"].toString() val androidxTestVersion = extra["ANDROIDX_TEST_VERSION"].toString() + val androidxTracingVersion = extra["ANDROIDX_TRACING_VERSION"].toString() val androidxUiAutomatorVersion = extra["ANDROIDX_UI_AUTOMATOR_VERSION"].toString() val bip39Version = extra["BIP39_VERSION"].toString() val coroutinesOkhttpVersion = extra["COROUTINES_OKHTTP"].toString() @@ -125,6 +129,7 @@ dependencyResolutionManagement { library("androidx-navigation-fragment", "androidx.navigation:navigation-fragment-ktx:$androidxNavigationVersion") library("androidx-navigation-ui", "androidx.navigation:navigation-ui-ktx:$androidxNavigationVersion") library("androidx-paging", "androidx.paging:paging-runtime-ktx:$androidxPagingVersion") + library("androidx-profileinstaller", "androidx.profileinstaller:profileinstaller:$androidxProfileInstallerVersion") library("androidx-room-compiler", "androidx.room:room-compiler:$androidxRoomVersion") library("androidx-room-core", "androidx.room:room-ktx:$androidxRoomVersion") library("androidx-sqlite", "androidx.sqlite:sqlite-ktx:${androidxDatabaseVersion}") @@ -150,9 +155,11 @@ dependencyResolutionManagement { library("androidx-espresso-intents", "androidx.test.espresso:espresso-intents:$androidxEspressoVersion") library("androidx-test-core", "androidx.test:core:$androidxTestVersion") library("androidx-test-junit", "androidx.test.ext:junit:$androidxTestJunitVersion") + library("androidx-test-macrobenchmark", "androidx.benchmark:benchmark-macro-junit4:$androidxTestMacrobenchmarkVersion") library("androidx-test-runner", "androidx.test:runner:$androidxTestVersion") - library("androidx-testOrchestrator", "androidx.test:orchestrator:$androidxTestOrchestratorVersion") - library("androidx-uiAutomator", "androidx.test.uiautomator:uiautomator-v18:$androidxUiAutomatorVersion") + library("androidx-test-orchestrator", "androidx.test:orchestrator:$androidxTestOrchestratorVersion") + library("androidx-tracing", "androidx.tracing:tracing:$androidxTracingVersion") + library("androidx-uiAutomator", "androidx.test.uiautomator:uiautomator:$androidxUiAutomatorVersion") library("coroutines-okhttp", "ru.gildor.coroutines:kotlin-coroutines-okhttp:$coroutinesOkhttpVersion") library("grpc-testing", "io.grpc:grpc-testing:$grpcVersion") library("junit-api", "org.junit.jupiter:junit-jupiter-api:$junitVersion") @@ -203,4 +210,5 @@ includeBuild("build-conventions") include("darkside-test-lib") include("sdk-lib") -include("demo-app") \ No newline at end of file +include("demo-app") +include("demo-app-benchmark-test") \ No newline at end of file