mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
cf194aebfe
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36152 [Changelog][Internal] By [the W3C standard](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver/observe), `PerformanceObserver.observer` can optionally take a `durationThreshold` option, so that only entries with duration larger than the threshold are reported. This diff adds support for this on the RN side, as well as unit tests for this feature on the JS side. NOTE: The standard suggests that default value for this is 104s. I left it at 0 for now, as for the RN use cases t may be to too high (needs discussion). Reviewed By: rubennorte Differential Revision: D43154319 fbshipit-source-id: 0f9d435506f48d8e8521e408211347e8391d22fc
319 lines
9.6 KiB
JavaScript
319 lines
9.6 KiB
JavaScript
/**
|
|
* 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<PerformanceEntry>;
|
|
|
|
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<PerformanceEntryType>,
|
|
}
|
|
| {
|
|
type: PerformanceEntryType,
|
|
durationThreshold?: HighResTimeStamp,
|
|
};
|
|
|
|
type PerformanceObserverConfig = {|
|
|
callback: PerformanceObserverCallback,
|
|
// Map of {entryType: durationThreshold}
|
|
entryTypes: $ReadOnlyMap<PerformanceEntryType, ?number>,
|
|
|};
|
|
|
|
const observerCountPerEntryType: Map<PerformanceEntryType, number> = new Map();
|
|
const registeredObservers: Map<PerformanceObserver, PerformanceObserverConfig> =
|
|
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<PerformanceEntryType, ?number> = 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<PerformanceEntryType> =
|
|
Object.freeze(['mark', 'measure', 'event']);
|
|
}
|
|
|
|
// As a Set union, except if value exists in both, we take minimum
|
|
function union<T>(
|
|
a: $ReadOnlyMap<T, ?number>,
|
|
b: $ReadOnlyMap<T, ?number>,
|
|
): Map<T, ?number> {
|
|
const res = new Map<T, ?number>();
|
|
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<T>(a: $ReadOnlySet<T>, b: $ReadOnlySet<T>): Set<T> {
|
|
return new Set([...a].filter(x => !b.has(x)));
|
|
}
|