Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added backend/__pycache__/debug_log.cpython-314.pyc
Binary file not shown.
Binary file added backend/ai/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file not shown.
Binary file added backend/ai/__pycache__/workflow.cpython-314.pyc
Binary file not shown.
Binary file added backend/api/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file added backend/api/__pycache__/router.cpython-314.pyc
Binary file not shown.
Binary file added backend/api/v1/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added backend/app/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file added backend/app/__pycache__/main.cpython-314.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added backend/models/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file not shown.
Binary file added backend/orbital/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added backend/schemas/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions frontend/src/components/EarthTwin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
type ObjectCategory,
} from '@/types/objectCategories';
import { LayerManagerPanel } from './OrbitLayers/LayerManagerPanel';
import { useCesiumPerformance } from '@/hooks/useCesiumPerformance';

interface CatalogObject {
id: number;
Expand Down Expand Up @@ -138,6 +139,7 @@ export const EarthTwin = forwardRef<EarthTwinHandle>((_props, ref) => {
const tooltipRef = useRef<HTMLDivElement>(null);
const [viewerInstance, setViewerInstance] = useState<Cesium.Viewer | null>(null);
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
useCesiumPerformance(viewerInstance);
const [datasetVersion, setDatasetVersion] = useState(0);
const activeSector = useActiveSector();
const setSelectedSatelliteId = useUIStore((s) => s.setSelectedSatelliteId);
Expand Down
547 changes: 547 additions & 0 deletions frontend/src/components/PerformanceOverlay/PerformanceOverlay.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Maintainability & Code Quality | 🟠 Major | ⚑ Quick win

The filter predicate always matches, so warningCount equals bottlenecks.length.

BottleneckAlert.severity is typed as 'WARNING' | 'CRITICAL' in performanceStore.ts at Line 16. The predicate accepts both values, so it excludes nothing. The call also allocates a new array on every render only to read its length.

If the intent is to count all alerts, use bottlenecks.length. If the intent is to highlight only critical alerts, narrow the predicate.

♻️ Proposed fix
-  const warningCount = bottlenecks.filter((b) => b.severity === 'WARNING' || b.severity === 'CRITICAL').length;
+  const warningCount = bottlenecks.length;
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const warningCount = bottlenecks.filter((b) => b.severity === 'WARNING' || b.severity === 'CRITICAL').length;
const warningCount = bottlenecks.length;
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/PerformanceOverlay/PerformanceToggleButton.tsx` at
line 7, Update the warningCount calculation in PerformanceToggleButton to use
bottlenecks.length directly if all typed alert severities should be counted;
otherwise change the predicate to count only CRITICAL alerts, matching the
intended UI behavior and avoiding unnecessary array allocation.


return (
<button
onClick={toggleOverlay}
title={`Developer Performance Overlay (Shift + P) - ${fps.toFixed(0)} FPS`}
className={`relative p-2 rounded-lg flex items-center gap-1.5 transition-all text-xs font-mono border ${
isEnabled
? 'bg-cyan-500/20 text-cyan-400 border-cyan-500/50 shadow-[0_0_12px_rgba(0,229,255,0.3)]'
: 'bg-surface-container-high/60 text-slate-400 border-surface-container-highest hover:text-slate-200 hover:border-slate-600'
}`}
>
<MaterialIcon name="speed" className="text-base" />
<span className="hidden sm:inline font-semibold">{fps.toFixed(0)} FPS</span>

{warningCount > 0 && (
<span className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" title={`${warningCount} active bottleneck alerts`} />
)}
</button>
);
};
5 changes: 5 additions & 0 deletions frontend/src/components/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -245,6 +247,7 @@ export const MainLayout: React.FC = () => {
<div className="text-primary/45 font-technical-data text-[10px] sm:text-[11px] hidden sm:block">
{utcTime}
</div>
<PerformanceToggleButton />
<div className="flex items-center gap-4">
<button
onClick={toggleRightDrawer}
Expand Down Expand Up @@ -417,6 +420,8 @@ export const MainLayout: React.FC = () => {

<NotificationCenter />
</div>

<PerformanceOverlay />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: PerformanceOverlay is mounted permanently, and its effects start the animation-frame loop, memory polling, keyboard listener, and network interception before checking isEnabled; closing or never opening the overlay only returns null from its render and does not stop those activities. This imposes continuous monitoring overhead on every dashboard session even when the developer overlay is disabled. Gate the monitoring effects on enabled state or mount the monitoring lifecycle only while enabled. [performance]

Severity Level: Major ⚠️
- ⚠️ Every dashboard session performs hidden frame monitoring.
- ⚠️ All fetches incur interceptor and store-update overhead.
- ⚠️ Disabled overlay still polls browser memory telemetry.

Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent πŸ€–
This is a comment left during a code review.

**Path:** frontend/src/components/layouts/MainLayout.tsx
**Line:** 414:414
**Comment:**
	*Performance: `PerformanceOverlay` is mounted permanently, and its effects start the animation-frame loop, memory polling, keyboard listener, and network interception before checking `isEnabled`; closing or never opening the overlay only returns `null` from its render and does not stop those activities. This imposes continuous monitoring overhead on every dashboard session even when the developer overlay is disabled. Gate the monitoring effects on enabled state or mount the monitoring lifecycle only while enabled.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
πŸ‘ | πŸ‘Ž

Comment thread
Pranitrane marked this conversation as resolved.
</div>
);
};
Expand Down
52 changes: 52 additions & 0 deletions frontend/src/hooks/useCesiumPerformance.ts
Original file line number Diff line number Diff line change
@@ -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<number>(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++;
}
Comment on lines +25 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Scanning every entity on every Cesium postRender event adds an O(n) operation to each rendered frame. Since useCesiumPerformance is installed for EarthTwin regardless of whether the overlay is enabled, large catalogs and collision sets can cause the diagnostic instrumentation itself to reduce rendering performance and distort the measured scene timings. Maintain these counts incrementally when entities are added or removed, or sample the scan less frequently. [performance]

Severity Level: Major ⚠️
- ⚠️ Cesium performs an entity scan on every rendered frame.
- ⚠️ Large catalog views receive unnecessary instrumentation work.
- ⚠️ Diagnostic overhead can distort reported scene timings.

Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent πŸ€–
This is a comment left during a code review.

**Path:** frontend/src/hooks/useCesiumPerformance.ts
**Line:** 25:32
**Comment:**
	*Performance: Scanning every entity on every Cesium `postRender` event adds an O(n) operation to each rendered frame. Since `useCesiumPerformance` is installed for `EarthTwin` regardless of whether the overlay is enabled, large catalogs and collision sets can cause the diagnostic instrumentation itself to reduce rendering performance and distort the measured scene timings. Maintain these counts incrementally when entities are added or removed, or sample the scan less frequently.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
πŸ‘ | πŸ‘Ž

}

usePerformanceStore.getState().updateCesiumMetrics(
updateTimeMs,
entityCount,
satelliteCount
);
};
Comment on lines +21 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸš€ Performance & Scalability | πŸ”΄ Critical | ⚑ Quick win

Per-frame O(n) entity scan on the render thread will degrade the frame rate it measures.

handlePostRender runs on every rendered frame. It iterates all entities and reads entity.properties?.catalogData for each one. EarthTwin.tsx adds one entity per catalog object, so this collection holds thousands of entities. The loop therefore performs thousands of property reads per frame inside Cesium's render callback.

The satellite count changes only when populateEntities runs, which is once per 5 minutes. Recomputing it per frame is not required. Two changes fix this:

  1. Sample the metrics at a fixed interval instead of every frame.
  2. Cache the satellite count and recompute it only when viewer.entities.values.length changes.

The store write also happens per frame. Each write notifies all subscribers and re-renders PerformanceOverlay, which compounds the cost.

♻️ Proposed fix: throttle sampling and cache the satellite count
 export function useCesiumPerformance(viewer: Cesium.Viewer | null): void {
   const preRenderTimeRef = useRef<number>(0);
+  const lastSampleRef = useRef<number>(0);
+  const cachedCountRef = useRef<{ total: number; satellites: number }>({ total: -1, satellites: 0 });
 
   useEffect(() => {
     if (!viewer || viewer.isDestroyed()) return;
 
     const scene = viewer.scene;
+    const SAMPLE_INTERVAL_MS = 500;
 
     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++;
-        }
-      }
+      const now = performance.now();
+      const updateTimeMs = now - preRenderTimeRef.current;
+      if (now - lastSampleRef.current < SAMPLE_INTERVAL_MS) return;
+      lastSampleRef.current = now;
+
+      const entities = viewer.entities.values;
+      const entityCount = entities.length;
+
+      // The entity set only changes when populateEntities runs, so recount
+      // only when the total changes.
+      if (cachedCountRef.current.total !== entityCount) {
+        let satellites = 0;
+        for (const entity of entities) {
+          if (entity.properties?.catalogData) satellites++;
+        }
+        cachedCountRef.current = { total: entityCount, satellites };
+      }
+      const satelliteCount = cachedCountRef.current.satellites;
 
       usePerformanceStore.getState().updateCesiumMetrics(
         updateTimeMs,
         entityCount,
         satelliteCount
       );
     };
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/useCesiumPerformance.ts` around lines 21 - 40, Update
handlePostRender to throttle metric sampling to a fixed interval rather than
processing and writing metrics on every frame. Cache satelliteCount and
recompute it only when viewer.entities.values.length changes, preserving the
existing catalogData detection logic; only call updateCesiumMetrics when the
sampling interval elapses.


const removePre = scene.preRender.addEventListener(handlePreRender);
const removePost = scene.postRender.addEventListener(handlePostRender);

return () => {
if (!scene.isDestroyed()) {
removePre();
removePost();
}
};
}, [viewer]);
Comment on lines +45 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect Cesium Event/Scene destroy semantics in the installed package.
set -euo pipefail

fd -t f 'Event.js' node_modules/cesium/Source/Core 2>/dev/null | head -5
fd -t f 'package.json' node_modules/cesium --max-depth 2 2>/dev/null | head -1 | xargs -r jq '.version'

# Show how other hooks in this repo detach Cesium scene event listeners.
rg -n -C 5 'preRender|postRender|removeEventListener' --glob 'frontend/src/**/*.ts*'

Repository: 7-Blocks/Kepler

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked ts files matching hook/EarthTwin/overlay names:"
git ls-files | rg '(^|/)useCesiumPerformance\.ts$|EarthTwin\.tsx$|PerformanceOverlay\.tsx$' || true

echo
echo "useCesiumPerformance outline/content:"
if [ -f frontend/src/hooks/useCesiumPerformance.ts ]; then
  wc -l frontend/src/hooks/useCesiumPerformance.ts
  cat -n frontend/src/hooks/useCesiumPerformance.ts
fi

echo
echo "EarthTwin relevant references:"
if [ -f frontend/src/components/EarthTwin.tsx ]; then
  rg -n -C 6 'useEffect|createViewer|viewer|setViewerInstance|destroy|scene' frontend/src/components/EarthTwin.tsx
else
  rg -n -C 6 'EarthTwin|viewer|setViewerInstance|destroy|scene' -g '*.tsx' -g '*.ts'
fi

echo
echo "PerformanceOverlay relevant references:"
if [ -f frontend/src/components/PerformanceOverlay.tsx ]; then
  wc -l frontend/src/components/PerformanceOverlay.tsx
  rg -n -C 6 'usePerformanceStore|isEnabled|requestAnimationFrame|setInterval|preRender|postRender' frontend/src/components/PerformanceOverlay.tsx
fi

echo
echo "Cesium package presence/version:"
if [ -f frontend/package.json ]; then
  jq -r '.dependencies.["cesium"], .devDependencies.["cesium"]' frontend/package.json
fi
if [ -f package.json ]; then
  jq -r '.dependencies.["cesium"]' package.json
fi

Repository: 7-Blocks/Kepler

Length of output: 18906


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Package files:"
git ls-files '*package.json' '*package-lock.json' '*yarn.lock' '*pnpm-lock.yaml' | sed -n '1,80p'

echo
echo "Frontend package version refs:"
if [ -f frontend/package.json ]; then
  jq -r '
    .dependencies.cesium // _,
    .devDependencies.cesium // _,
    .peerDependencies.cesium // _
  ' frontend/package.json
fi

echo
echo "Locate installed Cesium Event/Scene files if any:"
fd -t f 'Event|Scene' node_modules/cesium 2>/dev/null | sed -n '1,80p' || true

echo
echo "Search repository for scene destroy/event cleanup patterns:"
rg -n -C 3 'isDestroyed|addEventListener\(|removeEventListener\(|preRender|postRender|scene.*destroy|renderLoop' frontend/src --glob '*.ts' --glob '*.tsx'

Repository: 7-Blocks/Kepler

Length of output: 685


🌐 Web query:

Cesium Viewer destroy Scene preRender postRender event listeners removed

πŸ’‘ Result:

When you call viewer.destroy in CesiumJS, it performs a comprehensive cleanup of the viewer, including its associated widgets and the scene [1][2]. While viewer.destroy handles internal components, it does not automatically remove custom event listeners that you have manually attached to the scene's lifecycle events (such as preRender or postRender) or other external objects [3]. To ensure there are no memory leaks or unexpected behavior after destroying the viewer, you should manually remove any event listeners you added. Key points for managing these events: 1. Lifecycle Events (preRender/postRender): If you have attached functions to scene events, you must remove them explicitly before or during the cleanup process. The addEventListener method on these events returns a remove function that you should store and call [3]. Example: const removePreRender = viewer.scene.preRender.addEventListener(myCallback); // Later, when cleaning up: removePreRender; viewer.destroy; 2. ScreenSpaceEventHandler: If you created a custom ScreenSpaceEventHandler, you must call its.destroy method manually. The viewer's own internal screenSpaceEventHandler is managed by viewer.destroy, though updates in recent versions have improved robustness in checking its state [4][5][6]. 3. Viewer Cleanup: Calling viewer.destroy is the correct and necessary step to release WebGL resources and clear the scene, but it assumes responsibility only for the components the viewer itself instantiated [1][2]. Any custom references or listeners created outside of the viewer's direct scope remain your responsibility to clean up [7]. If you are experiencing memory issues, ensure that you are not holding onto references to the viewer or its objects in closures, as these will prevent the garbage collector from reclaiming that memory even after destroy is called [7].

Citations:


Always remove the Cesium performance listeners on cleanup.

EarthTwin destroys the viewer once and then calls setViewerInstance(null), which can make useCesiumPerformance()’s cleanup receive a destroyed scene without removing preRender/postRender handlers. Store the remove callbacks outside the scene.isDestroyed() check, so destroyed scenes do not leave the closure handlers and viewer reference registered.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/useCesiumPerformance.ts` around lines 45 - 51, Update the
cleanup returned by useCesiumPerformance to always invoke the removePre and
removePost callbacks, removing the scene.isDestroyed() guard around them.
Preserve the existing listener registration and ensure cleanup releases the
handlers and viewer reference even when the scene has already been destroyed.

}
247 changes: 247 additions & 0 deletions frontend/src/store/performanceStore.ts
Original file line number Diff line number Diff line change
@@ -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<MemoryMetrics>) => void;
addNetworkCall: (call: Omit<NetworkCallMetric, 'id'>) => void;
incrementActiveRequests: () => void;
decrementActiveRequests: () => void;
addBottleneck: (alert: Omit<BottleneckAlert, 'id' | 'timestamp'>) => void;
clearBottlenecks: () => void;
}

const MAX_HISTORY_LENGTH = 30;
const MAX_NETWORK_LOGS = 20;
const MAX_BOTTLENECK_LOGS = 15;

export const usePerformanceStore = create<PerformanceState>((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()}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

Alert id values can collide within the same millisecond.

Every BottleneckAlert id is derived only from Date.now(). updateCesiumMetrics runs per frame and addNetworkCall can complete several requests in the same millisecond. Two alerts then share an id. PerformanceOverlay.tsx at Line 515 uses alert.id as the React key, so React reports duplicate keys and can reuse the wrong element.

Use the same random-suffix pattern already applied to NetworkCallMetric at Line 202, or a module-level counter.

πŸ› Proposed fix: monotonic id helper
+let alertSeq = 0;
+const nextAlertId = (prefix: string) => `${prefix}-${Date.now()}-${++alertSeq}`;

Then replace each literal, for example:

-          id: `cesium-${Date.now()}`,
+          id: nextAlertId('cesium'),

Also applies to: 159-159, 183-183, 215-215, 241-241

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/store/performanceStore.ts` at line 136, Update all
BottleneckAlert id assignments in updateCesiumMetrics and addNetworkCall to
guarantee uniqueness beyond Date.now(), reusing the existing NetworkCallMetric
random-suffix pattern or a module-level monotonic counter. Apply the change to
each alert id literal, including the assignments near the referenced locations,
while preserving their existing id prefixes.

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)];
}
Comment on lines +157 to +166

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Every frame whose measured scene time exceeds 35 ms appends another SLOW_SCENE_UPDATE alert, with no duplicate suppression or cooldown. A sustained slowdown therefore fills the 15-entry log with identical samples within a fraction of a second, hides other alert types, and keeps the warning indicator active even when the slowdown has ended. Suppress repeated alerts for the same condition or only add one after a recovery transition/cooldown. [logic error]

Severity Level: Major ⚠️
- ⚠️ Bottleneck history fills with identical scene alerts.
- ⚠️ Earlier memory and network alerts are evicted.
- ⚠️ Developers lose a useful diagnosis timeline.

Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent πŸ€–
This is a comment left during a code review.

**Path:** frontend/src/store/performanceStore.ts
**Line:** 157:166
**Comment:**
	*Logic Error: Every frame whose measured scene time exceeds 35 ms appends another `SLOW_SCENE_UPDATE` alert, with no duplicate suppression or cooldown. A sustained slowdown therefore fills the 15-entry log with identical samples within a fraction of a second, hides other alert types, and keeps the warning indicator active even when the slowdown has ended. Suppress repeated alerts for the same condition or only add one after a recovery transition/cooldown.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
πŸ‘ | πŸ‘Ž


return {
cesiumSceneUpdateTimeMs: sceneTimeMs,
activeEntities: entityCount,
renderedSatellites: satelliteCount,
bottlenecks: newBottlenecks,
};
}),
Comment on lines +154 to +174

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸš€ Performance & Scalability | 🟠 Major | ⚑ Quick win

Two alert paths have no de-duplication, so a sustained condition floods the alert buffer.

updateFrameMetrics guards against repeating a LOW_FPS alert by inspecting state.bottlenecks[0]. The scene and memory paths have no equivalent guard. A condition that persists therefore appends a new alert on every call, evicts all other alert types from the 15-slot buffer, and notifies every store subscriber.

  • frontend/src/store/performanceStore.ts#L154-L174: updateCesiumMetrics runs once per rendered frame from useCesiumPerformance. Add a time-based cooldown before appending a SLOW_SCENE_UPDATE alert.
  • frontend/src/store/performanceStore.ts#L176-L196: updateMemoryMetrics runs every 2 seconds from the overlay. Add the same cooldown before appending a HIGH_MEMORY alert, or skip the alert when the newest entry is already HIGH_MEMORY.
πŸ“ Affects 1 file
  • frontend/src/store/performanceStore.ts#L154-L174 (this comment)
  • frontend/src/store/performanceStore.ts#L176-L196
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/store/performanceStore.ts` around lines 154 - 174, In
performanceStore.ts, update both updateCesiumMetrics (lines 154-174) and
updateMemoryMetrics (lines 176-196) to prevent sustained conditions from
appending alerts on every call: apply the same time-based cooldown used by
updateFrameMetrics before adding SLOW_SCENE_UPDATE or HIGH_MEMORY alerts, or
skip insertion when the newest bottlenecks entry has the same type. Preserve
metric updates and the existing bounded alert buffer behavior.


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: [] }),
}));
Loading
Loading