add bridge performance monitoring
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { request } from '../client'
|
||||
|
||||
export interface ProcessUsage {
|
||||
pid: number
|
||||
role: 'web' | 'broker' | 'worker'
|
||||
profile?: string
|
||||
running: boolean
|
||||
cpuPercent: number
|
||||
memoryRssBytes: number
|
||||
command?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface PerformanceRuntimeSnapshot {
|
||||
timestamp: number
|
||||
system: {
|
||||
platform: string
|
||||
arch: string
|
||||
uptimeSeconds: number
|
||||
cpuCount: number
|
||||
cpuPercent: number
|
||||
loadAverage: number[]
|
||||
totalMemoryBytes: number
|
||||
freeMemoryBytes: number
|
||||
usedMemoryBytes: number
|
||||
memoryPercent: number
|
||||
}
|
||||
web: {
|
||||
pid: number
|
||||
uptimeSeconds: number
|
||||
memory: Record<string, number>
|
||||
cpuPercent: number
|
||||
}
|
||||
bridge: {
|
||||
endpoint: string
|
||||
reachable: boolean
|
||||
error?: string
|
||||
broker: {
|
||||
running: boolean
|
||||
ready: boolean
|
||||
pid?: number
|
||||
process?: ProcessUsage
|
||||
restartScheduled: boolean
|
||||
restartAttempts: number
|
||||
}
|
||||
workers: Array<ProcessUsage & {
|
||||
endpoint?: string
|
||||
lastUsedAt?: number
|
||||
sessionCount: number
|
||||
runningSessionCount: number
|
||||
}>
|
||||
totalWorkerMemoryRssBytes: number
|
||||
}
|
||||
sessions: {
|
||||
active: number
|
||||
running: number
|
||||
byProfile: Record<string, number>
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPerformanceRuntime(): Promise<PerformanceRuntimeSnapshot> {
|
||||
return request<PerformanceRuntimeSnapshot>('/api/hermes/performance/runtime')
|
||||
}
|
||||
@@ -226,10 +226,17 @@ function openChangelog() {
|
||||
</svg>
|
||||
<span>{{ t("sidebar.usage") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.skillsUsage' }" @click="handleNav('hermes.skillsUsage')">
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.performance' }" @click="handleNav('hermes.performance')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.performance") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.skillsUsage' }" @click="handleNav('hermes.skillsUsage')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21.21 15.89A10 10 0 1 1 8.11 2.79" />
|
||||
<path d="M22 12A10 10 0 0 0 12 2v10z" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.skillsUsage") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
memory: 'Gedachtnis',
|
||||
logs: 'Protokolle',
|
||||
usage: 'Nutzung',
|
||||
performance: 'Leistung',
|
||||
skillsUsage: 'Skill-Nutzung',
|
||||
channels: 'Kanale',
|
||||
terminal: 'Konsole',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
collapse: 'Menü einklappen',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: 'Leistung',
|
||||
subtitle: 'Systemressourcen, Bridge Broker, Workers und aktive Sitzungen überwachen',
|
||||
refresh: 'Aktualisieren',
|
||||
autoRefreshOn: 'Automatisch aktualisieren',
|
||||
autoRefreshOff: 'Manuell aktualisieren',
|
||||
loadFailed: 'Leistungsdaten konnten nicht geladen werden',
|
||||
systemCpu: 'System-CPU',
|
||||
systemMemory: 'Systemspeicher',
|
||||
activeSessions: 'Aktive Sitzungen',
|
||||
runningSessions: 'Laufend {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Worker-Gesamtspeicher',
|
||||
processes: 'Prozesse',
|
||||
uptime: 'Laufzeit',
|
||||
running: 'Läuft',
|
||||
stopped: 'Gestoppt',
|
||||
workerMemory: 'Worker-Speicher',
|
||||
lastUpdated: 'Aktualisiert',
|
||||
profile: 'Profile',
|
||||
memory: 'Speicher',
|
||||
sessions: 'Sitzungen',
|
||||
runningActiveSessions: 'Laufend / Aktiv',
|
||||
lastUsed: 'Zuletzt verwendet',
|
||||
status: 'Status',
|
||||
noWorkers: 'Keine Workers',
|
||||
sessionsByProfile: 'Sitzungen nach Profile',
|
||||
noActiveSessions: 'Keine aktiven Sitzungen',
|
||||
},
|
||||
|
||||
// Drawer
|
||||
drawer: {
|
||||
terminal: 'Konsole',
|
||||
|
||||
@@ -83,6 +83,7 @@ export default {
|
||||
memory: 'Memory',
|
||||
logs: 'Logs',
|
||||
usage: 'Usage',
|
||||
performance: 'Performance',
|
||||
skillsUsage: 'Skills Usage',
|
||||
channels: 'Channels',
|
||||
gateways: 'Gateways',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
noChangelog: 'No changelog available',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: 'Performance',
|
||||
subtitle: 'Inspect system resources, bridge broker, workers, and active sessions',
|
||||
refresh: 'Refresh',
|
||||
autoRefreshOn: 'Auto refresh',
|
||||
autoRefreshOff: 'Manual refresh',
|
||||
loadFailed: 'Failed to load performance metrics',
|
||||
systemCpu: 'System CPU',
|
||||
systemMemory: 'System Memory',
|
||||
activeSessions: 'Active Sessions',
|
||||
runningSessions: 'Running {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Worker memory',
|
||||
processes: 'Processes',
|
||||
uptime: 'Uptime',
|
||||
running: 'Running',
|
||||
stopped: 'Stopped',
|
||||
workerMemory: 'Worker Memory',
|
||||
lastUpdated: 'Updated',
|
||||
profile: 'Profile',
|
||||
memory: 'Memory',
|
||||
sessions: 'Sessions',
|
||||
runningActiveSessions: 'Running / Active',
|
||||
lastUsed: 'Last Used',
|
||||
status: 'Status',
|
||||
noWorkers: 'No workers',
|
||||
sessionsByProfile: 'Sessions by Profile',
|
||||
noActiveSessions: 'No active sessions',
|
||||
},
|
||||
|
||||
// Drawer
|
||||
drawer: {
|
||||
terminal: 'Terminal',
|
||||
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
memory: 'Memoria',
|
||||
logs: 'Registros',
|
||||
usage: 'Uso',
|
||||
performance: 'Rendimiento',
|
||||
skillsUsage: 'Uso de habilidades',
|
||||
channels: 'Canales',
|
||||
terminal: 'Terminal',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
collapse: 'Contraer menú',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: 'Rendimiento',
|
||||
subtitle: 'Supervisa recursos del sistema, Bridge Broker, Workers y sesiones activas',
|
||||
refresh: 'Actualizar',
|
||||
autoRefreshOn: 'Actualización automática',
|
||||
autoRefreshOff: 'Actualización manual',
|
||||
loadFailed: 'No se pudieron cargar las métricas de rendimiento',
|
||||
systemCpu: 'CPU del sistema',
|
||||
systemMemory: 'Memoria del sistema',
|
||||
activeSessions: 'Sesiones activas',
|
||||
runningSessions: 'En ejecución {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Memoria total de Worker',
|
||||
processes: 'Procesos',
|
||||
uptime: 'Tiempo activo',
|
||||
running: 'En ejecución',
|
||||
stopped: 'Detenido',
|
||||
workerMemory: 'Memoria de Worker',
|
||||
lastUpdated: 'Actualizado',
|
||||
profile: 'Profile',
|
||||
memory: 'Memoria',
|
||||
sessions: 'Sesiones',
|
||||
runningActiveSessions: 'En ejecución / Activas',
|
||||
lastUsed: 'Último uso',
|
||||
status: 'Estado',
|
||||
noWorkers: 'Sin Workers',
|
||||
sessionsByProfile: 'Sesiones por Profile',
|
||||
noActiveSessions: 'No hay sesiones activas',
|
||||
},
|
||||
|
||||
// Drawer
|
||||
drawer: {
|
||||
terminal: 'Terminal',
|
||||
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
memory: 'Memoire',
|
||||
logs: 'Journaux',
|
||||
usage: 'Utilisation',
|
||||
performance: 'Performance',
|
||||
skillsUsage: 'Utilisation des compétences',
|
||||
channels: 'Canaux',
|
||||
terminal: 'Terminal',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
collapse: 'Replier le menu',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: 'Performance',
|
||||
subtitle: 'Surveiller les ressources système, Bridge Broker, Workers et sessions actives',
|
||||
refresh: 'Actualiser',
|
||||
autoRefreshOn: 'Actualisation auto',
|
||||
autoRefreshOff: 'Actualisation manuelle',
|
||||
loadFailed: 'Échec du chargement des métriques de performance',
|
||||
systemCpu: 'CPU système',
|
||||
systemMemory: 'Mémoire système',
|
||||
activeSessions: 'Sessions actives',
|
||||
runningSessions: 'En cours {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Mémoire totale Worker',
|
||||
processes: 'Processus',
|
||||
uptime: 'Disponibilité',
|
||||
running: 'En cours',
|
||||
stopped: 'Arrêté',
|
||||
workerMemory: 'Mémoire Worker',
|
||||
lastUpdated: 'Mis à jour',
|
||||
profile: 'Profile',
|
||||
memory: 'Mémoire',
|
||||
sessions: 'Sessions',
|
||||
runningActiveSessions: 'En cours / Actives',
|
||||
lastUsed: 'Dernière utilisation',
|
||||
status: 'Statut',
|
||||
noWorkers: 'Aucun Worker',
|
||||
sessionsByProfile: 'Sessions par Profile',
|
||||
noActiveSessions: 'Aucune session active',
|
||||
},
|
||||
|
||||
// Drawer
|
||||
drawer: {
|
||||
terminal: 'Terminal',
|
||||
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
memory: 'メモリ',
|
||||
logs: 'ログ',
|
||||
usage: '使用量',
|
||||
performance: 'パフォーマンス',
|
||||
skillsUsage: 'スキル使用状況',
|
||||
channels: 'チャンネル',
|
||||
terminal: 'ターミナル',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
collapse: 'メニューを折りたたむ',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: 'パフォーマンス',
|
||||
subtitle: 'システムリソース、Bridge Broker、Workers、アクティブセッションを確認',
|
||||
refresh: '更新',
|
||||
autoRefreshOn: '自動更新',
|
||||
autoRefreshOff: '手動更新',
|
||||
loadFailed: 'パフォーマンスデータの読み込みに失敗しました',
|
||||
systemCpu: 'システム CPU',
|
||||
systemMemory: 'システムメモリ',
|
||||
activeSessions: 'アクティブセッション',
|
||||
runningSessions: '実行中 {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Worker 合計メモリ',
|
||||
processes: 'プロセス',
|
||||
uptime: '稼働時間',
|
||||
running: '実行中',
|
||||
stopped: '停止',
|
||||
workerMemory: 'Worker メモリ',
|
||||
lastUpdated: '更新時刻',
|
||||
profile: 'Profile',
|
||||
memory: 'メモリ',
|
||||
sessions: 'セッション',
|
||||
runningActiveSessions: '実行中 / アクティブ',
|
||||
lastUsed: '最終使用',
|
||||
status: '状態',
|
||||
noWorkers: 'Worker はありません',
|
||||
sessionsByProfile: 'Profile 別セッション',
|
||||
noActiveSessions: 'アクティブセッションはありません',
|
||||
},
|
||||
|
||||
// ドロワー
|
||||
drawer: {
|
||||
terminal: 'ターミナル',
|
||||
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
memory: '메모리',
|
||||
logs: '로그',
|
||||
usage: '사용량',
|
||||
performance: '성능 모니터링',
|
||||
skillsUsage: '스킬 사용량',
|
||||
channels: '채널',
|
||||
terminal: '터미널',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
collapse: '메뉴 접기',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: '성능 모니터링',
|
||||
subtitle: '시스템 리소스, Bridge Broker, Workers, 활성 세션 확인',
|
||||
refresh: '새로고침',
|
||||
autoRefreshOn: '자동 새로고침',
|
||||
autoRefreshOff: '수동 새로고침',
|
||||
loadFailed: '성능 데이터를 불러오지 못했습니다',
|
||||
systemCpu: '시스템 CPU',
|
||||
systemMemory: '시스템 메모리',
|
||||
activeSessions: '활성 세션',
|
||||
runningSessions: '실행 중 {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Worker 총 메모리',
|
||||
processes: '프로세스',
|
||||
uptime: '실행 시간',
|
||||
running: '실행 중',
|
||||
stopped: '중지됨',
|
||||
workerMemory: 'Worker 메모리',
|
||||
lastUpdated: '업데이트 시간',
|
||||
profile: 'Profile',
|
||||
memory: '메모리',
|
||||
sessions: '세션',
|
||||
runningActiveSessions: '실행 중 / 활성',
|
||||
lastUsed: '마지막 사용',
|
||||
status: '상태',
|
||||
noWorkers: 'Worker 없음',
|
||||
sessionsByProfile: 'Profile별 세션',
|
||||
noActiveSessions: '활성 세션 없음',
|
||||
},
|
||||
|
||||
// 서랍
|
||||
drawer: {
|
||||
terminal: '터미널',
|
||||
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
memory: 'Memoria',
|
||||
logs: 'Logs',
|
||||
usage: 'Uso',
|
||||
performance: 'Desempenho',
|
||||
skillsUsage: 'Uso de habilidades',
|
||||
channels: 'Canais',
|
||||
terminal: 'Terminal',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
collapse: 'Recolher menu',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: 'Desempenho',
|
||||
subtitle: 'Monitore recursos do sistema, Bridge Broker, Workers e sessões ativas',
|
||||
refresh: 'Atualizar',
|
||||
autoRefreshOn: 'Atualização automática',
|
||||
autoRefreshOff: 'Atualização manual',
|
||||
loadFailed: 'Falha ao carregar métricas de desempenho',
|
||||
systemCpu: 'CPU do sistema',
|
||||
systemMemory: 'Memória do sistema',
|
||||
activeSessions: 'Sessões ativas',
|
||||
runningSessions: 'Em execução {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Memória total de Worker',
|
||||
processes: 'Processos',
|
||||
uptime: 'Tempo ativo',
|
||||
running: 'Em execução',
|
||||
stopped: 'Parado',
|
||||
workerMemory: 'Memória de Worker',
|
||||
lastUpdated: 'Atualizado',
|
||||
profile: 'Profile',
|
||||
memory: 'Memória',
|
||||
sessions: 'Sessões',
|
||||
runningActiveSessions: 'Em execução / Ativas',
|
||||
lastUsed: 'Último uso',
|
||||
status: 'Status',
|
||||
noWorkers: 'Nenhum Worker',
|
||||
sessionsByProfile: 'Sessões por Profile',
|
||||
noActiveSessions: 'Nenhuma sessão ativa',
|
||||
},
|
||||
|
||||
// Gaveta
|
||||
drawer: {
|
||||
terminal: 'Terminal',
|
||||
|
||||
@@ -83,6 +83,7 @@ export default {
|
||||
memory: '記憶',
|
||||
logs: '日誌',
|
||||
usage: '用量',
|
||||
performance: '效能監控',
|
||||
skillsUsage: '技能用量',
|
||||
channels: '頻道',
|
||||
gateways: '閘道',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
noChangelog: '目前無更新日誌',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: '效能監控',
|
||||
subtitle: '查看系統資源、Bridge Broker、Workers 和活躍會話',
|
||||
refresh: '重新整理',
|
||||
autoRefreshOn: '自動重新整理',
|
||||
autoRefreshOff: '手動重新整理',
|
||||
loadFailed: '效能資料載入失敗',
|
||||
systemCpu: '系統 CPU',
|
||||
systemMemory: '系統記憶體',
|
||||
activeSessions: '活躍會話',
|
||||
runningSessions: '執行中 {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Worker 總記憶體',
|
||||
processes: '程序',
|
||||
uptime: '執行',
|
||||
running: '執行中',
|
||||
stopped: '已停止',
|
||||
workerMemory: 'Worker 記憶體',
|
||||
lastUpdated: '更新時間',
|
||||
profile: 'Profile',
|
||||
memory: '記憶體',
|
||||
sessions: '會話',
|
||||
runningActiveSessions: '執行中 / 活躍',
|
||||
lastUsed: '最後使用',
|
||||
status: '狀態',
|
||||
noWorkers: '暫無 Worker',
|
||||
sessionsByProfile: '按 Profile 統計會話',
|
||||
noActiveSessions: '暫無活躍會話',
|
||||
},
|
||||
|
||||
// 抽屜
|
||||
drawer: {
|
||||
terminal: '終端機',
|
||||
|
||||
@@ -83,6 +83,7 @@ export default {
|
||||
memory: '记忆',
|
||||
logs: '日志',
|
||||
usage: '用量',
|
||||
performance: '性能监控',
|
||||
skillsUsage: '技能用量',
|
||||
channels: '频道',
|
||||
gateways: '网关',
|
||||
@@ -116,6 +117,36 @@ export default {
|
||||
noChangelog: '暂无更新日志',
|
||||
},
|
||||
|
||||
performance: {
|
||||
title: '性能监控',
|
||||
subtitle: '查看系统资源、Bridge Broker、Workers 和活跃会话',
|
||||
refresh: '刷新',
|
||||
autoRefreshOn: '自动刷新',
|
||||
autoRefreshOff: '手动刷新',
|
||||
loadFailed: '性能数据加载失败',
|
||||
systemCpu: '系统 CPU',
|
||||
systemMemory: '系统内存',
|
||||
activeSessions: '活跃会话',
|
||||
runningSessions: '运行中 {count}',
|
||||
workers: 'Workers',
|
||||
totalWorkerMemory: 'Worker 总内存',
|
||||
processes: '进程',
|
||||
uptime: '运行',
|
||||
running: '运行中',
|
||||
stopped: '已停止',
|
||||
workerMemory: 'Worker 内存',
|
||||
lastUpdated: '更新时间',
|
||||
profile: 'Profile',
|
||||
memory: '内存',
|
||||
sessions: '会话',
|
||||
runningActiveSessions: '运行中 / 活跃',
|
||||
lastUsed: '最后使用',
|
||||
status: '状态',
|
||||
noWorkers: '暂无 Worker',
|
||||
sessionsByProfile: '按 Profile 统计会话',
|
||||
noActiveSessions: '暂无活跃会话',
|
||||
},
|
||||
|
||||
// 抽屉
|
||||
drawer: {
|
||||
terminal: '终端',
|
||||
|
||||
@@ -50,6 +50,11 @@ const router = createRouter({
|
||||
name: 'hermes.usage',
|
||||
component: () => import('@/views/hermes/UsageView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/hermes/performance',
|
||||
name: 'hermes.performance',
|
||||
component: () => import('@/views/hermes/PerformanceView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/hermes/skills-usage',
|
||||
name: 'hermes.skillsUsage',
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { NButton, NSpin, useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { fetchPerformanceRuntime, type PerformanceRuntimeSnapshot } from '@/api/hermes/performance-monitor'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const snapshot = ref<PerformanceRuntimeSnapshot | null>(null)
|
||||
const loading = ref(false)
|
||||
const autoRefresh = ref(true)
|
||||
let timer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
const brokerMemory = computed(() => snapshot.value?.bridge.broker.process?.memoryRssBytes ?? null)
|
||||
const webRssMemory = computed(() => snapshot.value?.web.memory.rss ?? null)
|
||||
const workerCount = computed(() => snapshot.value?.bridge.workers.length ?? 0)
|
||||
const runningWorkerCount = computed(() => snapshot.value?.bridge.workers.filter(worker => worker.running).length ?? 0)
|
||||
|
||||
function formatBytes(value?: number | null): string {
|
||||
if (value == null || !Number.isFinite(value)) return '-'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = value
|
||||
let unit = 0
|
||||
while (size >= 1024 && unit < units.length - 1) {
|
||||
size /= 1024
|
||||
unit += 1
|
||||
}
|
||||
return `${size.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`
|
||||
}
|
||||
|
||||
function formatPercent(value?: number | null): string {
|
||||
return value == null || !Number.isFinite(value) ? '-' : `${value.toFixed(1)}%`
|
||||
}
|
||||
|
||||
function formatDuration(seconds?: number | null): string {
|
||||
if (seconds == null || !Number.isFinite(seconds)) return '-'
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
if (days > 0) return `${days}d ${hours}h`
|
||||
if (hours > 0) return `${hours}h ${minutes}m`
|
||||
return `${minutes}m`
|
||||
}
|
||||
|
||||
function formatTime(seconds?: number): string {
|
||||
if (!seconds) return '-'
|
||||
return new Date(seconds * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
function statusText(running: boolean): string {
|
||||
return running ? t('performance.running') : t('performance.stopped')
|
||||
}
|
||||
|
||||
async function loadRuntime(showError = true) {
|
||||
loading.value = true
|
||||
try {
|
||||
snapshot.value = await fetchPerformanceRuntime()
|
||||
} catch (err: any) {
|
||||
if (showError) message.error(err?.message || t('performance.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function setAutoRefresh(enabled: boolean) {
|
||||
autoRefresh.value = enabled
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = undefined
|
||||
}
|
||||
if (enabled) {
|
||||
timer = setInterval(() => loadRuntime(false), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRuntime()
|
||||
setAutoRefresh(true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="performance-view">
|
||||
<header class="page-header">
|
||||
<h2 class="header-title">{{ t('performance.title') }}</h2>
|
||||
<div class="header-actions">
|
||||
<NButton size="small" :type="autoRefresh ? 'primary' : 'default'" secondary @click="setAutoRefresh(!autoRefresh)">
|
||||
{{ autoRefresh ? t('performance.autoRefreshOn') : t('performance.autoRefreshOff') }}
|
||||
</NButton>
|
||||
<NButton size="small" :loading="loading" @click="loadRuntime()">{{ t('performance.refresh') }}</NButton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<NSpin :show="loading && !snapshot" class="performance-spin">
|
||||
<main v-if="snapshot" class="performance-content">
|
||||
<section class="summary-grid">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">{{ t('performance.systemCpu') }}</span>
|
||||
<strong>{{ formatPercent(snapshot.system.cpuPercent) }}</strong>
|
||||
<div class="meter"><span :style="{ width: `${snapshot.system.cpuPercent || 0}%` }" /></div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">{{ t('performance.systemMemory') }}</span>
|
||||
<strong>{{ formatPercent(snapshot.system.memoryPercent) }}</strong>
|
||||
<small>{{ formatBytes(snapshot.system.usedMemoryBytes) }} / {{ formatBytes(snapshot.system.totalMemoryBytes) }}</small>
|
||||
<div class="meter"><span :style="{ width: `${snapshot.system.memoryPercent || 0}%` }" /></div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">{{ t('performance.activeSessions') }}</span>
|
||||
<strong>{{ snapshot.sessions.active }}</strong>
|
||||
<small>{{ t('performance.runningSessions', { count: snapshot.sessions.running }) }}</small>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">{{ t('performance.workers') }}</span>
|
||||
<strong>{{ runningWorkerCount }} / {{ workerCount }}</strong>
|
||||
<small>{{ t('performance.totalWorkerMemory') }} {{ formatBytes(snapshot.bridge.totalWorkerMemoryRssBytes) }}</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="runtime-section">
|
||||
<div class="section-header">
|
||||
<h3>{{ t('performance.processes') }}</h3>
|
||||
<span>{{ snapshot.system.platform }} {{ snapshot.system.arch }} · {{ snapshot.system.cpuCount }} CPU · {{ t('performance.uptime') }} {{ formatDuration(snapshot.system.uptimeSeconds) }}</span>
|
||||
</div>
|
||||
<div class="process-grid">
|
||||
<div class="process-row">
|
||||
<div>
|
||||
<strong>Web UI</strong>
|
||||
<span>PID {{ snapshot.web.pid }}</span>
|
||||
</div>
|
||||
<span>{{ formatPercent(snapshot.web.cpuPercent) }}</span>
|
||||
<span>{{ formatBytes(webRssMemory) }}</span>
|
||||
<span class="status running">{{ statusText(true) }}</span>
|
||||
</div>
|
||||
<div class="process-row">
|
||||
<div>
|
||||
<strong>Bridge Broker</strong>
|
||||
<span>{{ snapshot.bridge.endpoint }}</span>
|
||||
</div>
|
||||
<span>{{ formatPercent(snapshot.bridge.broker.process?.cpuPercent) }}</span>
|
||||
<span>{{ formatBytes(brokerMemory) }}</span>
|
||||
<span class="status" :class="{ running: snapshot.bridge.reachable && snapshot.bridge.broker.running }">
|
||||
{{ snapshot.bridge.reachable && snapshot.bridge.broker.running ? statusText(true) : statusText(false) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="snapshot.bridge.error" class="runtime-error">{{ snapshot.bridge.error }}</div>
|
||||
</section>
|
||||
|
||||
<section class="runtime-section">
|
||||
<div class="section-header">
|
||||
<h3>{{ t('performance.workerMemory') }}</h3>
|
||||
<span>{{ t('performance.lastUpdated') }} {{ new Date(snapshot.timestamp).toLocaleTimeString() }}</span>
|
||||
</div>
|
||||
<div class="worker-table-wrap">
|
||||
<table class="worker-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('performance.profile') }}</th>
|
||||
<th>PID</th>
|
||||
<th>CPU</th>
|
||||
<th>{{ t('performance.memory') }}</th>
|
||||
<th>{{ t('performance.runningActiveSessions') }}</th>
|
||||
<th>{{ t('performance.lastUsed') }}</th>
|
||||
<th>{{ t('performance.status') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="snapshot.bridge.workers.length === 0">
|
||||
<td colspan="7" class="empty-cell">{{ t('performance.noWorkers') }}</td>
|
||||
</tr>
|
||||
<tr v-for="worker in snapshot.bridge.workers" :key="worker.profile || worker.pid">
|
||||
<td>{{ worker.profile || '-' }}</td>
|
||||
<td>{{ worker.pid || '-' }}</td>
|
||||
<td>{{ formatPercent(worker.cpuPercent) }}</td>
|
||||
<td>{{ formatBytes(worker.memoryRssBytes) }}</td>
|
||||
<td>{{ worker.runningSessionCount }} / {{ worker.sessionCount }}</td>
|
||||
<td>{{ formatTime(worker.lastUsedAt) }}</td>
|
||||
<td><span class="status" :class="{ running: worker.running }">{{ statusText(worker.running) }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="runtime-section">
|
||||
<div class="section-header">
|
||||
<h3>{{ t('performance.sessionsByProfile') }}</h3>
|
||||
</div>
|
||||
<div class="session-list">
|
||||
<div v-if="Object.keys(snapshot.sessions.byProfile).length === 0" class="session-empty">
|
||||
{{ t('performance.noActiveSessions') }}
|
||||
</div>
|
||||
<div v-for="(count, profile) in snapshot.sessions.byProfile" :key="profile" class="session-row">
|
||||
<span>{{ profile }}</span>
|
||||
<strong>{{ count }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</NSpin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/variables' as *;
|
||||
|
||||
.performance-view {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 21px 20px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
color: $text-primary;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.performance-spin {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.performance-content {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-item,
|
||||
.runtime-section {
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $radius-sm;
|
||||
background: $bg-card;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
min-height: 108px;
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.summary-label,
|
||||
.summary-item small,
|
||||
.section-header span,
|
||||
.process-row div span {
|
||||
color: $text-muted;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.summary-item strong {
|
||||
color: $text-primary;
|
||||
font-size: 24px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.meter {
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: $bg-secondary;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: $accent-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.runtime-section {
|
||||
margin-top: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
min-height: 46px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid $border-light;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $text-primary;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.process-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.process-row {
|
||||
min-height: 56px;
|
||||
padding: 10px 14px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 80px 110px 86px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid $border-light;
|
||||
color: $text-secondary;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
div {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $text-primary;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 999px;
|
||||
color: $text-muted;
|
||||
font-size: 12px;
|
||||
|
||||
&.running {
|
||||
border-color: rgba(var(--success-rgb), 0.35);
|
||||
color: $success;
|
||||
background: rgba(var(--success-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime-error {
|
||||
padding: 10px 14px;
|
||||
border-top: 1px solid $border-light;
|
||||
color: $error;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.worker-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.worker-table {
|
||||
width: 100%;
|
||||
min-width: 760px;
|
||||
border-collapse: collapse;
|
||||
color: $text-secondary;
|
||||
font-size: 13px;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 11px 14px;
|
||||
border-bottom: 1px solid $border-light;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th {
|
||||
color: $text-muted;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
color: $text-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-cell,
|
||||
.session-empty {
|
||||
color: $text-muted;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
padding: 6px 14px;
|
||||
}
|
||||
|
||||
.session-row {
|
||||
min-height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid $border-light;
|
||||
color: $text-secondary;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.session-empty {
|
||||
padding: 18px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
.page-header,
|
||||
.header-actions,
|
||||
.section-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.process-row {
|
||||
grid-template-columns: 1fr 72px;
|
||||
|
||||
> span:nth-child(3),
|
||||
> span:nth-child(4) {
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user