Adds a corner-overlay FPS readout gated on HERMES_TUI_FPS, fed by
ink's onFrame callback (so it's the REAL render rate, not a timer).
Displays fps, last-frame duration, and total frame count, colored by
threshold (green ≥50, yellow ≥30, red below).
Implementation:
* lib/fpsStore.ts — nanostore atom updated from a trackFrame()
sink. Ring buffer of last 30 frame timestamps; fps = 29/elapsed.
trackFrame is undefined when SHOW_FPS is off so ink's onFrame
short-circuits at the optional chain.
* components/fpsOverlay.tsx — tiny <Text> subscriber; returns null
when SHOW_FPS is off (React skips the subtree entirely).
* entry.tsx — composes onFrame from logFrameEvent (dev-perf) and
trackFrame (fps) so both flags can coexist. When both are off,
onFrame is undefined and ink never attaches the handler.
* appLayout.tsx — mounts the overlay as a flex-shrink=0 right-
aligned Box below the composer, conditional on SHOW_FPS.
Usage:
HERMES_TUI_FPS=1 hermes --tui
# bottom right: " 62.3fps · 0.8ms · #1234" (green/yellow/red)
Intended as a user-facing diagnostic during the scroll-perf tuning
pass — watch the counter drop while holding PageUp to see where
frames go silent, without having to run scripts/profile-tui.py in a
side terminal.
126 files post-compile with React Compiler; 352 tests still pass.
49 lines
1.2 KiB
TypeScript
49 lines
1.2 KiB
TypeScript
// FPS counter overlay — renders in the bottom-right corner when
|
|
// HERMES_TUI_FPS=1. Zero-cost when disabled (returns null at the
|
|
// top of the component; React skips the whole subtree).
|
|
//
|
|
// Subscribes to $fpsState via nanostores. The store is only updated
|
|
// when the env flag is on (trackFrame is undefined otherwise), so we
|
|
// also gate the subscription on SHOW_FPS to avoid a useless listener.
|
|
|
|
import { Text } from '@hermes/ink'
|
|
import { useStore } from '@nanostores/react'
|
|
|
|
import { SHOW_FPS } from '../config/env.js'
|
|
import { $fpsState } from '../lib/fpsStore.js'
|
|
|
|
const fpsColor = (fps: number) => {
|
|
if (fps >= 50) {
|
|
return 'green'
|
|
}
|
|
|
|
if (fps >= 30) {
|
|
return 'yellow'
|
|
}
|
|
|
|
return 'red'
|
|
}
|
|
|
|
export function FpsOverlay() {
|
|
if (!SHOW_FPS) {
|
|
return null
|
|
}
|
|
|
|
return <FpsOverlayInner />
|
|
}
|
|
|
|
function FpsOverlayInner() {
|
|
const { fps, lastDurationMs, totalFrames } = useStore($fpsState)
|
|
|
|
// Zero-pad to stable width so the corner doesn't jitter as digits
|
|
// come and go. Format: " 62fps 0.3ms #12345"
|
|
const fpsStr = fps.toFixed(1).padStart(5)
|
|
const durStr = lastDurationMs.toFixed(1).padStart(5)
|
|
|
|
return (
|
|
<Text color={fpsColor(fps)}>
|
|
{fpsStr}fps · {durStr}ms · #{totalFrames}
|
|
</Text>
|
|
)
|
|
}
|