Debugging
This guide covers Estella’s runtime debugging tools, including error handling, playback control, the logger system, and editor debugging panels.
Error Handling
System Error Handling
When an ECS system throws an exception, the engine prints the error to the console and continues running by default. Register a handler with app.onSystemError() to control the post-error behaviour — return 'continue' to keep executing subsequent systems, or 'pause' to skip all remaining systems for the current frame:
app.onSystemError((error: Error, systemName?: string) => { console.error(`System "${systemName}" failed:`, error.message);
if (error.message.includes('critical')) { return 'pause'; } return 'continue';});After returning 'pause', systems that have not yet run in the current frame are skipped, but the render loop does not stop. All systems resume normal execution at the start of the next frame.
WASM Error Handling
The C++ engine core runs via WebAssembly. Catch errors from the WASM layer with app.onWasmError():
app.onWasmError((error: unknown, context: string) => { console.error(`WASM error in ${context}:`, error);});The context parameter identifies the WASM operation that failed (e.g. rendering, physics), making it easier to locate the source of the problem.
Playback Control
The SDK provides methods to control game execution at runtime, useful for frame-by-frame debugging and slow-motion analysis.
Pause and Resume
app.setPaused(true); // Pause: systems stop, only Last schedule (rendering) runsapp.setPaused(false); // Resume normal execution
app.isPaused(); // Query current pause stateWhile paused, the engine still runs the render loop (Schedule.Last), so the screen stays on the last rendered frame.
Single-Frame Stepping
While paused, call stepFrame() to advance exactly one frame — a full system schedule runs then pauses again:
app.setPaused(true);app.stepFrame(); // Execute one full frame of system schedulingPlay Speed
Adjust time flow with setPlaySpeed(). The valid range is 0.1 to 4.0:
app.setPlaySpeed(0.25); // Quarter speed — good for observing fast animationsapp.setPlaySpeed(2.0); // Double speed
app.getPlaySpeed(); // Get the current speed multiplierPlay speed works by scaling each frame’s delta time. All systems that read Time.delta are affected automatically.
Logger System
The SDK provides a centralized logging system with level filtering and custom handlers.
LogLevel
import { LogLevel, setLogLevel } from 'esengine';
// Debug = 0, Info = 1, Warn = 2, Error = 3setLogLevel(LogLevel.Debug); // Output all levelssetLogLevel(LogLevel.Warn); // Output Warn and Error onlyUsing the Logger
Log functions accept a category tag, a message, and optional additional data:
import { debug, info, warn, error } from 'esengine';
debug('Physics', 'Collision detected', { entityA: 1, entityB: 2 });info('Scene', 'Level loaded');warn('Asset', 'Texture not found, using fallback');error('Render', 'WebGL context lost');The default ConsoleLogHandler formats output to the browser console:
[14:32:05.123] [INFO ] [Scene] Level loadedCustom LogHandler
Implement the LogHandler interface to send logs to a custom destination (remote service, file, UI panel, etc.):
import { getLogger, type LogHandler, type LogEntry } from 'esengine';
const customHandler: LogHandler = { handle(entry: LogEntry) { // entry.timestamp, entry.level, entry.category, entry.message, entry.data myRemoteService.send(entry); }};
const logger = getLogger();logger.addHandler(customHandler);
// Remove a handlerlogger.removeHandler(customHandler);Editor Debugging
Output Panel
The editor’s Output panel captures all runtime console output:
- Timestamps — each log entry includes millisecond-precision timing
- Type filtering — filter by info, warn, or error
- Search — search for keywords within log text
Game View Playback Controls
The editor’s Game View panel provides a graphical playback control toolbar:
- Play / Pause — start or pause the game preview
- Step — advance a single frame while paused
- Speed — adjust playback speed
These controls call the setPaused(), stepFrame(), and setPlaySpeed() APIs described above.
Frame Debugger
The Frame Debugger lets you capture a single frame and inspect every draw call — useful for diagnosing batching issues, unexpected flush reasons, and render order problems.
Editor Panel
In the editor, open the Frame Debugger panel (available as a detachable window). Click Capture to freeze the current frame. The panel displays each draw call with its type, texture, material, triangle count, flush reason, and stencil/scissor state. Click a draw call to replay rendering up to that point and see the intermediate result.
SDK API
You can also capture and analyze frames programmatically:
import { Renderer, FlushReason, RenderType } from 'esengine';
// Trigger capture (takes effect next frame)Renderer.captureNextFrame();
// After the frame renders, retrieve the captured dataconst capture = Renderer.getCapturedData();if (capture) { for (const dc of capture.drawCalls) { console.log( `#${dc.index} ${RenderType[dc.type]} | ` + `${dc.triangleCount} tris | ` + `flush: ${FlushReason[dc.flushReason]}` ); }}
// Replay to a specific draw call for visual inspectionRenderer.replayToDrawCall(3);const snapshot = Renderer.getSnapshotImageData();FlushReason
Why the renderer started a new draw call:
| Reason | Description |
|---|---|
BatchFull | Vertex buffer capacity reached |
TextureSlotsFull | All texture slots occupied |
ScissorChange | Scissor rect changed (UIMask) |
StencilChange | Stencil state changed |
MaterialChange | Different material/shader |
BlendModeChange | Blend mode changed |
StageEnd | Render stage boundary |
TypeChange | Different render type (sprite → text) |
FrameEnd | End of frame flush |
RenderType
| Type | Description |
|---|---|
Sprite | Sprite rendering |
Text | Bitmap text |
Spine | Spine skeleton |
Particle | Particle emitter |
Shape | Shape renderer |
UIElement | UI element |
Mesh | Custom mesh |
WASM Debugging
The engine core runs as WebAssembly. WASM debugging requires a debug build of the engine.
Debug Build
Build the engine in debug mode:
node build-tools/cli.js build -t web -dThis produces an unoptimized WASM binary with DWARF debug info. The binary is significantly larger than a release build.
Chrome WASM Debugging
- Install the C/C++ DevTools Support Chrome extension
- Open Chrome DevTools
- Navigate to the Sources panel
- WASM source files appear under the
file://tree - Set breakpoints in C++ source files and step through code
Performance Profiling
Frame Time
Use the browser Performance panel to record a trace:
- Open DevTools and go to the Performance tab
- Click Record, interact with your game, then stop
- Examine the flame chart for long frames
Look for:
- Long system execution — a single system taking too much time per frame
- Asset loading stalls — large textures or Spine data blocking the main thread
- GC pauses — frequent garbage collection from excessive allocations
Draw Call Count
Use the Draw API to monitor render statistics:
import { Draw } from 'esengine';
console.log('Draw calls:', Draw.getDrawCallCount());console.log('Primitives:', Draw.getPrimitiveCount());ECS Query Performance
If a system with a broad query runs slowly, narrow the query by adding more specific component filters. Queries with fewer matching entities run faster.
const narrowSystem = defineSystem( [Query(Mut(Transform), Velocity, Player)], (query) => { for (const [entity, transform, velocity] of query) { transform.position.x += velocity.linear.x; } });Common Errors
”WASM module failed to compile”
Cause: The WASM binary is corrupted or incompatible with the current browser.
Fix:
- Rebuild the WASM binary:
node build-tools/cli.js build -t web -c - Verify the Emscripten version matches what the project expects
- Check that the browser supports WebAssembly
”Entity not found” / Invalid entity access
Cause: Accessing an entity that has been despawned, or using a stale entity ID.
Fix:
- Check entity existence before access:
if (world.isAlive(entity)) { ... } - Avoid storing entity IDs across frames without validation
- Remember that
Commandsare deferred — entities are not despawned until the end of the schedule stage
”Component not registered”
Cause: Using a component type that has not been defined or imported.
Fix:
- Ensure
defineComponent()ordefineBuiltin()is called before using the component - Check that the component module is imported in your entry file
- For builtin components, verify the SDK version matches the WASM binary version
”WebGL context lost”
Cause: The GPU driver reset the WebGL context, usually due to resource exhaustion.
Fix:
- Reduce texture memory usage (smaller atlases, compressed textures)
- Reduce the number of active render targets
- On mobile devices, minimize background GPU usage
Blank or black screen in preview
Cause: Multiple possible causes.
Checklist:
- Check DevTools console for errors
- Verify the scene file exists and is valid JSON
- Ensure at least one camera entity exists in the scene
- Check that textures referenced by sprites are loaded (no 404s in Network tab)
- Verify the WASM binary loaded successfully
Build output does not match editor preview
Cause: Build-time asset processing (atlas packing, material compilation) can surface issues not visible in preview.
Fix:
- Compare the DevTools console output between preview and build
- Verify all assets are included (check
asset-manifest.json) - For WeChat builds, check that custom file extensions are in
packOptions.include
Next Steps
- Custom Draw — Debug visualization with the Draw API
- Building & Exporting — Build pipeline and configuration
- Scenes — Scene loading and management