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:
Родитель
581357bc9b
Коммит
cf194aebfe
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче