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 { 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
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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
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';
|
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;
|
||||||
|
}
|
||||||
|
@ -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
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