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.
319 lines
12 KiB
319 lines
12 KiB
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('广告');
|
|
}
|
|
}
|
|
|