Skip to content

Instantly share code, notes, and snippets.

@schickling
Created January 24, 2023 21:07
Show Gist options
  • Save schickling/9a5e835794eae777ed76e5996a205b9c to your computer and use it in GitHub Desktop.
Save schickling/9a5e835794eae777ed76e5996a205b9c to your computer and use it in GitHub Desktop.
import React from 'react'
const PR = Math.round(window.devicePixelRatio || 1)
const FRAME_BAR_WIDTH = 2
export type FPSMeterProps = {
width?: number
height?: number
resolutionInMs?: number
className?: string
}
export const FPSMeter: React.FC<FPSMeterProps> = ({ width = 100, height = 30, resolutionInMs = 10, className }) => {
const adjustedWidth = Math.round(width * PR)
const adjustedHeight = Math.round(height * PR)
const numberOfBars = Math.floor(adjustedWidth / FRAME_BAR_WIDTH)
const numberOfBucketsPerSecond = 1000 / resolutionInMs
const animationFrameRef = React.useRef<number | undefined>(undefined)
const canvasRef = React.useCallback(
(canvas: HTMLCanvasElement | null) => {
if (animationFrameRef.current !== undefined) {
window.cancelAnimationFrame(animationFrameRef.current)
}
if (canvas === null) return
let maxFps = 0
// each element is a frame count for a `resolutionInMs` time bucket
// eslint-disable-next-line unicorn/no-new-array
const frameCountBuckets: number[] = new Array(numberOfBars + 1).fill(0)
const ctx = canvas.getContext('2d')!
// TODO adjust this for 120+ Hz monitors
const draw = () => {
ctx.clearRect(0, 0, adjustedWidth, adjustedHeight)
const frameRateThreshold = maxFps * 0.75
for (const [i, frameCount] of frameCountBuckets.slice(0, -1).entries()) {
const fpsBasedOnFrameCount = frameCount * resolutionInMs
const barHeight = (fpsBasedOnFrameCount / maxFps) * adjustedHeight
const x = i * FRAME_BAR_WIDTH
ctx.fillStyle = fpsBasedOnFrameCount > frameRateThreshold ? 'rgba(255, 255, 255, 0.3)' : 'rgba(255, 0, 0, 1)'
ctx.fillRect(x, adjustedHeight, FRAME_BAR_WIDTH, -barHeight)
}
// write last frame rate as text
ctx.fillStyle = 'white'
const fontSize = PR * 10
ctx.font = `${fontSize}px monospace`
// NOTE larger values can result in more items taken from array than it has
const numberOfSecondsForAverage = 0.5
const averageFps = Math.round(
frameCountBuckets.slice(-numberOfBucketsPerSecond * numberOfSecondsForAverage).reduce((a, b) => a + b, 0) /
(resolutionInMs * numberOfSecondsForAverage),
)
ctx.fillText(`${averageFps} FPS`, 2 * PR, adjustedHeight - 3 * PR)
}
let previousTimeBucket = 0
const loop = () => {
animationFrameRef.current = window.requestAnimationFrame((now) => {
const timeBucket = Math.floor(now / numberOfBucketsPerSecond)
if (timeBucket === previousTimeBucket) {
frameCountBuckets[numberOfBars]++
} else {
previousTimeBucket = timeBucket
const lastFps = frameCountBuckets[numberOfBars]! * resolutionInMs
if (lastFps > maxFps) {
maxFps = lastFps
}
frameCountBuckets.shift()
frameCountBuckets.push(1)
draw()
}
loop()
})
}
loop()
},
[adjustedHeight, adjustedWidth, numberOfBars, numberOfBucketsPerSecond, resolutionInMs],
)
return (
<canvas
width={adjustedWidth}
height={adjustedHeight}
className={className}
ref={canvasRef}
style={{ width, height }}
/>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment