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