diff --git a/packages/react-native/src/private/webapis/performance/PerformanceObserver.js b/packages/react-native/src/private/webapis/performance/PerformanceObserver.js
index 980fd2d55aaa..a31edf4a3534 100644
--- a/packages/react-native/src/private/webapis/performance/PerformanceObserver.js
+++ b/packages/react-native/src/private/webapis/performance/PerformanceObserver.js
@@ -100,11 +100,17 @@ const onPerformanceEntry = () => {
const durationThreshold = observerConfig.entryTypes.get(entry.entryType);
return entry.duration >= (durationThreshold ?? 0);
});
- observerConfig.callback(
- new PerformanceObserverEntryList(entriesForObserver),
- observer,
- droppedEntriesCount,
- );
+ if (entriesForObserver.length !== 0) {
+ try {
+ observerConfig.callback(
+ new PerformanceObserverEntryList(entriesForObserver),
+ observer,
+ droppedEntriesCount,
+ );
+ } catch (error) {
+ console.error(error);
+ }
+ }
}
};
diff --git a/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js b/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js
index cf46dcbfd0e2..06cfbff59cd8 100644
--- a/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js
+++ b/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js
@@ -205,4 +205,58 @@ describe('PerformanceObserver', () => {
'mark7',
]);
});
+
+ it('should guard against errors in observer callbacks', () => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ const observer1Callback = jest.fn(() => {
+ throw new Error('observer 1 callback');
+ });
+ const observer1 = new PerformanceObserver(observer1Callback);
+
+ const observer2Callback = jest.fn();
+ const observer2 = new PerformanceObserver(observer2Callback);
+
+ observer1.observe({type: 'mark'});
+ observer2.observe({type: 'mark'});
+
+ NativePerformanceObserver.logRawEntry({
+ name: 'mark1',
+ entryType: RawPerformanceEntryTypeValues.MARK,
+ startTime: 0,
+ duration: 200,
+ });
+
+ jest.runAllTicks();
+
+ expect(observer1Callback).toHaveBeenCalled();
+ expect(observer2Callback).toHaveBeenCalled();
+
+ expect(console.error).toHaveBeenCalledWith(
+ new Error('observer 1 callback'),
+ );
+ });
+
+ it('should not invoke observers with non-matching entries', () => {
+ const observer1Callback = jest.fn();
+ const observer1 = new PerformanceObserver(observer1Callback);
+
+ const observer2Callback = jest.fn();
+ const observer2 = new PerformanceObserver(observer2Callback);
+
+ observer1.observe({type: 'mark'});
+ observer2.observe({type: 'measure'});
+
+ NativePerformanceObserver.logRawEntry({
+ name: 'mark1',
+ entryType: RawPerformanceEntryTypeValues.MARK,
+ startTime: 0,
+ duration: 200,
+ });
+
+ jest.runAllTicks();
+
+ expect(observer1Callback).toHaveBeenCalled();
+ expect(observer2Callback).not.toHaveBeenCalled();
+ });
});
diff --git a/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js b/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js
index 243f7145f46d..609d721b2b21 100644
--- a/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js
+++ b/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js
@@ -19,6 +19,7 @@ import {Button, StyleSheet, Text, View} from 'react-native';
import Performance from 'react-native/src/private/webapis/performance/Performance';
import PerformanceObserver, {
type PerformanceEntry,
+ type PerformanceEventTiming,
} from 'react-native/src/private/webapis/performance/PerformanceObserver';
const {useState, useCallback} = React;
@@ -132,12 +133,13 @@ function PerformanceObserverUserTimingExample(): React.Node {
{entries.map((entry, index) =>
entry.entryType === 'mark' ? (
- Mark {entry.name}: {entry.startTime}
+ Mark {entry.name}: {entry.startTime.toFixed(2)}
) : (
- Measure {entry.name}: {entry.startTime} -{' '}
- {entry.startTime + entry.duration} ({entry.duration}ms)
+ Measure {entry.name}: {entry.startTime.toFixed(2)} -{' '}
+ {(entry.startTime + entry.duration).toFixed(2)} (
+ {entry.duration.toFixed(2)}ms)
),
)}
@@ -146,6 +148,66 @@ function PerformanceObserverUserTimingExample(): React.Node {
);
}
+function PerformanceObserverEventTimingExample(): React.Node {
+ const theme = useContext(RNTesterThemeContext);
+
+ const [count, setCount] = useState(0);
+
+ const [entries, setEntries] = useState<
+ $ReadOnlyArray,
+ >([]);
+
+ useEffect(() => {
+ const observer = new PerformanceObserver(list => {
+ const newEntries: $ReadOnlyArray =
+ // $FlowExpectedError[incompatible-type] This is guaranteed because we're only observing `event` entry types.
+ list.getEntries();
+ setEntries(newEntries);
+ });
+
+ observer.observe({entryTypes: ['event']});
+
+ return () => observer.disconnect();
+ }, []);
+
+ const onPress = useCallback(() => {
+ busyWait(500);
+ // Force a state update to show how/if we're reporting paint times as well.
+ setCount(currentCount => currentCount + 1);
+ }, []);
+
+ return (
+
+
+
+ {entries.map((entry, index) => (
+
+ Event: {entry.name}
+ {'\n'}
+ Start: {entry.startTime.toFixed(2)}
+ {'\n'}
+ End: {(entry.startTime + entry.duration).toFixed(2)}
+ {'\n'}
+ Duration: {entry.duration.toFixed(2)}ms{'\n'}
+ Processing start: {entry.processingStart.toFixed(2)} (delay:{' '}
+ {(entry.processingStart - entry.startTime).toFixed(2)}ms){'\n'}
+ Processing end: {entry.processingEnd.toFixed(2)} (duration:{' '}
+ {(entry.processingEnd - entry.processingStart).toFixed(2)}ms){'\n'}
+
+ ))}
+
+
+ );
+}
+
+function busyWait(ms: number): void {
+ const end = performance.now() + ms;
+ while (performance.now() < end) {}
+}
+
const styles = StyleSheet.create({
container: {
padding: 10,
@@ -174,4 +236,10 @@ exports.examples = [
return ;
},
},
+ {
+ title: 'PerformanceObserver (events)',
+ render: function (): React.Element {
+ return ;
+ },
+ },
];