收集远端数据再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

@@ -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 { 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>
)}

View File

@@ -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 => (