Skip to content

Commit 5adea45

Browse files
committed
phaser-optimization
1 parent 93479a6 commit 5adea45

File tree

6 files changed

+314
-75
lines changed

6 files changed

+314
-75
lines changed

agent/pkg/logs/parse.go

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"regexp"
66
"strconv"
77
"strings"
8+
"sync"
89
"time"
910
)
1011

@@ -41,16 +42,24 @@ type TraefikLog struct {
4142
RequestUserAgent string `json:"RequestUserAgent"`
4243
}
4344

45+
// OPTIMIZATION: Compile regex once at package initialization
4446
var clfRegex = regexp.MustCompile(`^(\S+) - (\S+) \[([^\]]+)\] "(\S+) (\S+) (\S+)" (\d+) (\d+) "([^"]*)" "([^"]*)" (\d+) "([^"]*)" "([^"]*)" (\d+)ms`)
4547

48+
// OPTIMIZED: ParseTraefikLog with early validation
4649
func ParseTraefikLog(logLine string) (*TraefikLog, error) {
50+
// OPTIMIZATION: Avoid TrimSpace if possible, check length first
51+
if len(logLine) == 0 {
52+
return nil, nil
53+
}
54+
4755
logLine = strings.TrimSpace(logLine)
48-
56+
4957
if logLine == "" {
5058
return nil, nil
5159
}
5260

53-
if strings.HasPrefix(logLine, "{") {
61+
// OPTIMIZATION: Direct byte check instead of HasPrefix for better performance
62+
if logLine[0] == '{' {
5463
return parseJSONLog(logLine)
5564
}
5665

@@ -101,8 +110,11 @@ func parseCLFLog(logLine string) (*TraefikLog, error) {
101110
return log, nil
102111
}
103112

113+
// OPTIMIZED: ParseTraefikLogs with pre-allocation
104114
func ParseTraefikLogs(logLines []string) []*TraefikLog {
105-
var logs []*TraefikLog
115+
// OPTIMIZATION: Pre-allocate slice with capacity to reduce reallocations
116+
logs := make([]*TraefikLog, 0, len(logLines))
117+
106118
for _, line := range logLines {
107119
log, err := ParseTraefikLog(line)
108120
if err == nil && log != nil {
@@ -111,3 +123,54 @@ func ParseTraefikLogs(logLines []string) []*TraefikLog {
111123
}
112124
return logs
113125
}
126+
127+
func ParseTraefikLogsBatched(logLines []string, batchSize int) []*TraefikLog {
128+
if batchSize <= 0 {
129+
batchSize = 1000
130+
}
131+
132+
numBatches := (len(logLines) + batchSize - 1) / batchSize
133+
results := make([][]*TraefikLog, numBatches)
134+
135+
var wg sync.WaitGroup
136+
137+
for i := 0; i < numBatches; i++ {
138+
wg.Add(1)
139+
go func(batchIdx int) {
140+
defer wg.Done()
141+
142+
start := batchIdx * batchSize
143+
end := start + batchSize
144+
if end > len(logLines) {
145+
end = len(logLines)
146+
}
147+
148+
batch := logLines[start:end]
149+
batchResults := make([]*TraefikLog, 0, len(batch))
150+
151+
for _, line := range batch {
152+
log, err := ParseTraefikLog(line)
153+
if err == nil && log != nil {
154+
batchResults = append(batchResults, log)
155+
}
156+
}
157+
158+
results[batchIdx] = batchResults
159+
}(i)
160+
}
161+
162+
wg.Wait()
163+
164+
// Merge results
165+
totalLogs := 0
166+
for _, batch := range results {
167+
totalLogs += len(batch)
168+
}
169+
170+
merged := make([]*TraefikLog, 0, totalLogs)
171+
for _, batch := range results {
172+
merged = append(merged, batch...)
173+
}
174+
175+
return merged
176+
}

dashboard/components/AgentHealthDashboard.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// dashboard/components/AgentHealthDashboard.tsx
22
'use client';
33

4-
import { useState } from 'react';
4+
import React, { useState } from 'react';
55
import { useAgents } from '@/lib/contexts/AgentContext';
66
import { useAgentHealth } from '@/lib/hooks/useAgentHealth';
77
import { Badge } from '@/components/ui/badge';
@@ -36,8 +36,30 @@ export default function AgentHealthDashboard() {
3636
},
3737
});
3838

39-
const overallHealth = getOverallHealth();
40-
const unhealthyAgents = getUnhealthyAgents();
39+
// SAFETY FIX: Add error boundary for health metrics calculation
40+
const overallHealth = React.useMemo(() => {
41+
try {
42+
return getOverallHealth();
43+
} catch (error) {
44+
console.error('Failed to calculate overall health:', error);
45+
return {
46+
totalAgents: agents.length,
47+
onlineAgents: 0,
48+
offlineAgents: agents.length,
49+
averageResponseTime: 0,
50+
overallUptime: 0,
51+
};
52+
}
53+
}, [getOverallHealth, agents.length]);
54+
55+
const unhealthyAgents = React.useMemo(() => {
56+
try {
57+
return getUnhealthyAgents();
58+
} catch (error) {
59+
console.error('Failed to get unhealthy agents:', error);
60+
return [];
61+
}
62+
}, [getUnhealthyAgents]);
4163

4264
const getStatusBadge = (isOnline: boolean) => {
4365
return isOnline ? (
@@ -102,7 +124,7 @@ export default function AgentHealthDashboard() {
102124
<Badge variant="secondary">{overallHealth.overallUptime.toFixed(1)}%</Badge>
103125
</div>
104126
<div className="text-2xl font-bold text-gray-900 mb-1">
105-
{overallHealth.overallUptime.toFixed(1)}%
127+
{(overallHealth?.overallUptime ?? 0).toFixed(1)}%
106128
</div>
107129
<div className="text-sm text-gray-600">Overall Uptime</div>
108130
</div>
@@ -121,7 +143,7 @@ export default function AgentHealthDashboard() {
121143
onChange={(e) => setAutoRefresh(e.target.checked)}
122144
className="rounded border-gray-300 text-red-600 focus:ring-red-500"
123145
/>
124-
Auto-refresh (30s)
146+
Auto-refresh (5min)
125147
</label>
126148
<Button
127149
onClick={() => checkAllAgents()}

dashboard/components/dashboard/GlobeToMapTransform.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,33 +327,33 @@ export function GlobeToMapTransform({ locations = [] }: Props) {
327327
aria-label="Interactive Globe/Map Visualization"
328328
/>
329329
<div className="absolute bottom-4 right-4 flex flex-col items-end gap-2 z-10">
330-
<div className="flex flex-col gap-1 bg-white/10 dark:bg-black/20 backdrop-blur-sm p-1 rounded-lg border border-neutral-200/20">
330+
<div className="flex flex-col bg-white dark:bg-neutral-950 shadow-md rounded-lg border border-neutral-200 dark:border-neutral-800 overflow-hidden">
331331
<Button
332332
onClick={handleZoomIn}
333333
variant="ghost"
334334
size="icon"
335-
className="w-8 h-8 hover:bg-neutral-100 dark:hover:bg-neutral-800"
335+
className="w-8 h-8 rounded-none hover:bg-neutral-100 dark:hover:bg-neutral-800 border-b border-neutral-100 dark:border-neutral-800"
336336
>
337-
<Plus className="w-4 h-4" />
337+
<Plus className="w-4 h-4 text-neutral-600 dark:text-neutral-400" />
338338
</Button>
339339
<Button
340340
onClick={handleZoomOut}
341341
variant="ghost"
342342
size="icon"
343-
className="w-8 h-8 hover:bg-neutral-100 dark:hover:bg-neutral-800"
343+
className="w-8 h-8 rounded-none hover:bg-neutral-100 dark:hover:bg-neutral-800"
344344
>
345-
<Minus className="w-4 h-4" />
345+
<Minus className="w-4 h-4 text-neutral-600 dark:text-neutral-400" />
346346
</Button>
347347
</div>
348348

349349
<div className="flex gap-2">
350-
<Button onClick={handleAnimate} disabled={isAnimating} className="cursor-pointer min-w-[120px] rounded shadow-sm">
350+
<Button onClick={handleAnimate} disabled={isAnimating} className="cursor-pointer min-w-[120px] rounded shadow-sm bg-red-600 hover:bg-red-700 text-white border-0">
351351
{isAnimating ? "Animating..." : progress[0] === 0 ? "Unroll Globe" : "Roll to Globe"}
352352
</Button>
353353
<Button
354354
onClick={handleReset}
355355
variant="outline"
356-
className="cursor-pointer min-w-[80px] hover:bg-neutral-100 bg-white dark:bg-black backdrop-blur-sm rounded shadow-sm"
356+
className="cursor-pointer min-w-[80px] hover:bg-neutral-100 bg-white dark:bg-black backdrop-blur-sm rounded shadow-sm border-neutral-200 dark:border-neutral-800"
357357
>
358358
Reset
359359
</Button>

dashboard/lib/contexts/AgentContext.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,32 +236,57 @@ export function AgentProvider({ children }: { children: React.ReactNode }) {
236236
}
237237
}, [agents, selectedAgent, selectAgent]);
238238

239-
// Check agent status
239+
// REFACTORED: Check agent status with race condition prevention
240240
const checkAgentStatus = useCallback(async (id: string): Promise<boolean> => {
241241
const agent = agents.find(a => a.id === id);
242-
if (!agent) return false;
243-
244-
// Update status to checking
245-
await updateAgent(id, { status: 'checking' });
242+
if (!agent) {
243+
console.warn(`Agent ${id} not found for status check`);
244+
return false;
245+
}
246246

247247
try {
248+
// OPTIMIZATION: Set checking status optimistically without await
249+
// This prevents blocking and reduces race conditions
250+
updateAgent(id, { status: 'checking' }).catch(err => {
251+
console.error('Failed to update checking status:', err);
252+
});
253+
254+
// FIXED: Add timeout to prevent hanging
255+
const controller = new AbortController();
256+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
257+
248258
const response = await fetch('/api/agents/check-status', {
249259
method: 'POST',
250260
headers: { 'Content-Type': 'application/json' },
251261
body: JSON.stringify({ agentUrl: agent.url, agentToken: agent.token }),
262+
signal: controller.signal,
252263
});
253264

265+
clearTimeout(timeoutId);
266+
254267
const data = await response.json();
255268
const isOnline = response.ok && data.online;
256269

270+
// FIXED: Single state update instead of multiple sequential updates
257271
await updateAgent(id, {
258272
status: isOnline ? 'online' : 'offline',
259273
lastSeen: isOnline ? new Date() : undefined,
260274
});
261275

262276
return isOnline;
263277
} catch (error) {
264-
await updateAgent(id, { status: 'offline' });
278+
// IMPROVED: Better error logging and handling
279+
if (error instanceof Error && error.name === 'AbortError') {
280+
console.warn(`Agent ${id} status check timeout`);
281+
} else {
282+
console.error(`Agent ${id} status check failed:`, error);
283+
}
284+
285+
// Non-blocking status update
286+
updateAgent(id, { status: 'offline' }).catch(err => {
287+
console.error('Failed to update offline status:', err);
288+
});
289+
265290
return false;
266291
}
267292
}, [agents, updateAgent]);

dashboard/lib/hooks/useAgentHealth.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export function useAgentHealth(options: HealthMonitorOptions = {}) {
3737
const healthMetricsRef = useRef(healthMetrics);
3838
const onStatusChangeRef = useRef(onStatusChange);
3939

40+
// RACE CONDITION FIX: Track ongoing checks to prevent duplicate requests
41+
const ongoingChecksRef = useRef<Set<string>>(new Set());
42+
4043
// PERFORMANCE FIX: Pause polling when tab is not visible
4144
useEffect(() => {
4245
const handleVisibilityChange = () => {
@@ -69,6 +72,28 @@ export function useAgentHealth(options: HealthMonitorOptions = {}) {
6972
}, []);
7073

7174
const checkSingleAgent = useCallback(async (agent: Agent): Promise<AgentHealthMetrics> => {
75+
// RACE CONDITION FIX: Skip if already checking this agent
76+
if (ongoingChecksRef.current.has(agent.id)) {
77+
// Return cached metrics or a default
78+
const cached = healthMetricsRef.current[agent.id];
79+
if (cached) {
80+
return cached;
81+
}
82+
// Return a placeholder while check is ongoing
83+
return {
84+
agentId: agent.id,
85+
isOnline: false,
86+
responseTime: 0,
87+
lastChecked: new Date(),
88+
consecutiveFailures: 0,
89+
uptime: 0,
90+
error: 'Check already in progress',
91+
};
92+
}
93+
94+
// Mark this agent as being checked
95+
ongoingChecksRef.current.add(agent.id);
96+
7297
const startTime = Date.now();
7398
let isOnline = false;
7499
let error: string | undefined;
@@ -77,6 +102,9 @@ export function useAgentHealth(options: HealthMonitorOptions = {}) {
77102
isOnline = await checkAgentStatus(agent.id);
78103
} catch (err) {
79104
error = err instanceof Error ? err.message : 'Unknown error';
105+
} finally {
106+
// CLEANUP: Always remove from ongoing checks
107+
ongoingChecksRef.current.delete(agent.id);
80108
}
81109

82110
const responseTime = Date.now() - startTime;
@@ -93,6 +121,7 @@ export function useAgentHealth(options: HealthMonitorOptions = {}) {
93121
};
94122
}, [checkAgentStatus, calculateUptime]);
95123

124+
// REFACTORED: Use Promise.allSettled to prevent crashes from single agent failures
96125
const checkAllAgents = useCallback(async () => {
97126
if (agents.length === 0) {
98127
setIsMonitoring(false);
@@ -101,18 +130,41 @@ export function useAgentHealth(options: HealthMonitorOptions = {}) {
101130

102131
setIsMonitoring(true);
103132

104-
const results = await Promise.all(
133+
// CRITICAL FIX: Use Promise.allSettled instead of Promise.all
134+
// This prevents the entire health check from failing if one agent fails
135+
const results = await Promise.allSettled(
105136
agents.map(agent => checkSingleAgent(agent))
106137
);
107138

108139
const newMetrics: Record<string, AgentHealthMetrics> = {};
109-
results.forEach(metric => {
110-
newMetrics[metric.agentId] = metric;
111140

112-
// Trigger status change callback if status changed
113-
const previousStatus = healthMetricsRef.current[metric.agentId]?.isOnline;
114-
if (previousStatus !== undefined && previousStatus !== metric.isOnline && onStatusChangeRef.current) {
115-
onStatusChangeRef.current(metric.agentId, metric.isOnline);
141+
results.forEach((result, index) => {
142+
if (result.status === 'fulfilled') {
143+
const metric = result.value;
144+
newMetrics[metric.agentId] = metric;
145+
146+
// Trigger status change callback if status changed
147+
const previousStatus = healthMetricsRef.current[metric.agentId]?.isOnline;
148+
if (previousStatus !== undefined && previousStatus !== metric.isOnline && onStatusChangeRef.current) {
149+
onStatusChangeRef.current(metric.agentId, metric.isOnline);
150+
}
151+
} else {
152+
// IMPROVED: Handle rejected promises gracefully
153+
const agent = agents[index];
154+
console.error(`Health check failed for agent ${agent?.id}:`, result.reason);
155+
156+
// Create a failure metric for the agent
157+
if (agent) {
158+
newMetrics[agent.id] = {
159+
agentId: agent.id,
160+
isOnline: false,
161+
responseTime: 0,
162+
lastChecked: new Date(),
163+
consecutiveFailures: (healthMetricsRef.current[agent.id]?.consecutiveFailures || 0) + 1,
164+
uptime: 0,
165+
error: result.reason instanceof Error ? result.reason.message : 'Health check failed',
166+
};
167+
}
116168
}
117169
});
118170

0 commit comments

Comments
 (0)