收集远端数据再profiler dockpanel上

This commit is contained in:
YHH
2025-10-15 23:24:13 +08:00
parent 6f1a2896dd
commit fcf3def284
11 changed files with 1077 additions and 160 deletions

View File

@@ -46,6 +46,8 @@ function App() {
const [showPluginManager, setShowPluginManager] = useState(false); const [showPluginManager, setShowPluginManager] = useState(false);
const [showProfiler, setShowProfiler] = useState(false); const [showProfiler, setShowProfiler] = useState(false);
const [showPortManager, setShowPortManager] = useState(false); const [showPortManager, setShowPortManager] = useState(false);
const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0);
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
useEffect(() => { 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(() => { useEffect(() => {
const initializeEditor = async () => { const initializeEditor = async () => {
// 使用 ref 防止 React StrictMode 的双重调用 // 使用 ref 防止 React StrictMode 的双重调用
@@ -244,8 +297,8 @@ function App() {
}; };
useEffect(() => { useEffect(() => {
if (projectLoaded && entityStore && messageHub && logService) { if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) {
setPanels([ const corePanels: DockablePanel[] = [
{ {
id: 'scene-hierarchy', id: 'scene-hierarchy',
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
@@ -281,9 +334,41 @@ function App() {
content: <ConsolePanel logService={logService} />, content: <ConsolePanel logService={logService} />,
closable: false 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: <Component />,
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) => { const handlePanelMove = (panelId: string, newPosition: any) => {
setPanels(prevPanels => setPanels(prevPanels =>
@@ -325,7 +410,7 @@ function App() {
return ( return (
<div className="editor-container"> <div className="editor-container">
<div className="editor-header"> <div className={`editor-header ${isRemoteConnected ? 'remote-connected' : ''}`}>
<MenuBar <MenuBar
locale={locale} locale={locale}
uiRegistry={uiRegistry || undefined} uiRegistry={uiRegistry || undefined}

View File

@@ -0,0 +1,190 @@
import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2 } from 'lucide-react';
import { ProfilerService, ProfilerData } from '../services/ProfilerService';
import { Core } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core';
import '../styles/ProfilerDockPanel.css';
export function ProfilerDockPanel() {
const [profilerData, setProfilerData] = useState<ProfilerData | null>(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 (
<div className="profiler-dock-panel">
<div className="profiler-dock-header">
<h3>Performance Monitor</h3>
<div className="profiler-dock-header-actions">
{isConnected && (
<button
className="profiler-dock-details-btn"
onClick={handleOpenDetails}
title="Open detailed profiler"
>
<Maximize2 size={14} />
</button>
)}
<div className="profiler-dock-status">
{isConnected ? (
<>
<Wifi size={12} />
<span className="status-text connected">Connected</span>
</>
) : isServerRunning ? (
<>
<WifiOff size={12} />
<span className="status-text waiting">Waiting...</span>
</>
) : (
<>
<WifiOff size={12} />
<span className="status-text disconnected">Server Off</span>
</>
)}
</div>
</div>
</div>
{!isServerRunning ? (
<div className="profiler-dock-empty">
<Cpu size={32} />
<p>Profiler server not running</p>
<p className="hint">Open Profiler window and connect to start monitoring</p>
</div>
) : !isConnected ? (
<div className="profiler-dock-empty">
<Activity size={32} />
<p>Waiting for game connection...</p>
<p className="hint">Connect your game to port 8080</p>
</div>
) : (
<div className="profiler-dock-content">
<div className="profiler-dock-stats">
<div className="stat-card">
<div className="stat-icon">
<Activity size={16} />
</div>
<div className="stat-info">
<div className="stat-label">FPS</div>
<div className={`stat-value ${fps < 55 ? 'warning' : ''}`}>{fps}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Cpu size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Frame Time</div>
<div className={`stat-value ${totalFrameTime > targetFrameTime ? 'warning' : ''}`}>
{totalFrameTime.toFixed(1)}ms
</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Layers size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Entities</div>
<div className="stat-value">{entityCount}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Package size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Components</div>
<div className="stat-value">{componentCount}</div>
</div>
</div>
</div>
{systems.length > 0 && (
<div className="profiler-dock-systems">
<h4>Top Systems</h4>
<div className="systems-list">
{systems.map((system) => (
<div key={system.name} className="system-item">
<div className="system-item-header">
<span className="system-item-name">{system.name}</span>
<span className="system-item-time">
{system.executionTime.toFixed(2)}ms
</span>
</div>
<div className="system-item-bar">
<div
className="system-item-bar-fill"
style={{
width: `${Math.min(system.percentage, 100)}%`,
backgroundColor: system.executionTime > targetFrameTime
? 'var(--color-danger)'
: system.executionTime > targetFrameTime * 0.5
? 'var(--color-warning)'
: 'var(--color-success)'
}}
/>
</div>
<div className="system-item-footer">
<span className="system-item-percentage">{system.percentage.toFixed(1)}%</span>
{system.entityCount > 0 && (
<span className="system-item-entities">{system.entityCount} entities</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Core } from '@esengine/ecs-framework'; 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 { 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'; import '../styles/ProfilerWindow.css';
interface SystemPerformanceData { interface SystemPerformanceData {
@@ -31,45 +31,27 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
const [dataSource, setDataSource] = useState<DataSource>('local'); const [dataSource, setDataSource] = useState<DataSource>('local');
const [viewMode, setViewMode] = useState<'tree' | 'table'>('table'); const [viewMode, setViewMode] = useState<'tree' | 'table'>('table');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [wsPort, setWsPort] = useState('8080');
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false); const [isServerRunning, setIsServerRunning] = useState(false);
const [connectionError, setConnectionError] = useState<string | null>(null);
const animationRef = useRef<number>(); const animationRef = useRef<number>();
const wsRef = useRef<WebSocket | null>(null);
// WebSocket connection management // Check ProfilerService connection status
useEffect(() => { useEffect(() => {
if (dataSource === 'remote' && isConnected && wsRef.current) { const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
// Keep WebSocket connection alive
const pingInterval = setInterval(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: 'ping' }));
}
}, 5000);
return () => clearInterval(pingInterval); if (!profilerService) {
return;
} }
}, [dataSource, isConnected]);
// Cleanup WebSocket and stop server on unmount const checkStatus = () => {
useEffect(() => { setIsConnected(profilerService.isConnected());
return () => { setIsServerRunning(profilerService.isServerActive());
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
// Check if server is running and stop it
invoke<boolean>('get_profiler_status')
.then(isRunning => {
if (isRunning) {
return invoke<string>('stop_profiler_server');
}
})
.then(() => console.log('[Profiler] Server stopped on unmount'))
.catch(err => console.error('[Profiler] Failed to stop server on unmount:', err));
}; };
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => clearInterval(interval);
}, []); }, []);
const buildSystemTree = (flatSystems: Map<string, any>, statsMap: Map<string, any>): SystemPerformanceData[] => { const buildSystemTree = (flatSystems: Map<string, any>, statsMap: Map<string, any>): SystemPerformanceData[] => {
@@ -144,6 +126,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
return [coreNode]; return [coreNode];
}; };
// Subscribe to local performance data
useEffect(() => { useEffect(() => {
if (dataSource !== 'local') return; if (dataSource !== 'local') return;
@@ -181,6 +164,39 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
}; };
}, [isPaused, sortBy, dataSource]); }, [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 = () => { const handleReset = () => {
if (dataSource === 'local') { if (dataSource === 'local') {
const coreInstance = Core.Instance; 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<string>('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<string>('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) => { const handleRemoteDebugData = (debugData: any) => {
if (isPaused) return; if (isPaused) return;
@@ -310,9 +248,6 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
if (animationRef.current) { if (animationRef.current) {
cancelAnimationFrame(animationRef.current); cancelAnimationFrame(animationRef.current);
} }
} else if (newSource === 'local' && dataSource === 'remote') {
// Switching to local
handleDisconnect();
} }
setDataSource(newSource); setDataSource(newSource);
setSystems([]); setSystems([]);
@@ -424,42 +359,25 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
{dataSource === 'remote' && ( {dataSource === 'remote' && (
<div className="profiler-connection"> <div className="profiler-connection">
<input <div className="connection-port-display">
type="text" <Server size={14} />
className="connection-port" <span>Port: 8080</span>
placeholder="Port" </div>
value={wsPort}
onChange={(e) => setWsPort(e.target.value)}
disabled={isConnected || isConnecting}
/>
{isConnected ? ( {isConnected ? (
<button <div className="connection-status-indicator connected">
className="connection-btn disconnect"
onClick={handleDisconnect}
title="Disconnect"
>
<WifiOff size={14} />
<span>Disconnect</span>
</button>
) : (
<button
className="connection-btn connect"
onClick={handleConnect}
disabled={isConnecting}
title="Connect to Remote Game"
>
<Wifi size={14} /> <Wifi size={14} />
<span>{isConnecting ? 'Connecting...' : 'Connect'}</span> <span>Connected</span>
</button> </div>
)} ) : isServerRunning ? (
{isConnected && ( <div className="connection-status-indicator waiting">
<span className="connection-status connected">Connected</span> <WifiOff size={14} />
)} <span>Waiting for game...</span>
{isConnecting && ( </div>
<span className="connection-status connected">Connecting...</span> ) : (
)} <div className="connection-status-indicator disconnected">
{connectionError && ( <WifiOff size={14} />
<span className="connection-status error" title={connectionError}>Error</span> <span>Server Off</span>
</div>
)} )}
</div> </div>
)} )}

View File

@@ -2,7 +2,8 @@ import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework'; import { Entity } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import { EntityStoreService, MessageHub } from '@esengine/editor-core';
import { useLocale } from '../hooks/useLocale'; 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'; import '../styles/SceneHierarchy.css';
interface SceneHierarchyProps { interface SceneHierarchyProps {
@@ -12,9 +13,12 @@ interface SceneHierarchyProps {
export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) { export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) {
const [entities, setEntities] = useState<Entity[]>([]); const [entities, setEntities] = useState<Entity[]>([]);
const [remoteEntities, setRemoteEntities] = useState<RemoteEntity[]>([]);
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
const [selectedId, setSelectedId] = useState<number | null>(null); const [selectedId, setSelectedId] = useState<number | null>(null);
const { t } = useLocale(); const { t } = useLocale();
// Subscribe to local entity changes
useEffect(() => { useEffect(() => {
const updateEntities = () => { const updateEntities = () => {
setEntities(entityStore.getRootEntities()); setEntities(entityStore.getRootEntities());
@@ -39,23 +43,79 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
}; };
}, [entityStore, messageHub]); }, [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) => { const handleEntityClick = (entity: Entity) => {
entityStore.selectEntity(entity); entityStore.selectEntity(entity);
}; };
// Determine which entities to display
const displayEntities = isRemoteConnected ? remoteEntities : entities;
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
return ( return (
<div className="scene-hierarchy"> <div className="scene-hierarchy">
<div className="hierarchy-header"> <div className="hierarchy-header">
<Layers size={16} className="hierarchy-header-icon" /> <Layers size={16} className="hierarchy-header-icon" />
<h3>{t('hierarchy.title')}</h3> <h3>{t('hierarchy.title')}</h3>
{showRemoteIndicator && (
<div className="remote-indicator" title="Showing remote entities">
<Wifi size={12} />
</div>
)}
</div> </div>
<div className="hierarchy-content scrollable"> <div className="hierarchy-content scrollable">
{entities.length === 0 ? ( {displayEntities.length === 0 ? (
<div className="empty-state"> <div className="empty-state">
<Box size={48} strokeWidth={1.5} className="empty-icon" /> <Box size={48} strokeWidth={1.5} className="empty-icon" />
<div className="empty-title">{t('hierarchy.empty')}</div> <div className="empty-title">{t('hierarchy.empty')}</div>
<div className="empty-hint">Create an entity to get started</div> <div className="empty-hint">
{isRemoteConnected
? 'No entities in remote game'
: 'Create an entity to get started'}
</div>
</div> </div>
) : isRemoteConnected ? (
<ul className="entity-list">
{remoteEntities.map(entity => (
<li
key={entity.id}
className={`entity-item remote-entity ${!entity.enabled ? 'disabled' : ''}`}
title={`${entity.name} - ${entity.components.join(', ')}`}
>
<Box size={14} className="entity-icon" />
<span className="entity-name">{entity.name}</span>
{entity.components.length > 0 && (
<span className="component-count">{entity.components.length}</span>
)}
</li>
))}
</ul>
) : ( ) : (
<ul className="entity-list"> <ul className="entity-list">
{entities.map(entity => ( {entities.map(entity => (

View File

@@ -14,7 +14,8 @@ export const en: Translations = {
initializing: 'Initializing...', initializing: 'Initializing...',
ready: 'Editor Ready', ready: 'Editor Ready',
failed: 'Initialization Failed', failed: 'Initialization Failed',
projectOpened: 'Project Opened' projectOpened: 'Project Opened',
remoteConnected: 'Remote Game Connected'
} }
}, },
hierarchy: { hierarchy: {

View File

@@ -14,7 +14,8 @@ export const zh: Translations = {
initializing: '初始化中...', initializing: '初始化中...',
ready: '编辑器就绪', ready: '编辑器就绪',
failed: '初始化失败', failed: '初始化失败',
projectOpened: '项目已打开' projectOpened: '项目已打开',
remoteConnected: '远程游戏已连接'
} }
}, },
hierarchy: { hierarchy: {

View File

@@ -1,5 +1,7 @@
import type { Core, ServiceContainer } from '@esengine/ecs-framework'; 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 * Profiler Plugin
@@ -15,14 +17,30 @@ export class ProfilerPlugin implements IEditorPlugin {
readonly icon = '📊'; readonly icon = '📊';
private messageHub: MessageHub | null = null; private messageHub: MessageHub | null = null;
private profilerService: ProfilerService | null = null;
async install(_core: Core, services: ServiceContainer): Promise<void> { async install(_core: Core, services: ServiceContainer): Promise<void> {
this.messageHub = services.resolve(MessageHub); 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<void> { async uninstall(): Promise<void> {
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<void> { async onEditorReady(): Promise<void> {
@@ -45,4 +63,17 @@ export class ProfilerPlugin implements IEditorPlugin {
console.log('[ProfilerPlugin] Registering menu items:', items); console.log('[ProfilerPlugin] Registering menu items:', items);
return items; return items;
} }
registerPanels(): PanelDescriptor[] {
return [
{
id: 'profiler-monitor',
title: 'Performance Monitor',
position: 'center' as any,
closable: true,
component: ProfilerDockPanel,
order: 200
}
];
}
} }

View File

@@ -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<ProfilerDataListener> = 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<void> {
try {
const status = await invoke<boolean>('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<void> {
try {
const port = parseInt(this.wsPort);
const result = await invoke<string>('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;
}
}

View File

@@ -33,6 +33,33 @@
background-color: var(--color-bg-elevated); background-color: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border-default); border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0; 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 { .header-right {

View File

@@ -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;
}

View File

@@ -29,6 +29,20 @@
color: var(--color-text-primary); color: var(--color-text-primary);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; 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 { .hierarchy-content {
@@ -133,3 +147,53 @@
.entity-item.selected .entity-name { .entity-item.selected .entity-name {
font-weight: var(--font-weight-medium); 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);
}
}