Profiler & Debug System

Built-in performance monitoring and event system for debugging GPU operations.

Enabling Events

Enable the event system when initializing the GPU context. You can optionally filter which event types to track.

TypeScript
import { gpu } from "ralph-gpu";
 
const ctx = await gpu.init(canvas, {
  events: {
    enabled: true,
    types: ['draw', 'compute', 'frame', 'memory'], // Optional filter
    historySize: 1000,  // Event history buffer size
  },
});

Event Options

  • enabled — Enable/disable event emission
  • types — Array of event types to track (omit for all)
  • historySize — Max events to keep in history (default: 1000)

Using the Profiler

The Profiler class provides a high-level API for tracking performance. It automatically subscribes to context events.

TypeScript
import { Profiler } from "ralph-gpu";
 
// Create profiler attached to context
const profiler = new Profiler(ctx, {
  maxFrameHistory: 120,
  autoTrackFrames: true,
});
 
// In your render loop
function animate() {
  profiler.tick();  // Track frame timing for FPS
  
  // Profile specific regions
  profiler.begin('physics');
  updatePhysics();
  profiler.end('physics');
  
  profiler.begin('render');
  myPass.draw();
  profiler.end('render');
  
  // Get stats
  const fps = profiler.getFPS();
  console.log(`FPS: ${fps.toFixed(1)}`);
  
  requestAnimationFrame(animate);
}
 
// Cleanup when done
profiler.dispose();

Profiler API

TypeScript
// FPS tracking (use with pass.draw() API)
profiler.tick()                        // Call once per animation frame
profiler.getFPS(sampleCount?)          // Get averaged FPS (default: 60 samples)
 
// Region profiling - measure specific code sections
profiler.begin(name)                   // Start timing a region
profiler.end(name)                     // End timing a region
profiler.getRegion(name)               // Get stats for a specific region
profiler.getResults()                  // Get all region stats as Map
 
// Frame statistics
profiler.getFrameStats()               // Get overall frame time statistics
profiler.getLastFrames(count)          // Get last N frame profiles
profiler.getAverageFrameTime(frames?)  // Average frame interval (for FPS)
profiler.getAverageRenderTime(frames?) // Average GPU work duration
 
// Control
profiler.setEnabled(enabled)           // Enable/disable profiling
profiler.isEnabled()                   // Check if profiling is enabled
profiler.reset()                       // Clear all collected data
profiler.dispose()                     // Cleanup and unsubscribe from events
Important: Call profiler.tick() once per animation frame for accurate FPS tracking. This works with the regular pass.draw() API — no need to change your rendering code.

Region Profiling

Use begin() and end() to measure specific code sections.

TypeScript
interface ProfilerRegion {
  name: string;        // Region name
  calls: number;       // Total number of calls
  totalTime: number;   // Total time in ms
  minTime: number;     // Minimum time in ms
  maxTime: number;     // Maximum time in ms
  averageTime: number; // Average time in ms
  lastTime: number;    // Most recent time in ms
}
 
// Example usage
profiler.begin('particles');
particleSystem.update();
particleSystem.draw();
profiler.end('particles');
 
const stats = profiler.getRegion('particles');
console.log(`Particles: ${stats?.averageTime.toFixed(2)}ms avg`);

Best Practices

  • • Use descriptive region names
  • • Always pair begin/end calls
  • • Nest regions for hierarchical timing

Common Regions

  • physics — Simulation updates
  • render — Drawing passes
  • postprocess — Effects

Frame Statistics

Get aggregated statistics about frame timing.

TypeScript
interface FrameStats {
  frameCount: number;   // Total frames tracked
  totalTime: number;    // Total time in ms
  minTime: number;      // Fastest frame in ms
  maxTime: number;      // Slowest frame in ms
  averageTime: number;  // Average frame time in ms
  lastTime: number;     // Most recent frame time in ms
}
 
const stats = profiler.getFrameStats();
console.log(`
  Frames: ${stats.frameCount}
  Avg: ${stats.averageTime.toFixed(2)}ms
  Min: ${stats.minTime.toFixed(2)}ms
  Max: ${stats.maxTime.toFixed(2)}ms
`);

Frame Time vs Render Time

Frame time is the interval between frames (includes vsync wait).Render time is just the GPU work duration. Use getAverageFrameTime() for FPS calculations and getAverageRenderTime() to measure GPU load.

Event Listeners

Subscribe to GPU events directly on the context for custom debugging or visualization.

TypeScript
// Subscribe to specific event type
const unsubscribe = ctx.on('draw', (event) => {
  console.log(`Draw: ${event.source}, vertices: ${event.vertexCount}`);
});
 
// Subscribe to all events
const unsubAll = ctx.onAll((event) => {
  console.log(`[${event.type}]`, event);
});
 
// One-time listener
ctx.once('shader_compile', (event) => {
  console.log('Shader compiled:', event.label);
});
 
// Get event history
const drawEvents = ctx.getEventHistory(['draw']);
const allEvents = ctx.getEventHistory();
 
// Unsubscribe when done
unsubscribe();
unsubAll();

Event Types

Available event types and their data structures.

TypeScript
// Draw event - emitted at start and end of each draw call
interface DrawEvent {
  type: "draw";
  phase: "start" | "end";  // Distinguish start vs end
  source: "pass" | "material" | "particles";
  label?: string;
  vertexCount?: number;
  instanceCount?: number;
  topology?: GPUPrimitiveTopology;
  target: "screen" | "texture";
  targetSize: [number, number];
}
 
// Compute event - emitted at start and end of each dispatch
interface ComputeEvent {
  type: "compute";
  phase: "start" | "end";  // Distinguish start vs end
  label?: string;
  workgroups?: [number, number, number];
  workgroupSize?: [number, number, number];
  totalInvocations?: number;
}
 
// Frame event - emitted at frame boundaries
interface FrameEvent {
  type: "frame";
  phase: "start" | "end";
  frameNumber: number;
  deltaTime: number;  // Time since last frame in ms
  time: number;       // Total elapsed time in ms
}
 
// Memory event - buffer/texture allocation
interface MemoryEvent {
  type: "memory";
  resourceType: "buffer" | "texture" | "sampler";
  action: "allocate" | "free" | "resize";
  label?: string;
  size?: number;  // in bytes
}
 
// Other events: shader_compile, target, pipeline, gpu_timing
Event TypeDescription
drawDraw call (pass, material, particles)
computeCompute shader dispatch
frameFrame start/end with timing
shader_compileShader compilation
memoryBuffer/texture allocate/free/resize
targetRender target set/clear
pipelinePipeline creation (with cache hit info)
gpu_timingGPU timing queries (when available)

Full Example

A complete React component demonstrating the profiler with live FPS and region stats.

TypeScript
"use client";
 
import { useEffect, useRef, useState } from "react";
import { gpu, GPUContext, Profiler } from "ralph-gpu";
 
export default function ProfilerDemo() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [fps, setFps] = useState(0);
  const [regions, setRegions] = useState<Map<string, any>>(new Map());
 
  useEffect(() => {
    let ctx: GPUContext | null = null;
    let profiler: Profiler | null = null;
    let animationId: number;
    let disposed = false;
 
    async function init() {
      if (!canvasRef.current || !gpu.isSupported()) return;
 
      ctx = await gpu.init(canvasRef.current, {
        events: { enabled: true, historySize: 100 },
      });
 
      if (disposed) {
        ctx.dispose();
        return;
      }
 
      profiler = new Profiler(ctx, { maxFrameHistory: 120 });
 
      // Create some passes to profile
      const background = ctx.pass(`
        @fragment
        fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
          let uv = pos.xy / globals.resolution;
          return vec4f(uv * 0.3, 0.1, 1.0);
        }
      `);
 
      const effect = ctx.pass(`
        @fragment
        fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
          let uv = pos.xy / globals.resolution;
          let d = length(uv - 0.5);
          let pulse = sin(globals.time * 3.0) * 0.1 + 0.2;
          if (d < pulse) {
            return vec4f(1.0, 1.0, 1.0, 0.5);
          }
          discard;
        }
      `, { blend: 'alpha' });
 
      let lastUpdate = 0;
 
      function frame() {
        if (disposed || !profiler) return;
 
        profiler.tick();
 
        profiler.begin('background');
        background.draw();
        profiler.end('background');
 
        profiler.begin('effect');
        effect.draw();
        profiler.end('effect');
 
        // Update UI every 100ms
        const now = performance.now();
        if (now - lastUpdate > 100) {
          lastUpdate = now;
          setFps(profiler.getFPS());
          setRegions(new Map(profiler.getResults()));
        }
 
        animationId = requestAnimationFrame(frame);
      }
 
      frame();
    }
 
    init();
 
    return () => {
      disposed = true;
      cancelAnimationFrame(animationId);
      profiler?.dispose();
      ctx?.dispose();
    };
  }, []);
 
  return (
    <div>
      <canvas ref={canvasRef} width={800} height={400} />
      <div>
        <p>FPS: {fps.toFixed(1)}</p>
        {Array.from(regions.entries()).map(([name, stats]) => (
          <p key={name}>
            {name}: {stats.averageTime.toFixed(2)}ms
          </p>
        ))}
      </div>
    </div>
  );
}

Next Steps