diff --git a/.eslintrc.js b/.eslintrc.js index f821efb79..b54473bfe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -68,6 +68,8 @@ module.exports = { }, ], '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 68617b7ed..948a3192e 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 a826bf44c..18f2e2fa1 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -47,7 +47,7 @@ function init({ }: InitOptions): void { if (enablePerformanceMetrics) { GlobalSettings.setPerformanceMetricsEnabled(true); - applyDecorators(); + applyPerformanceMetricsDecorators(); } Storage.init(); @@ -777,27 +777,47 @@ const Onyx = { registerLogger: Logger.registerLogger, }; -function applyDecorators() { +function applyPerformanceMetricsDecorators() { // We are reassigning the functions directly so that internal function calls are also decorated - /* eslint-disable rulesdir/prefer-actions-set-data */ // @ts-expect-error Reassign connect = decorateWithMetrics(connect, 'Onyx.connect'); + Onyx.connect = connect; // @ts-expect-error Reassign connectWithoutView = decorateWithMetrics(connectWithoutView, 'Onyx.connectWithoutView'); + Onyx.connectWithoutView = 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'); - /* eslint-enable rulesdir/prefer-actions-set-data */ + 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, Mapping, ConnectOptions, SetOptions}; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 04b7e2fa6..def65cae2 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1701,46 +1701,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 Reassign - maybeFlushBatchUpdates = decorateWithMetrics(maybeFlushBatchUpdates, 'OnyxUtils.maybeFlushBatchUpdates'); - // @ts-expect-error Reassign - batchUpdates = decorateWithMetrics(batchUpdates, 'OnyxUtils.batchUpdates'); - // @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 - evictStorageAndRetry = decorateWithMetrics(evictStorageAndRetry, 'OnyxUtils.evictStorageAndRetry'); - // @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'); + OnyxUtils.subscribeToKey = subscribeToKey; }); export type {OnyxMethod}; diff --git a/lib/metrics.ts b/lib/metrics.ts index 5ae0c0cdc..0f4849a93 100644 --- a/lib/metrics.ts +++ b/lib/metrics.ts @@ -1,14 +1,21 @@ import PerformanceProxy from './dependencies/PerformanceProxy'; +import * as Logger from './Logger'; + +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) { + const measurement = PerformanceProxy.measure(startMark.name, { start: startMark.startTime, end: PerformanceProxy.now(), detail: {...startMark.detail, ...detail}, }); + Logger.logInfo(`Performance - ${measurement.name}: ${measurement.duration} ms`, {isPerformanceMetric: true}); } function isPromiseLike(value: unknown): value is Promise { @@ -20,7 +27,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); @@ -43,7 +50,6 @@ function decorateWithMetrics(func: (...args: measureMarkToNow(mark, {result: originalReturnValue}); return originalReturnValue; } - decorated.name = `${alias}_DECORATED`; return decorated; } diff --git a/lib/storage/providers/IDBKeyValProvider.ts b/lib/storage/providers/IDBKeyValProvider.ts index 0fd2a63ec..0c2a42359 100644 --- a/lib/storage/providers/IDBKeyValProvider.ts +++ b/lib/storage/providers/IDBKeyValProvider.ts @@ -3,6 +3,8 @@ import {set, keys, getMany, setMany, get, clear, del, delMany, createStore, prom import utils from '../../utils'; import type StorageProvider from './types'; import type {OnyxKey, OnyxValue} from '../../types'; +import * as GlobalSettings from '../../GlobalSettings'; +import decorateWithMetrics from '../../metrics'; // We don't want to initialize the store while the JS bundle loads as idb-keyval will try to use global.indexedDB // which might not be available in certain environments that load the bundle (e.g. electron main process). @@ -100,4 +102,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 2367e972e..c09bf3997 100644 --- a/lib/storage/providers/MemoryOnlyProvider.ts +++ b/lib/storage/providers/MemoryOnlyProvider.ts @@ -3,6 +3,8 @@ import utils from '../../utils'; import type StorageProvider from './types'; import type {StorageKeyValuePair} from './types'; import type {OnyxKey, OnyxValue} from '../../types'; +import * as GlobalSettings from '../../GlobalSettings'; +import decorateWithMetrics from '../../metrics'; type Store = Record>; @@ -144,5 +146,23 @@ const setMockStore = (data: Store) => { store = 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 {store as mockStore, set as mockSet, setMockStore}; diff --git a/lib/storage/providers/SQLiteProvider.ts b/lib/storage/providers/SQLiteProvider.ts index 859176505..3f44adc1d 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". @@ -58,12 +60,22 @@ function objectMarkRemover(key: string, value: unknown) { function generateJSONReplaceSQLQueries(key: string, patches: FastMergeReplaceNullPatch[]): string[][] { const queries = patches.map(([pathArray, value]) => { const jsonPath = `$.${pathArray.join('.')}`; - return [jsonPath, JSON.stringify(value), key]; + return [jsonPath, stringifyJSON(value), key]; }); return queries; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function stringifyJSON(data: any, replacer?: (key: string, value: any) => any): string { + return JSON.stringify(data, replacer); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function parseJSON(text: string): any { + return JSON.parse(text); +} + const provider: StorageProvider = { /** * The name of the provider that can be printed to the logs @@ -94,7 +106,7 @@ const provider: StorageProvider = { return null; } - return JSON.parse(result.valueJSON); + return parseJSON(result.valueJSON); }); }, multiGet(keys) { @@ -102,16 +114,16 @@ const provider: StorageProvider = { const command = `SELECT record_key, valueJSON FROM keyvaluepairs WHERE record_key IN (${placeholders});`; return db.executeAsync(command, keys).then(({rows}) => { // eslint-disable-next-line no-underscore-dangle - const result = rows?._array.map((row) => [row.record_key, JSON.parse(row.valueJSON)]); + const result = rows?._array.map((row) => [row.record_key, parseJSON(row.valueJSON)]); return (result ?? []) as StorageKeyValuePair[]; }); }, setItem(key, value) { - return db.executeAsync('REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, ?);', [key, JSON.stringify(value)]).then(() => undefined); + return db.executeAsync('REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, ?);', [key, stringifyJSON(value)]).then(() => undefined); }, multiSet(pairs) { const query = 'REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, json(?));'; - const params = pairs.map((pair) => [pair[0], JSON.stringify(pair[1] === undefined ? null : pair[1])]); + const params = pairs.map((pair) => [pair[0], stringifyJSON(pair[1] === undefined ? null : pair[1])]); if (utils.isEmptyObject(params)) { return Promise.resolve(); } @@ -138,7 +150,7 @@ const provider: StorageProvider = { const nonNullishPairs = pairs.filter((pair) => pair[1] !== undefined); for (const [key, value, replaceNullPatches] of nonNullishPairs) { - const changeWithoutMarkers = JSON.stringify(value, objectMarkRemover); + const changeWithoutMarkers = stringifyJSON(value, objectMarkRemover); patchQueryArguments.push([key, changeWithoutMarkers]); const patches = replaceNullPatches ?? []; @@ -189,5 +201,29 @@ 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'); + // @ts-expect-error Reassign + generateJSONReplaceSQLQueries = decorateWithMetrics(generateJSONReplaceSQLQueries, 'SQLiteProvider.generateJSONReplaceSQLQueries'); + // @ts-expect-error Reassign + stringifyJSON = decorateWithMetrics(stringifyJSON, 'SQLiteProvider.stringifyJSON'); + // @ts-expect-error Reassign + parseJSON = decorateWithMetrics(parseJSON, 'SQLiteProvider.parseJSON'); +}); + export default provider; export type {OnyxSQLiteKeyValuePair}; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 462a61899..3771c47b6 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'; @@ -378,19 +376,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; }