-
Notifications
You must be signed in to change notification settings - Fork 41
feat: implement real-time performance overlay (#168) #182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
|
|
||
| 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> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = () => { | |
| <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} | ||
|
|
@@ -417,6 +420,8 @@ export const MainLayout: React.FC = () => { | |
|
|
||
| <NotificationCenter /> | ||
| </div> | ||
|
|
||
| <PerformanceOverlay /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Severity Level: Major
|
||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Scanning every entity on every Cesium Severity Level: Major
|
||
| } | ||
|
|
||
| usePerformanceStore.getState().updateCesiumMetrics( | ||
| updateTimeMs, | ||
| entityCount, | ||
| satelliteCount | ||
| ); | ||
| }; | ||
|
Comment on lines
+21
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
The satellite count changes only when
The store write also happens per frame. Each write notifies all subscribers and re-renders β»οΈ 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 |
||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
fiRepository: 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:
π‘ 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.
π€ Prompt for AI Agents |
||
| } | ||
| 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()}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π‘ Minor | β‘ Quick win Alert Every Use the same random-suffix pattern already applied to π 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 |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Every frame whose measured scene time exceeds 35 ms appends another Severity Level: Major
|
||
|
|
||
| return { | ||
| cesiumSceneUpdateTimeMs: sceneTimeMs, | ||
| activeEntities: entityCount, | ||
| renderedSatellites: satelliteCount, | ||
| bottlenecks: newBottlenecks, | ||
| }; | ||
| }), | ||
|
Comment on lines
+154
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
π Affects 1 file
π€ Prompt for AI Agents |
||
|
|
||
| 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: [] }), | ||
| })); | ||
There was a problem hiding this comment.
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
filterpredicate always matches, sowarningCountequalsbottlenecks.length.BottleneckAlert.severityis typed as'WARNING' | 'CRITICAL'inperformanceStore.tsat 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
π Committable suggestion
π€ Prompt for AI Agents