Choreographer クラスによる FPS 計測
Android の Choreographer クラスを使用すると、フレームの描画開始のタイミングで呼び出されるコールバック (Choreographer.FrameCallback) を登録することができます。
Choreographer#postFrameCallback(callback: Choreographer.FrameCallback!)
上記のメソッドでコールバックを登録すると、次のフレーム描画のタイミングで doFrame(long frameTimeNanos)
メソッドが呼び出されます。
Choreographer.FrameCallback#doFrame(frameTimeNanos: Long)
パラメータとして描画開始時刻(ナノ秒単位)が渡されるため、前回のコールバック時の描画開始時刻からの差分を取れば、1 フレームの描画にかかった時間を求めることができます。 この値を使えば、FPS (Frame per second) は下記のように計算できます。
FPS = 1秒あたりのナノ秒 / 描画にかかった時間(ナノ秒)
サンプルコード
Fps クラスの実装
下記の Fps
クラスは、FPS を簡単に計測するためのクラスです。
内部で Android の Choreographer
を使っています。
import android.view.Choreographer
import java.util.concurrent.TimeUnit
class Fps : Choreographer.FrameCallback {
interface FpsCallback {
/** Called when the latest FPS is calculated. */
fun onFpsUpdated(fps: Float)
}
private val choreographer = Choreographer.getInstance()
private var fpsCallback: FpsCallback? = null
private var prevFrameTimeNanos: Long = 0
/**
* Starts observing the FPS.
* [fpsCallback] is invoked continuously after calling this method.
*/
fun startObserving(fpsCallback: FpsCallback) {
this.fpsCallback = fpsCallback
prevFrameTimeNanos = 0
choreographer.postFrameCallback(this)
}
/**
* Starts observing the FPS.
* [fpsCallback] is invoked continuously after calling this method.
*/
fun startObserving(fpsCallback: (Float) -> Unit) {
startObserving(object : FpsCallback {
override fun onFpsUpdated(fps: Float) = fpsCallback(fps)
})
}
/**
* Stops observing the FPS.
*/
fun stopObserving() {
choreographer.removeFrameCallback(this)
}
/**
* Implementation for [Choreographer.FrameCallback].
*/
override fun doFrame(frameTimeNanos: Long) {
// Register the same callback again to be called continuously
choreographer.postFrameCallback(this)
// At first, just store the frame time for later calculation
if (prevFrameTimeNanos == 0L) {
prevFrameTimeNanos = frameTimeNanos
return
}
// Calculate FPS and pass it to the callback function
val elapsed = frameTimeNanos - prevFrameTimeNanos
val fps = TimeUnit.SECONDS.toNanos(1) / elapsed.toFloat()
checkNotNull(fpsCallback).onFpsUpdated(fps)
prevFrameTimeNanos = frameTimeNanos
}
}
Choreographer#postFrameCallback()
で登録したコールバックは、一度しか呼び出されないことに注意してください。
連続してフレーム描画のタイミングを取得するには、毎回同じコールバックを登録し直す必要があります(上記では、doFrame()
メソッドの先頭で登録しています)。
Fps クラスの使用例
Fps
クラスを使って FPS の観測を行うには、例えば次のようにします。
Fps().startObserving { fps ->
Log.i("DEBUG", "FPS = %.3f".format(fps))
}
(応用) 1 秒おきに平均 FPS を求める
上記のサンプルコードでは、毎フレーム FPS を求めてコールバック関数を呼び出していましたが、これでは高頻度すぎるという場合は、下記のようにすれば約 1 秒ごとに呼び出すように軽量化できます。
FrameCallback.doFrame()
が呼び出されたときに、前回のコールバックから 1 秒以上経過している場合だけ、コールバックするようにしています。
ここでは、約 1 秒間のフレーム数を数えて平均 FPS を求めています。
import android.view.Choreographer
import java.util.concurrent.TimeUnit
/**
* Utility class for obtaining FPS (frames per second).
*/
class Fps : Choreographer.FrameCallback {
companion object {
/** Callbacks are invoked at intervals of this time. */
private val CALLBACK_INTERVAL_NANOS = TimeUnit.SECONDS.toNanos(1)
}
interface FpsCallback {
/** Called when the latest FPS is calculated. */
fun onFpsUpdated(fps: Double)
}
private val choreographer = Choreographer.getInstance()
private var fpsCallback: FpsCallback? = null
private var prevCallbackTimeNanos: Long = 0
/** How many times doFrame is called since the last onFpsUpdated. */
private var frames = 0
/**
* Starts observing the FPS.
* [fpsCallback] is invoked continuously after calling this method.
*/
fun startObserving(fpsCallback: FpsCallback) {
this.fpsCallback = fpsCallback
prevCallbackTimeNanos = 0
frames = 0
// Add a frame callback but prevents duplicate registration
choreographer.removeFrameCallback(this)
choreographer.postFrameCallback(this)
}
/**
* Starts observing the FPS.
* [fpsCallback] is invoked continuously after calling this method.
*/
fun startObserving(fpsCallback: (Double) -> Unit) {
startObserving(object : FpsCallback {
override fun onFpsUpdated(fps: Double) = fpsCallback(fps)
})
}
/**
* Stops observing the FPS.
*/
fun stopObserving() {
choreographer.removeFrameCallback(this)
}
/**
* Implementation for [Choreographer.FrameCallback].
*/
override fun doFrame(frameTimeNanos: Long) {
// Register the same callback again to be called continuously
choreographer.postFrameCallback(this)
// At first, just store the frame time for later calculation
if (prevCallbackTimeNanos == 0L) {
prevCallbackTimeNanos = frameTimeNanos
return
}
frames++
// Callback at the intervals of CALLBACK_INTERVAL_NANOS
val elapsed = frameTimeNanos - prevCallbackTimeNanos
if (elapsed >= CALLBACK_INTERVAL_NANOS) {
// Calculate FPS and pass it to the callback function
val fps = frames.toDouble() * TimeUnit.SECONDS.toNanos(1) / elapsed
checkNotNull(fpsCallback).onFpsUpdated(fps)
// Reset counters
prevCallbackTimeNanos = frameTimeNanos
frames = 0
}
}
}
(応用) 1 秒おきに最低 FPS を求める
上記の例では、1 秒間の描画フレーム数を数えることで秒間平均 FPS を求めていましたが、その方法だと、あるフレームが非常に遅くても、残りのフレームが高速であれば、FPS としては比較的高い値が出てしまいます。
ここでは、もっとストイックに、過去 1 秒間で最も時間のかかったフレームの描画時間をもとに、FPS 計算するようにしてみます。 つまり、 秒間最低 FPS(秒間最悪 FPS) です。
秒間最低FPS = 1秒あたりのns / 最も時間のかかったフレームの描画時間(ns)
import android.view.Choreographer
import java.lang.Long.max
import java.util.concurrent.TimeUnit
/**
* Utility class for obtaining FPS (frames per second).
*/
class Fps : Choreographer.FrameCallback {
companion object {
/** Callbacks are invoked at intervals of this time. */
private val CALLBACK_INTERVAL_NANOS = TimeUnit.SECONDS.toNanos(1)
}
interface FpsCallback {
/** Called when the latest FPS is calculated. */
fun onFpsUpdated(fps: Double)
}
private val choreographer = Choreographer.getInstance()
private var fpsCallback: FpsCallback? = null
private var prevCallbackTimeNanos: Long = 0
private var prevFrameTimeNanos: Long = 0
/** Maximum frame time over the last one minute. */
private var worstFrameTimeNanos: Long = 1
/**
* Starts observing the FPS.
* [fpsCallback] is invoked continuously after calling this method.
*/
fun startObserving(fpsCallback: FpsCallback) {
this.fpsCallback = fpsCallback
prevCallbackTimeNanos = 0
// Add a frame callback but prevents duplicate registration
choreographer.removeFrameCallback(this)
choreographer.postFrameCallback(this)
}
/**
* Starts observing the FPS.
* [fpsCallback] is invoked continuously after calling this method.
*/
fun startObserving(fpsCallback: (Double) -> Unit) {
startObserving(object : FpsCallback {
override fun onFpsUpdated(fps: Double) = fpsCallback(fps)
})
}
/**
* Stops observing the FPS.
*/
fun stopObserving() {
choreographer.removeFrameCallback(this)
}
/**
* Implementation for [Choreographer.FrameCallback].
*/
override fun doFrame(frameTimeNanos: Long) {
// Register the same callback again to be called continuously
choreographer.postFrameCallback(this)
// At first, just store the frame time for later calculation
if (prevCallbackTimeNanos == 0L) {
prevCallbackTimeNanos = frameTimeNanos
prevFrameTimeNanos = frameTimeNanos
return
}
worstFrameTimeNanos = max(worstFrameTimeNanos, frameTimeNanos - prevFrameTimeNanos)
// Callback at the intervals of CALLBACK_INTERVAL_NANOS
val elapsed = frameTimeNanos - prevCallbackTimeNanos
if (elapsed >= CALLBACK_INTERVAL_NANOS) {
// Calculate the minimum FPS and pass it to the callback function
val fps = TimeUnit.SECONDS.toNanos(1).toDouble() / worstFrameTimeNanos
checkNotNull(fpsCallback).onFpsUpdated(fps)
// Reset counters
prevCallbackTimeNanos = frameTimeNanos
worstFrameTimeNanos = 1
}
prevFrameTimeNanos = frameTimeNanos
}
}