(function (global) { 'use strict'; /** * 性能面板(壳侧):采样在游戏 Agent,曲线只在壳 document 绘制。 * @constructor */ function PerfPanel() { this.el = null; this.charts = {}; this._bridge = null; this._data = null; this._renderTimer = 0; this._dirty = false; this._paused = false; } 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'; root.style.height = '100%'; root.style.display = 'flex'; root.style.flexDirection = 'column'; root.innerHTML = '
' + '性能' + '
' + '' + '' + '' + '' + '' + '' + '' + '
' + '
' + '
' + '
' + '
' + '
'; this.el = root; var defs = [ { key: 'fps', title: '帧率 FPS', color: '#89d185' }, { key: 'frame', title: '帧耗时 (ms)', color: '#3794ff' }, { key: 'logic', title: '逻辑 (ms)', color: '#4ec9b0' }, { key: 'render', title: '渲染 (ms)', color: '#f0a020' }, { key: 'present', title: '呈现 (ms)', color: '#c586c0' }, { key: 'draws', title: 'DrawCall', color: '#dcdcaa' }, { key: 'tricount', title: '三角面', color: '#9cdcfe' }, { key: 'textureMemory', title: '纹理显存 (MB)', color: '#f14c4c' }, { key: 'bufferMemory', title: '缓冲显存 (MB)', color: '#ce9178' }, { key: 'jsHeap', title: 'JS 堆 (MB)', color: '#b5cea8' }, { key: 'assetCount', title: '资源数量', color: '#569cd6' }, ]; var chartsEl = root.querySelector('#pw-perf-charts'); for (var i = 0; i < defs.length; i++) { var d = defs[i]; var card = document.createElement('div'); card.className = 'pw-chart-card'; card.innerHTML = '

' + d.title + '

'; chartsEl.appendChild(card); var canvas = card.querySelector('canvas'); var chart = new global.PWRingChart(canvas); chart.color = d.color; chart.fill = hexToRgba(d.color, 0.15); this.charts[d.key] = chart; } root.querySelector('#pw-perf-pause').addEventListener('click', function (ev) { self._paused = !self._paused; if (self._bridge) self._bridge.call('perf.setPaused', { paused: self._paused }); ev.currentTarget.textContent = self._paused ? '继续' : '暂停'; ev.currentTarget.classList.toggle('active', self._paused); }); root.querySelector('#pw-perf-clear').addEventListener('click', function () { if (self._bridge) self._bridge.call('perf.clear'); self._data = null; self.render(); }); root.querySelector('#pw-perf-mark').addEventListener('click', function () { var text = root.querySelector('#pw-perf-mark-text').value; if (self._bridge) self._bridge.call('perf.mark', { text: text }); }); root.querySelector('#pw-perf-csv').addEventListener('click', function () { var csv = self.toCsv(); var blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'workbench-perf.csv'; a.click(); URL.revokeObjectURL(a.href); }); root.querySelector('#pw-perf-window').addEventListener('change', function (ev) { if (self._bridge) self._bridge.call('perf.setCapacity', { n: parseInt(ev.target.value, 10) || 300 }); }); root.querySelector('#pw-perf-interval').addEventListener('change', function (ev) { if (self._bridge) self._bridge.call('perf.setInterval', { ms: parseInt(ev.target.value, 10) || 500 }); }); document.addEventListener('keydown', function (ev) { if (ev.altKey && (ev.key === 'm' || ev.key === 'M')) { if (self._bridge) self._bridge.call('perf.mark', { text: '' }); } }); // 绘图与采样解耦:最多约 5fps 重绘 this._renderTimer = setInterval(function () { if (self._dirty) { self._dirty = false; self.render(); } }, 200); return root; }; PerfPanel.prototype.applySample = function (data) { this._data = data; this._dirty = true; }; PerfPanel.prototype.render = function () { if (!this.el) return; var s = this._data; var latest = (s && s.latest) || {}; var sum = this.el.querySelector('#pw-perf-summary'); if (sum) { var fpsArr = (s && s.series && s.series.fps) || []; var avg = average(fpsArr); var min = minOf(fpsArr); var max = maxOf(fpsArr); var low = 0; for (var i = 0; i < fpsArr.length; i++) if (fpsArr[i] < 30) low++; var lowPct = fpsArr.length ? Math.round((low / fpsArr.length) * 100) : 0; sum.innerHTML = '帧率 ' + Math.round(latest.fps || 0) + ' 均 ' + Math.round(avg) + ' 最低 ' + Math.round(min) + ' 最高 ' + Math.round(max) + '' + '<30帧占比 ' + lowPct + '%' + 'DrawCall ' + Math.round(latest.draws || 0) + '' + '帧耗时 ' + (Math.round((latest.frame || 0) * 10) / 10) + 'ms' + '纹理 ' + (Math.round((latest.textureMemory || 0) * 100) / 100) + 'MB' + '缓冲 ' + (Math.round((latest.bufferMemory || 0) * 100) / 100) + 'MB' + '堆内存 ' + (Math.round((latest.jsHeap || 0) * 100) / 100) + 'MB'; } var markEl = this.el.querySelector('#pw-perf-markers'); if (markEl) { var marks = (s && s.markers) ? s.markers.slice(-8) : []; markEl.textContent = marks.length ? ('标记:' + marks.map(function (m) { return m.label; }).join(' | ')) : '标记:(自动 scene/bundle + Alt+M 手动)'; } if (!s || !s.series) return; for (var key in this.charts) { if (!Object.prototype.hasOwnProperty.call(this.charts, key)) continue; this.charts[key].draw(s.series[key] || [], s.markers || [], s.times || []); } }; PerfPanel.prototype.toCsv = function () { var s = this._data; if (!s || !s.series) return ''; var keys = Object.keys(s.series); var lines = ['t,' + keys.join(',')]; for (var i = 0; i < (s.times || []).length; i++) { var row = [s.times[i]]; for (var k = 0; k < keys.length; k++) row.push(s.series[keys[k]][i]); lines.push(row.join(',')); } return lines.join('\n'); }; function average(arr) { if (!arr.length) return 0; var t = 0; for (var i = 0; i < arr.length; i++) t += arr[i]; return t / arr.length; } function minOf(arr) { if (!arr.length) return 0; var m = arr[0]; for (var i = 1; i < arr.length; i++) if (arr[i] < m) m = arr[i]; return m; } function maxOf(arr) { if (!arr.length) return 0; var m = arr[0]; for (var i = 1; i < arr.length; i++) if (arr[i] > m) m = arr[i]; return m; } function hexToRgba(hex, a) { var h = hex.replace('#', ''); var r = parseInt(h.slice(0, 2), 16); var g = parseInt(h.slice(2, 4), 16); var b = parseInt(h.slice(4, 6), 16); return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; } global.PWPerfPanel = PerfPanel; })(window);