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/react-native/gradle/libs.versions.toml b/packages/react-native/gradle/libs.versions.toml index d0a15530a771..e936fdc45801 100644 --- a/packages/react-native/gradle/libs.versions.toml +++ b/packages/react-native/gradle/libs.versions.toml @@ -10,12 +10,16 @@ agp = "8.8.0" 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 000000000000..5fda7717f578 --- /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 000000000000..882c04ff6949 --- /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 000000000000..232e9c07643d --- /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 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..74dc723aa0fd --- /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 000000000000..cf49d461fbcd --- /dev/null +++ b/packages/rn-tester/android/app/benchmark/src/main/java/com/facebook/react/uiapp/benchmark/RNTesterStartupBenchmark.kt @@ -0,0 +1,46 @@ +/* + * 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 d7d263c303f8..a4db0dbb417d 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 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/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 e1394a77e0fb..13ed22d2e6cd 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 2fe4530c3e27..738c20ca228a 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,12 @@ 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 +114,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 000000000000..bd092b41b7e4 --- /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 000000000000..113af05c4d01 --- /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.SimpleViewManager +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 e41e6cd640d3..ef69b0b2fc45 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 4a6e16a5aeaa..ce06b908ac73 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/")