Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -132,12 +133,13 @@ function PerformanceObserverUserTimingExample(): React.Node {
{entries.map((entry, index) =>
entry.entryType === 'mark' ? (
<Text style={{color: theme.LabelColor}} key={index}>
Mark {entry.name}: {entry.startTime}
Mark {entry.name}: {entry.startTime.toFixed(2)}
</Text>
) : (
<Text style={{color: theme.LabelColor}} key={index}>
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)
</Text>
),
)}
Expand All @@ -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<PerformanceEventTiming>,
>([]);

useEffect(() => {
const observer = new PerformanceObserver(list => {
const newEntries: $ReadOnlyArray<PerformanceEventTiming> =
// $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 (
<View style={styles.container}>
<Button
onPress={onPress}
title={`Click to force a slow event (clicked ${count} times)`}
/>
<View>
{entries.map((entry, index) => (
<Text style={{color: theme.LabelColor}} key={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'}
</Text>
))}
</View>
</View>
);
}

function busyWait(ms: number): void {
const end = performance.now() + ms;
while (performance.now() < end) {}
}

const styles = StyleSheet.create({
container: {
padding: 10,
Expand Down Expand Up @@ -174,4 +236,10 @@ exports.examples = [
return <PerformanceObserverUserTimingExample />;
},
},
{
title: 'PerformanceObserver (events)',
render: function (): React.Element<typeof StartupTimingExample> {
return <PerformanceObserverEventTimingExample />;
},
},
];