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)