Use FpsCounter if you don't need the frame rate graph
Last active
November 11, 2025 07:38
-
-
Save miredirex/e7d47d6f85e91cb897032204f2273e3b to your computer and use it in GitHub Desktop.
Android Compose FPS counter with frame rate graph
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.fps | |
| import android.view.Choreographer | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.* | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.text.font.FontStyle | |
| import kotlin.math.roundToInt | |
| private const val UPDATE_FPS_EVERY_MS = 60 | |
| @Composable | |
| fun FpsCounter(modifier: Modifier = Modifier) { | |
| var fps by remember { mutableIntStateOf(0) } | |
| Text("FPS: $fps", modifier = modifier) | |
| DisposableEffect(Unit) { | |
| val everyFrameCallback = object : Choreographer.FrameCallback { | |
| var acc = 0.0 | |
| var latestFrameTime = 0.0 | |
| override fun doFrame(frameTimeNanos: Long) { | |
| val frameTimeMillis: Double = frameTimeNanos / 1_000_000.0 | |
| val deltaMillis = frameTimeMillis - latestFrameTime | |
| acc += deltaMillis | |
| if (acc >= UPDATE_FPS_EVERY_MS) { | |
| fps = (1000.0 / deltaMillis).roundToInt() | |
| acc = 0.0 | |
| } | |
| latestFrameTime = frameTimeMillis | |
| Choreographer.getInstance().postFrameCallback(this) // Enqueue again | |
| } | |
| } | |
| Choreographer.getInstance().postFrameCallback(everyFrameCallback) | |
| onDispose { | |
| Choreographer.getInstance().removeFrameCallback(everyFrameCallback) | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.fps | |
| private const val UPDATE_FPS_EVERY_MS = 60 | |
| private const val GRAPH_Y_AXIS_FPS_LIMIT = 144 | |
| @Composable | |
| fun FpsCounterGraph(modifier: Modifier = Modifier) { | |
| var fps by remember { mutableIntStateOf(0) } | |
| var updateCount by remember { mutableIntStateOf(0) } | |
| Row(modifier = modifier.height(IntrinsicSize.Max)) { | |
| Text( | |
| modifier = Modifier | |
| .weight(1 / 3f) | |
| .align(Alignment.CenterVertically), | |
| text = "FPS: $fps", | |
| fontStyle = FontStyle.Italic | |
| ) | |
| FrameRateGraph( | |
| modifier = Modifier | |
| .weight(2 / 3f) | |
| .fillMaxHeight() | |
| .padding(4.dp) | |
| .clip(RoundedCornerShape(4.dp)) | |
| .background(Color.DarkGray.copy(alpha = 0.25f)), | |
| fps = fps, | |
| updateCount = updateCount | |
| ) | |
| } | |
| DisposableEffect(Unit) { | |
| val everyFrameCallback = object : Choreographer.FrameCallback { | |
| var acc = 0.0 | |
| var latestFrameTime = 0.0 | |
| override fun doFrame(frameTimeNanos: Long) { | |
| val frameTimeMillis: Double = frameTimeNanos / 1_000_000.0 | |
| val deltaMillis = frameTimeMillis - latestFrameTime | |
| acc += deltaMillis | |
| if (acc >= UPDATE_FPS_EVERY_MS) { | |
| fps = (1000.0 / deltaMillis).roundToInt() | |
| updateCount += 1 | |
| acc = 0.0 | |
| } | |
| latestFrameTime = frameTimeMillis | |
| Choreographer.getInstance().postFrameCallback(this) // Enqueue again | |
| } | |
| } | |
| Choreographer.getInstance().postFrameCallback(everyFrameCallback) | |
| onDispose { | |
| Choreographer.getInstance().removeFrameCallback(everyFrameCallback) | |
| } | |
| } | |
| } | |
| @Composable | |
| private fun FrameRateGraph(fps: Int, updateCount: Int, modifier: Modifier = Modifier) { | |
| val graphPoints = remember { MutableLongList(initialCapacity = 0) } | |
| Canvas(modifier) { | |
| val canvasWidth = drawContext.size.width | |
| if (graphPoints.capacity == 0) | |
| graphPoints.ensureCapacity(canvasWidth.toInt()) | |
| var firstPointX = 0f | |
| if (graphPoints.size == graphPoints.capacity) | |
| firstPointX = unpackFloat1(graphPoints.removeAt(0)) | |
| val x = updateCount.toFloat() | |
| val y = size.height - (fps / GRAPH_Y_AXIS_FPS_LIMIT.toFloat() * size.height) | |
| graphPoints.add(packFloats(x, y)) | |
| translate(left = if (x > canvasWidth) -firstPointX else 0f) { | |
| graphPoints.forEach { | |
| drawCircle(Color.LightGray, 1f, Offset(unpackFloat1(it), unpackFloat2(it))) | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
