Skip to content

Instantly share code, notes, and snippets.

@miredirex
Last active November 11, 2025 07:38
Show Gist options
  • Select an option

  • Save miredirex/e7d47d6f85e91cb897032204f2273e3b to your computer and use it in GitHub Desktop.

Select an option

Save miredirex/e7d47d6f85e91cb897032204f2273e3b to your computer and use it in GitHub Desktop.
Android Compose FPS counter with frame rate graph
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)
}
}
}

FpsCounterGraph composable: Frame rate graph demo

Use FpsCounter if you don't need the frame rate graph

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