Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const getConstantsForViewManager: ?(viewManagerName: string) => ?Object =
const getDefaultEventTypes: ?() => Object =
global.RN$LegacyInterop_UIManager_getDefaultEventTypes;

const uiManagerOptimization = global.RN$discordUiManagerOptimization ?? 0;
const shouldLoadViewManagerConstantsOnDemand = uiManagerOptimization === 2;

const getDefaultEventTypesCached = (function () {
let wasCalledOnce = false;
let result = null;
Expand Down Expand Up @@ -260,13 +263,28 @@ const UIManagerJSPlatformAPIs = Platform.select({
},
});

// Building every eagerly registered view manager's constants up front is slow.
// In the on-demand experiment, cache only the view managers JS actually uses.
const viewManagerConfigs: {[viewManagerName: string]: ?Object} = {};

const UIManagerJS: UIManagerJSInterface & {[string]: any} = {
...UIManagerJSOverridenAPIs,
...UIManagerJSDeprecatedPlatformAPIs,
...UIManagerJSPlatformAPIs,
...UIManagerJSUnusedInNewArchAPIs,
getViewManagerConfig: (viewManagerName: string): unknown => {
if (getUIManagerConstants) {
if (shouldLoadViewManagerConstantsOnDemand) {
if (
viewManagerConfigs[viewManagerName] == null &&
UIManagerJS.getConstantsForViewManager
) {
viewManagerConfigs[viewManagerName] =
UIManagerJS.getConstantsForViewManager(viewManagerName);
}
return viewManagerConfigs[viewManagerName];
}

const constants = getUIManagerConstantsCached();
if (
!constants[viewManagerName] &&
Expand Down Expand Up @@ -396,7 +414,7 @@ const UIManagerJS: UIManagerJSInterface & {[string]: any} = {
},
};

if (getUIManagerConstants) {
if (getUIManagerConstants && !shouldLoadViewManagerConstantsOnDemand) {
Object.keys(getUIManagerConstantsCached()).forEach(viewConfigName => {
UIManagerJS[viewConfigName] = getUIManagerConstantsCached()[viewConfigName];
});
Expand Down
47 changes: 44 additions & 3 deletions packages/react-native/Libraries/ReactNative/UIManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,26 @@ const UIManagerImpl: UIManagerJSInterface =
? require('./BridgelessUIManager').default
: require('./PaperUIManager').default;

// $FlowFixMe[cannot-spread-interface]
const UIManager: UIManagerJSInterface = {
...UIManagerImpl,
const UIManagerOverrides = {
/**
* Determines the location on screen, width, and height of the given view and
* returns the values via an async callback. If successful, the callback will
* be called with the following arguments:
*
* - x
* - y
* - width
* - height
* - pageX
* - pageY
*
* Note that these measurements are not available until after the rendering
* has been completed in native. If you need the measurements as soon as
* possible, consider using the [`onLayout`
* prop](docs/view.html#onlayout) instead.
*
* @deprecated Use `ref.measure` instead.
*/
measure(
reactTag: number,
callback: (
Expand Down Expand Up @@ -190,4 +207,28 @@ const UIManager: UIManagerJSInterface = {
},
};

let UIManager: UIManagerJSInterface;
const uiManagerOptimization = global.RN$discordUiManagerOptimization ?? 0;
const shouldPreserveLazyProperties = uiManagerOptimization === 1;

if (shouldPreserveLazyProperties) {
// Copy property descriptors instead of spreading UIManagerImpl. In Bridgeless
// mode, UIManagerImpl may define enumerable lazy ViewManager getters. Spreading
// the object would invoke every getter during module initialization.
// $FlowFixMe[incompatible-type] Flow cannot infer properties from descriptors.
UIManager = Object.create(Object.getPrototypeOf(UIManagerImpl), {
...Object.getOwnPropertyDescriptors(
// $FlowFixMe[class-object-subtyping] UIManagerImpl is a runtime object.
UIManagerImpl,
),
...Object.getOwnPropertyDescriptors(UIManagerOverrides),
});
} else {
// $FlowFixMe[cannot-spread-interface]
UIManager = {
...UIManagerImpl,
...UIManagerOverrides,
};
}

export default UIManager;
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* 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
*/

'use strict';

jest.mock('../../Utilities/Platform', () => ({
OS: 'android',
select: spec => spec.android ?? spec.native ?? spec.default,
}));
jest.unmock('../UIManager');

describe('UIManager', () => {
const originalBridgelessValue = global.RN$Bridgeless;
const originalUIManagerOptimization = global.RN$discordUiManagerOptimization;
const originalGetConstants = global.RN$LegacyInterop_UIManager_getConstants;
const originalGetConstantsForViewManager =
global.RN$LegacyInterop_UIManager_getConstantsForViewManager;
const originalGetDefaultEventTypes =
global.RN$LegacyInterop_UIManager_getDefaultEventTypes;
const viewManagerConfig = {Commands: {focus: 1}, NativeProps: {}};
const getConstants = jest.fn(() => ({
LazyViewManagersEnabled: true,
ViewManagerNames: ['RCTTestView'],
}));
const getConstantsForViewManager = jest.fn(() => viewManagerConfig);
const getDefaultEventTypes = jest.fn(() => ({}));

beforeEach(() => {
jest.resetModules();
getConstants.mockClear();
getConstantsForViewManager.mockClear();
getDefaultEventTypes.mockClear();

// $FlowExpectedError[cannot-write]
global.RN$Bridgeless = true;
// $FlowExpectedError[cannot-write]
global.RN$LegacyInterop_UIManager_getConstants = getConstants;
// $FlowExpectedError[cannot-write]
global.RN$LegacyInterop_UIManager_getConstantsForViewManager =
getConstantsForViewManager;
// $FlowExpectedError[cannot-write]
global.RN$LegacyInterop_UIManager_getDefaultEventTypes =
getDefaultEventTypes;
});

afterEach(() => {
// $FlowExpectedError[cannot-write]
global.RN$Bridgeless = originalBridgelessValue;
// $FlowExpectedError[cannot-write]
global.RN$discordUiManagerOptimization = originalUIManagerOptimization;
// $FlowExpectedError[cannot-write]
global.RN$LegacyInterop_UIManager_getConstants = originalGetConstants;
// $FlowExpectedError[cannot-write]
global.RN$LegacyInterop_UIManager_getConstantsForViewManager =
originalGetConstantsForViewManager;
// $FlowExpectedError[cannot-write]
global.RN$LegacyInterop_UIManager_getDefaultEventTypes =
originalGetDefaultEventTypes;
jest.resetModules();
});

it('preserves lazy ViewManager getters in optimization mode 1', () => {
// $FlowExpectedError[cannot-write]
global.RN$discordUiManagerOptimization = 1;

const UIManager = require('../UIManager').default;

expect(getConstants).toHaveBeenCalledTimes(1);
expect(getConstantsForViewManager).not.toHaveBeenCalled();
expect(
Object.getOwnPropertyDescriptor(UIManager, 'RCTTestView'),
).toBeDefined();
expect(Object.keys(UIManager)).toContain('RCTTestView');

expect(Reflect.get(UIManager, 'RCTTestView')).toBe(viewManagerConfig);
expect(Reflect.get(UIManager, 'RCTTestView')).toBe(viewManagerConfig);
expect(getConstantsForViewManager).toHaveBeenCalledTimes(1);
});

it('eagerly reads ViewManager getters in optimization mode 0', () => {
// $FlowExpectedError[cannot-write]
global.RN$discordUiManagerOptimization = 0;

const UIManager = require('../UIManager').default;

expect(getConstants).toHaveBeenCalledTimes(1);
expect(getConstantsForViewManager).toHaveBeenCalledTimes(1);
expect(Reflect.get(UIManager, 'RCTTestView')).toBe(viewManagerConfig);
});

it('loads and caches individual ViewManager configs on demand in optimization mode 2', () => {
// $FlowExpectedError[cannot-write]
global.RN$discordUiManagerOptimization = 2;

const UIManager = require('../UIManager').default;

expect(getConstants).not.toHaveBeenCalled();
expect(getConstantsForViewManager).not.toHaveBeenCalled();

expect(UIManager.getViewManagerConfig('RCTTestView')).toBe(
viewManagerConfig,
);
expect(UIManager.getViewManagerConfig('RCTTestView')).toBe(
viewManagerConfig,
);
expect(getConstantsForViewManager).toHaveBeenCalledTimes(1);

expect(UIManager.getConstants()).toEqual({
LazyViewManagersEnabled: true,
ViewManagerNames: ['RCTTestView'],
});
expect(getConstants).toHaveBeenCalledTimes(1);
});

it('does not load all constants when attaching default event types in optimization mode 2', () => {
// $FlowExpectedError[cannot-write]
global.RN$discordUiManagerOptimization = 2;

const getNativeComponentAttributes =
require('../getNativeComponentAttributes').default;
getNativeComponentAttributes('RCTTestView');

expect(getConstants).not.toHaveBeenCalled();
expect(getConstantsForViewManager).toHaveBeenCalledTimes(1);
expect(getDefaultEventTypes).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const sizesDiffer = require('../Utilities/differ/sizesDiffer').default;
const UIManager = require('./UIManager').default;
const nullthrows = require('nullthrows');

const uiManagerOptimization = global.RN$discordUiManagerOptimization ?? 0;
const shouldLoadViewManagerConstantsOnDemand = uiManagerOptimization === 2;

function getNativeComponentAttributes(uiViewClassName: string): any {
const viewConfig = UIManager.getViewManagerConfig(uiViewClassName);

Expand Down Expand Up @@ -113,6 +116,18 @@ function attachDefaultEventTypes(viewConfig: any) {
// This is supported on UIManager platforms (ex: Android),
// as lazy view managers are not implemented for all platforms.
// See [UIManager] for details on constants and implementations.
if (
global.RN$Bridgeless === true &&
shouldLoadViewManagerConstantsOnDemand &&
UIManager.getDefaultEventTypes != null
) {
// The narrow native API avoids materializing every view manager's constants
// just to determine which default event types should be attached.
const defaultEventTypes = UIManager.getDefaultEventTypes();
merge(viewConfig, defaultEventTypes);
return;
}

const constants = UIManager.getConstants();
if (constants.ViewManagerNames || constants.LazyViewManagersEnabled) {
// Lazy view managers enabled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<85428eb50bc67ecf13b7a1adb13fcffa>>
* @generated SignedSource<<652b195c21ffe47263fcfbe77a898aa1>>
*/

/**
Expand Down Expand Up @@ -90,6 +90,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun disableViewPreallocationAndroid(): Boolean = accessor.disableViewPreallocationAndroid()

/**
* Select UIManager optimizations: 0 = none, 1 = preserve lazy properties, 2 = load view manager constants on demand.
*/
@JvmStatic
public fun discordUiManagerOptimization(): Double = accessor.discordUiManagerOptimization()

/**
* When enabled, the accessibilityOrder prop will propagate to native platforms and define the accessibility order.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<d67dac495790f02ac60be066c65af49f>>
* @generated SignedSource<<10c483c7007076bb3c1cee1197c20c89>>
*/

/**
Expand All @@ -30,6 +30,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var disableSubviewClippingAndroidCache: Boolean? = null
private var disableTextLayoutManagerCacheAndroidCache: Boolean? = null
private var disableViewPreallocationAndroidCache: Boolean? = null
private var discordUiManagerOptimizationCache: Double? = null
private var enableAccessibilityOrderCache: Boolean? = null
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
Expand Down Expand Up @@ -209,6 +210,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun discordUiManagerOptimization(): Double {
var cached = discordUiManagerOptimizationCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.discordUiManagerOptimization()
discordUiManagerOptimizationCache = cached
}
return cached
}

override fun enableAccessibilityOrder(): Boolean {
var cached = enableAccessibilityOrderCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<c70f3481ad326717ac270bd528af2b3b>>
* @generated SignedSource<<1492421cd4b9bca791ee9ad5f87507a3>>
*/

/**
Expand Down Expand Up @@ -48,6 +48,8 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun disableViewPreallocationAndroid(): Boolean

@DoNotStrip @JvmStatic public external fun discordUiManagerOptimization(): Double

@DoNotStrip @JvmStatic public external fun enableAccessibilityOrder(): Boolean

@DoNotStrip @JvmStatic public external fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<8e8b9dacb0ff4927016bf33eaa102005>>
* @generated SignedSource<<462ac270e53d73922c29341d2bf557cc>>
*/

/**
Expand Down Expand Up @@ -43,6 +43,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun disableViewPreallocationAndroid(): Boolean = false

override fun discordUiManagerOptimization(): Double = 0.0

override fun enableAccessibilityOrder(): Boolean = false

override fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<1d918610fc6dcc5a1448b2dec2234e82>>
* @generated SignedSource<<bf74b75d5d8ac7eac7666dafefb03b0f>>
*/

/**
Expand Down Expand Up @@ -34,6 +34,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var disableSubviewClippingAndroidCache: Boolean? = null
private var disableTextLayoutManagerCacheAndroidCache: Boolean? = null
private var disableViewPreallocationAndroidCache: Boolean? = null
private var discordUiManagerOptimizationCache: Double? = null
private var enableAccessibilityOrderCache: Boolean? = null
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
Expand Down Expand Up @@ -223,6 +224,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}

override fun discordUiManagerOptimization(): Double {
var cached = discordUiManagerOptimizationCache
if (cached == null) {
cached = currentProvider.discordUiManagerOptimization()
accessedFeatureFlags.add("discordUiManagerOptimization")
discordUiManagerOptimizationCache = cached
}
return cached
}

override fun enableAccessibilityOrder(): Boolean {
var cached = enableAccessibilityOrderCache
if (cached == null) {
Expand Down
Loading