mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-08 05:50:40 +00:00
Speed up diffPlayerUpdate with typed field comparisons
diffPlayerUpdate runs once per player per tick on the worker thread. The array/object fields (outgoingAttacks, incomingAttacks, alliances, outgoingEmojis) were compared via JSON.stringify — two string allocations per field, run on every call even when nothing changed. This made the cost flat at ~3.4µs/call regardless of what actually changed. Replace jsonEqual with three typed structural comparators (attackArrayEqual, allianceArrayEqual, emojiArrayEqual) that short-circuit on reference/length, compare known fields with ===, early-exit on the first difference, and allocate nothing — matching the existing numberArrayEqual/stringArrayEqual style. ~9-10x faster across all cases (276k -> 2.4M ops/sec when unchanged). Add tests/perf/DiffPlayerUpdatePerf.ts (BEFORE/AFTER benchmark, run via npm run perf) and warnings on PlayerUpdate and diffPlayerUpdate noting that new fields must be wired into the diff/apply functions or their changes are silently dropped after the first emission. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import type { PlayerState } from "../../client/render/types";
|
||||
import { GameUpdateType, PlayerUpdate } from "./GameUpdates";
|
||||
import type { EmojiMessage } from "./Game";
|
||||
import {
|
||||
AllianceView,
|
||||
AttackUpdate,
|
||||
GameUpdateType,
|
||||
PlayerUpdate,
|
||||
} from "./GameUpdates";
|
||||
|
||||
/**
|
||||
* Build a partial PlayerUpdate containing only fields whose value differs
|
||||
@@ -8,6 +14,12 @@ import { GameUpdateType, PlayerUpdate } from "./GameUpdates";
|
||||
* `type` and `id` are always included on the returned diff. Array/object
|
||||
* fields are compared by structural equality (length + per-element);
|
||||
* `embargoes` is compared as a set; primitive fields by `===`.
|
||||
*
|
||||
* WARNING: this diff is field-by-field by design (no JSON.stringify, for
|
||||
* perf — see tests/perf/DiffPlayerUpdatePerf.ts). When you add a field to
|
||||
* PlayerUpdate, you MUST add a matching setIfDifferent(...) line here, and an
|
||||
* apply line in applyStateUpdate below. A field missing here is never diffed,
|
||||
* so its changes silently never reach the main thread after the first update.
|
||||
*/
|
||||
export function diffPlayerUpdate(
|
||||
prev: PlayerUpdate,
|
||||
@@ -62,17 +74,20 @@ export function diffPlayerUpdate(
|
||||
setIfDifferent("embargoes", stringSetEqual(prev.embargoes, next.embargoes));
|
||||
setIfDifferent(
|
||||
"outgoingEmojis",
|
||||
jsonEqual(prev.outgoingEmojis, next.outgoingEmojis),
|
||||
emojiArrayEqual(prev.outgoingEmojis, next.outgoingEmojis),
|
||||
);
|
||||
setIfDifferent(
|
||||
"outgoingAttacks",
|
||||
jsonEqual(prev.outgoingAttacks, next.outgoingAttacks),
|
||||
attackArrayEqual(prev.outgoingAttacks, next.outgoingAttacks),
|
||||
);
|
||||
setIfDifferent(
|
||||
"incomingAttacks",
|
||||
jsonEqual(prev.incomingAttacks, next.incomingAttacks),
|
||||
attackArrayEqual(prev.incomingAttacks, next.incomingAttacks),
|
||||
);
|
||||
setIfDifferent(
|
||||
"alliances",
|
||||
allianceArrayEqual(prev.alliances, next.alliances),
|
||||
);
|
||||
setIfDifferent("alliances", jsonEqual(prev.alliances, next.alliances));
|
||||
|
||||
return changed ? diff : null;
|
||||
}
|
||||
@@ -144,7 +159,61 @@ function stringSetEqual(a?: Set<string>, b?: Set<string>): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function jsonEqual(a: unknown, b: unknown): boolean {
|
||||
function attackArrayEqual(a?: AttackUpdate[], b?: AttackUpdate[]): boolean {
|
||||
if (a === b) return true;
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
if (!a || !b) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const x = a[i];
|
||||
const y = b[i];
|
||||
if (
|
||||
x.attackerID !== y.attackerID ||
|
||||
x.targetID !== y.targetID ||
|
||||
x.troops !== y.troops ||
|
||||
x.id !== y.id ||
|
||||
x.retreating !== y.retreating
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function allianceArrayEqual(a?: AllianceView[], b?: AllianceView[]): boolean {
|
||||
if (a === b) return true;
|
||||
if (!a || !b) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const x = a[i];
|
||||
const y = b[i];
|
||||
if (
|
||||
x.id !== y.id ||
|
||||
x.other !== y.other ||
|
||||
x.createdAt !== y.createdAt ||
|
||||
x.expiresAt !== y.expiresAt ||
|
||||
x.hasExtensionRequest !== y.hasExtensionRequest
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function emojiArrayEqual(a?: EmojiMessage[], b?: EmojiMessage[]): boolean {
|
||||
if (a === b) return true;
|
||||
if (!a || !b) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const x = a[i];
|
||||
const y = b[i];
|
||||
if (
|
||||
x.message !== y.message ||
|
||||
x.senderID !== y.senderID ||
|
||||
x.recipientID !== y.recipientID ||
|
||||
x.createdAt !== y.createdAt
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -181,6 +181,10 @@ export interface AttackUpdate {
|
||||
* value matches the previous emission for the same player. The first emission
|
||||
* for a player always includes all fields; consumers must handle subsequent
|
||||
* partial updates by merging into local state, not overwriting.
|
||||
*
|
||||
* When adding a field here, also wire it into diffPlayerUpdate() and
|
||||
* applyStateUpdate() in GameUpdateUtils.ts — otherwise it is only ever sent on
|
||||
* the first emission and later changes are silently dropped.
|
||||
*/
|
||||
export interface PlayerUpdate {
|
||||
type: GameUpdateType.Player;
|
||||
|
||||
Reference in New Issue
Block a user