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
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, '&').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);
|
|
|