From fcf3def284f1b05be05abca3e0d3a5d549704fd7 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 15 Oct 2025 23:24:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E9=9B=86=E8=BF=9C=E7=AB=AF=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=86=8Dprofiler=20dockpanel=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor-app/src/App.tsx | 95 +++++- .../src/components/ProfilerDockPanel.tsx | 190 ++++++++++++ .../src/components/ProfilerWindow.tsx | 212 +++++--------- .../src/components/SceneHierarchy.tsx | 66 ++++- packages/editor-app/src/locales/en.ts | 3 +- packages/editor-app/src/locales/zh.ts | 3 +- .../editor-app/src/plugins/ProfilerPlugin.tsx | 37 ++- .../src/services/ProfilerService.ts | 270 ++++++++++++++++++ packages/editor-app/src/styles/App.css | 27 ++ .../src/styles/ProfilerDockPanel.css | 270 ++++++++++++++++++ .../editor-app/src/styles/SceneHierarchy.css | 64 +++++ 11 files changed, 1077 insertions(+), 160 deletions(-) create mode 100644 packages/editor-app/src/components/ProfilerDockPanel.tsx create mode 100644 packages/editor-app/src/services/ProfilerService.ts create mode 100644 packages/editor-app/src/styles/ProfilerDockPanel.css diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 534efcb0..a33bae4d 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -46,6 +46,8 @@ function App() { const [showPluginManager, setShowPluginManager] = useState(false); const [showProfiler, setShowProfiler] = useState(false); const [showPortManager, setShowPortManager] = useState(false); + const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0); + const [isRemoteConnected, setIsRemoteConnected] = useState(false); useEffect(() => { // 禁用默认右键菜单 @@ -60,6 +62,57 @@ function App() { }; }, []); + useEffect(() => { + if (messageHub) { + const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => { + console.log('[App] Plugin enabled, updating panels'); + setPluginUpdateTrigger(prev => prev + 1); + }); + + const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => { + console.log('[App] Plugin disabled, updating panels'); + setPluginUpdateTrigger(prev => prev + 1); + }); + + return () => { + unsubscribeEnabled(); + unsubscribeDisabled(); + }; + } + }, [messageHub]); + + // 监听远程连接状态 + useEffect(() => { + const checkConnection = () => { + const profilerService = (window as any).__PROFILER_SERVICE__; + if (profilerService && profilerService.isConnected()) { + if (!isRemoteConnected) { + console.log('[App] Remote game connected'); + setIsRemoteConnected(true); + setStatus(t('header.status.remoteConnected')); + } + } else { + if (isRemoteConnected) { + console.log('[App] Remote game disconnected'); + setIsRemoteConnected(false); + if (projectLoaded) { + const projectService = Core.services.resolve(ProjectService); + const componentRegistry = Core.services.resolve(ComponentRegistry); + const componentCount = componentRegistry?.getRegisteredComponents().length || 0; + setStatus(t('header.status.projectOpened') + (componentCount > 0 ? ` (${componentCount} components registered)` : '')); + } else { + setStatus(t('header.status.ready')); + } + } + } + }; + + checkConnection(); + const interval = setInterval(checkConnection, 1000); + + return () => clearInterval(interval); + }, [projectLoaded, isRemoteConnected, t]); + useEffect(() => { const initializeEditor = async () => { // 使用 ref 防止 React StrictMode 的双重调用 @@ -244,8 +297,8 @@ function App() { }; useEffect(() => { - if (projectLoaded && entityStore && messageHub && logService) { - setPanels([ + if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) { + const corePanels: DockablePanel[] = [ { id: 'scene-hierarchy', title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', @@ -281,9 +334,41 @@ function App() { content: , closable: false } - ]); + ]; + + const enabledPlugins = pluginManager.getAllPluginMetadata() + .filter(p => p.enabled) + .map(p => p.name); + + const pluginPanels: DockablePanel[] = uiRegistry.getAllPanels() + .filter(panelDesc => { + if (!panelDesc.component) { + return false; + } + return enabledPlugins.some(pluginName => { + const plugin = pluginManager.getEditorPlugin(pluginName); + if (plugin && plugin.registerPanels) { + const pluginPanels = plugin.registerPanels(); + return pluginPanels.some(p => p.id === panelDesc.id); + } + return false; + }); + }) + .map(panelDesc => { + const Component = panelDesc.component; + return { + id: panelDesc.id, + title: (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title, + position: panelDesc.position as any, + content: , + closable: panelDesc.closable ?? true + }; + }); + + console.log('[App] Loading plugin panels:', pluginPanels); + setPanels([...corePanels, ...pluginPanels]); } - }, [projectLoaded, entityStore, messageHub, logService, locale, currentProjectPath, t]); + }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger]); const handlePanelMove = (panelId: string, newPosition: any) => { setPanels(prevPanels => @@ -325,7 +410,7 @@ function App() { return (
-
+
(null); + const [isConnected, setIsConnected] = useState(false); + const [isServerRunning, setIsServerRunning] = useState(false); + + useEffect(() => { + const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + + if (!profilerService) { + console.warn('[ProfilerDockPanel] ProfilerService not available - plugin may be disabled'); + setIsServerRunning(false); + setIsConnected(false); + return; + } + + // 订阅数据更新 + const unsubscribe = profilerService.subscribe((data: ProfilerData) => { + setProfilerData(data); + }); + + // 定期检查连接状态 + const checkStatus = () => { + setIsConnected(profilerService.isConnected()); + setIsServerRunning(profilerService.isServerActive()); + }; + + checkStatus(); + const interval = setInterval(checkStatus, 1000); + + return () => { + unsubscribe(); + clearInterval(interval); + }; + }, []); + + const fps = profilerData?.fps || 0; + const totalFrameTime = profilerData?.totalFrameTime || 0; + const systems = (profilerData?.systems || []).slice(0, 5); // Only show top 5 systems in dock panel + const entityCount = profilerData?.entityCount || 0; + const componentCount = profilerData?.componentCount || 0; + const targetFrameTime = 16.67; + + const handleOpenDetails = () => { + const messageHub = Core.services.resolve(MessageHub); + if (messageHub) { + messageHub.publish('ui:openWindow', { windowId: 'profiler' }); + } + }; + + return ( +
+
+

Performance Monitor

+
+ {isConnected && ( + + )} +
+ {isConnected ? ( + <> + + Connected + + ) : isServerRunning ? ( + <> + + Waiting... + + ) : ( + <> + + Server Off + + )} +
+
+
+ + {!isServerRunning ? ( +
+ +

Profiler server not running

+

Open Profiler window and connect to start monitoring

+
+ ) : !isConnected ? ( +
+ +

Waiting for game connection...

+

Connect your game to port 8080

+
+ ) : ( +
+
+
+
+ +
+
+
FPS
+
{fps}
+
+
+ +
+
+ +
+
+
Frame Time
+
targetFrameTime ? 'warning' : ''}`}> + {totalFrameTime.toFixed(1)}ms +
+
+
+ +
+
+ +
+
+
Entities
+
{entityCount}
+
+
+ +
+
+ +
+
+
Components
+
{componentCount}
+
+
+
+ + {systems.length > 0 && ( +
+

Top Systems

+
+ {systems.map((system) => ( +
+
+ {system.name} + + {system.executionTime.toFixed(2)}ms + +
+
+
targetFrameTime + ? 'var(--color-danger)' + : system.executionTime > targetFrameTime * 0.5 + ? 'var(--color-warning)' + : 'var(--color-success)' + }} + /> +
+
+ {system.percentage.toFixed(1)}% + {system.entityCount > 0 && ( + {system.entityCount} entities + )} +
+
+ ))} +
+
+ )} +
+ )} +
+ ); +} diff --git a/packages/editor-app/src/components/ProfilerWindow.tsx b/packages/editor-app/src/components/ProfilerWindow.tsx index 43dd014c..a1be5fd8 100644 --- a/packages/editor-app/src/components/ProfilerWindow.tsx +++ b/packages/editor-app/src/components/ProfilerWindow.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import { Core } from '@esengine/ecs-framework'; import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react'; -import { invoke } from '@tauri-apps/api/core'; +import { ProfilerService } from '../services/ProfilerService'; import '../styles/ProfilerWindow.css'; interface SystemPerformanceData { @@ -31,45 +31,27 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) { const [dataSource, setDataSource] = useState('local'); const [viewMode, setViewMode] = useState<'tree' | 'table'>('table'); const [searchQuery, setSearchQuery] = useState(''); - const [wsPort, setWsPort] = useState('8080'); const [isConnected, setIsConnected] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); - const [connectionError, setConnectionError] = useState(null); + const [isServerRunning, setIsServerRunning] = useState(false); const animationRef = useRef(); - const wsRef = useRef(null); - // WebSocket connection management + // Check ProfilerService connection status useEffect(() => { - if (dataSource === 'remote' && isConnected && wsRef.current) { - // Keep WebSocket connection alive - const pingInterval = setInterval(() => { - if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: 'ping' })); - } - }, 5000); + const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; - return () => clearInterval(pingInterval); + if (!profilerService) { + return; } - }, [dataSource, isConnected]); - // Cleanup WebSocket and stop server on unmount - useEffect(() => { - return () => { - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - - // Check if server is running and stop it - invoke('get_profiler_status') - .then(isRunning => { - if (isRunning) { - return invoke('stop_profiler_server'); - } - }) - .then(() => console.log('[Profiler] Server stopped on unmount')) - .catch(err => console.error('[Profiler] Failed to stop server on unmount:', err)); + const checkStatus = () => { + setIsConnected(profilerService.isConnected()); + setIsServerRunning(profilerService.isServerActive()); }; + + checkStatus(); + const interval = setInterval(checkStatus, 1000); + + return () => clearInterval(interval); }, []); const buildSystemTree = (flatSystems: Map, statsMap: Map): SystemPerformanceData[] => { @@ -144,6 +126,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) { return [coreNode]; }; + // Subscribe to local performance data useEffect(() => { if (dataSource !== 'local') return; @@ -181,6 +164,39 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) { }; }, [isPaused, sortBy, dataSource]); + // Subscribe to remote performance data from ProfilerService + useEffect(() => { + if (dataSource !== 'remote') return; + + const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + + if (!profilerService) { + console.warn('[ProfilerWindow] ProfilerService not available'); + return; + } + + const unsubscribe = profilerService.subscribe((data) => { + if (isPaused) return; + + handleRemoteDebugData({ + performance: { + frameTime: data.totalFrameTime, + systemPerformance: data.systems.map(sys => ({ + systemName: sys.name, + lastExecutionTime: sys.executionTime, + averageTime: sys.averageTime, + minTime: 0, + maxTime: 0, + entityCount: sys.entityCount, + percentage: sys.percentage + })) + } + }); + }); + + return () => unsubscribe(); + }, [dataSource, isPaused]); + const handleReset = () => { if (dataSource === 'local') { const coreInstance = Core.Instance; @@ -194,84 +210,6 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) { } }; - const handleConnect = async () => { - if (isConnecting) return; - - if (wsRef.current) { - wsRef.current.close(); - } - - setIsConnecting(true); - setConnectionError(null); - - try { - const port = parseInt(wsPort); - const result = await invoke('start_profiler_server', { port }); - console.log('[Profiler]', result); - - const ws = new WebSocket(`ws://localhost:${wsPort}`); - - ws.onopen = () => { - console.log('[Profiler] Frontend connected to profiler server'); - setIsConnected(true); - setIsConnecting(false); - setConnectionError(null); - }; - - ws.onclose = () => { - console.log('[Profiler] Frontend disconnected'); - setIsConnected(false); - setIsConnecting(false); - }; - - ws.onerror = (error) => { - console.error('[Profiler] WebSocket error:', error); - setConnectionError(`Failed to connect to profiler server`); - setIsConnected(false); - setIsConnecting(false); - }; - - ws.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - - if (message.type === 'debug_data' && message.data) { - handleRemoteDebugData(message.data); - } else if (message.type === 'pong') { - // Ping-pong response, connection is alive - } - } catch (error) { - console.error('[Profiler] Failed to parse message:', error); - } - }; - - wsRef.current = ws; - } catch (error) { - console.error('[Profiler] Failed to start server:', error); - setConnectionError(String(error)); - setIsConnected(false); - setIsConnecting(false); - } - }; - - const handleDisconnect = async () => { - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - - try { - // Stop WebSocket server in Tauri backend - const result = await invoke('stop_profiler_server'); - console.log('[Profiler]', result); - } catch (error) { - console.error('[Profiler] Failed to stop server:', error); - } - - setIsConnected(false); - setSystems([]); - setTotalFrameTime(0); - }; const handleRemoteDebugData = (debugData: any) => { if (isPaused) return; @@ -310,9 +248,6 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } - } else if (newSource === 'local' && dataSource === 'remote') { - // Switching to local - handleDisconnect(); } setDataSource(newSource); setSystems([]); @@ -424,42 +359,25 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) { {dataSource === 'remote' && (
- setWsPort(e.target.value)} - disabled={isConnected || isConnecting} - /> +
+ + Port: 8080 +
{isConnected ? ( - - ) : ( - - )} - {isConnected && ( - Connected - )} - {isConnecting && ( - Connecting... - )} - {connectionError && ( - Error + Connected +
+ ) : isServerRunning ? ( +
+ + Waiting for game... +
+ ) : ( +
+ + Server Off +
)}
)} diff --git a/packages/editor-app/src/components/SceneHierarchy.tsx b/packages/editor-app/src/components/SceneHierarchy.tsx index cda2457b..e009ab9f 100644 --- a/packages/editor-app/src/components/SceneHierarchy.tsx +++ b/packages/editor-app/src/components/SceneHierarchy.tsx @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react'; import { Entity } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import { useLocale } from '../hooks/useLocale'; -import { Box, Layers } from 'lucide-react'; +import { Box, Layers, Wifi } from 'lucide-react'; +import { ProfilerService, RemoteEntity } from '../services/ProfilerService'; import '../styles/SceneHierarchy.css'; interface SceneHierarchyProps { @@ -12,9 +13,12 @@ interface SceneHierarchyProps { export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) { const [entities, setEntities] = useState([]); + const [remoteEntities, setRemoteEntities] = useState([]); + const [isRemoteConnected, setIsRemoteConnected] = useState(false); const [selectedId, setSelectedId] = useState(null); const { t } = useLocale(); + // Subscribe to local entity changes useEffect(() => { const updateEntities = () => { setEntities(entityStore.getRootEntities()); @@ -39,23 +43,79 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) }; }, [entityStore, messageHub]); + // Subscribe to remote entity data from ProfilerService + useEffect(() => { + const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + + if (!profilerService) { + console.warn('[SceneHierarchy] ProfilerService not available'); + return; + } + + console.log('[SceneHierarchy] Subscribing to ProfilerService'); + + const unsubscribe = profilerService.subscribe((data) => { + const connected = profilerService.isConnected(); + console.log('[SceneHierarchy] Received data, connected:', connected, 'entities:', data.entities?.length || 0); + setIsRemoteConnected(connected); + + if (connected && data.entities && data.entities.length > 0) { + console.log('[SceneHierarchy] Setting remote entities:', data.entities); + setRemoteEntities(data.entities); + } else { + setRemoteEntities([]); + } + }); + + return () => unsubscribe(); + }, []); + const handleEntityClick = (entity: Entity) => { entityStore.selectEntity(entity); }; + // Determine which entities to display + const displayEntities = isRemoteConnected ? remoteEntities : entities; + const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0; + return (

{t('hierarchy.title')}

+ {showRemoteIndicator && ( +
+ +
+ )}
- {entities.length === 0 ? ( + {displayEntities.length === 0 ? (
{t('hierarchy.empty')}
-
Create an entity to get started
+
+ {isRemoteConnected + ? 'No entities in remote game' + : 'Create an entity to get started'} +
+ ) : isRemoteConnected ? ( +
    + {remoteEntities.map(entity => ( +
  • + + {entity.name} + {entity.components.length > 0 && ( + {entity.components.length} + )} +
  • + ))} +
) : (
    {entities.map(entity => ( diff --git a/packages/editor-app/src/locales/en.ts b/packages/editor-app/src/locales/en.ts index 6d20df46..33c50102 100644 --- a/packages/editor-app/src/locales/en.ts +++ b/packages/editor-app/src/locales/en.ts @@ -14,7 +14,8 @@ export const en: Translations = { initializing: 'Initializing...', ready: 'Editor Ready', failed: 'Initialization Failed', - projectOpened: 'Project Opened' + projectOpened: 'Project Opened', + remoteConnected: 'Remote Game Connected' } }, hierarchy: { diff --git a/packages/editor-app/src/locales/zh.ts b/packages/editor-app/src/locales/zh.ts index 2ac86ac6..1acbc527 100644 --- a/packages/editor-app/src/locales/zh.ts +++ b/packages/editor-app/src/locales/zh.ts @@ -14,7 +14,8 @@ export const zh: Translations = { initializing: '初始化中...', ready: '编辑器就绪', failed: '初始化失败', - projectOpened: '项目已打开' + projectOpened: '项目已打开', + remoteConnected: '远程游戏已连接' } }, hierarchy: { diff --git a/packages/editor-app/src/plugins/ProfilerPlugin.tsx b/packages/editor-app/src/plugins/ProfilerPlugin.tsx index f76e5496..b4140e52 100644 --- a/packages/editor-app/src/plugins/ProfilerPlugin.tsx +++ b/packages/editor-app/src/plugins/ProfilerPlugin.tsx @@ -1,5 +1,7 @@ import type { Core, ServiceContainer } from '@esengine/ecs-framework'; -import { IEditorPlugin, EditorPluginCategory, MenuItem, MessageHub } from '@esengine/editor-core'; +import { IEditorPlugin, EditorPluginCategory, MenuItem, MessageHub, PanelDescriptor } from '@esengine/editor-core'; +import { ProfilerDockPanel } from '../components/ProfilerDockPanel'; +import { ProfilerService } from '../services/ProfilerService'; /** * Profiler Plugin @@ -15,14 +17,30 @@ export class ProfilerPlugin implements IEditorPlugin { readonly icon = '📊'; private messageHub: MessageHub | null = null; + private profilerService: ProfilerService | null = null; async install(_core: Core, services: ServiceContainer): Promise { this.messageHub = services.resolve(MessageHub); - console.log('[ProfilerPlugin] Installed'); + + // 创建并启动 ProfilerService + this.profilerService = new ProfilerService(); + + // 将服务实例存储到全局,供组件访问 + (window as any).__PROFILER_SERVICE__ = this.profilerService; + + console.log('[ProfilerPlugin] Installed and ProfilerService started'); } async uninstall(): Promise { - console.log('[ProfilerPlugin] Uninstalled'); + // 清理 ProfilerService + if (this.profilerService) { + this.profilerService.destroy(); + this.profilerService = null; + } + + delete (window as any).__PROFILER_SERVICE__; + + console.log('[ProfilerPlugin] Uninstalled and ProfilerService stopped'); } async onEditorReady(): Promise { @@ -45,4 +63,17 @@ export class ProfilerPlugin implements IEditorPlugin { console.log('[ProfilerPlugin] Registering menu items:', items); return items; } + + registerPanels(): PanelDescriptor[] { + return [ + { + id: 'profiler-monitor', + title: 'Performance Monitor', + position: 'center' as any, + closable: true, + component: ProfilerDockPanel, + order: 200 + } + ]; + } } diff --git a/packages/editor-app/src/services/ProfilerService.ts b/packages/editor-app/src/services/ProfilerService.ts new file mode 100644 index 00000000..64e53de0 --- /dev/null +++ b/packages/editor-app/src/services/ProfilerService.ts @@ -0,0 +1,270 @@ +import { invoke } from '@tauri-apps/api/core'; + +export interface SystemPerformanceData { + name: string; + executionTime: number; + entityCount: number; + averageTime: number; + percentage: number; +} + +export interface RemoteEntity { + id: number; + name: string; + enabled: boolean; + components: string[]; +} + +export interface ProfilerData { + totalFrameTime: number; + systems: SystemPerformanceData[]; + entityCount: number; + componentCount: number; + fps: number; + entities?: RemoteEntity[]; +} + +type ProfilerDataListener = (data: ProfilerData) => void; + +export class ProfilerService { + private ws: WebSocket | null = null; + private isServerRunning = false; + private wsPort = '8080'; + private listeners: Set = new Set(); + private currentData: ProfilerData | null = null; + private checkServerInterval: NodeJS.Timeout | null = null; + private reconnectTimeout: NodeJS.Timeout | null = null; + + constructor() { + this.startServerCheck(); + } + + public subscribe(listener: ProfilerDataListener): () => void { + this.listeners.add(listener); + + // 如果已有数据,立即发送给新订阅者 + if (this.currentData) { + listener(this.currentData); + } + + return () => { + this.listeners.delete(listener); + }; + } + + public isConnected(): boolean { + return this.ws !== null && this.ws.readyState === WebSocket.OPEN; + } + + public isServerActive(): boolean { + return this.isServerRunning; + } + + private startServerCheck(): void { + this.checkServerStatus(); + this.checkServerInterval = setInterval(() => { + this.checkServerStatus(); + }, 2000); + } + + private async checkServerStatus(): Promise { + try { + const status = await invoke('get_profiler_status'); + const wasRunning = this.isServerRunning; + this.isServerRunning = status; + + // 如果服务器还没运行,自动启动它 + if (!status) { + await this.startServer(); + return; + } + + // 服务器启动了,尝试连接 + if (status && !this.ws) { + this.connectToServer(); + } + + // 服务器从运行变为停止 + if (wasRunning && !status) { + console.log('[ProfilerService] Server stopped'); + this.disconnect(); + } + } catch (error) { + this.isServerRunning = false; + } + } + + private async startServer(): Promise { + try { + const port = parseInt(this.wsPort); + const result = await invoke('start_profiler_server', { port }); + console.log('[ProfilerService]', result); + this.isServerRunning = true; + } catch (error) { + console.error('[ProfilerService] Failed to start server:', error); + } + } + + private connectToServer(): void { + if (this.ws) return; + + try { + console.log(`[ProfilerService] Connecting to ws://localhost:${this.wsPort}`); + const ws = new WebSocket(`ws://localhost:${this.wsPort}`); + + ws.onopen = () => { + console.log('[ProfilerService] Connected to profiler server'); + this.notifyListeners(this.createEmptyData()); + }; + + ws.onclose = () => { + console.log('[ProfilerService] Disconnected from profiler server'); + this.ws = null; + + // 如果服务器还在运行,尝试重连 + if (this.isServerRunning && !this.reconnectTimeout) { + this.reconnectTimeout = setTimeout(() => { + this.reconnectTimeout = null; + this.connectToServer(); + }, 3000); + } + }; + + ws.onerror = (error) => { + console.error('[ProfilerService] WebSocket error:', error); + this.ws = null; + }; + + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + if (message.type === 'debug_data' && message.data) { + this.handleDebugData(message.data); + } + } catch (error) { + console.error('[ProfilerService] Failed to parse message:', error); + } + }; + + this.ws = ws; + } catch (error) { + console.error('[ProfilerService] Failed to create WebSocket:', error); + } + } + + private handleDebugData(debugData: any): void { + const performance = debugData.performance; + if (!performance) return; + + const totalFrameTime = performance.frameTime || 0; + const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0; + + let systems: SystemPerformanceData[] = []; + if (performance.systemPerformance && Array.isArray(performance.systemPerformance)) { + systems = performance.systemPerformance + .map((sys: any) => ({ + name: sys.systemName, + executionTime: sys.lastExecutionTime || sys.averageTime || 0, + entityCount: sys.entityCount || 0, + averageTime: sys.averageTime || 0, + percentage: 0 + })) + .sort((a: SystemPerformanceData, b: SystemPerformanceData) => + b.executionTime - a.executionTime + ); + + const totalTime = performance.frameTime || 1; + systems.forEach((sys: SystemPerformanceData) => { + sys.percentage = (sys.executionTime / totalTime) * 100; + }); + } + + const entityCount = debugData.entities?.totalCount || 0; + const componentTypes = debugData.components?.types || []; + const componentCount = componentTypes.length; + + // 解析实体列表 + console.log('[ProfilerService] debugData.entities:', debugData.entities); + let entities: RemoteEntity[] = []; + + // 尝试从 topEntitiesByComponents 获取实体列表 + if (debugData.entities?.topEntitiesByComponents && Array.isArray(debugData.entities.topEntitiesByComponents)) { + console.log('[ProfilerService] Found topEntitiesByComponents, length:', debugData.entities.topEntitiesByComponents.length); + entities = debugData.entities.topEntitiesByComponents.map((e: any) => ({ + id: parseInt(e.id) || 0, + name: e.name || `Entity ${e.id}`, + enabled: true, // topEntitiesByComponents doesn't have enabled flag, assume true + components: [] // componentCount is provided but not component names + })); + console.log('[ProfilerService] Parsed entities from topEntitiesByComponents:', entities.length); + } + // 尝试从 entities 获取实体列表(旧格式兼容) + else if (debugData.entities?.entities && Array.isArray(debugData.entities.entities)) { + console.log('[ProfilerService] Found entities array, length:', debugData.entities.entities.length); + entities = debugData.entities.entities.map((e: any) => ({ + id: e.id, + name: e.name || `Entity ${e.id}`, + enabled: e.enabled !== false, + components: e.components || [] + })); + console.log('[ProfilerService] Parsed entities:', entities.length); + } else { + console.log('[ProfilerService] No entities array found'); + } + + this.currentData = { + totalFrameTime, + systems, + entityCount, + componentCount, + fps, + entities + }; + + this.notifyListeners(this.currentData); + } + + private createEmptyData(): ProfilerData { + return { + totalFrameTime: 0, + systems: [], + entityCount: 0, + componentCount: 0, + fps: 0 + }; + } + + private notifyListeners(data: ProfilerData): void { + this.listeners.forEach(listener => { + try { + listener(data); + } catch (error) { + console.error('[ProfilerService] Error in listener:', error); + } + }); + } + + private disconnect(): void { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } + } + + public destroy(): void { + this.disconnect(); + + if (this.checkServerInterval) { + clearInterval(this.checkServerInterval); + this.checkServerInterval = null; + } + + this.listeners.clear(); + this.currentData = null; + } +} diff --git a/packages/editor-app/src/styles/App.css b/packages/editor-app/src/styles/App.css index 64897baf..d2577e30 100644 --- a/packages/editor-app/src/styles/App.css +++ b/packages/editor-app/src/styles/App.css @@ -33,6 +33,33 @@ background-color: var(--color-bg-elevated); border-bottom: 1px solid var(--color-border-default); flex-shrink: 0; + transition: all 0.3s ease; +} + +.editor-header.remote-connected { + background-color: rgba(16, 185, 129, 0.15); + border-bottom-color: rgba(16, 185, 129, 0.5); +} + +.editor-header.remote-connected .status { + color: rgb(16, 185, 129); + font-weight: 600; +} + +.editor-header.remote-connected .status::before { + background-color: rgb(16, 185, 129); + animation: pulse-green 1.5s ease-in-out infinite; +} + +@keyframes pulse-green { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(1.2); + } } .header-right { diff --git a/packages/editor-app/src/styles/ProfilerDockPanel.css b/packages/editor-app/src/styles/ProfilerDockPanel.css new file mode 100644 index 00000000..eaf44489 --- /dev/null +++ b/packages/editor-app/src/styles/ProfilerDockPanel.css @@ -0,0 +1,270 @@ +.profiler-dock-panel { + display: flex; + flex-direction: column; + height: 100%; + background: var(--color-bg-elevated); + overflow: hidden; +} + +.profiler-dock-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--color-border-default); + flex-shrink: 0; +} + +.profiler-dock-header h3 { + margin: 0; + font-size: 13px; + font-weight: 600; + color: var(--color-text-primary); +} + +.profiler-dock-header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.profiler-dock-details-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + background: var(--color-bg-inset); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); +} + +.profiler-dock-details-btn:hover { + background: var(--color-bg-hover); + border-color: var(--color-border-strong); + color: var(--color-text-primary); +} + +.profiler-dock-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + padding: 4px 8px; + border-radius: var(--radius-sm); + background: var(--color-bg-inset); +} + +.status-text { + font-weight: 500; +} + +.status-text.connected { + color: var(--color-success); +} + +.status-text.waiting { + color: var(--color-warning); +} + +.status-text.disconnected { + color: var(--color-text-tertiary); +} + +.profiler-dock-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + padding: 24px; + text-align: center; + color: var(--color-text-tertiary); + gap: 12px; +} + +.profiler-dock-empty p { + margin: 0; + font-size: 13px; +} + +.profiler-dock-empty .hint { + font-size: 11px; + opacity: 0.7; +} + +.profiler-dock-content { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.profiler-dock-content::-webkit-scrollbar { + width: 6px; +} + +.profiler-dock-content::-webkit-scrollbar-track { + background: transparent; +} + +.profiler-dock-content::-webkit-scrollbar-thumb { + background: var(--color-border-default); + border-radius: 3px; +} + +.profiler-dock-stats { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.stat-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: var(--color-bg-inset); + border-radius: var(--radius-md); + border: 1px solid var(--color-border-default); + transition: all var(--transition-fast); +} + +.stat-card:hover { + border-color: var(--color-border-strong); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.stat-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: var(--radius-sm); + background: rgba(99, 102, 241, 0.1); + color: rgb(99, 102, 241); + flex-shrink: 0; +} + +.stat-info { + flex: 1; + min-width: 0; +} + +.stat-label { + font-size: 10px; + color: var(--color-text-tertiary); + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; + margin-bottom: 2px; +} + +.stat-value { + font-size: 18px; + font-weight: 700; + color: var(--color-text-primary); + font-family: var(--font-family-mono); +} + +.stat-value.warning { + color: var(--color-warning); +} + +.profiler-dock-systems { + display: flex; + flex-direction: column; + gap: 12px; +} + +.profiler-dock-systems h4 { + margin: 0; + font-size: 12px; + font-weight: 600; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.systems-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.system-item { + padding: 10px 12px; + background: var(--color-bg-inset); + border-radius: var(--radius-md); + border: 1px solid var(--color-border-default); + transition: all var(--transition-fast); +} + +.system-item:hover { + border-color: var(--color-border-strong); +} + +.system-item-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.system-item-name { + font-size: 11px; + font-weight: 500; + color: var(--color-text-primary); + font-family: var(--font-family-mono); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.system-item-time { + font-size: 11px; + font-weight: 600; + color: var(--color-text-primary); + font-family: var(--font-family-mono); + flex-shrink: 0; + margin-left: 8px; +} + +.system-item-bar { + width: 100%; + height: 4px; + background: var(--color-bg-elevated); + border-radius: 2px; + overflow: hidden; + margin-bottom: 6px; +} + +.system-item-bar-fill { + height: 100%; + transition: width 0.3s ease; + border-radius: 2px; +} + +.system-item-footer { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 10px; + color: var(--color-text-tertiary); +} + +.system-item-percentage { + font-weight: 600; + font-family: var(--font-family-mono); +} + +.system-item-entities { + font-size: 9px; +} diff --git a/packages/editor-app/src/styles/SceneHierarchy.css b/packages/editor-app/src/styles/SceneHierarchy.css index ac9353aa..4a598457 100644 --- a/packages/editor-app/src/styles/SceneHierarchy.css +++ b/packages/editor-app/src/styles/SceneHierarchy.css @@ -29,6 +29,20 @@ color: var(--color-text-primary); text-transform: uppercase; letter-spacing: 0.05em; + flex: 1; +} + +.remote-indicator { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background-color: rgba(16, 185, 129, 0.15); + border: 1px solid rgba(16, 185, 129, 0.5); + border-radius: var(--radius-sm); + color: rgb(16, 185, 129); + font-size: var(--font-size-xs); + animation: pulse-green 1.5s ease-in-out infinite; } .hierarchy-content { @@ -133,3 +147,53 @@ .entity-item.selected .entity-name { font-weight: var(--font-weight-medium); } + +/* Remote entity styles */ +.entity-item.remote-entity { + cursor: default; + border-left: 2px solid rgba(16, 185, 129, 0.5); +} + +.entity-item.remote-entity:hover { + background-color: rgba(16, 185, 129, 0.05); +} + +.entity-item.remote-entity::before { + display: none; +} + +.entity-item.remote-entity.disabled { + opacity: 0.5; +} + +.entity-item.remote-entity.disabled .entity-name { + text-decoration: line-through; + color: var(--color-text-tertiary); +} + +.component-count { + display: flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 4px; + background-color: rgba(16, 185, 129, 0.2); + border: 1px solid rgba(16, 185, 129, 0.5); + border-radius: var(--radius-full); + color: rgb(16, 185, 129); + font-size: 10px; + font-weight: var(--font-weight-semibold); + flex-shrink: 0; +} + +@keyframes pulse-green { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(1.05); + } +}