Skip to content

Commit

Permalink
Merge pull request #598 from margelo/chore/add-metrics
Browse files Browse the repository at this point in the history
chore: re-enable performance metrics
  • Loading branch information
deetergp authored Dec 4, 2024
2 parents 0ff5851 + 42055a0 commit 769b622
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 6 deletions.
32 changes: 32 additions & 0 deletions lib/GlobalSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Stores settings from Onyx.init globally so they can be made accessible by other parts of the library.
*/

const globalSettings = {
enablePerformanceMetrics: false,
};

type GlobalSettings = typeof globalSettings;

const listeners = new Set<(settings: GlobalSettings) => unknown>();
function addGlobalSettingsChangeListener(listener: (settings: GlobalSettings) => unknown) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}

function notifyListeners() {
listeners.forEach((listener) => listener(globalSettings));
}

function setPerformanceMetricsEnabled(enabled: boolean) {
globalSettings.enablePerformanceMetrics = enabled;
notifyListeners();
}

function isPerformanceMetricsEnabled() {
return globalSettings.enablePerformanceMetrics;
}

export {setPerformanceMetricsEnabled, isPerformanceMetricsEnabled, addGlobalSettingsChangeListener};
30 changes: 29 additions & 1 deletion lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import OnyxUtils from './OnyxUtils';
import logMessages from './logMessages';
import type {Connection} from './OnyxConnectionManager';
import connectionManager from './OnyxConnectionManager';
import * as GlobalSettings from './GlobalSettings';
import decorateWithMetrics from './metrics';

/** Initialize the store with actions and listening for storage events */
function init({
Expand All @@ -41,7 +43,13 @@ function init({
maxCachedKeysCount = 1000,
shouldSyncMultipleInstances = Boolean(global.localStorage),
debugSetState = false,
enablePerformanceMetrics = false,
}: InitOptions): void {
if (enablePerformanceMetrics) {
GlobalSettings.setPerformanceMetricsEnabled(true);
applyDecorators();
}

Storage.init();

if (shouldSyncMultipleInstances) {
Expand Down Expand Up @@ -776,7 +784,27 @@ const Onyx = {
clear,
init,
registerLogger: Logger.registerLogger,
} as const;
};

function applyDecorators() {
// 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');
// @ts-expect-error Reassign
set = decorateWithMetrics(set, 'Onyx.set');
// @ts-expect-error Reassign
multiSet = decorateWithMetrics(multiSet, 'Onyx.multiSet');
// @ts-expect-error Reassign
merge = decorateWithMetrics(merge, 'Onyx.merge');
// @ts-expect-error Reassign
mergeCollection = decorateWithMetrics(mergeCollection, 'Onyx.mergeCollection');
// @ts-expect-error Reassign
update = decorateWithMetrics(update, 'Onyx.update');
// @ts-expect-error Reassign
clear = decorateWithMetrics(clear, 'Onyx.clear');
/* eslint-enable rulesdir/prefer-actions-set-data */
}

export default Onyx;
export type {OnyxUpdate, Mapping, ConnectOptions};
49 changes: 48 additions & 1 deletion lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import utils from './utils';
import type {WithOnyxState} from './withOnyx/types';
import type {DeferredTask} from './createDeferredTask';
import createDeferredTask from './createDeferredTask';
import * as GlobalSettings from './GlobalSettings';
import decorateWithMetrics from './metrics';

// Method constants
const METHOD = {
Expand Down Expand Up @@ -1418,6 +1420,51 @@ const OnyxUtils = {
getEvictionBlocklist,
};

export type {OnyxMethod};
GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {
if (!enablePerformanceMetrics) {
return;
}
// We are reassigning the functions directly so that internal function calls are also decorated

// @ts-expect-error Reassign
initStoreValues = decorateWithMetrics(initStoreValues, 'OnyxUtils.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');
// @ts-expect-error Reassign
getAllKeys = decorateWithMetrics(getAllKeys, 'OnyxUtils.getAllKeys');
// @ts-expect-error Reassign
getCollectionKeys = decorateWithMetrics(getCollectionKeys, 'OnyxUtils.getCollectionKeys');
// @ts-expect-error Reassign
addAllSafeEvictionKeysToRecentlyAccessedList = decorateWithMetrics(addAllSafeEvictionKeysToRecentlyAccessedList, 'OnyxUtils.addAllSafeEvictionKeysToRecentlyAccessedList');
// @ts-expect-error Reassign
keysChanged = decorateWithMetrics(keysChanged, 'OnyxUtils.keysChanged');
// @ts-expect-error Reassign
keyChanged = decorateWithMetrics(keyChanged, 'OnyxUtils.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');
// @ts-expect-error Reassign
remove = decorateWithMetrics(remove, 'OnyxUtils.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');
// @ts-expect-error Reassign
initializeWithDefaultKeyStates = decorateWithMetrics(initializeWithDefaultKeyStates, 'OnyxUtils.initializeWithDefaultKeyStates');
// @ts-expect-error Complex type signature
multiGet = decorateWithMetrics(multiGet, 'OnyxUtils.multiGet');
// @ts-expect-error Reassign
subscribeToKey = decorateWithMetrics(subscribeToKey, 'OnyxUtils.subscribeToKey');
});

export type {OnyxMethod};
export default OnyxUtils;
39 changes: 39 additions & 0 deletions lib/dependencies/ModuleProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
type ImportType = ReturnType<typeof require>;

/**
* Create a lazily-imported module proxy.
* This is useful for lazily requiring optional dependencies.
*/
const createModuleProxy = <TModule>(getModule: () => ImportType): TModule => {
const holder: {module: TModule | undefined} = {module: undefined};

const proxy = new Proxy(holder, {
get: (target, property) => {
if (property === '$$typeof') {
// If inlineRequires is enabled, Metro will look up all imports
// with the $$typeof operator. In this case, this will throw the
// `OptionalDependencyNotInstalledError` error because we try to access the module
// even though we are not using it (Metro does it), so instead we return undefined
// to bail out of inlineRequires here.
return undefined;
}

if (target.module == null) {
// lazy initialize module via require()
// caller needs to make sure the require() call is wrapped in a try/catch
// eslint-disable-next-line no-param-reassign
target.module = getModule() as TModule;
}
return target.module[property as keyof typeof holder.module];
},
});
return proxy as unknown as TModule;
};

class OptionalDependencyNotInstalledError extends Error {
constructor(name: string) {
super(`${name} is not installed!`);
}
}

export {createModuleProxy, OptionalDependencyNotInstalledError};
13 changes: 13 additions & 0 deletions lib/dependencies/PerformanceProxy/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type performance from 'react-native-performance';
import {createModuleProxy, OptionalDependencyNotInstalledError} from '../ModuleProxy';

const PerformanceProxy = createModuleProxy<typeof performance>(() => {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('react-native-performance').default;
} catch {
throw new OptionalDependencyNotInstalledError('react-native-performance');
}
});

export default PerformanceProxy;
2 changes: 2 additions & 0 deletions lib/dependencies/PerformanceProxy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Use the existing performance API on web
export default performance;
58 changes: 58 additions & 0 deletions lib/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import PerformanceProxy from './dependencies/PerformanceProxy';

const decoratedAliases = new Set();

/**
* Capture a measurement between the start mark and now
*/
function measureMarkToNow(startMark: PerformanceMark, detail: Record<string, unknown>) {
PerformanceProxy.measure(`${startMark.name} [${startMark.detail.args.toString()}]`, {
start: startMark.startTime,
end: PerformanceProxy.now(),
detail: {...startMark.detail, ...detail},
});
}

function isPromiseLike(value: unknown): value is Promise<unknown> {
return value != null && typeof value === 'object' && 'then' in value;
}

/**
* Wraps a function with metrics capturing logic
*/
function decorateWithMetrics<Args extends unknown[], ReturnType>(func: (...args: Args) => ReturnType, alias = func.name) {
if (decoratedAliases.has(alias)) {
throw new Error(`"${alias}" is already decorated`);
}

decoratedAliases.add(alias);
function decorated(...args: Args) {
const mark = PerformanceProxy.mark(alias, {detail: {args, alias}});

const originalReturnValue = func(...args);

if (isPromiseLike(originalReturnValue)) {
/*
* The handlers added here are not affecting the original promise
* They create a separate chain that's not exposed (returned) to the original caller
*/
originalReturnValue
.then((result) => {
measureMarkToNow(mark, {result});
})
.catch((error) => {
measureMarkToNow(mark, {error});
});

return originalReturnValue;
}

measureMarkToNow(mark, {result: originalReturnValue});
return originalReturnValue;
}
decorated.name = `${alias}_DECORATED`;

return decorated;
}

export default decorateWithMetrics;
24 changes: 22 additions & 2 deletions lib/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import PlatformStorage from './platforms';
import InstanceSync from './InstanceSync';
import MemoryOnlyProvider from './providers/MemoryOnlyProvider';
import type StorageProvider from './providers/types';
import * as GlobalSettings from '../GlobalSettings';
import decorateWithMetrics from '../metrics';

let provider = PlatformStorage;
let shouldKeepInstancesSync = false;
Expand Down Expand Up @@ -55,7 +57,7 @@ function tryOrDegradePerformance<T>(fn: () => Promise<T> | T, waitForInitializat
});
}

const Storage: Storage = {
const storage: Storage = {
/**
* Returns the storage provider currently in use
*/
Expand Down Expand Up @@ -202,4 +204,22 @@ const Storage: Storage = {
},
};

export default Storage;
GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {
if (!enablePerformanceMetrics) {
return;
}

// Apply decorators
storage.getItem = decorateWithMetrics(storage.getItem, 'Storage.getItem');
storage.multiGet = decorateWithMetrics(storage.multiGet, 'Storage.multiGet');
storage.setItem = decorateWithMetrics(storage.setItem, 'Storage.setItem');
storage.multiSet = decorateWithMetrics(storage.multiSet, 'Storage.multiSet');
storage.mergeItem = decorateWithMetrics(storage.mergeItem, 'Storage.mergeItem');
storage.multiMerge = decorateWithMetrics(storage.multiMerge, 'Storage.multiMerge');
storage.removeItem = decorateWithMetrics(storage.removeItem, 'Storage.removeItem');
storage.removeItems = decorateWithMetrics(storage.removeItems, 'Storage.removeItems');
storage.clear = decorateWithMetrics(storage.clear, 'Storage.clear');
storage.getAllKeys = decorateWithMetrics(storage.getAllKeys, 'Storage.getAllKeys');
});

export default storage;
6 changes: 6 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,12 @@ type InitOptions = {

/** Enables debugging setState() calls to connected components */
debugSetState?: boolean;

/**
* If enabled it will use the performance API to measure the time taken by Onyx operations.
* @default false
*/
enablePerformanceMetrics?: boolean;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
"README.md",
"LICENSE.md"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "lib/index.ts",
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
Expand Down

0 comments on commit 769b622

Please sign in to comment.