性能分析器及端口管理器
This commit is contained in:
67
packages/editor-app/src-tauri/Cargo.lock
generated
67
packages/editor-app/src-tauri/Cargo.lock
generated
@@ -362,8 +362,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
@@ -557,6 +559,12 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.4"
|
||||
@@ -718,6 +726,8 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
name = "ecs-editor"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"glob",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -725,6 +735,8 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-shell",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3269,6 +3281,17 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -3989,13 +4012,38 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.16"
|
||||
@@ -4209,6 +4257,25 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
|
||||
@@ -19,6 +19,10 @@ tauri-plugin-dialog = "2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
glob = "0.3"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = "0.21"
|
||||
futures-util = "0.3"
|
||||
chrono = "0.4"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use crate::profiler_ws::ProfilerServer;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ProjectInfo {
|
||||
@@ -23,3 +26,51 @@ impl Default for EditorConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProfilerState {
|
||||
pub server: Arc<Mutex<Option<Arc<ProfilerServer>>>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_profiler_server(
|
||||
port: u16,
|
||||
state: tauri::State<'_, ProfilerState>,
|
||||
) -> Result<String, String> {
|
||||
let mut server_lock = state.server.lock().await;
|
||||
|
||||
if server_lock.is_some() {
|
||||
return Err("Profiler server is already running".to_string());
|
||||
}
|
||||
|
||||
let server = Arc::new(ProfilerServer::new(port));
|
||||
|
||||
match server.start().await {
|
||||
Ok(_) => {
|
||||
*server_lock = Some(server);
|
||||
Ok(format!("Profiler server started on port {}", port))
|
||||
}
|
||||
Err(e) => Err(format!("Failed to start profiler server: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_profiler_server(
|
||||
state: tauri::State<'_, ProfilerState>,
|
||||
) -> Result<String, String> {
|
||||
let mut server_lock = state.server.lock().await;
|
||||
|
||||
if server_lock.is_none() {
|
||||
return Err("Profiler server is not running".to_string());
|
||||
}
|
||||
|
||||
*server_lock = None;
|
||||
Ok("Profiler server stopped".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_profiler_status(
|
||||
state: tauri::State<'_, ProfilerState>,
|
||||
) -> Result<bool, String> {
|
||||
let server_lock = state.server.lock().await;
|
||||
Ok(server_lock.is_some())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
pub mod commands;
|
||||
pub mod project;
|
||||
pub mod profiler_ws;
|
||||
|
||||
pub use commands::*;
|
||||
pub use project::*;
|
||||
pub use profiler_ws::*;
|
||||
|
||||
@@ -5,6 +5,7 @@ use tauri::Manager;
|
||||
use tauri::AppHandle;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::HashMap;
|
||||
use ecs_editor_lib::profiler_ws::ProfilerServer;
|
||||
|
||||
// IPC Commands
|
||||
#[tauri::command]
|
||||
@@ -177,10 +178,68 @@ fn toggle_devtools(app: AppHandle) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Profiler State
|
||||
pub struct ProfilerState {
|
||||
pub server: Arc<tokio::sync::Mutex<Option<Arc<ProfilerServer>>>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_profiler_server(
|
||||
port: u16,
|
||||
state: tauri::State<'_, ProfilerState>,
|
||||
) -> Result<String, String> {
|
||||
let mut server_lock = state.server.lock().await;
|
||||
|
||||
if server_lock.is_some() {
|
||||
return Err("Profiler server is already running".to_string());
|
||||
}
|
||||
|
||||
let server = Arc::new(ProfilerServer::new(port));
|
||||
|
||||
match server.start().await {
|
||||
Ok(_) => {
|
||||
*server_lock = Some(server);
|
||||
Ok(format!("Profiler server started on port {}", port))
|
||||
}
|
||||
Err(e) => Err(format!("Failed to start profiler server: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn stop_profiler_server(
|
||||
state: tauri::State<'_, ProfilerState>,
|
||||
) -> Result<String, String> {
|
||||
let mut server_lock = state.server.lock().await;
|
||||
|
||||
if server_lock.is_none() {
|
||||
return Err("Profiler server is not running".to_string());
|
||||
}
|
||||
|
||||
// 调用 stop 方法正确关闭服务器
|
||||
if let Some(server) = server_lock.as_ref() {
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
*server_lock = None;
|
||||
Ok("Profiler server stopped".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_profiler_status(
|
||||
state: tauri::State<'_, ProfilerState>,
|
||||
) -> Result<bool, String> {
|
||||
let server_lock = state.server.lock().await;
|
||||
Ok(server_lock.is_some())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let project_paths: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
let project_paths_clone = Arc::clone(&project_paths);
|
||||
|
||||
let profiler_state = ProfilerState {
|
||||
server: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
@@ -232,6 +291,7 @@ fn main() {
|
||||
})
|
||||
.setup(move |app| {
|
||||
app.manage(project_paths);
|
||||
app.manage(profiler_state);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -244,7 +304,10 @@ fn main() {
|
||||
read_file_content,
|
||||
list_directory,
|
||||
set_project_base_path,
|
||||
toggle_devtools
|
||||
toggle_devtools,
|
||||
start_profiler_server,
|
||||
stop_profiler_server,
|
||||
get_profiler_status
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
176
packages/editor-app/src-tauri/src/profiler_ws.rs
Normal file
176
packages/editor-app/src-tauri/src/profiler_ws.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_tungstenite::{accept_async, tungstenite::Message};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
|
||||
pub struct ProfilerServer {
|
||||
tx: broadcast::Sender<String>,
|
||||
port: u16,
|
||||
shutdown_tx: Arc<Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
|
||||
task_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
|
||||
}
|
||||
|
||||
impl ProfilerServer {
|
||||
pub fn new(port: u16) -> Self {
|
||||
let (tx, _) = broadcast::channel(100);
|
||||
Self {
|
||||
tx,
|
||||
port,
|
||||
shutdown_tx: Arc::new(Mutex::new(None)),
|
||||
task_handle: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let addr = format!("127.0.0.1:{}", self.port);
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
println!("[ProfilerServer] Listening on: {}", addr);
|
||||
|
||||
let tx = self.tx.clone();
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
// 存储 shutdown sender
|
||||
*self.shutdown_tx.lock().await = Some(shutdown_tx);
|
||||
|
||||
// 启动服务器任务
|
||||
let task = tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 监听新连接
|
||||
result = listener.accept() => {
|
||||
match result {
|
||||
Ok((stream, peer_addr)) => {
|
||||
println!("[ProfilerServer] New connection from: {}", peer_addr);
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(handle_connection(stream, peer_addr, tx));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ProfilerServer] Failed to accept connection: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 监听关闭信号
|
||||
_ = &mut shutdown_rx => {
|
||||
println!("[ProfilerServer] Received shutdown signal");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("[ProfilerServer] Server task ending");
|
||||
});
|
||||
|
||||
// 存储任务句柄
|
||||
*self.task_handle.lock().await = Some(task);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
println!("[ProfilerServer] Stopping server...");
|
||||
|
||||
// 发送关闭信号
|
||||
if let Some(shutdown_tx) = self.shutdown_tx.lock().await.take() {
|
||||
let _ = shutdown_tx.send(());
|
||||
}
|
||||
|
||||
// 等待任务完成
|
||||
if let Some(handle) = self.task_handle.lock().await.take() {
|
||||
let _ = handle.await;
|
||||
}
|
||||
|
||||
println!("[ProfilerServer] Server stopped");
|
||||
}
|
||||
|
||||
pub fn broadcast(&self, message: String) {
|
||||
let _ = self.tx.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
stream: TcpStream,
|
||||
peer_addr: SocketAddr,
|
||||
tx: broadcast::Sender<String>,
|
||||
) {
|
||||
let ws_stream = match accept_async(stream).await {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
eprintln!("[ProfilerServer] WebSocket error: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
|
||||
let mut rx = tx.subscribe();
|
||||
|
||||
println!("[ProfilerServer] Client {} connected", peer_addr);
|
||||
|
||||
// Send initial connection confirmation
|
||||
let _ = ws_sender
|
||||
.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "connected",
|
||||
"message": "Connected to ECS Editor Profiler"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.await;
|
||||
|
||||
// Spawn task to forward broadcast messages to this client
|
||||
let forward_task = tokio::spawn(async move {
|
||||
while let Ok(msg) = rx.recv().await {
|
||||
if ws_sender.send(Message::Text(msg)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle incoming messages from client
|
||||
while let Some(msg) = ws_receiver.next().await {
|
||||
match msg {
|
||||
Ok(Message::Text(text)) => {
|
||||
// Parse incoming debug data from game client
|
||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if json_value.get("type").and_then(|t| t.as_str()) == Some("debug_data") {
|
||||
// Broadcast to frontend (ProfilerWindow)
|
||||
tx.send(text).ok();
|
||||
} else if json_value.get("type").and_then(|t| t.as_str()) == Some("ping") {
|
||||
// Respond to ping
|
||||
let _ = tx.send(
|
||||
serde_json::json!({
|
||||
"type": "pong",
|
||||
"timestamp": chrono::Utc::now().timestamp_millis()
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
println!("[ProfilerServer] Client {} disconnected", peer_addr);
|
||||
break;
|
||||
}
|
||||
Ok(Message::Ping(data)) => {
|
||||
// Respond to WebSocket ping
|
||||
tx.send(
|
||||
serde_json::json!({
|
||||
"type": "pong",
|
||||
"data": String::from_utf8_lossy(&data)
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ProfilerServer] Error: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
forward_task.abort();
|
||||
println!("[ProfilerServer] Connection handler ended for {}", peer_addr);
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, ComponentLoaderService, PropertyMetadataService, LogService } from '@esengine/editor-core';
|
||||
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
|
||||
import { ProfilerPlugin } from './plugins/ProfilerPlugin';
|
||||
import { StartupPage } from './components/StartupPage';
|
||||
import { SceneHierarchy } from './components/SceneHierarchy';
|
||||
import { EntityInspector } from './components/EntityInspector';
|
||||
import { AssetBrowser } from './components/AssetBrowser';
|
||||
import { ConsolePanel } from './components/ConsolePanel';
|
||||
import { ProfilerPanel } from './components/ProfilerPanel';
|
||||
import { PluginManagerWindow } from './components/PluginManagerWindow';
|
||||
import { ProfilerWindow } from './components/ProfilerWindow';
|
||||
import { PortManager } from './components/PortManager';
|
||||
import { Viewport } from './components/Viewport';
|
||||
import { MenuBar } from './components/MenuBar';
|
||||
import { DockContainer, DockablePanel } from './components/DockContainer';
|
||||
@@ -25,6 +29,7 @@ localeService.registerTranslations('zh', zh);
|
||||
Core.services.registerInstance(LocaleService, localeService);
|
||||
|
||||
function App() {
|
||||
const initRef = useRef(false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [projectLoaded, setProjectLoaded] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -34,10 +39,13 @@ function App() {
|
||||
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
|
||||
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
||||
const [logService, setLogService] = useState<LogService | null>(null);
|
||||
const [uiRegistry, setUiRegistry] = useState<UIRegistry | null>(null);
|
||||
const { t, locale, changeLocale } = useLocale();
|
||||
const [status, setStatus] = useState(t('header.status.initializing'));
|
||||
const [panels, setPanels] = useState<DockablePanel[]>([]);
|
||||
const [showPluginManager, setShowPluginManager] = useState(false);
|
||||
const [showProfiler, setShowProfiler] = useState(false);
|
||||
const [showPortManager, setShowPortManager] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 禁用默认右键菜单
|
||||
@@ -54,7 +62,15 @@ function App() {
|
||||
|
||||
useEffect(() => {
|
||||
const initializeEditor = async () => {
|
||||
// 使用 ref 防止 React StrictMode 的双重调用
|
||||
if (initRef.current) {
|
||||
console.log('[App] Already initialized via ref, skipping second initialization');
|
||||
return;
|
||||
}
|
||||
initRef.current = true;
|
||||
|
||||
try {
|
||||
console.log('[App] Starting editor initialization...');
|
||||
(window as any).__ECS_FRAMEWORK__ = await import('@esengine/ecs-framework');
|
||||
|
||||
const editorScene = new Scene();
|
||||
@@ -84,8 +100,23 @@ function App() {
|
||||
|
||||
const pluginMgr = new EditorPluginManager();
|
||||
pluginMgr.initialize(coreInstance, Core.services);
|
||||
Core.services.registerInstance(EditorPluginManager, pluginMgr);
|
||||
|
||||
await pluginMgr.installEditor(new SceneInspectorPlugin());
|
||||
await pluginMgr.installEditor(new ProfilerPlugin());
|
||||
|
||||
console.log('[App] All plugins installed');
|
||||
console.log('[App] UIRegistry menu count:', uiRegistry.getAllMenus().length);
|
||||
console.log('[App] UIRegistry all menus:', uiRegistry.getAllMenus());
|
||||
console.log('[App] UIRegistry window menus:', uiRegistry.getChildMenus('window'));
|
||||
|
||||
messageHub.subscribe('ui:openWindow', (data: any) => {
|
||||
if (data.windowId === 'profiler') {
|
||||
setShowProfiler(true);
|
||||
} else if (data.windowId === 'pluginManager') {
|
||||
setShowPluginManager(true);
|
||||
}
|
||||
});
|
||||
|
||||
const greeting = await TauriAPI.greet('Developer');
|
||||
console.log(greeting);
|
||||
@@ -95,6 +126,7 @@ function App() {
|
||||
setEntityStore(entityStore);
|
||||
setMessageHub(messageHub);
|
||||
setLogService(logService);
|
||||
setUiRegistry(uiRegistry);
|
||||
setStatus(t('header.status.ready'));
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize editor:', error);
|
||||
@@ -296,6 +328,9 @@ function App() {
|
||||
<div className="editor-header">
|
||||
<MenuBar
|
||||
locale={locale}
|
||||
uiRegistry={uiRegistry || undefined}
|
||||
messageHub={messageHub || undefined}
|
||||
pluginManager={pluginManager || undefined}
|
||||
onNewScene={handleNewScene}
|
||||
onOpenScene={handleOpenScene}
|
||||
onSaveScene={handleSaveScene}
|
||||
@@ -304,6 +339,8 @@ function App() {
|
||||
onCloseProject={handleCloseProject}
|
||||
onExit={handleExit}
|
||||
onOpenPluginManager={() => setShowPluginManager(true)}
|
||||
onOpenProfiler={() => setShowProfiler(true)}
|
||||
onOpenPortManager={() => setShowPortManager(true)}
|
||||
onToggleDevtools={handleToggleDevtools}
|
||||
/>
|
||||
<div className="header-right">
|
||||
@@ -330,6 +367,14 @@ function App() {
|
||||
onClose={() => setShowPluginManager(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showProfiler && (
|
||||
<ProfilerWindow onClose={() => setShowProfiler(false)} />
|
||||
)}
|
||||
|
||||
{showPortManager && (
|
||||
<PortManager onClose={() => setShowPortManager(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { UIRegistry, MessageHub, EditorPluginManager } from '@esengine/editor-core';
|
||||
import type { MenuItem as PluginMenuItem } from '@esengine/editor-core';
|
||||
import '../styles/MenuBar.css';
|
||||
|
||||
interface MenuItem {
|
||||
@@ -12,6 +14,9 @@ interface MenuItem {
|
||||
|
||||
interface MenuBarProps {
|
||||
locale?: string;
|
||||
uiRegistry?: UIRegistry;
|
||||
messageHub?: MessageHub;
|
||||
pluginManager?: EditorPluginManager;
|
||||
onNewScene?: () => void;
|
||||
onOpenScene?: () => void;
|
||||
onSaveScene?: () => void;
|
||||
@@ -20,11 +25,16 @@ interface MenuBarProps {
|
||||
onCloseProject?: () => void;
|
||||
onExit?: () => void;
|
||||
onOpenPluginManager?: () => void;
|
||||
onOpenProfiler?: () => void;
|
||||
onOpenPortManager?: () => void;
|
||||
onToggleDevtools?: () => void;
|
||||
}
|
||||
|
||||
export function MenuBar({
|
||||
locale = 'en',
|
||||
uiRegistry,
|
||||
messageHub,
|
||||
pluginManager,
|
||||
onNewScene,
|
||||
onOpenScene,
|
||||
onSaveScene,
|
||||
@@ -33,11 +43,74 @@ export function MenuBar({
|
||||
onCloseProject,
|
||||
onExit,
|
||||
onOpenPluginManager,
|
||||
onOpenProfiler,
|
||||
onOpenPortManager,
|
||||
onToggleDevtools
|
||||
}: MenuBarProps) {
|
||||
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||
const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updateMenuItems = () => {
|
||||
if (uiRegistry && pluginManager) {
|
||||
const items = uiRegistry.getChildMenus('window');
|
||||
// 过滤掉被禁用插件的菜单项
|
||||
const enabledPlugins = pluginManager.getAllPluginMetadata()
|
||||
.filter(p => p.enabled)
|
||||
.map(p => p.name);
|
||||
|
||||
// 只显示启用插件的菜单项
|
||||
const filteredItems = items.filter(item => {
|
||||
// 检查菜单项是否属于某个插件
|
||||
return enabledPlugins.some(pluginName => {
|
||||
const plugin = pluginManager.getEditorPlugin(pluginName);
|
||||
if (plugin && plugin.registerMenuItems) {
|
||||
const pluginMenus = plugin.registerMenuItems();
|
||||
return pluginMenus.some(m => m.id === item.id);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
setPluginMenuItems(filteredItems);
|
||||
console.log('[MenuBar] Updated menu items:', filteredItems);
|
||||
} else if (uiRegistry) {
|
||||
// 如果没有 pluginManager,显示所有菜单项
|
||||
const items = uiRegistry.getChildMenus('window');
|
||||
setPluginMenuItems(items);
|
||||
console.log('[MenuBar] Updated menu items (no filter):', items);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateMenuItems();
|
||||
}, [uiRegistry, pluginManager]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messageHub) {
|
||||
const unsubscribeInstalled = messageHub.subscribe('plugin:installed', () => {
|
||||
console.log('[MenuBar] Plugin installed, updating menu items');
|
||||
updateMenuItems();
|
||||
});
|
||||
|
||||
const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => {
|
||||
console.log('[MenuBar] Plugin enabled, updating menu items');
|
||||
updateMenuItems();
|
||||
});
|
||||
|
||||
const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => {
|
||||
console.log('[MenuBar] Plugin disabled, updating menu items');
|
||||
updateMenuItems();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeInstalled();
|
||||
unsubscribeEnabled();
|
||||
unsubscribeDisabled();
|
||||
};
|
||||
}
|
||||
}, [messageHub, uiRegistry, pluginManager]);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
@@ -64,6 +137,8 @@ export function MenuBar({
|
||||
console: 'Console',
|
||||
viewport: 'Viewport',
|
||||
pluginManager: 'Plugin Manager',
|
||||
tools: 'Tools',
|
||||
portManager: 'Port Manager',
|
||||
help: 'Help',
|
||||
documentation: 'Documentation',
|
||||
about: 'About',
|
||||
@@ -93,6 +168,8 @@ export function MenuBar({
|
||||
console: '控制台',
|
||||
viewport: '视口',
|
||||
pluginManager: '插件管理器',
|
||||
tools: '工具',
|
||||
portManager: '端口管理器',
|
||||
help: '帮助',
|
||||
documentation: '文档',
|
||||
about: '关于',
|
||||
@@ -133,10 +210,20 @@ export function MenuBar({
|
||||
{ label: t('console'), disabled: true },
|
||||
{ label: t('viewport'), disabled: true },
|
||||
{ separator: true },
|
||||
...pluginMenuItems.map(item => ({
|
||||
label: item.label,
|
||||
shortcut: item.shortcut,
|
||||
disabled: item.disabled,
|
||||
onClick: item.onClick
|
||||
})),
|
||||
...(pluginMenuItems.length > 0 ? [{ separator: true }] : []),
|
||||
{ label: t('pluginManager'), onClick: onOpenPluginManager },
|
||||
{ separator: true },
|
||||
{ label: t('devtools'), onClick: onToggleDevtools }
|
||||
],
|
||||
tools: [
|
||||
{ label: t('portManager'), onClick: onOpenPortManager }
|
||||
],
|
||||
help: [
|
||||
{ label: t('documentation'), disabled: true },
|
||||
{ separator: true },
|
||||
|
||||
110
packages/editor-app/src/components/PortManager.tsx
Normal file
110
packages/editor-app/src/components/PortManager.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { X, Server, WifiOff } from 'lucide-react';
|
||||
import '../styles/PortManager.css';
|
||||
|
||||
interface PortManagerProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function PortManager({ onClose }: PortManagerProps) {
|
||||
const [isServerRunning, setIsServerRunning] = useState(false);
|
||||
const [serverPort, setServerPort] = useState<number>(8080);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [isStopping, setIsStopping] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkServerStatus();
|
||||
}, []);
|
||||
|
||||
const checkServerStatus = async () => {
|
||||
setIsChecking(true);
|
||||
try {
|
||||
const status = await invoke<boolean>('get_profiler_status');
|
||||
setIsServerRunning(status);
|
||||
} catch (error) {
|
||||
console.error('[PortManager] Failed to check server status:', error);
|
||||
setIsServerRunning(false);
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStopServer = async () => {
|
||||
setIsStopping(true);
|
||||
try {
|
||||
const result = await invoke<string>('stop_profiler_server');
|
||||
console.log('[PortManager]', result);
|
||||
setIsServerRunning(false);
|
||||
} catch (error) {
|
||||
console.error('[PortManager] Failed to stop server:', error);
|
||||
} finally {
|
||||
setIsStopping(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="port-manager-overlay" onClick={onClose}>
|
||||
<div className="port-manager" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="port-manager-header">
|
||||
<div className="port-manager-title">
|
||||
<Server size={20} />
|
||||
<h2>Port Manager</h2>
|
||||
</div>
|
||||
<button className="port-manager-close" onClick={onClose} title="Close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="port-manager-content">
|
||||
<div className="port-section">
|
||||
<h3>Profiler Server</h3>
|
||||
<div className="port-info">
|
||||
<div className="port-item">
|
||||
<span className="port-label">Status:</span>
|
||||
<span className={`port-status ${isServerRunning ? 'running' : 'stopped'}`}>
|
||||
{isChecking ? 'Checking...' : isServerRunning ? 'Running' : 'Stopped'}
|
||||
</span>
|
||||
</div>
|
||||
{isServerRunning && (
|
||||
<div className="port-item">
|
||||
<span className="port-label">Port:</span>
|
||||
<span className="port-value">{serverPort}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isServerRunning && (
|
||||
<div className="port-actions">
|
||||
<button
|
||||
className="action-btn danger"
|
||||
onClick={handleStopServer}
|
||||
disabled={isStopping}
|
||||
>
|
||||
<WifiOff size={16} />
|
||||
<span>{isStopping ? 'Stopping...' : 'Stop Server'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isServerRunning && (
|
||||
<div className="port-hint">
|
||||
<p>No server is currently running.</p>
|
||||
<p className="hint-text">Open Profiler window to start the server.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="port-tips">
|
||||
<h4>Tips</h4>
|
||||
<ul>
|
||||
<li>Use this when the Profiler server port is stuck and cannot be restarted</li>
|
||||
<li>The server will automatically stop when the Profiler window is closed</li>
|
||||
<li>Default port: 8080</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
234
packages/editor-app/src/components/ProfilerPanel.tsx
Normal file
234
packages/editor-app/src/components/ProfilerPanel.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { Activity, BarChart3, Clock, Cpu, TrendingUp, RefreshCw, Pause, Play } from 'lucide-react';
|
||||
import '../styles/ProfilerPanel.css';
|
||||
|
||||
interface SystemPerformanceData {
|
||||
name: string;
|
||||
executionTime: number;
|
||||
entityCount: number;
|
||||
averageTime: number;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export function ProfilerPanel() {
|
||||
const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
|
||||
const [totalFrameTime, setTotalFrameTime] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time');
|
||||
const animationRef = useRef<number>();
|
||||
|
||||
useEffect(() => {
|
||||
const updateProfilerData = () => {
|
||||
if (isPaused) {
|
||||
animationRef.current = requestAnimationFrame(updateProfilerData);
|
||||
return;
|
||||
}
|
||||
|
||||
const coreInstance = Core.Instance;
|
||||
if (!coreInstance || !coreInstance._performanceMonitor?.isEnabled) {
|
||||
animationRef.current = requestAnimationFrame(updateProfilerData);
|
||||
return;
|
||||
}
|
||||
|
||||
const performanceMonitor = coreInstance._performanceMonitor;
|
||||
const systemDataMap = performanceMonitor.getAllSystemData();
|
||||
const systemStatsMap = performanceMonitor.getAllSystemStats();
|
||||
|
||||
const systemsData: SystemPerformanceData[] = [];
|
||||
let total = 0;
|
||||
|
||||
for (const [name, data] of systemDataMap.entries()) {
|
||||
const stats = systemStatsMap.get(name);
|
||||
if (stats) {
|
||||
systemsData.push({
|
||||
name,
|
||||
executionTime: data.executionTime,
|
||||
entityCount: data.entityCount,
|
||||
averageTime: stats.averageTime,
|
||||
minTime: stats.minTime,
|
||||
maxTime: stats.maxTime,
|
||||
percentage: 0
|
||||
});
|
||||
total += data.executionTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate percentages
|
||||
systemsData.forEach(system => {
|
||||
system.percentage = total > 0 ? (system.executionTime / total) * 100 : 0;
|
||||
});
|
||||
|
||||
// Sort systems
|
||||
systemsData.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'time':
|
||||
return b.executionTime - a.executionTime;
|
||||
case 'average':
|
||||
return b.averageTime - a.averageTime;
|
||||
case 'name':
|
||||
return a.name.localeCompare(b.name);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
setSystems(systemsData);
|
||||
setTotalFrameTime(total);
|
||||
|
||||
animationRef.current = requestAnimationFrame(updateProfilerData);
|
||||
};
|
||||
|
||||
animationRef.current = requestAnimationFrame(updateProfilerData);
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [isPaused, sortBy]);
|
||||
|
||||
const handleReset = () => {
|
||||
const coreInstance = Core.Instance;
|
||||
if (coreInstance && coreInstance._performanceMonitor) {
|
||||
coreInstance._performanceMonitor.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0;
|
||||
const targetFrameTime = 16.67; // 60 FPS
|
||||
const isOverBudget = totalFrameTime > targetFrameTime;
|
||||
|
||||
return (
|
||||
<div className="profiler-panel">
|
||||
<div className="profiler-toolbar">
|
||||
<div className="profiler-toolbar-left">
|
||||
<div className="profiler-stats-summary">
|
||||
<div className="summary-item">
|
||||
<Clock size={14} />
|
||||
<span className="summary-label">Frame:</span>
|
||||
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
|
||||
{totalFrameTime.toFixed(2)}ms
|
||||
</span>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<Activity size={14} />
|
||||
<span className="summary-label">FPS:</span>
|
||||
<span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<BarChart3 size={14} />
|
||||
<span className="summary-label">Systems:</span>
|
||||
<span className="summary-value">{systems.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="profiler-toolbar-right">
|
||||
<select
|
||||
className="profiler-sort"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
>
|
||||
<option value="time">Sort by Time</option>
|
||||
<option value="average">Sort by Average</option>
|
||||
<option value="name">Sort by Name</option>
|
||||
</select>
|
||||
<button
|
||||
className="profiler-btn"
|
||||
onClick={() => setIsPaused(!isPaused)}
|
||||
title={isPaused ? 'Resume' : 'Pause'}
|
||||
>
|
||||
{isPaused ? <Play size={14} /> : <Pause size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className="profiler-btn"
|
||||
onClick={handleReset}
|
||||
title="Reset Statistics"
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profiler-content">
|
||||
{systems.length === 0 ? (
|
||||
<div className="profiler-empty">
|
||||
<Cpu size={48} />
|
||||
<p>No performance data available</p>
|
||||
<p className="profiler-empty-hint">
|
||||
Make sure Core debug mode is enabled and systems are running
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="profiler-systems">
|
||||
{systems.map((system, index) => (
|
||||
<div key={system.name} className="system-row">
|
||||
<div className="system-header">
|
||||
<div className="system-info">
|
||||
<span className="system-rank">#{index + 1}</span>
|
||||
<span className="system-name">{system.name}</span>
|
||||
{system.entityCount > 0 && (
|
||||
<span className="system-entities">
|
||||
({system.entityCount} entities)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="system-metrics">
|
||||
<span className="metric-time">{system.executionTime.toFixed(2)}ms</span>
|
||||
<span className="metric-percentage">{system.percentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="system-bar">
|
||||
<div
|
||||
className="system-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-stats">
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Avg:</span>
|
||||
<span className="stat-value">{system.averageTime.toFixed(2)}ms</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Min:</span>
|
||||
<span className="stat-value">{system.minTime.toFixed(2)}ms</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Max:</span>
|
||||
<span className="stat-value">{system.maxTime.toFixed(2)}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="profiler-footer">
|
||||
<div className="profiler-legend">
|
||||
<div className="legend-item">
|
||||
<div className="legend-color" style={{ background: 'var(--color-success)' }} />
|
||||
<span>Good (<8ms)</span>
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<div className="legend-color" style={{ background: 'var(--color-warning)' }} />
|
||||
<span>Warning (8-16ms)</span>
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<div className="legend-color" style={{ background: 'var(--color-danger)' }} />
|
||||
<span>Critical (>16ms)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
633
packages/editor-app/src/components/ProfilerWindow.tsx
Normal file
633
packages/editor-app/src/components/ProfilerWindow.tsx
Normal file
@@ -0,0 +1,633 @@
|
||||
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 '../styles/ProfilerWindow.css';
|
||||
|
||||
interface SystemPerformanceData {
|
||||
name: string;
|
||||
executionTime: number;
|
||||
entityCount: number;
|
||||
averageTime: number;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
percentage: number;
|
||||
level: number;
|
||||
children?: SystemPerformanceData[];
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
interface ProfilerWindowProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type DataSource = 'local' | 'remote';
|
||||
|
||||
export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
|
||||
const [totalFrameTime, setTotalFrameTime] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time');
|
||||
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 animationRef = useRef<number>();
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
// WebSocket connection management
|
||||
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);
|
||||
|
||||
return () => clearInterval(pingInterval);
|
||||
}
|
||||
}, [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 buildSystemTree = (flatSystems: Map<string, any>, statsMap: Map<string, any>): SystemPerformanceData[] => {
|
||||
const coreUpdate = flatSystems.get('Core.update');
|
||||
const servicesUpdate = flatSystems.get('Services.update');
|
||||
|
||||
if (!coreUpdate) return [];
|
||||
|
||||
const coreStats = statsMap.get('Core.update');
|
||||
const coreNode: SystemPerformanceData = {
|
||||
name: 'Core.update',
|
||||
executionTime: coreUpdate.executionTime,
|
||||
entityCount: 0,
|
||||
averageTime: coreStats?.averageTime || 0,
|
||||
minTime: coreStats?.minTime || 0,
|
||||
maxTime: coreStats?.maxTime || 0,
|
||||
percentage: 100,
|
||||
level: 0,
|
||||
children: [],
|
||||
isExpanded: true
|
||||
};
|
||||
|
||||
if (servicesUpdate) {
|
||||
const servicesStats = statsMap.get('Services.update');
|
||||
coreNode.children!.push({
|
||||
name: 'Services.update',
|
||||
executionTime: servicesUpdate.executionTime,
|
||||
entityCount: 0,
|
||||
averageTime: servicesStats?.averageTime || 0,
|
||||
minTime: servicesStats?.minTime || 0,
|
||||
maxTime: servicesStats?.maxTime || 0,
|
||||
percentage: coreUpdate.executionTime > 0
|
||||
? (servicesUpdate.executionTime / coreUpdate.executionTime) * 100
|
||||
: 0,
|
||||
level: 1,
|
||||
isExpanded: false
|
||||
});
|
||||
}
|
||||
|
||||
const sceneSystems: SystemPerformanceData[] = [];
|
||||
let sceneSystemsTotal = 0;
|
||||
|
||||
for (const [name, data] of flatSystems.entries()) {
|
||||
if (name !== 'Core.update' && name !== 'Services.update') {
|
||||
const stats = statsMap.get(name);
|
||||
if (stats) {
|
||||
sceneSystems.push({
|
||||
name,
|
||||
executionTime: data.executionTime,
|
||||
entityCount: data.entityCount,
|
||||
averageTime: stats.averageTime,
|
||||
minTime: stats.minTime,
|
||||
maxTime: stats.maxTime,
|
||||
percentage: 0,
|
||||
level: 1,
|
||||
isExpanded: false
|
||||
});
|
||||
sceneSystemsTotal += data.executionTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sceneSystems.forEach(system => {
|
||||
system.percentage = coreUpdate.executionTime > 0
|
||||
? (system.executionTime / coreUpdate.executionTime) * 100
|
||||
: 0;
|
||||
});
|
||||
|
||||
sceneSystems.sort((a, b) => b.executionTime - a.executionTime);
|
||||
coreNode.children!.push(...sceneSystems);
|
||||
|
||||
return [coreNode];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dataSource !== 'local') return;
|
||||
|
||||
const updateProfilerData = () => {
|
||||
if (isPaused) {
|
||||
animationRef.current = requestAnimationFrame(updateProfilerData);
|
||||
return;
|
||||
}
|
||||
|
||||
const coreInstance = Core.Instance;
|
||||
if (!coreInstance || !coreInstance._performanceMonitor?.isEnabled) {
|
||||
animationRef.current = requestAnimationFrame(updateProfilerData);
|
||||
return;
|
||||
}
|
||||
|
||||
const performanceMonitor = coreInstance._performanceMonitor;
|
||||
const systemDataMap = performanceMonitor.getAllSystemData();
|
||||
const systemStatsMap = performanceMonitor.getAllSystemStats();
|
||||
|
||||
const tree = buildSystemTree(systemDataMap, systemStatsMap);
|
||||
const coreData = systemDataMap.get('Core.update');
|
||||
|
||||
setSystems(tree);
|
||||
setTotalFrameTime(coreData?.executionTime || 0);
|
||||
|
||||
animationRef.current = requestAnimationFrame(updateProfilerData);
|
||||
};
|
||||
|
||||
animationRef.current = requestAnimationFrame(updateProfilerData);
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [isPaused, sortBy, dataSource]);
|
||||
|
||||
const handleReset = () => {
|
||||
if (dataSource === 'local') {
|
||||
const coreInstance = Core.Instance;
|
||||
if (coreInstance && coreInstance._performanceMonitor) {
|
||||
coreInstance._performanceMonitor.reset();
|
||||
}
|
||||
} else {
|
||||
// Reset remote data
|
||||
setSystems([]);
|
||||
setTotalFrameTime(0);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
const performance = debugData.performance;
|
||||
if (!performance) return;
|
||||
|
||||
if (!performance.systemPerformance || !Array.isArray(performance.systemPerformance)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const flatSystemsMap = new Map();
|
||||
const statsMap = new Map();
|
||||
|
||||
for (const system of performance.systemPerformance) {
|
||||
flatSystemsMap.set(system.systemName, {
|
||||
executionTime: system.lastExecutionTime || system.averageTime || 0,
|
||||
entityCount: system.entityCount || 0
|
||||
});
|
||||
|
||||
statsMap.set(system.systemName, {
|
||||
averageTime: system.averageTime || 0,
|
||||
minTime: system.minTime || 0,
|
||||
maxTime: system.maxTime || 0
|
||||
});
|
||||
}
|
||||
|
||||
const tree = buildSystemTree(flatSystemsMap, statsMap);
|
||||
setSystems(tree);
|
||||
setTotalFrameTime(performance.frameTime || 0);
|
||||
};
|
||||
|
||||
const handleDataSourceChange = (newSource: DataSource) => {
|
||||
if (newSource === 'remote' && dataSource === 'local') {
|
||||
// Switching to remote
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
} else if (newSource === 'local' && dataSource === 'remote') {
|
||||
// Switching to local
|
||||
handleDisconnect();
|
||||
}
|
||||
setDataSource(newSource);
|
||||
setSystems([]);
|
||||
setTotalFrameTime(0);
|
||||
};
|
||||
|
||||
const toggleExpand = (systemName: string) => {
|
||||
const toggleNode = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
|
||||
return nodes.map(node => {
|
||||
if (node.name === systemName) {
|
||||
return { ...node, isExpanded: !node.isExpanded };
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: toggleNode(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
setSystems(toggleNode(systems));
|
||||
};
|
||||
|
||||
const flattenTree = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
|
||||
const result: SystemPerformanceData[] = [];
|
||||
for (const node of nodes) {
|
||||
result.push(node);
|
||||
if (node.isExpanded && node.children) {
|
||||
result.push(...flattenTree(node.children));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0;
|
||||
const targetFrameTime = 16.67;
|
||||
const isOverBudget = totalFrameTime > targetFrameTime;
|
||||
|
||||
let displaySystems = viewMode === 'tree' ? flattenTree(systems) : systems;
|
||||
|
||||
// Apply search filter
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
if (viewMode === 'tree') {
|
||||
displaySystems = displaySystems.filter(sys =>
|
||||
sys.name.toLowerCase().includes(query)
|
||||
);
|
||||
} else {
|
||||
// For table view, flatten and filter
|
||||
const flatList: SystemPerformanceData[] = [];
|
||||
const flatten = (nodes: SystemPerformanceData[]) => {
|
||||
for (const node of nodes) {
|
||||
flatList.push(node);
|
||||
if (node.children) flatten(node.children);
|
||||
}
|
||||
};
|
||||
flatten(systems);
|
||||
displaySystems = flatList.filter(sys =>
|
||||
sys.name.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
} else if (viewMode === 'table') {
|
||||
// For table view without search, flatten all
|
||||
const flatList: SystemPerformanceData[] = [];
|
||||
const flatten = (nodes: SystemPerformanceData[]) => {
|
||||
for (const node of nodes) {
|
||||
flatList.push(node);
|
||||
if (node.children) flatten(node.children);
|
||||
}
|
||||
};
|
||||
flatten(systems);
|
||||
displaySystems = flatList;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="profiler-window-overlay" onClick={onClose}>
|
||||
<div className="profiler-window" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="profiler-window-header">
|
||||
<div className="profiler-window-title">
|
||||
<BarChart3 size={20} />
|
||||
<h2>Performance Profiler</h2>
|
||||
{isPaused && (
|
||||
<span className="paused-indicator">PAUSED</span>
|
||||
)}
|
||||
</div>
|
||||
<button className="profiler-window-close" onClick={onClose} title="Close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="profiler-window-toolbar">
|
||||
<div className="profiler-toolbar-left">
|
||||
<div className="profiler-mode-switch">
|
||||
<button
|
||||
className={`mode-btn ${dataSource === 'local' ? 'active' : ''}`}
|
||||
onClick={() => handleDataSourceChange('local')}
|
||||
title="Local Core Instance"
|
||||
>
|
||||
<Cpu size={14} />
|
||||
<span>Local</span>
|
||||
</button>
|
||||
<button
|
||||
className={`mode-btn ${dataSource === 'remote' ? 'active' : ''}`}
|
||||
onClick={() => handleDataSourceChange('remote')}
|
||||
title="Remote Game Connection"
|
||||
>
|
||||
<Server size={14} />
|
||||
<span>Remote</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{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}
|
||||
/>
|
||||
{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"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dataSource === 'local' && (
|
||||
<div className="profiler-stats-summary">
|
||||
<div className="summary-item">
|
||||
<Clock size={14} />
|
||||
<span className="summary-label">Frame:</span>
|
||||
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
|
||||
{totalFrameTime.toFixed(2)}ms
|
||||
</span>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<Activity size={14} />
|
||||
<span className="summary-label">FPS:</span>
|
||||
<span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<BarChart3 size={14} />
|
||||
<span className="summary-label">Systems:</span>
|
||||
<span className="summary-value">{systems.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="profiler-toolbar-right">
|
||||
<div className="profiler-search">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search systems..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="view-mode-switch">
|
||||
<button
|
||||
className={`view-mode-btn ${viewMode === 'table' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('table')}
|
||||
title="Table View"
|
||||
>
|
||||
<Table2 size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`view-mode-btn ${viewMode === 'tree' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('tree')}
|
||||
title="Tree View"
|
||||
>
|
||||
<TreePine size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className={`profiler-btn ${isPaused ? 'paused' : ''}`}
|
||||
onClick={() => setIsPaused(!isPaused)}
|
||||
title={isPaused ? 'Resume' : 'Pause'}
|
||||
>
|
||||
{isPaused ? <Play size={14} /> : <Pause size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className="profiler-btn"
|
||||
onClick={handleReset}
|
||||
title="Reset Statistics"
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profiler-window-content">
|
||||
{displaySystems.length === 0 ? (
|
||||
<div className="profiler-empty">
|
||||
<Cpu size={48} />
|
||||
<p>No performance data available</p>
|
||||
<p className="profiler-empty-hint">
|
||||
{searchQuery ? 'No systems match your search' : 'Make sure Core debug mode is enabled and systems are running'}
|
||||
</p>
|
||||
</div>
|
||||
) : viewMode === 'table' ? (
|
||||
<table className="profiler-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="col-name">System Name</th>
|
||||
<th className="col-time">Current</th>
|
||||
<th className="col-time">Average</th>
|
||||
<th className="col-time">Min</th>
|
||||
<th className="col-time">Max</th>
|
||||
<th className="col-percent">%</th>
|
||||
<th className="col-entities">Entities</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displaySystems.map((system) => (
|
||||
<tr key={system.name} className={`level-${system.level}`}>
|
||||
<td className="col-name">
|
||||
<span className="system-name-cell" style={{ paddingLeft: `${system.level * 16}px` }}>
|
||||
{system.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="col-time">
|
||||
<span className={`time-value ${system.executionTime > targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}>
|
||||
{system.executionTime.toFixed(2)}ms
|
||||
</span>
|
||||
</td>
|
||||
<td className="col-time">{system.averageTime.toFixed(2)}ms</td>
|
||||
<td className="col-time">{system.minTime.toFixed(2)}ms</td>
|
||||
<td className="col-time">{system.maxTime.toFixed(2)}ms</td>
|
||||
<td className="col-percent">{system.percentage.toFixed(1)}%</td>
|
||||
<td className="col-entities">{system.entityCount || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="profiler-tree">
|
||||
{displaySystems.map((system) => (
|
||||
<div key={system.name} className={`tree-row level-${system.level}`}>
|
||||
<div className="tree-row-header">
|
||||
<div className="tree-row-left">
|
||||
{system.children && system.children.length > 0 && (
|
||||
<button
|
||||
className="expand-btn"
|
||||
onClick={() => toggleExpand(system.name)}
|
||||
>
|
||||
{system.isExpanded ? '▼' : '▶'}
|
||||
</button>
|
||||
)}
|
||||
<span className="system-name">{system.name}</span>
|
||||
{system.entityCount > 0 && (
|
||||
<span className="system-entities">({system.entityCount})</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="tree-row-right">
|
||||
<span className={`time-value ${system.executionTime > targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}>
|
||||
{system.executionTime.toFixed(2)}ms
|
||||
</span>
|
||||
<span className="percentage-badge">{system.percentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tree-row-stats">
|
||||
<span>Avg: {system.averageTime.toFixed(2)}ms</span>
|
||||
<span>Min: {system.minTime.toFixed(2)}ms</span>
|
||||
<span>Max: {system.maxTime.toFixed(2)}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="profiler-window-footer">
|
||||
<div className="profiler-legend">
|
||||
<div className="legend-item">
|
||||
<div className="legend-color" style={{ background: 'var(--color-success)' }} />
|
||||
<span>Good (<8ms)</span>
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<div className="legend-color" style={{ background: 'var(--color-warning)' }} />
|
||||
<span>Warning (8-16ms)</span>
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<div className="legend-color" style={{ background: 'var(--color-danger)' }} />
|
||||
<span>Critical (>16ms)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
packages/editor-app/src/plugins/ProfilerPlugin.tsx
Normal file
48
packages/editor-app/src/plugins/ProfilerPlugin.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { IEditorPlugin, EditorPluginCategory, MenuItem, MessageHub } from '@esengine/editor-core';
|
||||
|
||||
/**
|
||||
* Profiler Plugin
|
||||
*
|
||||
* Displays real-time performance metrics for ECS systems
|
||||
*/
|
||||
export class ProfilerPlugin implements IEditorPlugin {
|
||||
readonly name = '@esengine/profiler';
|
||||
readonly version = '1.0.0';
|
||||
readonly displayName = 'Performance Profiler';
|
||||
readonly category = EditorPluginCategory.Tool;
|
||||
readonly description = 'Real-time performance monitoring for ECS systems';
|
||||
readonly icon = '📊';
|
||||
|
||||
private messageHub: MessageHub | null = null;
|
||||
|
||||
async install(_core: Core, services: ServiceContainer): Promise<void> {
|
||||
this.messageHub = services.resolve(MessageHub);
|
||||
console.log('[ProfilerPlugin] Installed');
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
console.log('[ProfilerPlugin] Uninstalled');
|
||||
}
|
||||
|
||||
async onEditorReady(): Promise<void> {
|
||||
console.log('[ProfilerPlugin] Editor is ready');
|
||||
}
|
||||
|
||||
registerMenuItems(): MenuItem[] {
|
||||
const items = [
|
||||
{
|
||||
id: 'window.profiler',
|
||||
label: 'Profiler',
|
||||
parentId: 'window',
|
||||
order: 100,
|
||||
onClick: () => {
|
||||
console.log('[ProfilerPlugin] Menu item clicked!');
|
||||
this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' });
|
||||
}
|
||||
}
|
||||
];
|
||||
console.log('[ProfilerPlugin] Registering menu items:', items);
|
||||
return items;
|
||||
}
|
||||
}
|
||||
242
packages/editor-app/src/styles/PortManager.css
Normal file
242
packages/editor-app/src/styles/PortManager.css
Normal file
@@ -0,0 +1,242 @@
|
||||
.port-manager-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--z-index-modal);
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.port-manager {
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.port-manager-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
background: var(--color-bg-overlay);
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.port-manager-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.port-manager-title h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.port-manager-close {
|
||||
padding: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.port-manager-close:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.port-manager-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.port-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.port-section h3 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.port-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--color-bg-inset);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.port-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.port-label {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.port-status {
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.port-status.running {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.port-status.stopped {
|
||||
background: rgba(107, 114, 128, 0.2);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.port-value {
|
||||
font-family: var(--font-family-mono);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.port-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover:not(:disabled) {
|
||||
background: var(--color-danger-dark);
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.port-hint {
|
||||
padding: 16px;
|
||||
background: var(--color-bg-inset);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.port-hint p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.port-hint p:first-child {
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 12px !important;
|
||||
color: var(--color-text-tertiary) !important;
|
||||
}
|
||||
|
||||
.port-tips {
|
||||
padding: 16px;
|
||||
background: var(--color-bg-inset);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.port-tips h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.port-tips ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.port-tips li {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.port-manager-overlay,
|
||||
.port-manager,
|
||||
.action-btn,
|
||||
.port-manager-close {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
303
packages/editor-app/src/styles/ProfilerPanel.css
Normal file
303
packages/editor-app/src/styles/ProfilerPanel.css
Normal file
@@ -0,0 +1,303 @@
|
||||
.profiler-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--color-bg-base);
|
||||
}
|
||||
|
||||
.profiler-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-bg-elevated);
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profiler-toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.profiler-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.profiler-stats-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.summary-item svg {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-family: var(--font-family-mono);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.summary-value.over-budget {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.summary-value.low-fps {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.profiler-sort {
|
||||
padding: 4px 8px;
|
||||
background: var(--color-bg-inset);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.profiler-sort:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.profiler-sort:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.profiler-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.profiler-btn:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.profiler-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.profiler-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.profiler-content::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-elevated);
|
||||
}
|
||||
|
||||
.profiler-content::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border-default);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.profiler-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.profiler-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--color-text-tertiary);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profiler-empty p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.profiler-empty-hint {
|
||||
font-size: 11px !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.profiler-systems {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.system-row {
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.system-row:hover {
|
||||
border-color: var(--color-border-strong);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.system-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.system-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.system-rank {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 22px;
|
||||
background: var(--color-bg-inset);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-mono);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.system-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.system-entities {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.system-metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-time {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-mono);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.metric-percentage {
|
||||
font-size: 12px;
|
||||
font-family: var(--font-family-mono);
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-bg-inset);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.system-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--color-bg-inset);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.system-bar-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.system-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-family: var(--font-family-mono);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.profiler-footer {
|
||||
padding: 10px 12px;
|
||||
background: var(--color-bg-elevated);
|
||||
border-top: 1px solid var(--color-border-default);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profiler-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.system-row,
|
||||
.system-bar-fill,
|
||||
.profiler-btn {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
848
packages/editor-app/src/styles/ProfilerWindow.css
Normal file
848
packages/editor-app/src/styles/ProfilerWindow.css
Normal file
@@ -0,0 +1,848 @@
|
||||
.profiler-window-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--z-index-modal);
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.profiler-window {
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
height: 80vh;
|
||||
max-height: 700px;
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-window-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
background: var(--color-bg-overlay);
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profiler-window-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.profiler-window-title h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.paused-indicator {
|
||||
padding: 4px 10px;
|
||||
background: var(--color-warning);
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
border-radius: var(--radius-sm);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-window-close {
|
||||
padding: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profiler-window-close:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.profiler-window-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
background: var(--color-bg-elevated);
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profiler-toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.profiler-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.profiler-mode-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--color-bg-inset);
|
||||
padding: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.profiler-connection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.connection-port {
|
||||
width: 80px;
|
||||
padding: 6px 10px;
|
||||
background: var(--color-bg-inset);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-family-mono);
|
||||
outline: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.connection-port:hover:not(:disabled) {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.connection-port:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.connection-port:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.connection-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.connection-btn.connect {
|
||||
background: var(--color-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.connection-btn.connect:hover {
|
||||
background: var(--color-success-dark);
|
||||
}
|
||||
|
||||
.connection-btn.disconnect {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.connection-btn.disconnect:hover {
|
||||
background: var(--color-danger-dark);
|
||||
}
|
||||
|
||||
.connection-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.connection-btn.connect:disabled:hover {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.connection-status.connected {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.connection-status.error {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.profiler-stats-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.summary-item svg {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-family: var(--font-family-mono);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.summary-value.over-budget {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.summary-value.low-fps {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.profiler-sort {
|
||||
padding: 4px 8px;
|
||||
background: var(--color-bg-inset);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.profiler-sort:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.profiler-sort:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.profiler-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.profiler-btn:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.profiler-btn.paused {
|
||||
background: var(--color-warning);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.profiler-btn.paused:hover {
|
||||
background: var(--color-warning-dark);
|
||||
}
|
||||
|
||||
.profiler-window-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.profiler-window-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.profiler-window-content::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-elevated);
|
||||
}
|
||||
|
||||
.profiler-window-content::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border-default);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.profiler-window-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.profiler-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--color-text-tertiary);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profiler-empty p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.profiler-empty-hint {
|
||||
font-size: 11px !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.profiler-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: var(--color-bg-inset);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.profiler-search:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.profiler-search svg {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-primary);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* View Mode Switch */
|
||||
.view-mode-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background: var(--color-bg-inset);
|
||||
padding: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.view-mode-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-mode-btn:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.view-mode-btn.active {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Table View */
|
||||
.profiler-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.profiler-table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-bg-overlay);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.profiler-table th {
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
border-bottom: 2px solid var(--color-border-default);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.profiler-table tbody tr {
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.profiler-table tbody tr:hover {
|
||||
background: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
.profiler-table tbody tr.level-0 {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profiler-table tbody tr.level-0:hover {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.profiler-table td {
|
||||
padding: 10px 16px;
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.col-name {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.col-time {
|
||||
width: 12%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.col-percent {
|
||||
width: 8%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.col-entities {
|
||||
width: 10%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.system-name-cell {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.time-value.warning {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.time-value.critical {
|
||||
color: var(--color-danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Tree View */
|
||||
.profiler-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tree-row {
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 16px;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.tree-row:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-border-strong);
|
||||
}
|
||||
|
||||
.tree-row.level-0 {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
border-color: rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.tree-row.level-1 {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
.tree-row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tree-row-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tree-row-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.percentage-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px;
|
||||
background: var(--color-bg-inset);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.tree-row-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.profiler-systems {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.system-row {
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.system-row.level-0 {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.05) 100%);
|
||||
border: 2px solid rgba(99, 102, 241, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.system-row.level-1 {
|
||||
margin-left: 24px;
|
||||
border-left: 3px solid rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.system-row.level-1::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
top: 50%;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.system-row:hover {
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.system-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.system-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgb(99, 102, 241);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.expand-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.system-rank {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 22px;
|
||||
background: var(--color-bg-inset);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-mono);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.system-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.system-row.level-0 .system-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: rgb(99, 102, 241);
|
||||
text-shadow: 0 1px 2px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.system-entities {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.system-metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-time {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-mono);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.system-row.level-0 .metric-time {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgb(99, 102, 241);
|
||||
}
|
||||
|
||||
.metric-percentage {
|
||||
font-size: 12px;
|
||||
font-family: var(--font-family-mono);
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-bg-inset);
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.system-row.level-0 .metric-percentage {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: rgb(99, 102, 241);
|
||||
font-size: 13px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.system-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--color-bg-inset);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.system-row.level-0 .system-bar {
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.system-bar-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease, background 0.3s ease;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.system-bar-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.2) 50%,
|
||||
rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
|
||||
.system-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-family: var(--font-family-mono);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.profiler-window-footer {
|
||||
padding: 12px 20px;
|
||||
background: var(--color-bg-overlay);
|
||||
border-top: 1px solid var(--color-border-default);
|
||||
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profiler-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.profiler-window-overlay,
|
||||
.profiler-window,
|
||||
.system-row,
|
||||
.system-bar-fill,
|
||||
.profiler-btn,
|
||||
.profiler-window-close {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
--color-bg-elevated: #252526;
|
||||
--color-bg-overlay: #2d2d2d;
|
||||
--color-bg-input: #3c3c3c;
|
||||
--color-bg-inset: #181818;
|
||||
--color-bg-hover: #2a2d2e;
|
||||
--color-bg-active: #37373d;
|
||||
|
||||
@@ -29,6 +30,7 @@
|
||||
--color-success: #4ec9b0;
|
||||
--color-warning: #ce9178;
|
||||
--color-error: #f48771;
|
||||
--color-danger: #f14c4c;
|
||||
--color-info: #4fc1ff;
|
||||
|
||||
/* 颜色系统 - 特殊 */
|
||||
|
||||
@@ -63,9 +63,12 @@ export class EditorPluginManager extends PluginManager {
|
||||
this.pluginMetadata.set(plugin.name, metadata);
|
||||
|
||||
try {
|
||||
console.log('[EditorPluginManager] Checking registerMenuItems:', !!plugin.registerMenuItems);
|
||||
if (plugin.registerMenuItems) {
|
||||
const menuItems = plugin.registerMenuItems();
|
||||
console.log('[EditorPluginManager] Got menu items:', menuItems);
|
||||
this.uiRegistry.registerMenus(menuItems);
|
||||
console.log('[EditorPluginManager] Registered menu items to UIRegistry');
|
||||
logger.debug(`Registered ${menuItems.length} menu items for ${plugin.name}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user