diff --git a/app/src/examples/PerfomanceMonitorExample.tsx b/app/src/examples/PerfomanceMonitorExample.tsx new file mode 100644 index 000000000000..f714756a5236 --- /dev/null +++ b/app/src/examples/PerfomanceMonitorExample.tsx @@ -0,0 +1,68 @@ +import React, { useRef, useState } from 'react'; +import { Text, StyleSheet, Pressable, View } from 'react-native'; +import { PerformanceMonitor } from 'react-native-reanimated'; + +import EmptyExample from './EmptyExample'; +import BokehExample from './BokehExample'; +import PlanetsExample from './PlanetsExample'; +import EmojiWaterfallExample from './EmojiWaterfallExample'; + +enum Examples { + Empty = 'Empty Example', + Bokeh = 'Bokeh Example', + Planets = 'Planets Example', + Emojis = 'Emoji Waterfall Example', +} + +export default function PerformanceMonitorExample() { + const exampleElements = useRef( + new Map([ + [Examples.Empty, ], + [Examples.Bokeh, ], + [Examples.Planets, ], + [Examples.Emojis, ], + ]) + ); + + const [currentExample, setCurrentExample] = useState(Examples.Empty); + + return ( + <> + + {exampleElements.current.get(currentExample)!} + + {[ + Examples.Empty, + Examples.Bokeh, + Examples.Planets, + Examples.Emojis, + ].map((element) => ( + setCurrentExample(element)}> + {element} + + ))} + + + ); +} + +const styles = StyleSheet.create({ + buttonContainer: { + position: 'absolute', + flex: 1, + flexDirection: 'column', + gap: 4, + margin: 8, + marginLeft: 200, + }, + button: { + backgroundColor: 'lightblue', + padding: 8, + borderRadius: 8, + flex: 1, + textAlign: 'center', + }, +}); diff --git a/app/src/examples/index.ts b/app/src/examples/index.ts index 118f37b2cbba..8c8d4c6060fc 100644 --- a/app/src/examples/index.ts +++ b/app/src/examples/index.ts @@ -122,6 +122,7 @@ import WorkletFactoryCrash from './WorkletFactoryCrashExample'; import RuntimeTestsExample from './RuntimeTests/RuntimeTestsExample'; import HabitsExample from './LayoutAnimations/HabitsExample'; import MemoExample from './MemoExample'; +import PerformanceMonitorExample from './PerfomanceMonitorExample'; import ScreenTransitionExample from './ScreenTransitionExample'; interface Example { @@ -486,6 +487,11 @@ export const EXAMPLES: Record = { title: 'Habits', screen: HabitsExample, }, + PerformanceMonitorExample: { + icon: '⏱️', + title: 'Performance monitor', + screen: PerformanceMonitorExample, + }, // Old examples diff --git a/src/createAnimatedComponent/PropsFilter.tsx b/src/createAnimatedComponent/PropsFilter.tsx index 78932cfd508e..eb0ae1b399e7 100644 --- a/src/createAnimatedComponent/PropsFilter.tsx +++ b/src/createAnimatedComponent/PropsFilter.tsx @@ -1,8 +1,8 @@ 'use strict'; import { shallowEqual } from '../reanimated2/hook/utils'; -import type { StyleProps } from '../reanimated2'; -import { isSharedValue } from '../reanimated2'; +import type { StyleProps } from '../reanimated2/commonTypes'; +import { isSharedValue } from '../reanimated2/isSharedValue'; import { isChromeDebugger } from '../reanimated2/PlatformChecker'; import WorkletEventHandler from '../reanimated2/WorkletEventHandler'; import { initialUpdaterRun } from '../reanimated2/animation'; diff --git a/src/reanimated2/component/PerformanceMonitor.tsx b/src/reanimated2/component/PerformanceMonitor.tsx new file mode 100644 index 000000000000..7a74de184fd0 --- /dev/null +++ b/src/reanimated2/component/PerformanceMonitor.tsx @@ -0,0 +1,216 @@ +'use strict'; + +import React, { useEffect, useRef } from 'react'; +import { TextInput, StyleSheet, View } from 'react-native'; + +import type { FrameInfo } from '../frameCallback'; +import type { SharedValue } from '../commonTypes'; +import { useSharedValue, useAnimatedProps, useFrameCallback } from '../hook'; +import { createAnimatedComponent } from '../../createAnimatedComponent'; +import { addWhitelistedNativeProps } from '../../ConfigHelper'; + +type CircularBuffer = ReturnType; +function createCircularDoublesBuffer(size: number) { + 'worklet'; + + return { + next: 0 as number, + buffer: new Float32Array(size), + size, + count: 0 as number, + + push(value: number): number | null { + const oldValue = this.buffer[this.next]; + const oldCount = this.count; + this.buffer[this.next] = value; + + this.next = (this.next + 1) % this.size; + this.count = Math.min(this.size, this.count + 1); + return oldCount === this.size ? oldValue : null; + }, + + front(): number | null { + const notEmpty = this.count > 0; + if (notEmpty) { + const current = this.next - 1; + const index = current < 0 ? this.size - 1 : current; + return this.buffer[index]; + } + return null; + }, + + back(): number | null { + const notEmpty = this.count > 0; + return notEmpty ? this.buffer[this.next] : null; + }, + }; +} + +const DEFAULT_BUFFER_SIZE = 60; +addWhitelistedNativeProps({ text: true }); +const AnimatedTextInput = createAnimatedComponent(TextInput); + +function loopAnimationFrame(fn: (lastTime: number, time: number) => void) { + let lastTime = 0; + + function loop() { + requestAnimationFrame((time) => { + if (lastTime > 0) { + fn(lastTime, time); + } + lastTime = time; + requestAnimationFrame(loop); + }); + } + + loop(); +} + +function getFps(renderTimeInMs: number): number { + 'worklet'; + return 1000 / renderTimeInMs; +} + +function getTimeDelta( + timestamp: number, + previousTimestamp: number | null +): number { + 'worklet'; + return previousTimestamp !== null ? timestamp - previousTimestamp : 0; +} + +function completeBufferRoutine( + buffer: CircularBuffer, + timestamp: number, + previousTimestamp: number, + totalRenderTime: SharedValue +): number { + 'worklet'; + timestamp = Math.round(timestamp); + previousTimestamp = Math.round(previousTimestamp) ?? timestamp; + + const droppedTimestamp = buffer.push(timestamp); + const nextToDrop = buffer.back()!; + + const delta = getTimeDelta(timestamp, previousTimestamp); + const droppedDelta = getTimeDelta(nextToDrop, droppedTimestamp); + + totalRenderTime.value += delta - droppedDelta; + + return getFps(totalRenderTime.value / buffer.count); +} + +function JsPerformance() { + const jsFps = useSharedValue(null); + const totalRenderTime = useSharedValue(0); + const circularBuffer = useRef( + createCircularDoublesBuffer(DEFAULT_BUFFER_SIZE) + ); + + useEffect(() => { + loopAnimationFrame((_, timestamp) => { + timestamp = Math.round(timestamp); + const previousTimestamp = circularBuffer.current.front() ?? timestamp; + + const currentFps = completeBufferRoutine( + circularBuffer.current, + timestamp, + previousTimestamp, + totalRenderTime + ); + + // JS fps have to be measured every 2nd frame, + // thus 2x multiplication has to occur here + jsFps.value = (currentFps * 2).toFixed(0); + }); + }, []); + + const animatedProps = useAnimatedProps(() => { + const text = 'JS: ' + jsFps.value ?? 'N/A'; + return { text, defaultValue: text }; + }); + + return ( + + + + ); +} + +function UiPerformance() { + const uiFps = useSharedValue(null); + const totalRenderTime = useSharedValue(0); + const circularBuffer = useSharedValue(null); + + useFrameCallback(({ timestamp }: FrameInfo) => { + if (circularBuffer.value === null) { + circularBuffer.value = createCircularDoublesBuffer(DEFAULT_BUFFER_SIZE); + } + + timestamp = Math.round(timestamp); + const previousTimestamp = circularBuffer.value.front() ?? timestamp; + + const currentFps = completeBufferRoutine( + circularBuffer.value, + timestamp, + previousTimestamp, + totalRenderTime + ); + + uiFps.value = currentFps.toFixed(0); + }); + + const animatedProps = useAnimatedProps(() => { + const text = 'UI: ' + uiFps.value ?? 'N/A'; + return { text, defaultValue: text }; + }); + + return ( + + + + ); +} + +export function PerformanceMonitor() { + return ( + + + + + ); +} + +const styles = StyleSheet.create({ + monitor: { + flexDirection: 'row', + position: 'absolute', + backgroundColor: '#0006', + zIndex: 1000, + }, + header: { + fontSize: 14, + color: '#ffff', + paddingHorizontal: 5, + }, + text: { + fontSize: 13, + color: '#ffff', + fontFamily: 'monospace', + paddingHorizontal: 3, + }, + container: { + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + flexWrap: 'wrap', + }, +}); diff --git a/src/reanimated2/index.ts b/src/reanimated2/index.ts index dab75bc73537..5885fa1a4e4b 100644 --- a/src/reanimated2/index.ts +++ b/src/reanimated2/index.ts @@ -257,6 +257,7 @@ export { getAnimatedStyle, } from './jestUtils'; export { LayoutAnimationConfig } from './component/LayoutAnimationConfig'; +export { PerformanceMonitor } from './component/PerformanceMonitor'; export type { Adaptable, AdaptTransforms,