From c0821dd60dc8ac593966769546daa3dd056343dc Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Thu, 21 May 2026 14:45:41 +0200 Subject: [PATCH 1/4] Pin react-native-onyx to perf branch HEAD (731fe957) for testing Bumps react-native-onyx to elirangoshen/perf/mergeCollection-multiGet-prewarm HEAD (731fe957), which stacks on top of #787 (cache-first mergeCollectionWithPatches) and adds a cold-cache pre-warm optimization: - Fast path (every existingKey is in cache): skip pre-warm entirely. Preserves the original promise-chain depth so subscriber-callback timing is unchanged for warm-cache merges (the common case). - Slow path (any existingKey is a cache miss): use Storage.multiGet to batch the missing keys into a single storage round-trip, instead of N parallel Storage.getItem calls. Net runtime effect: same correctness, fewer storage operations on cold-cache merges, same broadcast timing for warm-cache merges. This branch also drops the now-obsolete `react-native-onyx+3.0.73.patch` (the canonical upstream revert it implemented landed in Onyx PR #785, which is already part of the pinned SHA). Not for merge -- branch is for performance verification only. --- package-lock.json | 8 +-- package.json | 2 +- patches/react-native-onyx/details.md | 10 +-- .../react-native-onyx+3.0.73.patch | 67 ------------------- 4 files changed, 6 insertions(+), 81 deletions(-) delete mode 100644 patches/react-native-onyx/react-native-onyx+3.0.73.patch diff --git a/package-lock.json b/package-lock.json index 9cb31a77c5aa..9c6e910f4228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,7 +115,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "3.0.73", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#731fe95764798364c54797ed94df605a7b97c86d", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -34993,9 +34993,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.73", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.73.tgz", - "integrity": "sha512-0fvL8q7Rx3QBoHJJLB4e2wd0a9/4f//FvTBGKJliU5rkO/05APKZKFxMprSQpBm+O+i3T2R1lwSQbeaj9AbIDA==", + "version": "3.0.75", + "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#731fe95764798364c54797ed94df605a7b97c86d", + "integrity": "sha512-hfbgTi6IqlNCApFhpeb6VC62xGkabnOjE5PdCrRCUlzOmOFkfAaCD0e5687UEMFq7sxx9XaUtjH6Q0LC/7o+/w==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 3fdfbf1b6c9f..9ff8c82fc589 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "3.0.73", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#731fe95764798364c54797ed94df605a7b97c86d", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", diff --git a/patches/react-native-onyx/details.md b/patches/react-native-onyx/details.md index 8d435bf932d1..839eda61743e 100644 --- a/patches/react-native-onyx/details.md +++ b/patches/react-native-onyx/details.md @@ -1,11 +1,3 @@ # `react-native-onyx` patches -### [react-native-onyx+3.0.73.patch](react-native-onyx+3.0.73.patch) - -- Reason: - - > Reverts [Onyx PR #770 (the subscription-side skip for skippable collection member ids in subscribeToKey)](https://github.com/Expensify/react-native-onyx/pull/770) and the line [PR #779](https://github.com/Expensify/react-native-onyx/pull/779) added to work around [PR #770](https://github.com/Expensify/react-native-onyx/pull/770)'s silent-no-callback contract. - -- Upstream PR/issue: https://github.com/Expensify/react-native-onyx/pull/785 -- E/App issue: https://github.com/Expensify/App/issues/86181 -- PR Introducing Patch: https://github.com/Expensify/App/pull/90764 \ No newline at end of file +_No active patches._ \ No newline at end of file diff --git a/patches/react-native-onyx/react-native-onyx+3.0.73.patch b/patches/react-native-onyx/react-native-onyx+3.0.73.patch deleted file mode 100644 index 81d048105d69..000000000000 --- a/patches/react-native-onyx/react-native-onyx+3.0.73.patch +++ /dev/null @@ -1,67 +0,0 @@ -diff --git a/node_modules/react-native-onyx/dist/OnyxUtils.js b/node_modules/react-native-onyx/dist/OnyxUtils.js -index 87d163f..1971cd2 100644 ---- a/node_modules/react-native-onyx/dist/OnyxUtils.js -+++ b/node_modules/react-native-onyx/dist/OnyxUtils.js -@@ -886,24 +886,6 @@ function subscribeToKey(connectOptions) { - const subscriptionID = lastSubscriptionID++; - callbackToStateMapping[subscriptionID] = mapping; - callbackToStateMapping[subscriptionID].subscriptionID = subscriptionID; -- // If the subscriber is attempting to connect to a collection member whose ID is skippable (e.g. "undefined", "null", etc.) -- // we suppress wiring the subscription fully to avoid unnecessary callback emissions such as for "report_undefined". -- // We still return a valid subscriptionID so callers can disconnect safely. -- try { -- const skippableIDs = getSkippableCollectionMemberIDs(); -- if (skippableIDs.size) { -- const [, collectionMemberID] = OnyxKeys_1.default.splitCollectionMemberKey(mapping.key); -- if (skippableIDs.has(collectionMemberID)) { -- // Clean up the provisional mapping to avoid retaining unused subscribers. -- OnyxCache_1.default.addNullishStorageKey(mapping.key); -- delete callbackToStateMapping[subscriptionID]; -- return subscriptionID; -- } -- } -- } -- catch (e) { -- // Not a collection member key, proceed as usual. -- } - // When keyChanged is called, a key is passed and the method looks through all the Subscribers in callbackToStateMapping for the matching key to get the subscriptionID - // to avoid having to loop through all the Subscribers all the time (even when just one connection belongs to one key), - // We create a mapping from key to lists of subscriptionIDs to access the specific list of subscriptionIDs. -@@ -1440,12 +1422,6 @@ function logKeyChanged(onyxMethod, key, value, hasChanged) { - function logKeyRemoved(onyxMethod, key) { - Logger.logInfo(`${onyxMethod} called for key: ${key} => null passed, so key was removed`); - } --/** -- * Getter - returns the callback to state mapping, useful in test environments. -- */ --function getCallbackToStateMapping() { -- return callbackToStateMapping; --} - /** - * Clear internal variables used in this file, useful in test environments. - */ -@@ -1504,6 +1480,5 @@ const OnyxUtils = { - setWithRetry, - multiSetWithRetry, - setCollectionWithRetry, -- getCallbackToStateMapping, - }; - exports.default = OnyxUtils; -diff --git a/node_modules/react-native-onyx/dist/useOnyx.js b/node_modules/react-native-onyx/dist/useOnyx.js -index 9213cff..2e48c73 100644 ---- a/node_modules/react-native-onyx/dist/useOnyx.js -+++ b/node_modules/react-native-onyx/dist/useOnyx.js -@@ -220,12 +220,8 @@ function useOnyx(key, options, dependencies = []) { - newValueRef.current = null; - sourceValueRef.current = undefined; - resultRef.current = [undefined, { status: (options === null || options === void 0 ? void 0 : options.initWithStoredValues) === false ? 'loaded' : 'loading' }]; -+ shouldGetCachedValueRef.current = true; - } -- // Force a cache re-read on every (re)subscription so any side effects from -- // subscribeToKey (e.g. addNullishStorageKey for skippable collection member ids) -- // are reflected in the next getSnapshot. Resetting this flag does not change -- // resultRef by itself, so it doesn't cause an extra mount render. -- shouldGetCachedValueRef.current = true; - hasMountedRef.current = true; - isConnectingRef.current = true; - onStoreChangeFnRef.current = onStoreChange; From e749645afa4a9b165b000c56019999e073f69490 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Fri, 22 May 2026 11:27:43 +0200 Subject: [PATCH 2/4] Add OnyxUpdates.applyHTTPSOnyxUpdates Sentry span for perf comparison Wraps applyHTTPSOnyxUpdates in a Sentry span via the existing @libs/telemetry/activeSpans helper so we can measure local Onyx-update processing time per API response, isolated from network latency. Cold-cache mergeCollection batches dominate this duration during OpenApp / ReconnectApp / Search, so this span is a clean A/B-test signal for mergeCollectionWithPatches changes (cache-first ordering and multiGet pre-warm). Span attributes: command, onyx_data_count, success_data_count, failure_data_count. Local console output via the helper: [Sentry][OnyxUpdates.applyHTTPSOnyxUpdates:OpenApp:] Ending span (Xms) Not for merge -- perf-test instrumentation only. --- src/CONST/index.ts | 1 + src/libs/actions/OnyxUpdates.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index a1be80816c35..100556a69745 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -2061,6 +2061,7 @@ const CONST = { EMOJI_TRIE_BUILD: 'LocaleEmojiTrieBuild', }, SPAN_ONYX_DERIVED_COMPUTE: 'OnyxDerivedCompute', + SPAN_ONYX_UPDATES_APPLY_HTTPS: 'OnyxUpdates.applyHTTPSOnyxUpdates', SPAN_NAVIGATION: { ROOT: 'BootsplashVisibleNavigation', PUSHER_INIT: 'NavigationPusherInit', diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 47df0ee60635..474963002e62 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -4,6 +4,7 @@ import type {Merge} from 'type-fest'; import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import Log from '@libs/Log'; import PusherUtils from '@libs/PusherUtils'; +import {endSpan, startSpan} from '@libs/telemetry/activeSpans'; import {trackExpenseApiError} from '@libs/telemetry/trackExpenseCreationError'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -30,6 +31,23 @@ let airshipEventsPromise = Promise.resolve(); function applyHTTPSOnyxUpdates(request: Request, response: Response, lastUpdateID: number) { Log.info('[OnyxUpdateManager] Applying https update', false, {lastUpdateID}); + + // Perf-test instrumentation: measure local Onyx-update processing time per API response + // (excludes network time). Cold-cache mergeCollection batches dominate this duration during + // OpenApp / ReconnectApp / Search; comparing this span across branches isolates the impact + // of mergeCollectionWithPatches changes (cache-first ordering, multiGet pre-warm). + const spanId = `${CONST.TELEMETRY.SPAN_ONYX_UPDATES_APPLY_HTTPS}:${request?.command ?? 'unknown'}:${lastUpdateID}`; + startSpan(spanId, { + name: CONST.TELEMETRY.SPAN_ONYX_UPDATES_APPLY_HTTPS, + op: 'onyx.updates.apply.https', + attributes: { + command: String(request?.command ?? 'unknown'), + onyx_data_count: Array.isArray(response.onyxData) ? response.onyxData.length : 0, + success_data_count: Array.isArray(request?.successData) ? request.successData.length : 0, + failure_data_count: Array.isArray(request?.failureData) ? request.failureData.length : 0, + }, + }); + // For most requests we can immediately update Onyx. For write requests we queue the updates and apply them after the sequential queue has flushed to prevent a replay effect in // the UI. See https://github.com/Expensify/App/issues/12775 for more info. const updateHandler: (updates: Array>) => Promise = request?.data?.apiRequestType === CONST.API_REQUEST_TYPE.WRITE ? queueOnyxUpdates : Onyx.update; @@ -75,6 +93,9 @@ function applyHTTPSOnyxUpdates(request: Request, res .then(() => { Log.info('[OnyxUpdateManager] Done applying HTTPS update', false, {lastUpdateID}); return Promise.resolve(response); + }) + .finally(() => { + endSpan(spanId); }); } From 92bf3073f1a3ad6d6ab6639dedefe8379575cd5b Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Mon, 25 May 2026 11:42:32 +0200 Subject: [PATCH 3/4] Repoint react-native-onyx pin to callstack PR #5 head with test additions Updates the package.json pin from Expensify/react-native-onyx#731fe957 to callstack-internal/react-native-onyx#c1be5731. The new SHA brings in the 5 mergeCollection pre-warm regression tests added on top of the original perf change, so the bundled build matches the test names referenced in PR #5's Manual Tests section. Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c6e910f4228..e78d30edee95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,7 +115,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#731fe95764798364c54797ed94df605a7b97c86d", + "react-native-onyx": "git+https://github.com/callstack-internal/react-native-onyx.git#c1be57316bef150d2f8d6bda6b1e0ae328b55a29", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -34994,7 +34994,7 @@ }, "node_modules/react-native-onyx": { "version": "3.0.75", - "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#731fe95764798364c54797ed94df605a7b97c86d", + "resolved": "git+ssh://git@github.com/callstack-internal/react-native-onyx.git#c1be57316bef150d2f8d6bda6b1e0ae328b55a29", "integrity": "sha512-hfbgTi6IqlNCApFhpeb6VC62xGkabnOjE5PdCrRCUlzOmOFkfAaCD0e5687UEMFq7sxx9XaUtjH6Q0LC/7o+/w==", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9ff8c82fc589..945023dd65b8 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#731fe95764798364c54797ed94df605a7b97c86d", + "react-native-onyx": "git+https://github.com/callstack-internal/react-native-onyx.git#c1be57316bef150d2f8d6bda6b1e0ae328b55a29", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", From faa9c02179a0a4d407b837b1ddadc420bdb21ec1 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Thu, 28 May 2026 11:30:09 +0200 Subject: [PATCH 4/4] Bump react-native-onyx pin to PR #793 head (98ddd13) Brings the companion build in line with the latest commits on Expensify/react-native-onyx#793: - align multiGet cache hit check with cache.hasCacheForKey - preserve cache-first merge when multiGet pre-warm rejects - multiGet: prefer cache over stale storage on concurrent writes - additional regression tests for mergeCollection pre-warm Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e78d30edee95..dd907ee5569c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,7 +115,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/callstack-internal/react-native-onyx.git#c1be57316bef150d2f8d6bda6b1e0ae328b55a29", + "react-native-onyx": "git+https://github.com/callstack-internal/react-native-onyx.git#98ddd1304d2fd4fa1d37aa853014db59dd346155", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -34993,9 +34993,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.75", - "resolved": "git+ssh://git@github.com/callstack-internal/react-native-onyx.git#c1be57316bef150d2f8d6bda6b1e0ae328b55a29", - "integrity": "sha512-hfbgTi6IqlNCApFhpeb6VC62xGkabnOjE5PdCrRCUlzOmOFkfAaCD0e5687UEMFq7sxx9XaUtjH6Q0LC/7o+/w==", + "version": "3.0.79", + "resolved": "git+ssh://git@github.com/callstack-internal/react-native-onyx.git#98ddd1304d2fd4fa1d37aa853014db59dd346155", + "integrity": "sha512-90vcEp5nk9dd2WXdDp5V8tN0lgLviI/sUptkvQVKG+uUYIN2qwJmrqhtikyBEvbfm9SLxjPzvfy0RpToKoNxkw==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 945023dd65b8..e94f26072d0a 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/callstack-internal/react-native-onyx.git#c1be57316bef150d2f8d6bda6b1e0ae328b55a29", + "react-native-onyx": "git+https://github.com/callstack-internal/react-native-onyx.git#98ddd1304d2fd4fa1d37aa853014db59dd346155", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0",