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.
 
 
 
 

227 lines
7.7 KiB

(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);