feat: add runtime diagnostics (#13295)

This commit is contained in:
ssongliu
2026-07-20 13:55:55 +08:00
committed by GitHub
parent 3e9848554b
commit 7f3fd0cb1d
22 changed files with 1366 additions and 14 deletions
+9 -8
View File
@@ -37,14 +37,15 @@ var (
cronjobService = service.NewICronjobService()
fileService = service.NewIFileService()
fileHistoryService = service.NewIFileHistoryService()
fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService()
firewallService = service.NewIFirewallService()
iptablesService = service.NewIIptablesService()
monitorService = service.NewIMonitorService()
systemService = service.NewISystemService()
fileService = service.NewIFileService()
fileHistoryService = service.NewIFileHistoryService()
fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService()
firewallService = service.NewIFirewallService()
iptablesService = service.NewIIptablesService()
monitorService = service.NewIMonitorService()
systemService = service.NewISystemService()
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
deviceService = service.NewIDeviceService()
fail2banService = service.NewIFail2BanService()
+63
View File
@@ -0,0 +1,63 @@
package v2
import (
"os"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags RuntimeDiagnostics
// @Summary Load runtime diagnostics summary
// @Success 200 {object} dto.RuntimeDiagnosticsSummary
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/summary [get]
func (b *BaseApi) LoadRuntimeDiagnosticsSummary(c *gin.Context) {
data, err := runtimeDiagnosticsService.Summary()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Load grouped goroutine snapshot
// @Success 200 {object} dto.RuntimeGoroutineSnapshot
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/goroutines [get]
func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) {
data, err := runtimeDiagnosticsService.Goroutines()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithDataGzipped(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Capture runtime profile
// @Param request body dto.RuntimeProfileCreate true "request"
// @Success 200 {file} file
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/profiles [post]
func (b *BaseApi) CreateRuntimeProfile(c *gin.Context) {
var req dto.RuntimeProfileCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
profile, err := runtimeDiagnosticsService.CreateProfile(req)
if err != nil {
helper.BadRequest(c, err)
return
}
defer os.Remove(profile.Path)
c.Header("Content-Disposition", `attachment; filename="`+profile.Name+`"`)
c.Header("Content-Type", "application/octet-stream")
c.File(profile.Path)
c.Abort()
}
+30
View File
@@ -0,0 +1,30 @@
package dto
import "time"
type RuntimeDiagnosticsSummary struct {
RSS uint64 `json:"rss"`
HeapAlloc uint64 `json:"heapAlloc"`
HeapObjects uint64 `json:"heapObjects"`
Goroutines int `json:"goroutines"`
}
type RuntimeGoroutineGroup struct {
State string `json:"state"`
Top string `json:"top"`
Count int `json:"count"`
Stack []string `json:"stack"`
}
type RuntimeGoroutineSnapshot struct {
Total int `json:"total"`
GroupCount int `json:"groupCount"`
Truncated bool `json:"truncated"`
CapturedAt time.Time `json:"capturedAt"`
Goroutines []RuntimeGoroutineGroup `json:"goroutines"`
}
type RuntimeProfileCreate struct {
Type string `json:"type" validate:"required,oneof=cpu heap goroutine mutex block"`
Duration int `json:"duration" validate:"omitempty,min=5,max=30"`
}
+411
View File
@@ -0,0 +1,411 @@
package service
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
stdpprof "runtime/pprof"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
profile "github.com/google/pprof/profile"
"github.com/shirou/gopsutil/v4/process"
)
const (
diagnosticsDefaultDuration = 15
diagnosticsMaxProfileSize = 64 * 1024 * 1024
diagnosticsMaxSnapshotSize = 16 * 1024 * 1024
diagnosticsDetailedGoroutines = 10_000
diagnosticsBlockProfileRate = 1_000_000
diagnosticsMaxGroups = 200
)
var (
runtimeDiagnosticsInstance = &RuntimeDiagnosticsService{}
goroutineHeaderPattern = regexp.MustCompile(`^goroutine \d+ \[([^]]+)\]:$`)
goroutineArgPattern = regexp.MustCompile(`0x[0-9a-fA-F]+`)
errProfileSizeLimit = errors.New("runtime profile exceeds the 64 MiB size limit")
)
type cappedWriter struct {
writer io.Writer
remaining int64
exceeded bool
}
func (w *cappedWriter) Write(data []byte) (int, error) {
if int64(len(data)) <= w.remaining {
n, err := w.writer.Write(data)
w.remaining -= int64(n)
return n, err
}
w.exceeded = true
if w.remaining <= 0 {
return 0, errProfileSizeLimit
}
allowed := int(w.remaining)
n, err := w.writer.Write(data[:allowed])
w.remaining -= int64(n)
if err != nil {
return n, err
}
return n, errProfileSizeLimit
}
type IRuntimeDiagnosticsService interface {
Summary() (dto.RuntimeDiagnosticsSummary, error)
Goroutines() (dto.RuntimeGoroutineSnapshot, error)
CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error)
}
type RuntimeProfileResult struct {
Path string
Name string
}
type RuntimeDiagnosticsService struct {
captureMu sync.Mutex
processMu sync.Mutex
process *process.Process
}
func NewIRuntimeDiagnosticsService() IRuntimeDiagnosticsService {
return runtimeDiagnosticsInstance
}
func (s *RuntimeDiagnosticsService) Summary() (dto.RuntimeDiagnosticsSummary, error) {
rss, err := s.processRSS()
if err != nil {
return dto.RuntimeDiagnosticsSummary{}, err
}
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
return dto.RuntimeDiagnosticsSummary{
RSS: rss,
HeapAlloc: stats.HeapAlloc,
HeapObjects: stats.HeapObjects,
Goroutines: runtime.NumGoroutine(),
}, nil
}
func (s *RuntimeDiagnosticsService) Goroutines() (dto.RuntimeGoroutineSnapshot, error) {
total := runtime.NumGoroutine()
if total > diagnosticsDetailedGoroutines {
groups, truncated := compactGoroutineSnapshot(total)
return dto.RuntimeGoroutineSnapshot{
Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups,
}, nil
}
var data bytes.Buffer
writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxSnapshotSize}
if err := stdpprof.Lookup("goroutine").WriteTo(writer, 2); err != nil {
groups, truncated := compactGoroutineSnapshot(total)
return dto.RuntimeGoroutineSnapshot{
Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups,
}, nil
}
groups, truncated := parseGoroutineDump(&data, diagnosticsMaxGroups)
result := dto.RuntimeGoroutineSnapshot{
Total: total,
GroupCount: len(groups),
Truncated: truncated,
CapturedAt: time.Now(),
}
result.Goroutines = groups
return result, nil
}
func (s *RuntimeDiagnosticsService) CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error) {
if !s.captureMu.TryLock() {
return RuntimeProfileResult{}, errors.New("another runtime profile is being captured")
}
defer s.captureMu.Unlock()
return captureRuntimeProfile(req)
}
func captureRuntimeProfile(req dto.RuntimeProfileCreate) (result RuntimeProfileResult, err error) {
duration := req.Duration
if duration == 0 {
duration = diagnosticsDefaultDuration
}
if duration < 5 || duration > 30 {
return result, errors.New("profile duration must be between 5 and 30 seconds")
}
if req.Type == "heap" || req.Type == "goroutine" {
duration = 0
}
name := fmt.Sprintf("%s-%s-%ds.pb.gz", req.Type, newRuntimeEventID(), duration)
file, err := os.CreateTemp("", "1panel-runtime-profile-*.tmp")
if err != nil {
return result, err
}
writer := &cappedWriter{writer: file, remaining: diagnosticsMaxProfileSize}
removeOnError := true
defer func() {
_ = file.Close()
if removeOnError {
_ = os.Remove(file.Name())
}
}()
switch req.Type {
case "cpu":
if err = stdpprof.StartCPUProfile(writer); err != nil {
return result, err
}
time.Sleep(time.Duration(duration) * time.Second)
stdpprof.StopCPUProfile()
case "heap":
err = stdpprof.Lookup("heap").WriteTo(writer, 0)
case "goroutine":
err = stdpprof.Lookup("goroutine").WriteTo(writer, 0)
case "mutex":
err = captureWindowedRuntimeProfile("mutex", duration, writer, func() func() {
previous := runtime.SetMutexProfileFraction(5)
return func() { runtime.SetMutexProfileFraction(previous) }
})
case "block":
err = captureWindowedRuntimeProfile("block", duration, writer, func() func() {
runtime.SetBlockProfileRate(diagnosticsBlockProfileRate)
return func() { runtime.SetBlockProfileRate(0) }
})
default:
return result, errors.New("unsupported runtime profile type")
}
if err != nil {
return result, err
}
if writer.exceeded {
return result, errProfileSizeLimit
}
if err = file.Close(); err != nil {
return result, err
}
removeOnError = false
return RuntimeProfileResult{Path: file.Name(), Name: name}, nil
}
func captureWindowedRuntimeProfile(name string, duration int, writer io.Writer, enable func() func()) error {
restore := enable()
sampling := true
defer func() {
if sampling {
restore()
}
}()
before, err := readRuntimeProfile(name)
if err != nil {
return err
}
startedAt := time.Now()
time.Sleep(time.Duration(duration) * time.Second)
restore()
sampling = false
after, err := readRuntimeProfile(name)
if err != nil {
return err
}
delta, err := diffRuntimeProfiles(before, after, startedAt, time.Duration(duration)*time.Second)
if err != nil {
return err
}
return delta.Write(writer)
}
func readRuntimeProfile(name string) (*profile.Profile, error) {
var data bytes.Buffer
writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxProfileSize}
if err := stdpprof.Lookup(name).WriteTo(writer, 0); err != nil {
return nil, err
}
if writer.exceeded {
return nil, errProfileSizeLimit
}
return profile.Parse(&data)
}
func diffRuntimeProfiles(before, after *profile.Profile, startedAt time.Time, duration time.Duration) (*profile.Profile, error) {
baseline := before.Copy()
baseline.Scale(-1)
delta, err := profile.Merge([]*profile.Profile{after, baseline})
if err != nil {
return nil, err
}
delta.TimeNanos = startedAt.UnixNano()
delta.DurationNanos = duration.Nanoseconds()
return delta, nil
}
func (s *RuntimeDiagnosticsService) processRSS() (uint64, error) {
s.processMu.Lock()
defer s.processMu.Unlock()
if s.process == nil {
proc, err := process.NewProcess(int32(os.Getpid()))
if err != nil {
return 0, err
}
s.process = proc
}
memoryInfo, err := s.process.MemoryInfo()
if err != nil {
return 0, err
}
if memoryInfo == nil {
return 0, errors.New("process memory information is unavailable")
}
return memoryInfo.RSS, nil
}
func newRuntimeEventID() string {
return time.Now().Format("20060102-150405.000000000")
}
func parseGoroutineDump(reader io.Reader, maxGroups int) ([]dto.RuntimeGoroutineGroup, bool) {
type groupValue struct {
state string
top string
stack []string
count int
}
groups := make(map[string]*groupValue)
truncated := false
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
var block []string
flush := func() {
if len(block) == 0 {
return
}
matches := goroutineHeaderPattern.FindStringSubmatch(block[0])
state := "unknown"
if len(matches) == 2 {
state = matches[1]
}
stack := append([]string(nil), block[1:]...)
if len(stack) > 40 {
stack = stack[:40]
}
top := "runtime"
functionLines := make([]string, 0, len(stack)/2)
for _, line := range stack {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "created by ") {
continue
}
normalized := goroutineArgPattern.ReplaceAllString(trimmed, "0x…")
functionLines = append(functionLines, normalized)
if top == "runtime" {
top = strings.Split(normalized, "(")[0]
}
}
signature := state + "\n" + strings.Join(functionLines, "\n")
if existing, ok := groups[signature]; ok {
existing.count++
return
}
if len(groups) >= maxGroups {
truncated = true
return
}
groups[signature] = &groupValue{state: state, top: top, stack: stack, count: 1}
}
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "]:") {
flush()
block = []string{line}
continue
}
if len(block) > 0 {
block = append(block, line)
}
}
if scanner.Err() != nil {
truncated = true
}
flush()
result := make([]dto.RuntimeGoroutineGroup, 0, len(groups))
for _, group := range groups {
result = append(result, dto.RuntimeGoroutineGroup{State: group.state, Top: group.top, Count: group.count, Stack: group.stack})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Top < result[j].Top
}
return result[i].Count > result[j].Count
})
return result, truncated
}
func compactGoroutineSnapshot(initialSize int) ([]dto.RuntimeGoroutineGroup, bool) {
records := make([]runtime.StackRecord, initialSize+32)
count, ok := runtime.GoroutineProfile(records)
if !ok {
records = make([]runtime.StackRecord, count+32)
count, ok = runtime.GoroutineProfile(records)
}
truncated := !ok
if count > len(records) {
count = len(records)
truncated = true
}
records = records[:count]
type compactGroup struct {
top string
stack []string
count int
}
groups := make(map[string]*compactGroup)
for _, record := range records {
frames := runtime.CallersFrames(record.Stack())
stack := make([]string, 0, 16)
functions := make([]string, 0, 8)
top := "runtime"
for {
frame, more := frames.Next()
if frame.Function != "" {
if top == "runtime" {
top = frame.Function
}
functions = append(functions, frame.Function)
stack = append(stack, frame.Function, fmt.Sprintf("\t%s:%d", frame.File, frame.Line))
}
if !more || len(functions) >= 20 {
break
}
}
signature := strings.Join(functions, "\n")
if existing, exists := groups[signature]; exists {
existing.count++
continue
}
if len(groups) >= diagnosticsMaxGroups {
truncated = true
continue
}
groups[signature] = &compactGroup{top: top, stack: stack, count: 1}
}
result := make([]dto.RuntimeGoroutineGroup, 0, len(groups))
for _, group := range groups {
result = append(result, dto.RuntimeGoroutineGroup{State: "profiled", Top: group.top, Count: group.count, Stack: group.stack})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Top < result[j].Top
}
return result[i].Count > result[j].Count
})
return result, truncated
}
+1 -1
View File
@@ -20,6 +20,7 @@ require (
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-resty/resty/v2 v2.17.2
github.com/go-sql-driver/mysql v1.10.0
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/jackc/pgx/v5 v5.10.0
@@ -130,7 +131,6 @@ require (
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.198 // indirect
+1
View File
@@ -24,5 +24,6 @@ func commonGroups() []CommonRouter {
&AIToolsRouter{},
&GroupRouter{},
&AlertRouter{},
&RuntimeDiagnosticsRouter{},
}
}
+18
View File
@@ -0,0 +1,18 @@
package router
import (
v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2"
"github.com/gin-gonic/gin"
)
type RuntimeDiagnosticsRouter struct{}
func (s *RuntimeDiagnosticsRouter) InitRouter(Router *gin.RouterGroup) {
diagnosticsRouter := Router.Group("hosts/diagnostics")
baseApi := v2.ApiGroupApp.BaseApi
{
diagnosticsRouter.GET("/summary", baseApi.LoadRuntimeDiagnosticsSummary)
diagnosticsRouter.GET("/goroutines", baseApi.LoadRuntimeGoroutines)
diagnosticsRouter.POST("/profiles", baseApi.CreateRuntimeProfile)
}
}
+23
View File
@@ -160,6 +160,29 @@ export namespace Host {
endTime: Date;
}
export interface RuntimeDiagnosticsSummary {
rss: number;
heapAlloc: number;
heapObjects: number;
goroutines: number;
}
export interface RuntimeGoroutineGroup {
state: string;
top: string;
count: number;
stack: string[];
}
export interface RuntimeGoroutineSnapshot {
total: number;
groupCount: number;
truncated: boolean;
capturedAt: string;
goroutines: RuntimeGoroutineGroup[];
}
export interface RuntimeProfileCreate {
type: 'cpu' | 'heap' | 'goroutine' | 'mutex' | 'block';
duration: number;
}
export interface SSHInfo {
autoStart: boolean;
isActive: boolean;
+52 -1
View File
@@ -97,7 +97,58 @@ export const loadMonitorSetting = (currentNode?: string) => {
export const updateMonitorSetting = (key: string, value: string) => {
return http.post(`/hosts/monitor/setting/update`, { key: key, value: value });
};
export const loadRuntimeDiagnosticsSummary = (currentNode?: string) => {
return http.get<Host.RuntimeDiagnosticsSummary>(
`/hosts/diagnostics/summary`,
{},
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
};
export const loadRuntimeGoroutines = (currentNode?: string) => {
return http.get<Host.RuntimeGoroutineSnapshot>(
`/hosts/diagnostics/goroutines`,
{},
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
};
export class RuntimeProfileDownloadError extends Error {
constructor(message = '') {
super(message);
this.name = 'RuntimeProfileDownloadError';
}
}
const parseRuntimeProfileError = async (data: unknown) => {
if (!(data instanceof Blob) || !data.type.includes('application/json')) {
return;
}
try {
const response = JSON.parse(await data.text()) as { message?: string };
return new RuntimeProfileDownloadError(response.message);
} catch {
return new RuntimeProfileDownloadError();
}
};
export const createRuntimeProfile = async (params: Host.RuntimeProfileCreate, currentNode?: string) => {
try {
const data = await http.download<Blob>(`/hosts/diagnostics/profiles`, params, {
responseType: 'blob',
timeout: TimeoutEnum.T_60S,
headers: currentNode ? { CurrentNode: currentNode } : undefined,
});
const profileError = await parseRuntimeProfileError(data);
if (profileError) {
throw profileError;
}
return data;
} catch (error) {
if (error instanceof RuntimeProfileDownloadError) {
throw error;
}
const responseData = (error as { response?: { data?: unknown } })?.response?.data;
const profileError = await parseRuntimeProfileError(responseData);
throw profileError || error;
}
};
// ssh
export const getSSHInfo = (currentNode?: string) => {
return http.post<Host.SSHInfo>(
+35
View File
@@ -1905,6 +1905,41 @@ const message = {
down: 'Down',
interval: 'Collection Interval',
intervalHelper: 'Enter an appropriate monitoring collection interval (10 seconds - 12 hours)',
runtimeDiagnostics: 'Runtime Diagnostics',
runtimeRSS: 'Process Memory (RSS)',
runtimeHeapAlloc: 'Heap Memory',
runtimeHeapObjects: 'Heap Objects',
runtimeGoroutines: 'Goroutines',
goroutineSnapshot: 'Goroutine Snapshot',
goroutineSummary: '{0} goroutines grouped into {1} call stacks',
goroutineTruncated: 'The snapshot is too large. Only part of the call stacks are shown.',
captureSnapshot: 'Refresh Snapshot',
count: 'Count',
topFunction: 'Top Function',
manualCapture: 'Manual Capture',
startCapture: 'Start Capture',
capturing: 'Capturing',
captureSuccess: 'Diagnostic file generated; download started',
profileCPU: 'CPU',
profileHeap: 'Heap',
profileGoroutine: 'Goroutine',
profileMutex: 'Mutex',
profileBlock: 'Block',
runtimeRSSDesc:
'Physical memory currently occupied by the process, including the Go heap, thread stacks, and native memory.',
runtimeHeapAllocDesc:
'Memory allocated on the Go heap that has not been released. Sustained growth may indicate retained objects.',
runtimeHeapObjectsDesc:
'Number of live objects on the Go heap. Sustained growth may indicate retained objects.',
runtimeGoroutineDesc:
'Current lightweight concurrent tasks. Sustained growth usually warrants checking for goroutine leaks.',
profileCPUDesc:
'Samples CPU call stacks for the selected duration to locate functions that actually consume CPU.',
profileHeapDesc:
'Records Go heap objects still in use and their allocation stacks to investigate sustained memory growth.',
profileGoroutineDesc: 'Saves the state and call stack of every goroutine and groups identical call paths.',
profileMutexDesc: 'Measures wait time caused by mutex contention to locate lock contention hotspots.',
profileBlockDesc: 'Measures blocking on channels, select statements, and synchronization primitives.',
},
terminal: {
local: 'Local',
+36
View File
@@ -1946,6 +1946,42 @@ const message = {
down: 'Bajada',
interval: 'Intervalo de Recolección',
intervalHelper: 'Ingrese un intervalo de recolección de monitoreo apropiado (10 segundos - 12 horas)',
runtimeDiagnostics: 'Diagnóstico en tiempo de ejecución',
runtimeRSS: 'Memoria del proceso (RSS)',
runtimeHeapAlloc: 'Memoria del heap',
runtimeHeapObjects: 'Objetos del heap',
runtimeGoroutines: 'Goroutines',
goroutineSnapshot: 'Instantánea de Goroutines',
goroutineSummary: '{0} goroutines agrupadas en {1} pilas de llamadas',
goroutineTruncated: 'La instantánea es demasiado grande. Solo se muestran algunas pilas de llamadas.',
captureSnapshot: 'Actualizar instantánea',
count: 'Cantidad',
topFunction: 'Función superior',
manualCapture: 'Captura manual',
startCapture: 'Iniciar captura',
capturing: 'Capturando',
captureSuccess: 'Archivo de diagnóstico generado; descarga iniciada',
profileCPU: 'CPU',
profileHeap: 'Heap',
profileGoroutine: 'Goroutine',
profileMutex: 'Mutex',
profileBlock: 'Bloqueo',
runtimeRSSDesc:
'Memoria física ocupada por el proceso, incluido el heap de Go, las pilas de hilos y la memoria nativa.',
runtimeHeapAllocDesc:
'Memoria asignada en el heap de Go que aún no se ha liberado. Un crecimiento continuo puede indicar objetos retenidos.',
runtimeHeapObjectsDesc:
'Número de objetos vivos en el heap de Go. Un crecimiento continuo puede indicar objetos retenidos.',
runtimeGoroutineDesc:
'Tareas concurrentes ligeras actuales. Un crecimiento continuo puede indicar una fuga de goroutines.',
profileCPUDesc:
'Muestrea las pilas de CPU durante el tiempo seleccionado para localizar las funciones que consumen CPU.',
profileHeapDesc:
'Registra los objetos del heap aún en uso y sus pilas de asignación para investigar el crecimiento de memoria.',
profileGoroutineDesc: 'Guarda el estado y la pila de cada goroutine y agrupa las rutas de llamada idénticas.',
profileMutexDesc:
'Mide el tiempo de espera causado por la contención de mutex para localizar bloqueos disputados.',
profileBlockDesc: 'Mide los bloqueos en canales, select y primitivas de sincronización.',
},
terminal: {
local: 'Local',
+30
View File
@@ -1895,6 +1895,36 @@ const message = {
down: 'پایین',
interval: 'فاصله جمع‌آوری',
intervalHelper: 'فاصله جمع‌آوری نظارت مناسب را وارد کنید (۱۰ ثانیه - ۱۲ ساعت)',
runtimeDiagnostics: 'تشخیص زمان اجرا',
runtimeRSS: 'حافظه پردازش (RSS)',
runtimeHeapAlloc: 'حافظه Heap',
runtimeHeapObjects: 'اشیای Heap',
runtimeGoroutines: 'Goroutineها',
goroutineSnapshot: 'تصویر لحظه‌ای Goroutine',
goroutineSummary: '{0} goroutine در {1} پشته فراخوانی گروه‌بندی شده‌اند',
goroutineTruncated: 'تصویر لحظه‌ای بسیار بزرگ است. فقط بخشی از پشته‌های فراخوانی نمایش داده می‌شود.',
captureSnapshot: 'تازه‌سازی تصویر',
count: 'تعداد',
topFunction: 'تابع بالایی',
manualCapture: 'ثبت دستی',
startCapture: 'شروع ثبت',
capturing: 'در حال ثبت',
captureSuccess: 'فایل تشخیصی ایجاد شد؛ دانلود آغاز شد',
profileCPU: 'CPU',
profileHeap: 'Heap',
profileGoroutine: 'Goroutine',
profileMutex: 'Mutex',
profileBlock: 'انسداد',
runtimeRSSDesc: 'حافظه فیزیکی اشغال‌شده توسط پردازش، شامل heap زبان Go، پشته رشته‌ها و حافظه بومی.',
runtimeHeapAllocDesc:
'حافظه تخصیص‌یافته در heap زبان Go که هنوز آزاد نشده است. رشد مداوم می‌تواند نشانه باقی‌ماندن اشیا باشد.',
runtimeHeapObjectsDesc: 'تعداد اشیای زنده در heap زبان Go. رشد مداوم می‌تواند نشانه باقی‌ماندن اشیا باشد.',
runtimeGoroutineDesc: 'تعداد وظایف هم‌زمان سبک فعلی. رشد مداوم می‌تواند نشانه نشت Goroutine باشد.',
profileCPUDesc: 'پشته‌های فراخوانی CPU را در مدت انتخاب‌شده نمونه‌برداری می‌کند تا توابع پرمصرف مشخص شوند.',
profileHeapDesc: 'اشیای heap در حال استفاده و پشته تخصیص آن‌ها را برای بررسی رشد حافظه ثبت می‌کند.',
profileGoroutineDesc: 'وضعیت و پشته فراخوانی هر Goroutine را ذخیره و مسیرهای یکسان را گروه‌بندی می‌کند.',
profileMutexDesc: 'زمان انتظار ناشی از رقابت mutex را برای یافتن نقاط رقابت قفل اندازه‌گیری می‌کند.',
profileBlockDesc: 'زمان انسداد روی Channel، Select و ابزارهای همگام‌سازی را اندازه‌گیری می‌کند.',
},
terminal: {
local: 'محلی',
+31
View File
@@ -1911,6 +1911,37 @@ const message = {
down: '下',
interval: '収集間隔',
intervalHelper: '適切な監視収集間隔を入力してください10 - 12時間',
runtimeDiagnostics: 'ランタイム診断',
runtimeRSS: 'プロセスメモリ (RSS)',
runtimeHeapAlloc: 'ヒープメモリ',
runtimeHeapObjects: 'ヒープオブジェクト',
runtimeGoroutines: 'Goroutine ',
goroutineSnapshot: 'Goroutine スナップショット',
goroutineSummary: '{0} 個の Goroutine {1} 個の呼び出しスタックに集約',
goroutineTruncated: 'スナップショットが大きすぎるため一部の呼び出しスタックのみ表示しています',
captureSnapshot: 'スナップショットを更新',
count: '数',
topFunction: 'トップ関数',
manualCapture: '手動取得',
startCapture: '取得開始',
capturing: '取得中',
captureSuccess: '診断ファイルを生成しダウンロードを開始しました',
profileCPU: 'CPU',
profileHeap: 'ヒープ',
profileGoroutine: 'Goroutine',
profileMutex: 'ミューテックス',
profileBlock: 'ブロック',
runtimeRSSDesc: 'Go ヒープスレッドスタックネイティブメモリを含むプロセスが使用中の物理メモリです',
runtimeHeapAllocDesc:
'Go ヒープに割り当てられまだ解放されていないメモリです継続的な増加はオブジェクトの滞留を示す場合があります',
runtimeHeapObjectsDesc:
'Go ヒープ上の生存オブジェクト数です継続的な増加はオブジェクトの滞留を示す場合があります',
runtimeGoroutineDesc: '現在の軽量な並行タスク数です継続的な増加は Goroutine リークの確認が必要です',
profileCPUDesc: '選択した期間の CPU 呼び出しスタックを取得しCPU を消費する関数を特定します',
profileHeapDesc: '使用中の Go ヒープオブジェクトと割り当てスタックを記録しメモリ増加を調査します',
profileGoroutineDesc: 'すべての Goroutine の状態と呼び出しスタックを保存し同じ呼び出し経路を集約します',
profileMutexDesc: 'Mutex 競合による待機時間を測定しロック競合のホットスポットを特定します',
profileBlockDesc: 'ChannelSelect同期プリミティブでのブロック待機時間を測定します',
},
terminal: {
local: 'ローカル',
+31
View File
@@ -1879,6 +1879,37 @@ const message = {
down: '다운',
interval: '수집 간격',
intervalHelper: '적절한 모니터링 수집 간격을 입력하세요 (10 - 12시간)',
runtimeDiagnostics: '런타임 진단',
runtimeRSS: '프로세스 메모리 (RSS)',
runtimeHeapAlloc: ' 메모리',
runtimeHeapObjects: ' 객체',
runtimeGoroutines: 'Goroutine ',
goroutineSnapshot: 'Goroutine 스냅샷',
goroutineSummary: 'Goroutine {0}개를 호출 스택 {1}개로 그룹화',
goroutineTruncated: '스냅샷이 너무 커서 일부 호출 스택만 표시됩니다.',
captureSnapshot: '스냅샷 새로고침',
count: '수량',
topFunction: '최상위 함수',
manualCapture: '수동 캡처',
startCapture: '캡처 시작',
capturing: '캡처 ',
captureSuccess: '진단 파일이 생성되어 다운로드를 시작했습니다',
profileCPU: 'CPU',
profileHeap: '힙',
profileGoroutine: 'Goroutine',
profileMutex: '뮤텍스',
profileBlock: '블록',
runtimeRSSDesc: 'Go , 스레드 스택 네이티브 메모리를 포함해 프로세스가 사용하는 실제 메모리입니다.',
runtimeHeapAllocDesc:
'Go 힙에 할당되어 아직 해제되지 않은 메모리입니다. 지속적인 증가는 객체가 유지되고 있음을 나타낼 있습니다.',
runtimeHeapObjectsDesc:
'Go 힙의 현재 활성 객체 수입니다. 지속적인 증가는 객체가 유지되고 있음을 나타낼 있습니다.',
runtimeGoroutineDesc: '현재 경량 동시 작업 수입니다. 지속적으로 증가하면 Goroutine 누수를 확인해야 합니다.',
profileCPUDesc: '선택한 시간 동안 CPU 호출 스택을 샘플링하여 CPU를 소비하는 함수를 찾습니다.',
profileHeapDesc: '사용 중인 Go 객체와 할당 스택을 기록하여 지속적인 메모리 증가를 조사합니다.',
profileGoroutineDesc: '모든 Goroutine의 상태와 호출 스택을 저장하고 동일한 호출 경로를 그룹화합니다.',
profileMutexDesc: 'Mutex 경합으로 인한 대기 시간을 측정하여 잠금 경합 지점을 찾습니다.',
profileBlockDesc: 'Channel, Select 동기화 프리미티브의 차단 대기 시간을 측정합니다.',
},
terminal: {
local: '로컬',
+35
View File
@@ -1929,6 +1929,41 @@ const message = {
down: 'Turun',
interval: 'Selang Kumpulan',
intervalHelper: 'Sila masukkan selang kumpulan pemantauan yang sesuai (10 saat - 12 jam)',
runtimeDiagnostics: 'Diagnostik Masa Jalan',
runtimeRSS: 'Memori Proses (RSS)',
runtimeHeapAlloc: 'Memori Heap',
runtimeHeapObjects: 'Objek Heap',
runtimeGoroutines: 'Goroutine',
goroutineSnapshot: 'Petikan Goroutine',
goroutineSummary: '{0} goroutine dikumpulkan kepada {1} tindanan panggilan',
goroutineTruncated: 'Petikan terlalu besar. Hanya sebahagian tindanan panggilan dipaparkan.',
captureSnapshot: 'Segarkan Petikan',
count: 'Bilangan',
topFunction: 'Fungsi Teratas',
manualCapture: 'Tangkapan Manual',
startCapture: 'Mula Tangkap',
capturing: 'Sedang Menangkap',
captureSuccess: 'Fail diagnostik dijana; muat turun dimulakan',
profileCPU: 'CPU',
profileHeap: 'Heap',
profileGoroutine: 'Goroutine',
profileMutex: 'Mutex',
profileBlock: 'Sekatan',
runtimeRSSDesc: 'Memori fizikal yang digunakan proses, termasuk heap Go, tindanan thread dan memori natif.',
runtimeHeapAllocDesc:
'Memori yang diperuntukkan pada heap Go dan belum dilepaskan. Pertumbuhan berterusan mungkin menunjukkan objek tertahan.',
runtimeHeapObjectsDesc:
'Bilangan objek hidup dalam heap Go. Pertumbuhan berterusan mungkin menunjukkan objek tertahan.',
runtimeGoroutineDesc:
'Bilangan tugas serentak ringan semasa. Pertumbuhan berterusan mungkin menunjukkan kebocoran Goroutine.',
profileCPUDesc:
'Mengambil sampel tindanan panggilan CPU untuk tempoh dipilih bagi mencari fungsi yang menggunakan CPU.',
profileHeapDesc:
'Merekod objek heap Go yang masih digunakan dan tindanan peruntukannya untuk menyiasat pertumbuhan memori.',
profileGoroutineDesc:
'Menyimpan keadaan dan tindanan panggilan setiap Goroutine serta mengumpulkan laluan yang sama.',
profileMutexDesc: 'Mengukur masa menunggu akibat persaingan mutex untuk mencari titik persaingan kunci.',
profileBlockDesc: 'Mengukur sekatan pada Channel, Select dan primitif penyegerakan.',
},
terminal: {
local: 'Tempatan',
+35
View File
@@ -1937,6 +1937,41 @@ const message = {
down: 'Para baixo',
interval: 'Intervalo de Coleta',
intervalHelper: 'Insira um intervalo de coleta de monitoramento apropriado (10 segundos - 12 horas)',
runtimeDiagnostics: 'Diagnóstico de execução',
runtimeRSS: 'Memória do processo (RSS)',
runtimeHeapAlloc: 'Memória do heap',
runtimeHeapObjects: 'Objetos do heap',
runtimeGoroutines: 'Goroutines',
goroutineSnapshot: 'Instantâneo de Goroutines',
goroutineSummary: '{0} goroutines agrupadas em {1} pilhas de chamadas',
goroutineTruncated: 'O instantâneo é muito grande. Apenas parte das pilhas de chamadas é exibida.',
captureSnapshot: 'Atualizar instantâneo',
count: 'Quantidade',
topFunction: 'Função principal',
manualCapture: 'Captura manual',
startCapture: 'Iniciar captura',
capturing: 'Capturando',
captureSuccess: 'Arquivo de diagnóstico gerado; download iniciado',
profileCPU: 'CPU',
profileHeap: 'Heap',
profileGoroutine: 'Goroutine',
profileMutex: 'Mutex',
profileBlock: 'Bloqueio',
runtimeRSSDesc:
'Memória física ocupada pelo processo, incluindo heap do Go, pilhas de threads e memória nativa.',
runtimeHeapAllocDesc:
'Memória alocada no heap do Go que ainda não foi liberada. Crescimento contínuo pode indicar objetos retidos.',
runtimeHeapObjectsDesc:
'Número de objetos vivos no heap do Go. Crescimento contínuo pode indicar objetos retidos.',
runtimeGoroutineDesc:
'Tarefas concorrentes leves atuais. Crescimento contínuo geralmente indica possível vazamento de goroutines.',
profileCPUDesc:
'Amostra pilhas de CPU pelo período selecionado para localizar funções que realmente consomem CPU.',
profileHeapDesc:
'Registra objetos do heap ainda em uso e suas pilhas de alocação para investigar crescimento de memória.',
profileGoroutineDesc: 'Salva o estado e a pilha de cada goroutine e agrupa caminhos de chamada idênticos.',
profileMutexDesc: 'Mede o tempo de espera causado por contenção de mutex para localizar pontos de disputa.',
profileBlockDesc: 'Mede bloqueios em canais, select e primitivas de sincronização.',
},
terminal: {
local: 'Local',
+32
View File
@@ -1920,6 +1920,38 @@ const message = {
down: 'Входящий',
interval: 'Интервал Сбора',
intervalHelper: 'Пожалуйста, введите подходящий интервал сбора мониторинга (10 секунд - 12 часов)',
runtimeDiagnostics: 'Диагностика среды выполнения',
runtimeRSS: 'Память процесса (RSS)',
runtimeHeapAlloc: 'Память кучи',
runtimeHeapObjects: 'Объекты кучи',
runtimeGoroutines: 'Goroutine',
goroutineSnapshot: 'Снимок Goroutine',
goroutineSummary: '{0} goroutine сгруппированы в {1} стеков вызовов',
goroutineTruncated: 'Снимок слишком большой. Показана только часть стеков вызовов.',
captureSnapshot: 'Обновить снимок',
count: 'Количество',
topFunction: 'Верхняя функция',
manualCapture: 'Ручной сбор',
startCapture: 'Начать сбор',
capturing: 'Идёт сбор',
captureSuccess: 'Диагностический файл создан; скачивание началось',
profileCPU: 'CPU',
profileHeap: 'Куча',
profileGoroutine: 'Goroutine',
profileMutex: 'Mutex',
profileBlock: 'Блокировки',
runtimeRSSDesc: 'Физическая память процесса, включая кучу Go, стеки потоков и нативную память.',
runtimeHeapAllocDesc:
'Выделенная, но ещё не освобождённая память кучи Go. Постоянный рост может указывать на удерживаемые объекты.',
runtimeHeapObjectsDesc:
'Количество живых объектов в куче Go. Постоянный рост может указывать на удерживаемые объекты.',
runtimeGoroutineDesc:
'Текущее число лёгких параллельных задач. Постоянный рост может указывать на утечку goroutine.',
profileCPUDesc: 'Собирает стеки CPU за выбранное время для поиска функций, реально потребляющих процессор.',
profileHeapDesc: 'Записывает используемые объекты кучи и стеки их выделения для анализа роста памяти.',
profileGoroutineDesc: 'Сохраняет состояние и стек каждой goroutine и группирует одинаковые пути вызовов.',
profileMutexDesc: 'Измеряет ожидание из-за конкуренции mutex для поиска горячих блокировок.',
profileBlockDesc: 'Измеряет блокировки на каналах, select и примитивах синхронизации.',
},
terminal: {
local: 'Локальный',
+30
View File
@@ -1927,6 +1927,36 @@ const message = {
down: 'Aşağı',
interval: 'Toplama Aralığı',
intervalHelper: 'Lütfen uygun bir izleme toplama aralığı girin (10 saniye - 12 saat)',
runtimeDiagnostics: 'Çalışma Zamanı Tanılama',
runtimeRSS: 'İşlem Belleği (RSS)',
runtimeHeapAlloc: 'Heap Belleği',
runtimeHeapObjects: 'Heap Nesneleri',
runtimeGoroutines: 'Goroutine',
goroutineSnapshot: 'Goroutine Anlık Görüntüsü',
goroutineSummary: '{0} goroutine, {1} çağrı yığını altında gruplandı',
goroutineTruncated: 'Anlık görüntü çok büyük. Çağrı yığınlarının yalnızca bir kısmı gösteriliyor.',
captureSnapshot: 'Anlık Görüntüyü Yenile',
count: 'Sayı',
topFunction: 'Üst İşlev',
manualCapture: 'Manuel Yakalama',
startCapture: 'Yakalamayı Başlat',
capturing: 'Yakalanıyor',
captureSuccess: 'Tanılama dosyası oluşturuldu; indirme başladı',
profileCPU: 'CPU',
profileHeap: 'Heap',
profileGoroutine: 'Goroutine',
profileMutex: 'Mutex',
profileBlock: 'Engelleme',
runtimeRSSDesc: 'Go heap, parçacığı yığınları ve yerel bellek dahil işlemin kullandığı fiziksel bellek.',
runtimeHeapAllocDesc:
'Go heap üzerinde ayrılmış ve henüz serbest bırakılmamış bellek. Sürekli artış tutulan nesneleri gösterebilir.',
runtimeHeapObjectsDesc: 'Go heap üzerindeki canlı nesne sayısı. Sürekli artış tutulan nesneleri gösterebilir.',
runtimeGoroutineDesc: 'Geçerli hafif eşzamanlı görevler. Sürekli artış goroutine sızıntısını gösterebilir.',
profileCPUDesc: 'CPU tüketen işlevleri bulmak için seçilen süre boyunca CPU çağrı yığınlarını örnekler.',
profileHeapDesc: 'Bellek artışını incelemek için kullanılan heap nesnelerini ve ayırma yığınlarını kaydeder.',
profileGoroutineDesc: 'Her goroutine durumunu ve çağrı yığınını kaydeder, aynı çağrı yollarını gruplar.',
profileMutexDesc: 'Kilit çekişmesi noktalarını bulmak için mutex bekleme süresini ölçer.',
profileBlockDesc: 'Channel, select ve eşzamanlama araçlarındaki engelleme süresini ölçer.',
},
terminal: {
local: 'Yerel',
+29
View File
@@ -1793,6 +1793,35 @@ const message = {
down: '下行',
interval: '採集間隔',
intervalHelper: '請輸入合適的監控採集時間間隔10 - 12小時',
runtimeDiagnostics: '執行階段診斷',
runtimeRSS: '程序記憶體 (RSS)',
runtimeHeapAlloc: '堆積記憶體',
runtimeHeapObjects: '堆積物件',
runtimeGoroutines: 'Goroutine 數量',
goroutineSnapshot: 'Goroutine 快照',
goroutineSummary: ' {0} Goroutine彙總為 {1} 組呼叫堆疊',
goroutineTruncated: '快照內容過大目前僅顯示部分呼叫堆疊',
captureSnapshot: '重新整理快照',
count: '數量',
topFunction: '頂層函式',
manualCapture: '手動擷取',
startCapture: '開始擷取',
capturing: '正在擷取',
captureSuccess: '診斷檔案已產生已開始下載',
profileCPU: 'CPU',
profileHeap: '堆積記憶體',
profileGoroutine: 'Goroutine',
profileMutex: '互斥鎖',
profileBlock: '阻塞',
runtimeRSSDesc: '程序目前佔用的實體記憶體包括 Go 堆積執行緒堆疊及原生記憶體',
runtimeHeapAllocDesc: 'Go 堆積中已配置且尚未釋放的記憶體持續增長可能表示物件未釋放',
runtimeHeapObjectsDesc: 'Go 堆積中目前存活的物件數量持續增長可能表示物件未釋放',
runtimeGoroutineDesc: '目前輕量級並行工作數量持續增長通常需要檢查 Goroutine 洩漏',
profileCPUDesc: '在指定時間內取樣 CPU 呼叫堆疊用於定位實際消耗 CPU 的熱點函式',
profileHeapDesc: '記錄目前仍在使用的 Go 堆積物件及其配置呼叫堆疊用於排查記憶體持續增長',
profileGoroutineDesc: '儲存所有 Goroutine 的狀態和呼叫堆疊並依相同呼叫鏈彙總',
profileMutexDesc: '統計互斥鎖競爭造成的等待時間用於定位鎖競爭熱點',
profileBlockDesc: '統計 ChannelSelect 和同步原語的阻塞等待用於定位長時間阻塞',
},
terminal: {
local: '本機',
+29
View File
@@ -1803,6 +1803,35 @@ const message = {
down: '下行',
interval: '采集间隔',
intervalHelper: '请输入合适的监控采集时间间隔10 - 12小时',
runtimeDiagnostics: '运行时诊断',
runtimeRSS: '进程内存 (RSS)',
runtimeHeapAlloc: '堆内存',
runtimeHeapObjects: '堆对象',
runtimeGoroutines: 'Goroutine 数量',
goroutineSnapshot: 'Goroutine 快照',
goroutineSummary: ' {0} Goroutine聚合为 {1} 组调用栈',
goroutineTruncated: '快照内容过大当前仅展示部分调用栈',
captureSnapshot: '刷新快照',
count: '数量',
topFunction: '顶层函数',
manualCapture: '手动抓取',
startCapture: '开始抓取',
capturing: '正在抓取',
captureSuccess: '诊断文件已生成已开始下载',
profileCPU: 'CPU',
profileHeap: '堆内存',
profileGoroutine: 'Goroutine',
profileMutex: '互斥锁',
profileBlock: '阻塞',
runtimeRSSDesc: '进程当前占用的物理内存包括 Go 线程栈及原生内存',
runtimeHeapAllocDesc: 'Go 堆中已分配且尚未释放的内存持续增长可能表示对象未释放',
runtimeHeapObjectsDesc: 'Go 堆中当前存活的对象数量持续增长可能表示对象未释放',
runtimeGoroutineDesc: '当前轻量级并发任务数量持续增长通常需要检查 Goroutine 泄漏',
profileCPUDesc: '在指定时间内采样 CPU 调用栈用于定位实际消耗 CPU 的热点函数',
profileHeapDesc: '记录当前仍在使用的 Go 堆对象及其分配调用栈用于排查内存持续增长',
profileGoroutineDesc: '保存所有 Goroutine 的状态和调用栈并按相同调用链聚合',
profileMutexDesc: '统计互斥锁竞争造成的等待时间用于定位锁竞争热点',
profileBlockDesc: '统计 ChannelSelect 和同步原语的阻塞等待用于定位长时间阻塞',
},
terminal: {
local: '本机',
@@ -0,0 +1,379 @@
<template>
<div>
<DrawerPro
v-model="open"
:header="$t('monitor.runtimeDiagnostics')"
size="large"
:confirm-before-close="captureLoading"
@before-close="handleBeforeClose"
>
<div
v-loading="captureLoading"
element-loading-lock
:element-loading-text="$t('monitor.capturing')"
class="diagnostics-content"
>
<section>
<el-row :gutter="12">
<el-col v-for="item in summaryCards" :key="item.label" :xs="12" :sm="8" :md="6">
<div class="summary-item">
<el-card class="summary-title">
<span>{{ item.label }}</span>
<el-tooltip :content="item.description" placement="top" :show-after="200">
<el-icon class="summary-help"><QuestionFilled /></el-icon>
</el-tooltip>
<div class="summary-value">{{ item.value }}</div>
</el-card>
</div>
</el-col>
</el-row>
</section>
<el-divider />
<section>
<div class="section-title">{{ $t('monitor.manualCapture') }}</div>
<div class="profile-toolbar">
<el-select
v-model="captureForm.type"
class="profile-type"
popper-class="runtime-profile-select-popper"
>
<el-option
v-for="item in profileTypes"
:key="item.value"
:label="item.label"
:value="item.value"
>
<div class="runtime-profile-option">
<span class="runtime-profile-option-title">{{ item.label }}</span>
<span class="runtime-profile-option-description">{{ item.description }}</span>
</div>
</el-option>
</el-select>
<el-input-number
v-if="showDuration"
v-model="captureForm.duration"
:min="5"
:max="30"
controls-position="right"
/>
<span v-if="showDuration" class="text-sm">{{ $t('commons.units.secondUnit') }}</span>
<el-button type="primary" @click="captureProfile">
{{ $t('monitor.startCapture') }}
</el-button>
</div>
</section>
<el-divider />
<section>
<div class="section-header">
<div>
<div class="section-title">{{ $t('monitor.goroutineSnapshot') }}</div>
<span class="goroutine-summary">
{{
$t('monitor.goroutineSummary', [
goroutineSnapshot.total,
goroutineSnapshot.groupCount,
])
}}
</span>
</div>
<el-tooltip :content="$t('monitor.captureSnapshot')" placement="top">
<el-button
icon="Refresh"
text
circle
:loading="goroutineLoading"
:aria-label="$t('monitor.captureSnapshot')"
@click="loadGoroutines"
/>
</el-tooltip>
</div>
<el-alert
v-if="goroutineSnapshot.truncated"
class="snapshot-warning"
type="warning"
:title="$t('monitor.goroutineTruncated')"
:closable="false"
show-icon
/>
<ComplexTable :data="goroutineSnapshot.goroutines" v-loading="goroutineLoading" max-height="420">
<el-table-column prop="count" :label="$t('monitor.count')" width="90" sortable />
<el-table-column prop="state" :label="$t('commons.table.status')" width="180" />
<el-table-column
prop="top"
:label="$t('monitor.topFunction')"
min-width="360"
show-overflow-tooltip
/>
<el-table-column :label="$t('commons.table.operate')" fixed="right" width="90">
<template #default="scope">
<el-button link type="primary" @click="showGoroutineStack(scope.row)">
{{ $t('commons.button.view') }}
</el-button>
</template>
</el-table-column>
</ComplexTable>
</section>
</div>
</DrawerPro>
<el-dialog
v-model="stackDialogVisible"
:title="$t('monitor.goroutineSnapshot')"
width="70%"
append-to-body
destroy-on-close
>
<pre class="stack-view">{{ selectedStack.join('\n') }}</pre>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import {
createRuntimeProfile,
loadRuntimeDiagnosticsSummary,
loadRuntimeGoroutines,
RuntimeProfileDownloadError,
} from '@/api/modules/host';
import { Host } from '@/api/interface/host';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { computeSize } from '@/utils/size';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
const { currentNode } = useGlobalStore();
const open = ref(false);
const captureLoading = ref(false);
const goroutineLoading = ref(false);
const stackDialogVisible = ref(false);
const selectedStack = ref<string[]>([]);
const summary = reactive<Host.RuntimeDiagnosticsSummary>({
rss: 0,
heapAlloc: 0,
heapObjects: 0,
goroutines: 0,
});
const goroutineSnapshot = reactive<Host.RuntimeGoroutineSnapshot>({
total: 0,
groupCount: 0,
truncated: false,
capturedAt: '',
goroutines: [],
});
const captureForm = reactive<Host.RuntimeProfileCreate>({ type: 'cpu', duration: 15 });
const profileTypes = computed(() => [
{
label: i18n.global.t('monitor.profileCPU'),
description: i18n.global.t('monitor.profileCPUDesc'),
value: 'cpu',
},
{
label: i18n.global.t('monitor.profileHeap'),
description: i18n.global.t('monitor.profileHeapDesc'),
value: 'heap',
},
{
label: i18n.global.t('monitor.profileGoroutine'),
description: i18n.global.t('monitor.profileGoroutineDesc'),
value: 'goroutine',
},
{
label: i18n.global.t('monitor.profileMutex'),
description: i18n.global.t('monitor.profileMutexDesc'),
value: 'mutex',
},
{
label: i18n.global.t('monitor.profileBlock'),
description: i18n.global.t('monitor.profileBlockDesc'),
value: 'block',
},
]);
const showDuration = computed(() => captureForm.type !== 'heap' && captureForm.type !== 'goroutine');
const summaryCards = computed(() => [
{
label: i18n.global.t('monitor.runtimeRSS'),
value: computeSize(summary.rss),
description: i18n.global.t('monitor.runtimeRSSDesc'),
},
{
label: i18n.global.t('monitor.runtimeHeapAlloc'),
value: computeSize(summary.heapAlloc),
description: i18n.global.t('monitor.runtimeHeapAllocDesc'),
},
{
label: i18n.global.t('monitor.runtimeHeapObjects'),
value: summary.heapObjects.toLocaleString(),
description: i18n.global.t('monitor.runtimeHeapObjectsDesc'),
},
{
label: i18n.global.t('monitor.runtimeGoroutines'),
value: summary.goroutines.toLocaleString(),
description: i18n.global.t('monitor.runtimeGoroutineDesc'),
},
]);
const loadSummary = async () => {
const res = await loadRuntimeDiagnosticsSummary(currentNode.value);
Object.assign(summary, res.data);
};
const loadGoroutines = async () => {
goroutineLoading.value = true;
try {
const res = await loadRuntimeGoroutines(currentNode.value);
Object.assign(goroutineSnapshot, res.data);
} finally {
goroutineLoading.value = false;
}
};
const showGoroutineStack = (row: Host.RuntimeGoroutineGroup) => {
selectedStack.value = row.stack || [];
stackDialogVisible.value = true;
};
const captureProfile = async () => {
captureLoading.value = true;
try {
const data = await createRuntimeProfile(captureForm, currentNode.value);
const url = window.URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
link.download = `${captureForm.type}-${Date.now()}.pb.gz`;
link.click();
window.URL.revokeObjectURL(url);
MsgSuccess(i18n.global.t('monitor.captureSuccess'));
await loadSummary();
} catch (error) {
if (error instanceof RuntimeProfileDownloadError) {
MsgError(error.message || i18n.global.t('commons.res.commonError'));
}
} finally {
captureLoading.value = false;
}
};
const acceptParams = () => {
open.value = true;
Promise.all([loadSummary(), loadGoroutines()]);
};
const handleBeforeClose = (done: () => void) => {
if (!captureLoading.value) {
done();
}
};
defineExpose({ acceptParams });
</script>
<style scoped>
.summary-item {
padding: 4px 12px 4px 0;
}
.summary-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
.summary-help {
flex-shrink: 0;
color: var(--el-text-color-placeholder);
cursor: help;
}
.summary-value {
margin-top: 5px;
color: var(--el-text-color-primary);
font-size: 18px;
font-weight: 600;
}
.diagnostics-content {
min-height: 520px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 32px;
margin-bottom: 10px;
}
.section-title {
margin-bottom: 10px;
color: var(--el-text-color-primary);
font-size: 15px;
font-weight: 600;
}
.section-header .section-title {
margin-bottom: 4px;
}
.profile-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.profile-type {
width: 160px;
}
.goroutine-summary {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.snapshot-warning {
margin-bottom: 10px;
}
.stack-view {
max-height: 65vh;
margin: 0;
padding: 14px;
overflow: auto;
border-radius: 4px;
background: var(--el-fill-color-light);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
</style>
<style>
.runtime-profile-select-popper {
min-width: min(560px, calc(100vw - 64px));
}
.runtime-profile-select-popper .el-select-dropdown__item {
height: auto;
min-height: 54px;
padding-top: 6px;
padding-bottom: 6px;
line-height: normal;
}
.runtime-profile-option {
display: flex;
flex-direction: column;
gap: 4px;
white-space: normal;
}
.runtime-profile-option-title {
color: var(--el-text-color-primary);
font-weight: 500;
}
.runtime-profile-option-description {
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 17px;
}
</style>
@@ -59,6 +59,7 @@
<OpDialog ref="opRef" @search="search" />
<ProcessDetail ref="detailRef" />
<RuntimeDiagnostics ref="runtimeDiagnosticsRef" />
</div>
</template>
@@ -71,6 +72,7 @@ import { stopProcess } from '@/api/modules/process';
import { ProcessStore } from '@/store';
import { SortBy, TableV2SortOrder, ElButton } from 'element-plus';
import { useGlobalStore } from '@/composables/useGlobalStore';
import RuntimeDiagnostics from './diagnostics/index.vue';
const { currentNode } = useGlobalStore();
const processStore = ProcessStore();
@@ -94,6 +96,7 @@ const sortState = ref<SortBy>({
const data = ref<any[]>([]);
const detailRef = ref();
const runtimeDiagnosticsRef = ref<InstanceType<typeof RuntimeDiagnostics>>();
const filters = ref<string[]>([]);
const sortByNum = (a: any, b: any, prop: string): number => {
@@ -186,7 +189,7 @@ const columns = ref([
key: 'actions',
title: i18n.global.t('commons.table.operate'),
dataKey: 'actions',
width: 300,
width: 420,
cellRenderer: ({ rowData }) => {
const stopButton = h(
ElButton,
@@ -197,7 +200,7 @@ const columns = ref([
() => i18n.global.t('process.stopProcess'),
);
return h('div', { class: 'action-buttons' }, [
const buttons = [
h(
ElButton,
{
@@ -206,8 +209,23 @@ const columns = ref([
},
() => i18n.global.t('process.viewDetails'),
),
permissionDirective ? withDirectives(stopButton, [[permissionDirective]]) : stopButton,
]);
];
if (rowData.name === '1panel-agent') {
buttons.push(
h(
ElButton,
{
type: 'text',
onClick: openRuntimeDiagnostics,
},
() => i18n.global.t('monitor.runtimeDiagnostics'),
),
);
}
buttons.push(permissionDirective ? withDirectives(stopButton, [[permissionDirective]]) : stopButton);
return h('div', { class: 'action-buttons' }, buttons);
},
},
]);
@@ -250,6 +268,10 @@ const openDetail = (row: any) => {
detailRef.value.acceptParams(row.PID);
};
const openRuntimeDiagnostics = () => {
runtimeDiagnosticsRef.value?.acceptParams();
};
const changeSort = ({ key, order }) => {
if (!order) order = TableV2SortOrder.ASC;
sortState.value = { key, order };