import { game } from 'cc'; import { MemoryProfile } from './MemoryProfile'; import { MemoryDiag } from './MemoryDiag'; import { ThermalProfile } from './ThermalProfile'; import { DeviceProfile, type QualityTier } from '../battle/view/DeviceProfile'; import { ResourcePhase } from '../../mx/module/res/ResourcePhase'; import { createMembershipSet, membershipAdd, membershipHas, type MembershipSet, } from '../../mx/tools/MembershipSet'; /** 打点 schema 版本(PF-VER);v3 起含战斗帧率分档 30/40/50 与 BTL-CFG/BTL-SESS */ export const PERF_REPORT_SCHEMA = 3; /** 前台加载耗时上限(毫秒);超出记 _bg */ const LOAD_DURATION_CAP_MS = 60000; /** * 性能辅助打点(非运营口径)。 * * 约定: * - 一律 `PF-{域}-{标签}-{详情}`,仅走 gg.sdk.reportPerf(不上 Gravity userAction) * - 域标签禁用 LOAD(含 AD 子串);加载域用 LD * - 详情禁止 `-AD-`/「广告」及机型原文(避免 Gravity 误判) * - 加载耗时 v2:`{N}ms` 或 `{N}ms_bg`(100ms 粒度,前台计时,超 cap 或墙钟异常标 _bg) * - v3:BOOT-TIER 含 bt{战斗目标帧率};BTL-CFG/BTL-FPS/BTL-SESS 含 q/tgt/gap(捕获异常不上 PF/ZYSDK) * - 仅关键节点、带冷却/会话去重,禁止高频 */ export class PerfReport { private static _bootTierSent = false; private static _loadSdkSent = false; private static _loadResSent = false; private static _schemaSent = false; private static _lastMsByKey: Record = {}; private static _memDowngradeMax = 0; /** 本战斗是否已上报 BTL-CFG */ private static _battleCfgSent = false; /** 本会话已上报的 LD-BTL 章节(每章 1 次) */ private static _reportedBattleChapters: MembershipSet = createMembershipSet(); /** schema 版本(每会话 1 次,便于后台筛选 v2 数据) */ static reportSchemaVersion(): void { if (this._schemaSent) return; this._schemaSent = true; this._emit('VER', String(PERF_REPORT_SCHEMA), 0); } /** 启动完成:设备分级快照(每会话 1 次) */ static reportBootTier(): void { if (this._bootTierSent) return; this._bootTierSent = true; this.reportSchemaVersion(); MemoryProfile.resolve(); ThermalProfile.resolve(); const mem = MemoryProfile.tier; const thrm = MemoryProfile.tier; const qual = DeviceProfile.tier; const btFps = ThermalProfile.current.battleFrameRate; const day = gg.data?.LoginDays ?? 0; const ch = gg.data?.doc?.rankId ?? 0; this._emit('BOOT-TIER', `m${mem}_t${thrm}_q${this._qualCode(qual)}_bt${btFps}_d${day}_c${ch}`); } /** SDK 初始化完成耗时(每会话 1 次) */ static reportLoadSdk(foregroundMs: number, wallMs?: number): void { if (this._loadSdkSent) return; this._loadSdkSent = true; this._emit('LD-SDK', this._loadDurationDetail(foregroundMs, wallMs)); } /** Boot 资源包加载完成(每会话 1 次) */ static reportLoadRes(foregroundMs: number, wallMs?: number): void { if (this._loadResSent) return; this._loadResSent = true; this._emit('LD-RES', this._loadDurationDetail(foregroundMs, wallMs)); } /** 进入可交互大厅(战斗退出 / 冷启动进大厅) */ static reportLoadHall( foregroundMs: number, wallMs?: number, reason?: 'battle_exit' | 'cold_boot', ): void { const dur = this._loadDurationDetail(foregroundMs, wallMs); const detail = reason ? `${dur}_${reason}` : dur; this._emit('LD-HALL', detail, 30000); } /** 战斗场景可交互(map 加载完成);每章节每会话 1 次 */ static reportLoadBattle(chapterId: number, foregroundMs: number, wallMs?: number): void { const ch = Math.max(1, Math.floor(chapterId)); if (membershipHas(this._reportedBattleChapters, ch)) return; membershipAdd(this._reportedBattleChapters, ch); this._emit( 'LD-BTL', `ch${ch}_${this._loadDurationDetail(foregroundMs, wallMs)}`, 10000, ); } /** 大厅 silent 预载完成 */ static reportPreloadHall(foregroundMs: number, wallMs?: number): void { const snap = MemoryDiag.getStats(); this._emit( 'PRE-HALL', `${this._loadDurationDetail(foregroundMs, wallMs)}_${this._snap(snap)}`, 120000, ); } /** 章节背景预载完成 */ static reportPreloadChapter( chapterId: number, foregroundMs: number, resCount: number, wallMs?: number, ): void { const snap = MemoryDiag.getStats(); const ch = Math.max(1, Math.floor(chapterId)); this._emit( 'PRE-CH', `ch${ch}_${this._loadDurationDetail(foregroundMs, wallMs)}_n${resCount}_${this._snap(snap)}`, 60000, ); } /** 平台 onMemoryWarning(不含机型字符串,避免 AD 误判) */ static reportMemWarn(warnCountInPhase: number, downgradeLevel: number): void { const ph = this._phaseCode(); this._emit( 'MEM-WARN', `ph${ph}_w${warnCountInPhase}_lv${downgradeLevel}_${this._snap(MemoryDiag.getStats())}`, 600000, ); } /** 内存降档生效(每档每会话最多 1 次) */ static reportMemDowngrade(level: number): void { if (level <= this._memDowngradeMax) return; this._memDowngradeMax = level; const ph = this._phaseCode(); this._emit('MEM-DG', `lv${level}_ph${ph}`, 0); } /** ResourcePhase / 帧率策略变更 */ static reportThermalFps(phase: ResourcePhase, targetFps: number, stressLevel: number): void { const q = this._qualCode(DeviceProfile.tier); this._emit( 'THRM-FPS', `ph${this._resourcePhaseCode(phase)}_q${q}_fps${targetFps}_s${stressLevel}`, 30000, ); } /** * 战斗开局性能策略快照(每局 1 次):画质档、目标帧率、低档特效开关、ECS 分片。 */ static reportBattleRuntimeConfig(): void { if (this._battleCfgSent) return; this._battleCfgSent = true; ThermalProfile.resolve(); const th = ThermalProfile.current; const q = this._qualCode(DeviceProfile.tier); const trail = DeviceProfile.enableBoardPlaceTrail ? 1 : 0; const spine = DeviceProfile.enableGridUpgradeSpine ? 1 : 0; const floater = DeviceProfile.enableLevelUpTextFloater ? 1 : 0; const slice = DeviceProfile.battleTickSlice; this._emit( 'BTL-CFG', `q${q}_tgt${th.battleFrameRate}_sus${th.battleSustainFrameRate}_tr${trail}_sp${spine}_fl${floater}_sl${slice}`, 0, ); } /** 离开战斗时复位 BTL-CFG 门闩 */ static resetBattlePerfSession(): void { this._battleCfgSent = false; } /** 战斗结束回大厅:便于按会话聚合 */ static reportSessionEnd(reason: 'hall' | 'battle_exit'): void { this._emit( 'SESS-END', `${reason}_${this._snap(MemoryDiag.getStats())}`, 60000, ); } /** 战斗 FPS 窗口均值(相对目标帧率 gap) */ static reportBattleFps(avgFps: number, p10Fps?: number, targetFps?: number): void { ThermalProfile.resolve(); const tgt = Math.max(1, Math.floor(targetFps ?? ThermalProfile.current.battleFrameRate)); const avg = Math.round(avgFps); const gap = avg - tgt; const p10 = p10Fps != null ? `_p10${Math.round(p10Fps)}` : ''; const q = this._qualCode(DeviceProfile.tier); this._emit('BTL-FPS', `q${q}_tgt${tgt}_avg${avg}${p10}_gap${gap}`, 45000); } /** * 战斗结束 FPS 汇总(每局 1 次)。 * @param avgFps 全程均值 * @param p10Fps 全程 p10 * @param targetFps 目标帧率 * @param sampleFrames 采样帧数 */ static reportBattleSessionFps(avgFps: number, p10Fps: number, targetFps: number, sampleFrames: number): void { const tgt = Math.max(1, Math.floor(targetFps)); const avg = Math.round(avgFps); const p10 = Math.round(p10Fps); const gap = avg - tgt; const q = this._qualCode(DeviceProfile.tier); const n = Math.max(0, Math.floor(sampleFrames)); this._emit('BTL-SESS', `q${q}_tgt${tgt}_avg${avg}_p10${p10}_gap${gap}_n${n}`, 0); } /** 已废弃:捕获异常仅走 ClientErrorReporter → 平台日志,不上 ZYSDK */ static reportClientError(_capture: string, _detail: string): void { } /** 资源加载失败(PF-LOAD-FAIL) */ static reportLoadFail(scene: string, failCount: number): void { this._emit('资源加载失败', `${scene}_n${failCount}`, 30000); } /** recoverHome 原因(PF-RECOVER) */ static reportRecover(reason: string): void { this._emit('RECOVER', reason.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 48), 10000); } /** 版本更新下载结果(PF-LD-UPDATE) */ static reportLoadUpdate(outcome: 'ready' | 'fail' | 'retry', detail = ''): void { const d = detail ? `_${detail.slice(0, 24)}` : ''; this._emit('LD-UPDATE', `${outcome}${d}`, 60000); } /** 阶段内存快照(PF-MEM-SNAP) */ static reportMemSnap(phase: string): void { this._emit('MEM-SNAP', `${phase}_${this._snap(MemoryDiag.getStats())}`, 120000); } private static _emit(tag: string, detail: string, cooldownMs = 0): void { const msg = `PF-${tag}-${detail}`; if (PerfReport._looksLikeOpsAd(msg)) { console.error('[PerfReport] 拒绝上报:疑似运营广告口径', msg); return; } if (cooldownMs > 0) { const now = Date.now(); const last = this._lastMsByKey[tag] ?? 0; if (now - last < cooldownMs) return; this._lastMsByKey[tag] = now; } gg.sdk.reportPerf?.(msg); } /** * 加载耗时详情:100ms 粒度毫秒字符串。 * `_bg`:墙钟远超前台累计(典型切后台挂起)或前台计时触顶 cap。 */ private static _loadDurationDetail(foregroundMs: number, wallMs?: number): string { const fg = Math.max(0, Math.round(foregroundMs / 100) * 100); const wall = wallMs ?? fg; const cappedFg = Math.min(fg, LOAD_DURATION_CAP_MS); const suspended = wall > LOAD_DURATION_CAP_MS || wall > cappedFg + 3000 || fg > LOAD_DURATION_CAP_MS; return suspended ? `${cappedFg}ms_bg` : `${cappedFg}ms`; } private static _snap(s: { bundleCount: number; pkgCount: number; assetCount: number; preloadedCount: number; }): string { return `b${s.bundleCount}p${s.pkgCount}a${s.assetCount}r${s.preloadedCount}`; } private static _qualCode(t: QualityTier): string { if (t === 'low') return 'ql'; if (t === 'high') return 'qh'; return 'qm'; } private static _phaseCode(): number { switch (MemoryDiag.getPhase()) { case '启动': return 0; case '大厅': return 1; case '大厅预载': return 2; case '战斗': return 3; case '战斗退出': return 4; default: return 9; } } private static _resourcePhaseCode(phase: ResourcePhase): number { switch (phase) { case ResourcePhase.Boot: return 0; case ResourcePhase.Hall: return 1; case ResourcePhase.HallPreload: return 2; case ResourcePhase.Battle: return 3; case ResourcePhase.BattleExit: return 4; default: return 9; } } /** 供 ThermalGovernor 读取当前目标帧率 */ static getCurrentFrameRate(): number { return Number(game.frameRate); } /** 与 GEMgr 一致:仅拦截运营广告格式,不用 indexOf('AD')(会误伤 LOAD/Android 等) */ static _looksLikeOpsAd(msg: string): boolean { return /-AD-/.test(msg) || msg.includes('广告'); } }