/** * 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 {HighResTimeStamp, PerformanceEntryType} from './PerformanceEntry'; import warnOnce from '../Utilities/warnOnce'; import NativePerformanceObserver from './NativePerformanceObserver'; import {PerformanceEntry} from './PerformanceEntry'; import { performanceEntryTypeToRaw, rawToPerformanceEntry, } from './RawPerformanceEntry'; export type PerformanceEntryList = $ReadOnlyArray; export class PerformanceObserverEntryList { _entries: PerformanceEntryList; constructor(entries: PerformanceEntryList) { this._entries = entries; } getEntries(): PerformanceEntryList { return this._entries; } getEntriesByType(type: PerformanceEntryType): PerformanceEntryList { return this._entries.filter(entry => entry.entryType === type); } getEntriesByName( name: string, type?: PerformanceEntryType, ): PerformanceEntryList { if (type === undefined) { return this._entries.filter(entry => entry.name === name); } else { return this._entries.filter( entry => entry.name === name && entry.entryType === type, ); } } } export type PerformanceObserverCallback = ( list: PerformanceObserverEntryList, observer: PerformanceObserver, // The number of buffered entries which got dropped from the buffer due to the buffer being full: droppedEntryCount?: number, ) => void; export type PerformanceObserverInit = | { entryTypes: Array, } | { type: PerformanceEntryType, durationThreshold?: HighResTimeStamp, }; type PerformanceObserverConfig = {| callback: PerformanceObserverCallback, // Map of {entryType: durationThreshold} entryTypes: $ReadOnlyMap, |}; const observerCountPerEntryType: Map = new Map(); const registeredObservers: Map = new Map(); let isOnPerformanceEntryCallbackSet: boolean = false; // This is a callback that gets scheduled and periodically called from the native side const onPerformanceEntry = () => { if (!NativePerformanceObserver) { return; } const entryResult = NativePerformanceObserver.popPendingEntries(); const rawEntries = entryResult?.entries ?? []; const droppedEntriesCount = entryResult?.droppedEntriesCount; if (rawEntries.length === 0) { return; } const entries = rawEntries.map(rawToPerformanceEntry); for (const [observer, observerConfig] of registeredObservers.entries()) { const entriesForObserver: PerformanceEntryList = entries.filter(entry => { if (!observerConfig.entryTypes.has(entry.entryType)) { return false; } const durationThreshold = observerConfig.entryTypes.get(entry.entryType); return entry.duration >= (durationThreshold ?? 0); }); observerConfig.callback( new PerformanceObserverEntryList(entriesForObserver), observer, droppedEntriesCount, ); } }; export function warnNoNativePerformanceObserver() { warnOnce( 'missing-native-performance-observer', 'Missing native implementation of PerformanceObserver', ); } function applyDurationThresholds() { const durationThresholds: Map = Array.from( registeredObservers.values(), ) .map(config => config.entryTypes) .reduce( (accumulator, currentValue) => union(accumulator, currentValue), new Map(), ); for (const [entryType, durationThreshold] of durationThresholds) { NativePerformanceObserver?.setDurationThreshold( performanceEntryTypeToRaw(entryType), durationThreshold ?? 0, ); } } /** * Implementation of the PerformanceObserver interface for RN, * corresponding to the standard in https://www.w3.org/TR/performance-timeline/ * * @example * const observer = new PerformanceObserver((list, _observer) => { * const entries = list.getEntries(); * entries.forEach(entry => { * reportEvent({ * eventName: entry.name, * startTime: entry.startTime, * endTime: entry.startTime + entry.duration, * processingStart: entry.processingStart, * processingEnd: entry.processingEnd, * interactionId: entry.interactionId, * }); * }); * }); * observer.observe({ type: "event" }); */ export default class PerformanceObserver { _callback: PerformanceObserverCallback; _type: 'single' | 'multiple' | void; constructor(callback: PerformanceObserverCallback) { this._callback = callback; } observe(options: PerformanceObserverInit): void { if (!NativePerformanceObserver) { warnNoNativePerformanceObserver(); return; } this._validateObserveOptions(options); let requestedEntryTypes; if (options.entryTypes) { this._type = 'multiple'; requestedEntryTypes = new Map( options.entryTypes.map(t => [t, undefined]), ); } else { this._type = 'single'; requestedEntryTypes = new Map([ [options.type, options.durationThreshold], ]); } // The same observer may receive multiple calls to "observe", so we need // to check what is new on this call vs. previous ones. const currentEntryTypes = registeredObservers.get(this)?.entryTypes; const nextEntryTypes = currentEntryTypes ? union(requestedEntryTypes, currentEntryTypes) : requestedEntryTypes; // This `observe` call is a no-op because there are no new things to observe. if (currentEntryTypes && currentEntryTypes.size === nextEntryTypes.size) { return; } registeredObservers.set(this, { callback: this._callback, entryTypes: nextEntryTypes, }); if (!isOnPerformanceEntryCallbackSet) { NativePerformanceObserver.setOnPerformanceEntryCallback( onPerformanceEntry, ); isOnPerformanceEntryCallbackSet = true; } // We only need to start listenening to new entry types being observed in // this observer. const newEntryTypes = currentEntryTypes ? difference( new Set(requestedEntryTypes.keys()), new Set(currentEntryTypes.keys()), ) : new Set(requestedEntryTypes.keys()); for (const type of newEntryTypes) { if (!observerCountPerEntryType.has(type)) { const rawType = performanceEntryTypeToRaw(type); NativePerformanceObserver.startReporting(rawType); } observerCountPerEntryType.set( type, (observerCountPerEntryType.get(type) ?? 0) + 1, ); } applyDurationThresholds(); } disconnect(): void { if (!NativePerformanceObserver) { warnNoNativePerformanceObserver(); return; } const observerConfig = registeredObservers.get(this); if (!observerConfig) { return; } // Disconnect this observer for (const type of observerConfig.entryTypes.keys()) { const numberOfObserversForThisType = observerCountPerEntryType.get(type) ?? 0; if (numberOfObserversForThisType === 1) { observerCountPerEntryType.delete(type); NativePerformanceObserver.stopReporting( performanceEntryTypeToRaw(type), ); } else if (numberOfObserversForThisType !== 0) { observerCountPerEntryType.set(type, numberOfObserversForThisType - 1); } } // Disconnect all observers if this was the last one registeredObservers.delete(this); if (registeredObservers.size === 0) { NativePerformanceObserver.setOnPerformanceEntryCallback(undefined); isOnPerformanceEntryCallbackSet = false; } applyDurationThresholds(); } _validateObserveOptions(options: PerformanceObserverInit): void { const {type, entryTypes, durationThreshold} = options; if (!type && !entryTypes) { throw new TypeError( "Failed to execute 'observe' on 'PerformanceObserver': An observe() call must not include both entryTypes and type arguments.", ); } if (entryTypes && type) { throw new TypeError( "Failed to execute 'observe' on 'PerformanceObserver': An observe() call must include either entryTypes or type arguments.", ); } if (this._type === 'multiple' && type) { throw new Error( "Failed to execute 'observe' on 'PerformanceObserver': This observer has performed observe({entryTypes:...}, therefore it cannot perform observe({type:...})", ); } if (this._type === 'single' && entryTypes) { throw new Error( "Failed to execute 'observe' on 'PerformanceObserver': This PerformanceObserver has performed observe({type:...}, therefore it cannot perform observe({entryTypes:...})", ); } if (entryTypes && durationThreshold !== undefined) { throw new TypeError( "Failed to execute 'observe' on 'PerformanceObserver': An observe() call must not include both entryTypes and durationThreshold arguments.", ); } } static supportedEntryTypes: $ReadOnlyArray = Object.freeze(['mark', 'measure', 'event']); } // As a Set union, except if value exists in both, we take minimum function union( a: $ReadOnlyMap, b: $ReadOnlyMap, ): Map { const res = new Map(); for (const [k, v] of a) { if (!b.has(k)) { res.set(k, v); } else { res.set(k, Math.min(v ?? 0, b.get(k) ?? 0)); } } return res; } function difference(a: $ReadOnlySet, b: $ReadOnlySet): Set { return new Set([...a].filter(x => !b.has(x))); }