mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-10-31 20:32:58 +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