はじめに
ブラウザ環境でも、他環境の手法を活用したりするためにrequestAnimationFrameなどを使用してメインループ方式で駆動させることもあるかと思われます。
その時に実行速度が可視化されていた方が、いいことが多いのではないかと思われます。
手法(実装方針)
- フレームレートを計測するために最低限必要なフレーム数を決める
これに関しては60フレームを採用するのが無難かと思われます - フレーム処理時間配列として、最低限必要なフレーム数ぶんの要素数の配列を宣言、生成して全要素に0を設定する
この配列に最新のフレームごとの処理にかかった時間が最低限必要なフレーム数の間保持されます - 時間カウンター変数とフレームカウンター変数を宣言し、両方ともゼロクリアする
上の配列とどちらか片方でいいようにも実装できますが、今回は併用する方針で実装します
(ここまで初期化処理、これ以降はループ内の処理) - ループ処理で現在フレームの処理時間を計算し、フレームレート計測処理のループ内処理に渡す
現在フレームの処理時間はフレームレートの計測以外でも使用することもあると思われるため、ループの先頭であらかじめ計算しておいてループ内の処理で使い回すのがいいかと考えられます - 時間カウンター変数からフレーム処理時間配列の【フレームカウンター変数】番目の要素の値を減算する
時間カウンター変数に蓄積している時間から、最低限必要なフレーム数ぶん前の影響を打ち消します
これにより、フレームレート計測処理でfor文を回して区間合計を計測しなくてもよくなります - 時間カウンター変数に現在フレームの処理時間を加算する
フレームレートの計算に使われます - フレーム処理時間配列の【フレームカウンター変数】番目の要素に現在フレームの処理時間を代入する
将来の手順5. のために値を保持しておきます - フレームカウンター変数をインクリメントする
値が最低限必要なフレーム数以上になった場合は最低限必要なフレーム数の値以上にならないようにします
加えて、最低限必要なフレーム数ぶんの時間が経過したフラグを立てます - 最低限必要なフレーム数ぶんの時間が経過したフラグが立っている場合は、フレームレートを計算して変数にキャッシュする
【フレームレート】=【数値の単位で1秒を表すのに必要な数値】×【最低限必要なフレーム数】÷【時間カウンター変数の値】
(「数値の単位で1秒を表すのに必要な数値」の例:時間の単位が1ミリ秒の場合は1000.0)
また、この手法はJavascript以外でも活用できると思われます。
実装(Javascript)
取り回しを考えて、クラスとしてひとまとめに実装しました。
// フレームレートを計測するクラス
export default class Fps
{
// 計測に最低限必要なフレーム数
#maxFrame = 60;
// フレームカウンター変数 (手順3.)
#currentFrame = 0;
// 最低限必要なフレーム数ぶんの時間が経過したフラグ
#isComputeFps = false;
// フレーム処理時間配列 (手順2.)
#frameDeltaTimes = null;
// 時間カウンター変数 (手順3.)
#totalTimes = 0;
// フレームレート
#fps = 0;
/**
* フレームレートを計測するインスタンスを生成する
* @param {number} maxFrame 計測に最低限必要なフレーム数
*/
constructor(maxFrame)
{
// 手順1.
const processedMaxFrame = (Math.trunc(maxFrame) > 0) ? Math.trunc(maxFrame) : 1;
this.#maxFrame = processedMaxFrame;
// 手順2.
this.#frameDeltaTimes = Array(processedMaxFrame);
for (var i = 0; i < processedMaxFrame; ++i)
{
this.#frameDeltaTimes[i] = 0;
}
}
/**
* ループ内処理
* @param {number} deltaTimeMilli 処理時間(ミリ秒単位)
*/
OnUpdate(deltaTimeMilli)
{
// 手順5.
this.#totalTimes -= this.#frameDeltaTimes[this.#currentFrame];
// 手順6.
this.#totalTimes += deltaTimeMilli;
// 手順7.と手順8.
this.#frameDeltaTimes[this.#currentFrame++] = deltaTimeMilli;
if (this.#currentFrame >= this.#maxFrame)
{
// 手順8.
this.#isComputeFps = true;
do
{
this.#currentFrame -= this.#maxFrame;
} while (this.#currentFrame >= this.#maxFrame);
}
if (this.#isComputeFps)
{
// 手順9.
this.#fps = 1000.0 * this.#maxFrame / this.#totalTimes;
}
}
/**
* フレームレートを取得する
* @returns {number} フレームレート
*/
GetFps()
{
return this.#isComputeFps ? this.#fps : 0;
}
}
使い方
import Fps from "./Common/Fps.js";でFps.jsをモジュールとして読み込み、const fps = new Fps(60);といったようにインスタンスを生成し、ループ更新処理(のできれば最初の方)でfps.OnUpdate(deltaTimeMilli);といったようにフレームの処理時間をミリ秒で渡して呼び出してください。
また、計測されたフレームレートはfps.GetFps()で取得できます。ただし、計測に最低限必要なフレーム数が経過するまではフレームレートの計算を行わず0を返します。
また、このクラスのOnUpdate(deltaTimeMilli)に渡すフレームの処理時間は、
// beforeTimestampは前フレームのcurrentTimestampの値
const currentTimestamp = new Date().getTime();
const deltaTimeMilli = currentTimestamp - beforeTimestamp;
beforeTimestamp = currentTimestamp;
といった処理の結果の値を想定していますが、ミリ秒単位で経過時間をとることができれば他の手法でも問題ありません。