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
+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>