Skip to main content

Telemetry And Debugging

Hs2d performance work starts with measured phases and bounded counters. Use telemetry to identify the slow phase, use snapshots to find the active layer or pool, then optimize that named source of work.

Source References

SourceWhy it matters
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dTelemetry.tsOptional phase timer wrapper, named accumulators, one-decimal formatting, and reset behavior.
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dLevelScene.tsBuilt-in telemetry around sceneSprite and particleUpdate, stats-window reset, and HUD text renderer hookup.
../games/hosanna-ui/src/hosanna-game/runtime/GameRuntime.tsRuntime frame stats for update, render, present, FPS, and collision-debug toggling.
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorld.tsWorld snapshots with camera, layers, tile layer stats, sprite pool stats, and particle layer stats.
../games/hosanna-ui/src/hosanna-game/hosanna2d/types.tsSnapshot and stats shapes used by debug tooling.
../games/hosanna-ui/src/hosanna-game/collision/GameCollisionManager.tsCollision debug overlay, global toggle, registered manager pruning, and collision stats.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.tsReference phase timing for projectile update, projectile hit checks, sprite sync, particle update, render, and stats-window HUD.

Telemetry Pieces

PieceUse
Hs2dTelemetry.markPhase()Reset or mark the platform phase timer before a measured block.
Hs2dTelemetry.readPhaseMs()Read current timer elapsed milliseconds. Returns 0 when no timer exists.
Hs2dTelemetry.accumulatePhase(name)Read elapsed phase time and add it to a named accumulator.
Hs2dTelemetry.accumulate(name, ms)Add a known value to a named accumulator.
Hs2dTelemetry.getAccumulatedMs(name)Read a named total for the current stats window.
Hs2dTelemetry.resetAccumulators()Clear named totals, usually once per stats window.
Hs2dTelemetry.formatPerfMs(value)Floor to one decimal place for HUD/status text.

Hs2dTelemetry accepts an optional platform timer. Construct and call it behind the compile-time __TELEMETRY__ flag. On Roku-style paths, samples pass CreateObject('roTimespan') only when that flag is enabled. When the flag is disabled, level-scene timing and stats-window work is compiled out; when an enabled build has no timer, phase reads are zero.

Built-In Level Phases

Hs2dLevelScene.update uses this frame shape once the world is ready:

handleInputs
onUpdate
mark phase
updateSceneSprites
accumulate sceneSprite
mark phase
updateParticles
accumulate particleUpdate
if __TELEMETRY__: updateStats

This gives every level scene two common phase names:

PhaseMeaning
sceneSpriteLogical state synchronized into pooled sprites, text, visibility, and layer state.
particleUpdateParticle emitter updates and particle sprite movement.

Scenes can add more phases around game-specific hot blocks.

Game-Specific Phase Timing

Instrument phases at the boundary you can act on:

protected override onUpdate(deltaMs: number, inputs: GameInput[]): void {
this.updatePlayer(deltaMs, inputs);
this.tickSceneCamera(deltaMs);

this.markPhaseTimer();
this.updateProjectiles(deltaMs);
if (__TELEMETRY__) this.telemetry.accumulatePhase('projectileUpdate');

this.markPhaseTimer();
this.checkProjectileHits();
if (__TELEMETRY__) this.telemetry.accumulatePhase('projectileHit');

this.floatingScoreTextPool?.update(deltaMs);
}

protected override onRender(screen: GameScreen, world: Hs2dWorld): void {
this.markPhaseTimer();
world.render(screen);
if (__TELEMETRY__) {
this.telemetry.accumulatePhase('render');
this.frameCount++;
}
}

Use phase names that describe the code you will optimize. slowStuff is not actionable. projectileHit, tileWindow, enemyVisibility, sceneSprite, and render are actionable.

Stats Windows

Do not rebuild long debug strings every frame. Use onStatsWindow:

protected override onStatsWindow(): void {
this.statusText =
`fps: ${this.fps}`
+ ` projectiles: ${this.activeProjectiles}`
+ ` particles: ${this.activeParticles}`
+ ` projHitMs: ${this.formatPerfMs(this.telemetry.getAccumulatedMs('projectileHit'))}`
+ ` spriteMs: ${this.formatPerfMs(this.telemetry.getAccumulatedMs('sceneSprite'))}`
+ ` particleMs: ${this.formatPerfMs(this.telemetry.getAccumulatedMs('particleUpdate'))}`
+ ` renderMs: ${this.formatPerfMs(this.telemetry.getAccumulatedMs('render'))}`;

this.projectileCollisionChecksThisSecond = 0;
this.projectileEnemyHitsThisSecond = 0;
this.particles?.resetCounters();
}

In telemetry builds, Hs2dLevelScene.updateStats computes FPS once per second and resets telemetry accumulators after onStatsWindow. Non-telemetry builds do not call this hook, so it must not own required gameplay work.

Runtime Stats

GameRuntime tracks:

GroupFieldsMeaning
TargettargetFps, targetFrameMsConfigured 30/60 FPS target and its frame budget.
Current windowframeCountAccepted frames accumulated in the current unpublished measurement window. It resets after each publish.
Published windowfps, windowFrameCount, windowElapsedMsLast complete approximately one-second window.
Latest accepted framelastFrameMs, lastUpdateMs, lastRenderMs, lastPresentMsInput delta and measured update, render, and present/swap phases.
Frame-delta distributionframeDeltaP50Ms, frameDeltaP95Ms, frameDeltaP99Ms, frameDeltaMaxMsPercentiles and maximum from the published window.
Budget pressureoverBudgetFrameCount, missedFrameBudgetCount, slowFrameCount, consecutiveSlowFrameCount, maxConsecutiveSlowFramesFrames over tolerance, estimated missed budgets, and slow-frame streaks.
BunchingshortFrameCount, longFrameCount, bunchedFrameCount, shortLongFrameCountShort/long classifications and adjacent timing patterns that expose uneven delivery.
Phase aggregatesaverageUpdateMs, maxUpdateMs, averageRenderMs, maxRenderMs, averagePresentMs, maxPresentMsAverage and maximum phase costs from the published window.

The runtime populates timing windows only in __TELEMETRY__ builds. Outside those builds, target fields remain useful but timing values stay at their initialized/reset values. Use runtime stats when the whole controller is slow or present cost is suspicious. Use Hs2dTelemetry when the scene needs named sub-phase attribution.

World Snapshots

Hs2dWorld.getSnapshot() returns:

FieldMeaning
cameraCamera position, viewport, and scale.
layerCountNumber of renderer layers.
tileLayersPool size, visible tiles, recycled tiles, region changes, drawable changes, moved tiles.
spritePoolsPool size and active sprites for registered pools.
spritesSprite pool stats with layer IDs and pool type.
particleLayersActive, emitted, and moved particle counts.
layersLayer ID, type, order, HUD flag, and surface details where available.

Expose it from scenes with getHs2dDebugSnapshot:

override getHs2dDebugSnapshot(): unknown {
const sceneSnapshot = this.world?.getSnapshot();
return {
source: 'MyLevelScene',
scene: {
camera: sceneSnapshot?.camera,
tileLayers: sceneSnapshot?.tileLayers ?? [],
spritePools: sceneSnapshot?.spritePools ?? [],
particleLayers: sceneSnapshot?.particleLayers ?? [],
},
};
}

Snapshots answer "how much is active?" Telemetry answers "where did the time go?" Use both.

Collision Debugging

Collision debug overlays are managed by GameCollisionManager:

GameCollisionManager.setDebugRenderingEnabled(true);
GameCollisionManager.toggleDebugRendering();

In __DEV__ builds, GameRuntime toggles the overlay with the runtime options input and renders colliders after controller render unless the active controller reports isCollisionDebugRenderingHandled(). The shortcut and runtime render pass are compiled out of non-development builds. Manual manager toggle/render calls still exist independently.

There is a current Hs2d integration limitation: Hs2dSprite.setCollision(...) does not forward debug shape/pretranslation data into its manager registration. The global overlay can render direct GameCollisionManager.registerSprite(...) registrations that provide debugShape, but it cannot automatically visualize every ordinary Hs2d sprite collider. Use the collider diagnostic for shape validation or fix that forwarding before treating the global overlay as complete.

Use the overlay to verify:

  • collision shape size
  • region pretranslation and anchor alignment
  • active/inactive collider state
  • stale offscreen colliders that still have flags

Use collision counters to verify:

  • how many checks ran in a stats window
  • how many hits were produced
  • whether checkFirst would be enough instead of check

Debug HUD Text

HUD debug text should use cached text and stats-window snapshots:

private hudStatusText = 'FPS 0  T 0  E 0  P 0';

override drawHud(screen: GameScreen): void {
this.drawCachedText(screen, 'status', this.hudStatusText, 36, 58, 0x55ff99ff, this.hudFont);
}

protected override onStatsWindow(): void {
const next = `FPS ${this.fps} T ${this.visibleTiles} E ${this.visibleEnemies} P ${this.activeProjectiles}`;
if (next !== this.hudStatusText) {
this.hudStatusText = next;
}
}

Avoid building interpolated status strings inside drawHud every frame unless the text is tiny and diagnostic-only.

Optimization Workflow

  1. Measure the frame. Check runtime FPS, update, render, and present timings.
  2. Measure scene phases. Add Hs2dTelemetry around the suspected hot blocks.
  3. Correlate counters. Compare phase time with active sprites, visible tiles, active particles, collision checks, and text cache redraws.
  4. Disable one feature behind a flag. Confirm the phase changes before committing an optimization.
  5. Optimize the named phase. Reduce work in that phase, not unrelated code.
  6. Keep the counter. Leave useful stats-window counters in diagnostics while the feature is still being tuned.

Examples:

EvidenceLikely action
High sceneSprite, high active sprite countReduce active pools, cull earlier, amortize visibility refresh, avoid region churn.
High particleUpdate, high moved particlesLower emitter rates, reduce burst sizes, shorten lifetimes, lower pool size only if visuals still hold.
High projectileHit, high collision checksUse checkFirst, add broad-phase filters, skip offscreen projectiles, reduce projectile pool or fire rate.
High render, low update phasesInspect layer count, tile mode, cached surfaces, direct draw loops, and HUD draws.
High text cache redrawsStabilize cache keys, call prewarmBitmapText(target, items) for common labels, and update HUD strings once per stats window.
High present timeSuspect platform or surface composition cost before rewriting gameplay logic.

Do And Don't

DoDon't
Name phase accumulators after code blocks.Add generic timers that do not point to an owner.
Use stats windows for HUD/debug text.Rebuild long debug strings every frame.
Pair time with counts.Optimize from milliseconds without knowing active sprites/tiles/particles/checks.
Use feature flags to isolate expensive visuals.Disable features permanently without measuring the phase impact.
Keep collision overlays out of gameplay logic.Use debug rendering as a collision system dependency.
Read world snapshots before changing pool sizes.Guess that a pool is too large because FPS dropped.
Report caveats when telemetry is unavailable.Treat zero timings from an absent timer as proof of no cost.
Talk to us