@@ -29,14 +29,19 @@ import (
2929	"time" 
3030	"unsafe" 
3131
32+ 	"net/http" 
33+ 
34+ 	"github.com/prometheus/client_golang/prometheus" 
35+ 	"github.com/prometheus/client_golang/prometheus/promhttp" 
36+ 
3237	ui "github.com/gizak/termui/v3" 
3338	w "github.com/gizak/termui/v3/widgets" 
3439	"github.com/shirou/gopsutil/mem" 
3540	"howett.net/plist" 
3641)
3742
3843var  (
39- 	version                                       =  "v0.2.2 " 
44+ 	version                                       =  "v0.2.3 " 
4045	cpuGauge , gpuGauge , memoryGauge               * w.Gauge 
4146	modelText , PowerChart , NetworkInfo , helpText  * w.Paragraph 
4247	grid                                          * ui.Grid 
6772	maxPowerSeen                                  =  0.1 
6873	powerHistory                                  =  make ([]float64 , 100 )
6974	maxPower                                      =  0.0  // Track maximum power for better scaling 
70- 	gpuValues                                     =  make ([]float64 , 65 )
75+ 	gpuValues                                     =  make ([]float64 , 100 )
76+ 	prometheusPort                                string 
77+ )
78+ 
79+ var  (
80+ 	// Prometheus metrics 
81+ 	cpuUsage  =  prometheus .NewGauge (
82+ 		prometheus.GaugeOpts {
83+ 			Name : "mactop_cpu_usage_percent" ,
84+ 			Help : "Current Total CPU usage percentage" ,
85+ 		},
86+ 	)
87+ 
88+ 	gpuUsage  =  prometheus .NewGauge (
89+ 		prometheus.GaugeOpts {
90+ 			Name : "mactop_gpu_usage_percent" ,
91+ 			Help : "Current GPU usage percentage" ,
92+ 		},
93+ 	)
94+ 
95+ 	gpuFreqMHz  =  prometheus .NewGauge (
96+ 		prometheus.GaugeOpts {
97+ 			Name : "mactop_gpu_freq_mhz" ,
98+ 			Help : "Current GPU frequency in MHz" ,
99+ 		},
100+ 	)
101+ 
102+ 	powerUsage  =  prometheus .NewGaugeVec (
103+ 		prometheus.GaugeOpts {
104+ 			Name : "mactop_power_watts" ,
105+ 			Help : "Current power usage in watts" ,
106+ 		},
107+ 		[]string {"component" }, // "cpu", "gpu", "total" 
108+ 	)
109+ 
110+ 	memoryUsage  =  prometheus .NewGaugeVec (
111+ 		prometheus.GaugeOpts {
112+ 			Name : "mactop_memory_gb" ,
113+ 			Help : "Memory usage in GB" ,
114+ 		},
115+ 		[]string {"type" }, // "used", "total", "swap_used", "swap_total" 
116+ 	)
71117)
72118
119+ func  startPrometheusServer (port  string ) {
120+ 	registry  :=  prometheus .NewRegistry ()
121+ 	registry .MustRegister (cpuUsage )
122+ 	registry .MustRegister (gpuUsage )
123+ 	registry .MustRegister (gpuFreqMHz )
124+ 	registry .MustRegister (powerUsage )
125+ 	registry .MustRegister (memoryUsage )
126+ 
127+ 	handler  :=  promhttp .HandlerFor (registry , promhttp.HandlerOpts {})
128+ 
129+ 	http .Handle ("/metrics" , handler )
130+ 	go  func () {
131+ 		err  :=  http .ListenAndServe (":" + port , nil )
132+ 		if  err  !=  nil  {
133+ 			stderrLogger .Printf ("Failed to start Prometheus metrics server: %v\n " , err )
134+ 		}
135+ 	}()
136+ }
137+ 
73138type  CPUUsage  struct  {
74139	User    float64 
75140	System  float64 
@@ -364,7 +429,31 @@ func setupUI() {
364429		pCoreCount ,
365430		gpuCoreCount ,
366431	)
367- 	helpText .Text  =  "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 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 --color, -c: Set the UI color. Default is none. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'.\n \n Version: "  +  version 
432+ 	prometheusStatus  :=  "Disabled" 
433+ 	if  prometheusPort  !=  ""  {
434+ 		prometheusStatus  =  fmt .Sprintf ("Enabled (Port: %s)" , prometheusPort )
435+ 	}
436+ 	helpText .Text  =  fmt .Sprintf (
437+ 		"mactop is open source monitoring tool for Apple Silicon authored by Carsen Klock in Go Lang!\n \n " + 
438+ 			"Repo: github.com/context-labs/mactop\n \n " + 
439+ 			"Prometheus Metrics: %s\n \n " + 
440+ 			"Controls:\n " + 
441+ 			"- r: Refresh the UI data manually\n " + 
442+ 			"- c: Cycle through UI color themes\n " + 
443+ 			"- p: Toggle party mode (color cycling)\n " + 
444+ 			"- l: Toggle the main display's layout\n " + 
445+ 			"- h or ?: Toggle this help menu\n " + 
446+ 			"- q or <C-c>: Quit the application\n \n " + 
447+ 			"Start Flags:\n " + 
448+ 			"--help, -h: Show this help menu\n " + 
449+ 			"--version, -v: Show the version of mactop\n " + 
450+ 			"--interval, -i: Set the powermetrics update interval in milliseconds. Default is 1000.\n " + 
451+ 			"--prometheus, -p: Set and enable a Prometheus metrics port. Default is none. (e.g. --prometheus=9090)\n " + 
452+ 			"--color, -c: Set the UI color. Default is none. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'.\n \n " + 
453+ 			"Version: %s" ,
454+ 		prometheusStatus ,
455+ 		version ,
456+ 	)
368457	stderrLogger .Printf ("Model: %s\n E-Core Count: %d\n P-Core Count: %d\n GPU Core Count: %s" , modelName , eCoreCount , pCoreCount , gpuCoreCount )
369458
370459	processList  =  w .NewList ()
@@ -392,8 +481,9 @@ func setupUI() {
392481
393482	termWidth , _  :=  ui .TerminalDimensions ()
394483	numPoints  :=  (termWidth  /  2 ) /  2 
484+ 	numPointsGPU  :=  (termWidth  /  2 )
395485	powerValues  =  make ([]float64 , numPoints )
396- 	gpuValues  =  make ([]float64 , numPoints )
486+ 	gpuValues  =  make ([]float64 , numPointsGPU )
397487
398488	sparkline  =  w .NewSparkline ()
399489	sparkline .LineColor  =  ui .ColorGreen 
@@ -404,7 +494,7 @@ func setupUI() {
404494
405495	gpuSparkline  =  w .NewSparkline ()
406496	gpuSparkline .LineColor  =  ui .ColorGreen 
407- 	gpuSparkline .MaxHeight  =  10 
497+ 	gpuSparkline .MaxHeight  =  100 
408498	gpuSparkline .Data  =  gpuValues 
409499	gpuSparklineGroup  =  w .NewSparklineGroup (gpuSparkline )
410500	gpuSparklineGroup .Title  =  "GPU Usage History" 
@@ -432,7 +522,7 @@ func setupGrid() {
432522	grid .Set (
433523		ui .NewRow (1.0 / 4 ,
434524			ui .NewCol (1.0 , cpuGauge ),
435- 			// ui.NewCol(1.0/2, gpuSparklineGroup ), 
525+ 			// ui.NewCol(1.0/2, gpuGauge ), 
436526		),
437527		ui .NewRow (2.0 / 4 ,
438528			ui .NewCol (1.0 / 2 ,
@@ -818,6 +908,14 @@ func cycleColors() {
818908		sparklineGroup .BorderStyle  =  ui .NewStyle (color )
819909		sparklineGroup .TitleStyle  =  ui .NewStyle (color )
820910	}
911+ 	if  gpuSparkline  !=  nil  {
912+ 		gpuSparkline .LineColor  =  color 
913+ 		gpuSparkline .TitleStyle  =  ui .NewStyle (color )
914+ 	}
915+ 	if  gpuSparklineGroup  !=  nil  {
916+ 		gpuSparklineGroup .BorderStyle  =  ui .NewStyle (color )
917+ 		gpuSparklineGroup .TitleStyle  =  ui .NewStyle (color )
918+ 	}
821919
822920	cpuCoreWidget .BorderStyle .Fg , cpuCoreWidget .TitleStyle .Fg  =  color , color 
823921	processList .TextStyle  =  ui .NewStyle (color )
@@ -858,6 +956,14 @@ func main() {
858956				fmt .Println ("Error: --color flag requires a color value" )
859957				os .Exit (1 )
860958			}
959+ 		case  "--prometheus" , "-p" :
960+ 			if  i + 1  <  len (os .Args ) {
961+ 				prometheusPort  =  os .Args [i + 1 ]
962+ 				i ++ 
963+ 			} else  {
964+ 				fmt .Println ("Error: --prometheus flag requires a port number" )
965+ 				os .Exit (1 )
966+ 			}
861967		case  "--interval" , "-i" :
862968			if  i + 1  <  len (os .Args ) {
863969				interval , err  =  strconv .Atoi (os .Args [i + 1 ])
@@ -889,6 +995,11 @@ func main() {
889995	}
890996	defer  ui .Close ()
891997	StderrToLogfile (logfile )
998+ 
999+ 	if  prometheusPort  !=  ""  {
1000+ 		startPrometheusServer (prometheusPort )
1001+ 		stderrLogger .Printf ("Prometheus metrics available at http://localhost:%s/metrics\n " , prometheusPort )
1002+ 	}
8921003	if  setColor  {
8931004		var  color  ui.Color 
8941005		switch  colorName  {
@@ -1060,12 +1171,25 @@ func collectMetrics(done chan struct{}, cpumetricsChan chan CPUMetrics, gpumetri
10601171	cmd .SysProcAttr  =  & syscall.SysProcAttr {Setpgid : true }
10611172	stdout , err  :=  cmd .StdoutPipe ()
10621173	if  err  !=  nil  {
1063- 		log .Fatal (err )
1174+ 		stderrLogger .Fatal (err )
10641175	}
10651176	if  err  :=  cmd .Start (); err  !=  nil  {
1066- 		log .Fatal (err )
1177+ 		stderrLogger .Fatal (err )
10671178	}
1068- 	scanner  :=  bufio .NewScanner (stdout )
1179+ 
1180+ 	defer  func () {
1181+ 		if  err  :=  cmd .Process .Kill (); err  !=  nil  {
1182+ 			stderrLogger .Fatalf ("ERROR: Failed to kill powermetrics: %v" , err )
1183+ 		}
1184+ 	}()
1185+ 
1186+ 	// Create buffered reader with larger buffer 
1187+ 	const  bufferSize  =  10  *  1024  *  1024  // 10MB 
1188+ 	reader  :=  bufio .NewReaderSize (stdout , bufferSize )
1189+ 
1190+ 	scanner  :=  bufio .NewScanner (reader )
1191+ 	scanner .Buffer (make ([]byte , bufferSize ), bufferSize )
1192+ 
10691193	scanner .Split (func (data  []byte , atEOF  bool ) (advance  int , token  []byte , err  error ) {
10701194		if  atEOF  &&  len (data ) ==  0  {
10711195			return  0 , nil , nil 
@@ -1080,27 +1204,36 @@ func collectMetrics(done chan struct{}, cpumetricsChan chan CPUMetrics, gpumetri
10801204			}
10811205		}
10821206		if  atEOF  {
1207+ 			if  start  >=  0  {
1208+ 				return  len (data ), data [start :], nil 
1209+ 			}
10831210			return  len (data ), nil , nil 
10841211		}
10851212		return  0 , nil , nil 
10861213	})
1214+ 	retryCount  :=  0 
1215+ 	maxRetries  :=  3 
10871216	for  scanner .Scan () {
1088- 		plistData  :=  scanner .Text ()
1089- 		if  ! strings .Contains (plistData , "<?xml" ) ||  ! strings .Contains (plistData , "</plist>" ) {
1090- 			continue 
1091- 		}
1092- 		var  data  map [string ]interface {}
1093- 		err  :=  plist .NewDecoder (strings .NewReader (plistData )).Decode (& data )
1094- 		if  err  !=  nil  {
1095- 			log .Printf ("Error decoding plist: %v" , err )
1096- 			continue 
1097- 		}
10981217		select  {
10991218		case  <- done :
1100- 			cmd .Process .Kill ()
11011219			return 
11021220		default :
1103- 			// Send all metrics at once 
1221+ 			plistData  :=  scanner .Text ()
1222+ 			if  ! strings .Contains (plistData , "<?xml" ) ||  ! strings .Contains (plistData , "</plist>" ) {
1223+ 				retryCount ++ 
1224+ 				if  retryCount  >=  maxRetries  {
1225+ 					retryCount  =  0 
1226+ 					continue 
1227+ 				}
1228+ 				continue 
1229+ 			}
1230+ 			retryCount  =  0  // Reset retry counter on successful parse 
1231+ 			var  data  map [string ]interface {}
1232+ 			err  :=  plist .NewDecoder (strings .NewReader (plistData )).Decode (& data )
1233+ 			if  err  !=  nil  {
1234+ 				stderrLogger .Printf ("Error decoding plist: %v" , err )
1235+ 				continue 
1236+ 			}
11041237			cpuMetrics  :=  parseCPUMetrics (data , NewCPUMetrics ())
11051238			gpuMetrics  :=  parseGPUMetrics (data )
11061239			netdiskMetrics  :=  parseNetDiskMetrics (data )
@@ -1271,6 +1404,16 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
12711404	memoryMetrics  :=  getMemoryMetrics ()
12721405	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 )
12731406	memoryGauge .Percent  =  int ((float64 (memoryMetrics .Used ) /  float64 (memoryMetrics .Total )) *  100 )
1407+ 
1408+ 	cpuUsage .Set (float64 (totalUsage ))
1409+ 	powerUsage .With (prometheus.Labels {"component" : "cpu" }).Set (cpuMetrics .CPUW )
1410+ 	powerUsage .With (prometheus.Labels {"component" : "total" }).Set (cpuMetrics .PackageW )
1411+ 	powerUsage .With (prometheus.Labels {"component" : "gpu" }).Set (cpuMetrics .GPUW )
1412+ 
1413+ 	memoryUsage .With (prometheus.Labels {"type" : "used" }).Set (float64 (memoryMetrics .Used ) /  1024  /  1024  /  1024 )
1414+ 	memoryUsage .With (prometheus.Labels {"type" : "total" }).Set (float64 (memoryMetrics .Total ) /  1024  /  1024  /  1024 )
1415+ 	memoryUsage .With (prometheus.Labels {"type" : "swap_used" }).Set (float64 (memoryMetrics .SwapUsed ) /  1024  /  1024  /  1024 )
1416+ 	memoryUsage .With (prometheus.Labels {"type" : "swap_total" }).Set (float64 (memoryMetrics .SwapTotal ) /  1024  /  1024  /  1024 )
12741417}
12751418
12761419func  updateGPUUI (gpuMetrics  GPUMetrics ) {
@@ -1299,7 +1442,10 @@ func updateGPUUI(gpuMetrics GPUMetrics) {
12991442
13001443	gpuSparkline .Data  =  gpuValues 
13011444	gpuSparkline .MaxVal  =  100  // GPU usage is 0-100% 
1302- 	gpuSparklineGroup .Title  =  fmt .Sprintf ("GPU: %d%% (Avg: %.1f%%)" , gpuMetrics .Active , avgGPU )
1445+ 	gpuSparklineGroup .Title  =  fmt .Sprintf ("GPU History: %d%% (Avg: %.1f%%)" , gpuMetrics .Active , avgGPU )
1446+ 
1447+ 	gpuUsage .Set (float64 (gpuMetrics .Active ))
1448+ 	gpuFreqMHz .Set (float64 (gpuMetrics .FreqMHz ))
13031449}
13041450
13051451func  getDiskStorage () (total , used , available  string ) {
0 commit comments