VirtualizedList: Delete Batchinator (#48515)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48515

Deletes `Batchinator`, inlines the timer, and cleans up the `disableInteractionManagerInBatchinator` feature flag.

Changelog:
[Internal]

Reviewed By: javache, NickGerleman

Differential Revision: D67885194

fbshipit-source-id: 5f3ec71a02cf1f1b382b41a480beed28fc8c5439
This commit is contained in:
Tim Yung
2025-01-22 07:14:37 -08:00
committed by Facebook GitHub Bot
parent ff0bcb2427
commit c8a387c2d1
6 changed files with 13 additions and 187 deletions
@@ -9856,7 +9856,6 @@ exports[`public API should not change unintentionally src/private/featureflags/R
animatedShouldDebounceQueueFlush: Getter<boolean>,
animatedShouldUseSingleOp: Getter<boolean>,
disableInteractionManager: Getter<boolean>,
disableInteractionManagerInBatchinator: Getter<boolean>,
enableAccessToHostTreeInFabric: Getter<boolean>,
enableAnimatedAllowlist: Getter<boolean>,
enableAnimatedClearImmediateFix: Getter<boolean>,
@@ -9927,7 +9926,6 @@ declare export const jsOnlyTestFlag: Getter<boolean>;
declare export const animatedShouldDebounceQueueFlush: Getter<boolean>;
declare export const animatedShouldUseSingleOp: Getter<boolean>;
declare export const disableInteractionManager: Getter<boolean>;
declare export const disableInteractionManagerInBatchinator: Getter<boolean>;
declare export const enableAccessToHostTreeInFabric: Getter<boolean>;
declare export const enableAnimatedAllowlist: Getter<boolean>;
declare export const enableAnimatedClearImmediateFix: Getter<boolean>;
@@ -529,16 +529,6 @@ const definitions: FeatureFlagDefinitions = {
purpose: 'experimentation',
},
},
disableInteractionManagerInBatchinator: {
defaultValue: false,
metadata: {
dateAdded: '2024-11-18',
description:
'Skips InteractionManager in `Batchinator` and invokes callbacks synchronously.',
expectedReleaseValue: true,
purpose: 'experimentation',
},
},
enableAccessToHostTreeInFabric: {
defaultValue: false,
metadata: {
@@ -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<<8ab1581a5660c8148c1ff87e12c042cc>>
* @generated SignedSource<<e710f456c7fa57f98e46cbc467755503>>
* @flow strict
*/
@@ -31,7 +31,6 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
animatedShouldDebounceQueueFlush: Getter<boolean>,
animatedShouldUseSingleOp: Getter<boolean>,
disableInteractionManager: Getter<boolean>,
disableInteractionManagerInBatchinator: Getter<boolean>,
enableAccessToHostTreeInFabric: Getter<boolean>,
enableAnimatedAllowlist: Getter<boolean>,
enableAnimatedClearImmediateFix: Getter<boolean>,
@@ -120,11 +119,6 @@ export const animatedShouldUseSingleOp: Getter<boolean> = createJavaScriptFlagGe
*/
export const disableInteractionManager: Getter<boolean> = createJavaScriptFlagGetter('disableInteractionManager', false);
/**
* Skips InteractionManager in `Batchinator` and invokes callbacks synchronously.
*/
export const disableInteractionManagerInBatchinator: Getter<boolean> = createJavaScriptFlagGetter('disableInteractionManagerInBatchinator', false);
/**
* Enables access to the host tree in Fabric using DOM-compatible APIs.
*/
@@ -1,85 +0,0 @@
/**
* 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 {InteractionManager} from 'react-native';
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
/**
* A simple class for batching up invocations of a low-pri callback. A timeout is set to run the
* callback once after a delay, no matter how many times it's scheduled. Once the delay is reached,
* InteractionManager.runAfterInteractions is used to invoke the callback after any hi-pri
* interactions are done running.
*
* Make sure to cleanup with dispose(). Example:
*
* class Widget extends React.Component {
* _batchedSave: new Batchinator(() => this._saveState, 1000);
* _saveSate() {
* // save this.state to disk
* }
* componentDidUpdate() {
* this._batchedSave.schedule();
* }
* componentWillUnmount() {
* this._batchedSave.dispose();
* }
* ...
* }
*/
class Batchinator {
_callback: () => void;
_delay: number;
_taskHandle: ?{cancel: () => void, ...};
constructor(callback: () => void, delay: number) {
this._delay = delay;
this._callback = callback;
}
/*
* Cleanup any pending tasks.
*
* By default, if there is a pending task the callback is run immediately. Set the option abort to
* true to not call the callback if it was pending.
*/
dispose(): void {
if (this._taskHandle) {
this._taskHandle.cancel();
this._taskHandle = null;
}
}
schedule(): void {
if (this._taskHandle) {
return;
}
const invokeCallback = () => {
// Note that we clear the handle before invoking the callback so that if the callback calls
// schedule again, it will actually schedule another task.
this._taskHandle = null;
this._callback();
};
const timeoutHandle = setTimeout(
// NOTE: When shipping this, delete `Batchinator` instead of only these
// lines of code. Without `InteractionManager`, it's just a `setTimeout`.
ReactNativeFeatureFlags.disableInteractionManagerInBatchinator()
? invokeCallback
: () => {
this._taskHandle =
InteractionManager.runAfterInteractions(invokeCallback);
},
this._delay,
);
this._taskHandle = {cancel: () => clearTimeout(timeoutHandle)};
}
}
module.exports = Batchinator;
@@ -1,74 +0,0 @@
/**
* 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.
*
* @format
* @oncall react_native
*/
'use strict';
describe('Batchinator', () => {
const Batchinator = require('../Batchinator');
it('executes vanilla tasks', () => {
const callback = jest.fn();
const batcher = new Batchinator(callback, 10000);
batcher.schedule();
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(1);
});
it('batches up tasks', () => {
const callback = jest.fn();
const batcher = new Batchinator(callback, 10000);
batcher.schedule();
batcher.schedule();
batcher.schedule();
batcher.schedule();
expect(callback).not.toHaveBeenCalled();
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(1);
});
it('does nothing after dispose', () => {
const callback = jest.fn();
const batcher = new Batchinator(callback, 10000);
batcher.schedule();
batcher.schedule();
batcher.dispose();
expect(callback).not.toHaveBeenCalled();
jest.runAllTimers();
expect(callback).not.toHaveBeenCalled();
});
it('should call tasks scheduled by the callback', () => {
let batcher = null;
let hasRescheduled = false;
const callback = jest.fn(() => {
if (!hasRescheduled) {
batcher.schedule();
hasRescheduled = true;
}
});
batcher = new Batchinator(callback, 10000);
batcher.schedule();
jest.runAllTimers();
expect(callback.mock.calls.length).toBe(2);
});
it('does not run callbacks more than once', () => {
const callback = jest.fn();
const batcher = new Batchinator(callback, 10000);
batcher.schedule();
batcher.schedule();
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(1);
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(1);
batcher.dispose();
expect(callback).toHaveBeenCalledTimes(1);
});
});
@@ -24,7 +24,6 @@ import type {
ScrollEvent,
} from 'react-native/Libraries/Types/CoreEventTypes';
import Batchinator from '../Interaction/Batchinator';
import clamp from '../Utilities/clamp';
import infoLog from '../Utilities/infoLog';
import {CellRenderMask} from './CellRenderMask';
@@ -375,10 +374,6 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
this._checkProps(props);
this._fillRateHelper = new FillRateHelper(this._listMetrics);
this._updateCellsToRenderBatcher = new Batchinator(
this._updateCellsToRender,
this.props.updateCellsBatchingPeriod ?? 50,
);
if (this.props.viewabilityConfigCallbackPairs) {
this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map(
@@ -687,7 +682,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
if (this._isNestedWithSameOrientation()) {
this.context.unregisterAsNestedChild({ref: this});
}
this._updateCellsToRenderBatcher.dispose();
clearTimeout(this._updateCellsToRenderTimeoutID);
this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.dispose();
});
@@ -1228,7 +1223,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
_scrollRef: ?React.ElementRef<any> = null;
_sentStartForContentLength = 0;
_sentEndForContentLength = 0;
_updateCellsToRenderBatcher: Batchinator;
_updateCellsToRenderTimeoutID: ?TimeoutID = null;
_viewabilityTuples: Array<ViewabilityHelperCallbackTuple> = [];
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
@@ -1763,11 +1758,19 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
this._hiPriInProgress = true;
// Don't worry about interactions when scrolling quickly; focus on filling content as fast
// as possible.
this._updateCellsToRenderBatcher.dispose();
if (this._updateCellsToRenderTimeoutID != null) {
clearTimeout(this._updateCellsToRenderTimeoutID);
this._updateCellsToRenderTimeoutID = null;
}
this._updateCellsToRender();
return;
} else {
this._updateCellsToRenderBatcher.schedule();
if (this._updateCellsToRenderTimeoutID == null) {
this._updateCellsToRenderTimeoutID = setTimeout(() => {
this._updateCellsToRenderTimeoutID = null;
this._updateCellsToRender();
}, this.props.updateCellsBatchingPeriod ?? 50);
}
}
}