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.
228 lines
7.7 KiB
228 lines
7.7 KiB
|
3 days ago
|
(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);
|