Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: re-enable performance metrics #598

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ love this!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i only hate the ts-expect-error suppressions 🤔

// @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