收集远端数据再profiler dockpanel上
This commit is contained in:
@@ -46,6 +46,8 @@ function App() {
|
||||
const [showPluginManager, setShowPluginManager] = useState(false);
|
||||
const [showProfiler, setShowProfiler] = useState(false);
|
||||
const [showPortManager, setShowPortManager] = useState(false);
|
||||
const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0);
|
||||
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 禁用默认右键菜单
|
||||
@@ -60,6 +62,57 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (messageHub) {
|
||||
const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => {
|
||||
console.log('[App] Plugin enabled, updating panels');
|
||||
setPluginUpdateTrigger(prev => prev + 1);
|
||||
});
|
||||
|
||||
const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => {
|
||||
console.log('[App] Plugin disabled, updating panels');
|
||||
setPluginUpdateTrigger(prev => prev + 1);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeEnabled();
|
||||
unsubscribeDisabled();
|
||||
};
|
||||
}
|
||||
}, [messageHub]);
|
||||
|
||||
// 监听远程连接状态
|
||||
useEffect(() => {
|
||||
const checkConnection = () => {
|
||||
const profilerService = (window as any).__PROFILER_SERVICE__;
|
||||
if (profilerService && profilerService.isConnected()) {
|
||||
if (!isRemoteConnected) {
|
||||
console.log('[App] Remote game connected');
|
||||
setIsRemoteConnected(true);
|
||||
setStatus(t('header.status.remoteConnected'));
|
||||
}
|
||||
} else {
|
||||
if (isRemoteConnected) {
|
||||
console.log('[App] Remote game disconnected');
|
||||
setIsRemoteConnected(false);
|
||||
if (projectLoaded) {
|
||||
const projectService = Core.services.resolve(ProjectService);
|
||||
const componentRegistry = Core.services.resolve(ComponentRegistry);
|
||||
const componentCount = componentRegistry?.getRegisteredComponents().length || 0;
|
||||
setStatus(t('header.status.projectOpened') + (componentCount > 0 ? ` (${componentCount} components registered)` : ''));
|
||||
} else {
|
||||
setStatus(t('header.status.ready'));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkConnection();
|
||||
const interval = setInterval(checkConnection, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [projectLoaded, isRemoteConnected, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const initializeEditor = async () => {
|
||||
// 使用 ref 防止 React StrictMode 的双重调用
|
||||
@@ -244,8 +297,8 @@ function App() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectLoaded && entityStore && messageHub && logService) {
|
||||
setPanels([
|
||||
if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) {
|
||||
const corePanels: DockablePanel[] = [
|
||||
{
|
||||
id: 'scene-hierarchy',
|
||||
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
||||
@@ -281,9 +334,41 @@ function App() {
|
||||
content: <ConsolePanel logService={logService} />,
|
||||
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) => {
|
||||
setPanels(prevPanels =>
|
||||
@@ -325,7 +410,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<div className="editor-container">
|
||||
<div className="editor-header">
|
||||
<div className={`editor-header ${isRemoteConnected ? 'remote-connected' : ''}`}>
|
||||
<MenuBar
|
||||
locale={locale}
|
||||
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 { 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 => (
|
||||
|
||||
@@ -14,7 +14,8 @@ export const en: Translations = {
|
||||
initializing: 'Initializing...',
|
||||
ready: 'Editor Ready',
|
||||
failed: 'Initialization Failed',
|
||||
projectOpened: 'Project Opened'
|
||||
projectOpened: 'Project Opened',
|
||||
remoteConnected: 'Remote Game Connected'
|
||||
}
|
||||
},
|
||||
hierarchy: {
|
||||
|
||||
@@ -14,7 +14,8 @@ export const zh: Translations = {
|
||||
initializing: '初始化中...',
|
||||
ready: '编辑器就绪',
|
||||
failed: '初始化失败',
|
||||
projectOpened: '项目已打开'
|
||||
projectOpened: '项目已打开',
|
||||
remoteConnected: '远程游戏已连接'
|
||||
}
|
||||
},
|
||||
hierarchy: {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { IEditorPlugin, EditorPluginCategory, MenuItem, MessageHub } from '@esengine/editor-core';
|
||||
import { IEditorPlugin, EditorPluginCategory, MenuItem, MessageHub, PanelDescriptor } from '@esengine/editor-core';
|
||||
import { ProfilerDockPanel } from '../components/ProfilerDockPanel';
|
||||
import { ProfilerService } from '../services/ProfilerService';
|
||||
|
||||
/**
|
||||
* Profiler Plugin
|
||||
@@ -15,14 +17,30 @@ export class ProfilerPlugin implements IEditorPlugin {
|
||||
readonly icon = '📊';
|
||||
|
||||
private messageHub: MessageHub | null = null;
|
||||
private profilerService: ProfilerService | null = null;
|
||||
|
||||
async install(_core: Core, services: ServiceContainer): Promise<void> {
|
||||
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> {
|
||||
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> {
|
||||
@@ -45,4 +63,17 @@ export class ProfilerPlugin implements IEditorPlugin {
|
||||
console.log('[ProfilerPlugin] Registering menu items:', items);
|
||||
return items;
|
||||
}
|
||||
|
||||
registerPanels(): PanelDescriptor[] {
|
||||
return [
|
||||
{
|
||||
id: 'profiler-monitor',
|
||||
title: 'Performance Monitor',
|
||||
position: 'center' as any,
|
||||
closable: true,
|
||||
component: ProfilerDockPanel,
|
||||
order: 200
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-header.remote-connected {
|
||||
background-color: rgba(16, 185, 129, 0.15);
|
||||
border-bottom-color: rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
.editor-header.remote-connected .status {
|
||||
color: rgb(16, 185, 129);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.editor-header.remote-connected .status::before {
|
||||
background-color: rgb(16, 185, 129);
|
||||
animation: pulse-green 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-green {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
|
||||
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);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.remote-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background-color: rgba(16, 185, 129, 0.15);
|
||||
border: 1px solid rgba(16, 185, 129, 0.5);
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgb(16, 185, 129);
|
||||
font-size: var(--font-size-xs);
|
||||
animation: pulse-green 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hierarchy-content {
|
||||
@@ -133,3 +147,53 @@
|
||||
.entity-item.selected .entity-name {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* Remote entity styles */
|
||||
.entity-item.remote-entity {
|
||||
cursor: default;
|
||||
border-left: 2px solid rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
.entity-item.remote-entity:hover {
|
||||
background-color: rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.entity-item.remote-entity::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.entity-item.remote-entity.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.entity-item.remote-entity.disabled .entity-name {
|
||||
text-decoration: line-through;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.component-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
background-color: rgba(16, 185, 129, 0.2);
|
||||
border: 1px solid rgba(16, 185, 129, 0.5);
|
||||
border-radius: var(--radius-full);
|
||||
color: rgb(16, 185, 129);
|
||||
font-size: 10px;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse-green {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user