Spaces:
Build error
Build error
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), | |
), | |
), | |
), | |
), | |
) | |
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) | |
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() | |
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) | |
def reset_history(): | |
cpu_history.set(None) | |
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 | |
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) | |