@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = {prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
mavenCentral()
|
|
8
|
+
}
|
|
9
|
+
dependencies {
|
|
10
|
+
classpath("com.android.tools.build:gradle:7.4.2")
|
|
11
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.8.22')}")
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
apply plugin: 'com.android.library'
|
|
16
|
+
apply plugin: 'kotlin-android'
|
|
17
|
+
|
|
18
|
+
def isNewArchitectureEnabled() {
|
|
19
|
+
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
android {
|
|
23
|
+
namespace "com.rnfpscounter"
|
|
24
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 34)
|
|
25
|
+
|
|
26
|
+
defaultConfig {
|
|
27
|
+
minSdkVersion safeExtGet('minSdkVersion', 21)
|
|
28
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 34)
|
|
29
|
+
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
buildFeatures {
|
|
33
|
+
buildConfig true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
sourceSets {
|
|
37
|
+
main {
|
|
38
|
+
if (isNewArchitectureEnabled()) {
|
|
39
|
+
java.srcDirs += ['src/newarch']
|
|
40
|
+
} else {
|
|
41
|
+
java.srcDirs += ['src/oldarch']
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
compileOptions {
|
|
47
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
48
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
kotlinOptions {
|
|
52
|
+
jvmTarget = '1.8'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
repositories {
|
|
57
|
+
google()
|
|
58
|
+
mavenCentral()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dependencies {
|
|
62
|
+
implementation "com.facebook.react:react-android"
|
|
63
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '1.8.22')}"
|
|
64
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
package com.rnfpscounter
|
|
2
|
+
|
|
3
|
+
import android.view.Choreographer
|
|
4
|
+
import com.facebook.react.bridge.*
|
|
5
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
6
|
+
|
|
7
|
+
class FpsCounterModule(reactContext: ReactApplicationContext) :
|
|
8
|
+
ReactContextBaseJavaModule(reactContext),
|
|
9
|
+
LifecycleEventListener {
|
|
10
|
+
|
|
11
|
+
private var choreographerCallback: Choreographer.FrameCallback? = null
|
|
12
|
+
private var lastFrameTimeNanos: Long = 0
|
|
13
|
+
private var frameCount: Int = 0
|
|
14
|
+
private var fps: Double = 0.0
|
|
15
|
+
private var averageFps: Double = 0.0
|
|
16
|
+
private var totalSamples: Int = 0
|
|
17
|
+
private var isMonitoring: Boolean = false
|
|
18
|
+
private var listenerCount: Int = 0
|
|
19
|
+
|
|
20
|
+
override fun getName(): String = NAME
|
|
21
|
+
|
|
22
|
+
init {
|
|
23
|
+
reactContext.addLifecycleEventListener(this)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@ReactMethod
|
|
27
|
+
fun startMonitoring() {
|
|
28
|
+
if (isMonitoring) return
|
|
29
|
+
|
|
30
|
+
isMonitoring = true
|
|
31
|
+
frameCount = 0
|
|
32
|
+
lastFrameTimeNanos = 0
|
|
33
|
+
fps = 0.0
|
|
34
|
+
averageFps = 0.0
|
|
35
|
+
totalSamples = 0
|
|
36
|
+
|
|
37
|
+
choreographerCallback = object : Choreographer.FrameCallback {
|
|
38
|
+
override fun doFrame(frameTimeNanos: Long) {
|
|
39
|
+
if (!isMonitoring) return
|
|
40
|
+
|
|
41
|
+
if (lastFrameTimeNanos == 0L) {
|
|
42
|
+
lastFrameTimeNanos = frameTimeNanos
|
|
43
|
+
Choreographer.getInstance().postFrameCallback(this)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
frameCount++
|
|
48
|
+
|
|
49
|
+
val elapsedNanos = frameTimeNanos - lastFrameTimeNanos
|
|
50
|
+
val elapsedSeconds = elapsedNanos / 1_000_000_000.0
|
|
51
|
+
|
|
52
|
+
// Update FPS approximately once per second
|
|
53
|
+
if (elapsedSeconds >= 1.0) {
|
|
54
|
+
fps = frameCount / elapsedSeconds
|
|
55
|
+
totalSamples++
|
|
56
|
+
|
|
57
|
+
// Calculate rolling average (capped at 120 FPS for high refresh rate displays)
|
|
58
|
+
averageFps = minOf(averageFps + (fps - averageFps) / totalSamples, 120.0)
|
|
59
|
+
|
|
60
|
+
frameCount = 0
|
|
61
|
+
lastFrameTimeNanos = frameTimeNanos
|
|
62
|
+
|
|
63
|
+
if (listenerCount > 0) {
|
|
64
|
+
sendEvent(fps, averageFps)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Choreographer.getInstance().postFrameCallback(this)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Choreographer.getInstance().postFrameCallback(choreographerCallback!!)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ReactMethod
|
|
76
|
+
fun stopMonitoring() {
|
|
77
|
+
if (!isMonitoring) return
|
|
78
|
+
|
|
79
|
+
isMonitoring = false
|
|
80
|
+
choreographerCallback?.let {
|
|
81
|
+
Choreographer.getInstance().removeFrameCallback(it)
|
|
82
|
+
}
|
|
83
|
+
choreographerCallback = null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@ReactMethod
|
|
87
|
+
fun addListener(eventName: String) {
|
|
88
|
+
listenerCount++
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@ReactMethod
|
|
92
|
+
fun removeListeners(count: Int) {
|
|
93
|
+
listenerCount = maxOf(listenerCount - count, 0)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun sendEvent(fps: Double, average: Double) {
|
|
97
|
+
reactApplicationContext
|
|
98
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
99
|
+
.emit("onUIFpsUpdate", Arguments.createMap().apply {
|
|
100
|
+
putDouble("fps", fps)
|
|
101
|
+
putDouble("average", average)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override fun onHostResume() {}
|
|
106
|
+
override fun onHostPause() {}
|
|
107
|
+
|
|
108
|
+
override fun onHostDestroy() {
|
|
109
|
+
stopMonitoring()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fun invalidate() {
|
|
113
|
+
stopMonitoring()
|
|
114
|
+
super.invalidate()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
companion object {
|
|
118
|
+
const val NAME = "FpsCounter"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.rnfpscounter
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class FpsCounterPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(FpsCounterModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ViewStyle, TextStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
type FrameData = {
|
|
5
|
+
fps: number;
|
|
6
|
+
lastStamp: number;
|
|
7
|
+
framesCount: number;
|
|
8
|
+
average: number;
|
|
9
|
+
totalCount: number;
|
|
10
|
+
};
|
|
11
|
+
type FPS = {
|
|
12
|
+
average: FrameData['average'];
|
|
13
|
+
fps: FrameData['fps'];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A custom hook that tracks frames per second (FPS) in React Native.
|
|
17
|
+
* Uses requestAnimationFrame to count frames and updates at most once per second.
|
|
18
|
+
*
|
|
19
|
+
* @returns An object containing the current FPS and rolling average FPS
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const { fps, average } = useFPSMetric();
|
|
24
|
+
* console.log(`Current: ${fps} FPS, Average: ${average.toFixed(2)} FPS`);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function useFPSMetric(): FPS;
|
|
28
|
+
|
|
29
|
+
type UIFPS = {
|
|
30
|
+
uiFps: number;
|
|
31
|
+
uiAverage: number;
|
|
32
|
+
/** Whether the native module is available */
|
|
33
|
+
isAvailable: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A custom hook that tracks native UI thread frames per second.
|
|
37
|
+
* Uses CADisplayLink on iOS and Choreographer on Android.
|
|
38
|
+
*
|
|
39
|
+
* @returns An object containing the current UI FPS, rolling average, and availability status
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const { uiFps, uiAverage, isAvailable } = useUIFPSMetric();
|
|
44
|
+
* if (isAvailable) {
|
|
45
|
+
* console.log(`UI Thread: ${uiFps} FPS`);
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare function useUIFPSMetric(): UIFPS;
|
|
50
|
+
|
|
51
|
+
interface FpsCounterStyles {
|
|
52
|
+
container?: ViewStyle;
|
|
53
|
+
text?: TextStyle;
|
|
54
|
+
labelText?: TextStyle;
|
|
55
|
+
valueText?: TextStyle;
|
|
56
|
+
jsSection?: ViewStyle;
|
|
57
|
+
uiSection?: ViewStyle;
|
|
58
|
+
}
|
|
59
|
+
interface FpsCounterProps {
|
|
60
|
+
/**
|
|
61
|
+
* Whether the FPS counter is visible
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
visible?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Whether to show JS thread FPS
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
showJsFps?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether to show UI thread FPS (requires native module)
|
|
72
|
+
* @default true
|
|
73
|
+
*/
|
|
74
|
+
showUiFps?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether to show average FPS values
|
|
77
|
+
* @default true
|
|
78
|
+
*/
|
|
79
|
+
showAverage?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Number of decimal places for average values
|
|
82
|
+
* @default 1
|
|
83
|
+
*/
|
|
84
|
+
averageDecimalPlaces?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Custom styles to override defaults
|
|
87
|
+
*/
|
|
88
|
+
style?: FpsCounterStyles;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A React Native component that displays both JS thread and native UI thread FPS.
|
|
92
|
+
* Should be placed at the root of your app for accurate measurements.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* import { FpsCounter } from 'react-native-fps-counter';
|
|
97
|
+
*
|
|
98
|
+
* export default function App() {
|
|
99
|
+
* return (
|
|
100
|
+
* <View style={{ flex: 1 }}>
|
|
101
|
+
* <YourAppContent />
|
|
102
|
+
* <FpsCounter visible={__DEV__} />
|
|
103
|
+
* </View>
|
|
104
|
+
* );
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
declare const FpsCounter: React.FC<FpsCounterProps>;
|
|
109
|
+
|
|
110
|
+
export { type FPS, FpsCounter, type FpsCounterProps, type FpsCounterStyles, type UIFPS, useFPSMetric, useUIFPSMetric };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ViewStyle, TextStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
type FrameData = {
|
|
5
|
+
fps: number;
|
|
6
|
+
lastStamp: number;
|
|
7
|
+
framesCount: number;
|
|
8
|
+
average: number;
|
|
9
|
+
totalCount: number;
|
|
10
|
+
};
|
|
11
|
+
type FPS = {
|
|
12
|
+
average: FrameData['average'];
|
|
13
|
+
fps: FrameData['fps'];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A custom hook that tracks frames per second (FPS) in React Native.
|
|
17
|
+
* Uses requestAnimationFrame to count frames and updates at most once per second.
|
|
18
|
+
*
|
|
19
|
+
* @returns An object containing the current FPS and rolling average FPS
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const { fps, average } = useFPSMetric();
|
|
24
|
+
* console.log(`Current: ${fps} FPS, Average: ${average.toFixed(2)} FPS`);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function useFPSMetric(): FPS;
|
|
28
|
+
|
|
29
|
+
type UIFPS = {
|
|
30
|
+
uiFps: number;
|
|
31
|
+
uiAverage: number;
|
|
32
|
+
/** Whether the native module is available */
|
|
33
|
+
isAvailable: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A custom hook that tracks native UI thread frames per second.
|
|
37
|
+
* Uses CADisplayLink on iOS and Choreographer on Android.
|
|
38
|
+
*
|
|
39
|
+
* @returns An object containing the current UI FPS, rolling average, and availability status
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const { uiFps, uiAverage, isAvailable } = useUIFPSMetric();
|
|
44
|
+
* if (isAvailable) {
|
|
45
|
+
* console.log(`UI Thread: ${uiFps} FPS`);
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare function useUIFPSMetric(): UIFPS;
|
|
50
|
+
|
|
51
|
+
interface FpsCounterStyles {
|
|
52
|
+
container?: ViewStyle;
|
|
53
|
+
text?: TextStyle;
|
|
54
|
+
labelText?: TextStyle;
|
|
55
|
+
valueText?: TextStyle;
|
|
56
|
+
jsSection?: ViewStyle;
|
|
57
|
+
uiSection?: ViewStyle;
|
|
58
|
+
}
|
|
59
|
+
interface FpsCounterProps {
|
|
60
|
+
/**
|
|
61
|
+
* Whether the FPS counter is visible
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
visible?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Whether to show JS thread FPS
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
showJsFps?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether to show UI thread FPS (requires native module)
|
|
72
|
+
* @default true
|
|
73
|
+
*/
|
|
74
|
+
showUiFps?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether to show average FPS values
|
|
77
|
+
* @default true
|
|
78
|
+
*/
|
|
79
|
+
showAverage?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Number of decimal places for average values
|
|
82
|
+
* @default 1
|
|
83
|
+
*/
|
|
84
|
+
averageDecimalPlaces?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Custom styles to override defaults
|
|
87
|
+
*/
|
|
88
|
+
style?: FpsCounterStyles;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A React Native component that displays both JS thread and native UI thread FPS.
|
|
92
|
+
* Should be placed at the root of your app for accurate measurements.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* import { FpsCounter } from 'react-native-fps-counter';
|
|
97
|
+
*
|
|
98
|
+
* export default function App() {
|
|
99
|
+
* return (
|
|
100
|
+
* <View style={{ flex: 1 }}>
|
|
101
|
+
* <YourAppContent />
|
|
102
|
+
* <FpsCounter visible={__DEV__} />
|
|
103
|
+
* </View>
|
|
104
|
+
* );
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
declare const FpsCounter: React.FC<FpsCounterProps>;
|
|
109
|
+
|
|
110
|
+
export { type FPS, FpsCounter, type FpsCounterProps, type FpsCounterStyles, type UIFPS, useFPSMetric, useUIFPSMetric };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var reactNative = require('react-native');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// src/useFPSMetric.ts
|
|
8
|
+
function useFPSMetric() {
|
|
9
|
+
const [frameState, setFrameState] = react.useState({
|
|
10
|
+
fps: 0,
|
|
11
|
+
lastStamp: Date.now(),
|
|
12
|
+
framesCount: 0,
|
|
13
|
+
average: 0,
|
|
14
|
+
totalCount: 0
|
|
15
|
+
});
|
|
16
|
+
react.useEffect(() => {
|
|
17
|
+
let timeout = null;
|
|
18
|
+
requestAnimationFrame(() => {
|
|
19
|
+
timeout = setTimeout(() => {
|
|
20
|
+
const currentStamp = Date.now();
|
|
21
|
+
const shouldSetState = currentStamp - frameState.lastStamp > 1e3;
|
|
22
|
+
const newFramesCount = frameState.framesCount + 1;
|
|
23
|
+
if (shouldSetState) {
|
|
24
|
+
const newValue = frameState.framesCount;
|
|
25
|
+
const totalCount = frameState.totalCount + 1;
|
|
26
|
+
const newMean = Math.min(
|
|
27
|
+
frameState.average + (newValue - frameState.average) / totalCount,
|
|
28
|
+
60
|
|
29
|
+
);
|
|
30
|
+
setFrameState({
|
|
31
|
+
fps: frameState.framesCount,
|
|
32
|
+
lastStamp: currentStamp,
|
|
33
|
+
framesCount: 0,
|
|
34
|
+
average: newMean,
|
|
35
|
+
totalCount
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
setFrameState({
|
|
39
|
+
...frameState,
|
|
40
|
+
framesCount: newFramesCount
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}, 0);
|
|
44
|
+
});
|
|
45
|
+
return () => {
|
|
46
|
+
if (timeout) clearTimeout(timeout);
|
|
47
|
+
};
|
|
48
|
+
}, [frameState]);
|
|
49
|
+
return { average: frameState.average, fps: frameState.fps };
|
|
50
|
+
}
|
|
51
|
+
var FpsCounterModule = reactNative.NativeModules.FpsCounter;
|
|
52
|
+
function useUIFPSMetric() {
|
|
53
|
+
const [frameData, setFrameData] = react.useState({
|
|
54
|
+
uiFps: 0,
|
|
55
|
+
uiAverage: 0,
|
|
56
|
+
isAvailable: !!FpsCounterModule
|
|
57
|
+
});
|
|
58
|
+
const eventEmitterRef = react.useRef(null);
|
|
59
|
+
react.useEffect(() => {
|
|
60
|
+
if (!FpsCounterModule) {
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
console.warn(
|
|
63
|
+
"[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."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
eventEmitterRef.current = new reactNative.NativeEventEmitter(FpsCounterModule);
|
|
69
|
+
const subscription = eventEmitterRef.current.addListener(
|
|
70
|
+
"onUIFpsUpdate",
|
|
71
|
+
(data) => {
|
|
72
|
+
setFrameData({
|
|
73
|
+
uiFps: Math.round(data.fps),
|
|
74
|
+
uiAverage: data.average,
|
|
75
|
+
isAvailable: true
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
FpsCounterModule.startMonitoring();
|
|
80
|
+
return () => {
|
|
81
|
+
subscription.remove();
|
|
82
|
+
FpsCounterModule.stopMonitoring();
|
|
83
|
+
};
|
|
84
|
+
}, []);
|
|
85
|
+
return frameData;
|
|
86
|
+
}
|
|
87
|
+
var defaultStyles = reactNative.StyleSheet.create({
|
|
88
|
+
container: {
|
|
89
|
+
position: "absolute",
|
|
90
|
+
top: 50,
|
|
91
|
+
left: 8,
|
|
92
|
+
backgroundColor: "rgba(0, 0, 0, 0.75)",
|
|
93
|
+
paddingHorizontal: 10,
|
|
94
|
+
paddingVertical: 6,
|
|
95
|
+
borderRadius: 6,
|
|
96
|
+
minWidth: 80
|
|
97
|
+
},
|
|
98
|
+
row: {
|
|
99
|
+
flexDirection: "row",
|
|
100
|
+
justifyContent: "space-between",
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
marginVertical: 1
|
|
103
|
+
},
|
|
104
|
+
label: {
|
|
105
|
+
color: "rgba(255, 255, 255, 0.7)",
|
|
106
|
+
fontSize: 10,
|
|
107
|
+
fontWeight: "500",
|
|
108
|
+
marginRight: 8
|
|
109
|
+
},
|
|
110
|
+
value: {
|
|
111
|
+
color: "white",
|
|
112
|
+
fontSize: 12,
|
|
113
|
+
fontWeight: "700",
|
|
114
|
+
fontVariant: ["tabular-nums"],
|
|
115
|
+
minWidth: 45,
|
|
116
|
+
textAlign: "right"
|
|
117
|
+
},
|
|
118
|
+
divider: {
|
|
119
|
+
height: 1,
|
|
120
|
+
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
|
121
|
+
marginVertical: 4
|
|
122
|
+
},
|
|
123
|
+
sectionLabel: {
|
|
124
|
+
color: "rgba(255, 255, 255, 0.5)",
|
|
125
|
+
fontSize: 9,
|
|
126
|
+
fontWeight: "600",
|
|
127
|
+
textTransform: "uppercase",
|
|
128
|
+
letterSpacing: 0.5,
|
|
129
|
+
marginBottom: 2
|
|
130
|
+
},
|
|
131
|
+
unavailable: {
|
|
132
|
+
color: "rgba(255, 255, 255, 0.4)",
|
|
133
|
+
fontSize: 9,
|
|
134
|
+
fontStyle: "italic"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
var FpsCounter = ({
|
|
138
|
+
visible = true,
|
|
139
|
+
showJsFps = true,
|
|
140
|
+
showUiFps = true,
|
|
141
|
+
showAverage = true,
|
|
142
|
+
averageDecimalPlaces = 1,
|
|
143
|
+
style
|
|
144
|
+
}) => {
|
|
145
|
+
const { fps: jsFps, average: jsAverage } = useFPSMetric();
|
|
146
|
+
const { uiFps, uiAverage, isAvailable: isUiFpsAvailable } = useUIFPSMetric();
|
|
147
|
+
if (!visible) return null;
|
|
148
|
+
const shouldShowUiFps = showUiFps && isUiFpsAvailable;
|
|
149
|
+
const shouldShowUiFpsUnavailable = showUiFps && !isUiFpsAvailable;
|
|
150
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
151
|
+
reactNative.View,
|
|
152
|
+
{
|
|
153
|
+
pointerEvents: "none",
|
|
154
|
+
style: [defaultStyles.container, style?.container],
|
|
155
|
+
children: [
|
|
156
|
+
shouldShowUiFps && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
157
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: defaultStyles.sectionLabel, children: "UI Thread" }),
|
|
158
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: defaultStyles.row, children: [
|
|
159
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [defaultStyles.label, style?.labelText], children: "FPS" }),
|
|
160
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [defaultStyles.value, style?.valueText], children: uiFps })
|
|
161
|
+
] }),
|
|
162
|
+
showAverage && /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: defaultStyles.row, children: [
|
|
163
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [defaultStyles.label, style?.labelText], children: "Avg" }),
|
|
164
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [defaultStyles.value, style?.valueText], children: uiAverage.toFixed(averageDecimalPlaces) })
|
|
165
|
+
] })
|
|
166
|
+
] }),
|
|
167
|
+
shouldShowUiFpsUnavailable && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
168
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: defaultStyles.sectionLabel, children: "UI Thread" }),
|
|
169
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: defaultStyles.unavailable, children: "Native module not linked" })
|
|
170
|
+
] }),
|
|
171
|
+
(shouldShowUiFps || shouldShowUiFpsUnavailable) && showJsFps && /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: defaultStyles.divider }),
|
|
172
|
+
showJsFps && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
173
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: defaultStyles.sectionLabel, children: "JS Thread" }),
|
|
174
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: defaultStyles.row, children: [
|
|
175
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [defaultStyles.label, style?.labelText], children: "FPS" }),
|
|
176
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [defaultStyles.value, style?.valueText], children: jsFps })
|
|
177
|
+
] }),
|
|
178
|
+
showAverage && /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: defaultStyles.row, children: [
|
|
179
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [defaultStyles.label, style?.labelText], children: "Avg" }),
|
|
180
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [defaultStyles.value, style?.valueText], children: jsAverage.toFixed(averageDecimalPlaces) })
|
|
181
|
+
] })
|
|
182
|
+
] })
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
exports.FpsCounter = FpsCounter;
|
|
189
|
+
exports.useFPSMetric = useFPSMetric;
|
|
190
|
+
exports.useUIFPSMetric = useUIFPSMetric;
|
|
191
|
+
//# sourceMappingURL=index.js.map
|
|
192
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useFPSMetric.ts","../src/useUIFPSMetric.ts","../src/FpsCounter.tsx"],"names":["useState","useEffect","NativeModules","useRef","NativeEventEmitter","StyleSheet","jsxs","View","Fragment","jsx","Text"],"mappings":"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","file":"index.js","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"]}
|