Skip to content

Commit 769b622

Browse files
authored
Merge pull request Expensify#598 from margelo/chore/add-metrics
chore: re-enable performance metrics
2 parents 0ff5851 + 42055a0 commit 769b622

File tree

10 files changed

+250
-6
lines changed

10 files changed

+250
-6
lines changed

lib/GlobalSettings.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Stores settings from Onyx.init globally so they can be made accessible by other parts of the library.
3+
*/
4+
5+
const globalSettings = {
6+
enablePerformanceMetrics: false,
7+
};
8+
9+
type GlobalSettings = typeof globalSettings;
10+
11+
const listeners = new Set<(settings: GlobalSettings) => unknown>();
12+
function addGlobalSettingsChangeListener(listener: (settings: GlobalSettings) => unknown) {
13+
listeners.add(listener);
14+
return () => {
15+
listeners.delete(listener);
16+
};
17+
}
18+
19+
function notifyListeners() {
20+
listeners.forEach((listener) => listener(globalSettings));
21+
}
22+
23+
function setPerformanceMetricsEnabled(enabled: boolean) {
24+
globalSettings.enablePerformanceMetrics = enabled;
25+
notifyListeners();
26+
}
27+
28+
function isPerformanceMetricsEnabled() {
29+
return globalSettings.enablePerformanceMetrics;
30+
}
31+
32+
export {setPerformanceMetricsEnabled, isPerformanceMetricsEnabled, addGlobalSettingsChangeListener};

lib/Onyx.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import OnyxUtils from './OnyxUtils';
3232
import logMessages from './logMessages';
3333
import type {Connection} from './OnyxConnectionManager';
3434
import connectionManager from './OnyxConnectionManager';
35+
import * as GlobalSettings from './GlobalSettings';
36+
import decorateWithMetrics from './metrics';
3537

3638
/** Initialize the store with actions and listening for storage events */
3739
function init({
@@ -41,7 +43,13 @@ function init({
4143
maxCachedKeysCount = 1000,
4244
shouldSyncMultipleInstances = Boolean(global.localStorage),
4345
debugSetState = false,
46+
enablePerformanceMetrics = false,
4447
}: InitOptions): void {
48+
if (enablePerformanceMetrics) {
49+
GlobalSettings.setPerformanceMetricsEnabled(true);
50+
applyDecorators();
51+
}
52+
4553
Storage.init();
4654

4755
if (shouldSyncMultipleInstances) {
@@ -776,7 +784,27 @@ const Onyx = {
776784
clear,
777785
init,
778786
registerLogger: Logger.registerLogger,
779-
} as const;
787+
};
788+
789+
function applyDecorators() {
790+
// We are reassigning the functions directly so that internal function calls are also decorated
791+
/* eslint-disable rulesdir/prefer-actions-set-data */
792+
// @ts-expect-error Reassign
793+
connect = decorateWithMetrics(connect, 'Onyx.connect');
794+
// @ts-expect-error Reassign
795+
set = decorateWithMetrics(set, 'Onyx.set');
796+
// @ts-expect-error Reassign
797+
multiSet = decorateWithMetrics(multiSet, 'Onyx.multiSet');
798+
// @ts-expect-error Reassign
799+
merge = decorateWithMetrics(merge, 'Onyx.merge');
800+
// @ts-expect-error Reassign
801+
mergeCollection = decorateWithMetrics(mergeCollection, 'Onyx.mergeCollection');
802+
// @ts-expect-error Reassign
803+
update = decorateWithMetrics(update, 'Onyx.update');
804+
// @ts-expect-error Reassign
805+
clear = decorateWithMetrics(clear, 'Onyx.clear');
806+
/* eslint-enable rulesdir/prefer-actions-set-data */
807+
}
780808

781809
export default Onyx;
782810
export type {OnyxUpdate, Mapping, ConnectOptions};

lib/OnyxUtils.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import utils from './utils';
3232
import type {WithOnyxState} from './withOnyx/types';
3333
import type {DeferredTask} from './createDeferredTask';
3434
import createDeferredTask from './createDeferredTask';
35+
import * as GlobalSettings from './GlobalSettings';
36+
import decorateWithMetrics from './metrics';
3537

3638
// Method constants
3739
const METHOD = {
@@ -1418,6 +1420,51 @@ const OnyxUtils = {
14181420
getEvictionBlocklist,
14191421
};
14201422

1421-
export type {OnyxMethod};
1423+
GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {
1424+
if (!enablePerformanceMetrics) {
1425+
return;
1426+
}
1427+
// We are reassigning the functions directly so that internal function calls are also decorated
1428+
1429+
// @ts-expect-error Reassign
1430+
initStoreValues = decorateWithMetrics(initStoreValues, 'OnyxUtils.initStoreValues');
1431+
// @ts-expect-error Reassign
1432+
maybeFlushBatchUpdates = decorateWithMetrics(maybeFlushBatchUpdates, 'OnyxUtils.maybeFlushBatchUpdates');
1433+
// @ts-expect-error Reassign
1434+
batchUpdates = decorateWithMetrics(batchUpdates, 'OnyxUtils.batchUpdates');
1435+
// @ts-expect-error Complex type signature
1436+
get = decorateWithMetrics(get, 'OnyxUtils.get');
1437+
// @ts-expect-error Reassign
1438+
getAllKeys = decorateWithMetrics(getAllKeys, 'OnyxUtils.getAllKeys');
1439+
// @ts-expect-error Reassign
1440+
getCollectionKeys = decorateWithMetrics(getCollectionKeys, 'OnyxUtils.getCollectionKeys');
1441+
// @ts-expect-error Reassign
1442+
addAllSafeEvictionKeysToRecentlyAccessedList = decorateWithMetrics(addAllSafeEvictionKeysToRecentlyAccessedList, 'OnyxUtils.addAllSafeEvictionKeysToRecentlyAccessedList');
1443+
// @ts-expect-error Reassign
1444+
keysChanged = decorateWithMetrics(keysChanged, 'OnyxUtils.keysChanged');
1445+
// @ts-expect-error Reassign
1446+
keyChanged = decorateWithMetrics(keyChanged, 'OnyxUtils.keyChanged');
1447+
// @ts-expect-error Reassign
1448+
sendDataToConnection = decorateWithMetrics(sendDataToConnection, 'OnyxUtils.sendDataToConnection');
1449+
// @ts-expect-error Reassign
1450+
scheduleSubscriberUpdate = decorateWithMetrics(scheduleSubscriberUpdate, 'OnyxUtils.scheduleSubscriberUpdate');
1451+
// @ts-expect-error Reassign
1452+
scheduleNotifyCollectionSubscribers = decorateWithMetrics(scheduleNotifyCollectionSubscribers, 'OnyxUtils.scheduleNotifyCollectionSubscribers');
1453+
// @ts-expect-error Reassign
1454+
remove = decorateWithMetrics(remove, 'OnyxUtils.remove');
1455+
// @ts-expect-error Reassign
1456+
reportStorageQuota = decorateWithMetrics(reportStorageQuota, 'OnyxUtils.reportStorageQuota');
1457+
// @ts-expect-error Complex type signature
1458+
evictStorageAndRetry = decorateWithMetrics(evictStorageAndRetry, 'OnyxUtils.evictStorageAndRetry');
1459+
// @ts-expect-error Reassign
1460+
broadcastUpdate = decorateWithMetrics(broadcastUpdate, 'OnyxUtils.broadcastUpdate');
1461+
// @ts-expect-error Reassign
1462+
initializeWithDefaultKeyStates = decorateWithMetrics(initializeWithDefaultKeyStates, 'OnyxUtils.initializeWithDefaultKeyStates');
1463+
// @ts-expect-error Complex type signature
1464+
multiGet = decorateWithMetrics(multiGet, 'OnyxUtils.multiGet');
1465+
// @ts-expect-error Reassign
1466+
subscribeToKey = decorateWithMetrics(subscribeToKey, 'OnyxUtils.subscribeToKey');
1467+
});
14221468

1469+
export type {OnyxMethod};
14231470
export default OnyxUtils;

lib/dependencies/ModuleProxy.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
type ImportType = ReturnType<typeof require>;
2+
3+
/**
4+
* Create a lazily-imported module proxy.
5+
* This is useful for lazily requiring optional dependencies.
6+
*/
7+
const createModuleProxy = <TModule>(getModule: () => ImportType): TModule => {
8+
const holder: {module: TModule | undefined} = {module: undefined};
9+
10+
const proxy = new Proxy(holder, {
11+
get: (target, property) => {
12+
if (property === '$$typeof') {
13+
// If inlineRequires is enabled, Metro will look up all imports
14+
// with the $$typeof operator. In this case, this will throw the
15+
// `OptionalDependencyNotInstalledError` error because we try to access the module
16+
// even though we are not using it (Metro does it), so instead we return undefined
17+
// to bail out of inlineRequires here.
18+
return undefined;
19+
}
20+
21+
if (target.module == null) {
22+
// lazy initialize module via require()
23+
// caller needs to make sure the require() call is wrapped in a try/catch
24+
// eslint-disable-next-line no-param-reassign
25+
target.module = getModule() as TModule;
26+
}
27+
return target.module[property as keyof typeof holder.module];
28+
},
29+
});
30+
return proxy as unknown as TModule;
31+
};
32+
33+
class OptionalDependencyNotInstalledError extends Error {
34+
constructor(name: string) {
35+
super(`${name} is not installed!`);
36+
}
37+
}
38+
39+
export {createModuleProxy, OptionalDependencyNotInstalledError};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type performance from 'react-native-performance';
2+
import {createModuleProxy, OptionalDependencyNotInstalledError} from '../ModuleProxy';
3+
4+
const PerformanceProxy = createModuleProxy<typeof performance>(() => {
5+
try {
6+
// eslint-disable-next-line @typescript-eslint/no-var-requires
7+
return require('react-native-performance').default;
8+
} catch {
9+
throw new OptionalDependencyNotInstalledError('react-native-performance');
10+
}
11+
});
12+
13+
export default PerformanceProxy;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Use the existing performance API on web
2+
export default performance;

lib/metrics.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import PerformanceProxy from './dependencies/PerformanceProxy';
2+
3+
const decoratedAliases = new Set();
4+
5+
/**
6+
* Capture a measurement between the start mark and now
7+
*/
8+
function measureMarkToNow(startMark: PerformanceMark, detail: Record<string, unknown>) {
9+
PerformanceProxy.measure(`${startMark.name} [${startMark.detail.args.toString()}]`, {
10+
start: startMark.startTime,
11+
end: PerformanceProxy.now(),
12+
detail: {...startMark.detail, ...detail},
13+
});
14+
}
15+
16+
function isPromiseLike(value: unknown): value is Promise<unknown> {
17+
return value != null && typeof value === 'object' && 'then' in value;
18+
}
19+
20+
/**
21+
* Wraps a function with metrics capturing logic
22+
*/
23+
function decorateWithMetrics<Args extends unknown[], ReturnType>(func: (...args: Args) => ReturnType, alias = func.name) {
24+
if (decoratedAliases.has(alias)) {
25+
throw new Error(`"${alias}" is already decorated`);
26+
}
27+
28+
decoratedAliases.add(alias);
29+
function decorated(...args: Args) {
30+
const mark = PerformanceProxy.mark(alias, {detail: {args, alias}});
31+
32+
const originalReturnValue = func(...args);
33+
34+
if (isPromiseLike(originalReturnValue)) {
35+
/*
36+
* The handlers added here are not affecting the original promise
37+
* They create a separate chain that's not exposed (returned) to the original caller
38+
*/
39+
originalReturnValue
40+
.then((result) => {
41+
measureMarkToNow(mark, {result});
42+
})
43+
.catch((error) => {
44+
measureMarkToNow(mark, {error});
45+
});
46+
47+
return originalReturnValue;
48+
}
49+
50+
measureMarkToNow(mark, {result: originalReturnValue});
51+
return originalReturnValue;
52+
}
53+
decorated.name = `${alias}_DECORATED`;
54+
55+
return decorated;
56+
}
57+
58+
export default decorateWithMetrics;

lib/storage/index.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import PlatformStorage from './platforms';
44
import InstanceSync from './InstanceSync';
55
import MemoryOnlyProvider from './providers/MemoryOnlyProvider';
66
import type StorageProvider from './providers/types';
7+
import * as GlobalSettings from '../GlobalSettings';
8+
import decorateWithMetrics from '../metrics';
79

810
let provider = PlatformStorage;
911
let shouldKeepInstancesSync = false;
@@ -55,7 +57,7 @@ function tryOrDegradePerformance<T>(fn: () => Promise<T> | T, waitForInitializat
5557
});
5658
}
5759

58-
const Storage: Storage = {
60+
const storage: Storage = {
5961
/**
6062
* Returns the storage provider currently in use
6163
*/
@@ -202,4 +204,22 @@ const Storage: Storage = {
202204
},
203205
};
204206

205-
export default Storage;
207+
GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {
208+
if (!enablePerformanceMetrics) {
209+
return;
210+
}
211+
212+
// Apply decorators
213+
storage.getItem = decorateWithMetrics(storage.getItem, 'Storage.getItem');
214+
storage.multiGet = decorateWithMetrics(storage.multiGet, 'Storage.multiGet');
215+
storage.setItem = decorateWithMetrics(storage.setItem, 'Storage.setItem');
216+
storage.multiSet = decorateWithMetrics(storage.multiSet, 'Storage.multiSet');
217+
storage.mergeItem = decorateWithMetrics(storage.mergeItem, 'Storage.mergeItem');
218+
storage.multiMerge = decorateWithMetrics(storage.multiMerge, 'Storage.multiMerge');
219+
storage.removeItem = decorateWithMetrics(storage.removeItem, 'Storage.removeItem');
220+
storage.removeItems = decorateWithMetrics(storage.removeItems, 'Storage.removeItems');
221+
storage.clear = decorateWithMetrics(storage.clear, 'Storage.clear');
222+
storage.getAllKeys = decorateWithMetrics(storage.getAllKeys, 'Storage.getAllKeys');
223+
});
224+
225+
export default storage;

lib/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,12 @@ type InitOptions = {
468468

469469
/** Enables debugging setState() calls to connected components */
470470
debugSetState?: boolean;
471+
472+
/**
473+
* If enabled it will use the performance API to measure the time taken by Onyx operations.
474+
* @default false
475+
*/
476+
enablePerformanceMetrics?: boolean;
471477
};
472478

473479
// eslint-disable-next-line @typescript-eslint/no-explicit-any

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
"README.md",
2626
"LICENSE.md"
2727
],
28-
"main": "dist/index.js",
29-
"types": "dist/index.d.ts",
28+
"main": "lib/index.ts",
3029
"scripts": {
3130
"lint": "eslint .",
3231
"typecheck": "tsc --noEmit",

0 commit comments

Comments
 (0)