From 19b7a7b46c3a51bbe843ffe1daa928be1912a392 Mon Sep 17 00:00:00 2001 From: dvovk Date: Tue, 6 Aug 2024 16:48:32 +0100 Subject: [PATCH 1/3] wp --- erigon-lib/sysutils/sysutils.go | 165 ++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 erigon-lib/sysutils/sysutils.go diff --git a/erigon-lib/sysutils/sysutils.go b/erigon-lib/sysutils/sysutils.go new file mode 100644 index 00000000000..e288c65534a --- /dev/null +++ b/erigon-lib/sysutils/sysutils.go @@ -0,0 +1,165 @@ +// 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 ( + "fmt" + "sort" + "time" + + "github.com/erigontech/erigon-lib/log/v3" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/process" +) + +type ProcessInfo struct { + Pid int32 + Name string + CPUUsage float64 + Memory float32 + StartTime int64 +} + +const ( + iterations = 5 + sleepSeconds = 2 + usageThreshold = 0.05 +) + +func AllProcessesCPU() { + procs, err := process.Processes() + if err != nil { + log.Fatalf("Error retrieving processes: %v", err) + } + + // Collect processes and calculate average stats. + allProcsRepeats := make([][]ProcessInfo, 0, iterations) + + // Collect all processes 5 times with a delay of 2 seconds to calculate average stats. + for i := 0; i < iterations; i++ { + processCPUUsage := allProcesses(procs) + allProcsRepeats = append(allProcsRepeats, processCPUUsage) + time.Sleep(sleepSeconds * time.Second) + } + + // Calculate average stats. + averageProcs := mergeProcesses(allProcsRepeats) + + // Sort by CPU usage in descending order and remove processes with negligible CPU and memory usage. + sort.Slice(averageProcs, func(i, j int) bool { + return averageProcs[i].CPUUsage > averageProcs[j].CPUUsage + }) + + // Filter out processes with negligible CPU or memory usage. + toDisplay := make([]ProcessInfo, 0, len(averageProcs)) + for _, proc := range averageProcs { + if proc.CPUUsage > usageThreshold || proc.Memory > usageThreshold { + toDisplay = append(toDisplay, proc) + } + } + + //print all + for idx, proc := range toDisplay { + fmt.Printf("#:%d PID: %d Name: %s CPU Usage: %.2f%% Memory: %.2f%% Start Time: %s\n", idx, proc.Pid, proc.Name, proc.CPUUsage, proc.Memory, time.Unix(proc.StartTime, 0)) + } + + // Optionally, you can also print total CPU usage for reference + totalCPUPercent, err := cpu.Percent(time.Second, false) + if err != nil { + log.Fatalf("Error retrieving total CPU usage: %v", err) + } + fmt.Printf("Total CPU Usage: %.2f%%\n", totalCPUPercent[0]) +} + +// merge all processes infos into one array with average values +func mergeProcesses(allProcsRepeats [][]ProcessInfo) []ProcessInfo { + if len(allProcsRepeats) == 0 || len(allProcsRepeats[0]) == 0 { + return nil + } + + repeats := len(allProcsRepeats) + allProcessLength := len(allProcsRepeats[0]) + resultArray := make([]ProcessInfo, 0, allProcessLength) + + for i := 0; i < allProcessLength; i++ { + firstProcess := allProcsRepeats[0][i] + totalCPUUsage := firstProcess.CPUUsage + totalMemory := firstProcess.Memory + + if repeats > 1 { + for j := 1; j < repeats; j++ { + totalCPUUsage += allProcsRepeats[j][i].CPUUsage + totalMemory += allProcsRepeats[j][i].Memory + } + + totalCPUUsage /= float64(repeats) + totalMemory /= float32(repeats) + } + + resultArray = append(resultArray, ProcessInfo{ + Pid: firstProcess.Pid, + Name: firstProcess.Name, + CPUUsage: totalCPUUsage, + Memory: totalMemory, + StartTime: firstProcess.StartTime, + }) + } + + return resultArray +} + +func allProcesses(procs []*process.Process) []ProcessInfo { + // add process cpu usage data to array in order to sort it later + processCPUUsage := make([]ProcessInfo, 0) + + for _, proc := range procs { + // Get process ID + pid := proc.Pid + // Get process name + name, err := proc.Name() + if err != nil { + name = "Unknown" + } + + // Get CPU percent + cpuPercent, err := proc.CPUPercent() + if err != nil { + //log.Printf("Error retrieving CPU percent for PID %d: %v Name: %s", pid, err, name) + continue + } + + // Get memory percent + memPercent, err := proc.MemoryPercent() + if err != nil { + //log.Printf("Error retrieving memory percent for PID %d: %v Name: %s", pid, err, name) + continue + } + + // Get process start time + createTime, err := proc.CreateTime() + if err != nil { + //log.Printf("Error retrieving create time for PID %d: %v Name: %s", pid, err, name) + continue + } + + // memory info + + processCPUUsage = append(processCPUUsage, ProcessInfo{Pid: pid, Name: name, CPUUsage: cpuPercent, Memory: memPercent, StartTime: createTime}) + } + + return processCPUUsage +} From 88569bd21c445c9eb7430c64986ccd6022266e8a Mon Sep 17 00:00:00 2001 From: dvovk Date: Wed, 7 Aug 2024 18:15:05 +0100 Subject: [PATCH 2/3] added collecting info about all processes --- cmd/diag/sysinfo/sysinfo.go | 65 +++++++++++ erigon-lib/sysutils/sysutils.go | 157 ++++++++++++++------------- erigon-lib/sysutils/sysutils_test.go | 68 ++++++++++++ 3 files changed, 214 insertions(+), 76 deletions(-) create mode 100644 erigon-lib/sysutils/sysutils_test.go 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 index e288c65534a..34de0069600 100644 --- a/erigon-lib/sysutils/sysutils.go +++ b/erigon-lib/sysutils/sysutils.go @@ -17,21 +17,24 @@ package sysutils import ( - "fmt" - "sort" "time" "github.com/erigontech/erigon-lib/log/v3" - "github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/process" ) type ProcessInfo struct { - Pid int32 - Name string - CPUUsage float64 - Memory float32 - StartTime int64 + Pid int32 + Name string + CPUUsage float64 + Memory float32 +} + +type ProcessMerge struct { + CPUUsage float64 + Memory float32 + Times int + Name string } const ( @@ -40,126 +43,128 @@ const ( usageThreshold = 0.05 ) -func AllProcessesCPU() { +func GetProcessesInfo() []*ProcessInfo { procs, err := process.Processes() if err != nil { - log.Fatalf("Error retrieving processes: %v", err) + 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) + allProcsRepeats := make([][]*ProcessInfo, 0, iterations) - // Collect all processes 5 times with a delay of 2 seconds to calculate average stats. + // Collect all processes N times with a delay of N seconds to calculate average stats. for i := 0; i < iterations; i++ { - processCPUUsage := allProcesses(procs) - allProcsRepeats = append(allProcsRepeats, processCPUUsage) + processes := allProcesses(procs) + allProcsRepeats = append(allProcsRepeats, processes) time.Sleep(sleepSeconds * time.Second) } // Calculate average stats. averageProcs := mergeProcesses(allProcsRepeats) + averageProcs = removeProcessesAboveThreshold(averageProcs, usageThreshold) - // Sort by CPU usage in descending order and remove processes with negligible CPU and memory usage. - sort.Slice(averageProcs, func(i, j int) bool { - return averageProcs[i].CPUUsage > averageProcs[j].CPUUsage - }) + return averageProcs +} - // Filter out processes with negligible CPU or memory usage. - toDisplay := make([]ProcessInfo, 0, len(averageProcs)) - for _, proc := range averageProcs { - if proc.CPUUsage > usageThreshold || proc.Memory > usageThreshold { - toDisplay = append(toDisplay, proc) - } - } +func RemoveProcessesAboveThreshold(processes []*ProcessInfo, treshold float64) []*ProcessInfo { + return removeProcessesAboveThreshold(processes, treshold) +} - //print all - for idx, proc := range toDisplay { - fmt.Printf("#:%d PID: %d Name: %s CPU Usage: %.2f%% Memory: %.2f%% Start Time: %s\n", idx, proc.Pid, proc.Name, proc.CPUUsage, proc.Memory, time.Unix(proc.StartTime, 0)) +func removeProcessesAboveThreshold(processes []*ProcessInfo, treshold float64) []*ProcessInfo { + // remove processes with CPU or Memory usage less than threshold + for i := 0; i < len(processes); i++ { + if processes[i].CPUUsage < treshold && processes[i].Memory < float32(treshold) { + processes = append(processes[:i], processes[i+1:]...) + i-- + } } + return processes +} - // Optionally, you can also print total CPU usage for reference - totalCPUPercent, err := cpu.Percent(time.Second, false) - if err != nil { - log.Fatalf("Error retrieving total CPU usage: %v", err) - } - fmt.Printf("Total CPU Usage: %.2f%%\n", totalCPUPercent[0]) +func MergeProcesses(allProcsRepeats [][]*ProcessInfo) []*ProcessInfo { + return mergeProcesses(allProcsRepeats) } -// merge all processes infos into one array with average values -func mergeProcesses(allProcsRepeats [][]ProcessInfo) []ProcessInfo { +func mergeProcesses(allProcsRepeats [][]*ProcessInfo) []*ProcessInfo { if len(allProcsRepeats) == 0 || len(allProcsRepeats[0]) == 0 { return nil } repeats := len(allProcsRepeats) - allProcessLength := len(allProcsRepeats[0]) - resultArray := make([]ProcessInfo, 0, allProcessLength) - - for i := 0; i < allProcessLength; i++ { - firstProcess := allProcsRepeats[0][i] - totalCPUUsage := firstProcess.CPUUsage - totalMemory := firstProcess.Memory - - if repeats > 1 { - for j := 1; j < repeats; j++ { - totalCPUUsage += allProcsRepeats[j][i].CPUUsage - totalMemory += allProcsRepeats[j][i].Memory - } + if repeats == 1 { + return allProcsRepeats[0] + } - totalCPUUsage /= float64(repeats) - totalMemory /= float32(repeats) + 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)) - resultArray = append(resultArray, ProcessInfo{ - Pid: firstProcess.Pid, - Name: firstProcess.Name, - CPUUsage: totalCPUUsage, - Memory: totalMemory, - StartTime: firstProcess.StartTime, + 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 { - // add process cpu usage data to array in order to sort it later - processCPUUsage := make([]ProcessInfo, 0) +func allProcesses(procs []*process.Process) []*ProcessInfo { + processes := make([]*ProcessInfo, 0) for _, proc := range procs { - // Get process ID pid := proc.Pid - // Get process name name, err := proc.Name() if err != nil { name = "Unknown" } - // Get CPU percent - cpuPercent, err := proc.CPUPercent() - if err != nil { - //log.Printf("Error retrieving CPU percent for PID %d: %v Name: %s", pid, err, name) + //remove gopls process as it is what we use to get info + if name == "gopls" { continue } - // Get memory percent - memPercent, err := proc.MemoryPercent() + cpuPercent, err := proc.CPUPercent() if err != nil { - //log.Printf("Error retrieving memory percent for PID %d: %v Name: %s", pid, err, name) + log.Trace("[Sysutil] Error retrieving CPU percent for PID %d: %v Name: %s", pid, err, name) continue } - // Get process start time - createTime, err := proc.CreateTime() + memPercent, err := proc.MemoryPercent() if err != nil { - //log.Printf("Error retrieving create time for PID %d: %v Name: %s", pid, err, name) + log.Trace("[Sysutil] Error retrieving memory percent for PID %d: %v Name: %s", pid, err, name) continue } - // memory info - - processCPUUsage = append(processCPUUsage, ProcessInfo{Pid: pid, Name: name, CPUUsage: cpuPercent, Memory: memPercent, StartTime: createTime}) + processes = append(processes, &ProcessInfo{Pid: pid, Name: name, CPUUsage: cpuPercent, Memory: memPercent}) } - return processCPUUsage + return processes } diff --git a/erigon-lib/sysutils/sysutils_test.go b/erigon-lib/sysutils/sysutils_test.go new file mode 100644 index 00000000000..76daca62d6d --- /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 TestRemoveProcessesAboveThreshold(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.RemoveProcessesAboveThreshold(result, 0.01) + for _, proc := range result { + require.Contains(t, expected, proc) + } +} From 0fed3367b4bd60416559d1a9517bcf210b10e52d Mon Sep 17 00:00:00 2001 From: dvovk Date: Wed, 7 Aug 2024 22:28:29 +0100 Subject: [PATCH 3/3] fixes --- erigon-lib/sysutils/sysutils.go | 19 ++++++++++--------- erigon-lib/sysutils/sysutils_test.go | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/erigon-lib/sysutils/sysutils.go b/erigon-lib/sysutils/sysutils.go index 34de0069600..9941c310faa 100644 --- a/erigon-lib/sysutils/sysutils.go +++ b/erigon-lib/sysutils/sysutils.go @@ -69,24 +69,25 @@ func averageProceses(procs []*process.Process) []*ProcessInfo { // Calculate average stats. averageProcs := mergeProcesses(allProcsRepeats) - averageProcs = removeProcessesAboveThreshold(averageProcs, usageThreshold) + averageProcs = removeProcessesBelowThreshold(averageProcs, usageThreshold) return averageProcs } -func RemoveProcessesAboveThreshold(processes []*ProcessInfo, treshold float64) []*ProcessInfo { - return removeProcessesAboveThreshold(processes, treshold) +func RemoveProcessesBelowThreshold(processes []*ProcessInfo, treshold float64) []*ProcessInfo { + return removeProcessesBelowThreshold(processes, treshold) } -func removeProcessesAboveThreshold(processes []*ProcessInfo, treshold float64) []*ProcessInfo { +func removeProcessesBelowThreshold(processes []*ProcessInfo, treshold float64) []*ProcessInfo { // remove processes with CPU or Memory usage less than threshold - for i := 0; i < len(processes); i++ { - if processes[i].CPUUsage < treshold && processes[i].Memory < float32(treshold) { - processes = append(processes[:i], processes[i+1:]...) - i-- + filtered := make([]*ProcessInfo, 0, len(processes)) + for _, p := range processes { + if p.CPUUsage >= treshold || p.Memory >= float32(treshold) { + filtered = append(filtered, p) } } - return processes + + return filtered } func MergeProcesses(allProcsRepeats [][]*ProcessInfo) []*ProcessInfo { diff --git a/erigon-lib/sysutils/sysutils_test.go b/erigon-lib/sysutils/sysutils_test.go index 76daca62d6d..758a3f646f9 100644 --- a/erigon-lib/sysutils/sysutils_test.go +++ b/erigon-lib/sysutils/sysutils_test.go @@ -36,7 +36,7 @@ func TestMergeProcesses(t *testing.T) { } } -func TestRemoveProcessesAboveThreshold(t *testing.T) { +func TestRemoveProcessesBelowThreshold(t *testing.T) { initaldata := [][]*sysutils.ProcessInfo{ { {Pid: 1, Name: "test1", CPUUsage: 1.0, Memory: 1.0}, @@ -61,7 +61,7 @@ func TestRemoveProcessesAboveThreshold(t *testing.T) { } result := sysutils.MergeProcesses(initaldata) - result = sysutils.RemoveProcessesAboveThreshold(result, 0.01) + result = sysutils.RemoveProcessesBelowThreshold(result, 0.01) for _, proc := range result { require.Contains(t, expected, proc) }