收集远端数据再profiler dockpanel上
This commit is contained in:
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 { Core } from '@esengine/ecs-framework';
|
||||
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { ProfilerService } from '../services/ProfilerService';
|
||||
import '../styles/ProfilerWindow.css';
|
||||
|
||||
interface SystemPerformanceData {
|
||||
@@ -31,45 +31,27 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
const [dataSource, setDataSource] = useState<DataSource>('local');
|
||||
const [viewMode, setViewMode] = useState<'tree' | 'table'>('table');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [wsPort, setWsPort] = useState('8080');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const [isServerRunning, setIsServerRunning] = useState(false);
|
||||
const animationRef = useRef<number>();
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
// WebSocket connection management
|
||||
// Check ProfilerService connection status
|
||||
useEffect(() => {
|
||||
if (dataSource === 'remote' && isConnected && wsRef.current) {
|
||||
// Keep WebSocket connection alive
|
||||
const pingInterval = setInterval(() => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
}, 5000);
|
||||
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
|
||||
|
||||
return () => clearInterval(pingInterval);
|
||||
if (!profilerService) {
|
||||
return;
|
||||
}
|
||||
}, [dataSource, isConnected]);
|
||||
|
||||
// Cleanup WebSocket and stop server on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
|
||||
// Check if server is running and stop it
|
||||
invoke<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));
|
||||
const checkStatus = () => {
|
||||
setIsConnected(profilerService.isConnected());
|
||||
setIsServerRunning(profilerService.isServerActive());
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
const interval = setInterval(checkStatus, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const buildSystemTree = (flatSystems: Map<string, any>, statsMap: Map<string, any>): SystemPerformanceData[] => {
|
||||
@@ -144,6 +126,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
return [coreNode];
|
||||
};
|
||||
|
||||
// Subscribe to local performance data
|
||||
useEffect(() => {
|
||||
if (dataSource !== 'local') return;
|
||||
|
||||
@@ -181,6 +164,39 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
};
|
||||
}, [isPaused, sortBy, dataSource]);
|
||||
|
||||
// Subscribe to remote performance data from ProfilerService
|
||||
useEffect(() => {
|
||||
if (dataSource !== 'remote') return;
|
||||
|
||||
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
|
||||
|
||||
if (!profilerService) {
|
||||
console.warn('[ProfilerWindow] ProfilerService not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribe = profilerService.subscribe((data) => {
|
||||
if (isPaused) return;
|
||||
|
||||
handleRemoteDebugData({
|
||||
performance: {
|
||||
frameTime: data.totalFrameTime,
|
||||
systemPerformance: data.systems.map(sys => ({
|
||||
systemName: sys.name,
|
||||
lastExecutionTime: sys.executionTime,
|
||||
averageTime: sys.averageTime,
|
||||
minTime: 0,
|
||||
maxTime: 0,
|
||||
entityCount: sys.entityCount,
|
||||
percentage: sys.percentage
|
||||
}))
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [dataSource, isPaused]);
|
||||
|
||||
const handleReset = () => {
|
||||
if (dataSource === 'local') {
|
||||
const coreInstance = Core.Instance;
|
||||
@@ -194,84 +210,6 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (isConnecting) return;
|
||||
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
|
||||
setIsConnecting(true);
|
||||
setConnectionError(null);
|
||||
|
||||
try {
|
||||
const port = parseInt(wsPort);
|
||||
const result = await invoke<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) => {
|
||||
if (isPaused) return;
|
||||
@@ -310,9 +248,6 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
} else if (newSource === 'local' && dataSource === 'remote') {
|
||||
// Switching to local
|
||||
handleDisconnect();
|
||||
}
|
||||
setDataSource(newSource);
|
||||
setSystems([]);
|
||||
@@ -424,42 +359,25 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
|
||||
{dataSource === 'remote' && (
|
||||
<div className="profiler-connection">
|
||||
<input
|
||||
type="text"
|
||||
className="connection-port"
|
||||
placeholder="Port"
|
||||
value={wsPort}
|
||||
onChange={(e) => setWsPort(e.target.value)}
|
||||
disabled={isConnected || isConnecting}
|
||||
/>
|
||||
<div className="connection-port-display">
|
||||
<Server size={14} />
|
||||
<span>Port: 8080</span>
|
||||
</div>
|
||||
{isConnected ? (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<div className="connection-status-indicator connected">
|
||||
<Wifi size={14} />
|
||||
<span>{isConnecting ? 'Connecting...' : 'Connect'}</span>
|
||||
</button>
|
||||
)}
|
||||
{isConnected && (
|
||||
<span className="connection-status connected">Connected</span>
|
||||
)}
|
||||
{isConnecting && (
|
||||
<span className="connection-status connected">Connecting...</span>
|
||||
)}
|
||||
{connectionError && (
|
||||
<span className="connection-status error" title={connectionError}>Error</span>
|
||||
<span>Connected</span>
|
||||
</div>
|
||||
) : isServerRunning ? (
|
||||
<div className="connection-status-indicator waiting">
|
||||
<WifiOff size={14} />
|
||||
<span>Waiting for game...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="connection-status-indicator disconnected">
|
||||
<WifiOff size={14} />
|
||||
<span>Server Off</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { Box, Layers } from 'lucide-react';
|
||||
import { Box, Layers, Wifi } from 'lucide-react';
|
||||
import { ProfilerService, RemoteEntity } from '../services/ProfilerService';
|
||||
import '../styles/SceneHierarchy.css';
|
||||
|
||||
interface SceneHierarchyProps {
|
||||
@@ -12,9 +13,12 @@ interface SceneHierarchyProps {
|
||||
|
||||
export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) {
|
||||
const [entities, setEntities] = useState<Entity[]>([]);
|
||||
const [remoteEntities, setRemoteEntities] = useState<RemoteEntity[]>([]);
|
||||
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const { t } = useLocale();
|
||||
|
||||
// Subscribe to local entity changes
|
||||
useEffect(() => {
|
||||
const updateEntities = () => {
|
||||
setEntities(entityStore.getRootEntities());
|
||||
@@ -39,23 +43,79 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
};
|
||||
}, [entityStore, messageHub]);
|
||||
|
||||
// Subscribe to remote entity data from ProfilerService
|
||||
useEffect(() => {
|
||||
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
|
||||
|
||||
if (!profilerService) {
|
||||
console.warn('[SceneHierarchy] ProfilerService not available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[SceneHierarchy] Subscribing to ProfilerService');
|
||||
|
||||
const unsubscribe = profilerService.subscribe((data) => {
|
||||
const connected = profilerService.isConnected();
|
||||
console.log('[SceneHierarchy] Received data, connected:', connected, 'entities:', data.entities?.length || 0);
|
||||
setIsRemoteConnected(connected);
|
||||
|
||||
if (connected && data.entities && data.entities.length > 0) {
|
||||
console.log('[SceneHierarchy] Setting remote entities:', data.entities);
|
||||
setRemoteEntities(data.entities);
|
||||
} else {
|
||||
setRemoteEntities([]);
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const handleEntityClick = (entity: Entity) => {
|
||||
entityStore.selectEntity(entity);
|
||||
};
|
||||
|
||||
// Determine which entities to display
|
||||
const displayEntities = isRemoteConnected ? remoteEntities : entities;
|
||||
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
|
||||
|
||||
return (
|
||||
<div className="scene-hierarchy">
|
||||
<div className="hierarchy-header">
|
||||
<Layers size={16} className="hierarchy-header-icon" />
|
||||
<h3>{t('hierarchy.title')}</h3>
|
||||
{showRemoteIndicator && (
|
||||
<div className="remote-indicator" title="Showing remote entities">
|
||||
<Wifi size={12} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="hierarchy-content scrollable">
|
||||
{entities.length === 0 ? (
|
||||
{displayEntities.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<Box size={48} strokeWidth={1.5} className="empty-icon" />
|
||||
<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>
|
||||
) : 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">
|
||||
{entities.map(entity => (
|
||||
|
||||
Reference in New Issue
Block a user