mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 17:46:39 +00:00
Use dirty flags to coalesce tile updates in SAB ring
- Extend SharedTileRing to include a shared dirtyFlags buffer alongside header and data - Pass shared dirty buffer through WorkerClient/WorkerMessages and initialize views in Worker.worker - In SAB mode, mark tiles dirty via Atomics.compareExchange before enqueuing to ensure each tile is queued at most once until processed - On the main thread, clear dirty flags when draining the ring and build packedTileUpdates from distinct tile refs - Keep non-SAB behaviour unchanged while reducing ring pressure and making overflows reflect true backlog, not duplicate updates
This commit is contained in:
@@ -192,6 +192,8 @@ async function createClientGame(
|
||||
|
||||
let sharedTileRingBuffers: SharedTileRingBuffers | undefined;
|
||||
let sharedTileRingViews: SharedTileRingViews | null = null;
|
||||
let sharedDirtyBuffer: SharedArrayBuffer | undefined;
|
||||
let sharedDirtyFlags: Uint8Array | null = null;
|
||||
const isIsolated =
|
||||
typeof (globalThis as any).crossOriginIsolated === "boolean"
|
||||
? (globalThis as any).crossOriginIsolated === true
|
||||
@@ -210,8 +212,14 @@ async function createClientGame(
|
||||
// Capacity is number of tile updates that can be queued.
|
||||
// This is a compromise between memory usage and backlog tolerance.
|
||||
const TILE_RING_CAPACITY = 262144;
|
||||
sharedTileRingBuffers = createSharedTileRingBuffers(TILE_RING_CAPACITY);
|
||||
const numTiles = gameMap.gameMap.width() * gameMap.gameMap.height();
|
||||
sharedTileRingBuffers = createSharedTileRingBuffers(
|
||||
TILE_RING_CAPACITY,
|
||||
numTiles,
|
||||
);
|
||||
sharedTileRingViews = createSharedTileRingViews(sharedTileRingBuffers);
|
||||
sharedDirtyBuffer = sharedTileRingBuffers.dirty;
|
||||
sharedDirtyFlags = sharedTileRingViews.dirtyFlags;
|
||||
}
|
||||
|
||||
const worker = new WorkerClient(
|
||||
@@ -219,6 +227,7 @@ async function createClientGame(
|
||||
lobbyConfig.clientID,
|
||||
sharedTileRingBuffers,
|
||||
sharedStateBuffer,
|
||||
sharedDirtyBuffer,
|
||||
);
|
||||
await worker.initialize();
|
||||
const gameView = new GameView(
|
||||
@@ -247,6 +256,7 @@ async function createClientGame(
|
||||
worker,
|
||||
gameView,
|
||||
sharedTileRingViews,
|
||||
sharedDirtyFlags,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -278,6 +288,7 @@ export class ClientGameRunner {
|
||||
private pendingStart = 0;
|
||||
private isProcessingUpdates = false;
|
||||
private tileRingViews: SharedTileRingViews | null;
|
||||
private dirtyFlags: Uint8Array | null;
|
||||
|
||||
constructor(
|
||||
private lobby: LobbyConfig,
|
||||
@@ -288,9 +299,11 @@ export class ClientGameRunner {
|
||||
private worker: WorkerClient,
|
||||
private gameView: GameView,
|
||||
tileRingViews: SharedTileRingViews | null,
|
||||
dirtyFlags: Uint8Array | null,
|
||||
) {
|
||||
this.lastMessageTime = Date.now();
|
||||
this.tileRingViews = tileRingViews;
|
||||
this.dirtyFlags = dirtyFlags;
|
||||
}
|
||||
|
||||
private saveGame(update: WinUpdate) {
|
||||
@@ -698,22 +711,31 @@ export class ClientGameRunner {
|
||||
);
|
||||
const drainTime = performance.now() - drainStart;
|
||||
|
||||
// Deduplicate tile refs for this render slice
|
||||
const uniqueTiles = new Set<TileRef>();
|
||||
for (const ref of tileRefs) {
|
||||
uniqueTiles.add(ref);
|
||||
}
|
||||
|
||||
// Calculate ring buffer utilization and overflow
|
||||
const TILE_RING_CAPACITY = 262144;
|
||||
const utilization = (tileRefs.length / TILE_RING_CAPACITY) * 100;
|
||||
const utilization = (uniqueTiles.size / TILE_RING_CAPACITY) * 100;
|
||||
const overflow = Atomics.load(
|
||||
this.tileRingViews.header,
|
||||
TILE_RING_HEADER_OVERFLOW,
|
||||
);
|
||||
|
||||
tileMetrics = {
|
||||
count: tileRefs.length,
|
||||
count: uniqueTiles.size,
|
||||
utilization,
|
||||
overflow,
|
||||
drainTime,
|
||||
};
|
||||
|
||||
for (const ref of tileRefs) {
|
||||
for (const ref of uniqueTiles) {
|
||||
if (this.dirtyFlags) {
|
||||
Atomics.store(this.dirtyFlags, ref, 0);
|
||||
}
|
||||
combinedPackedTileUpdates.push(BigInt(ref));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -7,6 +7,7 @@ export type TerrainMapData = {
|
||||
gameMap: GameMap;
|
||||
miniGameMap: GameMap;
|
||||
sharedStateBuffer?: SharedArrayBuffer;
|
||||
sharedDirtyBuffer?: SharedArrayBuffer;
|
||||
};
|
||||
|
||||
const loadedMaps = new Map<GameMapType, TerrainMapData>();
|
||||
@@ -104,6 +105,7 @@ export async function loadTerrainMap(
|
||||
stateBuffer instanceof SharedArrayBuffer
|
||||
? stateBuffer
|
||||
: undefined,
|
||||
sharedDirtyBuffer: undefined, // populated by consumer when needed
|
||||
};
|
||||
// Always cache the result, but only use cache when appropriate
|
||||
loadedMaps.set(map, result);
|
||||
|
||||
@@ -3,11 +3,13 @@ import { TileRef } from "../game/GameMap";
|
||||
export interface SharedTileRingBuffers {
|
||||
header: SharedArrayBuffer;
|
||||
data: SharedArrayBuffer;
|
||||
dirty: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
export interface SharedTileRingViews {
|
||||
header: Int32Array;
|
||||
buffer: Uint32Array;
|
||||
dirtyFlags: Uint8Array;
|
||||
capacity: number;
|
||||
}
|
||||
|
||||
@@ -18,10 +20,12 @@ export const TILE_RING_HEADER_OVERFLOW = 2;
|
||||
|
||||
export function createSharedTileRingBuffers(
|
||||
capacity: number,
|
||||
numTiles: number,
|
||||
): SharedTileRingBuffers {
|
||||
const header = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT);
|
||||
const data = new SharedArrayBuffer(capacity * Uint32Array.BYTES_PER_ELEMENT);
|
||||
return { header, data };
|
||||
const dirty = new SharedArrayBuffer(numTiles * Uint8Array.BYTES_PER_ELEMENT);
|
||||
return { header, data, dirty };
|
||||
}
|
||||
|
||||
export function createSharedTileRingViews(
|
||||
@@ -29,9 +33,11 @@ export function createSharedTileRingViews(
|
||||
): SharedTileRingViews {
|
||||
const header = new Int32Array(buffers.header);
|
||||
const buffer = new Uint32Array(buffers.data);
|
||||
const dirtyFlags = new Uint8Array(buffers.dirty);
|
||||
return {
|
||||
header,
|
||||
buffer,
|
||||
dirtyFlags,
|
||||
capacity: buffer.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ let gameRunner: Promise<GameRunner> | null = null;
|
||||
const mapLoader = new FetchGameMapLoader(`/maps`, version);
|
||||
let isProcessingTurns = false;
|
||||
let sharedTileRing: SharedTileRingViews | null = null;
|
||||
let dirtyFlags: Uint8Array | null = null;
|
||||
|
||||
function gameUpdate(gu: GameUpdateViewData | ErrorUpdate) {
|
||||
// skip if ErrorUpdate
|
||||
@@ -73,19 +74,35 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
sharedTileRing = createSharedTileRingViews({
|
||||
header: message.sharedTileRingHeader,
|
||||
data: message.sharedTileRingData,
|
||||
dirty: message.sharedDirtyBuffer!,
|
||||
});
|
||||
dirtyFlags = sharedTileRing.dirtyFlags;
|
||||
} else {
|
||||
sharedTileRing = null;
|
||||
dirtyFlags = null;
|
||||
}
|
||||
|
||||
console.log("[Worker.worker] init", {
|
||||
hasSharedStateBuffer: !!message.sharedStateBuffer,
|
||||
hasRingHeader: !!message.sharedTileRingHeader,
|
||||
hasRingData: !!message.sharedTileRingData,
|
||||
hasDirtyBuffer: !!message.sharedDirtyBuffer,
|
||||
});
|
||||
|
||||
gameRunner = createGameRunner(
|
||||
message.gameStartInfo,
|
||||
message.clientID,
|
||||
mapLoader,
|
||||
gameUpdate,
|
||||
sharedTileRing
|
||||
? (tile: TileRef) => pushTileUpdate(sharedTileRing!, tile)
|
||||
: undefined,
|
||||
sharedTileRing && dirtyFlags
|
||||
? (tile: TileRef) => {
|
||||
if (Atomics.compareExchange(dirtyFlags!, tile, 0, 1) === 0) {
|
||||
pushTileUpdate(sharedTileRing!, tile);
|
||||
}
|
||||
}
|
||||
: sharedTileRing
|
||||
? (tile: TileRef) => pushTileUpdate(sharedTileRing!, tile)
|
||||
: undefined,
|
||||
message.sharedStateBuffer,
|
||||
).then((gr) => {
|
||||
sendMessage({
|
||||
|
||||
@@ -25,6 +25,7 @@ export class WorkerClient {
|
||||
private clientID: ClientID,
|
||||
private sharedTileRingBuffers?: SharedTileRingBuffers,
|
||||
private sharedStateBuffer?: SharedArrayBuffer,
|
||||
private sharedDirtyBuffer?: SharedArrayBuffer,
|
||||
) {
|
||||
this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url));
|
||||
this.messageHandlers = new Map();
|
||||
@@ -76,6 +77,7 @@ export class WorkerClient {
|
||||
sharedTileRingHeader: this.sharedTileRingBuffers?.header,
|
||||
sharedTileRingData: this.sharedTileRingBuffers?.data,
|
||||
sharedStateBuffer: this.sharedStateBuffer,
|
||||
sharedDirtyBuffer: this.sharedDirtyBuffer,
|
||||
});
|
||||
|
||||
// Add timeout for initialization
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface InitMessage extends BaseWorkerMessage {
|
||||
sharedTileRingHeader?: SharedArrayBuffer;
|
||||
sharedTileRingData?: SharedArrayBuffer;
|
||||
sharedStateBuffer?: SharedArrayBuffer;
|
||||
sharedDirtyBuffer?: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
export interface TurnMessage extends BaseWorkerMessage {
|
||||
|
||||
Reference in New Issue
Block a user