@samitouri / QOS-React / commits / 7039834262

Create Fabric-specific version of ReactNativeAttributesPayload (#28841)

## Summary This PR introduces Fabric-only version of `ReactNativeAttributesPayload`. It is a copy-paste of `ReactNativeAttributesPayload.js`, and is called `ReactNativeAttributesPayloadFabric.js`. The idea behind this change is that certain optimizations in prop diffing may actually be a regression on the old architecture. For example, removing custom diffing may result in larger updateProps payloads. Which is, I guess, fine with JSI, but might be a problem with the bridge. ## How did you test this change? There should be no runtime effect of this change.

Dmytro Rykun committed May 7, 2024 at 11:53 UTC 703983426243422d9726ca3a0c7eef54e173a6bb
3 files changed +568 -69
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+1 -1
@@ -12,7 +12,7 @@ import type {
12 TouchedViewDataAtPoint,
13 ViewConfig,
14 } from './ReactNativeTypes';
15 -import {create, diff} from './ReactNativeAttributePayload';
15 +import {create, diff} from './ReactNativeAttributePayloadFabric';
16 import {dispatchEvent} from './ReactFabricEventEmitter';
17 import {
18 NoEventPriority,
packages/react-native-renderer/src/ReactNativeAttributePayload.js
+2 -68
@@ -15,7 +15,6 @@ import {
15 import isArray from 'shared/isArray';
16
17 import {enableEarlyReturnForPropDiffing} from 'shared/ReactFeatureFlags';
18 -import {enableAddPropertiesFastPath} from 'shared/ReactFeatureFlags';
18
19 import type {AttributeConfiguration} from './ReactNativeTypes';
20
@@ -445,68 +444,6 @@ function diffProperties(
444 return updatePayload;
445 }
446
448 -function fastAddProperties(
449 - updatePayload: null | Object,
450 - nextProps: Object,
451 - validAttributes: AttributeConfiguration,
452 -): null | Object {
453 - let attributeConfig;
454 - let nextProp;
455 -
456 - for (const propKey in nextProps) {
457 - nextProp = nextProps[propKey];
458 -
459 - if (nextProp === undefined) {
460 - continue;
461 - }
462 -
463 - attributeConfig = validAttributes[propKey];
464 -
465 - if (attributeConfig === undefined) {
466 - continue;
467 - }
468 -
469 - if (typeof nextProp === 'function') {
470 - nextProp = (true: any);
471 - }
472 -
473 - if (typeof attributeConfig !== 'object') {
474 - if (!updatePayload) {
475 - updatePayload = ({}: {[string]: $FlowFixMe});
476 - }
477 - updatePayload[propKey] = nextProp;
478 - continue;
479 - }
480 -
481 - if (typeof attributeConfig.process === 'function') {
482 - if (!updatePayload) {
483 - updatePayload = ({}: {[string]: $FlowFixMe});
484 - }
485 - updatePayload[propKey] = attributeConfig.process(nextProp);
486 - continue;
487 - }
488 -
489 - if (isArray(nextProp)) {
490 - for (let i = 0; i < nextProp.length; i++) {
491 - updatePayload = fastAddProperties(
492 - updatePayload,
493 - nextProp[i],
494 - ((attributeConfig: any): AttributeConfiguration),
495 - );
496 - }
497 - continue;
498 - }
499 -
500 - updatePayload = fastAddProperties(
501 - updatePayload,
502 - nextProp,
503 - ((attributeConfig: any): AttributeConfiguration),
504 - );
505 - }
506 -
507 - return updatePayload;
508 -}
509 -
447 /**
448 * addProperties adds all the valid props to the payload after being processed.
449 */
@@ -515,11 +452,8 @@ function addProperties(
452 props: Object,
453 validAttributes: AttributeConfiguration,
454 ): null | Object {
518 - if (enableAddPropertiesFastPath) {
519 - return fastAddProperties(updatePayload, props, validAttributes);
520 - } else {
521 - return diffProperties(updatePayload, emptyObject, props, validAttributes);
522 - }
455 + // TODO: Fast path
456 + return diffProperties(updatePayload, emptyObject, props, validAttributes);
457 }
458
459 /**
packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js new
+565
@@ -0,0 +1,565 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +// Modules provided by RN:
11 +import {
12 + deepDiffer,
13 + flattenStyle,
14 +} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
15 +import isArray from 'shared/isArray';
16 +
17 +import {enableEarlyReturnForPropDiffing} from 'shared/ReactFeatureFlags';
18 +import {enableAddPropertiesFastPath} from 'shared/ReactFeatureFlags';
19 +
20 +import type {AttributeConfiguration} from './ReactNativeTypes';
21 +
22 +const emptyObject = {};
23 +
24 +/**
25 + * Create a payload that contains all the updates between two sets of props.
26 + *
27 + * These helpers are all encapsulated into a single module, because they use
28 + * mutation as a performance optimization which leads to subtle shared
29 + * dependencies between the code paths. To avoid this mutable state leaking
30 + * across modules, I've kept them isolated to this module.
31 + */
32 +
33 +type NestedNode = Array<NestedNode> | Object;
34 +
35 +// Tracks removed keys
36 +let removedKeys: {[string]: boolean} | null = null;
37 +let removedKeyCount = 0;
38 +
39 +const deepDifferOptions = {
40 + unsafelyIgnoreFunctions: true,
41 +};
42 +
43 +function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean {
44 + if (typeof nextProp !== 'object' || nextProp === null) {
45 + // Scalars have already been checked for equality
46 + return true;
47 + } else {
48 + // For objects and arrays, the default diffing algorithm is a deep compare
49 + return deepDiffer(prevProp, nextProp, deepDifferOptions);
50 + }
51 +}
52 +
53 +function restoreDeletedValuesInNestedArray(
54 + updatePayload: Object,
55 + node: NestedNode,
56 + validAttributes: AttributeConfiguration,
57 +) {
58 + if (isArray(node)) {
59 + let i = node.length;
60 + while (i-- && removedKeyCount > 0) {
61 + restoreDeletedValuesInNestedArray(
62 + updatePayload,
63 + node[i],
64 + validAttributes,
65 + );
66 + }
67 + } else if (node && removedKeyCount > 0) {
68 + const obj = node;
69 + for (const propKey in removedKeys) {
70 + // $FlowFixMe[incompatible-use] found when upgrading Flow
71 + if (!removedKeys[propKey]) {
72 + continue;
73 + }
74 + let nextProp = obj[propKey];
75 + if (nextProp === undefined) {
76 + continue;
77 + }
78 +
79 + const attributeConfig = validAttributes[propKey];
80 + if (!attributeConfig) {
81 + continue; // not a valid native prop
82 + }
83 +
84 + if (typeof nextProp === 'function') {
85 + // $FlowFixMe[incompatible-type] found when upgrading Flow
86 + nextProp = true;
87 + }
88 + if (typeof nextProp === 'undefined') {
89 + // $FlowFixMe[incompatible-type] found when upgrading Flow
90 + nextProp = null;
91 + }
92 +
93 + if (typeof attributeConfig !== 'object') {
94 + // case: !Object is the default case
95 + updatePayload[propKey] = nextProp;
96 + } else if (
97 + typeof attributeConfig.diff === 'function' ||
98 + typeof attributeConfig.process === 'function'
99 + ) {
100 + // case: CustomAttributeConfiguration
101 + const nextValue =
102 + typeof attributeConfig.process === 'function'
103 + ? attributeConfig.process(nextProp)
104 + : nextProp;
105 + updatePayload[propKey] = nextValue;
106 + }
107 + // $FlowFixMe[incompatible-use] found when upgrading Flow
108 + removedKeys[propKey] = false;
109 + removedKeyCount--;
110 + }
111 + }
112 +}
113 +
114 +function diffNestedArrayProperty(
115 + updatePayload: null | Object,
116 + prevArray: Array<NestedNode>,
117 + nextArray: Array<NestedNode>,
118 + validAttributes: AttributeConfiguration,
119 +): null | Object {
120 + const minLength =
121 + prevArray.length < nextArray.length ? prevArray.length : nextArray.length;
122 + let i;
123 + for (i = 0; i < minLength; i++) {
124 + // Diff any items in the array in the forward direction. Repeated keys
125 + // will be overwritten by later values.
126 + updatePayload = diffNestedProperty(
127 + updatePayload,
128 + prevArray[i],
129 + nextArray[i],
130 + validAttributes,
131 + );
132 + }
133 + for (; i < prevArray.length; i++) {
134 + // Clear out all remaining properties.
135 + updatePayload = clearNestedProperty(
136 + updatePayload,
137 + prevArray[i],
138 + validAttributes,
139 + );
140 + }
141 + for (; i < nextArray.length; i++) {
142 + // Add all remaining properties.
143 + updatePayload = addNestedProperty(
144 + updatePayload,
145 + nextArray[i],
146 + validAttributes,
147 + );
148 + }
149 + return updatePayload;
150 +}
151 +
152 +function diffNestedProperty(
153 + updatePayload: null | Object,
154 + prevProp: NestedNode,
155 + nextProp: NestedNode,
156 + validAttributes: AttributeConfiguration,
157 +): null | Object {
158 + if (!updatePayload && prevProp === nextProp) {
159 + // If no properties have been added, then we can bail out quickly on object
160 + // equality.
161 + return updatePayload;
162 + }
163 +
164 + if (!prevProp || !nextProp) {
165 + if (nextProp) {
166 + return addNestedProperty(updatePayload, nextProp, validAttributes);
167 + }
168 + if (prevProp) {
169 + return clearNestedProperty(updatePayload, prevProp, validAttributes);
170 + }
171 + return updatePayload;
172 + }
173 +
174 + if (!isArray(prevProp) && !isArray(nextProp)) {
175 + // Both are leaves, we can diff the leaves.
176 + return diffProperties(updatePayload, prevProp, nextProp, validAttributes);
177 + }
178 +
179 + if (isArray(prevProp) && isArray(nextProp)) {
180 + // Both are arrays, we can diff the arrays.
181 + return diffNestedArrayProperty(
182 + updatePayload,
183 + prevProp,
184 + nextProp,
185 + validAttributes,
186 + );
187 + }
188 +
189 + if (isArray(prevProp)) {
190 + return diffProperties(
191 + updatePayload,
192 + flattenStyle(prevProp),
193 + nextProp,
194 + validAttributes,
195 + );
196 + }
197 +
198 + return diffProperties(
199 + updatePayload,
200 + prevProp,
201 + flattenStyle(nextProp),
202 + validAttributes,
203 + );
204 +}
205 +
206 +/**
207 + * addNestedProperty takes a single set of props and valid attribute
208 + * attribute configurations. It processes each prop and adds it to the
209 + * updatePayload.
210 + */
211 +function addNestedProperty(
212 + updatePayload: null | Object,
213 + nextProp: NestedNode,
214 + validAttributes: AttributeConfiguration,
215 +): $FlowFixMe {
216 + if (!nextProp) {
217 + return updatePayload;
218 + }
219 +
220 + if (!isArray(nextProp)) {
221 + // Add each property of the leaf.
222 + return addProperties(updatePayload, nextProp, validAttributes);
223 + }
224 +
225 + for (let i = 0; i < nextProp.length; i++) {
226 + // Add all the properties of the array.
227 + updatePayload = addNestedProperty(
228 + updatePayload,
229 + nextProp[i],
230 + validAttributes,
231 + );
232 + }
233 +
234 + return updatePayload;
235 +}
236 +
237 +/**
238 + * clearNestedProperty takes a single set of props and valid attributes. It
239 + * adds a null sentinel to the updatePayload, for each prop key.
240 + */
241 +function clearNestedProperty(
242 + updatePayload: null | Object,
243 + prevProp: NestedNode,
244 + validAttributes: AttributeConfiguration,
245 +): null | Object {
246 + if (!prevProp) {
247 + return updatePayload;
248 + }
249 +
250 + if (!isArray(prevProp)) {
251 + // Add each property of the leaf.
252 + return clearProperties(updatePayload, prevProp, validAttributes);
253 + }
254 +
255 + for (let i = 0; i < prevProp.length; i++) {
256 + // Add all the properties of the array.
257 + updatePayload = clearNestedProperty(
258 + updatePayload,
259 + prevProp[i],
260 + validAttributes,
261 + );
262 + }
263 + return updatePayload;
264 +}
265 +
266 +/**
267 + * diffProperties takes two sets of props and a set of valid attributes
268 + * and write to updatePayload the values that changed or were deleted.
269 + * If no updatePayload is provided, a new one is created and returned if
270 + * anything changed.
271 + */
272 +function diffProperties(
273 + updatePayload: null | Object,
274 + prevProps: Object,
275 + nextProps: Object,
276 + validAttributes: AttributeConfiguration,
277 +): null | Object {
278 + let attributeConfig;
279 + let nextProp;
280 + let prevProp;
281 +
282 + for (const propKey in nextProps) {
283 + attributeConfig = validAttributes[propKey];
284 + if (!attributeConfig) {
285 + continue; // not a valid native prop
286 + }
287 +
288 + prevProp = prevProps[propKey];
289 + nextProp = nextProps[propKey];
290 +
291 + // functions are converted to booleans as markers that the associated
292 + // events should be sent from native.
293 + if (typeof nextProp === 'function') {
294 + nextProp = (true: any);
295 + // If nextProp is not a function, then don't bother changing prevProp
296 + // since nextProp will win and go into the updatePayload regardless.
297 + if (typeof prevProp === 'function') {
298 + prevProp = (true: any);
299 + }
300 + }
301 +
302 + // An explicit value of undefined is treated as a null because it overrides
303 + // any other preceding value.
304 + if (typeof nextProp === 'undefined') {
305 + nextProp = (null: any);
306 + if (typeof prevProp === 'undefined') {
307 + prevProp = (null: any);
308 + }
309 + }
310 +
311 + if (removedKeys) {
312 + removedKeys[propKey] = false;
313 + }
314 +
315 + if (updatePayload && updatePayload[propKey] !== undefined) {
316 + // Something else already triggered an update to this key because another
317 + // value diffed. Since we're now later in the nested arrays our value is
318 + // more important so we need to calculate it and override the existing
319 + // value. It doesn't matter if nothing changed, we'll set it anyway.
320 +
321 + // Pattern match on: attributeConfig
322 + if (typeof attributeConfig !== 'object') {
323 + // case: !Object is the default case
324 + updatePayload[propKey] = nextProp;
325 + } else if (
326 + typeof attributeConfig.diff === 'function' ||
327 + typeof attributeConfig.process === 'function'
328 + ) {
329 + // case: CustomAttributeConfiguration
330 + const nextValue =
331 + typeof attributeConfig.process === 'function'
332 + ? attributeConfig.process(nextProp)
333 + : nextProp;
334 + updatePayload[propKey] = nextValue;
335 + }
336 + continue;
337 + }
338 +
339 + if (prevProp === nextProp) {
340 + continue; // nothing changed
341 + }
342 +
343 + // Pattern match on: attributeConfig
344 + if (typeof attributeConfig !== 'object') {
345 + // case: !Object is the default case
346 + if (defaultDiffer(prevProp, nextProp)) {
347 + // a normal leaf has changed
348 + (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
349 + propKey
350 + ] = nextProp;
351 + }
352 + } else if (
353 + typeof attributeConfig.diff === 'function' ||
354 + typeof attributeConfig.process === 'function'
355 + ) {
356 + // case: CustomAttributeConfiguration
357 + const shouldUpdate =
358 + prevProp === undefined ||
359 + (typeof attributeConfig.diff === 'function'
360 + ? attributeConfig.diff(prevProp, nextProp)
361 + : defaultDiffer(prevProp, nextProp));
362 + if (shouldUpdate) {
363 + const nextValue =
364 + typeof attributeConfig.process === 'function'
365 + ? // $FlowFixMe[incompatible-use] found when upgrading Flow
366 + attributeConfig.process(nextProp)
367 + : nextProp;
368 + (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
369 + propKey
370 + ] = nextValue;
371 + }
372 + } else {
373 + // default: fallthrough case when nested properties are defined
374 + removedKeys = null;
375 + removedKeyCount = 0;
376 + // We think that attributeConfig is not CustomAttributeConfiguration at
377 + // this point so we assume it must be AttributeConfiguration.
378 + updatePayload = diffNestedProperty(
379 + updatePayload,
380 + prevProp,
381 + nextProp,
382 + ((attributeConfig: any): AttributeConfiguration),
383 + );
384 + if (removedKeyCount > 0 && updatePayload) {
385 + restoreDeletedValuesInNestedArray(
386 + updatePayload,
387 + nextProp,
388 + ((attributeConfig: any): AttributeConfiguration),
389 + );
390 + removedKeys = null;
391 + }
392 + }
393 + }
394 +
395 + // Also iterate through all the previous props to catch any that have been
396 + // removed and make sure native gets the signal so it can reset them to the
397 + // default.
398 + for (const propKey in prevProps) {
399 + if (nextProps[propKey] !== undefined) {
400 + continue; // we've already covered this key in the previous pass
401 + }
402 + attributeConfig = validAttributes[propKey];
403 + if (!attributeConfig) {
404 + continue; // not a valid native prop
405 + }
406 +
407 + if (updatePayload && updatePayload[propKey] !== undefined) {
408 + // This was already updated to a diff result earlier.
409 + continue;
410 + }
411 +
412 + prevProp = prevProps[propKey];
413 + if (prevProp === undefined) {
414 + continue; // was already empty anyway
415 + }
416 + // Pattern match on: attributeConfig
417 + if (
418 + typeof attributeConfig !== 'object' ||
419 + typeof attributeConfig.diff === 'function' ||
420 + typeof attributeConfig.process === 'function'
421 + ) {
422 + // case: CustomAttributeConfiguration | !Object
423 + // Flag the leaf property for removal by sending a sentinel.
424 + (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
425 + propKey
426 + ] = null;
427 + if (!removedKeys) {
428 + removedKeys = ({}: {[string]: boolean});
429 + }
430 + if (!removedKeys[propKey]) {
431 + removedKeys[propKey] = true;
432 + removedKeyCount++;
433 + }
434 + } else {
435 + // default:
436 + // This is a nested attribute configuration where all the properties
437 + // were removed so we need to go through and clear out all of them.
438 + updatePayload = clearNestedProperty(
439 + updatePayload,
440 + prevProp,
441 + ((attributeConfig: any): AttributeConfiguration),
442 + );
443 + }
444 + }
445 + return updatePayload;
446 +}
447 +
448 +function fastAddProperties(
449 + updatePayload: null | Object,
450 + nextProps: Object,
451 + validAttributes: AttributeConfiguration,
452 +): null | Object {
453 + let attributeConfig;
454 + let nextProp;
455 +
456 + for (const propKey in nextProps) {
457 + nextProp = nextProps[propKey];
458 +
459 + if (nextProp === undefined) {
460 + continue;
461 + }
462 +
463 + attributeConfig = validAttributes[propKey];
464 +
465 + if (attributeConfig === undefined) {
466 + continue;
467 + }
468 +
469 + if (typeof nextProp === 'function') {
470 + nextProp = (true: any);
471 + }
472 +
473 + if (typeof attributeConfig !== 'object') {
474 + if (!updatePayload) {
475 + updatePayload = ({}: {[string]: $FlowFixMe});
476 + }
477 + updatePayload[propKey] = nextProp;
478 + continue;
479 + }
480 +
481 + if (typeof attributeConfig.process === 'function') {
482 + if (!updatePayload) {
483 + updatePayload = ({}: {[string]: $FlowFixMe});
484 + }
485 + updatePayload[propKey] = attributeConfig.process(nextProp);
486 + continue;
487 + }
488 +
489 + if (isArray(nextProp)) {
490 + for (let i = 0; i < nextProp.length; i++) {
491 + updatePayload = fastAddProperties(
492 + updatePayload,
493 + nextProp[i],
494 + ((attributeConfig: any): AttributeConfiguration),
495 + );
496 + }
497 + continue;
498 + }
499 +
500 + updatePayload = fastAddProperties(
501 + updatePayload,
502 + nextProp,
503 + ((attributeConfig: any): AttributeConfiguration),
504 + );
505 + }
506 +
507 + return updatePayload;
508 +}
509 +
510 +/**
511 + * addProperties adds all the valid props to the payload after being processed.
512 + */
513 +function addProperties(
514 + updatePayload: null | Object,
515 + props: Object,
516 + validAttributes: AttributeConfiguration,
517 +): null | Object {
518 + if (enableAddPropertiesFastPath) {
519 + return fastAddProperties(updatePayload, props, validAttributes);
520 + } else {
521 + return diffProperties(updatePayload, emptyObject, props, validAttributes);
522 + }
523 +}
524 +
525 +/**
526 + * clearProperties clears all the previous props by adding a null sentinel
527 + * to the payload for each valid key.
528 + */
529 +function clearProperties(
530 + updatePayload: null | Object,
531 + prevProps: Object,
532 + validAttributes: AttributeConfiguration,
533 +): null | Object {
534 + // TODO: Fast path
535 + return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
536 +}
537 +
538 +export function create(
539 + props: Object,
540 + validAttributes: AttributeConfiguration,
541 +): null | Object {
542 + return addProperties(
543 + null, // updatePayload
544 + props,
545 + validAttributes,
546 + );
547 +}
548 +
549 +export function diff(
550 + prevProps: Object,
551 + nextProps: Object,
552 + validAttributes: AttributeConfiguration,
553 +): null | Object {
554 + if (enableEarlyReturnForPropDiffing) {
555 + if (prevProps === nextProps) {
556 + return null; // no change
557 + }
558 + }
559 + return diffProperties(
560 + null, // updatePayload
561 + prevProps,
562 + nextProps,
563 + validAttributes,
564 + );
565 +}