Skip to content

Instantly share code, notes, and snippets.

@regenrek
Last active March 22, 2025 09:22
Show Gist options
  • Save regenrek/0076f7ff32765edcc11aba6770343d2b to your computer and use it in GitHub Desktop.
Save regenrek/0076f7ff32765edcc11aba6770343d2b to your computer and use it in GitHub Desktop.
import { CSSProperties, useEffect, useState } from 'react';
import { useFrame } from '@react-three/fiber';
import { useWorld } from 'koota/react';
// Define the Chrome performance memory interface
interface PerformanceMemory {
usedJSHeapSize: number;
jsHeapSizeLimit: number;
totalJSHeapSize: number;
}
interface ExtendedPerformance extends Performance {
memory?: PerformanceMemory;
}
export function DebugMetricsOverlay() {
const world = useWorld();
const [isActive, setIsActive] = useState(true);
const [metrics, setMetrics] = useState({
fps: 0,
memoryMB: 0,
entityCount: 0,
drawCalls: 0,
triangles: 0,
});
const [frameSamples, setFrameSamples] = useState<number[]>([]);
const maxSamples = 60;
// F3 Toggle handler
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'F3' || e.key === 'f3') {
setIsActive((prev) => !prev);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// Update metrics every frame - must be at the top level
useFrame(({ gl }, delta) => {
if (!isActive) return;
// FPS calculation
if (delta > 0) {
const fpsThisFrame = 1 / delta;
const newSamples = [...frameSamples, fpsThisFrame];
if (newSamples.length > maxSamples) {
newSamples.shift();
}
setFrameSamples(newSamples);
const sum = newSamples.reduce((a, b) => a + b, 0);
const avgFps = sum / newSamples.length;
// Memory usage (if available)
const perf = performance as ExtendedPerformance;
const memory = perf.memory?.usedJSHeapSize ? perf.memory.usedJSHeapSize / (1024 * 1024) : 0;
// Entity count from Koota
const entityCount = world.entities.length;
// Renderer stats
const drawCalls = gl.info.render?.calls || 0;
const triangles = gl.info.render?.triangles || 0;
setMetrics({
fps: avgFps,
memoryMB: memory,
entityCount,
drawCalls,
triangles,
});
}
});
// If not active, render nothing
if (!isActive) return null;
const { fps, memoryMB, entityCount, drawCalls, triangles } = metrics;
// Color thresholds
const fpsColor = fps < 30 ? 'red' : fps < 55 ? 'yellow' : 'green';
const memColor = memoryMB > 500 ? 'red' : memoryMB > 200 ? 'yellow' : 'green';
const dcColor = drawCalls > 500 ? 'red' : drawCalls > 100 ? 'yellow' : 'green';
const triColor = triangles > 500000 ? 'red' : triangles > 100000 ? 'yellow' : 'green';
const containerStyle: CSSProperties = {
position: 'absolute',
top: 0,
right: 0,
padding: 40,
backgroundColor: 'rgba(20, 20, 20, 0.6)',
color: 'white',
fontSize: '1rem',
fontFamily: 'monospace',
zIndex: 5,
borderRadius: '4px',
lineHeight: 1.4,
};
return (
<div style={containerStyle}>
<div>
FPS: <span style={{ color: fpsColor }}>{fps.toFixed(1)}</span>
</div>
<div>
MEM: <span style={{ color: memColor }}>{memoryMB.toFixed(1)} MB</span>
</div>
<div>Entities: {entityCount}</div>
<div>
DrawCalls: <span style={{ color: dcColor }}>{drawCalls}</span>
</div>
<div>
Triangles: <span style={{ color: triColor }}>{triangles}</span>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment