Skip to content

Commit dd38938

Browse files
committed
Experiment with resource monitor
1 parent f601be7 commit dd38938

File tree

4 files changed

+322
-9
lines changed

4 files changed

+322
-9
lines changed

site/components/ResourceMonitor.tsx

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import React from "react"
2+
import { Bar, Line } from "react-chartjs-2"
3+
import { Chart, ChartOptions } from "chart.js"
4+
5+
const multiply = {
6+
beforeDraw: function (chart: Chart, options: ChartOptions) {
7+
if (chart && chart.ctx) {
8+
chart.ctx.globalCompositeOperation = "multiply"
9+
}
10+
},
11+
afterDatasetsDraw: function (chart: Chart, options: ChartOptions) {
12+
if (chart && chart.ctx) {
13+
chart.ctx.globalCompositeOperation = "source-over"
14+
}
15+
},
16+
}
17+
18+
function formatBytes(bytes: number, decimals = 2) {
19+
if (bytes === 0) return "0 Bytes"
20+
21+
const k = 1024
22+
const dm = decimals < 0 ? 0 : decimals
23+
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
24+
25+
const i = Math.floor(Math.log(bytes) / Math.log(k))
26+
27+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]
28+
}
29+
30+
const padding = 64
31+
32+
const opts: ChartOptions = {
33+
responsive: true,
34+
maintainAspectRatio: false,
35+
legend: {
36+
fullWidth: true,
37+
display: false,
38+
},
39+
elements: {
40+
point: {
41+
radius: 0,
42+
hitRadius: 8,
43+
hoverRadius: 8,
44+
},
45+
rectangle: {
46+
borderWidth: 0,
47+
},
48+
},
49+
layout: {
50+
padding: {
51+
top: padding,
52+
bottom: padding,
53+
},
54+
},
55+
tooltips: {
56+
mode: "index",
57+
axis: "y",
58+
cornerRadius: 8,
59+
borderWidth: 0,
60+
titleFontStyle: "normal",
61+
callbacks: {
62+
label: (item: any, data: any) => {
63+
const dataset = data.datasets[item.datasetIndex]
64+
const num: number = dataset.data[item.index] as number
65+
if (num) {
66+
return dataset.label + ": " + num.toFixed(2) + "%"
67+
}
68+
},
69+
labelColor: (item: any, data: any) => {
70+
const dataset = data.data.datasets[item.datasetIndex]
71+
return {
72+
// Trim off the transparent hex code.
73+
backgroundColor: (dataset.pointBackgroundColor as string).substr(0, 7),
74+
borderColor: "#000000",
75+
}
76+
},
77+
title: (item) => {
78+
console.log(item[0])
79+
80+
return "Resources: " + item[0].label
81+
},
82+
},
83+
},
84+
plugins: {
85+
tooltip: {
86+
callbacks: {
87+
beforeTitle: (item: any) => {
88+
console.log("BEFORE TITLE: " + item)
89+
return "Resources"
90+
},
91+
},
92+
},
93+
legend: {
94+
display: false,
95+
},
96+
},
97+
scales: {
98+
xAxes: [
99+
{
100+
display: false,
101+
ticks: {
102+
stepSize: 10,
103+
maxTicksLimit: 4,
104+
maxRotation: 0,
105+
},
106+
},
107+
],
108+
yAxes: [
109+
{
110+
gridLines: {
111+
color: "rgba(0, 0, 0, 0.09)",
112+
zeroLineColor: "rgba(0, 0, 0, 0.09)",
113+
},
114+
ticks: {
115+
callback: (v) => v + "%",
116+
max: 100,
117+
maxTicksLimit: 2,
118+
min: 0,
119+
padding: 4,
120+
},
121+
},
122+
],
123+
},
124+
}
125+
126+
export interface ResourceUsageSnapshot {
127+
cpuPercentage: number
128+
memoryUsedBytes: number
129+
diskUsedBytes: number
130+
}
131+
132+
export interface ResourceMonitorProps {
133+
readonly diskTotalBytes: number
134+
readonly memoryTotalBytes: number
135+
readonly resources: ReadonlyArray<ResourceUsageSnapshot>
136+
}
137+
138+
export const ResourceMonitor: React.FC<ResourceMonitorProps> = (props) => {
139+
const dataF = React.useMemo(() => {
140+
return (canvas: any) => {
141+
// Store gradients inside the canvas object for easy access.
142+
// This function is called everytime resources values change...
143+
// we don't want to allocate a new gradient everytime.
144+
if (!canvas["cpuGradient"]) {
145+
const cpuGradient = canvas.getContext("2d").createLinearGradient(0, 0, 0, canvas.height)
146+
cpuGradient.addColorStop(1, "#9787FF32")
147+
cpuGradient.addColorStop(0, "#5555FFC4")
148+
canvas["cpuGradient"] = cpuGradient
149+
}
150+
151+
if (!canvas["memGradient"]) {
152+
const memGradient = canvas.getContext("2d").createLinearGradient(0, 0, 0, canvas.height)
153+
memGradient.addColorStop(1, "#55FF8532")
154+
memGradient.addColorStop(0, "#42B863C4")
155+
canvas["memGradient"] = memGradient
156+
}
157+
158+
if (!canvas["diskGradient"]) {
159+
const diskGradient = canvas.getContext("2d").createLinearGradient(0, 0, 0, canvas.height)
160+
diskGradient.addColorStop(1, "#97979700")
161+
diskGradient.addColorStop(0, "#797979C4")
162+
canvas["diskGradient"] = diskGradient
163+
}
164+
165+
const cpuPercentages = []
166+
//const cpuPercentages = Array(20 - props.resources.length).fill(null)
167+
cpuPercentages.push(...props.resources.map((r) => r.cpuPercentage))
168+
169+
//const memPercentages = Array(20 - props.resources.length).fill(null)
170+
const memPercentages = []
171+
memPercentages.push(...props.resources.map((r) => (r.memoryUsedBytes / props.memoryTotalBytes) * 100))
172+
173+
const diskPercentages = []
174+
//const diskPercentages = Array(20 - props.resources.length).fill(null)
175+
diskPercentages.push(...props.resources.map((r) => (r.diskUsedBytes / props.diskTotalBytes) * 100))
176+
177+
return {
178+
labels: Array(20)
179+
.fill(0)
180+
.map((_, index) => (20 - index) * 3 + "s ago"),
181+
datasets: [
182+
{
183+
label: "CPU",
184+
data: cpuPercentages,
185+
backgroundColor: canvas["cpuGradient"],
186+
borderColor: "transparent",
187+
pointBackgroundColor: "#9787FF32",
188+
pointBorderColor: "#FFFFFF",
189+
lineTension: 0.4,
190+
fill: true,
191+
},
192+
{
193+
label: "Memory",
194+
data: memPercentages,
195+
backgroundColor: canvas["memGradient"],
196+
borderColor: "transparent",
197+
pointBackgroundColor: "#55FF8532",
198+
pointBorderColor: "#FFFFFF",
199+
lineTension: 0.4,
200+
fill: true,
201+
},
202+
{
203+
label: "Disk",
204+
data: diskPercentages,
205+
backgroundColor: canvas["diskGradient"],
206+
borderColor: "transparent",
207+
pointBackgroundColor: "#97979732",
208+
pointBorderColor: "#FFFFFF",
209+
lineTension: 0.4,
210+
fill: true,
211+
},
212+
],
213+
}
214+
}
215+
}, [props.resources])
216+
217+
return (
218+
<Line
219+
type="line"
220+
height={40 + padding * 2}
221+
data={dataF}
222+
options={opts}
223+
plugins={[multiply]}
224+
ref={(ref) => {
225+
window.Chart.defaults.global.defaultFontFamily = "'Fira Code', Inter"
226+
}}
227+
/>
228+
)
229+
}

site/components/Workspace/Workspace.tsx

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import Paper from "@material-ui/core/Paper"
33
import { makeStyles } from "@material-ui/core/styles"
44
import Typography from "@material-ui/core/Typography"
55
import OpenInNewIcon from "@material-ui/icons/OpenInNew"
6-
import React, { useState } from "react"
6+
import React, { useEffect, useState } from "react"
77
import MoreVertIcon from "@material-ui/icons/MoreVert"
88
import { QuestionHelp } from "../QuestionHelp"
99
import { CircularProgress, IconButton, Link, Menu, MenuItem } from "@material-ui/core"
10-
10+
import { ResourceMonitor, ResourceMonitorProps, ResourceUsageSnapshot } from "../ResourceMonitor"
1111
import { Timeline as TestTimeline } from "../Timeline"
1212

1313
import * as API from "../../api"
14+
import { getAllByTestId } from "@testing-library/react"
1415

1516
export interface WorkspaceProps {
1617
workspace: API.Workspace
@@ -94,17 +95,17 @@ export const ResourceRow: React.FC<ResourceRowProps> = ({ icon, href, name, stat
9495
<MoreVertIcon fontSize="inherit" />
9596
</IconButton>
9697

97-
<Menu anchorEl={menuAnchorEl} open={!!menuAnchorEl} onClose={() => setMenuAnchorEl(undefined)}>
98+
<Menu anchorEl={menuAnchorEl} open={!!menuAnchorEl} onClose={() => setMenuAnchorEl(null)}>
9899
<MenuItem
99100
onClick={() => {
100-
setMenuAnchorEl(undefined)
101+
setMenuAnchorEl(null)
101102
}}
102103
>
103104
SSH
104105
</MenuItem>
105106
<MenuItem
106107
onClick={() => {
107-
setMenuAnchorEl(undefined)
108+
setMenuAnchorEl(null)
108109
}}
109110
>
110111
Remote Desktop
@@ -177,6 +178,36 @@ const TitleIconSize = 48
177178
export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
178179
const styles = useStyles()
179180

181+
const [resources, setResources] = useState<ResourceUsageSnapshot[]>([
182+
{
183+
cpuPercentage: 50,
184+
memoryUsedBytes: 8 * 1024 * 1024,
185+
diskUsedBytes: 24 * 1024 * 1024,
186+
},
187+
])
188+
189+
useEffect(() => {
190+
const rand = (range: number) => {
191+
return range * 2 * (Math.random() - 0.5)
192+
}
193+
const timeout = window.setTimeout(() => {
194+
setResources((res: ResourceUsageSnapshot[]) => {
195+
const latest = res[0]
196+
const newEntry = {
197+
cpuPercentage: latest.cpuPercentage + rand(5),
198+
memoryUsedBytes: latest.memoryUsedBytes + rand(256 * 1024),
199+
diskUsedBytes: latest.diskUsedBytes + rand(512 * 1024),
200+
}
201+
const ret: ResourceUsageSnapshot[] = [newEntry, ...res]
202+
return ret
203+
})
204+
}, 1000)
205+
206+
return () => {
207+
window.clearTimeout(timeout)
208+
}
209+
})
210+
180211
return (
181212
<div className={styles.root}>
182213
<Paper elevation={0} className={styles.section}>
@@ -193,6 +224,13 @@ export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
193224
</Typography>
194225
</div>
195226
</div>
227+
<div style={{ height: "200px", position: "relative" }}>
228+
<ResourceMonitor
229+
diskTotalBytes={256 * 1024 * 1024}
230+
memoryTotalBytes={16 * 1024 * 1024}
231+
resources={resources}
232+
/>
233+
</div>
196234
</Paper>
197235
<div className={styles.horizontal}>
198236
<div className={styles.sideBar}>
@@ -210,7 +248,7 @@ export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
210248
<ResourceRow name={"React App"} icon={"/static/react-icon.svg"} href={"placeholder"} status={"active"} />
211249
</div>
212250
</Paper>
213-
251+
214252
<Paper elevation={0} className={styles.section}>
215253
<Title>
216254
<Typography variant="h6">Resources</Typography>

site/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@material-ui/icons": "4.5.1",
2121
"@material-ui/lab": "4.0.0-alpha.60",
2222
"@testing-library/react": "12.1.2",
23+
"@types/chart.js": "^2.9.35",
2324
"@types/express": "4.17.13",
2425
"@types/jest": "27.4.0",
2526
"@types/node": "14.18.10",
@@ -28,6 +29,7 @@
2829
"@types/superagent": "4.1.15",
2930
"@typescript-eslint/eslint-plugin": "4.33.0",
3031
"@typescript-eslint/parser": "4.33.0",
32+
"chart.js": "2.9.4",
3133
"eslint": "7.32.0",
3234
"eslint-config-prettier": "8.3.0",
3335
"eslint-import-resolver-alias": "1.1.2",
@@ -48,6 +50,7 @@
4850
"next-router-mock": "^0.6.5",
4951
"prettier": "2.5.1",
5052
"react": "17.0.2",
53+
"react-chartjs-2": "2.11.2",
5154
"react-dom": "17.0.2",
5255
"sql-formatter": "^4.0.2",
5356
"swr": "1.2.0",

0 commit comments

Comments
 (0)