Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/components/game/PerformanceDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PerformanceSnapshot,
SystemTiming,
} from '@/engine/core/PerformanceMonitor';
import { getWorkerBridge } from '@/engine/workers/WorkerBridge';

// Mini sparkline graph component
const Sparkline = memo(function Sparkline({
Expand Down Expand Up @@ -142,6 +143,13 @@ export const PerformanceDashboard = memo(function PerformanceDashboard({
}, []);

useEffect(() => {
// Enable worker performance collection when dashboard mounts
const bridge = getWorkerBridge();
bridge?.setPerformanceCollection(true);

// Start main thread performance monitor
PerformanceMonitor.start();

// Subscribe to performance updates
const unsubscribe = PerformanceMonitor.subscribe(handleSnapshot);

Expand All @@ -151,7 +159,12 @@ export const PerformanceDashboard = memo(function PerformanceDashboard({
setFpsHistory(PerformanceMonitor.getFPSHistory().slice(-60));
setTickHistory(PerformanceMonitor.getTickTimeHistory().slice(-60));

return unsubscribe;
return () => {
// Disable worker performance collection when dashboard unmounts
bridge?.setPerformanceCollection(false);
PerformanceMonitor.stop();
unsubscribe();
};
}, [handleSnapshot]);

if (!snapshot) {
Expand Down
38 changes: 38 additions & 0 deletions src/engine/core/PerformanceMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,44 @@ class PerformanceMonitorClass {
this.networkMetrics = { ...this.networkMetrics, ...metrics };
}

/**
* Apply performance metrics received from the game worker.
* This bridges the worker's performance data to the main thread's PerformanceMonitor.
* Called by WorkerBridge when receiving 'performanceMetrics' messages.
*/
public applyWorkerMetrics(
tickTime: number,
systemTimings: Array<[string, number]>,
entityCounts: [number, number, number, number]
): void {
// Record tick time
this.lastTickTime = tickTime;
this.tickTimeHistory.push(tickTime);

// Apply system timings - clear first, then record each
this.currentSystemTimings.clear();
for (const [name, duration] of systemTimings) {
this.currentSystemTimings.set(name, duration);

// Update history ring buffer
if (!this.systemTimingHistory.has(name)) {
this.systemTimingHistory.set(name, new RingBuffer(SYSTEM_TIMING_HISTORY_SIZE));
}
this.systemTimingHistory.get(name)!.push(duration);
}

// Apply entity counts
const [units, buildings, resources, projectiles] = entityCounts;
this.entityCounts = {
total: units + buildings + resources + projectiles,
units,
buildings,
projectiles,
resources,
effects: 0,
};
}

/**
* Update render metrics (called by WebGPUGameCanvas each frame)
* @param drawCalls - Draw calls for this frame (reset after each frame)
Expand Down
83 changes: 83 additions & 0 deletions src/engine/workers/GameWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { GameCore, GameConfig } from '../core/GameCore';
import { PerformanceMonitor } from '../core/PerformanceMonitor';
import { debugInitialization, debugPerformance } from '@/utils/debugLogger';

// Components
Expand Down Expand Up @@ -54,6 +55,7 @@ import type {
ResourceRenderState,
ProjectileRenderState,
SpawnMapData,
WorkerPerformanceMetrics,
} from './types';
import type { GameState, TerrainCell } from '../core/GameCore';
import type { GameCommand } from '../core/GameCommand';
Expand Down Expand Up @@ -106,6 +108,12 @@ export class WorkerGame extends GameCore {
private hasLoggedFirstRenderState = false;
private renderStatesSent = 0;

// Performance collection (zero-cost when disabled)
private performanceCollectionEnabled = false;
private perfMetricsInterval: ReturnType<typeof setInterval> | null = null;
private lastTickDuration = 0;
private lastSystemTimings: Array<[string, number]> = [];

constructor(config: GameConfig) {
super(config);

Expand Down Expand Up @@ -478,9 +486,20 @@ export class WorkerGame extends GameCore {
// Process queued commands for this tick
this.processQueuedCommands();

// Track tick time only when performance collection is enabled (zero-cost when disabled)
let tickStart = 0;
if (this.performanceCollectionEnabled) {
tickStart = performance.now();
}

// Update all systems
this.world.update(deltaTime);

// Record tick duration for performance metrics
if (this.performanceCollectionEnabled) {
this.lastTickDuration = performance.now() - tickStart;
}

// Update player resources
this.updatePlayerResources();
}
Expand Down Expand Up @@ -803,6 +822,65 @@ export class WorkerGame extends GameCore {
}
}

// ============================================================================
// PERFORMANCE COLLECTION
// ============================================================================

/**
* Enable or disable performance metrics collection.
* When enabled, starts a 10Hz interval to send metrics to main thread.
* When disabled, all timing overhead is eliminated.
*/
public setPerformanceCollection(enabled: boolean): void {
if (this.performanceCollectionEnabled === enabled) return;

this.performanceCollectionEnabled = enabled;
PerformanceMonitor.setCollecting(enabled);

if (enabled) {
// Start 10Hz metrics reporting (100ms interval)
this.perfMetricsInterval = setInterval(() => this.sendPerformanceMetrics(), 100);
} else {
// Stop metrics reporting
if (this.perfMetricsInterval !== null) {
clearInterval(this.perfMetricsInterval);
this.perfMetricsInterval = null;
}
// Clear cached data
this.lastTickDuration = 0;
this.lastSystemTimings = [];
}
}

/**
* Collect and send performance metrics to main thread.
* Called at 10Hz when collection is enabled.
*/
private sendPerformanceMetrics(): void {
if (!this.performanceCollectionEnabled) return;

// Get system timings from PerformanceMonitor
const systemTimings = PerformanceMonitor.getSystemTimings();
const timingTuples: Array<[string, number]> = systemTimings.map(t => [t.name, t.duration]);

// Cache for next call (in case update hasn't run)
this.lastSystemTimings = timingTuples;

// Get entity counts (O(1) - just reading lengths)
const units = this.world.getEntitiesWith('Unit').length;
const buildings = this.world.getEntitiesWith('Building').length;
const resources = this.world.getEntitiesWith('Resource').length;
const projectiles = this.world.getEntitiesWith('Projectile').length;

const metrics: WorkerPerformanceMetrics = {
tickTime: this.lastTickDuration,
systemTimings: timingTuples,
entityCounts: [units, buildings, resources, projectiles],
};

postMessage({ type: 'performanceMetrics', metrics } satisfies WorkerToMainMessage);
}

// ============================================================================
// ENTITY SPAWNING
// ============================================================================
Expand Down Expand Up @@ -1108,6 +1186,11 @@ if (typeof self !== 'undefined') {
}
break;
}

case 'setPerformanceCollection': {
game?.setPerformanceCollection(message.enabled);
break;
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
Expand Down
23 changes: 23 additions & 0 deletions src/engine/workers/WorkerBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
removeMultiplayerMessageHandler,
} from '@/store/multiplayerStore';
import { debugInitialization } from '@/utils/debugLogger';
import { PerformanceMonitor } from '../core/PerformanceMonitor';

// ============================================================================
// TYPES
Expand Down Expand Up @@ -280,6 +281,15 @@ export class WorkerBridge {
remoteChecksum: message.remoteChecksum,
});
break;

case 'performanceMetrics':
// Forward worker performance metrics to main thread's PerformanceMonitor
PerformanceMonitor.applyWorkerMetrics(
message.metrics.tickTime,
message.metrics.systemTimings,
message.metrics.entityCounts
);
break;
}
}

Expand Down Expand Up @@ -420,6 +430,19 @@ export class WorkerBridge {
this.worker?.postMessage({ type: 'setDebugSettings', settings } satisfies MainToWorkerMessage);
}

// ============================================================================
// PERFORMANCE COLLECTION
// ============================================================================

/**
* Enable or disable performance metrics collection in the worker.
* When enabled, the worker sends metrics at 10Hz for the performance dashboard.
* When disabled, zero overhead - no timing, no messages.
*/
public setPerformanceCollection(enabled: boolean): void {
this.worker?.postMessage({ type: 'setPerformanceCollection', enabled } satisfies MainToWorkerMessage);
}

// ============================================================================
// ENTITY SPAWNING
// ============================================================================
Expand Down
19 changes: 17 additions & 2 deletions src/engine/workers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@ export type MainToWorkerMessage =
| { type: 'requestChecksum' }
| { type: 'setSelection'; entityIds: number[]; playerId: string }
| { type: 'setControlGroup'; groupNumber: number; entityIds: number[] }
| { type: 'spawnEntities'; mapData: SpawnMapData };
| { type: 'spawnEntities'; mapData: SpawnMapData }
| { type: 'setPerformanceCollection'; enabled: boolean };

/**
* Player slot info for spawning
Expand Down Expand Up @@ -426,6 +427,19 @@ export interface SpawnMapData {
playerSlots?: PlayerSlotInfo[];
}

/**
* Performance metrics sent from worker to main thread.
* Only sent when performance collection is enabled (panel is open).
* Uses flat arrays for efficient serialization.
*/
export interface WorkerPerformanceMetrics {
tickTime: number;
// Tuple array [systemName, durationMs] for efficient serialization
systemTimings: Array<[string, number]>;
// [units, buildings, resources, projectiles]
entityCounts: [number, number, number, number];
}

/**
* Messages sent FROM game worker TO main thread
*/
Expand All @@ -437,7 +451,8 @@ export type WorkerToMainMessage =
| { type: 'gameOver'; winnerId: string | null; reason: string }
| { type: 'error'; message: string; stack?: string }
| { type: 'multiplayerCommand'; command: GameCommand }
| { type: 'desync'; tick: number; localChecksum: string; remoteChecksum: string };
| { type: 'desync'; tick: number; localChecksum: string; remoteChecksum: string }
| { type: 'performanceMetrics'; metrics: WorkerPerformanceMetrics };

// ============================================================================
// GAME COMMAND - Re-export from shared module for consistency
Expand Down