perf: warm first battle map before handoff
This commit is contained in:
@@ -164,7 +164,10 @@ try {
|
||||
firstBattlePlayableMs = Date.now() - battleStartedAt;
|
||||
}
|
||||
assert(
|
||||
battleReadyState.combatAssets?.baseReady === true && battleReadyState.combatAssets?.actionReady === false,
|
||||
battleReadyState.mapTextureReady === true &&
|
||||
battleReadyState.mapBackgroundReady === true &&
|
||||
battleReadyState.combatAssets?.baseReady === true &&
|
||||
battleReadyState.combatAssets?.actionReady === false,
|
||||
`Expected deployment to open from base sheets before action sheets: ${JSON.stringify(battleReadyState)}`
|
||||
);
|
||||
assert(
|
||||
@@ -193,6 +196,24 @@ try {
|
||||
const battleEntryOnly = battleReadyEntries.filter((entry) => !preBattleNames.has(entry.name));
|
||||
const battleOnly = battleEntries.filter((entry) => !preBattleNames.has(entry.name));
|
||||
const battleDeferredOnly = battleOnly.filter((entry) => !battleReadyNames.has(entry.name));
|
||||
const battleEntryFirstMapRequests = battleEntryOnly
|
||||
.map((entry) => decodeURIComponent(entry.name))
|
||||
.filter((name) => /\/first-battle-map-v2(?:-[^/?]+)?\.webp(?:[?#]|$)/i.test(name));
|
||||
const storyFirstMapWarmupRequests = battleTransitionStart.resourceEntries
|
||||
.map((entry) => decodeURIComponent(entry.name))
|
||||
.filter((name) => /\/first-battle-map-v2(?:-[^/?]+)?\.webp(?:[?#]|$)/i.test(name));
|
||||
assert(
|
||||
battleTransitionStart.firstBattleWarmup?.mapTextureReady === true &&
|
||||
battleTransitionStart.firstBattleWarmup?.mapTextureRetained === true &&
|
||||
battleTransitionStart.firstBattleWarmup?.actionTextureKeysLoaded?.length === 0 &&
|
||||
storyFirstMapWarmupRequests.length === 1 &&
|
||||
battleEntryFirstMapRequests.length === 0,
|
||||
`Expected the first battle handoff to reuse the story-warmed map without action-sheet preloads: ${JSON.stringify({
|
||||
warmup: battleTransitionStart.firstBattleWarmup,
|
||||
storyFirstMapWarmupRequests,
|
||||
battleEntryFirstMapRequests
|
||||
})}`
|
||||
);
|
||||
const storyTransition = segmentStats(storyOnly);
|
||||
const battleEntryTransition = segmentStats(battleEntryOnly);
|
||||
const battleDeferredTransition = segmentStats(battleDeferredOnly);
|
||||
@@ -225,6 +246,13 @@ try {
|
||||
entry: battleReadyState.combatAssets,
|
||||
playable: battlePlayableState.combatAssets
|
||||
},
|
||||
firstBattleMapWarmup: {
|
||||
story: battleTransitionStart.firstBattleWarmup,
|
||||
battleMapTextureReady: battleReadyState.mapTextureReady,
|
||||
battleMapBackgroundReady: battleReadyState.mapBackgroundReady,
|
||||
warmupRequests: storyFirstMapWarmupRequests,
|
||||
handoffRequests: battleEntryFirstMapRequests
|
||||
},
|
||||
combatAssetStallFallback
|
||||
};
|
||||
report.budget = evaluateBudgets(report, budgets);
|
||||
@@ -406,6 +434,7 @@ async function advanceStoryUntilBattle(page, battleId) {
|
||||
storyLastPage: story?.isLastPage === true,
|
||||
storyTargetsBattle: story?.nextScene === 'BattleScene',
|
||||
currentPageReady: story?.assetStreaming?.currentPageReady === true,
|
||||
firstBattleWarmup: story?.assetStreaming?.firstBattleWarmup ?? null,
|
||||
villageActive: activeScenes.includes('PrologueVillageScene'),
|
||||
villageReady: village?.ready === true,
|
||||
villageNavigationPending: village?.navigationPending === true,
|
||||
@@ -428,6 +457,13 @@ async function advanceStoryUntilBattle(page, battleId) {
|
||||
state.storyTargetsBattle &&
|
||||
state.currentPageReady
|
||||
) {
|
||||
assert(
|
||||
state.firstBattleWarmup?.active === true &&
|
||||
state.firstBattleWarmup?.mapTextureReady === true &&
|
||||
state.firstBattleWarmup?.mapTextureRetained === true &&
|
||||
state.firstBattleWarmup?.actionTextureKeysLoaded?.length === 0,
|
||||
`Expected final first-battle story page to retain its warmed map without action sheets: ${JSON.stringify(state.firstBattleWarmup)}`
|
||||
);
|
||||
const entries = await resourceEntries(page);
|
||||
const startedAt = Date.now();
|
||||
await page.keyboard.press('Space');
|
||||
@@ -447,7 +483,11 @@ async function advanceStoryUntilBattle(page, battleId) {
|
||||
)
|
||||
);
|
||||
}, battleId, { timeout: 5000 });
|
||||
return { startedAt, resourceEntries: entries };
|
||||
return {
|
||||
startedAt,
|
||||
resourceEntries: entries,
|
||||
firstBattleWarmup: state.firstBattleWarmup
|
||||
};
|
||||
}
|
||||
|
||||
if (state.villageActive && state.villageReady && !state.villageNavigationPending) {
|
||||
@@ -502,7 +542,12 @@ async function waitForBattleReady(page, battleId) {
|
||||
state?.combatAssets?.baseReady === true &&
|
||||
state?.resultVisible === false
|
||||
)
|
||||
? { phase: state.phase, combatAssets: state.combatAssets }
|
||||
? {
|
||||
phase: state.phase,
|
||||
mapTextureReady: state.mapTextureReady,
|
||||
mapBackgroundReady: state.mapBackgroundReady,
|
||||
combatAssets: state.combatAssets
|
||||
}
|
||||
: false;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -611,6 +656,7 @@ async function verifyCombatAssetStallFallback(browserInstance, baseUrl, kind) {
|
||||
battleUrl.searchParams.set('debugCombatAssetWatchdogMs', String(watchdogMs));
|
||||
battleUrl.searchParams.set('assetStallQa', kind);
|
||||
await page.goto(battleUrl.toString(), { waitUntil: 'domcontentloaded' });
|
||||
await prewarmUnitActionAssetsModule(page, baseUrl);
|
||||
const entry = await waitForBattleReady(page, 'first-battle-zhuo-commandery');
|
||||
assert(
|
||||
entry.phase === 'deployment' && entry.combatAssets?.baseReady === true,
|
||||
@@ -699,6 +745,21 @@ async function verifyCombatAssetStallFallback(browserInstance, baseUrl, kind) {
|
||||
}
|
||||
}
|
||||
|
||||
async function prewarmUnitActionAssetsModule(page, baseUrl) {
|
||||
const assetsDir = join(process.cwd(), 'dist', 'assets');
|
||||
const candidates = existsSync(assetsDir)
|
||||
? readdirSync(assetsDir).filter((file) => /^unitActionAssets-[^.]+\.js$/i.test(file))
|
||||
: [];
|
||||
assert(
|
||||
candidates.length === 1,
|
||||
`Expected exactly one built unit action asset module, found ${JSON.stringify(candidates)}.`
|
||||
);
|
||||
const moduleUrl = new URL(`assets/${candidates[0]}`, baseUrl).toString();
|
||||
await page.evaluate(async (url) => {
|
||||
await import(url);
|
||||
}, moduleUrl);
|
||||
}
|
||||
|
||||
async function resourceEntries(page) {
|
||||
return page.evaluate(() =>
|
||||
performance.getEntriesByType('resource').map((entry) => ({
|
||||
|
||||
@@ -358,10 +358,14 @@ try {
|
||||
resumedPrologueSave,
|
||||
'resumed prologue story'
|
||||
);
|
||||
const firstBattleMapHandoffRequestStartIndex = requestedResourceUrls.length;
|
||||
const firstBattleTransition = await advanceUntilBattle(page, 'first-battle-zhuo-commandery', {
|
||||
startDeployment: false,
|
||||
captureLastStory: true
|
||||
});
|
||||
const firstBattleMapHandoffRequests = requestedResourceUrls
|
||||
.slice(firstBattleMapHandoffRequestStartIndex)
|
||||
.filter((url) => /\/first-battle-map-v2(?:-[^/?]+)?\.webp(?:[?#]|$)/i.test(decodeURIComponent(url)));
|
||||
assert(
|
||||
firstBattleTransition.lastStory?.isLastPage === true &&
|
||||
firstBattleTransition.lastStory?.nextScene === 'BattleScene' &&
|
||||
@@ -380,10 +384,19 @@ try {
|
||||
const firstDeploymentPanel = firstBattleDeployment?.battleHud?.deploymentPanel;
|
||||
assert(
|
||||
firstBattleDeployment?.phase === 'deployment' &&
|
||||
firstBattleDeployment?.mapTextureReady === true &&
|
||||
firstBattleDeployment?.mapBackgroundReady === true &&
|
||||
firstBattleTransition.lastStory?.assetStreaming?.firstBattleWarmup?.mapTextureReady === true &&
|
||||
firstBattleTransition.lastStory?.assetStreaming?.firstBattleWarmup?.mapTextureRetained === true &&
|
||||
firstBattleMapHandoffRequests.length === 0 &&
|
||||
firstDeploymentPanel?.recommended === true &&
|
||||
firstDeploymentPanel?.changeCount === 0 &&
|
||||
boundsInside(firstDeploymentPanel?.statusBounds, firstDeploymentPanel?.headerBounds),
|
||||
`Expected first deployment to show an FHD-safe recommended-formation status chip: ${JSON.stringify(firstBattleDeployment)}`
|
||||
`Expected first deployment to reuse the retained story-warmed map and show an FHD-safe recommended-formation status chip: ${JSON.stringify({
|
||||
firstBattleDeployment,
|
||||
warmup: firstBattleTransition.lastStory?.assetStreaming?.firstBattleWarmup,
|
||||
firstBattleMapHandoffRequests
|
||||
})}`
|
||||
);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-first-battle-deployment.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'first battle deployment');
|
||||
@@ -10394,6 +10407,16 @@ async function assertFreshStoryAssetStreaming(page, requestedResourceUrls, reque
|
||||
repeatedSortieBackgrounds.add(departureProbe.story.background);
|
||||
}
|
||||
}
|
||||
assert(
|
||||
departureProbe.story?.assetStreaming?.firstBattleWarmup?.enabled === true &&
|
||||
departureProbe.story.assetStreaming.firstBattleWarmup.pageIndex === 1 &&
|
||||
departureProbe.story.assetStreaming.firstBattleWarmup.active === true &&
|
||||
departureProbe.story.assetStreaming.firstBattleWarmup.mapTextureKey === 'battle-map-first' &&
|
||||
departureProbe.story.assetStreaming.firstBattleWarmup.mapTextureReady === true &&
|
||||
departureProbe.story.assetStreaming.firstBattleWarmup.mapTextureRetained === true &&
|
||||
departureProbe.story.assetStreaming.firstBattleWarmup.actionTextureKeysLoaded.length === 0,
|
||||
`Expected departure page 2 to warm and retain only the first battle map, without action sheets: ${JSON.stringify(departureProbe)}`
|
||||
);
|
||||
assert(
|
||||
repeatedSortieBackgrounds.size >= 2,
|
||||
`Expected repeated sortie story pages to distribute deterministic background variants: ${JSON.stringify([
|
||||
@@ -10404,10 +10427,17 @@ async function assertFreshStoryAssetStreaming(page, requestedResourceUrls, reque
|
||||
const actionRequests = requestedResourceUrls
|
||||
.slice(requestStartIndex)
|
||||
.filter((url) => /\/unit-[^/?]+-actions(?:-[^/?]+)?\.(?:png|webp)(?:[?#]|$)/i.test(decodeURIComponent(url)));
|
||||
const firstBattleMapWarmupRequests = requestedResourceUrls
|
||||
.slice(requestStartIndex)
|
||||
.filter((url) => /\/first-battle-map-v2(?:-[^/?]+)?\.webp(?:[?#]|$)/i.test(decodeURIComponent(url)));
|
||||
assert(
|
||||
actionRequests.length === 0,
|
||||
`Expected story streaming not to request battle action sheets: ${JSON.stringify(actionRequests)}`
|
||||
);
|
||||
assert(
|
||||
firstBattleMapWarmupRequests.length === 1,
|
||||
`Expected story streaming to request the first battle map exactly once on departure page 2: ${JSON.stringify(firstBattleMapWarmupRequests)}`
|
||||
);
|
||||
}
|
||||
|
||||
async function advanceOneStoryPage(page, expectedPageIndex) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Phaser from 'phaser';
|
||||
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map-v2.webp';
|
||||
import { soundDirector } from '../audio/SoundDirector';
|
||||
import { musicTracks, type MusicTrackKey } from '../audio/audioAssets';
|
||||
import {
|
||||
@@ -102,6 +103,7 @@ type StoryPageAssetPlan = {
|
||||
backgrounds: Array<{ key: string; url: string }>;
|
||||
portraits: PortraitAssetEntry[];
|
||||
unitTextureKeys: string[];
|
||||
retainedImages: Array<{ key: string; url: string }>;
|
||||
};
|
||||
|
||||
type StoryEnvironmentParticleView = {
|
||||
@@ -208,6 +210,7 @@ const firstBattleVictoryPageIds = new Set([
|
||||
'first-victory-next'
|
||||
]);
|
||||
const firstBattleId = 'first-battle-zhuo-commandery';
|
||||
const firstBattleMapTextureKey = 'battle-map-first';
|
||||
const firstBattleWarmupUnitTextureKeys = [
|
||||
'unit-rebel',
|
||||
'unit-rebel-archer',
|
||||
@@ -490,7 +493,21 @@ export class StoryScene extends Phaser.Scene {
|
||||
: [],
|
||||
currentUnitActionTexturesLoaded: page
|
||||
? this.storyPageAssetPlan(this.pageIndex).unitTextureKeys.filter((key) => this.textures.exists(`${key}-actions`))
|
||||
: []
|
||||
: [],
|
||||
firstBattleWarmup: {
|
||||
enabled: this.isFirstBattlePrelude(),
|
||||
pageIndex: Math.min(1, this.pages.length - 1),
|
||||
active: this.isFirstBattleWarmupPage(this.pageIndex),
|
||||
mapTextureKey: firstBattleMapTextureKey,
|
||||
mapTextureReady: this.isFirstBattlePrelude() && this.textures.exists(firstBattleMapTextureKey),
|
||||
mapTextureRetained:
|
||||
this.isFirstBattlePrelude() &&
|
||||
this.textures.exists(firstBattleMapTextureKey) &&
|
||||
!this.ownedStoryTextureKeys.has(firstBattleMapTextureKey),
|
||||
actionTextureKeysLoaded: firstBattleWarmupUnitTextureKeys
|
||||
.map((key) => `${key}-actions`)
|
||||
.filter((key) => this.textures.exists(key))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -705,8 +722,9 @@ export class StoryScene extends Phaser.Scene {
|
||||
};
|
||||
|
||||
const imageLoads = [
|
||||
...plan.backgrounds.map(({ key, url }) => ({ key, url })),
|
||||
...plan.portraits.map(({ textureKey: key, url }) => ({ key, url }))
|
||||
...plan.backgrounds.map(({ key, url }) => ({ key, url, retained: false })),
|
||||
...plan.portraits.map(({ textureKey: key, url }) => ({ key, url, retained: false })),
|
||||
...plan.retainedImages.map(({ key, url }) => ({ key, url, retained: true }))
|
||||
].filter(({ key }, index, entries) => {
|
||||
return !this.textures.exists(key) && entries.findIndex((entry) => entry.key === key) === index;
|
||||
});
|
||||
@@ -729,8 +747,10 @@ export class StoryScene extends Phaser.Scene {
|
||||
}
|
||||
loadUnitBases();
|
||||
});
|
||||
imageLoads.forEach(({ key, url }) => {
|
||||
this.ownedStoryTextureKeys.add(key);
|
||||
imageLoads.forEach(({ key, url, retained }) => {
|
||||
if (!retained) {
|
||||
this.ownedStoryTextureKeys.add(key);
|
||||
}
|
||||
this.load.image(key, url);
|
||||
});
|
||||
this.load.start();
|
||||
@@ -746,7 +766,7 @@ export class StoryScene extends Phaser.Scene {
|
||||
private storyPageAssetPlan(pageIndex: number): StoryPageAssetPlan {
|
||||
const page = this.pages[pageIndex];
|
||||
if (!page) {
|
||||
return { backgrounds: [], portraits: [], unitTextureKeys: [] };
|
||||
return { backgrounds: [], portraits: [], unitTextureKeys: [], retainedImages: [] };
|
||||
}
|
||||
|
||||
const backgroundKey = this.pageBackgroundAssetKey(page);
|
||||
@@ -774,7 +794,10 @@ export class StoryScene extends Phaser.Scene {
|
||||
unitTextureKeys: Array.from(new Set([
|
||||
...storyCutsceneActorTextureKeys(page.cutscene),
|
||||
...warmupUnitTextureKeys
|
||||
]))
|
||||
])),
|
||||
retainedImages: this.isFirstBattleWarmupPage(pageIndex)
|
||||
? [{ key: firstBattleMapTextureKey, url: firstBattleMapUrl }]
|
||||
: []
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user