はじめに
マウスのポーリングレートをブラウザで確認するツールを個人で開発しています。
開発で一番悩んだのは、値を算出することよりも、算出できてしまう値をいつ表示しないかでした。たとえば 8000Hz のマウスでは、サンプルの間隔はわずか 0.125ms です。タイムスタンプの分解能が 0.1ms なら、間隔のばらつきはほとんど読み取れません。それでも計算式は何かしらの数値を返します。
この記事では、ブラウザ上の計測で起きる誤りと、数値を保留する条件の設計をまとめます。
前提
- 対象は、Pointer Events で取得したマウス移動サンプルです
- Web ページから USB の転送は読めないため、値は「ブラウザから観測できたサンプルレート」です
- しきい値は筆者のツール(計測エンジン v2.1.0)で採用しているもので、標準規格ではありません
- 回帰テストは合成データとブラウザ自動操作によるもので、125〜8000Hz の実機で精度を検証した結果ではありません
落とし穴 1: タイムスタンプの分解能
ポーリングレートごとの理論上のサンプル間隔は次のとおりです。
| レート | 間隔 |
|---|---|
| 1000Hz | 1ms |
| 2000Hz | 0.5ms |
| 4000Hz | 0.25ms |
| 8000Hz | 0.125ms |
ブラウザの時刻はプライバシー保護などの理由で粗められています。ツールでは performance.now() の短いプローブに、ブラウザごとの保守的な下限を組み合わせて分解能を見積もっています。
| 条件 | 想定する分解能の下限 |
|---|---|
| 通常 | 0.1ms |
| Firefox / Safari | 1ms |
| cross-origin isolated | 0.005ms |
| cross-origin isolated の Firefox / Safari | 0.02ms |
あくまで防御的な見積もりで、すべてのイベントの実際の分解能を測ったものではありません。
間隔に基づく指標(ピーク、ジッターなど)は、間隔の中央値が分解能の 4 倍以上のときだけ表示します。
const resolutionRatio = period / clockGrain;
下限が 0.1ms なら、条件を満たす中央値は 0.4ms 以上で、レートに換算すると約 2500Hz までです。4000Hz や 8000Hz ではピークとジッターは — になります。計測時間を延ばしても分解能は上がらないので、これは長く測っても解決しません。
一方、平均や 500ms 窓のレートはサンプルの個数から求めるので、分解能の影響をほとんど受けません。「分解能が足りないから全部出さない」のではなく、指標ごとに判断を分けています。
落とし穴 2: 画面のフレームに同期した配信
pointermove の呼び出しがフレームに揃っていて getCoalescedEvents() でも個別のサンプルが出てこない場合、数えているのはマウスではなく画面の更新回数です。
ただし、レートが近いだけでは同期とは言えません。125Hz のマウスと 120Hz の画面は値が近くても、フレームに同期しているとは限らないからです。そこで複数の条件をすべて満たしたときだけ「フレーム同期の疑いあり」とします。
// 到着時刻をフレーム周期上の位相に変換する
const phases = arrivalTimes
.slice(-240)
.map((t) => (2 * Math.PI * (t % frameMs)) / frameMs);
// 位相の集中度(1 に近いほど同じ位相に集まっている)
const phaseConcentration = Math.hypot(
mean(phases.map(Math.cos)),
mean(phases.map(Math.sin))
);
const frameAligned =
frames.length >= 30 &&
arrivals.length >= 60 &&
frameMs > 0 &&
phaseConcentration > 0.95 &&
Math.abs(dispatchMs - frameMs) / frameMs < 0.08 &&
arrivals.filter((x) => Math.abs(x - frameMs) < frameMs * 0.2).length /
arrivals.length > 0.8;
位相の集中度を見ているのが要点です。周期が近いだけなら到着の位相はフレーム上を少しずつずれていきますが、本当に同期していれば同じ位置に集まります。
また、親イベントしか得られない pointermove では、個々のサンプルが公開されているとは言えません。この場合も品質に関する指標は保留します。
落とし穴 3: 手を止めた時間
計測中に手を止めると、サンプルの間隔が極端に長くなります。ピークやジッターにそのまま入れると、マウスの性質ではなくユーザーの動きを測ることになります。
そこで指標ごとに扱いを変えています。
| 対象 | 100ms 以上の間隔の扱い |
|---|---|
| ピーク / ジッター | 除外する |
| 平均 / 安定性 / 最低 500ms レート | 経過時間や空の窓としてそのまま反映する |
タブが非表示になったりフォーカスが外れたりした場合は、その計測を中断扱いにして、品質に関する指標は出しません。
落とし穴 4: 短すぎる計測
品質に関する指標には、最初の移動から 3 秒以上かつ 100 サンプル以上を要求しています。
間隔の指標にはさらに条件があります。
const timingOK =
enough && !limited && !bad && !interruption &&
period > 0 &&
resolutionRatio >= 4 &&
duplicateShare <= 0.01 &&
timingIntervals.length >= 50 &&
resolvableIntervals.length >= 20;
-
duplicateShare: 同じタイムスタンプが連続する割合で、1% まで許容 -
timingIntervals: 100ms 未満の正の間隔 -
resolvableIntervals: そのうち分解能の 2 倍以上あり、判別できる間隔
分解能より短い間隔が少し混ざっただけなら、計測全体を無効にせず、その間隔だけを統計から外します。
状態と指標を分けて見せる
ステータスは 1 つの「合格 / 不合格」にせず、計測がどの状態かを表します。
| 状態 | 意味 |
|---|---|
| waiting | まだサンプルがない |
| short | サンプルや時間が足りない |
| limited | ブラウザの配信に制限されている疑いがある |
| sufficient | 必要なサンプルが揃っている |
| invalid | 時刻の逆行などがあった |
| interrupted | フォーカス喪失などで中断された |
sufficient でも、分解能が足りなければピークとジッターは — のままです。そして sufficient はハードウェアの健全性を示すものでも、サンプルが漏れなく届いたことを保証するものでもありません。
同じ理由で、基準より長い間隔の件数を「パケットロス」とは表示していません。ブラウザからは原因が USB か OS かブラウザかを区別できないからです。
まだできていないこと
ここまでのロジックは合成データとブラウザ自動操作でテストしています。実機で精度を主張するには、デバイスやファームウェア、OS、ブラウザ、画面のリフレッシュレート、比較に使った基準ツールを明記した繰り返し測定が必要で、失敗や判定不能のケースも含めて公開すべきだと考えています。USB レベルの精度を主張するなら、USB レベルの基準も必要です。別の Web ツールと値が一致しても、その裏付けにはなりません。
おわりに
入力計測のように分解能と配信経路の制約が強い分野では、「出せる数値を全部出す」より「根拠がある数値だけ出す」ほうが、結果として使いやすくなると感じています。
記事中のロジックは、筆者が開発している次のツールで動いています。お手元の環境で — になるかどうかも含めて確認できます。