M5(T-M5-07): 冷启动优化——远程配置与内置资源准备并行化 + 耗时打点

- FETCH_REMOTE_CONFIG(网络 1-5s) 与 prepareBuiltin(本地拷贝解压 0.2-0.8s) 无数据
  依赖,改为同时进行;总耗时由 max(远程,内置) 决定而非求和(节省 ~内置耗时)
- 各阶段加 [startup] 耗时打点(fetch_remote_config/prepare_builtin/update_game/total),
  便于真机量化冷启动到首帧基线
- 行为变化注记:showmessage 阻断现发生在 prepareBuiltin 之后(并行所致),阻断时多做
  一次幂等的内置准备;属罕见运营公告路径,无副作用,可接受

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-26 07:13:44 +08:00
co-authored by Claude Opus 4.8
parent 0a372f1339
commit c00f639ef7
@@ -58,8 +58,14 @@ export class StartupOrchestrator {
} }
} }
/** 冷启动耗时打点(T-M5-07 可观测,单位 ms)。 */
private mark(label: string, since: number): void {
StartupOrchestrator.log.i(`[startup] ${label} +${Date.now() - since}ms`);
}
/** 执行启动流程。 */ /** 执行启动流程。 */
async run(): Promise<StartupResult> { async run(): Promise<StartupResult> {
const t0: number = Date.now();
this.stage(StartupStage.INIT, 0); this.stage(StartupStage.INIT, 0);
// 1) 本地配置 // 1) 本地配置
@@ -80,37 +86,53 @@ export class StartupOrchestrator {
if (local.weburl !== '') { if (local.weburl !== '') {
const entryUrl: string = `http://${local.weburl.replace(/-/g, '/')}?Launchtype=0`; const entryUrl: string = `http://${local.weburl.replace(/-/g, '/')}?Launchtype=0`;
this.stage(StartupStage.ENTER_HALL, 100); this.stage(StartupStage.ENTER_HALL, 100);
this.mark('total(weburl)', t0);
return { enter: true, entryUrl, message: '' }; return { enter: true, entryUrl, message: '' };
} }
// 3) 远程配置 + 4) 版本决策 // T-M5-07 并行化:远程配置(网络 IO,1-5s)与内置资源准备(本地拷贝解压,0.2-0.8s)
let decision: VersionDecision | undefined = undefined; // 无数据依赖——prepareBuiltin 只依赖本地 config,不依赖远程 decision。两者同时进行,
try { // 总耗时由 max(远程, 内置) 决定而非求和。
this.stage(StartupStage.FETCH_REMOTE_CONFIG, 20); this.stage(StartupStage.FETCH_REMOTE_CONFIG, 20);
decision = await config.resolve(); const tRemote: number = Date.now();
this.stage(StartupStage.RESOLVE_VERSION, 35); const remotePromise: Promise<VersionDecision | undefined> =
if (decision.showmessage !== '') { (async (): Promise<VersionDecision | undefined> => {
StartupOrchestrator.log.w(`blocked by showmessage: ${decision.showmessage}`); try {
return { enter: false, entryUrl: '', message: decision.showmessage }; const d: VersionDecision = await config.resolve();
} this.mark('fetch_remote_config', tRemote);
} catch (e) { return d;
const err = e as Error; } catch (e) {
StartupOrchestrator.log.w(`remote config failed, fallback to local: ${err.message}`); StartupOrchestrator.log.w(`remote config failed, fallback to local: ${(e as Error).message}`);
} return undefined;
}
})();
// 5) 资源准备 // 内置资源准备(与上面远程请求并行推进)
this.stage(StartupStage.PREPARE_RESOURCE, 45); this.stage(StartupStage.PREPARE_RESOURCE, 35);
const tBuiltin: number = Date.now();
if (!resource.isPrepared()) { if (!resource.isPrepared()) {
await resource.prepareBuiltin(); await resource.prepareBuiltin();
} }
this.mark('prepare_builtin', tBuiltin);
// 汇合:取远程版本决策
const decision: VersionDecision | undefined = await remotePromise;
this.stage(StartupStage.RESOLVE_VERSION, 55);
if (decision !== undefined && decision.showmessage !== '') {
StartupOrchestrator.log.w(`blocked by showmessage: ${decision.showmessage}`);
return { enter: false, entryUrl: '', message: decision.showmessage };
}
// 5) 版本更新(依赖内置资源已就绪 + 远程决策)
const localVer: number = resource.localVersion(); const localVer: number = resource.localVersion();
if (decision !== undefined && decision.gameVersion > localVer && decision.gameDownload !== '') { if (decision !== undefined && decision.gameVersion > localVer && decision.gameDownload !== '') {
StartupOrchestrator.log.i(`update needed: local=${localVer} remote=${decision.gameVersion}`); StartupOrchestrator.log.i(`update needed: local=${localVer} remote=${decision.gameVersion}`);
const tUpdate: number = Date.now();
try { try {
await resource.updateGame(decision.gameDownload, (p: number) => this.stage(StartupStage.PREPARE_RESOURCE, 45 + Math.floor(p * 0.4))); await resource.updateGame(decision.gameDownload, (p: number) => this.stage(StartupStage.PREPARE_RESOURCE, 55 + Math.floor(p * 0.3)));
this.mark('update_game', tUpdate);
} catch (e) { } catch (e) {
const err = e as Error; StartupOrchestrator.log.w(`update failed, keep local: ${(e as Error).message}`);
StartupOrchestrator.log.w(`update failed, keep local: ${err.message}`);
} }
} }
await resource.persistPaths(); await resource.persistPaths();
@@ -121,12 +143,13 @@ export class StartupOrchestrator {
} }
// 6) 注入 app_data.js(务必在进大厅之前)。大厅 launchtype='0' // 6) 注入 app_data.js(务必在进大厅之前)。大厅 launchtype='0'
this.stage(StartupStage.INJECT_APP_DATA, 90); this.stage(StartupStage.INJECT_APP_DATA, 92);
const values: AppDataValues = AppDataInjector.buildValues(local, `${resource.localVersion()}`, '0'); const values: AppDataValues = AppDataInjector.buildValues(local, `${resource.localVersion()}`, '0');
AppDataInjector.inject(resource.paths().gameDir, values); AppDataInjector.inject(resource.paths().gameDir, values);
// 7) 进大厅 // 7) 进大厅
this.stage(StartupStage.ENTER_HALL, 100); this.stage(StartupStage.ENTER_HALL, 100);
this.mark('total', t0);
return { enter: true, entryUrl: `${resource.paths().indexUrl}?Launchtype=0`, message: '' }; return { enter: true, entryUrl: `${resource.paths().indexUrl}?Launchtype=0`, message: '' };
} }
} }