diff --git a/cmd/diag/sysinfo/sysinfo.go b/cmd/diag/sysinfo/sysinfo.go index f4e776d7a6d..070425f4868 100644 --- a/cmd/diag/sysinfo/sysinfo.go +++ b/cmd/diag/sysinfo/sysinfo.go @@ -18,12 +18,15 @@ package sysinfo import ( "fmt" + "sort" "strconv" "strings" + "github.com/jedib0t/go-pretty/v6/table" "github.com/urfave/cli/v2" "github.com/erigontech/erigon-lib/diagnostics" + "github.com/erigontech/erigon-lib/sysutils" "github.com/erigontech/erigon/cmd/diag/flags" "github.com/erigontech/erigon/cmd/diag/util" ) @@ -59,6 +62,14 @@ var Command = cli.Command{ Description: "Collect information about system and save it to file in order to provide to support person", } +type SortType int + +const ( + SortByCPU SortType = iota + SortByMemory + SortByPID +) + func collectInfo(cliCtx *cli.Context) error { data, err := getData(cliCtx) if err != nil { @@ -72,6 +83,10 @@ func collectInfo(cliCtx *cli.Context) error { builder.WriteString("CPU info:\n") writeCPUToStringBuilder(data.CPU, &builder) + processes := sysutils.GetProcessesInfo() + builder.WriteString("\n\nProcesses info:\n") + writeProcessesToStringBuilder(processes, &builder) + // Save data to file err = util.SaveDataToFile(cliCtx.String(ExportPathFlag.Name), cliCtx.String(ExportFileNameFlag.Name), builder.String()) if err != nil { @@ -101,6 +116,56 @@ func writeCPUToStringBuilder(cpuInfo []diagnostics.CPUInfo, builder *strings.Bui } } +func writeProcessesToStringBuilder(prcInfo []*sysutils.ProcessInfo, builder *strings.Builder) { + prcInfo = sortProcessesByCPU(prcInfo) + rows := make([]table.Row, 0) + header := table.Row{"PID", "Name", "% CPU", "% Memory"} + for _, process := range prcInfo { + cpu := fmt.Sprintf("%.2f", process.CPUUsage) + memory := fmt.Sprintf("%.2f", process.Memory) + rows = append(rows, table.Row{process.Pid, process.Name, cpu, memory}) + } + + t := table.NewWriter() + + t.AppendHeader(header) + if len(rows) > 0 { + t.AppendRows(rows) + } + + t.AppendSeparator() + result := t.Render() + builder.WriteString(result) +} + +func sortProcesses(prcInfo []*sysutils.ProcessInfo, sorting SortType) []*sysutils.ProcessInfo { + sort.Slice(prcInfo, func(i, j int) bool { + switch sorting { + case SortByCPU: + return prcInfo[i].CPUUsage > prcInfo[j].CPUUsage + case SortByMemory: + return prcInfo[i].Memory > prcInfo[j].Memory + default: + return prcInfo[i].Pid < prcInfo[j].Pid + } + + }) + + return prcInfo +} + +func sortProcessesByCPU(prcInfo []*sysutils.ProcessInfo) []*sysutils.ProcessInfo { + return sortProcesses(prcInfo, SortByCPU) +} + +func sortProcessesByMemory(prcInfo []*sysutils.ProcessInfo) []*sysutils.ProcessInfo { + return sortProcesses(prcInfo, SortByMemory) +} + +func sortProcessesByPID(prcInfo []*sysutils.ProcessInfo) []*sysutils.ProcessInfo { + return sortProcesses(prcInfo, SortByPID) +} + func calculateSpacing(keysArray []string) int { max := 0 for _, key := range keysArray { diff --git a/erigon-lib/sysutils/sysutils.go b/erigon-lib/sysutils/sysutils.go new file mode 100644 index 00000000000..9941c310faa --- /dev/null +++ b/erigon-lib/sysutils/sysutils.go @@ -0,0 +1,171 @@ +// Copyright 2024 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package sysutils + +import ( + "time" + + "github.com/erigontech/erigon-lib/log/v3" + "github.com/shirou/gopsutil/v4/process" +) + +type ProcessInfo struct { + Pid int32 + Name string + CPUUsage float64 + Memory float32 +} + +type ProcessMerge struct { + CPUUsage float64 + Memory float32 + Times int + Name string +} + +const ( + iterations = 5 + sleepSeconds = 2 + usageThreshold = 0.05 +) + +func GetProcessesInfo() []*ProcessInfo { + procs, err := process.Processes() + if err != nil { + log.Debug("[Sysutil] Error retrieving processes: %v", err) + } + + return averageProceses(procs) +} + +func AverageProceses(procs []*process.Process) []*ProcessInfo { + return averageProceses(procs) +} + +func averageProceses(procs []*process.Process) []*ProcessInfo { + // Collect processes and calculate average stats. + allProcsRepeats := make([][]*ProcessInfo, 0, iterations) + + // Collect all processes N times with a delay of N seconds to calculate average stats. + for i := 0; i < iterations; i++ { + processes := allProcesses(procs) + allProcsRepeats = append(allProcsRepeats, processes) + time.Sleep(sleepSeconds * time.Second) + } + + // Calculate average stats. + averageProcs := mergeProcesses(allProcsRepeats) + averageProcs = removeProcessesBelowThreshold(averageProcs, usageThreshold) + + return averageProcs +} + +func RemoveProcessesBelowThreshold(processes []*ProcessInfo, treshold float64) []*ProcessInfo { + return removeProcessesBelowThreshold(processes, treshold) +} + +func removeProcessesBelowThreshold(processes []*ProcessInfo, treshold float64) []*ProcessInfo { + // remove processes with CPU or Memory usage less than threshold + filtered := make([]*ProcessInfo, 0, len(processes)) + for _, p := range processes { + if p.CPUUsage >= treshold || p.Memory >= float32(treshold) { + filtered = append(filtered, p) + } + } + + return filtered +} + +func MergeProcesses(allProcsRepeats [][]*ProcessInfo) []*ProcessInfo { + return mergeProcesses(allProcsRepeats) +} + +func mergeProcesses(allProcsRepeats [][]*ProcessInfo) []*ProcessInfo { + if len(allProcsRepeats) == 0 || len(allProcsRepeats[0]) == 0 { + return nil + } + + repeats := len(allProcsRepeats) + if repeats == 1 { + return allProcsRepeats[0] + } + + prcmap := make(map[int32]*ProcessMerge) + + for _, procList := range allProcsRepeats { + for _, proc := range procList { + if prc, exists := prcmap[proc.Pid]; exists { + prc.CPUUsage += proc.CPUUsage + prc.Memory += proc.Memory + prc.Times++ + } else { + prcmap[proc.Pid] = &ProcessMerge{ + CPUUsage: proc.CPUUsage, + Memory: proc.Memory, + Times: 1, + Name: proc.Name, + } + } + } + } + + resultArray := make([]*ProcessInfo, 0, len(prcmap)) + + for pid, prc := range prcmap { + resultArray = append(resultArray, &ProcessInfo{ + Pid: pid, + Name: prc.Name, + CPUUsage: prc.CPUUsage / float64(prc.Times), + Memory: prc.Memory / float32(prc.Times), + }) + } + + return resultArray +} + +func allProcesses(procs []*process.Process) []*ProcessInfo { + processes := make([]*ProcessInfo, 0) + + for _, proc := range procs { + pid := proc.Pid + name, err := proc.Name() + if err != nil { + name = "Unknown" + } + + //remove gopls process as it is what we use to get info + if name == "gopls" { + continue + } + + cpuPercent, err := proc.CPUPercent() + if err != nil { + log.Trace("[Sysutil] Error retrieving CPU percent for PID %d: %v Name: %s", pid, err, name) + continue + } + + memPercent, err := proc.MemoryPercent() + if err != nil { + log.Trace("[Sysutil] Error retrieving memory percent for PID %d: %v Name: %s", pid, err, name) + continue + } + + processes = append(processes, &ProcessInfo{Pid: pid, Name: name, CPUUsage: cpuPercent, Memory: memPercent}) + } + + return processes +} diff --git a/erigon-lib/sysutils/sysutils_test.go b/erigon-lib/sysutils/sysutils_test.go new file mode 100644 index 00000000000..758a3f646f9 --- /dev/null +++ b/erigon-lib/sysutils/sysutils_test.go @@ -0,0 +1,68 @@ +package sysutils_test + +import ( + "testing" + + "github.com/erigontech/erigon-lib/sysutils" + "github.com/stretchr/testify/require" +) + +func TestMergeProcesses(t *testing.T) { + initaldata := [][]*sysutils.ProcessInfo{ + { + {Pid: 1, Name: "test1", CPUUsage: 1.0, Memory: 1.0}, + {Pid: 2, Name: "test2", CPUUsage: 2.0, Memory: 2.0}, + {Pid: 3, Name: "test3", CPUUsage: 3.0, Memory: 3.0}, + {Pid: 31, Name: "test31", CPUUsage: 3.0, Memory: 3.0}, + }, + { + {Pid: 1, Name: "test1", CPUUsage: 1.0, Memory: 1.0}, + {Pid: 2, Name: "test2", CPUUsage: 1.0, Memory: 1.0}, + {Pid: 22, Name: "test4", CPUUsage: 1.0, Memory: 1.0}, + }, + } + + expected := []*sysutils.ProcessInfo{ + {Pid: 1, Name: "test1", CPUUsage: 1.0, Memory: 1.0}, + {Pid: 2, Name: "test2", CPUUsage: 1.5, Memory: 1.5}, + {Pid: 3, Name: "test3", CPUUsage: 3.0, Memory: 3.0}, + {Pid: 31, Name: "test31", CPUUsage: 3.0, Memory: 3.0}, + {Pid: 22, Name: "test4", CPUUsage: 1.0, Memory: 1.0}, + } + + result := sysutils.MergeProcesses(initaldata) + for _, proc := range result { + require.Contains(t, expected, proc) + } +} + +func TestRemoveProcessesBelowThreshold(t *testing.T) { + initaldata := [][]*sysutils.ProcessInfo{ + { + {Pid: 1, Name: "test1", CPUUsage: 1.0, Memory: 1.0}, + {Pid: 2, Name: "test2", CPUUsage: 2.0, Memory: 2.0}, + {Pid: 3, Name: "test3", CPUUsage: 3.0, Memory: 3.0}, + {Pid: 12, Name: "test5", CPUUsage: 0.001, Memory: 1.0}, + {Pid: 45, Name: "test8", CPUUsage: 0.001, Memory: 0.0}, + }, + { + {Pid: 1, Name: "test1", CPUUsage: 1.0, Memory: 1.0}, + {Pid: 2, Name: "test2", CPUUsage: 1.0, Memory: 1.0}, + {Pid: 22, Name: "test4", CPUUsage: 1.0, Memory: 0.001}, + }, + } + + expected := []*sysutils.ProcessInfo{ + {Pid: 1, Name: "test1", CPUUsage: 1.0, Memory: 1.0}, + {Pid: 2, Name: "test2", CPUUsage: 1.5, Memory: 1.5}, + {Pid: 3, Name: "test3", CPUUsage: 3.0, Memory: 3.0}, + {Pid: 22, Name: "test4", CPUUsage: 1.0, Memory: 0.001}, + {Pid: 12, Name: "test5", CPUUsage: 0.001, Memory: 1.0}, + } + + result := sysutils.MergeProcesses(initaldata) + result = sysutils.RemoveProcessesBelowThreshold(result, 0.01) + for _, proc := range result { + require.Contains(t, expected, proc) + } +}