From ea0cd5b7ca7cfc58cfffce8da17f50ec4817f709 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Fri, 4 Nov 2022 08:41:01 -0700 Subject: [PATCH] JS side implementation of PerformanceObserver API Summary: [Changelog][Internal] This adds module specs for the native part of PerformanceObserver, as well as the interaction logic vs the NativePerformanceObserver API. See https://fb.quip.com/MdqgAk1Eb2dV for more detail. Reviewed By: rubennorte Differential Revision: D40897006 fbshipit-source-id: 77475f21dad9ee9dbe15df5a989eb08d314e6db2 --- .../specs/NativePerformanceObserverCxx.js | 44 +++++++ .../WebPerformance/PerformanceObserver.js | 123 ++++++++++++++++-- 2 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 Libraries/NativeModules/specs/NativePerformanceObserverCxx.js diff --git a/Libraries/NativeModules/specs/NativePerformanceObserverCxx.js b/Libraries/NativeModules/specs/NativePerformanceObserverCxx.js new file mode 100644 index 00000000000..420b495e091 --- /dev/null +++ b/Libraries/NativeModules/specs/NativePerformanceObserverCxx.js @@ -0,0 +1,44 @@ +/** + * 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 {TurboModule} from '../../TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; + +export type RawTimeStamp = number; + +export const RawPerformanceEntryTypeValues = { + UNDEFINED: 0, +}; + +export type RawPerformanceEntryType = number; + +export type RawPerformanceEntry = $ReadOnly<{ + name: string, + entryType: RawPerformanceEntryType, + startTime: RawTimeStamp, + duration: number, + + // For "event" entries only: + processingStart?: RawTimeStamp, + processingEnd?: RawTimeStamp, + interactionId?: RawTimeStamp, +}>; + +export type RawPerformanceEntryList = $ReadOnlyArray; + +export interface Spec extends TurboModule { + +startReporting: (entryType: string) => void; + +stopReporting: (entryType: string) => void; + +getPendingEntries: () => RawPerformanceEntryList; + +setOnPerformanceEntryCallback: (callback?: () => void) => void; +} + +export default (TurboModuleRegistry.get('PerformanceObserver'): ?Spec); diff --git a/Libraries/WebPerformance/PerformanceObserver.js b/Libraries/WebPerformance/PerformanceObserver.js index 80857cac240..aa750b2166e 100644 --- a/Libraries/WebPerformance/PerformanceObserver.js +++ b/Libraries/WebPerformance/PerformanceObserver.js @@ -8,9 +8,19 @@ * @flow strict */ +import type { + RawPerformanceEntry, + RawPerformanceEntryList, + RawPerformanceEntryType, +} from '../NativeModules/specs/NativePerformanceObserverCxx'; + +import NativePerformanceObserver from '../NativeModules/specs/NativePerformanceObserverCxx'; +import warnOnce from '../Utilities/warnOnce'; + export type HighResTimeStamp = number; -// TODO: Extend once new types (such as event) are supported -export type PerformanceEntryType = empty; +// TODO: Extend once new types (such as event) are supported. +// TODO: Get rid of the "undefined" once there is at least one type supported. +export type PerformanceEntryType = 'undefined'; export class PerformanceEntry { name: string; @@ -18,6 +28,18 @@ export class PerformanceEntry { startTime: HighResTimeStamp; duration: number; + constructor(init: { + name: string, + entryType: PerformanceEntryType, + startTime: HighResTimeStamp, + duration: number, + }) { + this.name = init.name; + this.entryType = init.entryType; + this.startTime = init.startTime; + this.duration = init.duration; + } + // $FlowIgnore: Flow(unclear-type) toJSON(): Object { return { @@ -29,6 +51,21 @@ export class PerformanceEntry { } } +function rawToPerformanceEntryType( + type: RawPerformanceEntryType, +): PerformanceEntryType { + return 'undefined'; +} + +function rawToPerformanceEntry(entry: RawPerformanceEntry): PerformanceEntry { + return new PerformanceEntry({ + name: entry.name, + entryType: rawToPerformanceEntryType(entry.entryType), + startTime: entry.startTime, + duration: entry.duration, + }); +} + export type PerformanceEntryList = $ReadOnlyArray; export class PerformanceObserverEntryList { @@ -73,6 +110,19 @@ export type PerformanceObserverInit = type: PerformanceEntryType, }; +let _observedEntryTypeRefCount: Map = new Map(); + +let _observers: Set = new Set(); + +let _onPerformanceEntryCallbackIsSet: boolean = false; + +function warnNoNativePerformanceObserver() { + warnOnce( + 'missing-native-performance-observer', + 'Missing native implementation of PerformanceObserver', + ); +} + /** * Implementation of the PerformanceObserver interface for RN, * corresponding to the standard in https://www.w3.org/TR/performance-timeline/ @@ -95,24 +145,81 @@ export type PerformanceObserverInit = */ export default class PerformanceObserver { _callback: PerformanceObserverCallback; + _entryTypes: $ReadOnlySet; constructor(callback: PerformanceObserverCallback) { this._callback = callback; } observe(options: PerformanceObserverInit) { - console.log('PerformanceObserver: started observing'); + if (!NativePerformanceObserver) { + warnNoNativePerformanceObserver(); + return; + } + if (!_onPerformanceEntryCallbackIsSet) { + NativePerformanceObserver.setOnPerformanceEntryCallback( + onPerformanceEntry, + ); + _onPerformanceEntryCallbackIsSet = true; + } + if (options.entryTypes) { + this._entryTypes = new Set(options.entryTypes); + } else { + this._entryTypes = new Set([options.type]); + } + this._entryTypes.forEach(type => { + if (!_observedEntryTypeRefCount.has(type)) { + NativePerformanceObserver.startReporting(type); + } + _observedEntryTypeRefCount.set( + type, + (_observedEntryTypeRefCount.get(type) ?? 0) + 1, + ); + }); + _observers.add(this); } disconnect(): void { - console.log('PerformanceObserver: stopped observing'); - } - - takeRecords(): PerformanceEntryList { - return []; + if (!NativePerformanceObserver) { + warnNoNativePerformanceObserver(); + return; + } + this._entryTypes.forEach(type => { + const entryTypeRefCount = _observedEntryTypeRefCount.get(type) ?? 0; + if (entryTypeRefCount === 1) { + _observedEntryTypeRefCount.delete(type); + NativePerformanceObserver.stopReporting(type); + } else if (entryTypeRefCount !== 0) { + _observedEntryTypeRefCount.set(type, entryTypeRefCount - 1); + } + }); + _observers.delete(this); + if (_observers.size === 0) { + NativePerformanceObserver.setOnPerformanceEntryCallback(); + _onPerformanceEntryCallbackIsSet = false; + } } static supportedEntryTypes: $ReadOnlyArray = // TODO: add types once they are fully supported Object.freeze([]); } + +// This is a callback that gets scheduled and periodically called from the native side +function onPerformanceEntry() { + if (!NativePerformanceObserver) { + return; + } + const rawEntries: RawPerformanceEntryList = + NativePerformanceObserver.getPendingEntries(); + const entries = rawEntries.map(rawToPerformanceEntry); + _observers.forEach(observer => { + const entriesForObserver: PerformanceEntryList = entries.filter(entry => + observer._entryTypes.has(entry.entryType), + ); + observer._callback( + new PerformanceObserverEntryList(entriesForObserver), + observer, + ); + }); +}