feat: implement precise worker status tracking and monitoring UI

This commit is contained in:
duorameng
2026-06-12 18:48:33 +08:00
parent 5626873454
commit 633d683c0e
9 changed files with 707 additions and 2 deletions
+114
View File
@@ -0,0 +1,114 @@
package controllers
import (
"net/http"
"runtime"
"time"
"github.com/engigu/baihu-panel/internal/services/tasks"
"github.com/engigu/baihu-panel/internal/utils"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
type MonitorController struct {
executorService *tasks.ExecutorService
}
func NewMonitorController(executorService *tasks.ExecutorService) *MonitorController {
return &MonitorController{
executorService: executorService,
}
}
var monitorUpgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 开发环境允许所有跨域,生产环境可根据配置限制
},
}
// GetSystemMonitor 获取系统和内存监控信息 (HTTP)
func (mc *MonitorController) GetSystemMonitor(c *gin.Context) {
data := mc.getMonitorData()
utils.Success(c, data)
}
// MonitorWS WebSocket实时推送系统监控信息
func (mc *MonitorController) MonitorWS(c *gin.Context) {
ws, err := monitorUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer ws.Close()
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
// 先立即发送一次
mc.sendMonitorData(ws)
for {
select {
case <-ticker.C:
if err := mc.sendMonitorData(ws); err != nil {
return // 客户端断开连接或发送失败
}
case <-c.Request.Context().Done():
return
}
}
}
func (mc *MonitorController) sendMonitorData(ws *websocket.Conn) error {
data := mc.getMonitorData()
return ws.WriteJSON(gin.H{
"code": 200,
"data": data,
"msg": "success",
})
}
func (mc *MonitorController) getMonitorData() gin.H {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return gin.H{
"env": gin.H{
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"go_version": runtime.Version(),
"num_cpu": runtime.NumCPU(),
"goroutines": runtime.NumGoroutine(),
},
"mem": gin.H{
"alloc": m.Alloc,
"total_alloc": m.TotalAlloc,
"sys": m.Sys,
"lookups": m.Lookups,
"mallocs": m.Mallocs,
"frees": m.Frees,
},
"heap": gin.H{
"heap_alloc": m.HeapAlloc,
"heap_sys": m.HeapSys,
"heap_idle": m.HeapIdle,
"heap_inuse": m.HeapInuse,
"heap_released": m.HeapReleased,
"heap_objects": m.HeapObjects,
},
"gc": gin.H{
"next_gc": m.NextGC,
"last_gc": m.LastGC,
"pause_total_ns": m.PauseTotalNs,
"num_gc": m.NumGC,
},
"scheduler": gin.H{
"scheduled": mc.executorService.GetScheduledCount(),
"running": mc.executorService.GetRunningCount(),
"queue_size": mc.executorService.GetScheduler().GetQueueSize(),
"worker_count": mc.executorService.GetScheduler().GetConfig().WorkerCount,
"workers": mc.executorService.GetScheduler().GetWorkerStatuses(),
},
}
}
+68 -1
View File
@@ -174,6 +174,16 @@ func (h *schedulerHooksAdapter) OnHeartbeat(ctx context.Context, logID string, d
// TaskExecutor 定义任务执行函数签名
type TaskExecutor func(ctx context.Context, req *ExecutionRequest, stdout, stderr io.Writer) (*Result, error)
// WorkerStatus 定义并发池中单个 Worker 的状态
type WorkerStatus struct {
ID int `json:"id"`
Status string `json:"status"` // 状态: "idle" 或 "running"
TaskID string `json:"task_id,omitempty"`
TaskName string `json:"task_name,omitempty"`
StartTime int64 `json:"start_time,omitempty"` // 开始时间戳 (秒)
Duration int64 `json:"duration,omitempty"` // 已运行时长 (秒)
}
// Scheduler 统一调度器(独立组件,可在主服务和 Agent 中复用)
// 调度器本身只负责队列管理和任务调度,具体的执行逻辑和事件处理由 Handler 实现
type Scheduler struct {
@@ -188,6 +198,9 @@ type Scheduler struct {
logger SchedulerLogger
runningTasks map[string]context.CancelFunc // 记录运行中的任务,用于停止 (TaskID -> CancelFunc)
runningExecs map[string]context.CancelFunc // 记录运行中的执行,用于停止 (LogID -> CancelFunc)
workers []WorkerStatus
workerMu sync.RWMutex
}
// NewScheduler 创建调度器
@@ -224,6 +237,14 @@ func NewScheduler(config SchedulerConfig, handler SchedulerEventHandler) *Schedu
logger: &DefaultLogger{},
runningTasks: make(map[string]context.CancelFunc),
runningExecs: make(map[string]context.CancelFunc),
workers: make([]WorkerStatus, config.WorkerCount),
}
for i := 0; i < config.WorkerCount; i++ {
s.workers[i] = WorkerStatus{
ID: i,
Status: "idle",
}
}
return s
@@ -332,7 +353,32 @@ func (s *Scheduler) worker(id int) {
}()
// 速率限制
<-s.rateLimiter
s.executeTask(req)
func() {
// 恢复 worker 状态为空闲
defer func() {
s.workerMu.Lock()
if id >= 0 && id < len(s.workers) {
s.workers[id].Status = "idle"
s.workers[id].TaskID = ""
s.workers[id].TaskName = ""
s.workers[id].StartTime = 0
}
s.workerMu.Unlock()
}()
// 更新 worker 状态为运行中
s.workerMu.Lock()
if id >= 0 && id < len(s.workers) {
s.workers[id].Status = "running"
s.workers[id].TaskID = req.TaskID
s.workers[id].TaskName = req.Name
s.workers[id].StartTime = time.Now().Unix()
}
s.workerMu.Unlock()
s.executeTask(req)
}()
}()
}
}
@@ -615,3 +661,24 @@ func (s *Scheduler) GetConfig() SchedulerConfig {
defer s.mu.RUnlock()
return s.config
}
// GetWorkerStatuses 获取所有 Worker 的状态
func (s *Scheduler) GetWorkerStatuses() []WorkerStatus {
s.workerMu.RLock()
defer s.workerMu.RUnlock()
// 返回副本防止外部修改
statuses := make([]WorkerStatus, len(s.workers))
now := time.Now().Unix()
for i, w := range s.workers {
statuses[i] = w
// 在服务端计算运行时间,彻底避免客户端与服务端时钟不一致导致的计算偏差
if w.Status == "running" && w.StartTime > 0 {
duration := now - w.StartTime
if duration < 0 {
duration = 0
}
statuses[i].Duration = duration
}
}
return statuses
}
+9
View File
@@ -65,6 +65,7 @@ func initAuthorizedAPIRoutes(api *gin.RouterGroup, c *Controllers) {
registerAppLogRoutes(adminOnly, c)
registerSystemWSRoutes(adminOnly, c)
registerWebUIRoutes(adminOnly, c)
registerMonitorRoutes(adminOnly, c)
}
}
@@ -268,6 +269,14 @@ func registerSystemWSRoutes(g *gin.RouterGroup, c *Controllers) {
g.GET("/ws/events", c.SystemWS.HandleEvents)
}
func registerMonitorRoutes(g *gin.RouterGroup, c *Controllers) {
monitor := g.Group("/monitor")
{
monitor.GET("", c.Monitor.GetSystemMonitor)
monitor.GET("/ws", c.Monitor.MonitorWS)
}
}
func initAgentAPIRoutes(root *gin.RouterGroup, c *Controllers) {
// Agent API(供远程 Agent 调用,不使用 /v1 版本号)
agentAPI := root.Group("/api/agent")
+1
View File
@@ -64,6 +64,7 @@ func RegisterControllers() *Controllers {
AppLog: controllers.NewAppLogController(),
SystemWS: controllers.NewSystemWSController(),
WebUI: controllers.NewWebUIController(services.NewWebUIService(settingsService)),
Monitor: controllers.NewMonitorController(executorService),
}
}
+1
View File
@@ -31,6 +31,7 @@ type Controllers struct {
AppLog *controllers.AppLogController
SystemWS *controllers.SystemWSController
WebUI *controllers.WebUIController
Monitor *controllers.MonitorController
}
func Setup(c *Controllers) *gin.Engine {
+47
View File
@@ -9,6 +9,52 @@ interface ApiResponse<T> {
data: T
}
export interface MonitorStats {
env: {
os: string
arch: string
go_version: string
num_cpu: number
goroutines: number
}
mem: {
alloc: number
total_alloc: number
sys: number
lookups: number
mallocs: number
frees: number
}
heap: {
heap_alloc: number
heap_sys: number
heap_idle: number
heap_inuse: number
heap_released: number
heap_objects: number
}
gc: {
next_gc: number
last_gc: number
pause_total_ns: number
num_gc: number
}
scheduler: {
scheduled: number
running: number
queue_size: number
worker_count: number
workers: {
id: number
status: string
task_id?: string
task_name?: string
start_time?: number
duration?: number
}[]
}
}
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE_URL}${url}`, {
...options,
@@ -145,6 +191,7 @@ export const api = {
taskStats: (days?: number) => request<TaskStatsItem[]>(`/taskstats${days ? `?days=${days}` : ''}`)
},
settings: {
getMonitor: () => request<MonitorStats>('/monitor'),
changePassword: (data: { old_username?: string; username?: string; old_password: string; new_password?: string }) =>
request('/settings/password', { method: 'POST', body: JSON.stringify(data) }),
getSite: () => request<SiteSettings>('/settings/site'),
+2 -1
View File
@@ -2,7 +2,7 @@
import { ref, onMounted, computed } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import { resetAuthCache } from '@/router'
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound, Menu, X, Server, Globe, Bell } from 'lucide-vue-next'
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound, Menu, X, Server, Globe, Bell, Activity } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import ThemeToggle from '@/components/ThemeToggle.vue'
import SystemNotice from '@/components/SystemNotice.vue'
@@ -63,6 +63,7 @@ const navItems = [
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
{ to: '/notify', icon: Bell, label: '消息推送', exact: true },
{ to: '/logs', icon: KeyRound, label: '运行日志', exact: true },
{ to: '/monitor', icon: Activity, label: '系统监控', exact: true },
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
]
+1
View File
@@ -54,6 +54,7 @@ const router = createRouter({
{ path: 'logs', name: 'logs', component: () => import('@/views/logs/MessageLogs.vue') },
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
{ path: 'notify', name: 'notify', component: () => import('@/views/notify/Notify.vue') },
{ path: 'monitor', name: 'monitor', component: () => import('@/views/monitor/Monitor.vue') },
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
]
},
+464
View File
@@ -0,0 +1,464 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import type { MonitorStats } from '@/api'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { RefreshCw } from 'lucide-vue-next'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
Filler
} from 'chart.js'
import { Line, Bar } from 'vue-chartjs'
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
Filler
)
const activeTab = ref('charts')
const stats = ref<MonitorStats | null>(null)
const loading = ref(false)
let timer: any = null
// --- 时序数据池 ---
const historySize = 60 // 保存最近60次请求(约3分钟@3s)
const timeLabels = ref<string[]>([])
const goroutinesData = ref<number[]>([])
const allocData = ref<number[]>([])
const sysData = ref<number[]>([])
const gcPausesData = ref<number[]>([])
const scheduledData = ref<number[]>([])
const runningData = ref<number[]>([])
const queueData = ref<number[]>([])
let lastPauseNs = 0
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const baseUrl = (window as any).__BASE_URL__ || ''
const apiVersion = (window as any).__API_VERSION__ || '/api/v1'
const connectWS = () => {
if (timer) return
loading.value = true
const host = window.location.host
const wsUrl = `${protocol}//${host}${baseUrl}${apiVersion}/monitor/ws`
timer = new WebSocket(wsUrl)
timer.onopen = () => {
loading.value = false
}
timer.onmessage = (event: MessageEvent) => {
try {
const payload = JSON.parse(event.data)
if (payload.code === 200 && payload.data) {
const res = payload.data
stats.value = res
const nowStr = new Date().toLocaleTimeString('en-US', { hour12: false })
if (timeLabels.value.length >= historySize) {
timeLabels.value.shift()
goroutinesData.value.shift()
allocData.value.shift()
sysData.value.shift()
gcPausesData.value.shift()
scheduledData.value.shift()
runningData.value.shift()
queueData.value.shift()
}
timeLabels.value.push(nowStr)
goroutinesData.value.push(res.env.goroutines)
allocData.value.push(Number((res.mem.alloc / 1024 / 1024).toFixed(2)))
sysData.value.push(Number((res.mem.sys / 1024 / 1024).toFixed(2)))
let pauseDelta = 0
if (lastPauseNs > 0 && res.gc.pause_total_ns >= lastPauseNs) {
pauseDelta = Number(((res.gc.pause_total_ns - lastPauseNs) / 1000000).toFixed(2))
}
lastPauseNs = res.gc.pause_total_ns
gcPausesData.value.push(pauseDelta)
scheduledData.value.push(res.scheduler.scheduled)
runningData.value.push(res.scheduler.running)
queueData.value.push(res.scheduler.queue_size)
}
} catch (e) {
console.error('Parse WS message error:', e)
}
}
timer.onclose = () => {
loading.value = false
timer = null
// 断线后2秒自动重连
setTimeout(connectWS, 2000)
}
timer.onerror = () => {
loading.value = false
}
}
const disconnectWS = () => {
if (timer) {
// 置空 onclose 避免触发自动重连
timer.onclose = null
timer.close()
timer = null
}
}
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const formatNs = (ns: number) => {
if (ns < 1000) return ns + ' ns'
if (ns < 1000000) return (ns / 1000).toFixed(2) + ' μs'
return (ns / 1000000).toFixed(2) + ' ms'
}
onMounted(() => {
connectWS()
})
onUnmounted(() => {
disconnectWS()
})
// --- 图表配置 ---
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0 // 关闭过渡动画以支持实时流畅更新
},
interaction: {
mode: 'index' as const,
intersect: false,
},
plugins: {
legend: {
position: 'top' as const,
labels: {
usePointStyle: false,
boxWidth: 12,
boxHeight: 12,
useBorderRadius: true,
borderRadius: 2
}
}
},
scales: {
x: {
grid: { display: false }
},
y: {
beginAtZero: true,
border: { dash: [4, 4] },
grid: { color: 'rgba(0, 0, 0, 0.05)' }
}
}
}
const goroutineChartData = computed(() => ({
labels: [...timeLabels.value],
datasets: [
{
label: 'Goroutines (协程并发数)',
backgroundColor: 'rgba(37, 99, 235, 0.1)',
borderColor: '#2563eb', // blue-600
borderWidth: 2,
data: [...goroutinesData.value],
tension: 0.4,
fill: true,
pointRadius: 0,
pointHitRadius: 10
}
]
}))
const memChartData = computed(() => ({
labels: [...timeLabels.value],
datasets: [
{
label: 'Alloc (MB) 当前分配',
backgroundColor: 'rgba(5, 150, 105, 0.1)',
borderColor: '#059669', // emerald-600
borderWidth: 2,
data: [...allocData.value],
tension: 0.4,
fill: true,
pointRadius: 0,
pointHitRadius: 10
},
{
label: 'Sys (MB) 系统申请上限',
backgroundColor: 'transparent',
borderColor: '#d97706', // amber-600
borderWidth: 2,
borderDash: [5, 5],
data: [...sysData.value],
tension: 0.4,
pointRadius: 0,
pointHitRadius: 10
}
]
}))
const gcChartData = computed(() => ({
labels: [...timeLabels.value],
datasets: [
{
label: 'GC Pause (ms) 垃圾回收停顿时间',
backgroundColor: '#ea580c', // orange-600
data: [...gcPausesData.value],
borderRadius: 4
}
]
}))
const schedulerChartData = computed(() => ({
labels: [...timeLabels.value],
datasets: [
{
label: '正在运行 (Running)',
backgroundColor: 'rgba(16, 185, 129, 0.1)', // emerald-500
borderColor: '#10b981',
borderWidth: 2,
data: [...runningData.value],
tension: 0.4,
fill: true,
pointRadius: 0,
pointHitRadius: 10
},
{
label: '调度中 (Scheduled)',
backgroundColor: 'transparent',
borderColor: '#6366f1', // indigo-500
borderWidth: 2,
borderDash: [5, 5],
data: [...scheduledData.value],
tension: 0.4,
pointRadius: 0,
pointHitRadius: 10
},
{
label: '排队积压 (Queue)',
backgroundColor: 'transparent',
borderColor: '#f59e0b', // amber-500
borderWidth: 2,
borderDash: [2, 2],
data: [...queueData.value],
tension: 0.4,
pointRadius: 0,
pointHitRadius: 10
}
]
}))
</script>
<template>
<Tabs v-model="activeTab" class="space-y-6 w-full">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 class="text-xl sm:text-2xl font-bold tracking-tight">系统监控</h2>
<p class="text-muted-foreground text-sm">实时监控面板资源内存分配和垃圾回收状态</p>
</div>
<div class="flex items-center gap-2 w-full sm:w-auto">
<TabsList class="grid flex-1 sm:flex-none sm:w-[240px] grid-cols-2 h-9 p-1">
<TabsTrigger value="charts" class="text-xs sm:text-sm">实时图表</TabsTrigger>
<TabsTrigger value="dashboard" class="text-xs sm:text-sm">数据视图</TabsTrigger>
</TabsList>
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="() => { disconnectWS(); connectWS() }" :disabled="loading" title="刷新并重连">
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': loading }" />
</Button>
</div>
</div>
<TabsContent value="dashboard" class="mt-0 space-y-4">
<div v-if="stats" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base text-blue-600">执行环境</CardTitle>
</CardHeader>
<CardContent>
<dl class="space-y-1 text-sm">
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">Go 版本</dt><dd class="font-medium">{{ stats.env.go_version }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">系统 / 架构</dt><dd class="font-medium">{{ stats.env.os }} / {{ stats.env.arch }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">逻辑 CPU 数量</dt><dd class="font-medium">{{ stats.env.num_cpu }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">当前协程数 (Goroutines)</dt><dd class="font-bold text-blue-600">{{ stats.env.goroutines }}</dd></div>
</dl>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base text-indigo-600">任务调度</CardTitle>
</CardHeader>
<CardContent>
<dl class="space-y-1 text-sm">
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">驻留常驻任务 (Scheduled)</dt><dd class="font-medium text-indigo-600">{{ stats.scheduler.scheduled }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">当前运行中 (Running)</dt><dd class="font-bold text-emerald-600">{{ stats.scheduler.running }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">排队积压 (Queue Size)</dt><dd class="font-bold text-amber-500">{{ stats.scheduler.queue_size }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">并发池限制 (Worker Count)</dt><dd class="font-medium">{{ stats.scheduler.worker_count }}</dd></div>
</dl>
</CardContent>
</Card>
</div>
<div v-if="stats && stats.scheduler.workers" class="mt-4">
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base text-amber-600">并发池 Worker 状态</CardTitle>
<CardDescription>精确监控底层协程池调度执行情况</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mt-2">
<div v-for="worker in stats.scheduler.workers" :key="worker.id" class="border rounded-md p-3 flex flex-col justify-between" :class="[worker.status === 'running' ? 'bg-amber-50 border-amber-200 dark:bg-amber-950/20 dark:border-amber-900/50' : 'bg-green-50 border-green-200 dark:bg-green-950/20 dark:border-green-900/50']">
<div class="flex items-center justify-between mb-2">
<span class="font-semibold text-sm">Worker #{{ worker.id }}</span>
<span v-if="worker.status === 'idle'" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300">
🟢 空闲 (Idle)
</span>
<span v-else class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300">
执行中 (Running)
</span>
</div>
<div v-if="worker.status === 'running'" class="text-xs text-muted-foreground mt-1 space-y-1">
<div class="truncate" :title="worker.task_name"><span class="font-medium text-foreground">任务:</span> {{ worker.task_name || worker.task_id }}</div>
<div><span class="font-medium text-foreground">运行时间:</span> {{ worker.duration || 0 }}s</div>
</div>
<div v-else class="text-xs text-muted-foreground mt-1">
当前暂无任务分配
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base text-emerald-600">内存概览</CardTitle>
</CardHeader>
<CardContent>
<dl class="space-y-1 text-sm">
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">当前分配内存 (Alloc)</dt><dd class="font-bold text-emerald-600">{{ formatBytes(stats.mem.alloc) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">累计分配内存 (TotalAlloc)</dt><dd class="font-medium">{{ formatBytes(stats.mem.total_alloc) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">向系统获取内存 (Sys)</dt><dd class="font-medium">{{ formatBytes(stats.mem.sys) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">内存分配次数 (Mallocs)</dt><dd class="font-medium">{{ stats.mem.mallocs }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">内存释放次数 (Frees)</dt><dd class="font-medium">{{ stats.mem.frees }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">指针查找次数 (Lookups)</dt><dd class="font-medium">{{ stats.mem.lookups }}</dd></div>
</dl>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base text-purple-600">堆栈明细</CardTitle>
</CardHeader>
<CardContent>
<dl class="space-y-1 text-sm">
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">堆分配内存 (HeapAlloc)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_alloc) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">堆系统内存 (HeapSys)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_sys) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">空闲堆内存 (HeapIdle)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_idle) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">使用中堆内存 (HeapInuse)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_inuse) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">已释放堆内存 (HeapReleased)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_released) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">堆对象数量 (HeapObjects)</dt><dd class="font-medium">{{ stats.heap.heap_objects }}</dd></div>
</dl>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base text-orange-600">垃圾回收 (GC)</CardTitle>
</CardHeader>
<CardContent>
<dl class="space-y-1 text-sm">
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">下次 GC 目标 (NextGC)</dt><dd class="font-medium">{{ formatBytes(stats.gc.next_gc) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">上次 GC 时间 (LastGC)</dt><dd class="font-medium">{{ stats.gc.last_gc ? new Date(stats.gc.last_gc / 1000000).toLocaleString() : '-' }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">GC 总停顿时间 (PauseTotalNs)</dt><dd class="font-medium">{{ formatNs(stats.gc.pause_total_ns) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">执行次数 (NumGC)</dt><dd class="font-medium">{{ stats.gc.num_gc }}</dd></div>
</dl>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="charts" class="mt-0 space-y-4">
<div class="grid grid-cols-1 gap-4">
<!-- Scheduler 图表 -->
<Card>
<CardHeader class="py-4">
<CardTitle class="text-base text-indigo-600">任务调度 (Scheduler)</CardTitle>
<CardDescription>跟踪当前节点上正在运行和调度的任务数量</CardDescription>
</CardHeader>
<CardContent>
<div class="h-64 w-full">
<Line :data="schedulerChartData" :options="chartOptions" />
</div>
</CardContent>
</Card>
<!-- Goroutines 图表 -->
<Card>
<CardHeader class="py-4">
<CardTitle class="text-base text-blue-600">并发协程 (Goroutines)</CardTitle>
<CardDescription>跟踪系统中轻量级线程的创建与销毁情况</CardDescription>
</CardHeader>
<CardContent>
<div class="h-64 w-full">
<Line :data="goroutineChartData" :options="chartOptions" />
</div>
</CardContent>
</Card>
<!-- Memory 图表 -->
<Card>
<CardHeader class="py-4">
<CardTitle class="text-base text-emerald-600">内存分配 (Memory Alloc vs Sys)</CardTitle>
<CardDescription>真实使用的内存 (Alloc) 与向系统申请的内存上限 (Sys)</CardDescription>
</CardHeader>
<CardContent>
<div class="h-64 w-full">
<Line :data="memChartData" :options="chartOptions" />
</div>
</CardContent>
</Card>
<!-- GC 图表 -->
<Card>
<CardHeader class="py-4">
<CardTitle class="text-base text-orange-600">垃圾回收停顿 (GC Pauses)</CardTitle>
<CardDescription>每次探针周期内发生的垃圾回收暂停总耗时毫秒</CardDescription>
</CardHeader>
<CardContent>
<div class="h-64 w-full">
<Bar :data="gcChartData" :options="chartOptions" />
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</template>