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.
 
 
 
 

301 lines
13 KiB

(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 =
'<div class="pw-panel-toolbar">' +
'<button type="button" id="pw-perf-prof" class="pw-btn active">剖析中</button>' +
'<button type="button" id="pw-perf-pause" class="pw-btn">暂停</button>' +
'<button type="button" id="pw-perf-clear" class="pw-btn">清空</button>' +
'<button type="button" id="pw-perf-mark" class="pw-btn">打标记</button>' +
'<input id="pw-perf-mark-text" class="pw-input" placeholder="标记" style="width:72px" />' +
'<button type="button" id="pw-perf-csv" class="pw-btn">导出CSV</button>' +
'</div>' +
'<div class="pw-perf-body">' +
'<div class="pw-perf-row pw-perf-charts">' +
'<div class="pw-chart-card"><h4>帧率 FPS</h4><canvas id="pw-ch-fps"></canvas></div>' +
'<div class="pw-chart-card"><h4>总内存 (MB)</h4><canvas id="pw-ch-mem"></canvas></div>' +
'</div>' +
'<div class="pw-perf-row">' +
'<div class="pw-perf-block">' +
'<h4>函数耗时 Top20 <span class="pw-muted" id="pw-fn-count"></span></h4>' +
'<div class="pw-perf-list" id="pw-fn-list"></div>' +
'</div>' +
'<div class="pw-perf-block">' +
'<h4>慢帧堆栈 Top10 <span class="pw-muted" id="pw-slow-count"></span></h4>' +
'<div class="pw-perf-list" id="pw-slow-list"></div>' +
'</div>' +
'</div>' +
'<div class="pw-perf-block">' +
'<h4>内存资源 Top20 <span class="pw-muted" id="pw-res-count"></span></h4>' +
'<div class="pw-perf-list pw-res-list" id="pw-res-list"></div>' +
'</div>' +
'</div>';
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 = '<div class="pw-muted" style="padding:12px">点击「剖析中」开始采样(约 8ms 一帧栈)</div>';
} else {
var html = ['<div class="pw-perf-rowh"><span>函数</span><span>次数</span><span>估算ms</span></div>'];
for (var i = 0; i < fns.length; i++) {
html.push(
'<div class="pw-perf-item">' +
'<span class="pw-perf-name" title="' + escapeAttr(fns[i].name) + '">' + escapeHtml(shorten(fns[i].name, 42)) + '</span>' +
'<span class="pw-perf-num">' + fns[i].count + '</span>' +
'<span class="pw-perf-num">' + fns[i].ms + '</span>' +
'</div>'
);
}
fnEl.innerHTML = html.join('');
}
}
var slows = r.slowFrames || [];
if (slowC) slowC.textContent = slows.length ? (slows.length + ' 次 >20ms') : '';
if (slowEl) {
if (!slows.length) {
slowEl.innerHTML = '<div class="pw-muted" style="padding:12px">暂无慢帧(阈值 20ms)</div>';
} else {
var sh = ['<div class="pw-perf-rowh"><span>耗时</span><span>堆栈</span></div>'];
for (var j = 0; j < slows.length; j++) {
var st = escapeHtml(String(slows[j].stack || '').split('\n').slice(0, 3).join(' | '));
sh.push(
'<div class="pw-perf-item pw-slow-item">' +
'<span class="pw-perf-num pw-bad">' + slows[j].ms + 'ms</span>' +
'<span class="pw-perf-stack" title="' + escapeAttr(slows[j].stack || '') + '">' + st + '</span>' +
'</div>'
);
}
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 = '<div class="pw-muted" style="padding:12px">打开「资源」面板或稍候自动加载</div>';
return;
}
var html = ['<div class="pw-perf-rowh"><span>名称 / UUID</span><span>类型</span><span>内存</span><span>操作</span></div>'];
for (var i = 0; i < rows.length; i++) {
var r = rows[i];
html.push(
'<div class="pw-perf-item pw-res-item" data-uuid="' + escapeAttr(r.uuid) + '">' +
'<span class="pw-perf-name"><b>' + escapeHtml(r.name) + '</b><br><i class="pw-muted">' + escapeHtml(r.uuid) + '</i></span>' +
'<span class="pw-perf-type">' + escapeHtml(r.type) + '</span>' +
'<span class="pw-perf-num">' + fmtMB(r.mem) + '</span>' +
'<span class="pw-res-ops">' +
'<button type="button" class="pw-btn pw-btn-mini" data-copy="name">复制名</button>' +
'<button type="button" class="pw-btn pw-btn-mini" data-copy="uuid">复制UUID</button>' +
'<button type="button" class="pw-btn pw-btn-mini" data-preview="1">预览</button>' +
'</span></div>'
);
}
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 = '<img alt="" src="' + p.url + '" />';
} else if (p && p.kind === 'url' && p.url) {
box.innerHTML = '<img alt="" src="' + p.url + '" />';
} else {
box.innerHTML = '<span class="pw-muted">无预览</span>';
}
}).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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function escapeAttr(s) {
return escapeHtml(s).replace(/"/g, '&quot;');
}
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);