diff --git a/build.gradle.kts b/build.gradle.kts index e71219373530..0227a891c3b5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ plugins { alias(libs.plugins.download) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.binary.compatibility.validator) apply true + alias(libs.plugins.android.test) apply false } val reactAndroidProperties = java.util.Properties() diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt index b67e8826b339..a06c5b5d41a2 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt @@ -76,6 +76,14 @@ abstract class ReactExtension @Inject constructor(val project: Project) { val bundleAssetName: Property = objects.property(String::class.java).convention("index.android.bundle") + /** + * Whether the Bundle Asset should be compressed when packaged into a `.apk`, or not. Disabling + * compression for the `.bundle` allows it to be directly memory-mapped to RAM, hence improving + * startup time - at the cost of a larger resulting `.apk` size. + */ + val enableBundleCompression: Property = + objects.property(Boolean::class.java).convention(false) + /** * Toggles the .so Cleanup step. If enabled, we will clean up all the unnecessary files before the * bundle task. If disabled, the developers will have to manually cleanup the files. Default: true diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index 9cf504539e2e..3ab7aabef971 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -7,6 +7,7 @@ package com.facebook.react +import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.internal.tasks.factory.dependsOn import com.facebook.react.internal.PrivateReactExtension @@ -81,6 +82,7 @@ class ReactPlugin : Plugin { } configureAutolinking(project, extension) configureCodegen(project, extension, rootExtension, isLibrary = false) + configureResources(project, extension) } // Library Only Configuration @@ -110,6 +112,17 @@ class ReactPlugin : Plugin { } } + /** This function configures Android resources - in this case just the bundle */ + private fun configureResources(project: Project, reactExtension: ReactExtension) { + if (!reactExtension.enableBundleCompression.get()) { + // Bundle should not be compressed; add it to noCompress blacklist. + val bundleFileName = reactExtension.bundleAssetName.get() + val bundleFileExtension = bundleFileName.substringAfterLast('.', "") + val android = project.extensions.getByType(ApplicationExtension::class.java) + android.androidResources.noCompress.add(bundleFileExtension) + } + } + /** This function sets up `react-native-codegen` in our Gradle plugin. */ @Suppress("UnstableApiUsage") private fun configureCodegen( diff --git a/packages/react-native/gradle/libs.versions.toml b/packages/react-native/gradle/libs.versions.toml index 6216e087a713..dbae5d4284a4 100644 --- a/packages/react-native/gradle/libs.versions.toml +++ b/packages/react-native/gradle/libs.versions.toml @@ -42,6 +42,10 @@ fmt="11.0.2" folly="2024.11.18.00" glog="0.3.5" gtest="1.12.1" +junitVersion = "1.2.1" +espressoCore = "3.6.1" +uiautomator = "2.3.0" +benchmarkMacroJunit4 = "1.3.3" [libraries] androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } @@ -76,6 +80,10 @@ mockito = {module = "org.mockito:mockito-inline", version.ref = "mockito" } mockito-kotlin = {module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" } robolectric = {module = "org.robolectric:robolectric", version.ref = "robolectric" } thoughtworks = {module = "com.thoughtworks.xstream:xstream", version.ref = "xstream" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } +androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -84,3 +92,4 @@ download = { id = "de.undercouch.download", version.ref = "download" } nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus-publish" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } +android-test = { id = "com.android.test", version.ref = "agp" } diff --git a/packages/rn-tester/android/app/benchmark/.gitignore b/packages/rn-tester/android/app/benchmark/.gitignore new file mode 100644 index 000000000000..796b96d1c402 --- /dev/null +++ b/packages/rn-tester/android/app/benchmark/.gitignore @@ -0,0 +1 @@ +/build diff --git a/packages/rn-tester/android/app/benchmark/build.gradle.kts b/packages/rn-tester/android/app/benchmark/build.gradle.kts new file mode 100644 index 000000000000..d52ac7196534 --- /dev/null +++ b/packages/rn-tester/android/app/benchmark/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +plugins { + alias(libs.plugins.android.test) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.example.benchmark" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + targetSdk = 35 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + // This benchmark buildType is used for benchmarking, and should function like your + // release build (for example, with minification on). It"s signed with a debug key + // for easy local/CI testing. + create("benchmark") { + isDebuggable = true + signingConfig = getByName("debug").signingConfig + matchingFallbacks += listOf("release") + } + } + + flavorDimensions += listOf("vm") + productFlavors { + create("hermes") { dimension = "vm" } + create("jsc") { dimension = "vm" } + } + + targetProjectPath = ":packages:rn-tester:android:app" + experimentalProperties["android.experimental.self-instrumenting"] = true +} + +dependencies { + implementation(libs.androidx.junit) + implementation(libs.androidx.espresso.core) + implementation(libs.androidx.uiautomator) + implementation(libs.androidx.benchmark.macro.junit4) +} + +androidComponents { beforeVariants(selector().all()) { it.enable = it.buildType == "benchmark" } } diff --git a/packages/rn-tester/android/app/benchmark/src/main/AndroidManifest.xml b/packages/rn-tester/android/app/benchmark/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..227314eeb7de --- /dev/null +++ b/packages/rn-tester/android/app/benchmark/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/rn-tester/android/app/benchmark/src/main/java/com/example/benchmark/ExampleStartupBenchmark.kt b/packages/rn-tester/android/app/benchmark/src/main/java/com/example/benchmark/ExampleStartupBenchmark.kt new file mode 100644 index 000000000000..97ead405a39a --- /dev/null +++ b/packages/rn-tester/android/app/benchmark/src/main/java/com/example/benchmark/ExampleStartupBenchmark.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.example.benchmark + +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * This is an example startup benchmark. + * + * It navigates to the device's home screen, and launches the default activity. + * + * Before running this benchmark: + * 1) switch your app's active build variant in the Studio (affects Studio runs only) + * 2) add `` to your app's manifest, within the `` + * tag + * + * Run this benchmark from Studio to see startup measurements, and captured system traces for + * investigating your app's performance. + */ +@RunWith(AndroidJUnit4::class) +class ExampleStartupBenchmark { + @get:Rule val benchmarkRule = MacrobenchmarkRule() + + @Test + fun startup() = + benchmarkRule.measureRepeated( + packageName = "com.facebook.react.uiapp", + metrics = listOf(StartupTimingMetric()), + iterations = 10, + startupMode = StartupMode.COLD) { + pressHome() + startActivityAndWait() + } +} diff --git a/packages/rn-tester/android/app/build.gradle.kts b/packages/rn-tester/android/app/build.gradle.kts index d7d263c303f8..b45f0a177989 100644 --- a/packages/rn-tester/android/app/build.gradle.kts +++ b/packages/rn-tester/android/app/build.gradle.kts @@ -62,7 +62,9 @@ react { /* Hermes Commands */ // The hermes compiler command to run. By default it is 'hermesc' hermesCommand = "$reactNativeDirPath/ReactAndroid/hermes-engine/build/hermes/bin/hermesc" - enableHermesOnlyInVariants = listOf("hermesDebug", "hermesRelease") + enableHermesOnlyInVariants = listOf("hermesDebug", "hermesRelease", "hermesBenchmark") + + enableBundleCompression.set(false) autolinkLibrariesWithApp() } @@ -142,6 +144,11 @@ android { proguardFiles(getDefaultProguardFile("proguard-android.txt")) signingConfig = signingConfigs.getByName("debug") } + create("benchmark") { + initWith(buildTypes.getByName("release")) + matchingFallbacks += listOf("release") + isDebuggable = false + } } sourceSets.named("main") { // SampleTurboModule. @@ -165,6 +172,7 @@ dependencies { "jscImplementation"(jscFlavor) testImplementation(libs.junit) + implementation("androidx.profileinstaller:profileinstaller:1.4.1") } android { diff --git a/packages/rn-tester/android/app/src/main/AndroidManifest.xml b/packages/rn-tester/android/app/src/main/AndroidManifest.xml index 720b58f79675..0783a82713fb 100644 --- a/packages/rn-tester/android/app/src/main/AndroidManifest.xml +++ b/packages/rn-tester/android/app/src/main/AndroidManifest.xml @@ -1,88 +1,97 @@ - + - - - - + + + + - - - - - - + + + + + + + + + - - - - + + + + - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/AppTheme"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 4a6e16a5aeaa..8d22d7f38c63 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,3 +39,5 @@ configure { autolinkLibrariesFromCommand( workingDirectory = file("packages/rn-tester/"), lockFiles = files("yarn.lock")) } + +include(":packages:rn-tester:android:app:benchmark")