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) {
|
||||
|
||||
Reference in New Issue
Block a user