This commit is contained in:
nm1 2026-11-04 00:33:23 +05:30
commit 74ae93a96e

525
_resources/main-1.go Normal file
View file

@ -0,0 +1,525 @@
//go:build linux
package main
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"text/tabwriter"
"time"
)
var w *tabwriter.Writer
type Process struct {
PID int
Command string
CPUTime uint64 // clock ticks
RSSPages uint64
SwapBytes uint64
}
type Snapshot struct {
Processes map[int]Process
TotalCPU uint64
}
const INPUT_FILENAME = "/home/hpmi/Videos/imp/tests/others/go/process_info/input/process.txt"
func getProcessList() (processes []string, err error) {
file, err := os.Open(INPUT_FILENAME)
if err != nil {
fmt.Println("err: ", err)
return nil, err
}
sc := bufio.NewScanner(file)
processes = []string{}
for sc.Scan() {
processes = append(processes, sc.Text())
}
if err := sc.Err(); err != nil {
log.Fatalf("Error reading file: %s", err)
}
return
}
func printResultOfKeyword(keyword string) {
keyword = strings.ToLower(keyword)
interval := time.Second / 10
numCPU := uint64(os.Getuid()) // replaced below
_ = numCPU
cpuCount := uint64(getCPUCount())
if cpuCount == 0 {
cpuCount = 1
}
// First snapshot.
first, err := takeSnapshot(keyword)
if err != nil {
fmt.Fprintf(os.Stderr, "snapshot error: %v\n", err)
os.Exit(1)
}
// _ = interval
time.Sleep(interval)
// Second snapshot.
second, err := takeSnapshot(keyword)
if err != nil {
fmt.Fprintf(os.Stderr, "snapshot error: %v\n", err)
os.Exit(1)
}
result := calculate(first, second, cpuCount)
printResult(keyword, result)
}
func main() {
w = tabwriter.NewWriter(os.Stdout, 1, 1, 3, ' ', 0)
defer w.Flush()
if len(os.Args) < 2 {
// fmt.Printf("Usage: %s <keyword> [interval]\n", os.Args[0])
// fmt.Printf("\nExamples:\n")
// fmt.Printf(" %s firefox\n", os.Args[0])
// fmt.Printf(" %s firefox 1\n", os.Args[0])
// os.Exit(1)
processes, err := getProcessList()
if err != nil {
fmt.Println("err: ", err)
return
}
// fmt.Println("processes: ", processes)
printHeaders()
for _, process := range processes {
// fmt.Println(i + 1)
printResultOfKeyword(process)
}
return
}
printHeaders()
printResultOfKeyword(os.Args[1])
if len(os.Args) >= 3 {
seconds, err := strconv.ParseFloat(os.Args[2], 64)
if err != nil || seconds <= 0 {
fmt.Fprintf(os.Stderr, "invalid interval: %s\n", os.Args[2])
os.Exit(1)
}
}
}
type Result struct {
ProcessCount int
CPUPercent float64
RAMBytes uint64
SwapBytes uint64
}
func calculate(first, second Snapshot, cpuCount uint64) Result {
var result Result
// RAM and swap are taken from the second snapshot.
for _, p := range second.Processes {
result.ProcessCount++
result.RAMBytes += pagesToBytes(p.RSSPages)
result.SwapBytes += p.SwapBytes
}
/*
CPU calculation:
Process CPU time is measured in clock ticks.
We calculate:
process_delta / total_cpu_delta * number_of_cpus * 100
This allows CPU usage to exceed 100% when a process/group
is using multiple cores.
*/
var processDelta uint64
for pid, secondProcess := range second.Processes {
firstProcess, exists := first.Processes[pid]
if !exists {
continue
}
if secondProcess.CPUTime >= firstProcess.CPUTime {
processDelta += secondProcess.CPUTime - firstProcess.CPUTime
}
}
totalCPUDelta := second.TotalCPU - first.TotalCPU
if totalCPUDelta > 0 {
// result.CPUPercent =
// (float64(processDelta) / float64(totalCPUDelta)) *
// float64(cpuCount) *
// 100
// so instead of cpu percentage being 200% or something it will only be relative to 100%
result.CPUPercent =
(float64(processDelta) / float64(totalCPUDelta)) * 100
}
return result
}
func takeSnapshot(keyword string) (Snapshot, error) {
snapshot := Snapshot{
Processes: make(map[int]Process),
}
entries, err := os.ReadDir("/proc")
if err != nil {
return snapshot, err
}
selfPID := os.Getpid()
for _, entry := range entries {
if !entry.IsDir() {
continue
}
pid, err := strconv.Atoi(entry.Name())
if err != nil {
// Not a PID directory.
continue
}
// Don't count procusage itself.
if pid == selfPID {
continue
}
process, err := readProcess(pid)
if err != nil {
// Process may have exited between ReadDir and reading
// /proc/<pid>. That's normal.
continue
}
commandLower := strings.ToLower(process.Command)
if strings.Contains(commandLower, keyword) {
snapshot.Processes[pid] = process
}
}
snapshot.TotalCPU, _ = readTotalCPU()
return snapshot, nil
}
func readProcess(pid int) (Process, error) {
var process Process
process.PID = pid
// Read the complete command line.
cmdline, err := os.ReadFile(
filepath.Join("/proc", strconv.Itoa(pid), "cmdline"),
)
if err != nil {
return process, err
}
/*
/proc/<pid>/cmdline is separated by NUL bytes:
/usr/bin/firefox\0--some-option\0foo\0
Convert those NULs into spaces so matching and displaying
the command line is convenient.
*/
command := strings.ReplaceAll(string(cmdline), "\x00", " ")
command = strings.TrimSpace(command)
// Some kernel/system processes have an empty cmdline.
// Fall back to /comm if necessary.
if command == "" {
comm, err := os.ReadFile(
filepath.Join("/proc", strconv.Itoa(pid), "comm"),
)
if err != nil {
return process, err
}
command = strings.TrimSpace(string(comm))
}
process.Command = command
// CPU time from /proc/<pid>/stat.
cpuTime, rssPages, err := readStat(pid)
if err != nil {
return process, err
}
process.CPUTime = cpuTime
process.RSSPages = rssPages
// Swap from /proc/<pid>/status.
process.SwapBytes = readSwap(pid)
return process, nil
}
func readStat(pid int) (cpuTime uint64, rssPages uint64, err error) {
path := filepath.Join("/proc", strconv.Itoa(pid), "stat")
data, err := os.ReadFile(path)
if err != nil {
return 0, 0, err
}
line := string(data)
/*
/proc/<pid>/stat contains:
pid (comm) state ppid ...
The comm field itself can contain spaces and parentheses,
so we cannot simply strings.Fields() the whole line.
Find the LAST ')' which terminates comm.
*/
closeParen := strings.LastIndex(line, ")")
if closeParen == -1 {
return 0, 0, errors.New("invalid /proc/stat format")
}
rest := strings.TrimSpace(line[closeParen+1:])
fields := strings.Fields(rest)
/*
After comm, fields start at:
field 3 = state
field 4 = ppid
...
field 14 = utime
field 15 = stime
...
field 24 = rss
Because fields[] starts at field 3:
utime -> index 11
stime -> index 12
rss -> index 21
*/
if len(fields) <= 21 {
return 0, 0, errors.New("short /proc/stat entry")
}
utime, err := strconv.ParseUint(fields[11], 10, 64)
if err != nil {
return 0, 0, err
}
stime, err := strconv.ParseUint(fields[12], 10, 64)
if err != nil {
return 0, 0, err
}
rss, err := strconv.ParseInt(fields[21], 10, 64)
if err != nil {
return 0, 0, err
}
if rss < 0 {
rss = 0
}
return utime + stime, uint64(rss), nil
}
func readSwap(pid int) uint64 {
path := filepath.Join("/proc", strconv.Itoa(pid), "status")
file, err := os.Open(path)
if err != nil {
return 0
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "VmSwap:") {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
return 0
}
/*
VmSwap is normally reported in kB.
*/
kb, err := strconv.ParseUint(fields[1], 10, 64)
if err != nil {
return 0
}
return kb * 1024
}
return 0
}
func readTotalCPU() (uint64, error) {
data, err := os.ReadFile("/proc/stat")
if err != nil {
return 0, err
}
scanner := bufio.NewScanner(strings.NewReader(string(data)))
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "cpu ") {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
return 0, errors.New("invalid /proc/stat cpu line")
}
var total uint64
for _, field := range fields[1:] {
value, err := strconv.ParseUint(field, 10, 64)
if err != nil {
return 0, err
}
total += value
}
return total, nil
}
return 0, errors.New("cpu line not found")
}
func pagesToBytes(pages uint64) uint64 {
pageSize := uint64(os.Getpagesize())
return pages * pageSize
}
func getCPUCount() int {
count := os.Getenv("NUMBER_OF_PROCESSORS")
if count != "" {
if n, err := strconv.Atoi(count); err == nil && n > 0 {
return n
}
}
// Linux: use /proc/cpuinfo.
file, err := os.Open("/proc/cpuinfo")
if err != nil {
return 1
}
defer file.Close()
scanner := bufio.NewScanner(file)
cpus := 0
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "processor") {
cpus++
}
}
if cpus == 0 {
return 1
}
return cpus
}
func printHeaders() {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", "Name", "Processes", "CPU", "RAM", "Swap")
}
func printResult(keyword string, result Result) {
// fmt.Printf("%s\n", keyword)
// fmt.Printf(" Processes : %d\n", result.ProcessCount)
// fmt.Printf(" CPU : %.1f%%\n", result.CPUPercent)
// fmt.Printf(" RAM : %s\n", formatBytes(result.RAMBytes))
// fmt.Printf(" Swap : %s\n", formatBytes(result.SwapBytes))
// fmt.Printf("\n")
// fmt.Printf("\n")
fmt.Fprintf(w, "%s\t%d\t%.1f\t%s\t%s\n", keyword, result.ProcessCount, result.CPUPercent, formatBytes(result.RAMBytes), formatBytes(result.SwapBytes))
}
func formatBytes(bytes uint64) string {
const (
KB = 1024
MB = 1024 * KB
GB = 1024 * MB
)
switch {
case bytes >= GB:
return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GB))
case bytes >= MB:
return fmt.Sprintf("%.0f MB", float64(bytes)/float64(MB))
case bytes >= KB:
return fmt.Sprintf("%.0f KB", float64(bytes)/float64(KB))
default:
return fmt.Sprintf("%d B", bytes)
}
}