Skip to content

Commit

Permalink
Merge pull request #41 from context-labs/development
Browse files Browse the repository at this point in the history
mactop v0.2.3
  • Loading branch information
metaspartan authored Dec 1, 2024
2 parents 0dd077b + 77fa4a6 commit 4d16e46
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 24 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- Processes list (sorted by CPU usage)
- Disk Storage (Used, Total, Available)
- Party Mode (Randomly cycles through colors)
- Optional Prometheus Metrics server (default is disabled)
- Support for all Apple Silicon models.

## Install via Homebrew
Expand Down Expand Up @@ -91,6 +92,7 @@ sudo mactop --interval 1000 --color green
- `--interval` or `-i`: Set the powermetrics update interval in milliseconds. Default is 1000. (For low-end M chips, you may want to increase this value)
- `--color` or `-c`: Set the UI color. Default is white.
Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'. (-c green)
- `--prometheus` or `-p`: Set and enable the local Prometheus metrics server on the given port. Default is disabled. (e.g. -p 2112 to enable Prometheus metrics on port 2112)
- `--version` or `-v`: Print the version of mactop.
- `--help` or `-h`: Show a help message about these flags and how to run mactop.

Expand Down
13 changes: 11 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,22 @@ require (
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/sys v0.22.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
21 changes: 21 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
Expand Down
190 changes: 168 additions & 22 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ import (
"time"
"unsafe"

"net/http"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"

ui "github.com/gizak/termui/v3"
w "github.com/gizak/termui/v3/widgets"
"github.com/shirou/gopsutil/mem"
"howett.net/plist"
)

var (
version = "v0.2.2"
version = "v0.2.3"
cpuGauge, gpuGauge, memoryGauge *w.Gauge
modelText, PowerChart, NetworkInfo, helpText *w.Paragraph
grid *ui.Grid
Expand Down Expand Up @@ -67,9 +72,69 @@ var (
maxPowerSeen = 0.1
powerHistory = make([]float64, 100)
maxPower = 0.0 // Track maximum power for better scaling
gpuValues = make([]float64, 65)
gpuValues = make([]float64, 100)
prometheusPort string
)

var (
// Prometheus metrics
cpuUsage = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "mactop_cpu_usage_percent",
Help: "Current Total CPU usage percentage",
},
)

gpuUsage = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "mactop_gpu_usage_percent",
Help: "Current GPU usage percentage",
},
)

gpuFreqMHz = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "mactop_gpu_freq_mhz",
Help: "Current GPU frequency in MHz",
},
)

powerUsage = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "mactop_power_watts",
Help: "Current power usage in watts",
},
[]string{"component"}, // "cpu", "gpu", "total"
)

memoryUsage = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "mactop_memory_gb",
Help: "Memory usage in GB",
},
[]string{"type"}, // "used", "total", "swap_used", "swap_total"
)
)

func startPrometheusServer(port string) {
registry := prometheus.NewRegistry()
registry.MustRegister(cpuUsage)
registry.MustRegister(gpuUsage)
registry.MustRegister(gpuFreqMHz)
registry.MustRegister(powerUsage)
registry.MustRegister(memoryUsage)

handler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})

http.Handle("/metrics", handler)
go func() {
err := http.ListenAndServe(":"+port, nil)
if err != nil {
stderrLogger.Printf("Failed to start Prometheus metrics server: %v\n", err)
}
}()
}

type CPUUsage struct {
User float64
System float64
Expand Down Expand Up @@ -364,7 +429,31 @@ func setupUI() {
pCoreCount,
gpuCoreCount,
)
helpText.Text = "mactop is open source monitoring tool for Apple Silicon authored by Carsen Klock in Go Lang!\n\nRepo: github.com/context-labs/mactop\n\nControls:\n- r: Refresh the UI data manually\n- c: Cycle through UI color themes\n- p: Toggle party mode (color cycling)\n- l: Toggle the main display's layout\n- h or ?: Toggle this help menu\n- q or <C-c>: Quit the application\n\nStart Flags:\n--help, -h: Show this help menu\n--version, -v: Show the version of mactop\n--interval, -i: Set the powermetrics update interval in milliseconds. Default is 1000.\n--color, -c: Set the UI color. Default is none. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'.\n\nVersion: " + version
prometheusStatus := "Disabled"
if prometheusPort != "" {
prometheusStatus = fmt.Sprintf("Enabled (Port: %s)", prometheusPort)
}
helpText.Text = fmt.Sprintf(
"mactop is open source monitoring tool for Apple Silicon authored by Carsen Klock in Go Lang!\n\n"+
"Repo: github.com/context-labs/mactop\n\n"+
"Prometheus Metrics: %s\n\n"+
"Controls:\n"+
"- r: Refresh the UI data manually\n"+
"- c: Cycle through UI color themes\n"+
"- p: Toggle party mode (color cycling)\n"+
"- l: Toggle the main display's layout\n"+
"- h or ?: Toggle this help menu\n"+
"- q or <C-c>: Quit the application\n\n"+
"Start Flags:\n"+
"--help, -h: Show this help menu\n"+
"--version, -v: Show the version of mactop\n"+
"--interval, -i: Set the powermetrics update interval in milliseconds. Default is 1000.\n"+
"--prometheus, -p: Set and enable a Prometheus metrics port. Default is none. (e.g. --prometheus=9090)\n"+
"--color, -c: Set the UI color. Default is none. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'.\n\n"+
"Version: %s",
prometheusStatus,
version,
)
stderrLogger.Printf("Model: %s\nE-Core Count: %d\nP-Core Count: %d\nGPU Core Count: %s", modelName, eCoreCount, pCoreCount, gpuCoreCount)

processList = w.NewList()
Expand Down Expand Up @@ -392,8 +481,9 @@ func setupUI() {

termWidth, _ := ui.TerminalDimensions()
numPoints := (termWidth / 2) / 2
numPointsGPU := (termWidth / 2)
powerValues = make([]float64, numPoints)
gpuValues = make([]float64, numPoints)
gpuValues = make([]float64, numPointsGPU)

sparkline = w.NewSparkline()
sparkline.LineColor = ui.ColorGreen
Expand All @@ -404,7 +494,7 @@ func setupUI() {

gpuSparkline = w.NewSparkline()
gpuSparkline.LineColor = ui.ColorGreen
gpuSparkline.MaxHeight = 10
gpuSparkline.MaxHeight = 100
gpuSparkline.Data = gpuValues
gpuSparklineGroup = w.NewSparklineGroup(gpuSparkline)
gpuSparklineGroup.Title = "GPU Usage History"
Expand Down Expand Up @@ -432,7 +522,7 @@ func setupGrid() {
grid.Set(
ui.NewRow(1.0/4,
ui.NewCol(1.0, cpuGauge),
// ui.NewCol(1.0/2, gpuSparklineGroup),
// ui.NewCol(1.0/2, gpuGauge),
),
ui.NewRow(2.0/4,
ui.NewCol(1.0/2,
Expand Down Expand Up @@ -818,6 +908,14 @@ func cycleColors() {
sparklineGroup.BorderStyle = ui.NewStyle(color)
sparklineGroup.TitleStyle = ui.NewStyle(color)
}
if gpuSparkline != nil {
gpuSparkline.LineColor = color
gpuSparkline.TitleStyle = ui.NewStyle(color)
}
if gpuSparklineGroup != nil {
gpuSparklineGroup.BorderStyle = ui.NewStyle(color)
gpuSparklineGroup.TitleStyle = ui.NewStyle(color)
}

cpuCoreWidget.BorderStyle.Fg, cpuCoreWidget.TitleStyle.Fg = color, color
processList.TextStyle = ui.NewStyle(color)
Expand Down Expand Up @@ -858,6 +956,14 @@ func main() {
fmt.Println("Error: --color flag requires a color value")
os.Exit(1)
}
case "--prometheus", "-p":
if i+1 < len(os.Args) {
prometheusPort = os.Args[i+1]
i++
} else {
fmt.Println("Error: --prometheus flag requires a port number")
os.Exit(1)
}
case "--interval", "-i":
if i+1 < len(os.Args) {
interval, err = strconv.Atoi(os.Args[i+1])
Expand Down Expand Up @@ -889,6 +995,11 @@ func main() {
}
defer ui.Close()
StderrToLogfile(logfile)

if prometheusPort != "" {
startPrometheusServer(prometheusPort)
stderrLogger.Printf("Prometheus metrics available at http://localhost:%s/metrics\n", prometheusPort)
}
if setColor {
var color ui.Color
switch colorName {
Expand Down Expand Up @@ -1060,12 +1171,25 @@ func collectMetrics(done chan struct{}, cpumetricsChan chan CPUMetrics, gpumetri
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
stderrLogger.Fatal(err)
}
if err := cmd.Start(); err != nil {
log.Fatal(err)
stderrLogger.Fatal(err)
}
scanner := bufio.NewScanner(stdout)

defer func() {
if err := cmd.Process.Kill(); err != nil {
stderrLogger.Fatalf("ERROR: Failed to kill powermetrics: %v", err)
}
}()

// Create buffered reader with larger buffer
const bufferSize = 10 * 1024 * 1024 // 10MB
reader := bufio.NewReaderSize(stdout, bufferSize)

scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, bufferSize), bufferSize)

scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
Expand All @@ -1080,27 +1204,36 @@ func collectMetrics(done chan struct{}, cpumetricsChan chan CPUMetrics, gpumetri
}
}
if atEOF {
if start >= 0 {
return len(data), data[start:], nil
}
return len(data), nil, nil
}
return 0, nil, nil
})
retryCount := 0
maxRetries := 3
for scanner.Scan() {
plistData := scanner.Text()
if !strings.Contains(plistData, "<?xml") || !strings.Contains(plistData, "</plist>") {
continue
}
var data map[string]interface{}
err := plist.NewDecoder(strings.NewReader(plistData)).Decode(&data)
if err != nil {
log.Printf("Error decoding plist: %v", err)
continue
}
select {
case <-done:
cmd.Process.Kill()
return
default:
// Send all metrics at once
plistData := scanner.Text()
if !strings.Contains(plistData, "<?xml") || !strings.Contains(plistData, "</plist>") {
retryCount++
if retryCount >= maxRetries {
retryCount = 0
continue
}
continue
}
retryCount = 0 // Reset retry counter on successful parse
var data map[string]interface{}
err := plist.NewDecoder(strings.NewReader(plistData)).Decode(&data)
if err != nil {
stderrLogger.Printf("Error decoding plist: %v", err)
continue
}
cpuMetrics := parseCPUMetrics(data, NewCPUMetrics())
gpuMetrics := parseGPUMetrics(data)
netdiskMetrics := parseNetDiskMetrics(data)
Expand Down Expand Up @@ -1271,6 +1404,16 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
memoryMetrics := getMemoryMetrics()
memoryGauge.Title = fmt.Sprintf("Memory Usage: %.2f GB / %.2f GB (Swap: %.2f/%.2f GB)", float64(memoryMetrics.Used)/1024/1024/1024, float64(memoryMetrics.Total)/1024/1024/1024, float64(memoryMetrics.SwapUsed)/1024/1024/1024, float64(memoryMetrics.SwapTotal)/1024/1024/1024)
memoryGauge.Percent = int((float64(memoryMetrics.Used) / float64(memoryMetrics.Total)) * 100)

cpuUsage.Set(float64(totalUsage))
powerUsage.With(prometheus.Labels{"component": "cpu"}).Set(cpuMetrics.CPUW)
powerUsage.With(prometheus.Labels{"component": "total"}).Set(cpuMetrics.PackageW)
powerUsage.With(prometheus.Labels{"component": "gpu"}).Set(cpuMetrics.GPUW)

memoryUsage.With(prometheus.Labels{"type": "used"}).Set(float64(memoryMetrics.Used) / 1024 / 1024 / 1024)
memoryUsage.With(prometheus.Labels{"type": "total"}).Set(float64(memoryMetrics.Total) / 1024 / 1024 / 1024)
memoryUsage.With(prometheus.Labels{"type": "swap_used"}).Set(float64(memoryMetrics.SwapUsed) / 1024 / 1024 / 1024)
memoryUsage.With(prometheus.Labels{"type": "swap_total"}).Set(float64(memoryMetrics.SwapTotal) / 1024 / 1024 / 1024)
}

func updateGPUUI(gpuMetrics GPUMetrics) {
Expand Down Expand Up @@ -1299,7 +1442,10 @@ func updateGPUUI(gpuMetrics GPUMetrics) {

gpuSparkline.Data = gpuValues
gpuSparkline.MaxVal = 100 // GPU usage is 0-100%
gpuSparklineGroup.Title = fmt.Sprintf("GPU: %d%% (Avg: %.1f%%)", gpuMetrics.Active, avgGPU)
gpuSparklineGroup.Title = fmt.Sprintf("GPU History: %d%% (Avg: %.1f%%)", gpuMetrics.Active, avgGPU)

gpuUsage.Set(float64(gpuMetrics.Active))
gpuFreqMHz.Set(float64(gpuMetrics.FreqMHz))
}

func getDiskStorage() (total, used, available string) {
Expand Down

0 comments on commit 4d16e46

Please sign in to comment.