mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-19 17:16:08 +08:00
feat: 性能分析
This commit is contained in:
parent
d25555b032
commit
95dc31329d
@ -5,6 +5,12 @@ import { hovered } from './fixed';
|
||||
import { hasMarkedEnemy, markEnemy, unmarkEnemy } from '@/plugin/mark';
|
||||
import { mainUi } from './ui';
|
||||
import { GameStorage } from '../storage';
|
||||
import { mainSetting } from '../setting';
|
||||
import {
|
||||
isPaused as isFramePaused,
|
||||
pauseFrame,
|
||||
resumeFrame
|
||||
} from '@/plugin/frame';
|
||||
|
||||
export const mainScope = Symbol.for('@key_main');
|
||||
export const gameKey = new Hotkey('gameKey', '游戏按键');
|
||||
@ -396,6 +402,19 @@ gameKey
|
||||
id: '@fly_right_t_2',
|
||||
name: '后10张地图_2',
|
||||
defaults: KeyCode.PageUp
|
||||
})
|
||||
.group('debug', '调试按键')
|
||||
.register({
|
||||
id: 'toggleFrameMonitor',
|
||||
name: '暂停/继续帧率监控',
|
||||
defaults: KeyCode.F2,
|
||||
ctrl: true
|
||||
})
|
||||
.register({
|
||||
id: 'toggleFrameDisplay',
|
||||
name: '开关帧率显示',
|
||||
defaults: KeyCode.F3,
|
||||
ctrl: true
|
||||
});
|
||||
|
||||
gameKey.enable();
|
||||
@ -517,6 +536,17 @@ gameKey
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.realize('toggleFrameDisplay', () => {
|
||||
const value = mainSetting.getValue('debug.frame');
|
||||
mainSetting.setValue('debug.frame', !value);
|
||||
})
|
||||
.realize('toggleFrameMonitor', () => {
|
||||
if (isFramePaused()) {
|
||||
resumeFrame();
|
||||
} else {
|
||||
pauseFrame();
|
||||
}
|
||||
});
|
||||
|
||||
// ----- Storage
|
||||
|
@ -10,6 +10,7 @@ interface Components {
|
||||
HotkeySetting: SettingComponent;
|
||||
ToolbarEditor: SettingComponent;
|
||||
Radio: (items: string[]) => SettingComponent;
|
||||
Performance: SettingComponent;
|
||||
}
|
||||
|
||||
export type { Components as SettingDisplayComponents };
|
||||
@ -21,7 +22,8 @@ export function createSettingComponents() {
|
||||
Number: NumberSetting,
|
||||
HotkeySetting,
|
||||
ToolbarEditor,
|
||||
Radio: RadioSetting
|
||||
Radio: RadioSetting,
|
||||
Performance: PerformanceSetting
|
||||
};
|
||||
return com;
|
||||
}
|
||||
@ -180,3 +182,18 @@ function ToolbarEditor(props: SettingComponentProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PerformanceSetting(props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
style="font-size: 75%"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => showSpecialSetting('performance')}
|
||||
>
|
||||
打开性能界面
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ mainUi.register(
|
||||
new GameUi('shop', UI.Shop),
|
||||
new GameUi('hotkey', UI.Hotkey),
|
||||
new GameUi('toolEditor', UI.ToolEditor),
|
||||
new GameUi('virtualKey', MiscUI.VirtualKey)
|
||||
new GameUi('virtualKey', MiscUI.VirtualKey),
|
||||
new GameUi('performance', UI.Performance)
|
||||
// todo: 把游戏主 div 加入到 mainUi 里面
|
||||
);
|
||||
mainUi.showAll();
|
||||
|
@ -487,6 +487,8 @@ mainSetting
|
||||
'调试设置',
|
||||
new MotaSetting()
|
||||
.register('frame', '帧率显示', false, COM.Boolean)
|
||||
.register('performance', '性能分析', false, COM.Performance)
|
||||
.setDisplayFunc('performance', () => '')
|
||||
);
|
||||
|
||||
const loading = Mota.require('var', 'loading');
|
||||
|
208
src/panel/performance.vue
Normal file
208
src/panel/performance.vue
Normal file
@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div :id="`performance-${id}`" class="performance-main">
|
||||
<div class="frame">
|
||||
<div class="frame-title">
|
||||
<span>帧率监控</span>
|
||||
</div>
|
||||
<div :id="`frameDiv-${id}`" class="frame-canvas">
|
||||
<canvas :id="`frameCanvas-${id}`"></canvas>
|
||||
</div>
|
||||
<div class="frame-buttons">
|
||||
<a-button @click="toggleFrameMonitor" type="primary">
|
||||
{{ paused ? '继续' : '暂停' }}监控
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import {
|
||||
getFrameList,
|
||||
isPaused,
|
||||
pauseFrame,
|
||||
resumeFrame
|
||||
} from '@/plugin/frame';
|
||||
import { pColor } from '@/plugin/utils';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
const id = (1e8 * Math.random()).toFixed(0);
|
||||
|
||||
const list = getFrameList();
|
||||
|
||||
let frameDiv: HTMLDivElement;
|
||||
let frameCanvas: HTMLCanvasElement;
|
||||
let main: HTMLDivElement;
|
||||
|
||||
let mainObserver: ResizeObserver;
|
||||
let interval = 0;
|
||||
|
||||
interface PeriodInfo {
|
||||
start: number;
|
||||
end: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const paused = ref(isPaused());
|
||||
|
||||
function toggleFrameMonitor() {
|
||||
if (!isPaused()) pauseFrame();
|
||||
else resumeFrame();
|
||||
paused.value = isPaused();
|
||||
drawFrame();
|
||||
}
|
||||
|
||||
function drawFrame() {
|
||||
if (!frameCanvas) return;
|
||||
const ctx = frameCanvas.getContext('2d')!;
|
||||
ctx.clearRect(0, 0, frameCanvas.width, frameCanvas.height);
|
||||
const length = list.length;
|
||||
const per = (frameCanvas.width - 50) / (length - 1);
|
||||
const max = Math.max(...list.map(v => v.frame));
|
||||
const scaler = [15, 30, 60, 90, 120, 144, 240, 300, 360];
|
||||
const prescaler = [3, 3, 6, 6, 6, 6, 5, 5, 6];
|
||||
// find sclaer
|
||||
let min = 0;
|
||||
for (let i = 0; i < scaler.length; i++) {
|
||||
if (Math.abs(scaler[i] - max) < Math.abs(scaler[i] - scaler[min])) {
|
||||
min = i;
|
||||
}
|
||||
}
|
||||
// draw scaler
|
||||
const scalerStep = Math.round(scaler[min] / prescaler[min]);
|
||||
const scalerHeight = (frameCanvas.height * 2) / 3 / prescaler[min];
|
||||
const bottom = frameCanvas.height - frameCanvas.height / 6;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.fillStyle = pColor('#ddd8') as string;
|
||||
ctx.strokeStyle = pColor('#ddd4') as string;
|
||||
|
||||
for (let i = 0; i < prescaler[min]; i++) {
|
||||
ctx.beginPath();
|
||||
const y = bottom - scalerHeight * i;
|
||||
ctx.fillText((scalerStep * i).toFixed(0), 45, y);
|
||||
ctx.moveTo(50, y);
|
||||
ctx.lineTo(frameCanvas.width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
// drawFrame
|
||||
const frameHeight = (frameCanvas.height * 2) / 3;
|
||||
ctx.strokeStyle = 'rgb(54,162,235)';
|
||||
const periodInfo: PeriodInfo[] = [];
|
||||
|
||||
const element = list[0];
|
||||
const y = bottom - (element.frame / scaler[min]) * frameHeight;
|
||||
const x = 50;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const element = list[i];
|
||||
const y = bottom - (element.frame / scaler[min]) * frameHeight;
|
||||
const x = 50 + i * per;
|
||||
ctx.lineTo(x, y);
|
||||
if (element.mark === 'low_frame_start') {
|
||||
periodInfo.push({
|
||||
start: i - 1,
|
||||
end: 0,
|
||||
duration: 0
|
||||
});
|
||||
} else if (element.mark === 'low_frame_end') {
|
||||
const info = periodInfo.at(-1);
|
||||
if (info) {
|
||||
info.duration = element.period!;
|
||||
info.end = i - 9;
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
// draw period
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.beginPath();
|
||||
for (const info of periodInfo) {
|
||||
if (info.end === 0) continue;
|
||||
const x = 50 + info.start * per;
|
||||
const width = (info.end - info.start) * per;
|
||||
ctx.fillStyle = pColor('#d446') as string;
|
||||
ctx.fillRect(x, 0, width, frameCanvas.height);
|
||||
ctx.fillStyle = pColor('#ddda') as string;
|
||||
ctx.fillText(
|
||||
`${info.duration.toFixed(1)}ms`,
|
||||
x + ((info.end - info.start) / 2) * per,
|
||||
frameHeight / 3 - 2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const ratio = devicePixelRatio;
|
||||
// frame
|
||||
frameCanvas.width = frameDiv.clientWidth * ratio;
|
||||
frameCanvas.height = frameDiv.clientHeight * ratio;
|
||||
frameCanvas.style.width = `${frameDiv.clientWidth}px`;
|
||||
frameCanvas.style.height = `${frameDiv.clientHeight}py`;
|
||||
drawFrame();
|
||||
}
|
||||
|
||||
function createObserver() {
|
||||
const observer = new ResizeObserver(entries => {
|
||||
resize();
|
||||
});
|
||||
observer.observe(main);
|
||||
|
||||
mainObserver = observer;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
main = document.getElementById(`performance-${id}`) as HTMLDivElement;
|
||||
frameDiv = document.getElementById(`frameDiv-${id}`) as HTMLDivElement;
|
||||
frameCanvas = document.getElementById(
|
||||
`frameCanvas-${id}`
|
||||
) as HTMLCanvasElement;
|
||||
|
||||
mainSetting.setValue('debug.frame', true);
|
||||
|
||||
createObserver();
|
||||
resize();
|
||||
|
||||
interval = window.setInterval(drawFrame, 500);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
mainObserver.disconnect();
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.performance-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.frame {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.frame-title {
|
||||
font-size: 150%;
|
||||
flex-basis: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.frame-canvas {
|
||||
flex-basis: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -12,6 +12,21 @@ span.style.color = 'lightgreen';
|
||||
span.style.padding = '5px';
|
||||
|
||||
let showing = false;
|
||||
let pause = false;
|
||||
|
||||
interface FrameInfo {
|
||||
time: number;
|
||||
frame: number;
|
||||
mark?: string;
|
||||
period?: number;
|
||||
}
|
||||
|
||||
const frameList: FrameInfo[] = [];
|
||||
|
||||
let inLowFrame = false;
|
||||
let leaveLowFrameTime = 0;
|
||||
let starting = 0;
|
||||
let beginLeaveTime = 0;
|
||||
|
||||
export function init() {
|
||||
const settings = Mota.require('var', 'mainSetting');
|
||||
@ -19,18 +34,77 @@ export function init() {
|
||||
/** 记录前5帧的时间戳 */
|
||||
let lasttimes = [0, 0, 0, 0, 0];
|
||||
ticker.add(time => {
|
||||
if (!setting?.value) return;
|
||||
if (!setting?.value || pause) return;
|
||||
let marked = false;
|
||||
lasttimes.shift();
|
||||
lasttimes.push(time);
|
||||
span.innerText = (1000 / ((lasttimes[4] - lasttimes[0]) / 4)).toFixed(
|
||||
1
|
||||
);
|
||||
const frame = 1000 / ((lasttimes[4] - lasttimes[0]) / 4);
|
||||
starting++;
|
||||
if (frame < 50 && starting > 5) {
|
||||
if (!inLowFrame) {
|
||||
performance.mark(`low_frame_start`);
|
||||
inLowFrame = true;
|
||||
frameList.push({
|
||||
time,
|
||||
frame,
|
||||
mark: 'low_frame_start'
|
||||
});
|
||||
marked = true;
|
||||
}
|
||||
}
|
||||
if (inLowFrame) {
|
||||
if (leaveLowFrameTime === 0) {
|
||||
performance.mark('low_frame_end');
|
||||
const measure = performance.measure(
|
||||
'low_frame',
|
||||
'low_frame_start',
|
||||
'low_frame_end'
|
||||
);
|
||||
beginLeaveTime = measure.duration;
|
||||
}
|
||||
if (frame >= 50) {
|
||||
leaveLowFrameTime++;
|
||||
} else {
|
||||
performance.clearMarks('low_frame_end');
|
||||
performance.clearMeasures('low_frame');
|
||||
leaveLowFrameTime = 0;
|
||||
}
|
||||
if (leaveLowFrameTime >= 10) {
|
||||
leaveLowFrameTime = 0;
|
||||
inLowFrame = false;
|
||||
marked = true;
|
||||
console.warn(
|
||||
`Mota frame performance analyzer: Marked a low frame period.`
|
||||
);
|
||||
performance.clearMarks();
|
||||
frameList.push({
|
||||
time,
|
||||
frame,
|
||||
mark: 'low_frame_end',
|
||||
period: beginLeaveTime
|
||||
});
|
||||
}
|
||||
}
|
||||
frameList.push();
|
||||
span.innerText = frame.toFixed(1);
|
||||
if (!marked) {
|
||||
frameList.push({
|
||||
time,
|
||||
frame
|
||||
});
|
||||
}
|
||||
if (frameList.length >= 1000) frameList.shift();
|
||||
});
|
||||
}
|
||||
|
||||
export function getFrameList() {
|
||||
return frameList;
|
||||
}
|
||||
|
||||
export function show() {
|
||||
showing = true;
|
||||
document.body.appendChild(span);
|
||||
starting = 0;
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
@ -41,3 +115,17 @@ export function hide() {
|
||||
export function isShowing() {
|
||||
return showing;
|
||||
}
|
||||
|
||||
export function pauseFrame() {
|
||||
pause = true;
|
||||
span.innerText += '(paused)';
|
||||
}
|
||||
|
||||
export function resumeFrame() {
|
||||
pause = false;
|
||||
starting = 0;
|
||||
}
|
||||
|
||||
export function isPaused() {
|
||||
return pause;
|
||||
}
|
||||
|
@ -14,3 +14,4 @@ export { default as Toolbox } from './toolbox.vue';
|
||||
export { default as Hotkey } from './hotkey.vue';
|
||||
export { default as Toolbar } from './toolbar.vue';
|
||||
export { default as ToolEditor } from './toolEditor.vue';
|
||||
export { default as Performance } from './performance.vue';
|
||||
|
38
src/ui/performance.vue
Normal file
38
src/ui/performance.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="performance">
|
||||
<div class="tools">
|
||||
<span class="button-text" @click="exit"
|
||||
><left-outlined />返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<PerformancePanel></PerformancePanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { LeftOutlined } from '@ant-design/icons-vue';
|
||||
import PerformancePanel from '../panel/performance.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
ui: GameUi;
|
||||
num: number;
|
||||
}>();
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.performance {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tools {
|
||||
height: 6%;
|
||||
font-size: 3.2vh;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user