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 frames
const frames = frameProfiler.getRecentFrames(60); // Last 60 frames
// Analyze frames
frames.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 statistics
const 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#

PhaseDescriptionSystems
InputController and keyboard inputInputSystem, ControllerSystem
NetworkMultiplayer synchronizationNetworkSystem
LogicGame logic and AIBehaviorSystem, AISystem
PhysicsPhysics simulationPhysicsSystem, CollisionSystem
AnimationSkeletal and sprite animationsAnimationSystem
AudioSound playback and spatializationAudioSystem
RenderRendering and draw callsRenderSystem, InstancedRenderSystem
PostRenderPost-processing effectsPostProcessSystem

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 metrics
console.log('FPS:', globalStats.fps);
console.log('Frame time:', globalStats.frameTime, 'ms');
console.log('Draw calls:', globalStats.drawCalls);
console.log('Triangles:', globalStats.triangles);
// Memory stats
console.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 metrics
console.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 performance
const 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 tracking
memoryTracker.setEnabled(true);
// Get current memory snapshot
const 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 usage
const peak = memoryTracker.getPeak();
console.log('Peak heap:', (peak.usedJSHeapSize / 1024 / 1024).toFixed(2), 'MB');
// Get memory statistics
const 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 leaks
const 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 snapshot
const snapshot = memoryTracker.sample();
// Get memory history
const history = memoryTracker.getHistory(60); // Last 60 samples
// Analyze memory trends
const 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 level
if (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#

  1. Open DevTools — Press F12 or Cmd+Option+I (Mac)
  2. Go to Performance tab — Click the Performance tab
  3. Start recording — Click the record button and interact with your game
  4. Stop recording — Click stop after 5-10 seconds
  5. Analyze results — Look for long tasks (yellow blocks) and expensive functions

CPU Profiling Tips

• Record for 5-10 seconds of typical gameplay
• Look for functions taking >5ms
• Check for long tasks blocking the main thread
• Use the flame chart to identify call stacks

Heap Snapshots#

  1. Open Memory tab — Navigate to the Memory tab in DevTools
  2. Take heap snapshot — Select 'Heap snapshot' and click 'Take snapshot'
  3. Compare snapshots — Take multiple snapshots and compare to find leaks
  4. Analyze retaining paths — Click on large objects to see what's keeping them alive
// Add labels to heap snapshots for easier debugging
export 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 identification
const player = new MyGameClass('Player');
const enemy = new MyGameClass('Enemy');
// These will show up as "MyGameClass[Player]" and
// "MyGameClass[Enemy]" in heap snapshots

Allocation Timeline#

  1. Select Allocation timeline — Choose 'Allocation timeline' in Memory tab
  2. Start recording — Click 'Start' and run your game
  3. Stop after 30 seconds — Let it record gameplay
  4. 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 stats
renderer.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 programmatically
npm install spectorjs
import { Spector } from 'spectorjs';
// Create spector instance
const spector = new Spector();
spector.displayUI();
// Capture a frame
spector.captureNextFrame(document.querySelector('canvas'));
// Spector will show:
// - All WebGL calls
// - Shader source code
// - Texture previews
// - Draw call breakdown
// - State changes

Identifying 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#

CheckCPU BoundGPU 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 impactMinimalSignificant

Resolution Test

To test if you're GPU-bound, reduce the render resolution by 50%. If FPS increases significantly, you're GPU-bound. If FPS stays the same, you're CPU-bound.

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 seconds
setInterval(generatePerformanceReport, 10000);
Documentation | Web Engine