Profiling
Learn how to profile your game using STEM's built-in profiler, Chrome DevTools, and performance metrics to identify and fix bottlenecks.
Profiling is essential for understanding where your game spends time and identifying performance bottlenecks. STEM provides multiple profiling tools for different use cases.
FrameProfiler#
The FrameProfiler tracks detailed frame-by-frame performance metrics including phase times, system execution times, and hot path allocations.
Basic Usage#
import { frameProfiler } from '@stem/core/perf'; // The profiler runs automatically// Get recent framesconst frames = frameProfiler.getRecentFrames(60); // Last 60 frames // Analyze framesframes.forEach((frame, i) => { console.log(`Frame ${frame.frameNumber}:`); console.log(' Total time:', frame.totalTime.toFixed(2), 'ms'); console.log(' Timestamp:', new Date(frame.timestamp).toISOString()); // Phase breakdown Object.values(frame.phases).forEach(phase => { console.log(` ${phase.name}: ${phase.time.toFixed(2)}ms`); }); // Hot paths console.log(' Render:', frame.hotPaths.render.toFixed(2), 'ms'); console.log(' Physics:', frame.hotPaths.physics.toFixed(2), 'ms'); console.log(' Scripts:', frame.hotPaths.script.toFixed(2), 'ms');});Frame Statistics#
import { frameProfiler } from '@stem/core/perf'; // Get aggregated statisticsconst stats = frameProfiler.getStatistics(); console.log('=== Frame Statistics ===');console.log('Average frame time:', stats.averageFrameTime.toFixed(2), 'ms');console.log('Min frame time:', stats.minFrameTime.toFixed(2), 'ms');console.log('Max frame time:', stats.maxFrameTime.toFixed(2), 'ms');console.log('95th percentile:', stats.p95FrameTime.toFixed(2), 'ms');console.log('99th percentile:', stats.p99FrameTime.toFixed(2), 'ms'); console.log('\n=== Phase Times ===');Object.entries(stats.averagePhaseTimes).forEach(([phase, time]) => { console.log(`${phase}: ${time.toFixed(2)}ms`);}); console.log('\n=== Slowest Systems ===');stats.slowestSystems.forEach((system, i) => { console.log(`${i + 1}. ${system.name}`); console.log(` Avg: ${system.avgTime.toFixed(2)}ms`); console.log(` Max: ${system.maxTime.toFixed(2)}ms`);});Profiler Phases#
| Phase | Description | Systems |
|---|---|---|
| Input | Controller and keyboard input | InputSystem, ControllerSystem |
| Network | Multiplayer synchronization | NetworkSystem |
| Logic | Game logic and AI | BehaviorSystem, AISystem |
| Physics | Physics simulation | PhysicsSystem, CollisionSystem |
| Animation | Skeletal and sprite animations | AnimationSystem |
| Audio | Sound playback and spatialization | AudioSystem |
| Render | Rendering and draw calls | RenderSystem, InstancedRenderSystem |
| PostRender | Post-processing effects | PostProcessSystem |
Performance Metrics#
STEM tracks comprehensive performance metrics including frame rate, memory usage, draw calls, and hot path allocations.
Global Stats#
import { globalStats } from '@stem/core/ecs/systems/ProfilerSystem'; // Access global performance metricsconsole.log('FPS:', globalStats.fps);console.log('Frame time:', globalStats.frameTime, 'ms');console.log('Draw calls:', globalStats.drawCalls);console.log('Triangles:', globalStats.triangles); // Memory statsconsole.log('JS Heap used:', (globalStats.memory.usedJSHeapSize / 1024 / 1024).toFixed(2), 'MB');console.log('JS Heap total:', (globalStats.memory.totalJSHeapSize / 1024 / 1024).toFixed(2), 'MB');console.log('JS Heap limit:', (globalStats.memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2), 'MB'); // Hot path metricsconsole.log('\n=== Hot Path Metrics ===');const hotPaths = globalStats.hotPaths;console.log('Render:', hotPaths.render.avgAllocKB.toFixed(2), 'KB/frame');console.log('Physics:', hotPaths.physics.avgAllocKB.toFixed(2), 'KB/frame');console.log('Scripts:', hotPaths.script.avgAllocKB.toFixed(2), 'KB/frame');console.log('Behavior:', hotPaths.behavior.avgAllocKB.toFixed(2), 'KB/frame');console.log('Animation:', hotPaths.animation.avgAllocKB.toFixed(2), 'KB/frame');Hot Path Tracking#
Hot paths are code executed every frame. STEM tracks allocations and execution time in hot paths to help you achieve zero-GC performance.
import { beginHotPathSample, endHotPathSample, renderHotPathBuffer, summarizeHotPath, setHotPathTracking} from '@stem/core/perf/HotPathMetrics'; // Enable hot path tracking (adds ~0.1ms overhead)setHotPathTracking(true); export const MyRenderSystem = (world: IWorld) => { // Begin sampling const token = beginHotPathSample(); try { // Your render code here // ... } finally { // End sampling (tracks time and allocations) endHotPathSample(renderHotPathBuffer, token); } return world;}; // Analyze hot path performanceconst summary = summarizeHotPath(renderHotPathBuffer);console.log('Avg time:', summary.avgMs.toFixed(2), 'ms');console.log('P99 time:', summary.p99Ms.toFixed(2), 'ms');console.log('Avg alloc:', summary.avgAllocBytes.toFixed(2), 'bytes');console.log('P99 alloc:', summary.p99AllocBytes.toFixed(2), 'bytes');console.log('GC events/sec:', summary.gcEvents);Memory Profiling#
Memory profiling helps identify leaks, excessive allocations, and inefficient memory usage patterns.
Memory Tracker#
import { memoryTracker } from '@stem/core/perf'; // Enable memory trackingmemoryTracker.setEnabled(true); // Get current memory snapshotconst current = memoryTracker.getCurrent();if (current) { console.log('Used heap:', (current.usedJSHeapSize / 1024 / 1024).toFixed(2), 'MB'); console.log('Delta since last:', (current.delta / 1024).toFixed(2), 'KB');} // Get peak memory usageconst peak = memoryTracker.getPeak();console.log('Peak heap:', (peak.usedJSHeapSize / 1024 / 1024).toFixed(2), 'MB'); // Get memory statisticsconst stats = memoryTracker.getStats();console.log('Average usage:', (stats.average.usedJSHeapSize / 1024 / 1024).toFixed(2), 'MB');console.log('Trend:', stats.trend); // 'increasing', 'decreasing', or 'stable'console.log('GC events:', stats.gcEvents);console.log('Samples:', stats.samples); // Detect memory leaksconst leak = memoryTracker.detectLeak();if (leak?.isLeaking) { console.warn('MEMORY LEAK DETECTED!'); console.log('Leak rate:', (leak.leakRate / 1024).toFixed(2), 'KB/sec'); console.log('Confidence:', leak.confidence); // 'low', 'medium', 'high' console.log('Growth:', (leak.growthBytes / 1024).toFixed(2), 'KB'); console.log('Time window:', leak.timeWindow, 'seconds');}Memory Snapshots#
import { memoryTracker } from '@stem/core/perf'; // Take manual snapshotconst snapshot = memoryTracker.sample(); // Get memory historyconst history = memoryTracker.getHistory(60); // Last 60 samples // Analyze memory trendsconst recentAvg = history.slice(-10) .reduce((sum, s) => sum + s.usedJSHeapSize, 0) / 10; const oldAvg = history.slice(0, 10) .reduce((sum, s) => sum + s.usedJSHeapSize, 0) / 10; const growth = recentAvg - oldAvg;console.log('Memory growth:', (growth / 1024 / 1024).toFixed(2), 'MB'); // Check memory usage levelif (memoryTracker.isCriticalUsage()) { console.error('CRITICAL: Memory usage > 95%!');} else if (memoryTracker.isHighUsage()) { console.warn('Warning: Memory usage > 80%');}Chrome DevTools Integration#
Chrome DevTools provides advanced profiling capabilities including CPU profiling, memory heap snapshots, and performance timelines.
CPU Profiling#
- Open DevTools — Press F12 or Cmd+Option+I (Mac)
- Go to Performance tab — Click the Performance tab
- Start recording — Click the record button and interact with your game
- Stop recording — Click stop after 5-10 seconds
- Analyze results — Look for long tasks (yellow blocks) and expensive functions
CPU Profiling Tips
• Look for functions taking >5ms
• Check for long tasks blocking the main thread
• Use the flame chart to identify call stacks
Heap Snapshots#
- Open Memory tab — Navigate to the Memory tab in DevTools
- Take heap snapshot — Select 'Heap snapshot' and click 'Take snapshot'
- Compare snapshots — Take multiple snapshots and compare to find leaks
- Analyze retaining paths — Click on large objects to see what's keeping them alive
// Add labels to heap snapshots for easier debuggingexport class MyGameClass { constructor(public name: string) { // Set displayName for better heap snapshot visibility Object.defineProperty(this, 'displayName', { value: `MyGameClass[${name}]`, enumerable: false, }); }} // Mark objects for heap snapshot identificationconst player = new MyGameClass('Player');const enemy = new MyGameClass('Enemy'); // These will show up as "MyGameClass[Player]" and// "MyGameClass[Enemy]" in heap snapshotsAllocation Timeline#
- Select Allocation timeline — Choose 'Allocation timeline' in Memory tab
- Start recording — Click 'Start' and run your game
- Stop after 30 seconds — Let it record gameplay
- Analyze allocations — Look for spikes in allocation rate (should be flat)
GPU Profiling#
GPU profiling helps identify rendering bottlenecks like shader complexity, overdraw, and fill rate issues.
Three.js Stats#
import { getRenderer } from '@stem/core/rendering'; const renderer = getRenderer();const info = renderer.info; console.log('=== Render Stats ===');console.log('Draw calls:', info.render.calls);console.log('Triangles:', info.render.triangles);console.log('Points:', info.render.points);console.log('Lines:', info.render.lines); console.log('\n=== Memory Stats ===');console.log('Geometries:', info.memory.geometries);console.log('Textures:', info.memory.textures); console.log('\n=== Programs ===');console.log('Shader programs:', info.programs?.length || 0); // Reset statsrenderer.info.reset();Spector.js GPU Capture#
Spector.js is a WebGL capture tool that records all GPU calls and textures in a single frame.
# Install Spector.js Chrome extension# https://chrome.google.com/webstore/detail/spectorjs # Or use programmaticallynpm install spectorjsimport { Spector } from 'spectorjs'; // Create spector instanceconst spector = new Spector();spector.displayUI(); // Capture a framespector.captureNextFrame(document.querySelector('canvas')); // Spector will show:// - All WebGL calls// - Shader source code// - Texture previews// - Draw call breakdown// - State changesIdentifying Bottlenecks#
Use profiling data to identify whether your game is CPU-bound or GPU-bound.
CPU vs GPU Bound#
import { performanceMonitor, frameProfiler } from '@stem/core/perf';import { getRenderer } from '@stem/core/rendering'; function analyzeBottleneck() { const stats = frameProfiler.getStatistics(); const renderInfo = getRenderer().info; // Check if CPU bound const cpuTime = (stats.averagePhaseTimes.Input || 0) + (stats.averagePhaseTimes.Logic || 0) + (stats.averagePhaseTimes.Physics || 0) + (stats.averagePhaseTimes.Animation || 0); const gpuTime = (stats.averagePhaseTimes.Render || 0) + (stats.averagePhaseTimes.PostRender || 0); console.log('CPU time:', cpuTime.toFixed(2), 'ms'); console.log('GPU time:', gpuTime.toFixed(2), 'ms'); if (cpuTime > gpuTime * 1.5) { console.log('BOTTLENECK: CPU-bound'); console.log('Solutions:'); console.log('- Optimize scripts and systems'); console.log('- Reduce physics complexity'); console.log('- Spread work across multiple frames'); console.log('- Use web workers for heavy computation'); } else if (gpuTime > cpuTime * 1.5) { console.log('BOTTLENECK: GPU-bound'); console.log('Solutions:'); console.log('- Reduce draw calls:', renderInfo.render.calls); console.log('- Reduce triangle count:', renderInfo.render.triangles); console.log('- Use LOD system'); console.log('- Optimize shaders'); console.log('- Reduce post-processing effects'); } else { console.log('BALANCED: Both CPU and GPU are utilized evenly'); }} analyzeBottleneck();Bottleneck Checklist#
| Check | CPU Bound | GPU Bound |
|---|---|---|
| Frame time | > 16.67ms with low draw calls | > 16.67ms with high draw calls |
| Draw calls | < 200 | > 500 |
| Triangles | < 1M | > 3M |
| System time | > 10ms total | < 5ms total |
| Allocations | > 100KB/frame | < 50KB/frame |
| Resolution impact | Minimal | Significant |
Resolution Test
Performance Reports#
Generate comprehensive performance reports for analysis and debugging.
import { performanceMonitor, frameProfiler, memoryTracker} from '@stem/core/perf';import { getRenderer } from '@stem/core/rendering'; function generatePerformanceReport() { const summary = performanceMonitor.getSummary(); const stats = frameProfiler.getStatistics(); const memStats = memoryTracker.getStats(); const renderInfo = getRenderer().info; const report = { timestamp: new Date().toISOString(), // Frame metrics frame: { fps: summary.fps, frameTime: summary.frameTime, p95: stats.p95FrameTime, p99: stats.p99FrameTime, }, // Phase breakdown phases: stats.averagePhaseTimes, // System performance systems: summary.systems.slice(0, 10).map(s => ({ name: s.name, time: s.time, })), // Memory memory: { used: summary.memory.used, total: summary.memory.total, percent: summary.memory.percent, trend: memStats?.trend || 'unknown', gcEvents: summary.gcEvents, }, // Rendering rendering: { drawCalls: renderInfo.render.calls, triangles: renderInfo.render.triangles, geometries: renderInfo.memory.geometries, textures: renderInfo.memory.textures, }, // Allocations allocations: summary.allocations, // Warnings warnings: performanceMonitor.getWarnings().map(w => ({ type: w.type, severity: w.severity, message: w.message, recommendation: w.recommendation, })), }; console.log(JSON.stringify(report, null, 2)); return report;} // Generate report every 10 secondssetInterval(generatePerformanceReport, 10000);