Skip to content

[Baseline Profiles] Test Generation & Benchmarking via FTL (Without Login) #14256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
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
119 changes: 18 additions & 101 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
@@ -1,108 +1,25 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
---

# Nodes with values to reuse in the pipeline.
common_params:
# Common plugin settings to use with the `plugins` key.
- &test_collector_common_params
files: "WooCommerce/build/buildkite-test-analytics/*.xml"
format: "junit"

agents:
queue: "android"

steps:
- label: Gradle Wrapper Validation
command: validate_gradle_wrapper
agents:
queue: linter

# Wait for Gradle Wrapper to be validated before running any other jobs
- wait

########################################
- group: "🕵️ Linters"
steps:

- label: "☢️ Danger - PR Check"
command: danger
key: danger
if: "build.pull_request.id != null"
retry:
manual:
permit_on_passed: true
agents:
queue: "linter"

- label: "detekt"
command: |
if .buildkite/commands/should-skip-job.sh --job-type validation; then
exit 0
fi
./gradlew detektAll
plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/detekt/detekt.html"

- label: "lint"
command: .buildkite/commands/lint.sh
plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/lint-results*.*"

- label: "Dependency Tree Diff"
command: comment_with_dependency_diff 'woocommerce' 'vanillaReleaseRuntimeClasspath'
if: build.pull_request.id != null
plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/diff/*"

- label: "Merged Manifest Diff"
command: ".buildkite/commands/diff-merged-manifest.sh vanillaRelease"
if: build.pull_request.id != null
plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/diff_manifest/**/**/*"

########################################
- group: "🛠 Prototype Builds"
steps:

- label: "🛠 Prototype Build: Mobile App"
command: ".buildkite/commands/prototype-build.sh WooCommerce"
if: build.pull_request.id != null
plugins: [$CI_TOOLKIT]

- label: "🛠 Prototype Build: Wear App"
command: ".buildkite/commands/prototype-build.sh WooCommerce-Wear"
if: build.pull_request.id != null
plugins: [$CI_TOOLKIT]

########################################
- group: "🔬 Tests"
steps:

- label: "Unit tests"
command: .buildkite/commands/run-unit-tests.sh
plugins:
- $CI_TOOLKIT
- $TEST_COLLECTOR :
<<: *test_collector_common_params
api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_UNIT_TESTS"
artifact_paths:
- "**/build/test-results/merged-test-results.xml"

- label: "Instrumented tests"
command: .buildkite/commands/run-instrumented-tests.sh
plugins:
- $CI_TOOLKIT
- $TEST_COLLECTOR :
<<: *test_collector_common_params
api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_INSTRUMENTED_TESTS"
artifact_paths:
- "**/build/instrumented-tests/**/*"


- label: "🐘 Populate Gradle build cache"
command: .buildkite/commands/gradle-cache-build.sh
plugins: [$CI_TOOLKIT]
- label: "Generate & Benchmark Baseline Profile"
key: "generate-baseline-profile"
command: |
echo "--- :rubygems: Setting up Gems"
install_gems

echo "--- :closed_lock_with_key: Installing Secrets"
bundle exec fastlane run configure_apply

echo "--- :hammer_and_wrench: Generate Baseline Profile"
./gradlew :WooCommerce:generateVanillaReleaseBaselineProfile

echo "--- :hammer_and_wrench: Benchmark Baseline Profile"
./gradlew :baselineprofile:ftlDeviceTokay35VanillaBenchmarkReleaseAndroidTest
plugins: [ $CI_TOOLKIT ]
artifact_paths:
- "**/src/vanillaRelease/generated/baselineProfiles/*"
- "**/build/outputs/androidTest-results/managedDevice/benchmarkrelease/flavors/vanilla/**/results/**/artifacts/storage/emulated/0/Android/media/**/com.woocommerce.android.baselineprofile-benchmarkData.json"
7 changes: 7 additions & 0 deletions WooCommerce/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.google.dagger.hilt)
alias(libs.plugins.sentry)
alias(libs.plugins.androidx.baselineprofile)
alias(libs.plugins.androidx.navigation.safeargs)
alias(libs.plugins.google.services)
alias(libs.plugins.ksp)
Expand Down Expand Up @@ -173,6 +174,10 @@ android {
animationsDisabled = true
}

baselineProfile {
dexLayoutOptimization = true
}

signingConfigs {
if (["uploadStoreFile", "uploadStorePassword", "uploadKeyAlias", "uploadKeyPassword"].count { !secretProperties.containsKey(it) } == 0) {
logger.info("App signing properties found in secrets.properties, configuring signing for release builds.")
Expand Down Expand Up @@ -276,6 +281,8 @@ dependencies {
implementation(libs.androidx.hilt.navigation.fragment)
implementation(libs.androidx.hilt.common)
implementation(libs.androidx.hilt.work)
implementation libs.androidx.profileinstaller
baselineProfile project(':baselineprofile')

ksp(libs.androidx.hilt.compiler)
ksp(libs.google.dagger.hilt.compiler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package com.woocommerce.android.ui.main

import NotificationsPermissionCard
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ProgressDialog
import android.content.Intent
Expand Down Expand Up @@ -32,11 +33,15 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.NavHostFragment
import androidx.profileinstaller.ProfileVerifier
import androidx.profileinstaller.ProfileVerifier.CompilationStatus
import androidx.work.await
import com.automattic.android.tracks.crashlogging.CrashLogging
import com.google.android.material.appbar.AppBarLayout
import com.woocommerce.android.AppPrefs
Expand Down Expand Up @@ -109,11 +114,15 @@ import com.woocommerce.android.util.ChromeCustomTabUtils
import com.woocommerce.android.util.PackageUtils
import com.woocommerce.android.util.WooAnimUtils.Duration
import com.woocommerce.android.util.WooAnimUtils.animateBottomBar
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.util.WooPermissionUtils
import com.woocommerce.android.viewmodel.MultiLiveEvent
import com.woocommerce.android.widgets.AppRatingDialog
import com.woocommerce.android.widgets.DisabledAppBarLayoutBehavior
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.wordpress.android.login.LoginAnalyticsListener
import org.wordpress.android.login.LoginMode
import org.wordpress.android.util.NetworkUtils
Expand Down Expand Up @@ -389,6 +398,7 @@ class MainActivity :

override fun onResume() {
super.onResume()
lifecycleScope.launch { logCompilationStatus() }
AnalyticsTracker.trackViewShown(this)

// Track if App was opened from a widget
Expand All @@ -404,6 +414,49 @@ class MainActivity :
viewModel.showFeatureAnnouncementIfNeeded()
}

/**
* This method is used for debugging baseline profile purpose based on profile verifier compilation status.
*
* @see <a href="https://developer.android.com/topic/performance/baselineprofiles/debug-baseline-profiles#installation_issues</a> for more details.
*/
@Suppress("MaxLineLength")
@SuppressLint("RestrictedApi")
private suspend fun logCompilationStatus() {
withContext(Dispatchers.IO) {
val status = ProfileVerifier.getCompilationStatusAsync().await()
when (status.profileInstallResultCode) {
CompilationStatus.RESULT_CODE_NO_PROFILE_INSTALLED ->
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: Baseline Profile not found")

CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE ->
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: Compiled with profile")

CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION ->
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: Enqueued for compilation")

CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: App was installed through Play store")

CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST ->
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: PackageName not found")

CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ ->
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: Cache file exists but cannot be read")

CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE ->
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: Can't write cache file")

CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION ->
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: Enqueued for compilation")
Copy link
Preview

Copilot AI Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log message for RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION appears misleading. It should probably log something like 'Unsupported API version' instead of 'Enqueued for compilation'.

Suggested change
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: Enqueued for compilation")
WooLog.d(WooLog.T.BASELINE_PROFILES, "ProfileInstaller: Unsupported API version")

Copilot uses AI. Check for mistakes.


else -> WooLog.d(
WooLog.T.BASELINE_PROFILES,
"ProfileInstaller: Profile not compiled or enqueued [Result Code: ${status.profileInstallResultCode}]"
)
}
}
}

override fun onPause() {
binding.appBarLayout.removeOnOffsetChangedListener(appBarOffsetListener)
super.onPause()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ object WooLog {
GOOGLE_ADS,
POS,
CUSTOM_FIELDS,
SHIPPING_LABELS
SHIPPING_LABELS,
BASELINE_PROFILES,
}

// Breaking convention to be consistent with org.wordpress.android.util.AppLog
Expand Down
1 change: 1 addition & 0 deletions baselineprofile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
85 changes: 85 additions & 0 deletions baselineprofile/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import com.android.build.api.dsl.ManagedVirtualDevice

plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.androidx.baselineprofile)
alias(libs.plugins.firebase.testlab)
}

android {
namespace 'com.woocommerce.android.baselineprofile'
compileSdk gradle.ext.compileSdkVersion

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

defaultConfig {
minSdk 28 // Cant use current "gradle.ext.minSdkVersion", which points to 26, because various calls requires API level 28.
targetSdk gradle.ext.targetSdkVersion

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

targetProjectPath = ":WooCommerce"

flavorDimensions += ["buildType"]
productFlavors {
vanilla { dimension = "buildType" }
}

// This code creates the gradle managed device used to generate baseline profiles.
// To use GMD please invoke generation through the command line:
// ./gradlew :WooCommerce:generateVanillaReleaseBaselineProfile
testOptions.managedDevices.devices {
pixel6Api34(ManagedVirtualDevice) {
device = "Pixel 6"
apiLevel = 34
systemImageSource = "google"
}
}
}

def directory = "/storage/emulated/0/Android/media/${android.namespace}"
firebaseTestLab {
serviceAccountCredentials = rootProject.file(".configure-files/firebase.secrets.json")
managedDevices {
"ftlDeviceTokay35" {
device = "tokay"
apiLevel = 35
}
}
testOptions {
results {
directoriesToPull.addAll(directory.toString())
recordVideo = true
performanceMetrics = true
}
}
}

// This is the configuration block for the Baseline Profile plugin.
// You can specify to run the generators on a managed devices or connected devices.
baselineProfile {
managedDevices += "ftlDeviceTokay35"
useConnectedDevices = false
}

dependencies {
implementation libs.androidx.test.ext.junit
implementation libs.androidx.test.espresso.core
implementation libs.androidx.test.uiautomator
implementation libs.androidx.benchmark.macro.junit4
}

androidComponents {
onVariants(selector().all()) { v ->
def artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
v.instrumentationRunnerArguments.put(
"targetAppId",
v.testedApks.map { artifactsLoader.load(it)?.applicationId }
)
}
}
Loading