From b614c966ab18b086c8bbf789ccc8b455d3ffaeb4 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 4 Mar 2025 12:56:07 -0800 Subject: [PATCH] Setup a Macrobenchmark for RNTester (#49486) Summary: Benchmark to test changes from https://github.com/facebook/react-native/issues/49449 Might be nice to have some version of this in the repo. ## Changelog: [INTERNAL] [ADDED] - Setup a Macrobenchmark for RNTester Pull Request resolved: https://github.com/facebook/react-native/pull/49486 Test Plan: ### Methodology Picked various JS file from websites (facebook, instagram) to artificially grow RN tester bundle somewhat realistically. The files are required lazily from a button press callback to simulate the code being included, but not executed, as it would be in a large app that uses lazy requires for the different screens. I've also made the RN tester screens lazy so all their code is not loaded initially. This is more representative of real apps. Note this is implemented in a hacky way just for the purpose of this test. It would actually be nice to implement this properly. The tests were made using low end device Samsung Galaxy A03s. ### Compression ON with 10.5 mb bundle #### Peak allocated memory 60.9 mb #### ReactInstance.loadJSBundler 148.64 ms #### Benchmark timeToFullDisplayMs min 1,825.0, median 1,911.1, max 1,994.8 timeToInitialDisplayMs min 834.9, median 860.9, max 903.9 #### APK Size: 22.9 mb Download size: 14.5 mb ### Compression OFF with 10.5 mb bundle #### Peak allocated memory 51.5 mb #### ReactInstance.loadJSBundler 946 us #### Benchmark timeToFullDisplayMs min 1,752.8, median 1,827.2, max 1,977.5 timeToInitialDisplayMs min 837.7, median 881.3, max 937.2 #### APK Size: 28 mb Download size: 14.5 mb Reviewed By: rshest Differential Revision: D70002286 Pulled By: cortinico fbshipit-source-id: 436597f439ba244649373870c1facefdb12297d9 --- build.gradle.kts | 1 + .../react-native/gradle/libs.versions.toml | 17 +- .../ReportFullyDrawnView.android.js | 13 ++ .../ReportFullyDrawnView.js | 13 ++ .../ReportFullyDrawnViewNativeComponent.js | 24 +++ .../android/app/benchmark/.gitignore | 1 + .../android/app/benchmark/build.gradle.kts | 52 ++++++ .../benchmark/RNTesterStartupBenchmark.kt | 44 +++++ .../rn-tester/android/app/build.gradle.kts | 1 + .../android/app/src/main/AndroidManifest.xml | 169 +++++++++--------- .../facebook/react/uiapp/RNTesterActivity.kt | 3 + .../react/uiapp/RNTesterApplication.kt | 9 +- .../uiapp/component/ReportFullyDrawnView.kt | 28 +++ .../component/ReportFullyDrawnViewManager.kt | 36 ++++ packages/rn-tester/js/RNTesterAppShared.js | 2 + settings.gradle.kts | 3 +- 16 files changed, 330 insertions(+), 86 deletions(-) create mode 100644 packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnView.android.js create mode 100644 packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnView.js create mode 100644 packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnViewNativeComponent.js create mode 100644 packages/rn-tester/android/app/benchmark/.gitignore create mode 100644 packages/rn-tester/android/app/benchmark/build.gradle.kts create mode 100644 packages/rn-tester/android/app/benchmark/src/main/java/com/facebook/react/uiapp/benchmark/RNTesterStartupBenchmark.kt create mode 100644 packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/ReportFullyDrawnView.kt create mode 100644 packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/ReportFullyDrawnViewManager.kt diff --git a/build.gradle.kts b/build.gradle.kts index e7121937353..0227a891c3b 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/react-native/gradle/libs.versions.toml b/packages/react-native/gradle/libs.versions.toml index 171a75b8c5e..d377468a345 100644 --- a/packages/react-native/gradle/libs.versions.toml +++ b/packages/react-native/gradle/libs.versions.toml @@ -10,12 +10,16 @@ agp = "8.8.2" androidx-annotation = "1.6.0" androidx-appcompat = "1.7.0" androidx-autofill = "1.1.0" +androidx-benchmark-macro-junit4 = "1.3.3" +androidx-profileinstaller = "1.4.1" androidx-swiperefreshlayout = "1.1.0" androidx-test = "1.5.0" +androidx-test-junit = "1.2.1" androidx-tracing = "1.1.0" assertj = "3.21.0" binary-compatibility-validator = "0.13.2" download = "5.4.0" +espresso-core = "3.6.1" fbjni = "0.7.0" fresco = "3.6.0" infer-annotation = "0.18.0" @@ -32,6 +36,7 @@ okhttp = "4.9.2" okio = "2.9.0" robolectric = "4.9.2" soloader = "0.12.1" +uiautomator = "2.3.0" xstream = "1.4.20" yoga-proguard-annotations = "1.19.0" # Native Dependencies @@ -44,14 +49,19 @@ glog="0.3.5" gtest="1.12.1" [libraries] +androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-appcompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "androidx-appcompat" } -androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } androidx-autofill = { module = "androidx.autofill:autofill", version.ref = "androidx-autofill" } +androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidx-benchmark-macro-junit4" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } +androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidx-profileinstaller" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" } -androidx-tracing = { module = "androidx.tracing:tracing", version.ref = "androidx-tracing" } -androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" } +androidx-tracing = { module = "androidx.tracing:tracing", version.ref = "androidx-tracing" } +androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } fbjni = { module = "com.facebook.fbjni:fbjni", version.ref = "fbjni" } fresco = { module = "com.facebook.fresco:fresco", version.ref = "fresco" } @@ -84,3 +94,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/ReportFullyDrawnView/ReportFullyDrawnView.android.js b/packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnView.android.js new file mode 100644 index 00000000000..5fda7717f57 --- /dev/null +++ b/packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnView.android.js @@ -0,0 +1,13 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import ReportFullyDrawnViewNativeComponent from './ReportFullyDrawnViewNativeComponent'; + +export default ReportFullyDrawnViewNativeComponent; diff --git a/packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnView.js b/packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnView.js new file mode 100644 index 00000000000..882c04ff694 --- /dev/null +++ b/packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnView.js @@ -0,0 +1,13 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import {View} from 'react-native'; + +export default View; diff --git a/packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnViewNativeComponent.js b/packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnViewNativeComponent.js new file mode 100644 index 00000000000..232e9c07643 --- /dev/null +++ b/packages/rn-tester/ReportFullyDrawnView/ReportFullyDrawnViewNativeComponent.js @@ -0,0 +1,24 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type NativeProps = $ReadOnly<{ + ...ViewProps, +}>; + +export type ReportFullyDrawnViewType = HostComponent; + +export default (codegenNativeComponent( + 'RNTReportFullyDrawnView', +): ReportFullyDrawnViewType); diff --git a/packages/rn-tester/android/app/benchmark/.gitignore b/packages/rn-tester/android/app/benchmark/.gitignore new file mode 100644 index 00000000000..796b96d1c40 --- /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 00000000000..74dc723aa0f --- /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 = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + + 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/java/com/facebook/react/uiapp/benchmark/RNTesterStartupBenchmark.kt b/packages/rn-tester/android/app/benchmark/src/main/java/com/facebook/react/uiapp/benchmark/RNTesterStartupBenchmark.kt new file mode 100644 index 00000000000..bc3c2fb22c6 --- /dev/null +++ b/packages/rn-tester/android/app/benchmark/src/main/java/com/facebook/react/uiapp/benchmark/RNTesterStartupBenchmark.kt @@ -0,0 +1,44 @@ +/* + * 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.facebook.react.uiapp.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 androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * RNTester benchmarks. + * + * Run this benchmark from Android Studio to see startup measurements, and captured system traces + * for investigating performance. + */ +@RunWith(AndroidJUnit4::class) +class RNTesterStartupBenchmark { + @get:Rule val benchmarkRule = MacrobenchmarkRule() + + @Test + fun startup() = + benchmarkRule.measureRepeated( + packageName = "com.facebook.react.uiapp", + metrics = listOf(StartupTimingMetric()), + iterations = 10, + startupMode = StartupMode.COLD, + setupBlock = { pressHome() }) { + startActivityAndWait() + + // Waits for an element that corresponds to fully drawn state + device.wait(Until.hasObject(By.text("Components")), 10_000) + device.waitForIdle() + } +} diff --git a/packages/rn-tester/android/app/build.gradle.kts b/packages/rn-tester/android/app/build.gradle.kts index d7d263c303f..a4db0dbb417 100644 --- a/packages/rn-tester/android/app/build.gradle.kts +++ b/packages/rn-tester/android/app/build.gradle.kts @@ -165,6 +165,7 @@ dependencies { "jscImplementation"(jscFlavor) testImplementation(libs.junit) + implementation(libs.androidx.profileinstaller) } android { diff --git a/packages/rn-tester/android/app/src/main/AndroidManifest.xml b/packages/rn-tester/android/app/src/main/AndroidManifest.xml index 720b58f7967..0783a82713f 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/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt index e1394a77e0f..13ed22d2e6c 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt @@ -45,6 +45,9 @@ internal class RNTesterActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + fullyDrawnReporter.addReporter() + // set background color so it will show below transparent system bars on forced edge-to-edge this.window?.setBackgroundDrawable(ColorDrawable(Color.BLACK)) // register insets listener to update margins on the ReactRootView to avoid overlap w/ system diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt index 2fe4530c3e2..f4684656076 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt @@ -30,6 +30,7 @@ import com.facebook.react.shell.MainReactPackage import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.react.uiapp.component.MyLegacyViewManager import com.facebook.react.uiapp.component.MyNativeViewManager +import com.facebook.react.uiapp.component.ReportFullyDrawnViewManager import com.facebook.react.uimanager.ReactShadowNode import com.facebook.react.uimanager.ViewManager import com.facebook.soloader.SoLoader @@ -99,12 +100,15 @@ internal class RNTesterApplication : Application(), ReactApplication { ): List = emptyList() override fun getViewManagerNames(reactContext: ReactApplicationContext) = - listOf("RNTMyNativeView", "RNTMyLegacyNativeView") + listOf("RNTMyNativeView", "RNTMyLegacyNativeView", "RNTReportFullyDrawnView") override fun createViewManagers( reactContext: ReactApplicationContext ): List> = - listOf(MyNativeViewManager(), MyLegacyViewManager(reactContext)) + listOf( + MyNativeViewManager(), + MyLegacyViewManager(reactContext), + ReportFullyDrawnViewManager()) override fun createViewManager( reactContext: ReactApplicationContext, @@ -113,6 +117,7 @@ internal class RNTesterApplication : Application(), ReactApplication { when (viewManagerName) { "RNTMyNativeView" -> MyNativeViewManager() "RNTMyLegacyNativeView" -> MyLegacyViewManager(reactContext) + "RNTReportFullyDrawnView" -> ReportFullyDrawnViewManager() else -> null } }) diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/ReportFullyDrawnView.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/ReportFullyDrawnView.kt new file mode 100644 index 00000000000..bd092b41b7e --- /dev/null +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/ReportFullyDrawnView.kt @@ -0,0 +1,28 @@ +/* + * 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.facebook.react.uiapp.component + +import androidx.appcompat.app.AppCompatActivity +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.views.view.ReactViewGroup + +internal class ReportFullyDrawnView(context: ThemedReactContext) : ReactViewGroup(context) { + private val reactApplicationContext = context.reactApplicationContext + private var didReportFullyDrawn = false + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + + if (!didReportFullyDrawn) { + didReportFullyDrawn = true + + val activity = reactApplicationContext.currentActivity as? AppCompatActivity + activity?.fullyDrawnReporter?.removeReporter() + } + } +} diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/ReportFullyDrawnViewManager.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/ReportFullyDrawnViewManager.kt new file mode 100644 index 00000000000..44b9b83a1c1 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/ReportFullyDrawnViewManager.kt @@ -0,0 +1,36 @@ +/* + * 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.facebook.react.uiapp.component + +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.RNTReportFullyDrawnViewManagerDelegate +import com.facebook.react.viewmanagers.RNTReportFullyDrawnViewManagerInterface + +/** View manager for ReportFullyDrawnView components. */ +@ReactModule(name = ReportFullyDrawnViewManager.REACT_CLASS) +internal class ReportFullyDrawnViewManager : + ViewGroupManager(), + RNTReportFullyDrawnViewManagerInterface { + + companion object { + const val REACT_CLASS = "RNTReportFullyDrawnView" + } + + private val delegate: ViewManagerDelegate = + RNTReportFullyDrawnViewManagerDelegate(this) + + override fun getDelegate(): ViewManagerDelegate = delegate + + override fun getName(): String = REACT_CLASS + + override fun createViewInstance(reactContext: ThemedReactContext): ReportFullyDrawnView = + ReportFullyDrawnView(reactContext) +} diff --git a/packages/rn-tester/js/RNTesterAppShared.js b/packages/rn-tester/js/RNTesterAppShared.js index e41e6cd640d..ef69b0b2fc4 100644 --- a/packages/rn-tester/js/RNTesterAppShared.js +++ b/packages/rn-tester/js/RNTesterAppShared.js @@ -26,6 +26,7 @@ import { getExamplesListWithRecentlyUsed, initialNavigationState, } from './utils/testerStateUtils'; +import ReportFullyDrawnView from '../ReportFullyDrawnView/ReportFullyDrawnView'; import * as React from 'react'; import { BackHandler, @@ -298,6 +299,7 @@ const RNTesterApp = ({ handleNavBarPress={handleNavBarPress} /> + ); }; diff --git a/settings.gradle.kts b/settings.gradle.kts index 4a6e16a5aea..ce06b908ac7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,7 +18,8 @@ include( ":packages:react-native:ReactAndroid", ":packages:react-native:ReactAndroid:hermes-engine", ":packages:react-native:ReactAndroid:external-artifacts", - ":packages:rn-tester:android:app") + ":packages:rn-tester:android:app", + ":packages:rn-tester:android:app:benchmark") includeBuild("packages/gradle-plugin/")