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
This commit is contained in:
Janic Duplessis
2025-03-04 12:56:07 -08:00
committed by Facebook GitHub Bot
parent f94dbb30ee
commit b614c966ab
16 changed files with 330 additions and 86 deletions
+1
View File
@@ -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()
@@ -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" }
@@ -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;
@@ -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;
@@ -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<ViewProps>;
export default (codegenNativeComponent<NativeProps>(
'RNTReportFullyDrawnView',
): ReportFullyDrawnViewType);
@@ -0,0 +1 @@
/build
@@ -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" } }
@@ -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()
}
}
@@ -165,6 +165,7 @@ dependencies {
"jscImplementation"(jscFlavor)
testImplementation(libs.junit)
implementation(libs.androidx.profileinstaller)
}
android {
@@ -1,88 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Just to show permissions example -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--Just to show permissions example-->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<queries>
<package android:name="com.facebook.katana" />
<package android:name="com.facebook.lite" />
<package android:name="com.facebook.android" />
<!--
android:icon is used to display launcher icon on mobile devices.
android:banner is used to display a rectangular banned launcher icon on Android TV devices.
-->
<application
android:name=".RNTesterApplication"
android:allowBackup="true"
android:banner="@drawable/tv_banner"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:supportsRtl="true">
<activity
android:name=".RNTesterActivity"
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
<intent>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
</queries>
<application
android:name=".RNTesterApplication"
android:allowBackup="true"
android:banner="@drawable/tv_banner"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:screenOrientation="fullSensor"
android:launchMode="singleTask"
android:configChanges="orientation|screenSize|uiMode"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- Needed to properly create a launch intent when running on Android TV -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "rntester://example” -->
<data android:scheme="rntester" android:host="example" />
</intent-filter>
</activity>
<provider
android:name="com.facebook.react.modules.blob.BlobProvider"
android:authorities="@string/blob_provider_authority"
android:exported="false"
/>
</application>
<queries>
<package android:name="com.facebook.katana" />
<package android:name="com.facebook.lite" />
<package android:name="com.facebook.android" />
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
<intent>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
</queries>
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<profileable
android:shell="true"
tools:targetApi="29" />
<activity
android:name=".RNTesterActivity"
android:configChanges="orientation|screenSize|uiMode"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:screenOrientation="fullSensor">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- Needed to properly create a launch intent when running on Android TV -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "rntester://example” -->
<data
android:host="example"
android:scheme="rntester" />
</intent-filter>
</activity>
<provider
android:name="com.facebook.react.modules.blob.BlobProvider"
android:authorities="@string/blob_provider_authority"
android:exported="false" />
</application>
</manifest>
@@ -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
@@ -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<NativeModule> = emptyList()
override fun getViewManagerNames(reactContext: ReactApplicationContext) =
listOf("RNTMyNativeView", "RNTMyLegacyNativeView")
listOf("RNTMyNativeView", "RNTMyLegacyNativeView", "RNTReportFullyDrawnView")
override fun createViewManagers(
reactContext: ReactApplicationContext
): List<ViewManager<*, *>> =
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
}
})
@@ -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()
}
}
}
@@ -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<ReportFullyDrawnView>(),
RNTReportFullyDrawnViewManagerInterface<ReportFullyDrawnView> {
companion object {
const val REACT_CLASS = "RNTReportFullyDrawnView"
}
private val delegate: ViewManagerDelegate<ReportFullyDrawnView> =
RNTReportFullyDrawnViewManagerDelegate(this)
override fun getDelegate(): ViewManagerDelegate<ReportFullyDrawnView> = delegate
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): ReportFullyDrawnView =
ReportFullyDrawnView(reactContext)
}
@@ -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}
/>
</View>
<ReportFullyDrawnView />
</RNTesterThemeContext.Provider>
);
};
+2 -1
View File
@@ -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/")