Skip to content

Commit

Permalink
YAML listing and processes
Browse files Browse the repository at this point in the history
  • Loading branch information
tuxudo committed Jan 31, 2024
1 parent 9ca8a11 commit 817b23f
Show file tree
Hide file tree
Showing 11 changed files with 337 additions and 275 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ Table Schema
* gpu_busy - FLOAT - GPU cycles used
* kern_bootargs - VARCHAT(255) - Boot flags used by the kernel on last boot
* clusters - mediumtext - JSON of CPU cluster activity, Apple Silicon only
* processes - mediumtext - JSON of processes running on the Mac
* cpu_idle - varchar(255) - CPU idle percent
* cpu_sys - varchar(255) - CPU used by system
* cpu_user - varchar(255) - CPU used by user
* load_avg - varchar(255) - Load average

79 changes: 79 additions & 0 deletions js/usage_stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
var format_usage_stats_thermal_pressure = function(colNumber, row){
var col = $('td:eq('+colNumber+')', row),
colvar = col.text();
colvar = colvar == 'Heavy' ? '<span class="label label-danger">Heavy</span>' :
colvar = colvar == 'Moderate' ? '<span class="label label-warning">Moderate</span>' :
(colvar === 'Nominal' ? '<span class="label label-success">Nominal</span>' :
colvar = colvar)
col.html(colvar)
}

var format_usage_stats_byte_rate = function(colNumber, row){
var col = $('td:eq('+colNumber+')', row),
colvar = col.text();
if (colvar > 0){
col.text(fileSize(parseFloat(colvar), 2)+'/s')
} else {
col.text("")
}
}

var format_usage_stats_byte_size = function(colNumber, row){
var col = $('td:eq('+colNumber+')', row),
colvar = col.text();
if (colvar > 0){
col.text(fileSize(parseFloat(colvar), 2))
} else {
col.text("")
}
}

var format_usage_stats_rate = function(colNumber, row){
var col = $('td:eq('+colNumber+')', row),
colvar = col.text();
if (colvar > 0){
col.text(parseFloat(colvar).toFixed(2)+'/s')
} else {
col.text("")
}
}

var format_usage_stats_watts = function(colNumber, row){
var col = $('td:eq('+colNumber+')', row),
colvar = col.text();
if (colvar > 0){
col.text(parseFloat(colvar).toFixed(2)+' Watts')
} else {
col.text("")
}
}

var format_usage_stats_mhz = function(colNumber, row){
var col = $('td:eq('+colNumber+')', row),
colvar = col.text();
if (colvar > 0){
col.text(((parseFloat(colvar)/1000000).toFixed(2))+'Mhz')
} else {
col.text("")
}
}

var format_usage_stats_ghz = function(colNumber, row){
var col = $('td:eq('+colNumber+')', row),
colvar = col.text();
if (colvar > 0){
col.text(((parseFloat(colvar)/1000000000).toFixed(2))+'Ghz')
} else {
col.text("")
}
}

var format_usage_stats_ratio = function(colNumber, row){
var col = $('td:eq('+colNumber+')', row),
colvar = col.text();
if (colvar > 0){
col.text((parseFloat(colvar*100)).toFixed(2)+'%')
} else {
col.text("")
}
}
14 changes: 13 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,17 @@
"gpu_freq_ratio": "GPU Fraction of Nominal Speed",
"gpu_busy": "GPU Used",
"per_second": "per Second",
"per_second_short": "/s"
"per_second_short": "/s",
"processes": "Processes",
"cpu_idle": "CPU Idle",
"cpu_sys": "CPU System",
"cpu_user": "CPU User",
"load_avg": "Load Average",
"pid": "PID",
"cpu": "CPU Usage",
"memp": "Memory Usage",
"usr": "User",
"mem": "Memory Amount",
"proc": "Process",
"path": "Process Path"
}
33 changes: 33 additions & 0 deletions migrations/2024_02_01_000001_usage_stats_processes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Capsule\Manager as Capsule;

class UsageStatsProcesses extends Migration
{
private $tableName = 'usage_stats';

public function up()
{
$capsule = new Capsule();
$capsule::schema()->table($this->tableName, function (Blueprint $table) {
$table->mediumText('processes')->nullable();
$table->string('cpu_idle')->nullable();
$table->string('cpu_sys')->nullable();
$table->string('cpu_user')->nullable();
$table->string('load_avg')->nullable();
});
}

public function down()
{
$capsule = new Capsule();
$capsule::schema()->table($this->tableName, function (Blueprint $table) {
$table->dropColumn('processes');
$table->dropColumn('cpu_idle');
$table->dropColumn('cpu_sys');
$table->dropColumn('cpu_user');
$table->dropColumn('load_avg');
});
}
}
4 changes: 4 additions & 0 deletions provides.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
client_tabs:
processes-tab:
view: processes_tab
i18n: usage_stats.processes
badge: usage_stats_processes-cnt
usage_stats-tab:
view: usage_stats_tab
i18n: usage_stats.usage_stats
Expand Down
99 changes: 95 additions & 4 deletions scripts/usage_stats
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
#!/usr/local/munkireport/munkireport-python3
# Script by Bochoven and Tuxudo

import os, sys, plistlib, subprocess, platform, json
import os, sys, plistlib, subprocess, platform, json, math, re

def get_usage_metrics():
cmd = ['/usr/bin/powermetrics', '--show-initial-usage', ' -s', "network,disk", '-f', 'plist', '-n', '0']
cmd = ['/usr/bin/powermetrics', '--show-initial-usage', ' -s', 'network,disk', '-f', 'plist', '-n', '0']

proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()

# Need to remove a broken key "<key>all_tasks</key>" that Python doesn't like
output = output.decode().replace("<key>all_tasks</key>","")
output = output.decode().replace("<key>all_tasks</key>","").replace("&","and")

try:
plist = plistlib.readPlistFromString(output.rstrip(' \t\r\n\0').strip())
Expand All @@ -31,12 +31,13 @@ def get_usage_metrics():
try:
plist.update(plistlib.readPlistFromString(output.rstrip(' \t\r\n\0')))
except AttributeError as e:
plist.update(plistlib.loads(output.decode().rstrip(' \t\r\n\0').encode()))
plist.update(plistlib.loads(output.decode().rstrip(' \t\r\n\0').replace("&","and").encode()))

return plist

def parse_usage_plist(plist):
usage_info = {}

for item in plist:
if item == 'timestamp':
try:
Expand Down Expand Up @@ -298,6 +299,94 @@ def process_clusters(cpu_clusters):

return cpu_metric_dict

def get_processes():

cmd = ['/bin/ps', '-eo', 'pid,pcpu,pmem,user,rss,comm']

proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()

arr = []
for line in output.decode("utf-8", errors="ignore").split('\n'):

# Don't process the header
if "%CPU" in line:
continue

# Split at space
fields = line.split(" ")

# Break at the blank/empty row
if len(fields) < 2:
break

# Remove empty columns
fields = [i for a,i in enumerate(fields) if i!='']

# Minimize CPU and memory usage percent
if fields[1] == "0.0":
fields[1] = "0"
if fields[2] == "0.0":
fields[2] = "0"

# Process memory usage
memory_k = int(fields[4])
if memory_k > 100:
size_name = ("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(memory_k, 1024)))
p = math.pow(1024, i)
s = round(memory_k / p, 2)
fields[4] = "%s %s" % (s, size_name[i])
else:
fields[4] = ""

# Rejoin process path
if len(fields) > 6:
while len(fields) > 6:
fields[5] = fields[5] + " " + fields[6]
fields.remove(fields[6])

fields[5] = fields[5].strip()

# Get the process name from process path
fields.append(fields[5].split("/")[-1])

# Build the entry
arr.append({
"pid": fields[0],
"cpu": fields[1],
"memp": fields[2],
"usr": fields[3],
"mem": fields[4],
"proc": fields[6],
"path": fields[5],
})

return json.dumps(arr)

def get_cpu_usage():
cmd = ['/usr/bin/top', '-l', '1', '-stats', 'pid']

proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()

out = {}

for line in output.decode("utf-8", errors="ignore").split('\n'):

if "Load Avg:" in line:
out["load_avg"] = re.sub('Load Avg: ', '', line).strip()
elif "CPU usage: " in line:
cpu_usage = re.sub('CPU usage: ', '', line).split(', ')
out["cpu_user"] = re.sub(' user', '', cpu_usage[0]).strip()
out["cpu_sys"] = re.sub(' sys', '', cpu_usage[1]).strip()
out["cpu_idle"] = re.sub(' idle ', '', cpu_usage[2]).strip()
return out

def getDarwinVersion():
"""Returns the Darwin version."""
# Catalina -> 10.15.7 -> 19.6.0 -> 19
Expand All @@ -316,6 +405,8 @@ def main():
result = dict()
info = get_usage_metrics()
result = parse_usage_plist(info)
result.update({'processes': get_processes()})
result.update(get_cpu_usage())

# Check if Apple Silicon Mac
if "arm" in os.uname()[3].lower():
Expand Down
9 changes: 7 additions & 2 deletions usage_stats_model.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ function __construct($serial='')
$this->rs['gpu_busy'] = 0.0;
$this->rs['kern_bootargs'] = "";
$this->rs['clusters'] = null;
$this->rs['processes'] = null;
$this->rs['cpu_idle'] = null;
$this->rs['cpu_sys'] = null;
$this->rs['cpu_user'] = null;
$this->rs['load_avg'] = null;

if ($serial) {
$this->retrieve_record($serial);
Expand Down Expand Up @@ -69,8 +74,8 @@ function process($plist)
$parser = new CFPropertyList();
$parser->parse($plist, CFPropertyList::FORMAT_XML);
$plist = $parser->toArray();
$fields = array('timestamp','thermal_pressure','backlight_max','backlight_min','backlight','keyboard_backlight','ibyte_rate','ibytes','ipacket_rate','ipackets','obyte_rate','obytes','opacket_rate','opackets','rbytes_per_s','rops_per_s','wbytes_per_s','wops_per_s','rbytes_diff','rops_diff','wbytes_diff','wops_diff','package_watts','package_joules','freq_hz','freq_ratio','gpu_name','gpu_freq_hz','gpu_freq_mhz','gpu_freq_ratio','gpu_busy','kern_bootargs','clusters');

$fields = array('timestamp','thermal_pressure','backlight_max','backlight_min','backlight','keyboard_backlight','ibyte_rate','ibytes','ipacket_rate','ipackets','obyte_rate','obytes','opacket_rate','opackets','rbytes_per_s','rops_per_s','wbytes_per_s','wops_per_s','rbytes_diff','rops_diff','wbytes_diff','wops_diff','package_watts','package_joules','freq_hz','freq_ratio','gpu_name','gpu_freq_hz','gpu_freq_mhz','gpu_freq_ratio','gpu_busy','kern_bootargs','clusters','processes','cpu_idle','cpu_sys','cpu_user','load_avg');

foreach ($fields as $field) {
// If key does not exist in $plist, null it
Expand Down
7 changes: 7 additions & 0 deletions views/processes_tab.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

<div id="processes"></div>
<h2 data-i18n="usage_stats.processes"></h2>

<div id="processes-msg" data-i18n="listing.loading" class="col-lg-12 text-center"></div>

<!--All the brains of this tab are a part of the usage_stats_tab-->
Loading

0 comments on commit 817b23f

Please sign in to comment.