diff --git a/eslint.config.mjs b/eslint.config.mjs index 99fb430a..78d06f9a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -97,6 +97,8 @@ export default [ }, ], 'rulesdir/prefer-onyx-connect-in-libs': 'off', + 'rulesdir/no-onyx-connect': 'off', + 'rulesdir/prefer-actions-set-data': 'off', }, }, { diff --git a/lib/GlobalSettings.ts b/lib/GlobalSettings.ts index 5b3aec1f..a836f45d 100644 --- a/lib/GlobalSettings.ts +++ b/lib/GlobalSettings.ts @@ -1,15 +1,16 @@ /** * Stores settings from Onyx.init globally so they can be made accessible by other parts of the library. */ +type GlobalSettings = { + enablePerformanceMetrics: boolean; +}; -const globalSettings = { +const globalSettings: GlobalSettings = { enablePerformanceMetrics: false, }; -type GlobalSettings = typeof globalSettings; - -const listeners = new Set<(settings: GlobalSettings) => unknown>(); -function addGlobalSettingsChangeListener(listener: (settings: GlobalSettings) => unknown) { +const listeners = new Set<(settings: GlobalSettings) => void>(); +function addGlobalSettingsChangeListener(listener: (settings: GlobalSettings) => void) { listeners.add(listener); return () => { listeners.delete(listener); diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 15991694..e63155f5 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -43,7 +43,7 @@ function init({ }: InitOptions): void { if (enablePerformanceMetrics) { GlobalSettings.setPerformanceMetricsEnabled(true); - applyDecorators(); + applyPerformanceMetricsDecorators(); } initDevTools(enableDevTools); @@ -583,25 +583,41 @@ const Onyx = { registerLogger: Logger.registerLogger, }; -function applyDecorators() { +function applyPerformanceMetricsDecorators() { // We are reassigning the functions directly so that internal function calls are also decorated // @ts-expect-error Reassign - connect = decorateWithMetrics(connect, 'Onyx.connect'); - // @ts-expect-error Reassign - connectWithoutView = decorateWithMetrics(connectWithoutView, 'Onyx.connectWithoutView'); - // @ts-expect-error Reassign set = decorateWithMetrics(set, 'Onyx.set'); + Onyx.set = set; // @ts-expect-error Reassign multiSet = decorateWithMetrics(multiSet, 'Onyx.multiSet'); + Onyx.multiSet = multiSet; // @ts-expect-error Reassign merge = decorateWithMetrics(merge, 'Onyx.merge'); + Onyx.merge = merge; // @ts-expect-error Reassign mergeCollection = decorateWithMetrics(mergeCollection, 'Onyx.mergeCollection'); + Onyx.mergeCollection = mergeCollection; + // @ts-expect-error Reassign + setCollection = decorateWithMetrics(setCollection, 'Onyx.setCollection'); + Onyx.setCollection = setCollection; // @ts-expect-error Reassign update = decorateWithMetrics(update, 'Onyx.update'); + Onyx.update = update; // @ts-expect-error Reassign clear = decorateWithMetrics(clear, 'Onyx.clear'); + Onyx.clear = clear; + // @ts-expect-error Reassign + init = decorateWithMetrics(init, 'Onyx.init'); + Onyx.init = init; } +GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { + if (!enablePerformanceMetrics) { + return; + } + + applyPerformanceMetricsDecorators(); +}); + export default Onyx; export type {OnyxUpdate, ConnectOptions, SetOptions}; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 88939742..ae441793 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1722,48 +1722,47 @@ GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { if (!enablePerformanceMetrics) { return; } - // We are reassigning the functions directly so that internal function calls are also decorated + // We are reassigning the functions directly so that internal function calls are also decorated // @ts-expect-error Reassign initStoreValues = decorateWithMetrics(initStoreValues, 'OnyxUtils.initStoreValues'); + OnyxUtils.initStoreValues = initStoreValues; // @ts-expect-error Complex type signature get = decorateWithMetrics(get, 'OnyxUtils.get'); + OnyxUtils.get = get; // @ts-expect-error Reassign getAllKeys = decorateWithMetrics(getAllKeys, 'OnyxUtils.getAllKeys'); + OnyxUtils.getAllKeys = getAllKeys; + // @@ts-expect-error Reassign + // tryGetCachedValue = decorateWithMetrics(tryGetCachedValue, 'OnyxUtils.tryGetCachedValue'); + // OnyxUtils.tryGetCachedValue = tryGetCachedValue; // @ts-expect-error Reassign - getCollectionKeys = decorateWithMetrics(getCollectionKeys, 'OnyxUtils.getCollectionKeys'); + getCachedCollection = decorateWithMetrics(getCachedCollection, 'OnyxUtils.getCachedCollection'); + OnyxUtils.getCachedCollection = getCachedCollection; // @ts-expect-error Reassign keysChanged = decorateWithMetrics(keysChanged, 'OnyxUtils.keysChanged'); + OnyxUtils.keysChanged = keysChanged; // @ts-expect-error Reassign keyChanged = decorateWithMetrics(keyChanged, 'OnyxUtils.keyChanged'); + OnyxUtils.keyChanged = keyChanged; // @ts-expect-error Reassign sendDataToConnection = decorateWithMetrics(sendDataToConnection, 'OnyxUtils.sendDataToConnection'); - // @ts-expect-error Reassign - scheduleSubscriberUpdate = decorateWithMetrics(scheduleSubscriberUpdate, 'OnyxUtils.scheduleSubscriberUpdate'); - // @ts-expect-error Reassign - scheduleNotifyCollectionSubscribers = decorateWithMetrics(scheduleNotifyCollectionSubscribers, 'OnyxUtils.scheduleNotifyCollectionSubscribers'); + OnyxUtils.sendDataToConnection = sendDataToConnection; // @ts-expect-error Reassign remove = decorateWithMetrics(remove, 'OnyxUtils.remove'); + OnyxUtils.remove = remove; // @ts-expect-error Reassign - reportStorageQuota = decorateWithMetrics(reportStorageQuota, 'OnyxUtils.reportStorageQuota'); - // @ts-expect-error Complex type signature - retryOperation = decorateWithMetrics(retryOperation, 'OnyxUtils.retryOperation'); - // @ts-expect-error Reassign - broadcastUpdate = decorateWithMetrics(broadcastUpdate, 'OnyxUtils.broadcastUpdate'); + prepareKeyValuePairsForStorage = decorateWithMetrics(prepareKeyValuePairsForStorage, 'OnyxUtils.prepareKeyValuePairsForStorage'); + OnyxUtils.prepareKeyValuePairsForStorage = prepareKeyValuePairsForStorage; // @ts-expect-error Reassign initializeWithDefaultKeyStates = decorateWithMetrics(initializeWithDefaultKeyStates, 'OnyxUtils.initializeWithDefaultKeyStates'); - // @ts-expect-error Complex type signature - multiGet = decorateWithMetrics(multiGet, 'OnyxUtils.multiGet'); + OnyxUtils.initializeWithDefaultKeyStates = initializeWithDefaultKeyStates; // @ts-expect-error Reassign - tupleGet = decorateWithMetrics(tupleGet, 'OnyxUtils.tupleGet'); + multiGet = decorateWithMetrics(multiGet, 'OnyxUtils.multiGet'); + OnyxUtils.multiGet = multiGet; // @ts-expect-error Reassign subscribeToKey = decorateWithMetrics(subscribeToKey, 'OnyxUtils.subscribeToKey'); - // @ts-expect-error Reassign - setWithRetry = decorateWithMetrics(setWithRetry, 'OnyxUtils.setWithRetry'); - // @ts-expect-error Reassign - multiSetWithRetry = decorateWithMetrics(multiSetWithRetry, 'OnyxUtils.multiSetWithRetry'); - // @ts-expect-error Reassign - setCollectionWithRetry = decorateWithMetrics(setCollectionWithRetry, 'OnyxUtils.setCollectionWithRetry'); + OnyxUtils.subscribeToKey = subscribeToKey; }); export type {OnyxMethod}; diff --git a/lib/metrics.ts b/lib/metrics.ts index 5ae0c0cd..b38d9e7f 100644 --- a/lib/metrics.ts +++ b/lib/metrics.ts @@ -1,10 +1,15 @@ import PerformanceProxy from './dependencies/PerformanceProxy'; +type PerformanceMarkDetail = { + result?: unknown; + error?: unknown; +}; + /** * Capture a measurement between the start mark and now */ -function measureMarkToNow(startMark: PerformanceMark, detail: Record) { - PerformanceProxy.measure(`${startMark.name} [${startMark.detail.args.toString()}]`, { +function measureMarkToNow(startMark: PerformanceMark, detail?: PerformanceMarkDetail) { + PerformanceProxy.measure(startMark.name, { start: startMark.startTime, end: PerformanceProxy.now(), detail: {...startMark.detail, ...detail}, @@ -20,7 +25,7 @@ function isPromiseLike(value: unknown): value is Promise { */ function decorateWithMetrics(func: (...args: Args) => ReturnType, alias = func.name) { function decorated(...args: Args) { - const mark = PerformanceProxy.mark(alias, {detail: {args, alias}}); + const mark = PerformanceProxy.mark(alias, {detail: {alias}}); const originalReturnValue = func(...args); @@ -30,8 +35,8 @@ function decorateWithMetrics(func: (...args: * They create a separate chain that's not exposed (returned) to the original caller */ originalReturnValue - .then((result) => { - measureMarkToNow(mark, {result}); + .then(() => { + measureMarkToNow(mark); }) .catch((error) => { measureMarkToNow(mark, {error}); @@ -40,10 +45,9 @@ function decorateWithMetrics(func: (...args: return originalReturnValue; } - measureMarkToNow(mark, {result: originalReturnValue}); + measureMarkToNow(mark); return originalReturnValue; } - decorated.name = `${alias}_DECORATED`; return decorated; } diff --git a/lib/storage/providers/IDBKeyValProvider/index.ts b/lib/storage/providers/IDBKeyValProvider/index.ts index c140ed76..8d6e0661 100644 --- a/lib/storage/providers/IDBKeyValProvider/index.ts +++ b/lib/storage/providers/IDBKeyValProvider/index.ts @@ -4,6 +4,8 @@ import utils from '../../../utils'; import type StorageProvider from '../types'; import type {OnyxKey, OnyxValue} from '../../../types'; import createStore from './createStore'; +import * as GlobalSettings from '../../../GlobalSettings'; +import decorateWithMetrics from '../../../metrics'; const DB_NAME = 'OnyxDB'; const STORE_NAME = 'keyvaluepairs'; @@ -155,4 +157,22 @@ const provider: StorageProvider = { }, }; +GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { + if (!enablePerformanceMetrics) { + return; + } + + // Apply decorators + provider.getItem = decorateWithMetrics(provider.getItem, 'IDBKeyValProvider.getItem'); + provider.multiGet = decorateWithMetrics(provider.multiGet, 'IDBKeyValProvider.multiGet'); + provider.setItem = decorateWithMetrics(provider.setItem, 'IDBKeyValProvider.setItem'); + provider.multiSet = decorateWithMetrics(provider.multiSet, 'IDBKeyValProvider.multiSet'); + provider.mergeItem = decorateWithMetrics(provider.mergeItem, 'IDBKeyValProvider.mergeItem'); + provider.multiMerge = decorateWithMetrics(provider.multiMerge, 'IDBKeyValProvider.multiMerge'); + provider.removeItem = decorateWithMetrics(provider.removeItem, 'IDBKeyValProvider.removeItem'); + provider.removeItems = decorateWithMetrics(provider.removeItems, 'IDBKeyValProvider.removeItems'); + provider.clear = decorateWithMetrics(provider.clear, 'IDBKeyValProvider.clear'); + provider.getAllKeys = decorateWithMetrics(provider.getAllKeys, 'IDBKeyValProvider.getAllKeys'); +}); + export default provider; diff --git a/lib/storage/providers/MemoryOnlyProvider.ts b/lib/storage/providers/MemoryOnlyProvider.ts index 15e9c264..72275a26 100644 --- a/lib/storage/providers/MemoryOnlyProvider.ts +++ b/lib/storage/providers/MemoryOnlyProvider.ts @@ -3,6 +3,8 @@ import type {OnyxKey, OnyxValue} from '../../types'; import utils from '../../utils'; import type StorageProvider from './types'; import type {StorageKeyValuePair} from './types'; +import * as GlobalSettings from '../../GlobalSettings'; +import decorateWithMetrics from '../../metrics'; type Store = Record>; @@ -152,5 +154,23 @@ const setMockStore = (data: Store) => { Object.assign(storeInternal, data); }; +GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { + if (!enablePerformanceMetrics) { + return; + } + + // Apply decorators + provider.getItem = decorateWithMetrics(provider.getItem, 'MemoryOnlyProvider.getItem'); + provider.multiGet = decorateWithMetrics(provider.multiGet, 'MemoryOnlyProvider.multiGet'); + provider.setItem = decorateWithMetrics(provider.setItem, 'MemoryOnlyProvider.setItem'); + provider.multiSet = decorateWithMetrics(provider.multiSet, 'MemoryOnlyProvider.multiSet'); + provider.mergeItem = decorateWithMetrics(provider.mergeItem, 'MemoryOnlyProvider.mergeItem'); + provider.multiMerge = decorateWithMetrics(provider.multiMerge, 'MemoryOnlyProvider.multiMerge'); + provider.removeItem = decorateWithMetrics(provider.removeItem, 'MemoryOnlyProvider.removeItem'); + provider.removeItems = decorateWithMetrics(provider.removeItems, 'MemoryOnlyProvider.removeItems'); + provider.clear = decorateWithMetrics(provider.clear, 'MemoryOnlyProvider.clear'); + provider.getAllKeys = decorateWithMetrics(provider.getAllKeys, 'MemoryOnlyProvider.getAllKeys'); +}); + export default provider; export {set as mockSet, storeInternal as mockStore, setMockStore}; diff --git a/lib/storage/providers/SQLiteProvider.ts b/lib/storage/providers/SQLiteProvider.ts index 1d2927d8..b65e1bdb 100644 --- a/lib/storage/providers/SQLiteProvider.ts +++ b/lib/storage/providers/SQLiteProvider.ts @@ -9,6 +9,8 @@ import type {FastMergeReplaceNullPatch} from '../../utils'; import utils from '../../utils'; import type StorageProvider from './types'; import type {StorageKeyList, StorageKeyValuePair} from './types'; +import * as GlobalSettings from '../../GlobalSettings'; +import decorateWithMetrics from '../../metrics'; // By default, NitroSQLite does not accept nullish values due to current limitations in Nitro Modules. // This flag enables a feature in NitroSQLite that allows for nullish values to be passed to operations, such as "execute" or "executeBatch". @@ -235,5 +237,23 @@ const provider: StorageProvider = { }, }; +GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { + if (!enablePerformanceMetrics) { + return; + } + + // Apply decorators + provider.getItem = decorateWithMetrics(provider.getItem, 'SQLiteProvider.getItem'); + provider.multiGet = decorateWithMetrics(provider.multiGet, 'SQLiteProvider.multiGet'); + provider.setItem = decorateWithMetrics(provider.setItem, 'SQLiteProvider.setItem'); + provider.multiSet = decorateWithMetrics(provider.multiSet, 'SQLiteProvider.multiSet'); + provider.mergeItem = decorateWithMetrics(provider.mergeItem, 'SQLiteProvider.mergeItem'); + provider.multiMerge = decorateWithMetrics(provider.multiMerge, 'SQLiteProvider.multiMerge'); + provider.removeItem = decorateWithMetrics(provider.removeItem, 'SQLiteProvider.removeItem'); + provider.removeItems = decorateWithMetrics(provider.removeItems, 'SQLiteProvider.removeItems'); + provider.clear = decorateWithMetrics(provider.clear, 'SQLiteProvider.clear'); + provider.getAllKeys = decorateWithMetrics(provider.getAllKeys, 'SQLiteProvider.getAllKeys'); +}); + export default provider; export type {OnyxSQLiteKeyValuePair}; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index b1670e3c..d159c26a 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -5,10 +5,8 @@ import OnyxCache, {TASK} from './OnyxCache'; import type {Connection} from './OnyxConnectionManager'; import connectionManager from './OnyxConnectionManager'; import OnyxUtils from './OnyxUtils'; -import * as GlobalSettings from './GlobalSettings'; import type {CollectionKeyBase, OnyxKey, OnyxValue} from './types'; import usePrevious from './usePrevious'; -import decorateWithMetrics from './metrics'; import * as Logger from './Logger'; import onyxSnapshotCache from './OnyxSnapshotCache'; import useLiveRef from './useLiveRef'; @@ -380,19 +378,11 @@ function useOnyx>( [key, options?.initWithStoredValues, options?.reuseConnection, checkEvictableKey], ); - const getSnapshotDecorated = useMemo(() => { - if (!GlobalSettings.isPerformanceMetricsEnabled()) { - return getSnapshot; - } - - return decorateWithMetrics(getSnapshot, 'useOnyx.getSnapshot'); - }, [getSnapshot]); - useEffect(() => { checkEvictableKey(); }, [checkEvictableKey]); - const result = useSyncExternalStore>(subscribe, getSnapshotDecorated); + const result = useSyncExternalStore>(subscribe, getSnapshot); return result; }