diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index fd4eb83ddff2..3c013407da49 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -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() diff --git a/agent/app/api/v2/runtime_diagnostics.go b/agent/app/api/v2/runtime_diagnostics.go new file mode 100644 index 000000000000..fcc0fa48fe34 --- /dev/null +++ b/agent/app/api/v2/runtime_diagnostics.go @@ -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() +} diff --git a/agent/app/dto/runtime_diagnostics.go b/agent/app/dto/runtime_diagnostics.go new file mode 100644 index 000000000000..187f68910ef2 --- /dev/null +++ b/agent/app/dto/runtime_diagnostics.go @@ -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"` +} diff --git a/agent/app/service/runtime_diagnostics.go b/agent/app/service/runtime_diagnostics.go new file mode 100644 index 000000000000..f6c5a407e393 --- /dev/null +++ b/agent/app/service/runtime_diagnostics.go @@ -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 +} diff --git a/agent/go.mod b/agent/go.mod index b8767e10e1bf..f90e370e4047 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -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 diff --git a/agent/router/common.go b/agent/router/common.go index 5ebcd5e3d4c1..500a101855a9 100644 --- a/agent/router/common.go +++ b/agent/router/common.go @@ -24,5 +24,6 @@ func commonGroups() []CommonRouter { &AIToolsRouter{}, &GroupRouter{}, &AlertRouter{}, + &RuntimeDiagnosticsRouter{}, } } diff --git a/agent/router/ro_runtime_diagnostics.go b/agent/router/ro_runtime_diagnostics.go new file mode 100644 index 000000000000..06833d035f06 --- /dev/null +++ b/agent/router/ro_runtime_diagnostics.go @@ -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) + } +} diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index 405fbd143854..57691afed8c9 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -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; diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index 3d1d879f2349..b9f0d32bccf5 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -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( + `/hosts/diagnostics/summary`, + {}, + currentNode ? { headers: { CurrentNode: currentNode } } : {}, + ); +}; +export const loadRuntimeGoroutines = (currentNode?: string) => { + return http.get( + `/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(`/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( diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 4167a1c01b97..1c4eadce7030 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index fe3db2ab7feb..a515ca2cffe5 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -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', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index eee2b7ba6a82..b01275d9f3bb 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -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: 'محلی', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 0923df963015..28eee0b2cbdb 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -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: 'Channel、Select、同期プリミティブでのブロック待機時間を測定します。', }, terminal: { local: 'ローカル', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 49a096ef272f..dd350b3cfbc6 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -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: '로컬', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 35d4aebe7ab5..d307ffd52f4f 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -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', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 9ba1eda11092..bfc384aa85d8 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -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', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 047d9d7d8282..b9bd14150b92 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -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: 'Локальный', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index d943ce07e419..4a8de1576041 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -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, iş 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', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 793be045fb08..394914043fb0 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -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: '統計 Channel、Select 和同步原語的阻塞等待,用於定位長時間阻塞。', }, terminal: { local: '本機', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index be20689a6c17..95ef637c0e7e 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '统计 Channel、Select 和同步原语的阻塞等待,用于定位长时间阻塞。', }, terminal: { local: '本机', diff --git a/frontend/src/views/host/process/process/diagnostics/index.vue b/frontend/src/views/host/process/process/diagnostics/index.vue new file mode 100644 index 000000000000..3da5eff3f6ec --- /dev/null +++ b/frontend/src/views/host/process/process/diagnostics/index.vue @@ -0,0 +1,379 @@ + + + + + + + diff --git a/frontend/src/views/host/process/process/index.vue b/frontend/src/views/host/process/process/index.vue index 369410ae6dc8..45d662b42530 100644 --- a/frontend/src/views/host/process/process/index.vue +++ b/frontend/src/views/host/process/process/index.vue @@ -59,6 +59,7 @@ + @@ -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({ const data = ref([]); const detailRef = ref(); +const runtimeDiagnosticsRef = ref>(); const filters = ref([]); 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 };