Implemented time-sliced catch-up on the main thread to keep input responsive.

src/client/ClientGameRunner.ts now drains pending game updates in small chunks (max 100 updates or ~8ms per slice) via requestAnimationFrame, merging and rendering per slice, and only clears the processing flag when the queue is empty.
This commit is contained in:
scamiv
2025-11-24 17:40:11 +01:00
parent 8508baee84
commit 9d63fcabe9
+43 -30
View File
@@ -477,23 +477,25 @@ export class ClientGameRunner {
}
private processPendingUpdates() {
if (this.isProcessingUpdates) {
return;
}
if (this.pendingUpdates.length === 0) {
if (this.isProcessingUpdates || this.pendingUpdates.length === 0) {
return;
}
this.isProcessingUpdates = true;
const batch = this.pendingUpdates.splice(0);
const processSlice = () => {
const SLICE_BUDGET_MS = 8; // keep UI responsive while catching up
const MAX_PER_SLICE = 100;
const sliceStart = performance.now();
const batch: GameUpdateViewData[] = [];
let processedCount = 0;
let lastTickDuration: number | undefined;
let lastTick: number | undefined;
let processedCount = 0;
let lastTickDuration: number | undefined;
let lastTick: number | undefined;
try {
for (const gu of batch) {
while (this.pendingUpdates.length > 0) {
const gu = this.pendingUpdates.shift() as GameUpdateViewData;
processedCount++;
batch.push(gu);
this.transport.turnComplete();
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
@@ -509,30 +511,41 @@ export class ClientGameRunner {
lastTickDuration = gu.tickExecutionDuration;
}
lastTick = gu.tick;
}
} finally {
this.isProcessingUpdates = false;
}
if (processedCount > 0 && lastTick !== undefined) {
const combinedGu = this.mergeGameUpdates(batch);
if (combinedGu) {
this.gameView.update(combinedGu);
const elapsed = performance.now() - sliceStart;
if (processedCount >= MAX_PER_SLICE || elapsed >= SLICE_BUDGET_MS) {
break;
}
}
this.renderer.tick();
this.eventBus.emit(
new TickMetricsEvent(
lastTickDuration,
this.currentTickDelay,
this.backlogTurns,
this.renderEveryN,
),
);
if (processedCount > 0 && lastTick !== undefined) {
const combinedGu = this.mergeGameUpdates(batch);
if (combinedGu) {
this.gameView.update(combinedGu);
}
// Reset tick delay for next measurement
this.currentTickDelay = undefined;
}
this.renderer.tick();
this.eventBus.emit(
new TickMetricsEvent(
lastTickDuration,
this.currentTickDelay,
this.backlogTurns,
this.renderEveryN,
),
);
// Reset tick delay for next measurement
this.currentTickDelay = undefined;
}
if (this.pendingUpdates.length > 0) {
requestAnimationFrame(processSlice);
} else {
this.isProcessingUpdates = false;
}
};
requestAnimationFrame(processSlice);
}
private adaptRenderFrequency(frameDuration: number) {