Implement durationThreshold option for PerformanceObserver (#36152)

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
This commit is contained in:
Ruslan Shestopalyuk 2023-02-16 06:21:43 -08:00 коммит произвёл Facebook GitHub Bot
Родитель 581357bc9b
Коммит cf194aebfe
8 изменённых файлов: 281 добавлений и 17 удалений

Просмотреть файл

@ -59,4 +59,12 @@ NativePerformanceObserver::getEventCounts(jsi::Runtime &rt) {
eventCounts.begin(), eventCounts.end());
}
void NativePerformanceObserver::setDurationThreshold(
jsi::Runtime &rt,
int32_t entryType,
double durationThreshold) {
PerformanceEntryReporter::getInstance().setDurationThreshold(
static_cast<PerformanceEntryType>(entryType), durationThreshold);
}
} // namespace facebook::react

Просмотреть файл

@ -74,6 +74,11 @@ class NativePerformanceObserver
std::vector<std::pair<std::string, uint32_t>> getEventCounts(
jsi::Runtime &rt);
void setDurationThreshold(
jsi::Runtime &rt,
int32_t entryType,
double durationThreshold);
private:
};

Просмотреть файл

@ -37,6 +37,10 @@ export interface Spec extends TurboModule {
+setOnPerformanceEntryCallback: (callback?: () => void) => void;
+logRawEntry: (entry: RawPerformanceEntry) => void;
+getEventCounts: () => $ReadOnlyArray<[string, number]>;
+setDurationThreshold: (
entryType: RawPerformanceEntryType,
durationThreshold: number,
) => void;
}
export default (TurboModuleRegistry.get<Spec>(

Просмотреть файл

@ -31,8 +31,17 @@ void PerformanceEntryReporter::setReportingCallback(
}
void PerformanceEntryReporter::startReporting(PerformanceEntryType entryType) {
reportingType_[static_cast<int>(entryType)] = true;
int entryTypeIdx = static_cast<int>(entryType);
reportingType_[entryTypeIdx] = true;
durationThreshold_[entryTypeIdx] = DEFAULT_DURATION_THRESHOLD;
}
void PerformanceEntryReporter::setDurationThreshold(
PerformanceEntryType entryType,
double durationThreshold) {
durationThreshold_[static_cast<int>(entryType)] = durationThreshold;
}
void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) {
reportingType_[static_cast<int>(entryType)] = false;
}
@ -56,6 +65,11 @@ void PerformanceEntryReporter::logEntry(const RawPerformanceEntry &entry) {
return;
}
if (entry.duration < durationThreshold_[entry.entryType]) {
// The entries duration is lower than the desired reporting threshold, skip
return;
}
std::lock_guard<std::mutex> lock(entriesMutex_);
if (entries_.size() == MAX_ENTRY_BUFFER_SIZE) {

Просмотреть файл

@ -44,6 +44,8 @@ using PerformanceMarkRegistryType = std::
// memory for the sake of the "Performance.measure" mark name lookup
constexpr size_t MARKS_BUFFER_SIZE = 1024;
constexpr double DEFAULT_DURATION_THRESHOLD = 0.0;
enum class PerformanceEntryType {
UNDEFINED = 0,
MARK = 1,
@ -66,6 +68,9 @@ class PerformanceEntryReporter : public EventLogger {
void setReportingCallback(std::optional<AsyncCallback<>> callback);
void startReporting(PerformanceEntryType entryType);
void stopReporting(PerformanceEntryType entryType);
void setDurationThreshold(
PerformanceEntryType entryType,
double durationThreshold);
GetPendingEntriesResult popPendingEntries();
void logEntry(const RawPerformanceEntry &entry);
@ -122,6 +127,8 @@ class PerformanceEntryReporter : public EventLogger {
std::mutex entriesMutex_;
std::array<bool, (size_t)PerformanceEntryType::_COUNT> reportingType_{false};
std::unordered_map<std::string, uint32_t> eventCounts_;
std::array<double, (size_t)PerformanceEntryType::_COUNT> durationThreshold_{
DEFAULT_DURATION_THRESHOLD};
// Mark registry for "measure" lookup
PerformanceMarkRegistryType marksRegistry_;

Просмотреть файл

@ -8,7 +8,7 @@
* @flow strict
*/
import type {PerformanceEntryType} from './PerformanceEntry';
import type {HighResTimeStamp, PerformanceEntryType} from './PerformanceEntry';
import warnOnce from '../Utilities/warnOnce';
import NativePerformanceObserver from './NativePerformanceObserver';
@ -62,11 +62,13 @@ export type PerformanceObserverInit =
}
| {
type: PerformanceEntryType,
durationThreshold?: HighResTimeStamp,
};
type PerformanceObserverConfig = {|
callback: PerformanceObserverCallback,
entryTypes: $ReadOnlySet<PerformanceEntryType>,
// Map of {entryType: durationThreshold}
entryTypes: $ReadOnlyMap<PerformanceEntryType, ?number>,
|};
const observerCountPerEntryType: Map<PerformanceEntryType, number> = new Map();
@ -87,9 +89,13 @@ const onPerformanceEntry = () => {
}
const entries = rawEntries.map(rawToPerformanceEntry);
for (const [observer, observerConfig] of registeredObservers.entries()) {
const entriesForObserver: PerformanceEntryList = entries.filter(
entry => observerConfig.entryTypes.has(entry.entryType) !== -1,
);
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,
@ -105,6 +111,24 @@ export function warnNoNativePerformanceObserver() {
);
}
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/
@ -145,10 +169,14 @@ export default class PerformanceObserver {
if (options.entryTypes) {
this._type = 'multiple';
requestedEntryTypes = new Set(options.entryTypes);
requestedEntryTypes = new Map(
options.entryTypes.map(t => [t, undefined]),
);
} else {
this._type = 'single';
requestedEntryTypes = new Set([options.type]);
requestedEntryTypes = new Map([
[options.type, options.durationThreshold],
]);
}
// The same observer may receive multiple calls to "observe", so we need
@ -178,19 +206,22 @@ export default class PerformanceObserver {
// We only need to start listenening to new entry types being observed in
// this observer.
const newEntryTypes = currentEntryTypes
? difference(requestedEntryTypes, currentEntryTypes)
: requestedEntryTypes;
? difference(
new Set(requestedEntryTypes.keys()),
new Set(currentEntryTypes.keys()),
)
: new Set(requestedEntryTypes.keys());
for (const type of newEntryTypes) {
if (!observerCountPerEntryType.has(type)) {
NativePerformanceObserver.startReporting(
performanceEntryTypeToRaw(type),
);
const rawType = performanceEntryTypeToRaw(type);
NativePerformanceObserver.startReporting(rawType);
}
observerCountPerEntryType.set(
type,
(observerCountPerEntryType.get(type) ?? 0) + 1,
);
}
applyDurationThresholds();
}
disconnect(): void {
@ -205,7 +236,7 @@ export default class PerformanceObserver {
}
// Disconnect this observer
for (const type of observerConfig.entryTypes) {
for (const type of observerConfig.entryTypes.keys()) {
const numberOfObserversForThisType =
observerCountPerEntryType.get(type) ?? 0;
if (numberOfObserversForThisType === 1) {
@ -224,10 +255,12 @@ export default class PerformanceObserver {
NativePerformanceObserver.setOnPerformanceEntryCallback(undefined);
isOnPerformanceEntryCallbackSet = false;
}
applyDurationThresholds();
}
_validateObserveOptions(options: PerformanceObserverInit): void {
const {type, entryTypes} = options;
const {type, entryTypes, durationThreshold} = options;
if (!type && !entryTypes) {
throw new TypeError(
@ -252,14 +285,32 @@ export default class PerformanceObserver {
"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']);
}
function union<T>(a: $ReadOnlySet<T>, b: $ReadOnlySet<T>): Set<T> {
return new Set([...a, ...b]);
// 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> {

Просмотреть файл

@ -19,6 +19,7 @@ import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry';
const reportingType: Set<RawPerformanceEntryType> = new Set();
const eventCounts: Map<string, number> = new Map();
const durationThresholds: Map<RawPerformanceEntryType, number> = new Map();
let entries: Array<RawPerformanceEntry> = [];
let onPerformanceEntryCallback: ?() => void;
@ -29,6 +30,7 @@ const NativePerformanceObserverMock: NativePerformanceObserver = {
stopReporting: (entryType: RawPerformanceEntryType) => {
reportingType.delete(entryType);
durationThresholds.delete(entryType);
},
popPendingEntries: (): GetPendingEntriesResult => {
@ -46,6 +48,13 @@ const NativePerformanceObserverMock: NativePerformanceObserver = {
logRawEntry: (entry: RawPerformanceEntry) => {
if (reportingType.has(entry.entryType)) {
const durationThreshold = durationThresholds.get(entry.entryType);
if (
durationThreshold !== undefined &&
entry.duration < durationThreshold
) {
return;
}
entries.push(entry);
// $FlowFixMe[incompatible-call]
global.queueMicrotask(() => {
@ -61,6 +70,13 @@ const NativePerformanceObserverMock: NativePerformanceObserver = {
getEventCounts: (): $ReadOnlyArray<[string, number]> => {
return Array.from(eventCounts.entries());
},
setDurationThreshold: (
entryType: RawPerformanceEntryType,
durationThreshold: number,
) => {
durationThresholds.set(entryType, durationThreshold);
},
};
export default NativePerformanceObserverMock;

Просмотреть файл

@ -26,6 +26,7 @@ describe('PerformanceObserver', () => {
let totalEntries = 0;
const observer = new PerformanceObserver((list, _observer) => {
expect(_observer).toBe(observer);
const entries = list.getEntries();
expect(entries).toHaveLength(1);
const entry = entries[0];
@ -46,4 +47,162 @@ describe('PerformanceObserver', () => {
expect(totalEntries).toBe(1);
observer.disconnect();
});
it('prevents durationThreshold to be used together with entryTypes', async () => {
const observer = new PerformanceObserver((list, _observer) => {});
expect(() =>
observer.observe({entryTypes: ['mark'], durationThreshold: 100}),
).toThrow();
});
it('handles durationThreshold argument as expected', async () => {
let entries = [];
const observer = new PerformanceObserver((list, _observer) => {
entries = [...entries, ...list.getEntries()];
});
observer.observe({type: 'mark', durationThreshold: 100});
NativePerformanceObserver.logRawEntry({
name: 'mark1',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 200,
});
NativePerformanceObserver.logRawEntry({
name: 'mark2',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 20,
});
NativePerformanceObserver.logRawEntry({
name: 'mark3',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 100,
});
NativePerformanceObserver.logRawEntry({
name: 'mark4',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 500,
});
await jest.runAllTicks();
expect(entries).toHaveLength(3);
expect(entries.map(e => e.name)).toStrictEqual(['mark1', 'mark3', 'mark4']);
});
it('correctly works with multiple PerformanceObservers with durationThreshold', async () => {
let entries1 = [];
const observer1 = new PerformanceObserver((list, _observer) => {
entries1 = [...entries1, ...list.getEntries()];
});
let entries2 = [];
const observer2 = new PerformanceObserver((list, _observer) => {
entries2 = [...entries2, ...list.getEntries()];
});
let entries3 = [];
const observer3 = new PerformanceObserver((list, _observer) => {
entries3 = [...entries3, ...list.getEntries()];
});
let entries4 = [];
const observer4 = new PerformanceObserver((list, _observer) => {
entries4 = [...entries4, ...list.getEntries()];
});
observer2.observe({type: 'mark', durationThreshold: 200});
observer1.observe({type: 'mark', durationThreshold: 100});
observer3.observe({type: 'mark', durationThreshold: 300});
observer3.observe({type: 'measure', durationThreshold: 500});
observer4.observe({entryTypes: ['mark', 'measure']});
NativePerformanceObserver.logRawEntry({
name: 'mark1',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 200,
});
NativePerformanceObserver.logRawEntry({
name: 'mark2',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 20,
});
NativePerformanceObserver.logRawEntry({
name: 'mark3',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 100,
});
NativePerformanceObserver.logRawEntry({
name: 'mark4',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 500,
});
await jest.runAllTicks();
observer1.disconnect();
NativePerformanceObserver.logRawEntry({
name: 'mark5',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 200,
});
NativePerformanceObserver.logRawEntry({
name: 'mark6',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 300,
});
await jest.runAllTicks();
observer3.disconnect();
NativePerformanceObserver.logRawEntry({
name: 'mark7',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 200,
});
await jest.runAllTicks();
observer4.disconnect();
expect(entries1.map(e => e.name)).toStrictEqual([
'mark1',
'mark3',
'mark4',
]);
expect(entries2.map(e => e.name)).toStrictEqual([
'mark1',
'mark4',
'mark5',
'mark6',
'mark7',
]);
expect(entries3.map(e => e.name)).toStrictEqual(['mark4', 'mark6']);
expect(entries4.map(e => e.name)).toStrictEqual([
'mark1',
'mark2',
'mark3',
'mark4',
'mark5',
'mark6',
'mark7',
]);
});
});