diff --git a/backend/__pycache__/debug_log.cpython-314.pyc b/backend/__pycache__/debug_log.cpython-314.pyc new file mode 100644 index 0000000..3880fcc Binary files /dev/null and b/backend/__pycache__/debug_log.cpython-314.pyc differ diff --git a/backend/ai/__pycache__/__init__.cpython-314.pyc b/backend/ai/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..f4b7b52 Binary files /dev/null and b/backend/ai/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/ai/__pycache__/openai_service.cpython-314.pyc b/backend/ai/__pycache__/openai_service.cpython-314.pyc new file mode 100644 index 0000000..83eb929 Binary files /dev/null and b/backend/ai/__pycache__/openai_service.cpython-314.pyc differ diff --git a/backend/ai/__pycache__/workflow.cpython-314.pyc b/backend/ai/__pycache__/workflow.cpython-314.pyc new file mode 100644 index 0000000..2e7e5cf Binary files /dev/null and b/backend/ai/__pycache__/workflow.cpython-314.pyc differ diff --git a/backend/api/__pycache__/__init__.cpython-314.pyc b/backend/api/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..dd90041 Binary files /dev/null and b/backend/api/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/api/__pycache__/router.cpython-314.pyc b/backend/api/__pycache__/router.cpython-314.pyc new file mode 100644 index 0000000..b560ff5 Binary files /dev/null and b/backend/api/__pycache__/router.cpython-314.pyc differ diff --git a/backend/api/v1/__pycache__/__init__.cpython-314.pyc b/backend/api/v1/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..5b90c77 Binary files /dev/null and b/backend/api/v1/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/api/v1/endpoints/__pycache__/__init__.cpython-314.pyc b/backend/api/v1/endpoints/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..ce28fe4 Binary files /dev/null and b/backend/api/v1/endpoints/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/api/v1/endpoints/__pycache__/agents.cpython-314.pyc b/backend/api/v1/endpoints/__pycache__/agents.cpython-314.pyc new file mode 100644 index 0000000..1c1ff68 Binary files /dev/null and b/backend/api/v1/endpoints/__pycache__/agents.cpython-314.pyc differ diff --git a/backend/api/v1/endpoints/__pycache__/auth.cpython-314.pyc b/backend/api/v1/endpoints/__pycache__/auth.cpython-314.pyc new file mode 100644 index 0000000..10dfd48 Binary files /dev/null and b/backend/api/v1/endpoints/__pycache__/auth.cpython-314.pyc differ diff --git a/backend/api/v1/endpoints/__pycache__/catalog.cpython-314.pyc b/backend/api/v1/endpoints/__pycache__/catalog.cpython-314.pyc new file mode 100644 index 0000000..0c5263d Binary files /dev/null and b/backend/api/v1/endpoints/__pycache__/catalog.cpython-314.pyc differ diff --git a/backend/api/v1/endpoints/__pycache__/collisions.cpython-314.pyc b/backend/api/v1/endpoints/__pycache__/collisions.cpython-314.pyc new file mode 100644 index 0000000..867abfe Binary files /dev/null and b/backend/api/v1/endpoints/__pycache__/collisions.cpython-314.pyc differ diff --git a/backend/api/v1/endpoints/__pycache__/dashboard.cpython-314.pyc b/backend/api/v1/endpoints/__pycache__/dashboard.cpython-314.pyc new file mode 100644 index 0000000..36d3c3b Binary files /dev/null and b/backend/api/v1/endpoints/__pycache__/dashboard.cpython-314.pyc differ diff --git a/backend/api/v1/endpoints/__pycache__/satellites.cpython-314.pyc b/backend/api/v1/endpoints/__pycache__/satellites.cpython-314.pyc new file mode 100644 index 0000000..254e025 Binary files /dev/null and b/backend/api/v1/endpoints/__pycache__/satellites.cpython-314.pyc differ diff --git a/backend/api/v1/endpoints/__pycache__/weather.cpython-314.pyc b/backend/api/v1/endpoints/__pycache__/weather.cpython-314.pyc new file mode 100644 index 0000000..5bc5d3c Binary files /dev/null and b/backend/api/v1/endpoints/__pycache__/weather.cpython-314.pyc differ diff --git a/backend/app/__pycache__/__init__.cpython-314.pyc b/backend/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..7b31f9f Binary files /dev/null and b/backend/app/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..9bf6dd6 Binary files /dev/null and b/backend/app/__pycache__/main.cpython-314.pyc differ diff --git a/backend/app/core/__pycache__/__init__.cpython-314.pyc b/backend/app/core/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..5e85155 Binary files /dev/null and b/backend/app/core/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/app/core/__pycache__/config.cpython-314.pyc b/backend/app/core/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000..432ef3a Binary files /dev/null and b/backend/app/core/__pycache__/config.cpython-314.pyc differ diff --git a/backend/app/core/__pycache__/error_handlers.cpython-314.pyc b/backend/app/core/__pycache__/error_handlers.cpython-314.pyc new file mode 100644 index 0000000..3391163 Binary files /dev/null and b/backend/app/core/__pycache__/error_handlers.cpython-314.pyc differ diff --git a/backend/app/core/__pycache__/exceptions.cpython-314.pyc b/backend/app/core/__pycache__/exceptions.cpython-314.pyc new file mode 100644 index 0000000..0a419c2 Binary files /dev/null and b/backend/app/core/__pycache__/exceptions.cpython-314.pyc differ diff --git a/backend/app/core/__pycache__/scheduler.cpython-314.pyc b/backend/app/core/__pycache__/scheduler.cpython-314.pyc new file mode 100644 index 0000000..1c07793 Binary files /dev/null and b/backend/app/core/__pycache__/scheduler.cpython-314.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-314.pyc b/backend/app/core/__pycache__/security.cpython-314.pyc new file mode 100644 index 0000000..4e7fde6 Binary files /dev/null and b/backend/app/core/__pycache__/security.cpython-314.pyc differ diff --git a/backend/database/__pycache__/__init__.cpython-314.pyc b/backend/database/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..1fc528c Binary files /dev/null and b/backend/database/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/database/__pycache__/session.cpython-314.pyc b/backend/database/__pycache__/session.cpython-314.pyc new file mode 100644 index 0000000..535ab50 Binary files /dev/null and b/backend/database/__pycache__/session.cpython-314.pyc differ diff --git a/backend/models/__pycache__/__init__.cpython-314.pyc b/backend/models/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..67c20eb Binary files /dev/null and b/backend/models/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/models/__pycache__/db_models.cpython-314.pyc b/backend/models/__pycache__/db_models.cpython-314.pyc new file mode 100644 index 0000000..cdbcaee Binary files /dev/null and b/backend/models/__pycache__/db_models.cpython-314.pyc differ diff --git a/backend/orbital/__pycache__/__init__.cpython-314.pyc b/backend/orbital/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..357f7ef Binary files /dev/null and b/backend/orbital/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/orbital/__pycache__/collision_engine.cpython-314.pyc b/backend/orbital/__pycache__/collision_engine.cpython-314.pyc new file mode 100644 index 0000000..2d05f0a Binary files /dev/null and b/backend/orbital/__pycache__/collision_engine.cpython-314.pyc differ diff --git a/backend/orbital/__pycache__/orbit_engine.cpython-314.pyc b/backend/orbital/__pycache__/orbit_engine.cpython-314.pyc new file mode 100644 index 0000000..2cac840 Binary files /dev/null and b/backend/orbital/__pycache__/orbit_engine.cpython-314.pyc differ diff --git a/backend/orbital/__pycache__/spacetrack.cpython-314.pyc b/backend/orbital/__pycache__/spacetrack.cpython-314.pyc new file mode 100644 index 0000000..8185431 Binary files /dev/null and b/backend/orbital/__pycache__/spacetrack.cpython-314.pyc differ diff --git a/backend/orbital/providers/__pycache__/__init__.cpython-314.pyc b/backend/orbital/providers/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..b1cab4e Binary files /dev/null and b/backend/orbital/providers/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/orbital/providers/__pycache__/base.cpython-314.pyc b/backend/orbital/providers/__pycache__/base.cpython-314.pyc new file mode 100644 index 0000000..c808296 Binary files /dev/null and b/backend/orbital/providers/__pycache__/base.cpython-314.pyc differ diff --git a/backend/orbital/providers/__pycache__/cache.cpython-314.pyc b/backend/orbital/providers/__pycache__/cache.cpython-314.pyc new file mode 100644 index 0000000..4a134f5 Binary files /dev/null and b/backend/orbital/providers/__pycache__/cache.cpython-314.pyc differ diff --git a/backend/orbital/providers/__pycache__/celestrak.cpython-314.pyc b/backend/orbital/providers/__pycache__/celestrak.cpython-314.pyc new file mode 100644 index 0000000..bee75de Binary files /dev/null and b/backend/orbital/providers/__pycache__/celestrak.cpython-314.pyc differ diff --git a/backend/orbital/providers/__pycache__/chain.cpython-314.pyc b/backend/orbital/providers/__pycache__/chain.cpython-314.pyc new file mode 100644 index 0000000..9a94df2 Binary files /dev/null and b/backend/orbital/providers/__pycache__/chain.cpython-314.pyc differ diff --git a/backend/orbital/providers/__pycache__/spacetrack.cpython-314.pyc b/backend/orbital/providers/__pycache__/spacetrack.cpython-314.pyc new file mode 100644 index 0000000..db389ce Binary files /dev/null and b/backend/orbital/providers/__pycache__/spacetrack.cpython-314.pyc differ diff --git a/backend/schemas/__pycache__/__init__.cpython-314.pyc b/backend/schemas/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..9450626 Binary files /dev/null and b/backend/schemas/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/schemas/__pycache__/api_schemas.cpython-314.pyc b/backend/schemas/__pycache__/api_schemas.cpython-314.pyc new file mode 100644 index 0000000..9c634b8 Binary files /dev/null and b/backend/schemas/__pycache__/api_schemas.cpython-314.pyc differ diff --git a/backend/services/__pycache__/__init__.cpython-314.pyc b/backend/services/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..a5f964b Binary files /dev/null and b/backend/services/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/services/__pycache__/risk_service.cpython-314.pyc b/backend/services/__pycache__/risk_service.cpython-314.pyc new file mode 100644 index 0000000..0061512 Binary files /dev/null and b/backend/services/__pycache__/risk_service.cpython-314.pyc differ diff --git a/backend/services/__pycache__/simulation_service.cpython-314.pyc b/backend/services/__pycache__/simulation_service.cpython-314.pyc new file mode 100644 index 0000000..a83f60a Binary files /dev/null and b/backend/services/__pycache__/simulation_service.cpython-314.pyc differ diff --git a/backend/services/__pycache__/weather_service.cpython-314.pyc b/backend/services/__pycache__/weather_service.cpython-314.pyc new file mode 100644 index 0000000..7183410 Binary files /dev/null and b/backend/services/__pycache__/weather_service.cpython-314.pyc differ diff --git a/backend/websocket/__pycache__/__init__.cpython-314.pyc b/backend/websocket/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..9680ade Binary files /dev/null and b/backend/websocket/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/websocket/__pycache__/manager.cpython-314.pyc b/backend/websocket/__pycache__/manager.cpython-314.pyc new file mode 100644 index 0000000..89418e7 Binary files /dev/null and b/backend/websocket/__pycache__/manager.cpython-314.pyc differ diff --git a/frontend/src/components/EarthTwin.tsx b/frontend/src/components/EarthTwin.tsx index 6ef1ef3..ed279ad 100644 --- a/frontend/src/components/EarthTwin.tsx +++ b/frontend/src/components/EarthTwin.tsx @@ -29,6 +29,7 @@ import { type ObjectCategory, } from '@/types/objectCategories'; import { LayerManagerPanel } from './OrbitLayers/LayerManagerPanel'; +import { useCesiumPerformance } from '@/hooks/useCesiumPerformance'; interface CatalogObject { id: number; @@ -138,6 +139,7 @@ export const EarthTwin = forwardRef((_props, ref) => { const tooltipRef = useRef(null); const [viewerInstance, setViewerInstance] = useState(null); const [containerEl, setContainerEl] = useState(null); + useCesiumPerformance(viewerInstance); const [datasetVersion, setDatasetVersion] = useState(0); const activeSector = useActiveSector(); const setSelectedSatelliteId = useUIStore((s) => s.setSelectedSatelliteId); diff --git a/frontend/src/components/PerformanceOverlay/PerformanceOverlay.tsx b/frontend/src/components/PerformanceOverlay/PerformanceOverlay.tsx new file mode 100644 index 0000000..ff435f6 --- /dev/null +++ b/frontend/src/components/PerformanceOverlay/PerformanceOverlay.tsx @@ -0,0 +1,547 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { usePerformanceStore, type OverlayPosition, type DiagnosticTab } from '@/store/performanceStore'; +import { MaterialIcon } from '@/components/MaterialIcon'; +import { initNetworkInterceptor } from '@/utils/networkInterceptor'; + +export const PerformanceOverlay: React.FC = () => { + const { + isEnabled, + isMinimized, + position, + customCoords, + activeTab, + fps, + frameTimeMs, + droppedFrames, + fpsHistory, + renderedSatellites, + activeEntities, + cesiumSceneUpdateTimeMs, + memory, + activeRequests, + avgResponseTimeMs, + totalRequests, + slowRequestsCount, + recentCalls, + bottlenecks, + toggleOverlay, + toggleMinimized, + setPosition, + setCustomCoords, + setActiveTab, + updateFrameMetrics, + updateMemoryMetrics, + clearBottlenecks, + } = usePerformanceStore(); + + const [isDragging, setIsDragging] = useState(false); + const dragOffsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const containerRef = useRef(null); + + // Initialize network interceptor once + useEffect(() => { + initNetworkInterceptor(); + }, []); + + // Frame rate monitoring loop + useEffect(() => { + let frameId: number; + let lastTime = performance.now(); + let frameCount = 0; + let lastFpsUpdate = performance.now(); + + const measureFrame = (now: number) => { + const delta = now - lastTime; + lastTime = now; + frameCount++; + + const isDropped = delta > 33.3; // Dropped frame threshold (> 30fps baseline) + + if (now - lastFpsUpdate >= 500) { // Update store twice per second + const currentFps = Math.min(60, Math.round((frameCount * 1000) / (now - lastFpsUpdate))); + const avgFrameTime = delta; + updateFrameMetrics(currentFps, avgFrameTime, isDropped); + + frameCount = 0; + lastFpsUpdate = now; + } + + frameId = requestAnimationFrame(measureFrame); + }; + + frameId = requestAnimationFrame(measureFrame); + return () => cancelAnimationFrame(frameId); + }, [updateFrameMetrics]); + + // Memory usage polling loop + useEffect(() => { + const updateMem = () => { + const perfObj = window.performance as unknown as { + memory?: { usedJSHeapSize: number; totalJSHeapSize: number; jsHeapSizeLimit: number }; + }; + + if (perfObj.memory) { + const usedMB = perfObj.memory.usedJSHeapSize / (1024 * 1024); + const totalMB = perfObj.memory.totalJSHeapSize / (1024 * 1024); + const limitMB = perfObj.memory.jsHeapSizeLimit / (1024 * 1024); + const percent = (usedMB / limitMB) * 100; + + updateMemoryMetrics({ + usedJSHeapSizeMB: Math.round(usedMB * 10) / 10, + totalJSHeapSizeMB: Math.round(totalMB * 10) / 10, + jsHeapSizeLimitMB: Math.round(limitMB * 10) / 10, + percentUsed: Math.round(percent * 10) / 10, + isSupported: true, + }); + } else { + updateMemoryMetrics({ isSupported: false }); + } + }; + + updateMem(); + const interval = setInterval(updateMem, 2000); + return () => clearInterval(interval); + }, [updateMemoryMetrics]); + + // Keyboard shortcut Shift + P + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.shiftKey && (e.key === 'P' || e.key === 'p')) { + if (['INPUT', 'TEXTAREA', 'SELECT'].includes((e.target as HTMLElement)?.tagName)) { + return; + } + e.preventDefault(); + toggleOverlay(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [toggleOverlay]); + + // Dragging support + const handleMouseDown = (e: React.MouseEvent) => { + if (!containerRef.current) return; + setIsDragging(true); + const rect = containerRef.current.getBoundingClientRect(); + dragOffsetRef.current = { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }; + }; + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging) return; + setCustomCoords({ + x: Math.max(10, Math.min(window.innerWidth - 300, e.clientX - dragOffsetRef.current.x)), + y: Math.max(10, Math.min(window.innerHeight - 200, e.clientY - dragOffsetRef.current.y)), + }); + }; + + const handleMouseUp = () => setIsDragging(false); + + if (isDragging) { + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + } + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, setCustomCoords]); + + if (!isEnabled) return null; + + // Positioning classes + const getPositionStyle = (): React.CSSProperties => { + if (position === 'custom') { + return { position: 'fixed', left: `${customCoords.x}px`, top: `${customCoords.y}px`, zIndex: 9999 }; + } + const styles: Record = { + 'top-right': { position: 'fixed', top: '70px', right: '16px', zIndex: 9999 }, + 'top-left': { position: 'fixed', top: '70px', left: '16px', zIndex: 9999 }, + 'bottom-right': { position: 'fixed', bottom: '16px', right: '16px', zIndex: 9999 }, + 'bottom-left': { position: 'fixed', bottom: '16px', left: '16px', zIndex: 9999 }, + custom: { position: 'fixed', top: '70px', right: '16px', zIndex: 9999 }, + }; + return styles[position]; + }; + + const getFpsColor = (val: number) => { + if (val >= 55) return 'text-emerald-400 border-emerald-500/40 bg-emerald-500/10'; + if (val >= 30) return 'text-amber-400 border-amber-500/40 bg-amber-500/10'; + return 'text-rose-400 border-rose-500/40 bg-rose-500/10'; + }; + + // Render Minimized Pill View + if (isMinimized) { + return ( +
+
+ + + PERF +
+ +
+ {fps.toFixed(0)} FPS +
+ +
+ + {renderedSatellites} +
+ + {memory.isSupported && ( +
+ + {memory.usedJSHeapSizeMB} MB +
+ )} + +
+ + +
+
+ ); + } + + // Render Full Diagnostics Panel + return ( +
+ {/* Header Bar */} +
+
+ + + PERFORMANCE OVERLAY + v1.0 +
+ +
+ {/* Position Selector */} +
+ +
+ {(['top-right', 'top-left', 'bottom-right', 'bottom-left'] as OverlayPosition[]).map((pos) => ( + + ))} +
+
+ + + +
+
+ + {/* Tabs */} +
+ {(['OVERVIEW', 'CESIUM', 'NETWORK', 'MEMORY', 'BOTTLENECKS'] as DiagnosticTab[]).map((tab) => { + const isTabActive = activeTab === tab; + const hasBottleneck = tab === 'BOTTLENECKS' && bottlenecks.length > 0; + + return ( + + ); + })} +
+ + {/* Main Tab Content Area */} +
+ {/* OVERVIEW TAB */} + {activeTab === 'OVERVIEW' && ( +
+ {/* FPS & Sparkline */} +
+
+
Frame Rate
+
+ + {fps.toFixed(0)} FPS + + ({frameTimeMs.toFixed(1)}ms) +
+
+ + {/* Sparkline Graph */} +
+ {fpsHistory.map((val, idx) => { + const pct = Math.min(100, Math.max(10, (val / 60) * 100)); + const barColor = val >= 55 ? 'bg-emerald-500' : val >= 30 ? 'bg-amber-500' : 'bg-rose-500'; + return ( +
+ ); + })} +
+
+ + {/* Quick Metrics Grid */} +
+
+
Rendered Satellites
+
{renderedSatellites.toLocaleString()}
+
+ +
+
Active Cesium Entities
+
{activeEntities.toLocaleString()}
+
+ +
+
Avg API Latency
+
{avgResponseTimeMs} ms
+
+ +
+
JS Heap Used
+
+ {memory.isSupported ? `${memory.usedJSHeapSizeMB} MB` : 'N/A'} +
+
+
+
+ )} + + {/* CESIUM SCENE TAB */} + {activeTab === 'CESIUM' && ( +
+
+
+ Cesium Scene Update: + {cesiumSceneUpdateTimeMs.toFixed(2)} ms +
+
+
+
+
+ +
+
+ Satellites Plotted: + {renderedSatellites} +
+
+ Total Scene Entities: + {activeEntities} +
+
+ Target Frame Interval: + 16.6 ms (60 FPS) +
+
+
+ )} + + {/* NETWORK TAB */} + {activeTab === 'NETWORK' && ( +
+
+
+
Active
+
{activeRequests}
+
+
+
Total
+
{totalRequests}
+
+
+
Slow (>500ms)
+
0 ? 'text-amber-400' : 'text-emerald-400'}`}> + {slowRequestsCount} +
+
+
+ +
Recent Requests
+
+ {recentCalls.length === 0 ? ( +
No network requests logged yet.
+ ) : ( + recentCalls.map((call) => ( +
+
+ {call.method} + {call.url.replace(/^https?:\/\/[^/]+/, '')} +
+ +
+ + {call.status || 'ERR'} + + {call.durationMs}ms +
+
+ )) + )} +
+
+ )} + + {/* MEMORY TAB */} + {activeTab === 'MEMORY' && ( +
+ {memory.isSupported ? ( + <> +
+
+ JS Heap Usage: + {memory.usedJSHeapSizeMB} / {memory.jsHeapSizeLimitMB} MB +
+
+
80 ? 'bg-rose-500' : memory.percentUsed > 60 ? 'bg-amber-500' : 'bg-purple-500' + }`} + /> +
+
+ +
+
+ Total Allocated Heap: + {memory.totalJSHeapSizeMB} MB +
+
+ Heap Limit: + {memory.jsHeapSizeLimitMB} MB +
+
+ Memory Utilization: + {memory.percentUsed}% +
+
+ + ) : ( +
+ Browser memory diagnostics API is not supported in this browser (Chrome / Chromium required). +
+ )} +
+ )} + + {/* BOTTLENECKS TAB */} + {activeTab === 'BOTTLENECKS' && ( +
+
+ Active Alerts + {bottlenecks.length > 0 && ( + + )} +
+ +
+ {bottlenecks.length === 0 ? ( +
+ + No performance bottlenecks detected! +
+ ) : ( + bottlenecks.map((alert) => ( +
+
+ + + {alert.type} + + + {new Date(alert.timestamp).toLocaleTimeString()} + +
+
{alert.message}
+
+ )) + )} +
+
+ )} +
+ + {/* Footer Hotkey Notice */} +
+ Toggle shortcut: Shift + P + Kepler Dev Tools +
+
+ ); +}; diff --git a/frontend/src/components/PerformanceOverlay/PerformanceToggleButton.tsx b/frontend/src/components/PerformanceOverlay/PerformanceToggleButton.tsx new file mode 100644 index 0000000..c8caa0a --- /dev/null +++ b/frontend/src/components/PerformanceOverlay/PerformanceToggleButton.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { usePerformanceStore } from '@/store/performanceStore'; +import { MaterialIcon } from '@/components/MaterialIcon'; + +export const PerformanceToggleButton: React.FC = () => { + const { isEnabled, toggleOverlay, fps, bottlenecks } = usePerformanceStore(); + const warningCount = bottlenecks.filter((b) => b.severity === 'WARNING' || b.severity === 'CRITICAL').length; + + return ( + + ); +}; diff --git a/frontend/src/components/layouts/MainLayout.tsx b/frontend/src/components/layouts/MainLayout.tsx index 05a8e67..6a01b70 100644 --- a/frontend/src/components/layouts/MainLayout.tsx +++ b/frontend/src/components/layouts/MainLayout.tsx @@ -5,6 +5,8 @@ import { useUIStore, useSidebarCollapsed, useRightDrawerOpen, logEvent } from '@ import { DynamicBackground } from '@/components/DynamicBackground/DynamicBackground'; import { LogbookPanel } from '@/components/Logbook/LogbookPanel'; import { useLogbookStore, logEvent } from '@/store/logbookStore'; +import { PerformanceOverlay } from '@/components/PerformanceOverlay/PerformanceOverlay'; +import { PerformanceToggleButton } from '@/components/PerformanceOverlay/PerformanceToggleButton'; import { NotificationCenter } from '@/components/ui/NotificationCenter'; /** Ensures the mission-init System log fires once per browser tab session. */ @@ -245,6 +247,7 @@ export const MainLayout: React.FC = () => {
{utcTime}
+
+ +
); }; diff --git a/frontend/src/hooks/useCesiumPerformance.ts b/frontend/src/hooks/useCesiumPerformance.ts new file mode 100644 index 0000000..d6f9c7b --- /dev/null +++ b/frontend/src/hooks/useCesiumPerformance.ts @@ -0,0 +1,52 @@ +import { useEffect, useRef } from 'react'; +import * as Cesium from 'cesium'; +import { usePerformanceStore } from '@/store/performanceStore'; + +/** + * Custom hook to monitor Cesium rendering performance, scene update timing, + * and active entities count. + */ +export function useCesiumPerformance(viewer: Cesium.Viewer | null): void { + const preRenderTimeRef = useRef(0); + + useEffect(() => { + if (!viewer || viewer.isDestroyed()) return; + + const scene = viewer.scene; + + const handlePreRender = () => { + preRenderTimeRef.current = performance.now(); + }; + + const handlePostRender = () => { + if (!preRenderTimeRef.current) return; + const updateTimeMs = performance.now() - preRenderTimeRef.current; + + const entityCount = viewer.entities.values.length; + let satelliteCount = 0; + + // Count entities marked as catalog satellites/objects + for (const entity of viewer.entities.values) { + if (entity.properties?.catalogData) { + satelliteCount++; + } + } + + usePerformanceStore.getState().updateCesiumMetrics( + updateTimeMs, + entityCount, + satelliteCount + ); + }; + + const removePre = scene.preRender.addEventListener(handlePreRender); + const removePost = scene.postRender.addEventListener(handlePostRender); + + return () => { + if (!scene.isDestroyed()) { + removePre(); + removePost(); + } + }; + }, [viewer]); +} diff --git a/frontend/src/store/performanceStore.ts b/frontend/src/store/performanceStore.ts new file mode 100644 index 0000000..0fa550a --- /dev/null +++ b/frontend/src/store/performanceStore.ts @@ -0,0 +1,247 @@ +import { create } from 'zustand'; + +export interface NetworkCallMetric { + id: string; + url: string; + method: string; + status: number; + durationMs: number; + timestamp: number; + isSlow: boolean; +} + +export interface BottleneckAlert { + id: string; + type: 'LOW_FPS' | 'HIGH_MEMORY' | 'SLOW_API' | 'HIGH_ENTITY_COUNT' | 'SLOW_SCENE_UPDATE'; + severity: 'WARNING' | 'CRITICAL'; + message: string; + timestamp: number; + details?: string; +} + +export interface MemoryMetrics { + usedJSHeapSizeMB: number; + totalJSHeapSizeMB: number; + jsHeapSizeLimitMB: number; + percentUsed: number; + isSupported: boolean; +} + +export type OverlayPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'custom'; +export type DiagnosticTab = 'OVERVIEW' | 'CESIUM' | 'NETWORK' | 'MEMORY' | 'BOTTLENECKS'; + +interface PerformanceState { + // Config & UI State + isEnabled: boolean; + isMinimized: boolean; + position: OverlayPosition; + customCoords: { x: number; y: number }; + activeTab: DiagnosticTab; + + // FPS & Frame Metrics + fps: number; + frameTimeMs: number; + droppedFrames: number; + fpsHistory: number[]; + + // Cesium & Scene Metrics + renderedSatellites: number; + activeEntities: number; + cesiumSceneUpdateTimeMs: number; + + // Memory Metrics + memory: MemoryMetrics; + + // Network Metrics + activeRequests: number; + avgResponseTimeMs: number; + totalRequests: number; + slowRequestsCount: number; + recentCalls: NetworkCallMetric[]; + + // Bottleneck Alerts + bottlenecks: BottleneckAlert[]; + + // Actions + toggleOverlay: () => void; + setIsEnabled: (enabled: boolean) => void; + toggleMinimized: () => void; + setMinimized: (minimized: boolean) => void; + setPosition: (pos: OverlayPosition) => void; + setCustomCoords: (coords: { x: number; y: number }) => void; + setActiveTab: (tab: DiagnosticTab) => void; + + updateFrameMetrics: (fps: number, frameTimeMs: number, isDroppedFrame: boolean) => void; + updateCesiumMetrics: (sceneTimeMs: number, entityCount: number, satelliteCount: number) => void; + updateMemoryMetrics: (memory: Partial) => void; + addNetworkCall: (call: Omit) => void; + incrementActiveRequests: () => void; + decrementActiveRequests: () => void; + addBottleneck: (alert: Omit) => void; + clearBottlenecks: () => void; +} + +const MAX_HISTORY_LENGTH = 30; +const MAX_NETWORK_LOGS = 20; +const MAX_BOTTLENECK_LOGS = 15; + +export const usePerformanceStore = create((set) => ({ + isEnabled: false, + isMinimized: false, + position: 'top-right', + customCoords: { x: 20, y: 80 }, + activeTab: 'OVERVIEW', + + fps: 60, + frameTimeMs: 16.6, + droppedFrames: 0, + fpsHistory: Array(20).fill(60), + + renderedSatellites: 0, + activeEntities: 0, + cesiumSceneUpdateTimeMs: 0, + + memory: { + usedJSHeapSizeMB: 0, + totalJSHeapSizeMB: 0, + jsHeapSizeLimitMB: 0, + percentUsed: 0, + isSupported: false, + }, + + activeRequests: 0, + avgResponseTimeMs: 0, + totalRequests: 0, + slowRequestsCount: 0, + recentCalls: [], + + bottlenecks: [], + + toggleOverlay: () => set((state) => ({ isEnabled: !state.isEnabled })), + setIsEnabled: (enabled) => set({ isEnabled: enabled }), + toggleMinimized: () => set((state) => ({ isMinimized: !state.isMinimized })), + setMinimized: (minimized) => set({ isMinimized: minimized }), + setPosition: (position) => set({ position }), + setCustomCoords: (customCoords) => set({ customCoords, position: 'custom' }), + setActiveTab: (activeTab) => set({ activeTab }), + + updateFrameMetrics: (fps, frameTimeMs, isDroppedFrame) => + set((state) => { + const newFpsHistory = [...state.fpsHistory.slice(1), fps]; + const newDropped = isDroppedFrame ? state.droppedFrames + 1 : state.droppedFrames; + + let newBottlenecks = state.bottlenecks; + if (fps < 30 && (!state.bottlenecks.length || state.bottlenecks[0]?.type !== 'LOW_FPS')) { + const alert: BottleneckAlert = { + id: `fps-${Date.now()}`, + type: 'LOW_FPS', + severity: fps < 20 ? 'CRITICAL' : 'WARNING', + message: `Frame rate dropped to ${fps.toFixed(1)} FPS (${frameTimeMs.toFixed(1)}ms frame time)`, + timestamp: Date.now(), + }; + newBottlenecks = [alert, ...state.bottlenecks.slice(0, MAX_BOTTLENECK_LOGS - 1)]; + } + + return { + fps, + frameTimeMs, + droppedFrames: newDropped, + fpsHistory: newFpsHistory, + bottlenecks: newBottlenecks, + }; + }), + + updateCesiumMetrics: (sceneTimeMs, entityCount, satelliteCount) => + set((state) => { + let newBottlenecks = state.bottlenecks; + if (sceneTimeMs > 35) { + const alert: BottleneckAlert = { + id: `cesium-${Date.now()}`, + type: 'SLOW_SCENE_UPDATE', + severity: sceneTimeMs > 60 ? 'CRITICAL' : 'WARNING', + message: `Cesium scene update took ${sceneTimeMs.toFixed(1)}ms (${entityCount} active entities)`, + timestamp: Date.now(), + }; + newBottlenecks = [alert, ...state.bottlenecks.slice(0, MAX_BOTTLENECK_LOGS - 1)]; + } + + return { + cesiumSceneUpdateTimeMs: sceneTimeMs, + activeEntities: entityCount, + renderedSatellites: satelliteCount, + bottlenecks: newBottlenecks, + }; + }), + + updateMemoryMetrics: (mem) => + set((state) => { + const updatedMemory = { ...state.memory, ...mem }; + let newBottlenecks = state.bottlenecks; + + if (updatedMemory.percentUsed > 80) { + const alert: BottleneckAlert = { + id: `mem-${Date.now()}`, + type: 'HIGH_MEMORY', + severity: updatedMemory.percentUsed > 90 ? 'CRITICAL' : 'WARNING', + message: `JS Heap memory usage at ${updatedMemory.percentUsed.toFixed(1)}% (${updatedMemory.usedJSHeapSizeMB.toFixed(1)} MB)`, + timestamp: Date.now(), + }; + newBottlenecks = [alert, ...state.bottlenecks.slice(0, MAX_BOTTLENECK_LOGS - 1)]; + } + + return { + memory: updatedMemory, + bottlenecks: newBottlenecks, + }; + }), + + addNetworkCall: (call) => + set((state) => { + const newCall: NetworkCallMetric = { + ...call, + id: `net-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`, + }; + + const newTotal = state.totalRequests + 1; + const newSlowCount = call.isSlow ? state.slowRequestsCount + 1 : state.slowRequestsCount; + const newAvgResponse = Math.round( + (state.avgResponseTimeMs * state.totalRequests + call.durationMs) / newTotal + ); + const newCalls = [newCall, ...state.recentCalls.slice(0, MAX_NETWORK_LOGS - 1)]; + + let newBottlenecks = state.bottlenecks; + if (call.isSlow) { + const alert: BottleneckAlert = { + id: `net-slow-${Date.now()}`, + type: 'SLOW_API', + severity: call.durationMs > 1500 ? 'CRITICAL' : 'WARNING', + message: `Slow API call: ${call.method} ${call.url.split('?')[0]} (${call.durationMs}ms)`, + timestamp: Date.now(), + details: `Status ${call.status}`, + }; + newBottlenecks = [alert, ...state.bottlenecks.slice(0, MAX_BOTTLENECK_LOGS - 1)]; + } + + return { + totalRequests: newTotal, + slowRequestsCount: newSlowCount, + avgResponseTimeMs: newAvgResponse, + recentCalls: newCalls, + bottlenecks: newBottlenecks, + }; + }), + + incrementActiveRequests: () => set((state) => ({ activeRequests: state.activeRequests + 1 })), + decrementActiveRequests: () => + set((state) => ({ activeRequests: Math.max(0, state.activeRequests - 1) })), + + addBottleneck: (alert) => + set((state) => ({ + bottlenecks: [ + { ...alert, id: `bn-${Date.now()}`, timestamp: Date.now() }, + ...state.bottlenecks.slice(0, MAX_BOTTLENECK_LOGS - 1), + ], + })), + + clearBottlenecks: () => set({ bottlenecks: [] }), +})); diff --git a/frontend/src/utils/networkInterceptor.ts b/frontend/src/utils/networkInterceptor.ts new file mode 100644 index 0000000..701dcd8 --- /dev/null +++ b/frontend/src/utils/networkInterceptor.ts @@ -0,0 +1,70 @@ +import { usePerformanceStore } from '@/store/performanceStore'; + +let isInterceptorInitialized = false; + +/** + * Initializes global network request interceptor for measuring latency, + * response times, active request counts, and highlighting slow API calls. + */ +export function initNetworkInterceptor(): void { + if (isInterceptorInitialized || typeof window === 'undefined') return; + isInterceptorInitialized = true; + + const originalFetch = window.fetch; + + window.fetch = async function (...args) { + const startTime = performance.now(); + const store = usePerformanceStore.getState(); + store.incrementActiveRequests(); + + let url = 'unknown'; + let method = 'GET'; + + try { + if (typeof args[0] === 'string') { + url = args[0]; + } else if (args[0] instanceof URL) { + url = args[0].toString(); + } else if (args[0] && typeof args[0] === 'object' && 'url' in args[0]) { + url = (args[0] as Request).url; + } + + if (args[1] && args[1].method) { + method = args[1].method.toUpperCase(); + } + } catch { + // Ignore URL parsing errors + } + + try { + const response = await originalFetch.apply(this, args); + const durationMs = Math.round(performance.now() - startTime); + + store.addNetworkCall({ + url, + method, + status: response.status, + durationMs, + timestamp: Date.now(), + isSlow: durationMs > 500, + }); + + return response; + } catch (error) { + const durationMs = Math.round(performance.now() - startTime); + + store.addNetworkCall({ + url, + method, + status: 0, // Network failure or CORS error + durationMs, + timestamp: Date.now(), + isSlow: durationMs > 500, + }); + + throw error; + } finally { + store.decrementActiveRequests(); + } + }; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 2768475..38694ff 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -2,13 +2,21 @@ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "es2023", - "lib": ["ES2023", "DOM"], + "lib": [ + "ES2023", + "DOM" + ], "module": "esnext", - "types": ["vite/client", "node"], + "types": [ + "vite/client", + "node" + ], "skipLibCheck": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] }, /* Bundler mode */ "moduleResolution": "bundler", @@ -17,12 +25,13 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", - /* Linting */ "noUnusedLocals": false, "noUnusedParameters": false, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 37e7ede..a2a2b06 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -17,6 +17,19 @@ export default defineConfig({ '@': path.resolve(__dirname, './src') } }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true + }, + '/ws': { + target: 'ws://localhost:8000', + ws: true + } + } + }, test: { projects: [{ extends: true,