Reference implementation (mock) for NativePerformanceObserver (#36116)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/36116

[Changelog][Internal]

Add a minimal/reference JavaScript implementation for NativePerformanceObserver - the purpose is both unit testing (JS and native sides separately) and potentially shimming the part of functionality that is not dependent on native side.

This is both a setup for adding general unit tests for the Performance* APIs, but also to be able to do non-trivial changes on JS side for WebPerformance (such as in (D43154319).

Reviewed By: rubennorte

Differential Revision: D43167392

fbshipit-source-id: 213d9534d810dece1dd464f910e92e08dbf39508
This commit is contained in:
Ruslan Shestopalyuk
2023-02-15 06:03:12 -08:00
committed by Facebook GitHub Bot
parent 96fb708d3e
commit f76d4dee6f
8 changed files with 261 additions and 71 deletions
@@ -45,4 +45,10 @@ void NativePerformanceObserver::setOnPerformanceEntryCallback(
PerformanceEntryReporter::getInstance().setReportingCallback(callback);
}
void NativePerformanceObserver::logRawEntry(
jsi::Runtime &rt,
RawPerformanceEntry entry) {
PerformanceEntryReporter::getInstance().logEntry(entry);
}
} // namespace facebook::react
@@ -69,6 +69,8 @@ class NativePerformanceObserver
jsi::Runtime &rt,
std::optional<AsyncCallback<>> callback);
void logRawEntry(jsi::Runtime &rt, RawPerformanceEntry entry);
private:
};
@@ -12,13 +12,6 @@ import type {TurboModule} from '../TurboModule/RCTExport';
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
export const RawPerformanceEntryTypeValues = {
UNDEFINED: 0,
MARK: 1,
MEASURE: 2,
EVENT: 3,
};
export type RawPerformanceEntryType = number;
export type RawPerformanceEntry = {|
@@ -8,74 +8,15 @@
* @flow strict
*/
import type {
RawPerformanceEntry,
RawPerformanceEntryType,
} from './NativePerformanceObserver';
import type {PerformanceEntryType} from './PerformanceEntry';
import warnOnce from '../Utilities/warnOnce';
import NativePerformanceObserver, {
RawPerformanceEntryTypeValues,
} from './NativePerformanceObserver';
import NativePerformanceObserver from './NativePerformanceObserver';
import {PerformanceEntry} from './PerformanceEntry';
import {PerformanceEventTiming} from './PerformanceEventTiming';
function rawToPerformanceEntryType(
type: RawPerformanceEntryType,
): PerformanceEntryType {
switch (type) {
case RawPerformanceEntryTypeValues.MARK:
return 'mark';
case RawPerformanceEntryTypeValues.MEASURE:
return 'measure';
case RawPerformanceEntryTypeValues.EVENT:
return 'event';
default:
throw new TypeError(
`rawToPerformanceEntryType: unexpected performance entry type received: ${type}`,
);
}
}
function performanceEntryTypeToRaw(
type: PerformanceEntryType,
): RawPerformanceEntryType {
switch (type) {
case 'mark':
return RawPerformanceEntryTypeValues.MARK;
case 'measure':
return RawPerformanceEntryTypeValues.MEASURE;
case 'event':
return RawPerformanceEntryTypeValues.EVENT;
default:
// Verify exhaustive check with Flow
(type: empty);
throw new TypeError(
`performanceEntryTypeToRaw: unexpected performance entry type received: ${type}`,
);
}
}
function rawToPerformanceEntry(entry: RawPerformanceEntry): PerformanceEntry {
if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) {
return new PerformanceEventTiming({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
interactionId: entry.interactionId,
});
} else {
return new PerformanceEntry({
name: entry.name,
entryType: rawToPerformanceEntryType(entry.entryType),
startTime: entry.startTime,
duration: entry.duration,
});
}
}
import {
performanceEntryTypeToRaw,
rawToPerformanceEntry,
} from './RawPerformanceEntry';
export type PerformanceEntryList = $ReadOnlyArray<PerformanceEntry>;
@@ -0,0 +1,87 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/
import type {
RawPerformanceEntry,
RawPerformanceEntryType,
} from './NativePerformanceObserver';
import type {PerformanceEntryType} from './PerformanceEntry';
import {PerformanceEntry} from './PerformanceEntry';
import {PerformanceEventTiming} from './PerformanceEventTiming';
export const RawPerformanceEntryTypeValues = {
UNDEFINED: 0,
MARK: 1,
MEASURE: 2,
EVENT: 3,
};
export function rawToPerformanceEntry(
entry: RawPerformanceEntry,
): PerformanceEntry {
if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) {
return new PerformanceEventTiming({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
interactionId: entry.interactionId,
});
} else {
return new PerformanceEntry({
name: entry.name,
entryType: rawToPerformanceEntryType(entry.entryType),
startTime: entry.startTime,
duration: entry.duration,
});
}
}
export function rawToPerformanceEntryType(
type: RawPerformanceEntryType,
): PerformanceEntryType {
switch (type) {
case RawPerformanceEntryTypeValues.MARK:
return 'mark';
case RawPerformanceEntryTypeValues.MEASURE:
return 'measure';
case RawPerformanceEntryTypeValues.EVENT:
return 'event';
case RawPerformanceEntryTypeValues.UNDEFINED:
throw new TypeError(
"rawToPerformanceEntryType: UNDEFINED can't be cast to PerformanceEntryType",
);
default:
throw new TypeError(
`rawToPerformanceEntryType: unexpected performance entry type received: ${type}`,
);
}
}
export function performanceEntryTypeToRaw(
type: PerformanceEntryType,
): RawPerformanceEntryType {
switch (type) {
case 'mark':
return RawPerformanceEntryTypeValues.MARK;
case 'measure':
return RawPerformanceEntryTypeValues.MEASURE;
case 'event':
return RawPerformanceEntryTypeValues.EVENT;
default:
// Verify exhaustive check with Flow
(type: empty);
throw new TypeError(
`performanceEntryTypeToRaw: unexpected performance entry type received: ${type}`,
);
}
}
@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import type {
GetPendingEntriesResult,
RawPerformanceEntry,
RawPerformanceEntryType,
Spec as NativePerformanceObserver,
} from '../NativePerformanceObserver';
const reportingType: Set<RawPerformanceEntryType> = new Set();
let entries: Array<RawPerformanceEntry> = [];
let onPerformanceEntryCallback: ?() => void;
const NativePerformanceObserverMock: NativePerformanceObserver = {
startReporting: (entryType: RawPerformanceEntryType) => {
reportingType.add(entryType);
},
stopReporting: (entryType: RawPerformanceEntryType) => {
reportingType.delete(entryType);
},
popPendingEntries: (): GetPendingEntriesResult => {
const res = entries;
entries = [];
return {
droppedEntriesCount: 0,
entries: res,
};
},
setOnPerformanceEntryCallback: (callback?: () => void) => {
onPerformanceEntryCallback = callback;
},
logRawEntry: (entry: RawPerformanceEntry) => {
if (reportingType.has(entry.entryType)) {
entries.push(entry);
// $FlowFixMe[incompatible-call]
global.queueMicrotask(() => {
// We want to emulate the way it's done in native (i.e. async/batched)
onPerformanceEntryCallback?.();
});
}
},
};
export default NativePerformanceObserverMock;
@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/
import NativePerformanceObserverMock from '../__mocks__/NativePerformanceObserver';
import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry';
describe('NativePerformanceObserver', () => {
it('correctly starts and stops listening to entries in a nominal scenario', async () => {
NativePerformanceObserverMock.startReporting(
RawPerformanceEntryTypeValues.MARK,
);
NativePerformanceObserverMock.logRawEntry({
name: 'mark1',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 10,
});
NativePerformanceObserverMock.logRawEntry({
name: 'mark2',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 20,
});
NativePerformanceObserverMock.logRawEntry({
name: 'event1',
entryType: RawPerformanceEntryTypeValues.EVENT,
startTime: 0,
duration: 20,
});
const entriesResult = NativePerformanceObserverMock.popPendingEntries();
expect(entriesResult).not.toBe(undefined);
const entries = entriesResult.entries;
expect(entries.length).toBe(2);
expect(entries[0].name).toBe('mark1');
expect(entries[1].name).toBe('mark2');
const entriesResult1 = NativePerformanceObserverMock.popPendingEntries();
expect(entriesResult1).not.toBe(undefined);
const entries1 = entriesResult1.entries;
expect(entries1.length).toBe(0);
NativePerformanceObserverMock.stopReporting('mark');
});
});
@@ -0,0 +1,49 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/
import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry';
// NOTE: Jest mocks of transitive dependencies don't appear to work with
// ES6 module imports, therefore forced to use commonjs style imports here.
const NativePerformanceObserver = require('../NativePerformanceObserver');
const PerformanceObserver = require('../PerformanceObserver').default;
jest.mock(
'../NativePerformanceObserver',
() => require('../__mocks__/NativePerformanceObserver').default,
);
describe('PerformanceObserver', () => {
it('can be mocked by a reference NativePerformanceObserver implementation', async () => {
expect(NativePerformanceObserver).not.toBe(undefined);
let totalEntries = 0;
const observer = new PerformanceObserver((list, _observer) => {
const entries = list.getEntries();
expect(entries).toHaveLength(1);
const entry = entries[0];
expect(entry.name).toBe('mark1');
expect(entry.entryType).toBe('mark');
totalEntries += entries.length;
});
expect(() => observer.observe({entryTypes: ['mark']})).not.toThrow();
NativePerformanceObserver.logRawEntry({
name: 'mark1',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 0,
});
await jest.runAllTicks();
expect(totalEntries).toBe(1);
observer.disconnect();
});
});