收集远端数据再profiler dockpanel上
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}, [projectLoaded, entityStore, messageHub, logService, locale, currentProjectPath, t]);
|
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, 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}
|
||||||
|
|||||||
190
packages/editor-app/src/components/ProfilerDockPanel.tsx
Normal file
190
packages/editor-app/src/components/ProfilerDockPanel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if server is running and stop it
|
const checkStatus = () => {
|
||||||
invoke<boolean>('get_profiler_status')
|
setIsConnected(profilerService.isConnected());
|
||||||
.then(isRunning => {
|
setIsServerRunning(profilerService.isServerActive());
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 => (
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export const zh: Translations = {
|
|||||||
initializing: '初始化中...',
|
initializing: '初始化中...',
|
||||||
ready: '编辑器就绪',
|
ready: '编辑器就绪',
|
||||||
failed: '初始化失败',
|
failed: '初始化失败',
|
||||||
projectOpened: '项目已打开'
|
projectOpened: '项目已打开',
|
||||||
|
remoteConnected: '远程游戏已连接'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hierarchy: {
|
hierarchy: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
270
packages/editor-app/src/services/ProfilerService.ts
Normal file
270
packages/editor-app/src/services/ProfilerService.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
270
packages/editor-app/src/styles/ProfilerDockPanel.css
Normal file
270
packages/editor-app/src/styles/ProfilerDockPanel.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user