feat: 性能分析

This commit is contained in:
unanmed 2024-03-12 16:23:04 +08:00
parent d25555b032
commit 95dc31329d
8 changed files with 391 additions and 6 deletions

View File

@ -5,6 +5,12 @@ import { hovered } from './fixed';
import { hasMarkedEnemy, markEnemy, unmarkEnemy } from '@/plugin/mark'; import { hasMarkedEnemy, markEnemy, unmarkEnemy } from '@/plugin/mark';
import { mainUi } from './ui'; import { mainUi } from './ui';
import { GameStorage } from '../storage'; 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 mainScope = Symbol.for('@key_main');
export const gameKey = new Hotkey('gameKey', '游戏按键'); export const gameKey = new Hotkey('gameKey', '游戏按键');
@ -396,6 +402,19 @@ gameKey
id: '@fly_right_t_2', id: '@fly_right_t_2',
name: '后10张地图_2', name: '后10张地图_2',
defaults: KeyCode.PageUp 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(); gameKey.enable();
@ -517,6 +536,17 @@ gameKey
break; break;
} }
} }
})
.realize('toggleFrameDisplay', () => {
const value = mainSetting.getValue('debug.frame');
mainSetting.setValue('debug.frame', !value);
})
.realize('toggleFrameMonitor', () => {
if (isFramePaused()) {
resumeFrame();
} else {
pauseFrame();
}
}); });
// ----- Storage // ----- Storage

View File

@ -10,6 +10,7 @@ interface Components {
HotkeySetting: SettingComponent; HotkeySetting: SettingComponent;
ToolbarEditor: SettingComponent; ToolbarEditor: SettingComponent;
Radio: (items: string[]) => SettingComponent; Radio: (items: string[]) => SettingComponent;
Performance: SettingComponent;
} }
export type { Components as SettingDisplayComponents }; export type { Components as SettingDisplayComponents };
@ -21,7 +22,8 @@ export function createSettingComponents() {
Number: NumberSetting, Number: NumberSetting,
HotkeySetting, HotkeySetting,
ToolbarEditor, ToolbarEditor,
Radio: RadioSetting Radio: RadioSetting,
Performance: PerformanceSetting
}; };
return com; return com;
} }
@ -180,3 +182,18 @@ function ToolbarEditor(props: SettingComponentProps) {
</div> </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>
);
}

View File

@ -13,7 +13,8 @@ mainUi.register(
new GameUi('shop', UI.Shop), new GameUi('shop', UI.Shop),
new GameUi('hotkey', UI.Hotkey), new GameUi('hotkey', UI.Hotkey),
new GameUi('toolEditor', UI.ToolEditor), new GameUi('toolEditor', UI.ToolEditor),
new GameUi('virtualKey', MiscUI.VirtualKey) new GameUi('virtualKey', MiscUI.VirtualKey),
new GameUi('performance', UI.Performance)
// todo: 把游戏主 div 加入到 mainUi 里面 // todo: 把游戏主 div 加入到 mainUi 里面
); );
mainUi.showAll(); mainUi.showAll();

View File

@ -487,6 +487,8 @@ mainSetting
'调试设置', '调试设置',
new MotaSetting() new MotaSetting()
.register('frame', '帧率显示', false, COM.Boolean) .register('frame', '帧率显示', false, COM.Boolean)
.register('performance', '性能分析', false, COM.Performance)
.setDisplayFunc('performance', () => '')
); );
const loading = Mota.require('var', 'loading'); const loading = Mota.require('var', 'loading');

208
src/panel/performance.vue Normal file
View 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>

View File

@ -12,6 +12,21 @@ span.style.color = 'lightgreen';
span.style.padding = '5px'; span.style.padding = '5px';
let showing = false; 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() { export function init() {
const settings = Mota.require('var', 'mainSetting'); const settings = Mota.require('var', 'mainSetting');
@ -19,18 +34,77 @@ export function init() {
/** 记录前5帧的时间戳 */ /** 记录前5帧的时间戳 */
let lasttimes = [0, 0, 0, 0, 0]; let lasttimes = [0, 0, 0, 0, 0];
ticker.add(time => { ticker.add(time => {
if (!setting?.value) return; if (!setting?.value || pause) return;
let marked = false;
lasttimes.shift(); lasttimes.shift();
lasttimes.push(time); lasttimes.push(time);
span.innerText = (1000 / ((lasttimes[4] - lasttimes[0]) / 4)).toFixed( const frame = 1000 / ((lasttimes[4] - lasttimes[0]) / 4);
1 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() { export function show() {
showing = true; showing = true;
document.body.appendChild(span); document.body.appendChild(span);
starting = 0;
} }
export function hide() { export function hide() {
@ -41,3 +115,17 @@ export function hide() {
export function isShowing() { export function isShowing() {
return showing; return showing;
} }
export function pauseFrame() {
pause = true;
span.innerText += '(paused)';
}
export function resumeFrame() {
pause = false;
starting = 0;
}
export function isPaused() {
return pause;
}

View File

@ -14,3 +14,4 @@ export { default as Toolbox } from './toolbox.vue';
export { default as Hotkey } from './hotkey.vue'; export { default as Hotkey } from './hotkey.vue';
export { default as Toolbar } from './toolbar.vue'; export { default as Toolbar } from './toolbar.vue';
export { default as ToolEditor } from './toolEditor.vue'; export { default as ToolEditor } from './toolEditor.vue';
export { default as Performance } from './performance.vue';

38
src/ui/performance.vue Normal file
View 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>