Spaces:
Build error
Build error
File size: 7,192 Bytes
577d5c4 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
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)
|