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.

212 lines
9.2 KiB

(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 =
'<div class="pw-panel-head">' +
'<span class="pw-panel-title">性能</span>' +
'<div class="pw-panel-tools">' +
'<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:80px" />' +
'<button type="button" id="pw-perf-csv" class="pw-btn">导出CSV</button>' +
'<select id="pw-perf-window" class="pw-select">' +
'<option value="300">窗口 60秒</option>' +
'<option value="1500">窗口 5分钟</option>' +
'</select>' +
'<select id="pw-perf-interval" class="pw-select">' +
'<option value="200">采样 200ms</option>' +
'<option value="500" selected>采样 500ms</option>' +
'<option value="1000">采样 1s</option>' +
'</select>' +
'</div></div>' +
'<div class="pw-panel-body">' +
'<div id="pw-perf-summary" class="pw-summary"></div>' +
'<div id="pw-perf-markers" class="pw-muted" style="margin-bottom:6px;max-height:36px;overflow:auto"></div>' +
'<div class="pw-charts" id="pw-perf-charts"></div>' +
'</div>';
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 = '<h4>' + d.title + '</h4><canvas data-key="' + d.key + '"></canvas>';
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 =
'<span>帧率 <b>' + Math.round(latest.fps || 0) + '</b> 均 ' + Math.round(avg) + ' 最低 ' + Math.round(min) + ' 最高 ' + Math.round(max) + '</span>' +
'<span>&lt;30帧占比 <b>' + lowPct + '%</b></span>' +
'<span>DrawCall <b>' + Math.round(latest.draws || 0) + '</b></span>' +
'<span>帧耗时 <b>' + (Math.round((latest.frame || 0) * 10) / 10) + 'ms</b></span>' +
'<span>纹理 <b>' + (Math.round((latest.textureMemory || 0) * 100) / 100) + 'MB</b></span>' +
'<span>缓冲 <b>' + (Math.round((latest.bufferMemory || 0) * 100) / 100) + 'MB</b></span>' +
'<span>堆内存 <b>' + (Math.round((latest.jsHeap || 0) * 100) / 100) + 'MB</b></span>';
}
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);