perf: stream story assets by page

This commit is contained in:
2026-07-18 11:06:56 +09:00
parent af2f5dbf06
commit 1df324ecd2
6 changed files with 941 additions and 182 deletions

View File

@@ -59,6 +59,7 @@ try {
const consoleMessages = [];
const pageErrors = [];
const requestFailures = [];
const requestedResourceUrls = [];
page.on('console', (message) => {
consoleMessages.push({ type: message.type(), text: message.text() });
});
@@ -72,6 +73,9 @@ try {
failure: request.failure()?.errorText ?? 'unknown'
});
});
page.on('request', (request) => {
requestedResourceUrls.push(request.url());
});
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => window.localStorage.clear());
@@ -88,6 +92,7 @@ try {
const emptySave = await readCampaignSave(page);
assert(!emptySave.current && !emptySave.slot1, `Expected first run without campaign save: ${JSON.stringify(emptySave)}`);
const freshStoryRequestStartIndex = requestedResourceUrls.length;
await clickLegacyUi(page, 962, 240);
await waitForStoryReady(page);
await setDebugQueryParam(page, 'debugOrderCommandReady', '1');
@@ -108,6 +113,7 @@ try {
);
await page.screenshot({ path: `${screenshotDir}/rc-new-game-story.png`, fullPage: true });
await assertCanvasPainted(page, 'new game story');
await assertFreshStoryAssetStreaming(page, requestedResourceUrls, freshStoryRequestStartIndex);
const firstBattleTransition = await advanceUntilBattle(page, 'first-battle-zhuo-commandery', {
startDeployment: false,
captureLastStory: true
@@ -7504,6 +7510,140 @@ async function waitForStoryReady(page) {
}, undefined, { timeout: 90000 });
}
async function assertFreshStoryAssetStreaming(page, requestedResourceUrls, requestStartIndex) {
for (let sampleIndex = 0; sampleIndex < 8; sampleIndex += 1) {
const probe = await readStoryAssetStreamingProbe(page);
assertStoryAssetStreamingProbe(probe, 0, 'fresh story page 1');
assert(
storyStreamingIndexes(probe.story.assetStreaming).every((pageIndex) => pageIndex <= 1),
`Expected fresh story loading to stay within pages 1-2 before input: ${JSON.stringify(probe)}`
);
await page.waitForTimeout(75);
}
let probe = await advanceOneStoryPage(page, 1);
assertStoryAssetStreamingProbe(probe, 1, 'fresh story page 2');
probe = await advanceOneStoryPage(page, 2);
assertStoryAssetStreamingProbe(probe, 2, 'fresh story page 3');
while ((probe.story?.cutsceneActors?.length ?? 0) === 0 && probe.story?.pageIndex < probe.story?.totalPages - 1) {
const targetPageIndex = probe.story.pageIndex + 1;
probe = await advanceOneStoryPage(page, targetPageIndex);
assertStoryAssetStreamingProbe(probe, targetPageIndex, `fresh story page ${targetPageIndex + 1}`);
}
assert(
(probe.story?.cutsceneActors?.length ?? 0) > 0 && probe.expectedUnitBaseTextureKeys.length > 0,
`Expected the fresh story regression to reach a cutscene that uses unit base sheets: ${JSON.stringify(probe)}`
);
assert(
sameJsonValue(
[...probe.story.assetStreaming.currentUnitBaseTexturesLoaded].sort(),
[...probe.expectedUnitBaseTextureKeys].sort()
),
`Expected every current cutscene unit base sheet to be loaded: ${JSON.stringify(probe)}`
);
const actionRequests = requestedResourceUrls
.slice(requestStartIndex)
.filter((url) => /\/unit-[^/?]+-actions(?:-[^/?]+)?\.png(?:[?#]|$)/i.test(decodeURIComponent(url)));
assert(
actionRequests.length === 0,
`Expected story streaming not to request battle action sheets: ${JSON.stringify(actionRequests)}`
);
}
async function advanceOneStoryPage(page, expectedPageIndex) {
const previousPageIndex = expectedPageIndex - 1;
const before = await readStoryAssetStreamingProbe(page);
assert(
before.story?.pageIndex === previousPageIndex,
`Expected story page ${previousPageIndex + 1} before advancing once: ${JSON.stringify(before)}`
);
await page.keyboard.press('Space');
await page.waitForFunction((targetPageIndex) => {
const scene = window.__HEROS_GAME__?.scene.getScene('StoryScene');
const story = scene?.getDebugState?.();
return (
story?.pageIndex === targetPageIndex &&
story?.assetStreaming?.currentPageReady === true &&
story?.backgroundReady === true &&
story?.activeTexture === story?.background &&
scene?.transitioning === false
);
}, expectedPageIndex, { timeout: 90000 });
const after = await readStoryAssetStreamingProbe(page);
assert(
after.story?.pageIndex === previousPageIndex + 1,
`Expected exactly one story page advance without skips: ${JSON.stringify(after)}`
);
return after;
}
async function readStoryAssetStreamingProbe(page) {
return page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('StoryScene');
const story = scene?.getDebugState?.() ?? null;
const plan = story && scene?.storyPageAssetPlan?.(story.pageIndex);
const textureKeys = scene?.textures?.list ? Object.keys(scene.textures.list) : [];
return {
viewport: {
width: window.innerWidth,
height: window.innerHeight,
devicePixelRatio: window.devicePixelRatio
},
story,
expectedUnitBaseTextureKeys: plan?.unitTextureKeys ?? [],
loadedUnitActionTextureKeys: textureKeys.filter((key) => key.endsWith('-actions')),
transitioning: scene?.transitioning ?? null
};
});
}
function assertStoryAssetStreamingProbe(probe, expectedPageIndex, label) {
const streaming = probe.story?.assetStreaming;
assert(
probe.viewport?.width === baselineViewport.width &&
probe.viewport?.height === baselineViewport.height &&
probe.viewport?.devicePixelRatio === desktopBrowserDeviceScaleFactor,
`Expected ${label} at the explicit 1920x1080 DPR1 baseline: ${JSON.stringify(probe.viewport)}`
);
assert(
probe.story?.ready === true &&
probe.story.pageIndex === expectedPageIndex &&
streaming?.currentPageReady === true &&
probe.story.backgroundReady === true &&
probe.story.activeTexture === probe.story.background,
`Expected ${label} to use its loaded deterministic background: ${JSON.stringify(probe)}`
);
assert(
Array.isArray(streaming?.failedAssetKeys) && streaming.failedAssetKeys.length === 0,
`Expected ${label} without story asset failures: ${JSON.stringify(probe)}`
);
assert(
storyStreamingIndexes(streaming).every((pageIndex) => pageIndex <= expectedPageIndex + 1),
`Expected ${label} streaming not to run beyond the current/next page window: ${JSON.stringify(probe)}`
);
assert(
probe.expectedUnitBaseTextureKeys.every((key) => streaming.currentUnitBaseTexturesLoaded.includes(key)),
`Expected ${label} current unit base sheets to be ready: ${JSON.stringify(probe)}`
);
assert(
streaming.currentUnitActionTexturesLoaded.length === 0 && probe.loadedUnitActionTextureKeys.length === 0,
`Expected ${label} to keep battle action sheets unloaded: ${JSON.stringify(probe)}`
);
}
function storyStreamingIndexes(streaming) {
return [
...(streaming?.readyPageIndexes ?? []),
...(streaming?.queuedPageIndexes ?? []),
...(Number.isInteger(streaming?.loadingPageIndex) ? [streaming.loadingPageIndex] : [])
];
}
async function waitForStoryOrEnding(page) {
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];