shiny-cpu-info / app.py
elonmuskceo's picture
Upload 3 files
577d5c4
import sys
from psutil import cpu_count, cpu_percent
from math import ceil
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
shinylive_message = ""
# The agg matplotlib backend seems to be a little more efficient than the default when
# running on macOS, and also gives more consistent results across operating systems
matplotlib.use("agg")
# max number of samples to retain
MAX_SAMPLES = 1000
# secs between samples
SAMPLE_PERIOD = 1
ncpu = cpu_count(logical=True)
app_ui = ui.page_fluid(
ui.tags.style(
"""
/* Don't apply fade effect, it's constantly recalculating */
.recalculating {
opacity: 1;
}
tbody > tr:last-child {
/*border: 3px solid var(--bs-dark);*/
box-shadow:
0 0 2px 1px #fff, /* inner white */
0 0 4px 2px #0ff, /* middle cyan */
0 0 5px 3px #00f; /* outer blue */
}
#table table {
table-layout: fixed;
width: %s;
font-size: 0.8em;
}
th, td {
text-align: center;
}
"""
% f"{ncpu*4}em"
),
ui.h3("CPU Usage %", class_="mt-2"),
ui.layout_sidebar(
ui.panel_sidebar(
ui.input_select(
"cmap",
"Colormap",
{
"inferno": "inferno",
"viridis": "viridis",
"copper": "copper",
"prism": "prism (not recommended)",
},
),
ui.p(ui.input_action_button("reset", "Clear history", class_="btn-sm")),
ui.input_switch("hold", "Freeze output", value=False),
shinylive_message,
class_="mb-3",
),
ui.panel_main(
ui.div(
{"class": "card mb-3"},
ui.div(
{"class": "card-body"},
ui.h5({"class": "card-title mt-0"}, "Graphs"),
ui.output_plot("plot", height=f"{ncpu * 40}px"),
),
ui.div(
{"class": "card-footer"},
ui.input_numeric("sample_count", "Number of samples per graph", 50),
),
),
ui.div(
{"class": "card"},
ui.div(
{"class": "card-body"},
ui.h5({"class": "card-title m-0"}, "Heatmap"),
),
ui.div(
{"class": "card-body overflow-auto pt-0"},
ui.output_table("table"),
),
ui.div(
{"class": "card-footer"},
ui.input_numeric("table_rows", "Rows to display", 5),
),
),
),
),
)
@reactive.Calc
def cpu_current():
reactive.invalidate_later(SAMPLE_PERIOD)
return cpu_percent(percpu=True)
def server(input: Inputs, output: Outputs, session: Session):
cpu_history = reactive.Value(None)
@reactive.Calc
def cpu_history_with_hold():
# If "hold" is on, grab an isolated snapshot of cpu_history; if not, then do a
# regular read
if not input.hold():
return cpu_history()
else:
# Even if frozen, we still want to respond to input.reset()
input.reset()
with reactive.isolate():
return cpu_history()
@reactive.Effect
def collect_cpu_samples():
"""cpu_percent() reports just the current CPU usage sample; this Effect gathers
them up and stores them in the cpu_history reactive value, in a numpy 2D array
(rows are CPUs, columns are time)."""
new_data = np.vstack(cpu_current())
with reactive.isolate():
if cpu_history() is None:
cpu_history.set(new_data)
else:
combined_data = np.hstack([cpu_history(), new_data])
# Throw away extra data so we don't consume unbounded amounts of memory
if combined_data.shape[1] > MAX_SAMPLES:
combined_data = combined_data[:, -MAX_SAMPLES:]
cpu_history.set(combined_data)
@reactive.Effect(priority=100)
@reactive.event(input.reset)
def reset_history():
cpu_history.set(None)
@output
@render.plot
def plot():
history = cpu_history_with_hold()
if history is None:
history = np.array([])
history.shape = (ncpu, 0)
nsamples = input.sample_count()
# Throw away samples too old to fit on the plot
if history.shape[1] > nsamples:
history = history[:, -nsamples:]
ncols = 2
nrows = int(ceil(ncpu / ncols))
fig, axeses = plt.subplots(
nrows=nrows,
ncols=ncols,
squeeze=False,
)
for i in range(0, ncols * nrows):
row = i // ncols
col = i % ncols
axes = axeses[row, col]
if i >= len(history):
axes.set_visible(False)
continue
data = history[i]
axes.yaxis.set_label_position("right")
axes.yaxis.tick_right()
axes.set_xlim(-(nsamples - 1), 0)
axes.set_ylim(0, 100)
assert len(data) <= nsamples
# Set up an array of x-values that will right-align the data relative to the
# plotting area
x = np.arange(0, len(data))
x = np.flip(-x)
# Color bars by cmap
color = plt.get_cmap(input.cmap())(data / 100)
axes.bar(x, data, color=color, linewidth=0, width=1.0)
axes.set_yticks([25, 50, 75])
for ytl in axes.get_yticklabels():
if col == ncols - 1 or i == ncpu - 1 or True:
ytl.set_fontsize(7)
else:
ytl.set_visible(False)
hide_ticks(axes.yaxis)
for xtl in axes.get_xticklabels():
xtl.set_visible(False)
hide_ticks(axes.xaxis)
axes.grid(True, linewidth=0.25)
return fig
@output
@render.table
def table():
history = cpu_history_with_hold()
latest = pd.DataFrame(history).transpose().tail(input.table_rows())
if latest.shape[0] == 0:
return latest
return (
latest.style.format(precision=0)
.hide(axis="index")
.set_table_attributes(
'class="dataframe shiny-table table table-borderless font-monospace"'
)
.background_gradient(cmap=input.cmap(), vmin=0, vmax=100)
)
def hide_ticks(axis):
for ticks in [axis.get_major_ticks(), axis.get_minor_ticks()]:
for tick in ticks:
tick.tick1line.set_visible(False)
tick.tick2line.set_visible(False)
tick.label1.set_visible(False)
tick.label2.set_visible(False)
app = App(app_ui, server)