(function (global) { 'use strict'; /** * 游戏侧性能剖析(零侵入,按需采样): * - 函数耗时 Top(Error.stack 采样聚合) * - 慢帧堆栈 Top(帧耗时超阈值时抓栈) * - 真实 FPS / 内存 */ function StackProfiler() { this.enabled = false; this.intervalMs = 8; this._timer = 0; this._fnStats = {}; // name -> {count, ms} this._slowFrames = []; this._maxSlow = 20; this._lastTick = 0; this._tickHooked = false; this._origTick = null; this._cc = null; } StackProfiler.prototype.bindEngine = function (cc) { this._cc = cc; this._hookTick(); }; StackProfiler.prototype._hookTick = function () { var self = this; var cc = this._cc; if (!cc || !cc.director || this._tickHooked) return; var dir = cc.director; var proto = Object.getPrototypeOf(dir) || dir; if (typeof proto.tick !== 'function') return; this._origTick = proto.tick; this._tickHooked = true; proto.tick = function () { if (!self.enabled) { return self._origTick.apply(this, arguments); } var t0 = performance.now(); var ret = self._origTick.apply(this, arguments); var dt = performance.now() - t0; self._noteFrame(dt); return ret; }; }; StackProfiler.prototype._noteFrame = function (ms) { if (ms < 20) return; var stack = ''; try { stack = new Error('pf').stack || ''; } catch (e) { /* */ } this._slowFrames.push({ t: Date.now(), ms: Math.round(ms * 100) / 100, stack: this._cleanStack(stack), }); if (this._slowFrames.length > this._maxSlow * 3) { this._slowFrames.sort(function (a, b) { return b.ms - a.ms; }); this._slowFrames.length = this._maxSlow; } }; StackProfiler.prototype._cleanStack = function (stack) { if (!stack) return ''; var lines = String(stack).split('\n'); var out = []; for (var i = 0; i < lines.length && out.length < 12; i++) { var s = lines[i].trim(); if (!s || /^error/i.test(s) || s.indexOf('Error') === 0 && s.indexOf('at ') < 0) continue; if (/StackProfiler|_cleanStack|_noteFrame|agent-profiler|workbench/.test(s)) continue; out.push(s); } return out.join('\n'); }; StackProfiler.prototype.start = function () { if (this.enabled) return; this.enabled = true; this._fnStats = {}; this._slowFrames = []; var self = this; this._timer = setInterval(function () { self._sampleStack(); }, this.intervalMs); }; StackProfiler.prototype.stop = function () { this.enabled = false; if (this._timer) { clearInterval(this._timer); this._timer = 0; } }; StackProfiler.prototype.clear = function () { this._fnStats = {}; this._slowFrames = []; }; /** 调用栈采样:每 tick 记一帧到最热函数 */ StackProfiler.prototype._sampleStack = function () { if (!this.enabled) return; var stack = ''; try { stack = new Error('s').stack || ''; } catch (e) { return; } var lines = String(stack).split('\n'); var name = ''; for (var i = 1; i < lines.length; i++) { var s = lines[i].trim(); if (!s) continue; if (/StackProfiler|_sampleStack|agent-profiler|workbench|setInterval/.test(s)) continue; // at fnName (url:line:col) 或 at url:line:col var m = s.match(/at\s+([^\s(]+)/); name = (m && m[1]) || s; break; } if (!name) return; if (!this._fnStats[name]) this._fnStats[name] = { count: 0, ms: 0 }; this._fnStats[name].count += 1; this._fnStats[name].ms += this.intervalMs; }; StackProfiler.prototype.topFunctions = function (limit) { var arr = []; for (var k in this._fnStats) { if (!Object.prototype.hasOwnProperty.call(this._fnStats, k)) continue; arr.push({ name: k, count: this._fnStats[k].count, ms: Math.round(this._fnStats[k].ms * 10) / 10, }); } arr.sort(function (a, b) { return b.ms - a.ms; }); return arr.slice(0, limit || 20); }; StackProfiler.prototype.topSlowFrames = function (limit) { var arr = this._slowFrames.slice(); arr.sort(function (a, b) { return b.ms - a.ms; }); return arr.slice(0, limit || 10); }; /** 当前真实帧率 / 内存 */ StackProfiler.prototype.liveStats = function () { var cc = this._cc; var fps = 0; var tex = 0; var buf = 0; try { var st = cc && cc.profiler && cc.profiler.stats; if (st) { if (st.fps && st.fps.counter) fps = st.fps.counter.value || 0; if (st.textureMemory && st.textureMemory.counter) tex = st.textureMemory.counter.value || 0; if (st.bufferMemory && st.bufferMemory.counter) buf = st.bufferMemory.counter.value || 0; } } catch (e) { /* */ } if (!fps && cc && cc.director && typeof cc.director.getTotalFrames === 'function') { var now = performance.now(); var frames = cc.director.getTotalFrames(); if (this._fpsLastFrames != null && this._fpsLastTime) { var dt = now - this._fpsLastTime; if (dt > 0) fps = ((frames - this._fpsLastFrames) * 1000) / dt; } this._fpsLastFrames = frames; this._fpsLastTime = now; } var heap = 0; try { if (performance.memory && performance.memory.usedJSHeapSize) { heap = performance.memory.usedJSHeapSize / (1024 * 1024); } } catch (e2) { /* */ } return { fps: fps, textureMemory: tex, bufferMemory: buf, jsHeap: heap, totalMemory: tex + buf + heap, }; }; global.PWStackProfiler = StackProfiler; // —— 挂到 Agent —— if (global.PWAgent) { var Agent = global.PWAgent; if (!Agent.prototype._profiler) { Agent.prototype._ensureProfiler = function () { if (!this.__stackProfiler) { this.__stackProfiler = new StackProfiler(); if (this.cc) this.__stackProfiler.bindEngine(this.cc); } else if (this.cc && !this.__stackProfiler._cc) { this.__stackProfiler.bindEngine(this.cc); } return this.__stackProfiler; }; Agent.prototype._profStart = function () { var p = this._ensureProfiler(); p.start(); return true; }; Agent.prototype._profStop = function () { if (this.__stackProfiler) this.__stackProfiler.stop(); return true; }; Agent.prototype._profClear = function () { if (this.__stackProfiler) this.__stackProfiler.clear(); return true; }; Agent.prototype._profReport = function () { var p = this._ensureProfiler(); return { enabled: p.enabled, functions: p.topFunctions(20), slowFrames: p.topSlowFrames(10), live: p.liveStats(), }; }; } } })(window);