From f76d4dee6f876a65cd6476fb399d413d0e96f911 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Wed, 15 Feb 2023 06:03:12 -0800 Subject: [PATCH] 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 --- .../NativePerformanceObserver.cpp | 6 ++ .../NativePerformanceObserver.h | 2 + .../NativePerformanceObserver.js | 7 -- .../WebPerformance/PerformanceObserver.js | 69 ++------------- .../WebPerformance/RawPerformanceEntry.js | 87 +++++++++++++++++++ .../__mocks__/NativePerformanceObserver.js | 56 ++++++++++++ .../NativePerformanceObserverMock-test.js | 56 ++++++++++++ .../__tests__/PerformanceObserver-test.js | 49 +++++++++++ 8 files changed, 261 insertions(+), 71 deletions(-) create mode 100644 Libraries/WebPerformance/RawPerformanceEntry.js create mode 100644 Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js create mode 100644 Libraries/WebPerformance/__tests__/NativePerformanceObserverMock-test.js create mode 100644 Libraries/WebPerformance/__tests__/PerformanceObserver-test.js diff --git a/Libraries/WebPerformance/NativePerformanceObserver.cpp b/Libraries/WebPerformance/NativePerformanceObserver.cpp index dce100dde1b..c1b6265536b 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.cpp +++ b/Libraries/WebPerformance/NativePerformanceObserver.cpp @@ -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 diff --git a/Libraries/WebPerformance/NativePerformanceObserver.h b/Libraries/WebPerformance/NativePerformanceObserver.h index 4dd874f558d..5305a6ce35b 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.h +++ b/Libraries/WebPerformance/NativePerformanceObserver.h @@ -69,6 +69,8 @@ class NativePerformanceObserver jsi::Runtime &rt, std::optional> callback); + void logRawEntry(jsi::Runtime &rt, RawPerformanceEntry entry); + private: }; diff --git a/Libraries/WebPerformance/NativePerformanceObserver.js b/Libraries/WebPerformance/NativePerformanceObserver.js index b35bfcb3b0a..4af889249a7 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.js +++ b/Libraries/WebPerformance/NativePerformanceObserver.js @@ -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 = {| diff --git a/Libraries/WebPerformance/PerformanceObserver.js b/Libraries/WebPerformance/PerformanceObserver.js index c52313f3af1..9f47ec7d513 100644 --- a/Libraries/WebPerformance/PerformanceObserver.js +++ b/Libraries/WebPerformance/PerformanceObserver.js @@ -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; diff --git a/Libraries/WebPerformance/RawPerformanceEntry.js b/Libraries/WebPerformance/RawPerformanceEntry.js new file mode 100644 index 00000000000..c9f3c493f45 --- /dev/null +++ b/Libraries/WebPerformance/RawPerformanceEntry.js @@ -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}`, + ); + } +} diff --git a/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js b/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js new file mode 100644 index 00000000000..617a9122196 --- /dev/null +++ b/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js @@ -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 = new Set(); +let entries: Array = []; +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; diff --git a/Libraries/WebPerformance/__tests__/NativePerformanceObserverMock-test.js b/Libraries/WebPerformance/__tests__/NativePerformanceObserverMock-test.js new file mode 100644 index 00000000000..0fe3b7720f1 --- /dev/null +++ b/Libraries/WebPerformance/__tests__/NativePerformanceObserverMock-test.js @@ -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'); + }); +}); diff --git a/Libraries/WebPerformance/__tests__/PerformanceObserver-test.js b/Libraries/WebPerformance/__tests__/PerformanceObserver-test.js new file mode 100644 index 00000000000..060b9a9ab9f --- /dev/null +++ b/Libraries/WebPerformance/__tests__/PerformanceObserver-test.js @@ -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(); + }); +});