774 lines
38 KiB
TypeScript
774 lines
38 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import {
|
||
Activity,
|
||
Cpu,
|
||
MemoryStick,
|
||
GitBranch,
|
||
Database,
|
||
RefreshCw,
|
||
Download,
|
||
AlertTriangle,
|
||
CheckCircle,
|
||
XCircle,
|
||
HelpCircle,
|
||
} from 'lucide-react';
|
||
import { Card, CardHeader, CardContent, CardTitle } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Progress } from '@/components/ui/progress';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { useToast } from '@/components/ui/use-toast';
|
||
import { Skeleton } from '@/components/ui/skeleton';
|
||
import {
|
||
LineChart,
|
||
Line,
|
||
PieChart,
|
||
Pie,
|
||
Cell,
|
||
XAxis,
|
||
YAxis,
|
||
CartesianGrid,
|
||
Tooltip,
|
||
Legend,
|
||
ResponsiveContainer,
|
||
} from 'recharts';
|
||
import type { SystemMetrics, MetricHistoryPoint, ThresholdConfig } from './types';
|
||
import { getAllMetrics } from './service';
|
||
import { ThreadDetailDialog } from './components/ThreadDetailDialog';
|
||
|
||
// 告警阈值配置
|
||
const THRESHOLDS: ThresholdConfig = {
|
||
cpu: {
|
||
warning: 70,
|
||
danger: 90,
|
||
},
|
||
memory: {
|
||
warning: 75,
|
||
danger: 90,
|
||
},
|
||
threadBlocked: {
|
||
warning: 5,
|
||
danger: 10,
|
||
},
|
||
dbConnections: {
|
||
warning: 80,
|
||
danger: 95,
|
||
},
|
||
};
|
||
|
||
// 线程状态颜色
|
||
const THREAD_STATE_COLORS: Record<string, string> = {
|
||
RUNNABLE: '#10b981', // 绿色
|
||
WAITING: '#3b82f6', // 蓝色
|
||
TIMED_WAITING: '#8b5cf6', // 紫色
|
||
BLOCKED: '#ef4444', // 红色
|
||
NEW: '#6b7280', // 灰色
|
||
TERMINATED: '#9ca3af', // 浅灰色
|
||
};
|
||
|
||
const MetricsDashboard: React.FC = () => {
|
||
const { toast } = useToast();
|
||
const [metrics, setMetrics] = useState<SystemMetrics | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [history, setHistory] = useState<MetricHistoryPoint[]>([]);
|
||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||
const [threadDialogOpen, setThreadDialogOpen] = useState(false);
|
||
|
||
// 加载指标数据
|
||
const loadMetrics = useCallback(async (silent: boolean = false) => {
|
||
if (!silent) {
|
||
setLoading(true);
|
||
}
|
||
try {
|
||
const data = await getAllMetrics();
|
||
setMetrics(data);
|
||
|
||
// 添加到历史记录(保留最近20个数据点)
|
||
setHistory(prev => {
|
||
const newPoint = {
|
||
timestamp: data.timestamp,
|
||
heapUsed: data.memory.heapUsed / 1024 / 1024 / 1024, // 转换为 GB
|
||
heapMax: data.memory.heapMax / 1024 / 1024 / 1024,
|
||
cpuUsage: data.cpu.systemCpu * 100,
|
||
};
|
||
|
||
// 检查是否有旧数据(MB单位),如果有则清空重新收集
|
||
if (prev.length > 0 && prev[0].heapUsed > 100) {
|
||
// 旧数据是 MB 单位(通常 > 100),清空
|
||
return [newPoint];
|
||
}
|
||
|
||
const newHistory = [...prev, newPoint];
|
||
return newHistory.slice(-20); // 只保留最近20个点
|
||
});
|
||
|
||
// 检查是否有数据异常(部分接口失败)
|
||
if (!silent) {
|
||
const hasIssues =
|
||
data.health.status === 'UNKNOWN' ||
|
||
data.memory.heapMax === 0 ||
|
||
data.cpu.systemCpu === 0;
|
||
|
||
if (hasIssues) {
|
||
toast({
|
||
title: '部分指标获取失败',
|
||
description: '某些监控指标无法获取,显示的可能是默认值',
|
||
variant: 'default',
|
||
});
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('加载指标失败:', error);
|
||
toast({
|
||
variant: 'destructive',
|
||
title: '加载失败',
|
||
description: error.response?.data?.message || '无法获取系统指标数据,请检查后端服务和 actuator 配置',
|
||
});
|
||
} finally {
|
||
if (!silent) {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
}, [toast]);
|
||
|
||
// 初始加载
|
||
useEffect(() => {
|
||
loadMetrics();
|
||
}, [loadMetrics]);
|
||
|
||
// 自动刷新(30秒)
|
||
useEffect(() => {
|
||
if (!autoRefresh) return;
|
||
|
||
const interval = setInterval(() => {
|
||
loadMetrics(true); // 静默刷新
|
||
}, 30000); // 30秒
|
||
|
||
return () => clearInterval(interval);
|
||
}, [autoRefresh, loadMetrics]);
|
||
|
||
// 格式化字节为 GB(统一单位)
|
||
// 处理 JVM 返回 -1 表示"无限制"的情况
|
||
const formatBytes = (bytes: number, showUnlimited: boolean = false): string => {
|
||
if (bytes < 0) {
|
||
return showUnlimited ? '无限制' : '-';
|
||
}
|
||
const gb = bytes / 1024 / 1024 / 1024;
|
||
return `${gb.toFixed(2)} GB`;
|
||
};
|
||
|
||
// 计算内存使用百分比(处理 max 为 -1 或 0 的情况)
|
||
const calcMemoryPercent = (used: number, max: number, committed?: number): number => {
|
||
if (max > 0) {
|
||
return (used / max) * 100;
|
||
}
|
||
// 如果 max 无效,使用 committed 作为参考
|
||
if (committed && committed > 0) {
|
||
return (used / committed) * 100;
|
||
}
|
||
return 0;
|
||
};
|
||
|
||
// 格式化百分比
|
||
const formatPercent = (value: number): string => {
|
||
return value.toFixed(2);
|
||
};
|
||
|
||
// 获取状态颜色
|
||
const getStatusColor = (value: number, threshold: { warning: number; danger: number }) => {
|
||
if (value >= threshold.danger) return 'text-red-600 dark:text-red-400';
|
||
if (value >= threshold.warning) return 'text-yellow-600 dark:text-yellow-400';
|
||
return 'text-green-600 dark:text-green-400';
|
||
};
|
||
|
||
// 获取健康状态图标
|
||
const getHealthIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'UP':
|
||
return <CheckCircle className="h-5 w-5 text-green-600" />;
|
||
case 'DOWN':
|
||
return <XCircle className="h-5 w-5 text-red-600" />;
|
||
default:
|
||
return <HelpCircle className="h-5 w-5 text-gray-600" />;
|
||
}
|
||
};
|
||
|
||
// 导出数据
|
||
const handleExport = () => {
|
||
if (!metrics) return;
|
||
|
||
const data = {
|
||
timestamp: new Date(metrics.timestamp).toLocaleString('zh-CN'),
|
||
health: metrics.health.status,
|
||
cpu: {
|
||
system: formatPercent(metrics.cpu.systemCpu * 100) + '%',
|
||
process: formatPercent(metrics.cpu.processCpu * 100) + '%',
|
||
},
|
||
memory: {
|
||
heap: {
|
||
used: formatBytes(metrics.memory.heapUsed),
|
||
max: formatBytes(metrics.memory.heapMax),
|
||
committed: formatBytes(metrics.memory.heapCommitted),
|
||
percent: formatPercent(metrics.memory.heapPercent) + '%',
|
||
},
|
||
nonHeap: {
|
||
used: formatBytes(metrics.memory.nonHeapUsed),
|
||
max: formatBytes(metrics.memory.nonHeapMax),
|
||
committed: formatBytes(metrics.memory.nonHeapCommitted),
|
||
percent: formatPercent(metrics.memory.nonHeapPercent) + '%',
|
||
},
|
||
regions: metrics.memory.regions ? {
|
||
eden: metrics.memory.regions.eden ? {
|
||
used: formatBytes(metrics.memory.regions.eden.used),
|
||
max: formatBytes(metrics.memory.regions.eden.max, true),
|
||
committed: formatBytes(metrics.memory.regions.eden.committed),
|
||
} : null,
|
||
oldGen: metrics.memory.regions.oldGen ? {
|
||
used: formatBytes(metrics.memory.regions.oldGen.used),
|
||
max: formatBytes(metrics.memory.regions.oldGen.max, true),
|
||
committed: formatBytes(metrics.memory.regions.oldGen.committed),
|
||
} : null,
|
||
survivor: metrics.memory.regions.survivor ? {
|
||
used: formatBytes(metrics.memory.regions.survivor.used),
|
||
max: formatBytes(metrics.memory.regions.survivor.max, true),
|
||
committed: formatBytes(metrics.memory.regions.survivor.committed),
|
||
} : null,
|
||
metaspace: metrics.memory.regions.metaspace ? {
|
||
used: formatBytes(metrics.memory.regions.metaspace.used),
|
||
max: formatBytes(metrics.memory.regions.metaspace.max, true),
|
||
committed: formatBytes(metrics.memory.regions.metaspace.committed),
|
||
} : null,
|
||
} : null,
|
||
},
|
||
threads: metrics.threads,
|
||
hikari: metrics.hikari,
|
||
gc: metrics.gc,
|
||
threadStates: metrics.threadStates,
|
||
};
|
||
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `system-metrics-${Date.now()}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
|
||
toast({
|
||
title: '导出成功',
|
||
description: '系统指标数据已导出',
|
||
});
|
||
};
|
||
|
||
// 准备线程状态饼图数据
|
||
const threadStateData = metrics
|
||
? Object.entries(metrics.threadStates)
|
||
.filter(([_, value]) => value > 0)
|
||
.map(([name, value]) => ({
|
||
name,
|
||
value,
|
||
}))
|
||
: [];
|
||
|
||
if (loading && !metrics) {
|
||
return (
|
||
<div className="p-6 space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold">系统指标监控</h1>
|
||
<p className="text-muted-foreground mt-1">实时监控系统运行状态和性能指标</p>
|
||
</div>
|
||
</div>
|
||
<div className="grid gap-4 md:grid-cols-4">
|
||
{[1, 2, 3, 4].map(i => (
|
||
<Card key={i}>
|
||
<CardContent className="p-6">
|
||
<Skeleton className="h-20" />
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!metrics) {
|
||
return null;
|
||
}
|
||
|
||
const cpuPercent = metrics.cpu.systemCpu * 100;
|
||
const memoryPercent = metrics.memory.heapPercent;
|
||
const blockedThreads = metrics.threadStates.BLOCKED || 0;
|
||
const dbUsagePercent = metrics.hikari.usagePercent;
|
||
|
||
return (
|
||
<div className="p-6 space-y-6">
|
||
{/* 标题栏 */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold">系统指标监控</h1>
|
||
<p className="text-muted-foreground mt-1">
|
||
实时监控系统运行状态和性能指标 ·
|
||
{autoRefresh && <span className="text-green-600 ml-2">自动刷新 (30秒)</span>}
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setAutoRefresh(!autoRefresh)}
|
||
>
|
||
{autoRefresh ? '停止刷新' : '开启刷新'}
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => loadMetrics()}
|
||
disabled={loading}
|
||
>
|
||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||
刷新
|
||
</Button>
|
||
<Button variant="outline" size="sm" onClick={handleExport}>
|
||
<Download className="h-4 w-4 mr-2" />
|
||
导出
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<div className="grid gap-4 md:grid-cols-4">
|
||
{/* 健康状态 */}
|
||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 border-blue-200 dark:border-blue-900">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
{getHealthIcon(metrics.health.status)}
|
||
<span className="text-xs font-medium text-muted-foreground">健康状态</span>
|
||
</div>
|
||
<div className="text-2xl font-bold">
|
||
{metrics.health.status === 'UP' ? '正常' :
|
||
metrics.health.status === 'DOWN' ? '异常' : '未知'}
|
||
</div>
|
||
<Badge
|
||
variant={metrics.health.status === 'UP' ? 'default' : 'destructive'}
|
||
>
|
||
{metrics.health.status}
|
||
</Badge>
|
||
</div>
|
||
<Activity className="h-10 w-10 text-blue-600 dark:text-blue-400 opacity-50" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* CPU 使用率 */}
|
||
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200 dark:border-green-900">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-2 flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<Cpu className="h-4 w-4" />
|
||
<span className="text-xs font-medium text-muted-foreground">CPU 使用率</span>
|
||
</div>
|
||
<div className={`text-2xl font-bold ${getStatusColor(cpuPercent, THRESHOLDS.cpu)}`}>
|
||
{formatPercent(cpuPercent)}%
|
||
</div>
|
||
<Progress value={cpuPercent} className="h-2" />
|
||
{cpuPercent >= THRESHOLDS.cpu.warning && (
|
||
<div className="flex items-center gap-1 text-xs text-yellow-600">
|
||
<AlertTriangle className="h-3 w-3" />
|
||
CPU 使用率偏高
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 内存使用 */}
|
||
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-purple-200 dark:border-purple-900">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-2 flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<MemoryStick className="h-4 w-4" />
|
||
<span className="text-xs font-medium text-muted-foreground">JVM 堆内存</span>
|
||
</div>
|
||
<div className={`text-2xl font-bold ${getStatusColor(memoryPercent, THRESHOLDS.memory)}`}>
|
||
{formatBytes(metrics.memory.heapUsed)}
|
||
</div>
|
||
<div className="text-xs text-muted-foreground space-y-0.5">
|
||
<div>最大: {formatBytes(metrics.memory.heapMax)}</div>
|
||
<div>已分配: {formatBytes(metrics.memory.heapCommitted)}</div>
|
||
</div>
|
||
<Progress value={memoryPercent} className="h-2" />
|
||
{memoryPercent >= THRESHOLDS.memory.warning && (
|
||
<div className="flex items-center gap-1 text-xs text-yellow-600">
|
||
<AlertTriangle className="h-3 w-3" />
|
||
内存使用率偏高
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 线程数 */}
|
||
<Card
|
||
className="bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-950/20 dark:to-amber-950/20 border-orange-200 dark:border-orange-900 cursor-pointer hover:shadow-lg transition-shadow"
|
||
onClick={() => setThreadDialogOpen(true)}
|
||
>
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<GitBranch className="h-4 w-4" />
|
||
<span className="text-xs font-medium text-muted-foreground">活跃线程</span>
|
||
<span className="text-xs text-blue-600 dark:text-blue-400">点击查看详情</span>
|
||
</div>
|
||
<div className="text-2xl font-bold text-orange-600 dark:text-orange-400">
|
||
{metrics.threads.live}
|
||
</div>
|
||
<div className="text-xs text-muted-foreground space-y-0.5">
|
||
<div>守护线程: {metrics.threads.daemon}</div>
|
||
<div>峰值: {metrics.threads.peak}</div>
|
||
</div>
|
||
{blockedThreads >= THRESHOLDS.threadBlocked.warning && (
|
||
<div className="flex items-center gap-1 text-xs text-red-600">
|
||
<AlertTriangle className="h-3 w-3" />
|
||
{blockedThreads} 个线程阻塞
|
||
</div>
|
||
)}
|
||
</div>
|
||
<GitBranch className="h-10 w-10 text-orange-600 dark:text-orange-400 opacity-50" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* JVM 内存趋势图 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>JVM 内存趋势</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="h-64">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<LineChart data={history}>
|
||
<CartesianGrid strokeDasharray="3 3" />
|
||
<XAxis
|
||
dataKey="timestamp"
|
||
tickFormatter={(value) =>
|
||
new Date(value).toLocaleTimeString('zh-CN', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})
|
||
}
|
||
/>
|
||
<YAxis
|
||
tickFormatter={(value: number) => `${value.toFixed(1)}G`}
|
||
/>
|
||
<Tooltip
|
||
labelFormatter={(value) =>
|
||
new Date(value).toLocaleTimeString('zh-CN')
|
||
}
|
||
formatter={(value: number) => `${value.toFixed(2)} GB`}
|
||
/>
|
||
<Legend />
|
||
<Line
|
||
type="monotone"
|
||
dataKey="heapUsed"
|
||
stroke="#8b5cf6"
|
||
strokeWidth={2}
|
||
name="堆内存使用"
|
||
dot={false}
|
||
/>
|
||
<Line
|
||
type="monotone"
|
||
dataKey="heapMax"
|
||
stroke="#94a3b8"
|
||
strokeWidth={1}
|
||
strokeDasharray="5 5"
|
||
name="堆内存最大值"
|
||
dot={false}
|
||
/>
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 数据库连接池 & 线程状态 */}
|
||
<div className="grid gap-4 md:grid-cols-2">
|
||
{/* 数据库连接池 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Database className="h-5 w-5" />
|
||
数据库连接池
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<div className="text-sm text-muted-foreground">活跃连接</div>
|
||
<div className="text-2xl font-bold text-green-600">
|
||
{metrics.hikari.active}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-sm text-muted-foreground">空闲连接</div>
|
||
<div className="text-2xl font-bold text-blue-600">
|
||
{metrics.hikari.idle}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-sm text-muted-foreground">等待连接</div>
|
||
<div className="text-2xl font-bold text-orange-600">
|
||
{metrics.hikari.pending}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-sm text-muted-foreground">最大连接</div>
|
||
<div className="text-2xl font-bold text-gray-600">
|
||
{metrics.hikari.max}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm font-medium">连接池使用率</span>
|
||
<span className={`text-sm font-bold ${getStatusColor(dbUsagePercent, THRESHOLDS.dbConnections)}`}>
|
||
{formatPercent(dbUsagePercent)}%
|
||
</span>
|
||
</div>
|
||
<Progress value={dbUsagePercent} className="h-3" />
|
||
<div className="text-xs text-muted-foreground mt-2">
|
||
总计: {metrics.hikari.total} / {metrics.hikari.max}
|
||
</div>
|
||
{dbUsagePercent >= THRESHOLDS.dbConnections.warning && (
|
||
<div className="flex items-center gap-1 text-xs text-yellow-600 mt-2">
|
||
<AlertTriangle className="h-3 w-3" />
|
||
连接池使用率偏高,建议增加连接数
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 线程状态分布 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>线程状态分布</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="h-64">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<PieChart>
|
||
<Pie
|
||
data={threadStateData}
|
||
cx="50%"
|
||
cy="50%"
|
||
labelLine={false}
|
||
label={({ name, value }) => `${name}: ${value}`}
|
||
outerRadius={80}
|
||
fill="#8884d8"
|
||
dataKey="value"
|
||
>
|
||
{threadStateData.map((entry, index) => (
|
||
<Cell
|
||
key={`cell-${index}`}
|
||
fill={THREAD_STATE_COLORS[entry.name] || '#6b7280'}
|
||
/>
|
||
))}
|
||
</Pie>
|
||
<Tooltip />
|
||
<Legend />
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
{blockedThreads >= THRESHOLDS.threadBlocked.danger && (
|
||
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 dark:bg-red-950/20 p-3 rounded-lg mt-4">
|
||
<AlertTriangle className="h-4 w-4" />
|
||
<span>检测到 {blockedThreads} 个线程处于阻塞状态,可能存在死锁或性能问题</span>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 内存区域详情(如果可用) */}
|
||
{metrics.memory.regions && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>JVM 内存区域详情</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
{/* Eden 区 */}
|
||
{metrics.memory.regions.eden && (
|
||
<div className="p-4 bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 rounded-lg border border-green-200 dark:border-green-900">
|
||
<div className="text-sm font-medium text-muted-foreground mb-2">Eden 区(年轻代)</div>
|
||
<div className="space-y-1 text-xs">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">已使用:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.eden.used)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">已分配:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.eden.committed)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">最大:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.eden.max, true)}</span>
|
||
</div>
|
||
<Progress
|
||
value={calcMemoryPercent(
|
||
metrics.memory.regions.eden.used,
|
||
metrics.memory.regions.eden.max,
|
||
metrics.memory.regions.eden.committed
|
||
)}
|
||
className="h-1 mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Old Gen */}
|
||
{metrics.memory.regions.oldGen && (
|
||
<div className="p-4 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 rounded-lg border border-blue-200 dark:border-blue-900">
|
||
<div className="text-sm font-medium text-muted-foreground mb-2">Old Gen(老年代)</div>
|
||
<div className="space-y-1 text-xs">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">已使用:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.oldGen.used)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">已分配:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.oldGen.committed)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">最大:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.oldGen.max, true)}</span>
|
||
</div>
|
||
<Progress
|
||
value={calcMemoryPercent(
|
||
metrics.memory.regions.oldGen.used,
|
||
metrics.memory.regions.oldGen.max,
|
||
metrics.memory.regions.oldGen.committed
|
||
)}
|
||
className="h-1 mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Survivor 区 */}
|
||
{metrics.memory.regions.survivor && (
|
||
<div className="p-4 bg-gradient-to-br from-yellow-50 to-orange-50 dark:from-yellow-950/20 dark:to-orange-950/20 rounded-lg border border-yellow-200 dark:border-yellow-900">
|
||
<div className="text-sm font-medium text-muted-foreground mb-2">Survivor 区</div>
|
||
<div className="space-y-1 text-xs">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">已使用:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.survivor.used)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">已分配:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.survivor.committed)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">最大:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.survivor.max, true)}</span>
|
||
</div>
|
||
<Progress
|
||
value={calcMemoryPercent(
|
||
metrics.memory.regions.survivor.used,
|
||
metrics.memory.regions.survivor.max,
|
||
metrics.memory.regions.survivor.committed
|
||
)}
|
||
className="h-1 mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Metaspace */}
|
||
{metrics.memory.regions.metaspace && (
|
||
<div className="p-4 bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 rounded-lg border border-purple-200 dark:border-purple-900">
|
||
<div className="text-sm font-medium text-muted-foreground mb-2">Metaspace(元空间)</div>
|
||
<div className="space-y-1 text-xs">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">已使用:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.metaspace.used)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">已分配:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.metaspace.committed)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">最大:</span>
|
||
<span className="font-medium">{formatBytes(metrics.memory.regions.metaspace.max, true)}</span>
|
||
</div>
|
||
<Progress
|
||
value={calcMemoryPercent(
|
||
metrics.memory.regions.metaspace.used,
|
||
metrics.memory.regions.metaspace.max,
|
||
metrics.memory.regions.metaspace.committed
|
||
)}
|
||
className="h-1 mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* GC 统计 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>垃圾回收 (GC) 统计</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<div className="text-center p-4 bg-muted/30 rounded-lg">
|
||
<div className="text-sm text-muted-foreground mb-1">GC 次数</div>
|
||
<div className="text-2xl font-bold">{metrics.gc.count}</div>
|
||
<div className="text-xs text-muted-foreground">次</div>
|
||
</div>
|
||
<div className="text-center p-4 bg-muted/30 rounded-lg">
|
||
<div className="text-sm text-muted-foreground mb-1">总耗时</div>
|
||
<div className="text-2xl font-bold">{metrics.gc.totalTime.toFixed(3)}</div>
|
||
<div className="text-xs text-muted-foreground">秒</div>
|
||
</div>
|
||
<div className="text-center p-4 bg-muted/30 rounded-lg">
|
||
<div className="text-sm text-muted-foreground mb-1">平均耗时</div>
|
||
<div className="text-2xl font-bold">{metrics.gc.avgTime.toFixed(2)}</div>
|
||
<div className="text-xs text-muted-foreground">毫秒</div>
|
||
</div>
|
||
<div className="text-center p-4 bg-muted/30 rounded-lg">
|
||
<div className="text-sm text-muted-foreground mb-1">最长耗时</div>
|
||
<div className="text-2xl font-bold">{(metrics.gc.maxTime * 1000).toFixed(2)}</div>
|
||
<div className="text-xs text-muted-foreground">毫秒</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 底部时间戳 */}
|
||
<div className="text-xs text-muted-foreground text-center">
|
||
最后更新: {new Date(metrics.timestamp).toLocaleString('zh-CN')}
|
||
</div>
|
||
|
||
{/* 线程详情对话框 */}
|
||
<ThreadDetailDialog
|
||
open={threadDialogOpen}
|
||
onOpenChange={setThreadDialogOpen}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default MetricsDashboard;
|