You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/game/PerfReport.ts

320 lines
12 KiB

3 weeks ago
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<string, number> = {};
private static _memDowngradeMax = 0;
/** 本战斗是否已上报 BTL-CFG */
private static _battleCfgSent = false;
/** 本会话已上报的 LD-BTL 章节(每章 1 次) */
private static _reportedBattleChapters: MembershipSet<number> = createMembershipSet<number>();
/** 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('广告');
}
}