Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
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.
The FrameProfiler tracks detailed frame-by-frame performance metrics including phase times, system execution times, and hot path allocations.
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 breakdownObject.values(frame.phases).forEach(phase => {console.log(` ${phase.name}: ${phase.time.toFixed(2)}ms`);});// Hot pathsconsole.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');});
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`);});
| 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 |
STEM tracks comprehensive performance metrics including frame rate, memory usage, draw calls, and hot path allocations.
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 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 samplingconst 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 helps identify leaks, excessive allocations, and inefficient memory usage patterns.
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');}
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 provides advanced profiling capabilities including CPU profiling, memory heap snapshots, and performance timelines.
CPU Profiling Tips
// Add labels to heap snapshots for easier debuggingexport class MyGameClass {constructor(public name: string) {// Set displayName for better heap snapshot visibilityObject.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 snapshots
GPU profiling helps identify rendering bottlenecks like shader complexity, overdraw, and fill rate issues.
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 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 spectorjs
import { 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 changes
Use profiling data to identify whether your game is CPU-bound or 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 boundconst 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();
| 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
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 metricsframe: {fps: summary.fps,frameTime: summary.frameTime,p95: stats.p95FrameTime,p99: stats.p99FrameTime,},// Phase breakdownphases: stats.averagePhaseTimes,// System performancesystems: summary.systems.slice(0, 10).map(s => ({name: s.name,time: s.time,})),// Memorymemory: {used: summary.memory.used,total: summary.memory.total,percent: summary.memory.percent,trend: memStats?.trend || 'unknown',gcEvents: summary.gcEvents,},// Renderingrendering: {drawCalls: renderInfo.render.calls,triangles: renderInfo.render.triangles,geometries: renderInfo.memory.geometries,textures: renderInfo.memory.textures,},// Allocationsallocations: summary.allocations,// Warningswarnings: 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);