| 'use client' |
|
|
| 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' |
|
|
| |
| 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` |
| } |
|
|
| |
| 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', |
| }) |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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' |
| } |
| } |
|
|
| |
| function getStackFilePath (stack: string[]): string { |
| for (const line of stack) { |
| |
| const match = line.match(/^\s*([^\s]+\.go):\d+/) |
| if (match) { |
| return match[1] |
| } |
| } |
| return '' |
| } |
|
|
| |
| function getGoroutineId (g: GoroutineGroup): string { |
| return `${g.top_func}::${g.state}::${g.count}::${g.wait_minutes ?? 0}` |
| } |
|
|
| |
| const SKIPPED_GOROUTINE_FILES_KEY = 'devProfiler.skippedGoroutineFiles' |
| const PROFILER_VISIBLE_KEY = 'devProfiler.isVisible' |
| const PROFILER_EXPANDED_KEY = 'devProfiler.isExpanded' |
|
|
| |
| function loadSkippedGoroutineFiles (): Set<string> { |
| 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() |
| } |
| } |
|
|
| |
| function saveSkippedGoroutineFiles (skipped: Set<string>): void { |
| if (typeof window === 'undefined') return |
| try { |
| localStorage.setItem(SKIPPED_GOROUTINE_FILES_KEY, JSON.stringify([...skipped])) |
| } catch { |
| |
| } |
| } |
|
|
| 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 { |
| |
| } |
| } |
|
|
| |
| 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<string> |
| toggleGoroutineExpand: (id: string) => void |
| skippedGoroutines: Set<string> |
| 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 ( |
| <div className="p-3"> |
| {/* Header with health status */} |
| <div className="mb-2 flex items-center justify-between"> |
| <div className="flex items-center gap-2"> |
| <Activity className="h-3 w-3 text-emerald-400" /> |
| <span className="text-zinc-400">Goroutine Health</span> |
| </div> |
| <div className="flex items-center gap-2"> |
| {goroutineTrend?.isGrowing && ( |
| <span className="flex items-center gap-1 text-amber-400" title="Goroutine count growing"> |
| <TrendingUp className="h-3 w-3" /> |
| <span className="text-[10px]">+{goroutineTrend.growthPercent.toFixed(0)}%</span> |
| </span> |
| )} |
| {goroutineHealth === 'critical' && ( |
| <span className="flex items-center gap-1 rounded bg-red-500/20 px-1.5 py-0.5 text-[10px] text-red-400"> |
| <AlertTriangle className="h-3 w-3" /> |
| Stuck |
| </span> |
| )} |
| {goroutineHealth === 'warning' && ( |
| <span className="flex items-center gap-1 rounded bg-amber-500/20 px-1.5 py-0.5 text-[10px] text-amber-400"> |
| <AlertTriangle className="h-3 w-3" /> |
| Long Wait |
| </span> |
| )} |
| {goroutineHealth === 'healthy' && ( |
| <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] text-emerald-400"> |
| Healthy |
| </span> |
| )} |
| </div> |
| </div> |
| |
| {/* Summary stats */} |
| <div className="mb-2 grid grid-cols-4 gap-2 rounded bg-zinc-800/50 p-2"> |
| <div className="flex flex-col items-center"> |
| <span className="text-[10px] text-zinc-500">Total</span> |
| <span className="font-semibold text-emerald-400">{total_goroutines}</span> |
| </div> |
| <div className="flex flex-col items-center"> |
| <span className="text-[10px] text-zinc-500">Background</span> |
| <span className="font-semibold text-blue-400">{summary.background}</span> |
| </div> |
| <div className="flex flex-col items-center"> |
| <span className="text-[10px] text-zinc-500">Per-Request</span> |
| <span className="font-semibold text-amber-400">{summary.per_request}</span> |
| </div> |
| <div className="flex flex-col items-center"> |
| <span className="text-[10px] text-zinc-500">Stuck</span> |
| <span className={`font-semibold ${summary.potentially_stuck > 0 ? 'text-red-400' : 'text-zinc-500'}`}> |
| {summary.potentially_stuck} |
| </span> |
| </div> |
| </div> |
| |
| {/* Problem goroutines list */} |
| {(problemGoroutines.length > 0 || skippedGoroutines.size > 0) && ( |
| <div className="space-y-1"> |
| <div className="flex items-center justify-between"> |
| <span className="text-[10px] text-zinc-500">Potential Leaks</span> |
| {skippedGoroutines.size > 0 && ( |
| <button |
| onClick={onClearSkipped} |
| className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200" |
| title="Clear all hidden goroutines" |
| > |
| <RotateCcw className="h-2.5 w-2.5" /> |
| {skippedGoroutines.size} hidden |
| </button> |
| )} |
| </div> |
| {problemGoroutines.map((g) => { |
| const gid = getGoroutineId(g) |
| return ( |
| <div key={gid} className="group relative rounded bg-zinc-800"> |
| <div |
| role="button" |
| tabIndex={0} |
| onClick={() => 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" |
| > |
| <div className="flex w-full items-center gap-2"> |
| {expandedGoroutines.has(gid) ? ( |
| <ChevronDown className="h-3 w-3 shrink-0 text-zinc-500" /> |
| ) : ( |
| <ChevronRight className="h-3 w-3 shrink-0 text-zinc-500" /> |
| )} |
| <span className="min-w-0 flex-1 break-all text-zinc-300" title={g.top_func}> |
| {truncateFunction(g.top_func)} |
| </span> |
| </div> |
| <div className="flex items-center gap-2 pl-5 text-[10px]"> |
| <span className={`rounded px-1 py-0.5 ${getCategoryColor(g.category)}`}> |
| {g.category} |
| </span> |
| <span className="text-zinc-500">{g.count}x</span> |
| {g.wait_minutes != null && ( |
| <span className="text-amber-400">{g.wait_minutes}m waiting</span> |
| )} |
| </div> |
| </div> |
| <button |
| onClick={(e) => { |
| e.stopPropagation() |
| const filePath = getStackFilePath(g.stack) |
| if (filePath) onSkipGoroutine(filePath) |
| }} |
| className="absolute right-1 top-1.5 shrink-0 rounded p-1 text-zinc-500 opacity-0 transition-opacity hover:bg-zinc-600 hover:text-zinc-300 group-hover:opacity-100" |
| title="Hide goroutines from this file" |
| > |
| <EyeOff className="h-3 w-3" /> |
| </button> |
| {expandedGoroutines.has(gid) && ( |
| <div className="border-t border-zinc-700 bg-zinc-900/50 px-2 py-1.5"> |
| <div className="mb-1 text-[10px] text-zinc-500"> |
| State: <span className="text-zinc-400">{g.state}</span> |
| {g.wait_reason && ( |
| <span className="ml-2">Wait: <span className="text-amber-400">{g.wait_reason}</span></span> |
| )} |
| </div> |
| <div className="max-h-32 overflow-y-auto overflow-x-hidden"> |
| {g.stack.slice(0, 10).map((line, j) => ( |
| <div key={j} className="break-all text-[9px] text-zinc-500"> |
| {line} |
| </div> |
| ))} |
| {g.stack.length > 10 && ( |
| <div className="text-[9px] text-zinc-600"> |
| ... {g.stack.length - 10} more frames |
| </div> |
| )} |
| </div> |
| </div> |
| )} |
| </div> |
| )})} |
| |
| {problemGoroutines.length === 0 && skippedGoroutines.size > 0 && ( |
| <div className="rounded bg-zinc-800/30 py-2 text-center text-[10px] text-zinc-500"> |
| All potential leaks hidden |
| </div> |
| )} |
| {problemGoroutines.length === 0 && skippedGoroutines.size === 0 && (summary.long_waiting > 0 || summary.potentially_stuck > 0) && ( |
| <div className="rounded bg-zinc-800/30 px-2 py-2 text-center text-[10px] text-zinc-500"> |
| {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)`} |
| </div> |
| )} |
| </div> |
| )} |
| |
| {/* No problems message */} |
| {problemGoroutines.length === 0 && summary.long_waiting === 0 && summary.potentially_stuck === 0 && ( |
| <div className="rounded bg-zinc-800/30 py-2 text-center text-[10px] text-zinc-500"> |
| No goroutine leaks detected |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|
| export function DevProfiler (): React.ReactNode { |
| const [isVisible, setIsVisible] = useState<boolean>(() => loadBooleanFromStorage(PROFILER_VISIBLE_KEY, true)) |
| const [isExpanded, setIsExpanded] = useState<boolean>(() => loadBooleanFromStorage(PROFILER_EXPANDED_KEY, true)) |
| const [isDismissed, setIsDismissed] = useState(false) |
| const [expandedGoroutines, setExpandedGoroutines] = useState<Set<string>>(new Set()) |
| const [skippedGoroutines, setSkippedGoroutines] = useState<Set<string>>(() => loadSkippedGoroutineFiles()) |
|
|
| |
| useEffect(() => { |
| saveSkippedGoroutineFiles(skippedGoroutines) |
| }, [skippedGoroutines]) |
|
|
| useEffect(() => { |
| saveBooleanToStorage(PROFILER_VISIBLE_KEY, isVisible) |
| }, [isVisible]) |
|
|
| useEffect(() => { |
| saveBooleanToStorage(PROFILER_EXPANDED_KEY, isExpanded) |
| }, [isExpanded]) |
|
|
| |
| const shouldFetch = isDevelopmentMode() && !isDismissed |
|
|
| const { data, isLoading, error } = useGetDevPprofQuery(undefined, { |
| pollingInterval: shouldFetch ? 10000 : 0, |
| skip: !shouldFetch, |
| }) |
|
|
| const { data: goroutineData } = useGetDevGoroutinesQuery(undefined, { |
| pollingInterval: shouldFetch ? 10000 : 0, |
| skip: !shouldFetch, |
| }) |
|
|
| |
| const memoryChartData = useMemo(() => { |
| if (!data?.history) return [] |
| return data.history.map((point) => ({ |
| time: formatTime(point.timestamp), |
| alloc: point.alloc / (1024 * 1024), |
| 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]) |
|
|
| |
| 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 |
| const growthPercent = avg > 0 ? ((current - avg) / avg) * 100 : 0 |
| return { isGrowing, growthPercent, avg } |
| }, [data?.history, data?.runtime?.num_goroutine]) |
|
|
| |
| 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]) |
|
|
| |
| 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) |
| }, []) |
|
|
| |
| if (!isDevelopmentMode() || isDismissed) { |
| return null |
| } |
|
|
| |
| if (!isVisible) { |
| return ( |
| <button |
| onClick={handleToggleVisible} |
| className="fixed right-4 bottom-4 z-50 flex h-10 w-10 items-center justify-center rounded-full bg-zinc-900 text-white shadow-lg transition-all hover:bg-zinc-800" |
| title="Show Dev Profiler" |
| > |
| <Activity className="h-5 w-5" /> |
| </button> |
| ) |
| } |
|
|
| return ( |
| <div className="fixed right-4 bottom-4 z-50 w-[420px] overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 font-mono text-xs text-zinc-100 shadow-2xl"> |
| {/* Header */} |
| <div className="flex items-center justify-between border-b border-zinc-700 bg-zinc-800 px-3 py-2"> |
| <div className="flex items-center gap-2"> |
| <span className="font-semibold text-emerald-400">Dev Profiler</span> |
| {isLoading && ( |
| <span className="ml-2 h-2 w-2 animate-pulse rounded-full bg-amber-400" /> |
| )} |
| </div> |
| <div className="flex items-center gap-1"> |
| <button |
| onClick={handleToggleExpand} |
| className="rounded p-1 transition-colors hover:bg-zinc-700" |
| title={isExpanded ? 'Collapse' : 'Expand'} |
| > |
| {isExpanded ? ( |
| <ChevronDown className="h-4 w-4" /> |
| ) : ( |
| <ChevronUp className="h-4 w-4" /> |
| )} |
| </button> |
| <button |
| onClick={handleToggleVisible} |
| className="rounded p-1 transition-colors hover:bg-zinc-700" |
| title="Minimize" |
| > |
| <ChevronDown className="h-4 w-4" /> |
| </button> |
| <button |
| onClick={handleDismiss} |
| className="rounded p-1 transition-colors hover:bg-zinc-700" |
| title="Dismiss" |
| > |
| <X className="h-4 w-4" /> |
| </button> |
| </div> |
| </div> |
| |
| {Boolean(error) && ( |
| <div className="border-b border-zinc-700 bg-red-900/30 px-3 py-2 text-red-300"> |
| Failed to load profiling data |
| </div> |
| )} |
| |
| {isExpanded && data && ( |
| <div className="max-h-[70vh] overflow-y-auto overflow-x-hidden custom-scrollbar"> |
| {/* Current Stats */} |
| <div className="grid grid-cols-3 gap-2 border-b border-zinc-700 p-3"> |
| <div className="flex flex-col"> |
| <span className="text-zinc-500">CPU Usage</span> |
| <span className="font-semibold text-orange-400"> |
| {data.cpu.usage_percent.toFixed(1)}% |
| </span> |
| </div> |
| <div className="flex flex-col"> |
| <span className="text-zinc-500">Heap Alloc</span> |
| <span className="font-semibold text-cyan-400"> |
| {formatBytes(data.memory.alloc)} |
| </span> |
| </div> |
| <div className="flex flex-col"> |
| <span className="text-zinc-500">Heap In-Use</span> |
| <span className="font-semibold text-blue-400"> |
| {formatBytes(data.memory.heap_inuse)} |
| </span> |
| </div> |
| <div className="flex flex-col"> |
| <span className="text-zinc-500">System</span> |
| <span className="font-semibold text-purple-400"> |
| {formatBytes(data.memory.sys)} |
| </span> |
| </div> |
| <div className="flex flex-col"> |
| <span className="text-zinc-500">Goroutines</span> |
| <span className="font-semibold text-emerald-400"> |
| {data.runtime.num_goroutine} |
| </span> |
| </div> |
| <div className="flex flex-col"> |
| <span className="text-zinc-500">GC Pause</span> |
| <span className="font-semibold text-amber-400"> |
| {formatNs(data.runtime.gc_pause_ns)} |
| </span> |
| </div> |
| </div> |
| |
| {/* CPU Chart */} |
| <div className="border-b border-zinc-700 p-3"> |
| <div className="mb-2 flex items-center gap-2"> |
| <Cpu className="h-3 w-3 text-orange-400" /> |
| <span className="text-zinc-400">CPU Usage (last 5 min)</span> |
| </div> |
| <div className="h-24"> |
| <ResponsiveContainer width="100%" height="100%"> |
| <AreaChart data={cpuChartData}> |
| <defs> |
| <linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="5%" stopColor="#f97316" stopOpacity={0.3} /> |
| <stop offset="95%" stopColor="#f97316" stopOpacity={0} /> |
| </linearGradient> |
| <linearGradient id="goroutineGradient" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="5%" stopColor="#34d399" stopOpacity={0.3} /> |
| <stop offset="95%" stopColor="#34d399" stopOpacity={0} /> |
| </linearGradient> |
| </defs> |
| <CartesianGrid strokeDasharray="3 3" stroke="#3f3f46" /> |
| <XAxis |
| dataKey="time" |
| tick={{ fill: '#71717a', fontSize: 9 }} |
| tickLine={false} |
| axisLine={false} |
| /> |
| <YAxis |
| yAxisId="left" |
| tick={{ fill: '#71717a', fontSize: 9 }} |
| tickLine={false} |
| axisLine={false} |
| tickFormatter={(v) => `${Number(v).toFixed(0)}%`} |
| width={35} |
| domain={[0, 'auto']} |
| /> |
| <YAxis |
| yAxisId="right" |
| orientation="right" |
| tick={{ fill: '#71717a', fontSize: 9 }} |
| tickLine={false} |
| axisLine={false} |
| width={30} |
| /> |
| <Tooltip |
| contentStyle={{ |
| backgroundColor: '#18181b', |
| border: '1px solid #3f3f46', |
| borderRadius: '6px', |
| fontSize: '10px', |
| }} |
| labelStyle={{ color: '#a1a1aa' }} |
| /> |
| <Area |
| type="monotone" |
| dataKey="cpuPercent" |
| stroke="#f97316" |
| strokeWidth={1.5} |
| fill="url(#cpuGradient)" |
| yAxisId="left" |
| name="CPU %" |
| /> |
| <Area |
| type="monotone" |
| dataKey="goroutines" |
| stroke="#34d399" |
| strokeWidth={1.5} |
| fill="url(#goroutineGradient)" |
| yAxisId="right" |
| name="Goroutines" |
| /> |
| </AreaChart> |
| </ResponsiveContainer> |
| </div> |
| <div className="mt-1 flex gap-4 text-[10px]"> |
| <span className="flex items-center gap-1"> |
| <span className="h-2 w-2 rounded-full bg-orange-500" /> |
| CPU % |
| </span> |
| <span className="flex items-center gap-1"> |
| <span className="h-2 w-2 rounded-full bg-emerald-400" /> |
| Goroutines |
| </span> |
| </div> |
| </div> |
| |
| {/* Memory Chart */} |
| <div className="border-b border-zinc-700 p-3"> |
| <div className="mb-2 flex items-center gap-2"> |
| <HardDrive className="h-3 w-3 text-cyan-400" /> |
| <span className="text-zinc-400">Memory (last 5 min)</span> |
| </div> |
| <div className="h-24"> |
| <ResponsiveContainer width="100%" height="100%"> |
| <AreaChart data={memoryChartData}> |
| <defs> |
| <linearGradient id="allocGradient" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="5%" stopColor="#22d3ee" stopOpacity={0.3} /> |
| <stop offset="95%" stopColor="#22d3ee" stopOpacity={0} /> |
| </linearGradient> |
| <linearGradient id="heapGradient" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} /> |
| <stop offset="95%" stopColor="#3b82f6" stopOpacity={0} /> |
| </linearGradient> |
| </defs> |
| <CartesianGrid strokeDasharray="3 3" stroke="#3f3f46" /> |
| <XAxis |
| dataKey="time" |
| tick={{ fill: '#71717a', fontSize: 9 }} |
| tickLine={false} |
| axisLine={false} |
| /> |
| <YAxis |
| tick={{ fill: '#71717a', fontSize: 9 }} |
| tickLine={false} |
| axisLine={false} |
| tickFormatter={(v) => `${Number(v).toFixed(0)}MB`} |
| width={45} |
| /> |
| <Tooltip |
| contentStyle={{ |
| backgroundColor: '#18181b', |
| border: '1px solid #3f3f46', |
| borderRadius: '6px', |
| fontSize: '10px', |
| }} |
| labelStyle={{ color: '#a1a1aa' }} |
| /> |
| <Area |
| type="monotone" |
| dataKey="alloc" |
| stroke="#22d3ee" |
| strokeWidth={1.5} |
| fill="url(#allocGradient)" |
| name="Alloc" |
| /> |
| <Area |
| type="monotone" |
| dataKey="heapInuse" |
| stroke="#3b82f6" |
| strokeWidth={1.5} |
| fill="url(#heapGradient)" |
| name="Heap In-Use" |
| /> |
| </AreaChart> |
| </ResponsiveContainer> |
| </div> |
| <div className="mt-1 flex gap-4 text-[10px]"> |
| <span className="flex items-center gap-1"> |
| <span className="h-2 w-2 rounded-full bg-cyan-400" /> |
| Alloc |
| </span> |
| <span className="flex items-center gap-1"> |
| <span className="h-2 w-2 rounded-full bg-blue-500" /> |
| Heap In-Use |
| </span> |
| </div> |
| </div> |
| |
| {/* Top Allocations */} |
| <div className="border-b border-zinc-700 p-3"> |
| <div className="mb-2 flex items-center gap-2"> |
| <HardDrive className="h-3 w-3 text-rose-400" /> |
| <span className="text-zinc-400">Top Allocations</span> |
| </div> |
| <div className="space-y-1"> |
| {(data.top_allocations ?? []).map((alloc, i) => ( |
| <div |
| key={i} |
| className="flex items-center justify-between rounded bg-zinc-800 px-2 py-1" |
| > |
| <div className="flex flex-col overflow-hidden"> |
| <span |
| className="truncate text-zinc-300" |
| title={alloc.function} |
| > |
| {truncateFunction(alloc.function)} |
| </span> |
| <span className="text-[10px] text-zinc-500"> |
| {alloc.file}:{alloc.line} |
| </span> |
| </div> |
| <div className="flex flex-col items-end"> |
| <span className="text-rose-400"> |
| {formatBytes(alloc.bytes)} |
| </span> |
| <span className="text-[10px] text-zinc-500"> |
| {alloc.count.toLocaleString()} allocs |
| </span> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| {/* Goroutine Health */} |
| <GoroutineHealthSection |
| goroutineData={goroutineData} |
| goroutineHealth={goroutineHealth} |
| goroutineTrend={goroutineTrend} |
| problemGoroutines={problemGoroutines} |
| expandedGoroutines={expandedGoroutines} |
| toggleGoroutineExpand={toggleGoroutineExpand} |
| skippedGoroutines={skippedGoroutines} |
| onSkipGoroutine={handleSkipGoroutine} |
| onClearSkipped={handleClearSkipped} |
| /> |
| |
| {/* Footer with info */} |
| <div className="border-t border-zinc-700 bg-zinc-800 px-3 py-2 text-[10px] text-zinc-500"> |
| CPUs: {data.runtime.num_cpu} | GOMAXPROCS: {data.runtime.gomaxprocs} | |
| GC: {data.runtime.num_gc} | Objects: {data.memory.heap_objects.toLocaleString()} |
| </div> |
| </div> |
| )} |
| |
| {/* Collapsed state */} |
| {!isExpanded && data && ( |
| <div className="flex items-center justify-between bg-zinc-800/50 px-3 py-2"> |
| <span className="text-orange-400"> |
| CPU: {data.cpu.usage_percent.toFixed(1)}% |
| </span> |
| <span className="text-zinc-400"> |
| Heap: {formatBytes(data.memory.heap_inuse)} |
| </span> |
| <span className="text-zinc-400"> |
| Goroutines: {data.runtime.num_goroutine} |
| </span> |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|