@vyshnav18/react-native-fps-counter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
- package/android/.gradle/9.2.0/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/9.2.0/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/9.2.0/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build/reports/problems/problems-report.html +659 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/rnfpscounter/FpsCounterModule.kt +120 -0
- package/android/src/main/java/com/rnfpscounter/FpsCounterPackage.kt +16 -0
- package/dist/index.d.mts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +192 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +188 -0
- package/dist/index.mjs.map +1 -0
- package/ios/FpsCounterModule.h +12 -0
- package/ios/FpsCounterModule.mm +123 -0
- package/ios/RNFpsCounter.podspec +21 -0
- package/package.json +73 -0
- package/react-native.config.js +14 -0
- package/src/FpsCounter.tsx +190 -0
- package/src/index.ts +10 -0
- package/src/specs/NativeFpsCounter.ts +26 -0
- package/src/useFPSMetric.ts +82 -0
- package/src/useUIFPSMetric.ts +81 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { NativeModules, StyleSheet, NativeEventEmitter, View, Text } from 'react-native';
|
|
3
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/useFPSMetric.ts
|
|
6
|
+
function useFPSMetric() {
|
|
7
|
+
const [frameState, setFrameState] = useState({
|
|
8
|
+
fps: 0,
|
|
9
|
+
lastStamp: Date.now(),
|
|
10
|
+
framesCount: 0,
|
|
11
|
+
average: 0,
|
|
12
|
+
totalCount: 0
|
|
13
|
+
});
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let timeout = null;
|
|
16
|
+
requestAnimationFrame(() => {
|
|
17
|
+
timeout = setTimeout(() => {
|
|
18
|
+
const currentStamp = Date.now();
|
|
19
|
+
const shouldSetState = currentStamp - frameState.lastStamp > 1e3;
|
|
20
|
+
const newFramesCount = frameState.framesCount + 1;
|
|
21
|
+
if (shouldSetState) {
|
|
22
|
+
const newValue = frameState.framesCount;
|
|
23
|
+
const totalCount = frameState.totalCount + 1;
|
|
24
|
+
const newMean = Math.min(
|
|
25
|
+
frameState.average + (newValue - frameState.average) / totalCount,
|
|
26
|
+
60
|
|
27
|
+
);
|
|
28
|
+
setFrameState({
|
|
29
|
+
fps: frameState.framesCount,
|
|
30
|
+
lastStamp: currentStamp,
|
|
31
|
+
framesCount: 0,
|
|
32
|
+
average: newMean,
|
|
33
|
+
totalCount
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
setFrameState({
|
|
37
|
+
...frameState,
|
|
38
|
+
framesCount: newFramesCount
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}, 0);
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
if (timeout) clearTimeout(timeout);
|
|
45
|
+
};
|
|
46
|
+
}, [frameState]);
|
|
47
|
+
return { average: frameState.average, fps: frameState.fps };
|
|
48
|
+
}
|
|
49
|
+
var FpsCounterModule = NativeModules.FpsCounter;
|
|
50
|
+
function useUIFPSMetric() {
|
|
51
|
+
const [frameData, setFrameData] = useState({
|
|
52
|
+
uiFps: 0,
|
|
53
|
+
uiAverage: 0,
|
|
54
|
+
isAvailable: !!FpsCounterModule
|
|
55
|
+
});
|
|
56
|
+
const eventEmitterRef = useRef(null);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!FpsCounterModule) {
|
|
59
|
+
if (__DEV__) {
|
|
60
|
+
console.warn(
|
|
61
|
+
"[react-native-fps-counter] Native module not found.\nMake sure you have run:\n - iOS: cd ios && pod install\n - Android: Rebuild the app (npx react-native run-android)\nIf using Expo, you need to create a development build."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
eventEmitterRef.current = new NativeEventEmitter(FpsCounterModule);
|
|
67
|
+
const subscription = eventEmitterRef.current.addListener(
|
|
68
|
+
"onUIFpsUpdate",
|
|
69
|
+
(data) => {
|
|
70
|
+
setFrameData({
|
|
71
|
+
uiFps: Math.round(data.fps),
|
|
72
|
+
uiAverage: data.average,
|
|
73
|
+
isAvailable: true
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
FpsCounterModule.startMonitoring();
|
|
78
|
+
return () => {
|
|
79
|
+
subscription.remove();
|
|
80
|
+
FpsCounterModule.stopMonitoring();
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
83
|
+
return frameData;
|
|
84
|
+
}
|
|
85
|
+
var defaultStyles = StyleSheet.create({
|
|
86
|
+
container: {
|
|
87
|
+
position: "absolute",
|
|
88
|
+
top: 50,
|
|
89
|
+
left: 8,
|
|
90
|
+
backgroundColor: "rgba(0, 0, 0, 0.75)",
|
|
91
|
+
paddingHorizontal: 10,
|
|
92
|
+
paddingVertical: 6,
|
|
93
|
+
borderRadius: 6,
|
|
94
|
+
minWidth: 80
|
|
95
|
+
},
|
|
96
|
+
row: {
|
|
97
|
+
flexDirection: "row",
|
|
98
|
+
justifyContent: "space-between",
|
|
99
|
+
alignItems: "center",
|
|
100
|
+
marginVertical: 1
|
|
101
|
+
},
|
|
102
|
+
label: {
|
|
103
|
+
color: "rgba(255, 255, 255, 0.7)",
|
|
104
|
+
fontSize: 10,
|
|
105
|
+
fontWeight: "500",
|
|
106
|
+
marginRight: 8
|
|
107
|
+
},
|
|
108
|
+
value: {
|
|
109
|
+
color: "white",
|
|
110
|
+
fontSize: 12,
|
|
111
|
+
fontWeight: "700",
|
|
112
|
+
fontVariant: ["tabular-nums"],
|
|
113
|
+
minWidth: 45,
|
|
114
|
+
textAlign: "right"
|
|
115
|
+
},
|
|
116
|
+
divider: {
|
|
117
|
+
height: 1,
|
|
118
|
+
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
|
119
|
+
marginVertical: 4
|
|
120
|
+
},
|
|
121
|
+
sectionLabel: {
|
|
122
|
+
color: "rgba(255, 255, 255, 0.5)",
|
|
123
|
+
fontSize: 9,
|
|
124
|
+
fontWeight: "600",
|
|
125
|
+
textTransform: "uppercase",
|
|
126
|
+
letterSpacing: 0.5,
|
|
127
|
+
marginBottom: 2
|
|
128
|
+
},
|
|
129
|
+
unavailable: {
|
|
130
|
+
color: "rgba(255, 255, 255, 0.4)",
|
|
131
|
+
fontSize: 9,
|
|
132
|
+
fontStyle: "italic"
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
var FpsCounter = ({
|
|
136
|
+
visible = true,
|
|
137
|
+
showJsFps = true,
|
|
138
|
+
showUiFps = true,
|
|
139
|
+
showAverage = true,
|
|
140
|
+
averageDecimalPlaces = 1,
|
|
141
|
+
style
|
|
142
|
+
}) => {
|
|
143
|
+
const { fps: jsFps, average: jsAverage } = useFPSMetric();
|
|
144
|
+
const { uiFps, uiAverage, isAvailable: isUiFpsAvailable } = useUIFPSMetric();
|
|
145
|
+
if (!visible) return null;
|
|
146
|
+
const shouldShowUiFps = showUiFps && isUiFpsAvailable;
|
|
147
|
+
const shouldShowUiFpsUnavailable = showUiFps && !isUiFpsAvailable;
|
|
148
|
+
return /* @__PURE__ */ jsxs(
|
|
149
|
+
View,
|
|
150
|
+
{
|
|
151
|
+
pointerEvents: "none",
|
|
152
|
+
style: [defaultStyles.container, style?.container],
|
|
153
|
+
children: [
|
|
154
|
+
shouldShowUiFps && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
155
|
+
/* @__PURE__ */ jsx(Text, { style: defaultStyles.sectionLabel, children: "UI Thread" }),
|
|
156
|
+
/* @__PURE__ */ jsxs(View, { style: defaultStyles.row, children: [
|
|
157
|
+
/* @__PURE__ */ jsx(Text, { style: [defaultStyles.label, style?.labelText], children: "FPS" }),
|
|
158
|
+
/* @__PURE__ */ jsx(Text, { style: [defaultStyles.value, style?.valueText], children: uiFps })
|
|
159
|
+
] }),
|
|
160
|
+
showAverage && /* @__PURE__ */ jsxs(View, { style: defaultStyles.row, children: [
|
|
161
|
+
/* @__PURE__ */ jsx(Text, { style: [defaultStyles.label, style?.labelText], children: "Avg" }),
|
|
162
|
+
/* @__PURE__ */ jsx(Text, { style: [defaultStyles.value, style?.valueText], children: uiAverage.toFixed(averageDecimalPlaces) })
|
|
163
|
+
] })
|
|
164
|
+
] }),
|
|
165
|
+
shouldShowUiFpsUnavailable && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
166
|
+
/* @__PURE__ */ jsx(Text, { style: defaultStyles.sectionLabel, children: "UI Thread" }),
|
|
167
|
+
/* @__PURE__ */ jsx(Text, { style: defaultStyles.unavailable, children: "Native module not linked" })
|
|
168
|
+
] }),
|
|
169
|
+
(shouldShowUiFps || shouldShowUiFpsUnavailable) && showJsFps && /* @__PURE__ */ jsx(View, { style: defaultStyles.divider }),
|
|
170
|
+
showJsFps && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
171
|
+
/* @__PURE__ */ jsx(Text, { style: defaultStyles.sectionLabel, children: "JS Thread" }),
|
|
172
|
+
/* @__PURE__ */ jsxs(View, { style: defaultStyles.row, children: [
|
|
173
|
+
/* @__PURE__ */ jsx(Text, { style: [defaultStyles.label, style?.labelText], children: "FPS" }),
|
|
174
|
+
/* @__PURE__ */ jsx(Text, { style: [defaultStyles.value, style?.valueText], children: jsFps })
|
|
175
|
+
] }),
|
|
176
|
+
showAverage && /* @__PURE__ */ jsxs(View, { style: defaultStyles.row, children: [
|
|
177
|
+
/* @__PURE__ */ jsx(Text, { style: [defaultStyles.label, style?.labelText], children: "Avg" }),
|
|
178
|
+
/* @__PURE__ */ jsx(Text, { style: [defaultStyles.value, style?.valueText], children: jsAverage.toFixed(averageDecimalPlaces) })
|
|
179
|
+
] })
|
|
180
|
+
] })
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export { FpsCounter, useFPSMetric, useUIFPSMetric };
|
|
187
|
+
//# sourceMappingURL=index.mjs.map
|
|
188
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useFPSMetric.ts","../src/useUIFPSMetric.ts","../src/FpsCounter.tsx"],"names":["useState","useEffect"],"mappings":"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","file":"index.mjs","sourcesContent":["import { useEffect, useState } from 'react';\n\ntype FrameData = {\n fps: number;\n lastStamp: number;\n framesCount: number;\n average: number;\n totalCount: number;\n};\n\nexport type FPS = {\n average: FrameData['average'];\n fps: FrameData['fps'];\n};\n\n/**\n * A custom hook that tracks frames per second (FPS) in React Native.\n * Uses requestAnimationFrame to count frames and updates at most once per second.\n *\n * @returns An object containing the current FPS and rolling average FPS\n *\n * @example\n * ```tsx\n * const { fps, average } = useFPSMetric();\n * console.log(`Current: ${fps} FPS, Average: ${average.toFixed(2)} FPS`);\n * ```\n */\nexport function useFPSMetric(): FPS {\n const [frameState, setFrameState] = useState<FrameData>({\n fps: 0,\n lastStamp: Date.now(),\n framesCount: 0,\n average: 0,\n totalCount: 0,\n });\n\n useEffect(() => {\n // NOTE: timeout is here because requestAnimationFrame is deferred\n // and to prevent setStates when unmounted\n let timeout: ReturnType<typeof setTimeout> | null = null;\n\n requestAnimationFrame((): void => {\n timeout = setTimeout((): void => {\n const currentStamp = Date.now();\n const shouldSetState = currentStamp - frameState.lastStamp > 1000;\n const newFramesCount = frameState.framesCount + 1;\n\n // Updates FPS at most once per second\n if (shouldSetState) {\n const newValue = frameState.framesCount;\n const totalCount = frameState.totalCount + 1;\n\n // Math.min is used because values over 60 aren't really important\n // Mean FPS is calculated incrementally instead of storing all values\n const newMean = Math.min(\n frameState.average + (newValue - frameState.average) / totalCount,\n 60\n );\n\n setFrameState({\n fps: frameState.framesCount,\n lastStamp: currentStamp,\n framesCount: 0,\n average: newMean,\n totalCount,\n });\n } else {\n setFrameState({\n ...frameState,\n framesCount: newFramesCount,\n });\n }\n }, 0);\n });\n\n return () => {\n if (timeout) clearTimeout(timeout);\n };\n }, [frameState]);\n\n return { average: frameState.average, fps: frameState.fps };\n}\n","import { useEffect, useState, useRef } from 'react';\nimport { NativeModules, NativeEventEmitter, Platform } from 'react-native';\n\nconst FpsCounterModule = NativeModules.FpsCounter;\n\ntype UIFrameData = {\n uiFps: number;\n uiAverage: number;\n isAvailable: boolean;\n};\n\nexport type UIFPS = {\n uiFps: number;\n uiAverage: number;\n /** Whether the native module is available */\n isAvailable: boolean;\n};\n\n/**\n * A custom hook that tracks native UI thread frames per second.\n * Uses CADisplayLink on iOS and Choreographer on Android.\n *\n * @returns An object containing the current UI FPS, rolling average, and availability status\n *\n * @example\n * ```tsx\n * const { uiFps, uiAverage, isAvailable } = useUIFPSMetric();\n * if (isAvailable) {\n * console.log(`UI Thread: ${uiFps} FPS`);\n * }\n * ```\n */\nexport function useUIFPSMetric(): UIFPS {\n const [frameData, setFrameData] = useState<UIFrameData>({\n uiFps: 0,\n uiAverage: 0,\n isAvailable: !!FpsCounterModule,\n });\n const eventEmitterRef = useRef<NativeEventEmitter | null>(null);\n\n useEffect(() => {\n // Handle case where native module is not available\n if (!FpsCounterModule) {\n if (__DEV__) {\n console.warn(\n '[react-native-fps-counter] Native module not found.\\n' +\n 'Make sure you have run:\\n' +\n ' - iOS: cd ios && pod install\\n' +\n ' - Android: Rebuild the app (npx react-native run-android)\\n' +\n 'If using Expo, you need to create a development build.'\n );\n }\n return;\n }\n\n // Create event emitter\n eventEmitterRef.current = new NativeEventEmitter(FpsCounterModule);\n\n // Subscribe to FPS updates\n const subscription = eventEmitterRef.current.addListener(\n 'onUIFpsUpdate',\n (data: { fps: number; average: number }) => {\n setFrameData({\n uiFps: Math.round(data.fps),\n uiAverage: data.average,\n isAvailable: true,\n });\n }\n );\n\n // Start monitoring\n FpsCounterModule.startMonitoring();\n\n return () => {\n subscription.remove();\n FpsCounterModule.stopMonitoring();\n };\n }, []);\n\n return frameData;\n}\n","import React from 'react';\nimport { StyleSheet, Text, View, ViewStyle, TextStyle } from 'react-native';\nimport { useFPSMetric } from './useFPSMetric';\nimport { useUIFPSMetric } from './useUIFPSMetric';\n\nexport interface FpsCounterStyles {\n container?: ViewStyle;\n text?: TextStyle;\n labelText?: TextStyle;\n valueText?: TextStyle;\n jsSection?: ViewStyle;\n uiSection?: ViewStyle;\n}\n\nexport interface FpsCounterProps {\n /**\n * Whether the FPS counter is visible\n * @default true\n */\n visible?: boolean;\n\n /**\n * Whether to show JS thread FPS\n * @default true\n */\n showJsFps?: boolean;\n\n /**\n * Whether to show UI thread FPS (requires native module)\n * @default true\n */\n showUiFps?: boolean;\n\n /**\n * Whether to show average FPS values\n * @default true\n */\n showAverage?: boolean;\n\n /**\n * Number of decimal places for average values\n * @default 1\n */\n averageDecimalPlaces?: number;\n\n /**\n * Custom styles to override defaults\n */\n style?: FpsCounterStyles;\n}\n\nconst defaultStyles = StyleSheet.create({\n container: {\n position: 'absolute',\n top: 50,\n left: 8,\n backgroundColor: 'rgba(0, 0, 0, 0.75)',\n paddingHorizontal: 10,\n paddingVertical: 6,\n borderRadius: 6,\n minWidth: 80,\n },\n row: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n alignItems: 'center',\n marginVertical: 1,\n },\n label: {\n color: 'rgba(255, 255, 255, 0.7)',\n fontSize: 10,\n fontWeight: '500',\n marginRight: 8,\n },\n value: {\n color: 'white',\n fontSize: 12,\n fontWeight: '700',\n fontVariant: ['tabular-nums'],\n minWidth: 45,\n textAlign: 'right',\n },\n divider: {\n height: 1,\n backgroundColor: 'rgba(255, 255, 255, 0.2)',\n marginVertical: 4,\n },\n sectionLabel: {\n color: 'rgba(255, 255, 255, 0.5)',\n fontSize: 9,\n fontWeight: '600',\n textTransform: 'uppercase',\n letterSpacing: 0.5,\n marginBottom: 2,\n },\n unavailable: {\n color: 'rgba(255, 255, 255, 0.4)',\n fontSize: 9,\n fontStyle: 'italic',\n },\n});\n\n/**\n * A React Native component that displays both JS thread and native UI thread FPS.\n * Should be placed at the root of your app for accurate measurements.\n *\n * @example\n * ```tsx\n * import { FpsCounter } from 'react-native-fps-counter';\n *\n * export default function App() {\n * return (\n * <View style={{ flex: 1 }}>\n * <YourAppContent />\n * <FpsCounter visible={__DEV__} />\n * </View>\n * );\n * }\n * ```\n */\nexport const FpsCounter: React.FC<FpsCounterProps> = ({\n visible = true,\n showJsFps = true,\n showUiFps = true,\n showAverage = true,\n averageDecimalPlaces = 1,\n style,\n}) => {\n const { fps: jsFps, average: jsAverage } = useFPSMetric();\n const { uiFps, uiAverage, isAvailable: isUiFpsAvailable } = useUIFPSMetric();\n\n if (!visible) return null;\n\n const shouldShowUiFps = showUiFps && isUiFpsAvailable;\n const shouldShowUiFpsUnavailable = showUiFps && !isUiFpsAvailable;\n\n return (\n <View\n pointerEvents=\"none\"\n style={[defaultStyles.container, style?.container]}\n >\n {shouldShowUiFps && (\n <>\n <Text style={defaultStyles.sectionLabel}>UI Thread</Text>\n <View style={defaultStyles.row}>\n <Text style={[defaultStyles.label, style?.labelText]}>FPS</Text>\n <Text style={[defaultStyles.value, style?.valueText]}>{uiFps}</Text>\n </View>\n {showAverage && (\n <View style={defaultStyles.row}>\n <Text style={[defaultStyles.label, style?.labelText]}>Avg</Text>\n <Text style={[defaultStyles.value, style?.valueText]}>\n {uiAverage.toFixed(averageDecimalPlaces)}\n </Text>\n </View>\n )}\n </>\n )}\n\n {shouldShowUiFpsUnavailable && (\n <>\n <Text style={defaultStyles.sectionLabel}>UI Thread</Text>\n <Text style={defaultStyles.unavailable}>Native module not linked</Text>\n </>\n )}\n\n {(shouldShowUiFps || shouldShowUiFpsUnavailable) && showJsFps && (\n <View style={defaultStyles.divider} />\n )}\n\n {showJsFps && (\n <>\n <Text style={defaultStyles.sectionLabel}>JS Thread</Text>\n <View style={defaultStyles.row}>\n <Text style={[defaultStyles.label, style?.labelText]}>FPS</Text>\n <Text style={[defaultStyles.value, style?.valueText]}>{jsFps}</Text>\n </View>\n {showAverage && (\n <View style={defaultStyles.row}>\n <Text style={[defaultStyles.label, style?.labelText]}>Avg</Text>\n <Text style={[defaultStyles.value, style?.valueText]}>\n {jsAverage.toFixed(averageDecimalPlaces)}\n </Text>\n </View>\n )}\n </>\n )}\n </View>\n );\n};\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
5
|
+
#import <RNFpsCounterSpec/RNFpsCounterSpec.h>
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
@interface FpsCounterModule : RCTEventEmitter <RCTBridgeModule>
|
|
9
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
10
|
+
, NativeFpsCounterSpec
|
|
11
|
+
#endif
|
|
12
|
+
@end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#import "FpsCounterModule.h"
|
|
2
|
+
#import <QuartzCore/CADisplayLink.h>
|
|
3
|
+
#import <React/RCTLog.h>
|
|
4
|
+
|
|
5
|
+
@implementation FpsCounterModule {
|
|
6
|
+
CADisplayLink *_displayLink;
|
|
7
|
+
CFTimeInterval _lastTimestamp;
|
|
8
|
+
NSInteger _frameCount;
|
|
9
|
+
double _fps;
|
|
10
|
+
double _averageFps;
|
|
11
|
+
NSInteger _totalSamples;
|
|
12
|
+
BOOL _isMonitoring;
|
|
13
|
+
BOOL _hasListeners;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
RCT_EXPORT_MODULE(FpsCounter)
|
|
17
|
+
|
|
18
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
19
|
+
return YES;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
- (instancetype)init {
|
|
23
|
+
if (self = [super init]) {
|
|
24
|
+
_frameCount = 0;
|
|
25
|
+
_lastTimestamp = 0;
|
|
26
|
+
_fps = 0;
|
|
27
|
+
_averageFps = 0;
|
|
28
|
+
_totalSamples = 0;
|
|
29
|
+
_isMonitoring = NO;
|
|
30
|
+
_hasListeners = NO;
|
|
31
|
+
}
|
|
32
|
+
return self;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
36
|
+
return @[ @"onUIFpsUpdate" ];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
- (void)startObserving {
|
|
40
|
+
_hasListeners = YES;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (void)stopObserving {
|
|
44
|
+
_hasListeners = NO;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
RCT_EXPORT_METHOD(startMonitoring) {
|
|
48
|
+
if (_isMonitoring) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_isMonitoring = YES;
|
|
53
|
+
_frameCount = 0;
|
|
54
|
+
_lastTimestamp = 0;
|
|
55
|
+
_fps = 0;
|
|
56
|
+
_averageFps = 0;
|
|
57
|
+
_totalSamples = 0;
|
|
58
|
+
|
|
59
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
60
|
+
self->_displayLink =
|
|
61
|
+
[CADisplayLink displayLinkWithTarget:self
|
|
62
|
+
selector:@selector(onDisplayLink:)];
|
|
63
|
+
[self->_displayLink addToRunLoop:[NSRunLoop mainRunLoop]
|
|
64
|
+
forMode:NSRunLoopCommonModes];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
RCT_EXPORT_METHOD(stopMonitoring) {
|
|
69
|
+
if (!_isMonitoring) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_isMonitoring = NO;
|
|
74
|
+
|
|
75
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
76
|
+
[self->_displayLink invalidate];
|
|
77
|
+
self->_displayLink = nil;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (void)onDisplayLink:(CADisplayLink *)displayLink {
|
|
82
|
+
if (_lastTimestamp == 0) {
|
|
83
|
+
_lastTimestamp = displayLink.timestamp;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_frameCount++;
|
|
88
|
+
|
|
89
|
+
CFTimeInterval elapsed = displayLink.timestamp - _lastTimestamp;
|
|
90
|
+
|
|
91
|
+
// Update FPS approximately once per second
|
|
92
|
+
if (elapsed >= 1.0) {
|
|
93
|
+
_fps = (double)_frameCount / elapsed;
|
|
94
|
+
_totalSamples++;
|
|
95
|
+
|
|
96
|
+
// Calculate rolling average (capped at 120 FPS for high refresh rate
|
|
97
|
+
// displays)
|
|
98
|
+
_averageFps =
|
|
99
|
+
MIN(_averageFps + (_fps - _averageFps) / _totalSamples, 120.0);
|
|
100
|
+
|
|
101
|
+
_frameCount = 0;
|
|
102
|
+
_lastTimestamp = displayLink.timestamp;
|
|
103
|
+
|
|
104
|
+
if (_hasListeners) {
|
|
105
|
+
[self sendEventWithName:@"onUIFpsUpdate"
|
|
106
|
+
body:@{@"fps" : @(_fps), @"average" : @(_averageFps)}];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
- (void)invalidate {
|
|
112
|
+
[self stopMonitoring];
|
|
113
|
+
[super invalidate];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
117
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
118
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
119
|
+
return std::make_shared<facebook::react::NativeFpsCounterSpecJSI>(params);
|
|
120
|
+
}
|
|
121
|
+
#endif
|
|
122
|
+
|
|
123
|
+
@end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "RNFpsCounter"
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.homepage = package['homepage']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.authors = package['author']
|
|
12
|
+
s.platforms = { :ios => "12.4" }
|
|
13
|
+
s.source = { :git => package['repository']['url'], :tag => "v#{s.version}" }
|
|
14
|
+
s.source_files = "*.{h,m,mm}"
|
|
15
|
+
|
|
16
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
17
|
+
install_modules_dependencies(s)
|
|
18
|
+
else
|
|
19
|
+
s.dependency "React-Core"
|
|
20
|
+
end
|
|
21
|
+
end
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vyshnav18/react-native-fps-counter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "High-performance FPS counter for React Native with native UI thread and JS thread tracking. Uses Turbo Modules for maximum performance.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"react-native": "src/index.ts",
|
|
16
|
+
"source": "src/index.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"ios",
|
|
21
|
+
"android",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!**/__tests__"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"react-native",
|
|
33
|
+
"fps",
|
|
34
|
+
"performance",
|
|
35
|
+
"counter",
|
|
36
|
+
"frames-per-second",
|
|
37
|
+
"debugging",
|
|
38
|
+
"profiling",
|
|
39
|
+
"turbo-module",
|
|
40
|
+
"jsi",
|
|
41
|
+
"ui-thread"
|
|
42
|
+
],
|
|
43
|
+
"author": "",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=16.8.0",
|
|
47
|
+
"react-native": ">=0.60.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/react": "^18.2.0",
|
|
51
|
+
"@types/react-native": "^0.72.0",
|
|
52
|
+
"react": "^18.2.0",
|
|
53
|
+
"react-native": "^0.72.0",
|
|
54
|
+
"tsup": "^8.0.0",
|
|
55
|
+
"typescript": "^5.3.0"
|
|
56
|
+
},
|
|
57
|
+
"codegenConfig": {
|
|
58
|
+
"name": "RNFpsCounterSpec",
|
|
59
|
+
"type": "modules",
|
|
60
|
+
"jsSrcsDir": "src/specs",
|
|
61
|
+
"android": {
|
|
62
|
+
"javaPackageName": "com.rnfpscounter"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "https://github.com/your-username/react-native-fps-counter.git"
|
|
68
|
+
},
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/your-username/react-native-fps-counter/issues"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/your-username/react-native-fps-counter#readme"
|
|
73
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
dependency: {
|
|
3
|
+
platforms: {
|
|
4
|
+
ios: {
|
|
5
|
+
podspecPath: './ios/RNFpsCounter.podspec',
|
|
6
|
+
},
|
|
7
|
+
android: {
|
|
8
|
+
sourceDir: './android',
|
|
9
|
+
packageImportPath: 'import com.rnfpscounter.FpsCounterPackage;',
|
|
10
|
+
packageInstance: 'new FpsCounterPackage()',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, Text, View, ViewStyle, TextStyle } from 'react-native';
|
|
3
|
+
import { useFPSMetric } from './useFPSMetric';
|
|
4
|
+
import { useUIFPSMetric } from './useUIFPSMetric';
|
|
5
|
+
|
|
6
|
+
export interface FpsCounterStyles {
|
|
7
|
+
container?: ViewStyle;
|
|
8
|
+
text?: TextStyle;
|
|
9
|
+
labelText?: TextStyle;
|
|
10
|
+
valueText?: TextStyle;
|
|
11
|
+
jsSection?: ViewStyle;
|
|
12
|
+
uiSection?: ViewStyle;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FpsCounterProps {
|
|
16
|
+
/**
|
|
17
|
+
* Whether the FPS counter is visible
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
visible?: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether to show JS thread FPS
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
showJsFps?: boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether to show UI thread FPS (requires native module)
|
|
30
|
+
* @default true
|
|
31
|
+
*/
|
|
32
|
+
showUiFps?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether to show average FPS values
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
showAverage?: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Number of decimal places for average values
|
|
42
|
+
* @default 1
|
|
43
|
+
*/
|
|
44
|
+
averageDecimalPlaces?: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Custom styles to override defaults
|
|
48
|
+
*/
|
|
49
|
+
style?: FpsCounterStyles;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const defaultStyles = StyleSheet.create({
|
|
53
|
+
container: {
|
|
54
|
+
position: 'absolute',
|
|
55
|
+
top: 50,
|
|
56
|
+
left: 8,
|
|
57
|
+
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
58
|
+
paddingHorizontal: 10,
|
|
59
|
+
paddingVertical: 6,
|
|
60
|
+
borderRadius: 6,
|
|
61
|
+
minWidth: 80,
|
|
62
|
+
},
|
|
63
|
+
row: {
|
|
64
|
+
flexDirection: 'row',
|
|
65
|
+
justifyContent: 'space-between',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
marginVertical: 1,
|
|
68
|
+
},
|
|
69
|
+
label: {
|
|
70
|
+
color: 'rgba(255, 255, 255, 0.7)',
|
|
71
|
+
fontSize: 10,
|
|
72
|
+
fontWeight: '500',
|
|
73
|
+
marginRight: 8,
|
|
74
|
+
},
|
|
75
|
+
value: {
|
|
76
|
+
color: 'white',
|
|
77
|
+
fontSize: 12,
|
|
78
|
+
fontWeight: '700',
|
|
79
|
+
fontVariant: ['tabular-nums'],
|
|
80
|
+
minWidth: 45,
|
|
81
|
+
textAlign: 'right',
|
|
82
|
+
},
|
|
83
|
+
divider: {
|
|
84
|
+
height: 1,
|
|
85
|
+
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
86
|
+
marginVertical: 4,
|
|
87
|
+
},
|
|
88
|
+
sectionLabel: {
|
|
89
|
+
color: 'rgba(255, 255, 255, 0.5)',
|
|
90
|
+
fontSize: 9,
|
|
91
|
+
fontWeight: '600',
|
|
92
|
+
textTransform: 'uppercase',
|
|
93
|
+
letterSpacing: 0.5,
|
|
94
|
+
marginBottom: 2,
|
|
95
|
+
},
|
|
96
|
+
unavailable: {
|
|
97
|
+
color: 'rgba(255, 255, 255, 0.4)',
|
|
98
|
+
fontSize: 9,
|
|
99
|
+
fontStyle: 'italic',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A React Native component that displays both JS thread and native UI thread FPS.
|
|
105
|
+
* Should be placed at the root of your app for accurate measurements.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```tsx
|
|
109
|
+
* import { FpsCounter } from 'react-native-fps-counter';
|
|
110
|
+
*
|
|
111
|
+
* export default function App() {
|
|
112
|
+
* return (
|
|
113
|
+
* <View style={{ flex: 1 }}>
|
|
114
|
+
* <YourAppContent />
|
|
115
|
+
* <FpsCounter visible={__DEV__} />
|
|
116
|
+
* </View>
|
|
117
|
+
* );
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export const FpsCounter: React.FC<FpsCounterProps> = ({
|
|
122
|
+
visible = true,
|
|
123
|
+
showJsFps = true,
|
|
124
|
+
showUiFps = true,
|
|
125
|
+
showAverage = true,
|
|
126
|
+
averageDecimalPlaces = 1,
|
|
127
|
+
style,
|
|
128
|
+
}) => {
|
|
129
|
+
const { fps: jsFps, average: jsAverage } = useFPSMetric();
|
|
130
|
+
const { uiFps, uiAverage, isAvailable: isUiFpsAvailable } = useUIFPSMetric();
|
|
131
|
+
|
|
132
|
+
if (!visible) return null;
|
|
133
|
+
|
|
134
|
+
const shouldShowUiFps = showUiFps && isUiFpsAvailable;
|
|
135
|
+
const shouldShowUiFpsUnavailable = showUiFps && !isUiFpsAvailable;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<View
|
|
139
|
+
pointerEvents="none"
|
|
140
|
+
style={[defaultStyles.container, style?.container]}
|
|
141
|
+
>
|
|
142
|
+
{shouldShowUiFps && (
|
|
143
|
+
<>
|
|
144
|
+
<Text style={defaultStyles.sectionLabel}>UI Thread</Text>
|
|
145
|
+
<View style={defaultStyles.row}>
|
|
146
|
+
<Text style={[defaultStyles.label, style?.labelText]}>FPS</Text>
|
|
147
|
+
<Text style={[defaultStyles.value, style?.valueText]}>{uiFps}</Text>
|
|
148
|
+
</View>
|
|
149
|
+
{showAverage && (
|
|
150
|
+
<View style={defaultStyles.row}>
|
|
151
|
+
<Text style={[defaultStyles.label, style?.labelText]}>Avg</Text>
|
|
152
|
+
<Text style={[defaultStyles.value, style?.valueText]}>
|
|
153
|
+
{uiAverage.toFixed(averageDecimalPlaces)}
|
|
154
|
+
</Text>
|
|
155
|
+
</View>
|
|
156
|
+
)}
|
|
157
|
+
</>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{shouldShowUiFpsUnavailable && (
|
|
161
|
+
<>
|
|
162
|
+
<Text style={defaultStyles.sectionLabel}>UI Thread</Text>
|
|
163
|
+
<Text style={defaultStyles.unavailable}>Native module not linked</Text>
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{(shouldShowUiFps || shouldShowUiFpsUnavailable) && showJsFps && (
|
|
168
|
+
<View style={defaultStyles.divider} />
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{showJsFps && (
|
|
172
|
+
<>
|
|
173
|
+
<Text style={defaultStyles.sectionLabel}>JS Thread</Text>
|
|
174
|
+
<View style={defaultStyles.row}>
|
|
175
|
+
<Text style={[defaultStyles.label, style?.labelText]}>FPS</Text>
|
|
176
|
+
<Text style={[defaultStyles.value, style?.valueText]}>{jsFps}</Text>
|
|
177
|
+
</View>
|
|
178
|
+
{showAverage && (
|
|
179
|
+
<View style={defaultStyles.row}>
|
|
180
|
+
<Text style={[defaultStyles.label, style?.labelText]}>Avg</Text>
|
|
181
|
+
<Text style={[defaultStyles.value, style?.valueText]}>
|
|
182
|
+
{jsAverage.toFixed(averageDecimalPlaces)}
|
|
183
|
+
</Text>
|
|
184
|
+
</View>
|
|
185
|
+
)}
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
188
|
+
</View>
|
|
189
|
+
);
|
|
190
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Hooks
|
|
2
|
+
export { useFPSMetric } from './useFPSMetric';
|
|
3
|
+
export type { FPS } from './useFPSMetric';
|
|
4
|
+
|
|
5
|
+
export { useUIFPSMetric } from './useUIFPSMetric';
|
|
6
|
+
export type { UIFPS } from './useUIFPSMetric';
|
|
7
|
+
|
|
8
|
+
// Component
|
|
9
|
+
export { FpsCounter } from './FpsCounter';
|
|
10
|
+
export type { FpsCounterProps, FpsCounterStyles } from './FpsCounter';
|