import { useContext, useEffect, useRef, useState } from 'react' import type { FrameEvent } from '../../core/frame.js' import FrameStatsContext from '../components/FrameStatsContext.js' /** * Frame rate statistics returned by `useFps`. */ export type FpsStats = { /** Current frames per second (rolling average over the sample window). */ fps: number /** Last frame duration in milliseconds. */ lastFrameMs: number /** Total frames counted since mount. */ totalFrames: number /** Per-phase timing from the most recent frame (if available). */ phases: FrameEvent['phases'] | undefined } const EMPTY_STATS: FpsStats = { fps: 0, lastFrameMs: 0, totalFrames: 0, phases: undefined, } /** * Hook that measures the actual terminal rendering frame rate. * * Subscribes to Ink's frame events and computes a rolling FPS average. * Returns `{ fps, lastFrameMs, totalFrames, phases }`. * * @param sampleWindowMs - Rolling window for FPS calculation (default 1000ms) */ export function useFps(sampleWindowMs = 1000): FpsStats { const ctx = useContext(FrameStatsContext) const [stats, setStats] = useState(EMPTY_STATS) // Ring buffer of frame timestamps for rolling FPS const timestampsRef = useRef([]) const totalRef = useRef(0) useEffect(() => { if (!ctx) return return ctx.subscribe((event: FrameEvent) => { const now = performance.now() const timestamps = timestampsRef.current timestamps.push(now) // Trim timestamps outside the sample window const cutoff = now - sampleWindowMs while (timestamps.length > 0 && timestamps[0]! < cutoff) { timestamps.shift() } totalRef.current++ // FPS = frames in window / window duration (in seconds) const fps = timestamps.length >= 2 ? ((timestamps.length - 1) / (now - timestamps[0]!)) * 1000 : 0 setStats({ fps: Math.round(fps * 10) / 10, lastFrameMs: Math.round(event.durationMs * 100) / 100, totalFrames: totalRef.current, phases: event.phases, }) }) }, [ctx, sampleWindowMs]) return stats }