import { useGetDevGoroutinesQuery, useGetDevPprofQuery } from "@/lib/store"; import type { GoroutineGroup } from "@/lib/store/apis/devApi"; import { isDevelopmentMode } from "@/lib/utils/port"; import { Activity, AlertTriangle, ChevronDown, ChevronRight, ChevronUp, Cpu, EyeOff, HardDrive, RotateCcw, TrendingUp, X, } from "lucide-react"; import { formatBytes } from "@/lib/utils/strings"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; // Format nanoseconds to human-readable string function formatNs(ns: number): string { if (ns < 1000) return `${ns}ns`; if (ns < 1000000) return `${(ns / 1000).toFixed(1)}µs`; if (ns < 1000000000) return `${(ns / 1000000).toFixed(1)}ms`; return `${(ns / 1000000000).toFixed(2)}s`; } // Format timestamp to HH:MM:SS function formatTime(timestamp: string): string { const date = new Date(timestamp); return date.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); } // Truncate function name for display function truncateFunction(fn: string): string { const parts = fn.split("/"); const last = parts[parts.length - 1]; if (last.length > 40) { return "..." + last.slice(-37); } return last; } // Get category badge color function getCategoryColor(category: string): string { switch (category) { case "per-request": return "text-amber-400 bg-amber-400/10"; case "background": return "text-blue-400 bg-blue-400/10"; default: return "text-zinc-400 bg-zinc-400/10"; } } // Extract file path from stack (first line containing .go:) function getStackFilePath(stack: string[]): string { for (const line of stack) { // Match file path like "/path/to/file.go:123" and extract just the path const match = line.match(/^\s*([^\s]+\.go):\d+/); if (match) { return match[1]; } } return ""; } // Generate a stable ID for a goroutine group function getGoroutineId(g: GoroutineGroup): string { return `${g.top_func}::${g.state}::${g.count}::${g.wait_minutes ?? 0}`; } // localStorage key for skipped goroutine file paths const SKIPPED_GOROUTINE_FILES_KEY = "devProfiler.skippedGoroutineFiles"; const PROFILER_VISIBLE_KEY = "devProfiler.isVisible"; const PROFILER_EXPANDED_KEY = "devProfiler.isExpanded"; // Load skipped goroutine file paths from localStorage function loadSkippedGoroutineFiles(): Set { if (typeof window === "undefined") return new Set(); try { const stored = localStorage.getItem(SKIPPED_GOROUTINE_FILES_KEY); return stored ? new Set(JSON.parse(stored)) : new Set(); } catch { return new Set(); } } // Save skipped goroutine file paths to localStorage function saveSkippedGoroutineFiles(skipped: Set): void { if (typeof window === "undefined") return; try { localStorage.setItem(SKIPPED_GOROUTINE_FILES_KEY, JSON.stringify([...skipped])); } catch { // Ignore storage errors } } function loadBooleanFromStorage(key: string, defaultValue: boolean): boolean { if (typeof window === "undefined") return defaultValue; try { const stored = localStorage.getItem(key); if (stored === null) return defaultValue; if (stored === "true") return true; if (stored === "false") return false; return defaultValue; } catch { return defaultValue; } } function saveBooleanToStorage(key: string, value: boolean): void { if (typeof window === "undefined") return; try { localStorage.setItem(key, value ? "true" : "false"); } catch { // Ignore storage errors } } // Goroutine Health Section subcomponent interface GoroutineHealthProps { goroutineData: | { summary: { background: number; per_request: number; long_waiting: number; potentially_stuck: number; }; total_goroutines: number; } | undefined; goroutineHealth: "healthy" | "warning" | "critical"; goroutineTrend: { isGrowing: boolean; growthPercent: number; avg: number; } | null; problemGoroutines: GoroutineGroup[]; expandedGoroutines: Set; toggleGoroutineExpand: (id: string) => void; skippedGoroutines: Set; onSkipGoroutine: (topFunc: string) => void; onClearSkipped: () => void; } function GoroutineHealthSection({ goroutineData, goroutineHealth, goroutineTrend, problemGoroutines, expandedGoroutines, toggleGoroutineExpand, skippedGoroutines, onSkipGoroutine, onClearSkipped, }: GoroutineHealthProps): React.ReactNode { if (!goroutineData) return null; const { summary, total_goroutines } = goroutineData; return (
{/* Header with health status */}
Goroutine Health
{goroutineTrend?.isGrowing && ( +{goroutineTrend.growthPercent.toFixed(0)}% )} {goroutineHealth === "critical" && ( Stuck )} {goroutineHealth === "warning" && ( Long Wait )} {goroutineHealth === "healthy" && ( Healthy )}
{/* Summary stats */}
Total {total_goroutines}
Background {summary.background}
Per-Request {summary.per_request}
Stuck 0 ? "text-red-400" : "text-zinc-500"}`}> {summary.potentially_stuck}
{/* Problem goroutines list */} {(problemGoroutines.length > 0 || skippedGoroutines.size > 0) && (
Potential Leaks {skippedGoroutines.size > 0 && ( )}
{problemGoroutines.map((g) => { const gid = getGoroutineId(g); return (
toggleGoroutineExpand(gid)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleGoroutineExpand(gid); } }} className="flex w-full cursor-pointer flex-col gap-1 px-2 py-1.5 pr-8 text-left hover:bg-zinc-700/50" >
{expandedGoroutines.has(gid) ? ( ) : ( )} {truncateFunction(g.top_func)}
{g.category} {g.count}x {g.wait_minutes != null && {g.wait_minutes}m waiting}
{expandedGoroutines.has(gid) && (
State: {g.state} {g.wait_reason && ( Wait: {g.wait_reason} )}
{g.stack.slice(0, 10).map((line, j) => (
{line}
))} {g.stack.length > 10 &&
... {g.stack.length - 10} more frames
}
)}
); })} {problemGoroutines.length === 0 && skippedGoroutines.size > 0 && (
All potential leaks hidden
)} {problemGoroutines.length === 0 && skippedGoroutines.size === 0 && (summary.long_waiting > 0 || summary.potentially_stuck > 0) && (
{summary.long_waiting > 0 && summary.potentially_stuck > 0 ? `${summary.long_waiting} long-waiting and ${summary.potentially_stuck} stuck goroutines (background workers filtered)` : summary.long_waiting > 0 ? `${summary.long_waiting} long-waiting goroutines (background workers filtered)` : `${summary.potentially_stuck} stuck goroutines (background workers filtered)`}
)}
)} {/* No problems message */} {problemGoroutines.length === 0 && summary.long_waiting === 0 && summary.potentially_stuck === 0 && (
No goroutine leaks detected
)}
); } export function DevProfiler(): React.ReactNode { const [isVisible, setIsVisible] = useState(() => loadBooleanFromStorage(PROFILER_VISIBLE_KEY, true)); const [isExpanded, setIsExpanded] = useState(() => loadBooleanFromStorage(PROFILER_EXPANDED_KEY, true)); const [isDismissed, setIsDismissed] = useState(false); const [expandedGoroutines, setExpandedGoroutines] = useState>(new Set()); const [skippedGoroutines, setSkippedGoroutines] = useState>(() => loadSkippedGoroutineFiles()); // Sync skipped goroutines to localStorage useEffect(() => { saveSkippedGoroutineFiles(skippedGoroutines); }, [skippedGoroutines]); useEffect(() => { saveBooleanToStorage(PROFILER_VISIBLE_KEY, isVisible); }, [isVisible]); useEffect(() => { saveBooleanToStorage(PROFILER_EXPANDED_KEY, isExpanded); }, [isExpanded]); // Only fetch in development mode and when not dismissed const shouldFetch = isDevelopmentMode() && !isDismissed; const { data, isLoading, error } = useGetDevPprofQuery(undefined, { pollingInterval: shouldFetch ? 10000 : 0, // Poll every 10 seconds skip: !shouldFetch, }); const { data: goroutineData } = useGetDevGoroutinesQuery(undefined, { pollingInterval: shouldFetch ? 10000 : 0, // Poll every 10 seconds skip: !shouldFetch, }); // Memoize chart data transformation const memoryChartData = useMemo(() => { if (!data?.history) return []; return data.history.map((point) => ({ time: formatTime(point.timestamp), alloc: point.alloc / (1024 * 1024), // Convert to MB heapInuse: point.heap_inuse / (1024 * 1024), })); }, [data?.history]); const cpuChartData = useMemo(() => { if (!data?.history) return []; return data.history.map((point) => ({ time: formatTime(point.timestamp), cpuPercent: point.cpu_percent, goroutines: point.goroutines, })); }, [data?.history]); // Detect goroutine count trend (growing = potential leak) const goroutineTrend = useMemo(() => { if (!data?.history || data.history.length < 5 || !data?.runtime) return null; const recent = data.history.slice(-5); const avg = recent.reduce((sum, p) => sum + p.goroutines, 0) / recent.length; const current = data.runtime.num_goroutine; const isGrowing = current > avg * 1.1; // 10% above average const growthPercent = avg > 0 ? ((current - avg) / avg) * 100 : 0; return { isGrowing, growthPercent, avg }; }, [data?.history, data?.runtime?.num_goroutine]); // Filter problem goroutines (stuck or long-waiting, excluding expected background workers and skipped) const problemGoroutines = useMemo(() => { if (!goroutineData?.groups) return []; return goroutineData.groups .filter((g) => { if (!g.wait_minutes || g.wait_minutes < 1) return false; if (g.category === "background") return false; const filePath = getStackFilePath(g.stack); if (filePath && skippedGoroutines.has(filePath)) return false; return true; }) .slice(0, 5); }, [goroutineData?.groups, skippedGoroutines]); // Get goroutine health status const goroutineHealth = useMemo(() => { if (!goroutineData?.summary) return "healthy"; const { potentially_stuck, long_waiting } = goroutineData.summary; if (potentially_stuck > 0) return "critical"; if (long_waiting > 0) return "warning"; return "healthy"; }, [goroutineData?.summary]); const handleDismiss = useCallback(() => { setIsDismissed(true); saveBooleanToStorage(PROFILER_VISIBLE_KEY, false); }, []); const toggleGoroutineExpand = useCallback((id: string) => { setExpandedGoroutines((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, []); const handleSkipGoroutine = useCallback((filePath: string) => { setSkippedGoroutines((prev) => { const next = new Set(prev); next.add(filePath); return next; }); }, []); const handleClearSkipped = useCallback(() => { setSkippedGoroutines(new Set()); }, []); const handleToggleExpand = useCallback(() => { setIsExpanded((prev) => !prev); }, []); const handleToggleVisible = useCallback(() => { setIsVisible((prev) => !prev); }, []); // Don't render in production mode or if dismissed if (!isDevelopmentMode() || isDismissed) { return null; } // Minimized state - just show a small button if (!isVisible) { return ( ); } return (
{/* Header */}
Dev Profiler {isLoading && }
{Boolean(error) &&
Failed to load profiling data
} {isExpanded && data && (
{/* Current Stats */}
CPU Usage {data.cpu.usage_percent.toFixed(1)}%
Heap Alloc {formatBytes(data.memory.alloc)}
Heap In-Use {formatBytes(data.memory.heap_inuse)}
System {formatBytes(data.memory.sys)}
Goroutines {data.runtime.num_goroutine}
GC Pause {formatNs(data.runtime.gc_pause_ns)}
{/* CPU Chart */}
CPU Usage (last 5 min)
`${Number(v).toFixed(0)}%`} width={35} domain={[0, "auto"]} />
CPU % Goroutines
{/* Memory Chart */}
Memory (last 5 min)
`${Number(v).toFixed(0)}MB`} width={45} />
Alloc Heap In-Use
{/* Top Allocations */}
Top Allocations
{(data.top_allocations ?? []).map((alloc, i) => (
{truncateFunction(alloc.function)} {alloc.file}:{alloc.line}
{formatBytes(alloc.bytes)} {alloc.count.toLocaleString()} allocs
))}
{/* Goroutine Health */} {/* Footer with info */}
CPUs: {data.runtime.num_cpu} | GOMAXPROCS: {data.runtime.gomaxprocs} | GC: {data.runtime.num_gc} | Objects:{" "} {data.memory.heap_objects.toLocaleString()}
)} {/* Collapsed state */} {!isExpanded && data && (
CPU: {data.cpu.usage_percent.toFixed(1)}% Heap: {formatBytes(data.memory.heap_inuse)} Goroutines: {data.runtime.num_goroutine}
)}
); }