(function (global) {
'use strict';
/**
* 性能面板(壳侧):
* 1) 帧率 / 内存曲线
* 2) 函数耗时 Top20
* 3) 慢帧堆栈 Top10
* 4) 内存资源 Top20(可复制名称 / UUID,可预览)
* @constructor
*/
function PerfPanel() {
this.el = null;
this._bridge = null;
this._chartFps = null;
this._chartMem = null;
this._data = null;
this._paused = false;
this._profiling = true;
this._pollTimer = 0;
this._assetRows = [];
}
PerfPanel.prototype.setBridge = function (bridge) {
this._bridge = bridge;
};
PerfPanel.prototype.mount = function () {
var self = this;
var root = document.createElement('div');
root.className = 'pw-panel-inner pw-perf';
root.style.height = '100%';
root.style.display = 'flex';
root.style.flexDirection = 'column';
root.innerHTML =
'
' +
'' +
'' +
'' +
'' +
'' +
'' +
'
' +
'' +
'
' +
'
帧率 FPS
' +
'
总内存 (MB)
' +
'
' +
'
' +
'
' +
'
函数耗时 Top20
' +
'
' +
'
' +
'
' +
'
慢帧堆栈 Top10
' +
'
' +
'
' +
'
' +
'
' +
'
内存资源 Top20
' +
'
' +
'
' +
'
';
this.el = root;
this._chartFps = new global.PWRingChart(root.querySelector('#pw-ch-fps'));
this._chartFps.color = '#3dd68c';
this._chartFps.fill = 'rgba(61,218,140,0.12)';
this._chartMem = new global.PWRingChart(root.querySelector('#pw-ch-mem'));
this._chartMem.color = '#5b9fd4';
this._chartMem.fill = 'rgba(91,159,212,0.12)';
root.querySelector('#pw-perf-pause').addEventListener('click', function () {
self._paused = !self._paused;
this.textContent = self._paused ? '继续' : '暂停';
this.classList.toggle('active', self._paused);
});
root.querySelector('#pw-perf-clear').addEventListener('click', function () {
if (!self._bridge) return;
self._bridge.call('perf.clear');
self._bridge.call('prof.clear');
self._fnCache = [];
self._slowCache = [];
self._renderReports({ functions: [], slowFrames: [] });
});
root.querySelector('#pw-perf-mark').addEventListener('click', function () {
if (!self._bridge) return;
var t = root.querySelector('#pw-perf-mark-text');
self._bridge.call('perf.mark', { text: (t && t.value) || '' });
if (t) t.value = '';
});
root.querySelector('#pw-perf-csv').addEventListener('click', function () {
if (!self._bridge) return;
self._bridge.call('perf.get').then(function (d) {
var csv = 't,fps,frame,jsHeap,totalMemory\n';
if (d && d.times) {
for (var i = 0; i < d.times.length; i++) {
var s = d.series || {};
csv += d.times[i] + ',' +
((s.fps && s.fps[i]) || 0) + ',' +
((s.frame && s.frame[i]) || 0) + ',' +
((s.jsHeap && s.jsHeap[i]) || 0) + ',' +
((s.textureMemory && s.textureMemory[i] || 0) + (s.bufferMemory && s.bufferMemory[i] || 0) + (s.jsHeap && s.jsHeap[i] || 0)) + '\n';
}
}
var blob = new Blob([csv], { type: 'text/csv' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'perf.csv';
a.click();
});
});
root.querySelector('#pw-perf-prof').addEventListener('click', function () {
self._profiling = !self._profiling;
this.classList.toggle('active', self._profiling);
this.textContent = self._profiling ? '剖析中' : '已停止';
if (!self._bridge) return;
self._bridge.call(self._profiling ? 'prof.start' : 'prof.stop');
});
this._startPoll();
return root;
};
PerfPanel.prototype._startPoll = function () {
var self = this;
if (this._pollTimer) clearInterval(this._pollTimer);
this._pollTimer = setInterval(function () {
if (self._paused || !self._bridge) return;
self._bridge.call('perf.get').then(function (d) {
self._data = d;
self._drawCurves(d);
}).catch(function () { /* */ });
self._bridge.call('prof.report').then(function (r) {
self._renderReports(r);
}).catch(function () { /* */ });
}, 500);
};
PerfPanel.prototype._drawCurves = function (d) {
if (!d) return;
var fpsArr = (d.series && d.series.fps) || [];
var memArr = [];
var ts = d.times || [];
var s = d.series || {};
for (var i = 0; i < ts.length; i++) {
var t = (s.textureMemory && s.textureMemory[i]) || 0;
var b = (s.bufferMemory && s.bufferMemory[i]) || 0;
var h = (s.jsHeap && s.jsHeap[i]) || 0;
memArr.push(Math.round((t + b + h) * 10) / 10);
}
if (this._chartFps) this._chartFps.draw(fpsArr, [], ts);
if (this._chartMem) this._chartMem.draw(memArr, [], ts);
};
PerfPanel.prototype._renderReports = function (r) {
if (!this.el || !r) return;
var fnEl = this.el.querySelector('#pw-fn-list');
var slowEl = this.el.querySelector('#pw-slow-list');
var fnC = this.el.querySelector('#pw-fn-count');
var slowC = this.el.querySelector('#pw-slow-count');
var fns = r.functions || [];
if (fnC) fnC.textContent = fns.length ? (fns.length + ' 项 · 单位 ms') : '';
if (fnEl) {
if (!fns.length) {
fnEl.innerHTML = '点击「剖析中」开始采样(约 8ms 一帧栈)
';
} else {
var html = ['函数次数估算ms
'];
for (var i = 0; i < fns.length; i++) {
html.push(
'' +
'' + escapeHtml(shorten(fns[i].name, 42)) + '' +
'' + fns[i].count + '' +
'' + fns[i].ms + '' +
'
'
);
}
fnEl.innerHTML = html.join('');
}
}
var slows = r.slowFrames || [];
if (slowC) slowC.textContent = slows.length ? (slows.length + ' 次 >20ms') : '';
if (slowEl) {
if (!slows.length) {
slowEl.innerHTML = '暂无慢帧(阈值 20ms)
';
} else {
var sh = ['耗时堆栈
'];
for (var j = 0; j < slows.length; j++) {
var st = escapeHtml(String(slows[j].stack || '').split('\n').slice(0, 3).join(' | '));
sh.push(
'' +
'' + slows[j].ms + 'ms' +
'' + st + '' +
'
'
);
}
slowEl.innerHTML = sh.join('');
}
}
};
/** 内存资源 Top20 */
PerfPanel.prototype.applyAssets = function (data) {
this._assetRows = (data && data.rows) || [];
this._renderAssets();
};
PerfPanel.prototype._renderAssets = function () {
var el = this.el && this.el.querySelector('#pw-res-list');
var cnt = this.el && this.el.querySelector('#pw-res-count');
if (!el) return;
var rows = this._assetRows.slice();
rows.sort(function (a, b) { return (b.mem || 0) - (a.mem || 0); });
rows = rows.slice(0, 20);
if (cnt) cnt.textContent = rows.length + ' 项';
if (!rows.length) {
el.innerHTML = '打开「资源」面板或稍候自动加载
';
return;
}
var html = ['名称 / UUID类型内存操作
'];
for (var i = 0; i < rows.length; i++) {
var r = rows[i];
html.push(
'' +
'' + escapeHtml(r.name) + '
' + escapeHtml(r.uuid) + '' +
'' + escapeHtml(r.type) + '' +
'' + fmtMB(r.mem) + '' +
'' +
'' +
'' +
'' +
'
'
);
}
el.innerHTML = html.join('');
var self = this;
el.addEventListener('click', function (ev) {
var btn = ev.target.closest ? ev.target.closest('button') : null;
if (!btn) return;
var item = btn.closest('.pw-res-item');
if (!item) return;
var uuid = item.getAttribute('data-uuid') || '';
var name = (item.querySelector('b') && item.querySelector('b').textContent) || '';
if (btn.getAttribute('data-copy') === 'name') copyText(name);
else if (btn.getAttribute('data-copy') === 'uuid') copyText(uuid);
else if (btn.getAttribute('data-preview') && self._bridge) {
self._bridge.call('assets.preview', { uuid: uuid }).then(function (p) {
var box = document.getElementById('pw-asset-preview');
if (!box) return;
if (p && p.kind === 'image' && p.url) {
box.innerHTML = '
';
} else if (p && p.kind === 'url' && p.url) {
box.innerHTML = '
';
} else {
box.innerHTML = '无预览';
}
}).catch(function () { /* */ });
}
});
};
function fmtMB(mb) {
var n = Number(mb) || 0;
if (n <= 0) return '0';
if (n < 10) return (Math.round(n * 100) / 100) + 'MB';
return (Math.round(n * 10) / 10) + 'MB';
}
function shorten(s, n) {
s = String(s || '');
return s.length > n ? s.slice(0, n - 1) + '…' : s;
}
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&').replace(//g, '>');
}
function escapeAttr(s) {
return escapeHtml(s).replace(/"/g, '"');
}
function copyText(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(String(text));
return;
}
} catch (e) { /* */ }
var ta = document.createElement('textarea');
ta.value = String(text);
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e2) { /* */ }
document.body.removeChild(ta);
}
global.PWPerfPanel = PerfPanel;
})(window);