Spaces:
Build error
Build error
Commit
·
577d5c4
1
Parent(s):
6fd80fb
Upload 3 files
Browse files- Dockerfile +22 -0
- app.py +232 -0
- requirements.txt +8 -0
Dockerfile
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM ubuntu:kinetic
|
2 |
+
|
3 |
+
# Doesn't usually have an "upgrade"
|
4 |
+
RUN apt-get update \
|
5 |
+
&& DEBIAN_FRONTEND=noninteractive \
|
6 |
+
apt-get install --no-install-recommends --assume-yes \
|
7 |
+
build-essential \
|
8 |
+
python3 \
|
9 |
+
python3-dev \
|
10 |
+
python3-pip
|
11 |
+
|
12 |
+
COPY requirements.txt .
|
13 |
+
|
14 |
+
RUN pip install -r requirements.txt
|
15 |
+
|
16 |
+
COPY . .
|
17 |
+
|
18 |
+
ENTRYPOINT ["/bin/sh", "-c"]
|
19 |
+
|
20 |
+
EXPOSE 7860
|
21 |
+
|
22 |
+
CMD ["shiny run --port 7860 --host 0.0.0.0 app.py"]
|
app.py
ADDED
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
|
3 |
+
from psutil import cpu_count, cpu_percent
|
4 |
+
|
5 |
+
from math import ceil
|
6 |
+
|
7 |
+
import matplotlib
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import numpy as np
|
10 |
+
import pandas as pd
|
11 |
+
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
|
12 |
+
|
13 |
+
shinylive_message = ""
|
14 |
+
|
15 |
+
# The agg matplotlib backend seems to be a little more efficient than the default when
|
16 |
+
# running on macOS, and also gives more consistent results across operating systems
|
17 |
+
matplotlib.use("agg")
|
18 |
+
|
19 |
+
# max number of samples to retain
|
20 |
+
MAX_SAMPLES = 1000
|
21 |
+
# secs between samples
|
22 |
+
SAMPLE_PERIOD = 1
|
23 |
+
|
24 |
+
|
25 |
+
ncpu = cpu_count(logical=True)
|
26 |
+
|
27 |
+
app_ui = ui.page_fluid(
|
28 |
+
ui.tags.style(
|
29 |
+
"""
|
30 |
+
/* Don't apply fade effect, it's constantly recalculating */
|
31 |
+
.recalculating {
|
32 |
+
opacity: 1;
|
33 |
+
}
|
34 |
+
tbody > tr:last-child {
|
35 |
+
/*border: 3px solid var(--bs-dark);*/
|
36 |
+
box-shadow:
|
37 |
+
0 0 2px 1px #fff, /* inner white */
|
38 |
+
0 0 4px 2px #0ff, /* middle cyan */
|
39 |
+
0 0 5px 3px #00f; /* outer blue */
|
40 |
+
}
|
41 |
+
#table table {
|
42 |
+
table-layout: fixed;
|
43 |
+
width: %s;
|
44 |
+
font-size: 0.8em;
|
45 |
+
}
|
46 |
+
th, td {
|
47 |
+
text-align: center;
|
48 |
+
}
|
49 |
+
"""
|
50 |
+
% f"{ncpu*4}em"
|
51 |
+
),
|
52 |
+
ui.h3("CPU Usage %", class_="mt-2"),
|
53 |
+
ui.layout_sidebar(
|
54 |
+
ui.panel_sidebar(
|
55 |
+
ui.input_select(
|
56 |
+
"cmap",
|
57 |
+
"Colormap",
|
58 |
+
{
|
59 |
+
"inferno": "inferno",
|
60 |
+
"viridis": "viridis",
|
61 |
+
"copper": "copper",
|
62 |
+
"prism": "prism (not recommended)",
|
63 |
+
},
|
64 |
+
),
|
65 |
+
ui.p(ui.input_action_button("reset", "Clear history", class_="btn-sm")),
|
66 |
+
ui.input_switch("hold", "Freeze output", value=False),
|
67 |
+
shinylive_message,
|
68 |
+
class_="mb-3",
|
69 |
+
),
|
70 |
+
ui.panel_main(
|
71 |
+
ui.div(
|
72 |
+
{"class": "card mb-3"},
|
73 |
+
ui.div(
|
74 |
+
{"class": "card-body"},
|
75 |
+
ui.h5({"class": "card-title mt-0"}, "Graphs"),
|
76 |
+
ui.output_plot("plot", height=f"{ncpu * 40}px"),
|
77 |
+
),
|
78 |
+
ui.div(
|
79 |
+
{"class": "card-footer"},
|
80 |
+
ui.input_numeric("sample_count", "Number of samples per graph", 50),
|
81 |
+
),
|
82 |
+
),
|
83 |
+
ui.div(
|
84 |
+
{"class": "card"},
|
85 |
+
ui.div(
|
86 |
+
{"class": "card-body"},
|
87 |
+
ui.h5({"class": "card-title m-0"}, "Heatmap"),
|
88 |
+
),
|
89 |
+
ui.div(
|
90 |
+
{"class": "card-body overflow-auto pt-0"},
|
91 |
+
ui.output_table("table"),
|
92 |
+
),
|
93 |
+
ui.div(
|
94 |
+
{"class": "card-footer"},
|
95 |
+
ui.input_numeric("table_rows", "Rows to display", 5),
|
96 |
+
),
|
97 |
+
),
|
98 |
+
),
|
99 |
+
),
|
100 |
+
)
|
101 |
+
|
102 |
+
|
103 |
+
@reactive.Calc
|
104 |
+
def cpu_current():
|
105 |
+
reactive.invalidate_later(SAMPLE_PERIOD)
|
106 |
+
return cpu_percent(percpu=True)
|
107 |
+
|
108 |
+
|
109 |
+
def server(input: Inputs, output: Outputs, session: Session):
|
110 |
+
cpu_history = reactive.Value(None)
|
111 |
+
|
112 |
+
@reactive.Calc
|
113 |
+
def cpu_history_with_hold():
|
114 |
+
# If "hold" is on, grab an isolated snapshot of cpu_history; if not, then do a
|
115 |
+
# regular read
|
116 |
+
if not input.hold():
|
117 |
+
return cpu_history()
|
118 |
+
else:
|
119 |
+
# Even if frozen, we still want to respond to input.reset()
|
120 |
+
input.reset()
|
121 |
+
with reactive.isolate():
|
122 |
+
return cpu_history()
|
123 |
+
|
124 |
+
@reactive.Effect
|
125 |
+
def collect_cpu_samples():
|
126 |
+
"""cpu_percent() reports just the current CPU usage sample; this Effect gathers
|
127 |
+
them up and stores them in the cpu_history reactive value, in a numpy 2D array
|
128 |
+
(rows are CPUs, columns are time)."""
|
129 |
+
|
130 |
+
new_data = np.vstack(cpu_current())
|
131 |
+
with reactive.isolate():
|
132 |
+
if cpu_history() is None:
|
133 |
+
cpu_history.set(new_data)
|
134 |
+
else:
|
135 |
+
combined_data = np.hstack([cpu_history(), new_data])
|
136 |
+
# Throw away extra data so we don't consume unbounded amounts of memory
|
137 |
+
if combined_data.shape[1] > MAX_SAMPLES:
|
138 |
+
combined_data = combined_data[:, -MAX_SAMPLES:]
|
139 |
+
cpu_history.set(combined_data)
|
140 |
+
|
141 |
+
@reactive.Effect(priority=100)
|
142 |
+
@reactive.event(input.reset)
|
143 |
+
def reset_history():
|
144 |
+
cpu_history.set(None)
|
145 |
+
|
146 |
+
@output
|
147 |
+
@render.plot
|
148 |
+
def plot():
|
149 |
+
history = cpu_history_with_hold()
|
150 |
+
|
151 |
+
if history is None:
|
152 |
+
history = np.array([])
|
153 |
+
history.shape = (ncpu, 0)
|
154 |
+
|
155 |
+
nsamples = input.sample_count()
|
156 |
+
|
157 |
+
# Throw away samples too old to fit on the plot
|
158 |
+
if history.shape[1] > nsamples:
|
159 |
+
history = history[:, -nsamples:]
|
160 |
+
|
161 |
+
ncols = 2
|
162 |
+
nrows = int(ceil(ncpu / ncols))
|
163 |
+
fig, axeses = plt.subplots(
|
164 |
+
nrows=nrows,
|
165 |
+
ncols=ncols,
|
166 |
+
squeeze=False,
|
167 |
+
)
|
168 |
+
for i in range(0, ncols * nrows):
|
169 |
+
row = i // ncols
|
170 |
+
col = i % ncols
|
171 |
+
axes = axeses[row, col]
|
172 |
+
if i >= len(history):
|
173 |
+
axes.set_visible(False)
|
174 |
+
continue
|
175 |
+
data = history[i]
|
176 |
+
axes.yaxis.set_label_position("right")
|
177 |
+
axes.yaxis.tick_right()
|
178 |
+
axes.set_xlim(-(nsamples - 1), 0)
|
179 |
+
axes.set_ylim(0, 100)
|
180 |
+
|
181 |
+
assert len(data) <= nsamples
|
182 |
+
|
183 |
+
# Set up an array of x-values that will right-align the data relative to the
|
184 |
+
# plotting area
|
185 |
+
x = np.arange(0, len(data))
|
186 |
+
x = np.flip(-x)
|
187 |
+
|
188 |
+
# Color bars by cmap
|
189 |
+
color = plt.get_cmap(input.cmap())(data / 100)
|
190 |
+
axes.bar(x, data, color=color, linewidth=0, width=1.0)
|
191 |
+
|
192 |
+
axes.set_yticks([25, 50, 75])
|
193 |
+
for ytl in axes.get_yticklabels():
|
194 |
+
if col == ncols - 1 or i == ncpu - 1 or True:
|
195 |
+
ytl.set_fontsize(7)
|
196 |
+
else:
|
197 |
+
ytl.set_visible(False)
|
198 |
+
hide_ticks(axes.yaxis)
|
199 |
+
for xtl in axes.get_xticklabels():
|
200 |
+
xtl.set_visible(False)
|
201 |
+
hide_ticks(axes.xaxis)
|
202 |
+
axes.grid(True, linewidth=0.25)
|
203 |
+
|
204 |
+
return fig
|
205 |
+
|
206 |
+
@output
|
207 |
+
@render.table
|
208 |
+
def table():
|
209 |
+
history = cpu_history_with_hold()
|
210 |
+
latest = pd.DataFrame(history).transpose().tail(input.table_rows())
|
211 |
+
if latest.shape[0] == 0:
|
212 |
+
return latest
|
213 |
+
return (
|
214 |
+
latest.style.format(precision=0)
|
215 |
+
.hide(axis="index")
|
216 |
+
.set_table_attributes(
|
217 |
+
'class="dataframe shiny-table table table-borderless font-monospace"'
|
218 |
+
)
|
219 |
+
.background_gradient(cmap=input.cmap(), vmin=0, vmax=100)
|
220 |
+
)
|
221 |
+
|
222 |
+
|
223 |
+
def hide_ticks(axis):
|
224 |
+
for ticks in [axis.get_major_ticks(), axis.get_minor_ticks()]:
|
225 |
+
for tick in ticks:
|
226 |
+
tick.tick1line.set_visible(False)
|
227 |
+
tick.tick2line.set_visible(False)
|
228 |
+
tick.label1.set_visible(False)
|
229 |
+
tick.label2.set_visible(False)
|
230 |
+
|
231 |
+
|
232 |
+
app = App(app_ui, server)
|
requirements.txt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Pandas needs Jinja2 for table styling, but it doesn't (yet) load automatically
|
2 |
+
# in Pyodide, so we need to explicitly list it here.
|
3 |
+
Jinja2
|
4 |
+
matplotlib
|
5 |
+
pandas
|
6 |
+
psutil
|
7 |
+
numpy
|
8 |
+
shiny
|