Skip to content

Commit 637fdcd

Browse files
anan474cpojer
andauthored
[Performance] EvaluationPanel run map evaluations in async worker #2 (#33)
Co-authored-by: cpojer <[email protected]>
1 parent 2e19113 commit 637fdcd

8 files changed

+185
-54
lines changed

.eslintrc.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ module.exports = {
100100
],
101101
'import/no-unresolved': [
102102
2,
103-
{ ignore: ['athena-crisis:*', 'glob', 'virtual:*'] },
103+
{ ignore: ['athena-crisis:*', 'glob', 'virtual:*', '\\?worker'] },
104104
],
105105
'no-extra-parens': 0,
106106
'no-restricted-globals': [2, 'alert', 'confirm'],

apollo/ActionResponseMutator.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ActionResponse } from './ActionResponse.tsx';
2+
3+
const Mutators = {
4+
actAsEveryPlayer: (actionResponse: ActionResponse) =>
5+
actionResponse.type === 'EndTurn'
6+
? { ...actionResponse, rotatePlayers: true }
7+
: actionResponse,
8+
} as const;
9+
10+
export type MutateActionResponseFnName = keyof typeof Mutators;
11+
12+
export default Mutators;

apollo/GameState.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import MapData from '@deities/athena/MapData.tsx';
2+
import {
3+
decodeActionResponse,
4+
encodeActionResponse,
5+
} from './EncodedActions.tsx';
6+
import { EncodedGameState, GameState } from './Types.tsx';
7+
8+
export function encodeGameState(gameState: GameState): EncodedGameState {
9+
return [...gameState].map(([actionResponse, map]) => [
10+
encodeActionResponse(actionResponse),
11+
map.toJSON(),
12+
]);
13+
}
14+
15+
export function decodeGameState(encodedGameState: EncodedGameState): GameState {
16+
return encodedGameState.map(([encodedActionResponse, plainMap]) => [
17+
decodeActionResponse(encodedActionResponse),
18+
MapData.fromObject(plainMap),
19+
]);
20+
}

apollo/Types.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Building, { PlainBuilding } from '@deities/athena/map/Building.tsx';
2-
import { PlainEntitiesList } from '@deities/athena/map/PlainMap.tsx';
2+
import { PlainEntitiesList, PlainMap } from '@deities/athena/map/PlainMap.tsx';
33
import Unit, { PlainUnit } from '@deities/athena/map/Unit.tsx';
44
import Vector from '@deities/athena/map/Vector.tsx';
55
import MapData from '@deities/athena/MapData.tsx';
@@ -9,7 +9,9 @@ import { Effects } from './Effects.tsx';
99
import { EncodedActionResponse } from './EncodedActions.tsx';
1010

1111
export type GameStateEntry = readonly [ActionResponse, MapData];
12+
export type EncodedGameStateEntry = readonly [EncodedActionResponse, PlainMap];
1213
export type GameState = ReadonlyArray<GameStateEntry>;
14+
export type EncodedGameState = ReadonlyArray<EncodedGameStateEntry>;
1315
export type MutableGameState = Array<GameStateEntry>;
1416
export type GameStateWithEffects = ReadonlyArray<
1517
readonly [...GameStateEntry, Effects]

hera/editor/MapEditor.tsx

+8-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ActionResponse } from '@deities/apollo/ActionResponse.tsx';
1+
import ActionResponseMutator from '@deities/apollo/ActionResponseMutator.tsx';
22
import {
33
decodeEffects,
44
Effects,
@@ -738,16 +738,12 @@ export default function MapEditor({
738738
[editor?.selected?.tile, setMap],
739739
);
740740

741-
const mutateAction = useCallback(
742-
(actionResponse: ActionResponse) =>
743-
actAsEveryPlayer && actionResponse.type === 'EndTurn'
744-
? { ...actionResponse, rotatePlayers: true }
745-
: actionResponse,
746-
[actAsEveryPlayer],
741+
const onAction = useClientGameAction(
742+
game,
743+
setGame,
744+
actAsEveryPlayer ? 'actAsEveryPlayer' : null,
747745
);
748746

749-
const onAction = useClientGameAction(game, setGame, mutateAction);
750-
751747
useEffect(() => {
752748
if (saveState) {
753749
const timer = setTimeout(() => setSaveState(null), 5000);
@@ -791,7 +787,9 @@ export default function MapEditor({
791787
key="play-test-map"
792788
lastActionResponse={game?.lastAction || undefined}
793789
map={game?.state || map}
794-
mutateAction={mutateAction}
790+
mutateAction={
791+
actAsEveryPlayer ? ActionResponseMutator.actAsEveryPlayer : undefined
792+
}
795793
onAction={onAction}
796794
pan
797795
scale={zoom}

hera/hooks/useClientGameAction.tsx

+71-42
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,57 @@
1-
import { Action, MutateActionResponseFn } from '@deities/apollo/Action.tsx';
1+
import { Action } from '@deities/apollo/Action.tsx';
22
import { ActionResponse } from '@deities/apollo/ActionResponse.tsx';
3+
import { MutateActionResponseFnName } from '@deities/apollo/ActionResponseMutator.tsx';
34
import encodeGameActionResponse from '@deities/apollo/actions/encodeGameActionResponse.tsx';
4-
import executeGameAction from '@deities/apollo/actions/executeGameAction.tsx';
5-
import { Effects } from '@deities/apollo/Effects.tsx';
5+
import {
6+
decodeEffects,
7+
Effects,
8+
EncodedEffects,
9+
encodeEffects,
10+
} from '@deities/apollo/Effects.tsx';
11+
import {
12+
decodeActionResponse,
13+
encodeAction,
14+
EncodedActionResponse,
15+
} from '@deities/apollo/EncodedActions.tsx';
16+
import { decodeGameState } from '@deities/apollo/GameState.tsx';
617
import { computeVisibleEndTurnActionResponse } from '@deities/apollo/lib/computeVisibleActions.tsx';
718
import decodeGameActionResponse from '@deities/apollo/lib/decodeGameActionResponse.tsx';
819
import dropLabelsFromActionResponse from '@deities/apollo/lib/dropLabelsFromActionResponse.tsx';
920
import dropLabelsFromGameState from '@deities/apollo/lib/dropLabelsFromGameState.tsx';
10-
import { GameActionResponse, GameState } from '@deities/apollo/Types.tsx';
21+
import {
22+
EncodedGameState,
23+
GameActionResponse,
24+
GameState,
25+
} from '@deities/apollo/Types.tsx';
26+
import { PlainMap } from '@deities/athena/map/PlainMap.tsx';
1127
import MapData from '@deities/athena/MapData.tsx';
1228
import { getHiddenLabels } from '@deities/athena/WinConditions.tsx';
13-
import AIRegistry from '@deities/dionysus/AIRegistry.tsx';
1429
import onGameEnd from '@deities/hermes/game/onGameEnd.tsx';
1530
import toClientGame, {
1631
ClientGame,
1732
} from '@deities/hermes/game/toClientGame.tsx';
1833
import { useCallback } from 'react';
34+
import gameActionWorker from '../workers/gameAction.tsx?worker';
35+
36+
type ClientGameAction = [
37+
actionResponse: EncodedActionResponse,
38+
map: PlainMap,
39+
gameState: EncodedGameState,
40+
effects: EncodedEffects,
41+
];
1942

2043
const ActionError = (action: Action) =>
2144
new Error(`Map: Error executing remote '${action.type}' action.`);
2245

2346
export default function useClientGameAction(
2447
game: ClientGame | null,
2548
setGame: (game: ClientGame) => void,
26-
mutateAction?: MutateActionResponseFn,
49+
mutateAction?: MutateActionResponseFnName | null,
2750
) {
2851
return useCallback(
29-
(action: Action): Promise<GameActionResponse> => {
52+
async (action: Action): Promise<GameActionResponse> => {
3053
if (!game) {
31-
return Promise.reject(new Error('Client Game: Map state is missing.'));
54+
throw new Error('Client Game: Map state is missing.');
3255
}
3356

3457
let actionResponse: ActionResponse | null;
@@ -42,23 +65,35 @@ export default function useClientGameAction(
4265
const isStart = action.type === 'Start';
4366

4467
if (isStart === !!game.lastAction) {
45-
return Promise.reject(ActionError(action));
68+
throw ActionError(action);
4669
}
4770

4871
try {
49-
[actionResponse, initialActiveMap, gameState, newEffects] =
50-
executeGameAction(
51-
map,
52-
vision,
53-
game.effects,
54-
action,
55-
AIRegistry,
72+
const worker = new gameActionWorker();
73+
const [
74+
encodedActionResponse,
75+
plainMap,
76+
encodedGameState,
77+
encodedEffects,
78+
] = await new Promise<ClientGameAction>((resolve) => {
79+
worker.postMessage([
80+
map.toJSON(),
81+
encodeEffects(game.effects),
82+
encodeAction(action),
5683
mutateAction,
57-
) || [null, null, null];
84+
]);
85+
worker.onmessage = (event: MessageEvent<ClientGameAction>) =>
86+
resolve(event.data);
87+
});
88+
89+
actionResponse = decodeActionResponse(encodedActionResponse);
90+
initialActiveMap = MapData.fromObject(plainMap);
91+
gameState = decodeGameState(encodedGameState);
92+
newEffects = decodeEffects(encodedEffects);
5893
} catch (error) {
59-
return Promise.reject(
60-
process.env.NODE_ENV === 'development' ? error : ActionError(action),
61-
);
94+
throw process.env.NODE_ENV === 'development'
95+
? error
96+
: ActionError(action);
6297
}
6398

6499
if (actionResponse && initialActiveMap && gameState) {
@@ -79,31 +114,25 @@ export default function useClientGameAction(
79114
);
80115
gameState = dropLabelsFromGameState(gameState, hiddenLabels);
81116

82-
return Promise.resolve(
83-
decodeGameActionResponse(
84-
encodeGameActionResponse(
85-
map,
86-
initialActiveMap,
87-
vision,
88-
onGameEnd(
89-
gameState,
90-
newEffects || game.effects,
91-
currentViewer.id,
92-
),
93-
null,
94-
actionResponse?.type === 'EndTurn'
95-
? computeVisibleEndTurnActionResponse(
96-
actionResponse,
97-
map,
98-
initialActiveMap,
99-
vision,
100-
)
101-
: actionResponse,
102-
),
117+
return decodeGameActionResponse(
118+
encodeGameActionResponse(
119+
map,
120+
initialActiveMap,
121+
vision,
122+
onGameEnd(gameState, newEffects || game.effects, currentViewer.id),
123+
null,
124+
actionResponse?.type === 'EndTurn'
125+
? computeVisibleEndTurnActionResponse(
126+
actionResponse,
127+
map,
128+
initialActiveMap,
129+
vision,
130+
)
131+
: actionResponse,
103132
),
104133
);
105134
}
106-
return Promise.reject(ActionError(action));
135+
throw ActionError(action);
107136
},
108137
[game, mutateAction, setGame],
109138
);

hera/workers/gameAction.tsx

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import './initializeWorker.tsx';
2+
import { ActionResponse } from '@deities/apollo/ActionResponse.tsx';
3+
import ActionResponseMutator, {
4+
MutateActionResponseFnName,
5+
} from '@deities/apollo/ActionResponseMutator.tsx';
6+
import executeGameAction from '@deities/apollo/actions/executeGameAction.tsx';
7+
import {
8+
decodeEffects,
9+
Effects,
10+
EncodedEffects,
11+
encodeEffects,
12+
} from '@deities/apollo/Effects.tsx';
13+
import {
14+
decodeAction,
15+
encodeActionResponse,
16+
EncodedAction,
17+
} from '@deities/apollo/EncodedActions.tsx';
18+
import { encodeGameState } from '@deities/apollo/GameState.tsx';
19+
import { GameState } from '@deities/apollo/Types.tsx';
20+
import { PlainMap } from '@deities/athena/map/PlainMap.tsx';
21+
import MapData from '@deities/athena/MapData.tsx';
22+
import AIRegistry from '@deities/dionysus/AIRegistry.tsx';
23+
24+
self.onmessage = function (
25+
event: MessageEvent<
26+
[
27+
map: PlainMap,
28+
encodedEffects: EncodedEffects,
29+
action: EncodedAction,
30+
mutateAction: MutateActionResponseFnName | undefined | null,
31+
]
32+
>,
33+
) {
34+
const [plainMap, encodedEffects, action, mutateAction] = event.data;
35+
const map = MapData.fromObject(plainMap);
36+
const vision = map.createVisionObject(map.getCurrentPlayer());
37+
const effects = decodeEffects(encodedEffects);
38+
const [actionResponse, initialActiveMap, gameState, newEffects]: [
39+
ActionResponse | null,
40+
MapData | null,
41+
GameState | null,
42+
Effects | null,
43+
] = executeGameAction(
44+
map,
45+
vision,
46+
effects,
47+
decodeAction(action),
48+
AIRegistry,
49+
mutateAction ? ActionResponseMutator[mutateAction] : undefined,
50+
);
51+
52+
self.postMessage([
53+
actionResponse ? encodeActionResponse(actionResponse) : null,
54+
initialActiveMap ? initialActiveMap?.toJSON() : null,
55+
gameState ? encodeGameState(gameState) : null,
56+
newEffects ? encodeEffects(newEffects) : null,
57+
]);
58+
};

hera/workers/initializeWorker.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
declare global {
2+
// eslint-disable-next-line no-var
3+
var $RefreshReg$: () => void;
4+
// eslint-disable-next-line no-var
5+
var $RefreshSig$: (type: string) => (type: string) => string;
6+
}
7+
8+
if (import.meta.hot) {
9+
self.$RefreshReg$ = () => {};
10+
// eslint-disable-next-line unicorn/consistent-function-scoping
11+
self.$RefreshSig$ = () => (type) => type;
12+
}

0 commit comments

Comments
 (0)