Skip to content

Commit 90b573e

Browse files
committed
feat: update charts to lay out gpu and cpu side-by-side for a node
1 parent b672b8d commit 90b573e

File tree

9 files changed

+190
-111
lines changed

9 files changed

+190
-111
lines changed

plugins/plugin-client-default/notebooks/dashboard.md

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,12 @@ layout:
5353

5454
---
5555

56-
=== "GPU Charts"
56+
=== "Utilization Charts"
5757
```shell
5858
---
5959
execute: now
6060
maximize: true
6161
outputOnly: true
6262
---
63-
chart gpu "${LOGDIR}/resources/gpu.txt"
64-
```
65-
66-
=== "CPU Charts"
67-
```shell
68-
---
69-
execute: now
70-
maximize: true
71-
outputOnly: true
72-
---
73-
chart vmstat "${LOGDIR}/resources/pod-vmstat.txt"
63+
chart all "${LOGDIR}"
7464
```

plugins/plugin-codeflare/src/components/Chart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export default class BaseChart extends React.PureComponent<Props> {
144144

145145
private static readonly titlePosition = {
146146
x: {
147-
left: BaseChart.padding.left - BaseChart.tickLabelFontSize * 4,
147+
left: BaseChart.padding.left - BaseChart.tickLabelFontSize * 3.5,
148148
right: BaseChart.dimensions.width - BaseChart.tickLabelFontSize * 2,
149149
},
150150
y: BaseChart.padding.top - BaseChart.fontSize * 1.5,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2022 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from "react"
18+
19+
export default function ChartGrid(props: React.PropsWithChildren<unknown>) {
20+
return <div className="codeflare-chart-grid flex-fill">{props.children}</div>
21+
}

plugins/plugin-codeflare/src/components/GPUChart.tsx

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
import React from "react"
1818

1919
import { Log } from "../controller/charts/gpu"
20-
import BaseChart, { BaseChartProps, Series } from "./Chart"
20+
import BaseChart, { BaseChartProps } from "./Chart"
21+
import { HostMap } from "../controller/charts/LogRecord"
2122

2223
type Props = {
23-
logs: Log[]
24+
logs: HostMap<Log>
2425
}
2526

2627
type State = {
@@ -39,49 +40,37 @@ export default class GPUChart extends React.PureComponent<Props, State> {
3940
}
4041
}
4142

42-
private static data(field: "utilizationGPU" | "utilizationMemory" | "temperatureGPU", props: Props) {
43-
return props.logs.map((log) => ({
44-
name: log.gpuType,
45-
x: log.timestamp,
46-
y: log[field],
47-
}))
48-
}
49-
5043
private static charts(props: Props): BaseChartProps[] {
51-
const earliestTimestamp = props.logs.reduce((min, line) => Math.min(min, line.timestamp), Number.MAX_VALUE)
52-
53-
const perNodeData = props.logs.reduce((M, line) => {
54-
if (!M[line.cluster]) {
55-
M[line.cluster] = [
56-
{ impl: "ChartArea", stroke: BaseChart.colors[1], data: [] },
57-
{ impl: "ChartLine", stroke: BaseChart.colors[2], data: [] },
58-
{ impl: "ChartDashedLine", stroke: BaseChart.colors[3], data: [] },
59-
]
60-
}
61-
62-
M[line.cluster][0].data.push({
63-
name: BaseChart.nodeNameLabel(line.cluster) + " GPU Utilization",
44+
const earliestTimestamp: number = Object.values(props.logs).reduce(
45+
(min, logs) => logs.reduce((min, line) => Math.min(min, line.timestamp), Number.MAX_VALUE),
46+
Number.MAX_VALUE
47+
)
48+
49+
return Object.entries(props.logs).map(([node, lines]) => {
50+
const d1 = lines.map((line) => ({
51+
name: BaseChart.nodeNameLabel(node) + " GPU Utilization",
6452
x: line.timestamp - earliestTimestamp,
6553
y: line.utilizationGPU,
66-
})
54+
}))
6755

68-
M[line.cluster][1].data.push({
69-
name: BaseChart.nodeNameLabel(line.cluster) + " GPU Memory Utilization",
56+
const d2 = lines.map((line) => ({
57+
name: BaseChart.nodeNameLabel(node) + " GPU Memory Utilization",
7058
x: line.timestamp - earliestTimestamp,
7159
y: line.utilizationMemory,
72-
})
60+
}))
7361

74-
M[line.cluster][2].data.push({
75-
name: BaseChart.nodeNameLabel(line.cluster) + " GPU Temperature",
62+
const d3 = lines.map((line) => ({
63+
name: BaseChart.nodeNameLabel(node) + " GPU Temperature",
7664
x: line.timestamp - earliestTimestamp,
7765
y: line.temperatureGPU,
78-
})
66+
}))
7967

80-
return M
81-
}, {} as Record<string, Series[]>)
68+
const series = [
69+
{ impl: "ChartArea" as const, stroke: BaseChart.colors[1], data: d1 },
70+
{ impl: "ChartLine" as const, stroke: BaseChart.colors[2], data: d2 },
71+
{ impl: "ChartDashedLine" as const, stroke: BaseChart.colors[3], data: d3 },
72+
]
8273

83-
return Object.keys(perNodeData).map((node) => {
84-
const series = perNodeData[node]
8574
const data = series.map((_, idx) => BaseChart.normalize(_, idx !== 2 ? "percentage" : "celsius"))
8675

8776
return {
@@ -90,7 +79,7 @@ export default class GPUChart extends React.PureComponent<Props, State> {
9079
padding: GPUChart.padding,
9180
yAxes: [
9281
{
93-
label: "Utilization",
82+
label: "GPU",
9483
format: "percentage",
9584
y: data[0].y,
9685
tickFormat: data[0].tickFormat,

plugins/plugin-codeflare/src/components/VmstatChart.tsx

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
import React from "react"
1818

19-
import BaseChart, { BaseChartProps, Series } from "./Chart"
2019
import { Log } from "../controller/charts/vmstat"
20+
import BaseChart, { BaseChartProps } from "./Chart"
21+
import { HostMap } from "../controller/charts/LogRecord"
2122

2223
type Props = {
23-
logs: Log[]
24+
logs: HostMap<Log>
2425
}
2526

2627
type State = {
@@ -40,33 +41,29 @@ export default class VmstatChart extends React.PureComponent<Props, State> {
4041
}
4142

4243
private static charts(props: Props): BaseChartProps[] {
43-
const earliestTimestamp = props.logs.reduce((min, line) => Math.min(min, line.timestamp), Number.MAX_VALUE)
44+
const earliestTimestamp: number = Object.values(props.logs).reduce(
45+
(min, logs) => logs.reduce((min, line) => Math.min(min, line.timestamp), Number.MAX_VALUE),
46+
Number.MAX_VALUE
47+
)
4448

45-
const perNodeData = props.logs.reduce((M, line) => {
46-
if (!M[line.hostname]) {
47-
M[line.hostname] = [
48-
{ impl: "ChartArea", stroke: BaseChart.colors[1], data: [] },
49-
{ impl: "ChartLine", stroke: BaseChart.colors[2], data: [] },
50-
]
51-
}
52-
53-
M[line.hostname][0].data.push({
54-
name: BaseChart.nodeNameLabel(line.hostname) + " CPU Utilization",
49+
return Object.entries(props.logs).map(([node, lines]) => {
50+
const d1 = lines.map((line) => ({
51+
name: BaseChart.nodeNameLabel(node) + " CPU Utilization",
5552
x: line.timestamp - earliestTimestamp,
5653
y: 100 - line.idle,
57-
})
54+
}))
5855

59-
M[line.hostname][1].data.push({
60-
name: BaseChart.nodeNameLabel(line.hostname) + " Free Memory",
56+
const d2 = lines.map((line) => ({
57+
name: BaseChart.nodeNameLabel(node) + " Free Memory",
6158
x: line.timestamp - earliestTimestamp,
6259
y: line.freeMemory,
63-
})
60+
}))
6461

65-
return M
66-
}, {} as Record<string, Series[]>)
62+
const series = [
63+
{ impl: "ChartArea" as const, stroke: BaseChart.colors[1], data: d1 },
64+
{ impl: "ChartLine" as const, stroke: BaseChart.colors[2], data: d2 },
65+
]
6766

68-
return Object.keys(perNodeData).map((node) => {
69-
const series = perNodeData[node]
7067
const data = series.map((_, idx) => BaseChart.normalize(_, idx === 0 ? "percentage" : "memory"))
7168

7269
return {
@@ -76,7 +73,7 @@ export default class VmstatChart extends React.PureComponent<Props, State> {
7673
padding: VmstatChart.padding,
7774
yAxes: [
7875
{
79-
label: "Utilization",
76+
label: "CPU",
8077
format: "percentage",
8178
y: data[0].y,
8279
tickFormat: data[0].tickFormat,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2022 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
type LogRecord<T> = T & {
18+
hostname: string
19+
timestamp: number
20+
}
21+
22+
export type HostMap<T, R extends LogRecord<T> = LogRecord<T>> = { [key: string]: R[] }
23+
24+
export function toHostMap<T, R extends LogRecord<T>>(records: R[]): HostMap<T, R> {
25+
return records.reduce((M, record) => {
26+
if (!M[record.hostname]) {
27+
M[record.hostname] = []
28+
}
29+
M[record.hostname].push(record)
30+
return M
31+
}, {} as HostMap<T, R>)
32+
}
33+
34+
export default LogRecord

plugins/plugin-codeflare/src/controller/charts/all.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,52 @@
1515
*/
1616

1717
import React from "react"
18-
import { Arguments, ReactResponse } from "@kui-shell/core"
18+
import { join } from "path"
19+
import { Arguments } from "@kui-shell/core"
1920

21+
/** Oops, sometimes we have no data for a give node */
22+
function noData(node: string, kind: "CPU Utilization" | "GPU Utilization") {
23+
return (
24+
<div className="flex-layout" title={`No ${kind} for ${node}`}>
25+
<span className="flex-fill flex-layout flex-align-center">no data</span>
26+
</div>
27+
)
28+
}
29+
30+
/**
31+
* Render a combo chart that interleaves GPU utilization and CPU
32+
* utilization charts, so that the two (for a given node) are
33+
* side-by-side.
34+
*
35+
*/
2036
export default async function all(args: Arguments) {
2137
const filepath = args.argvNoOptions[2]
2238
if (!filepath) {
2339
return `Usage chart all ${filepath}`
2440
}
2541

26-
const charts = await Promise.all([
27-
args.REPL.qexec<ReactResponse>(`chart gpu "${filepath}/resources/gpu.txt"`),
28-
args.REPL.qexec<ReactResponse>(`chart vmstat "${filepath}/resources/pod-vmstat.txt"`),
42+
const [gpuData, cpuData] = await Promise.all([
43+
import("./gpu").then((_) => _.parse(join(filepath, "resources/gpu.txt"), args.REPL)),
44+
import("./vmstat").then((_) => _.parse(join(filepath, "resources/pod-vmstat.txt"), args.REPL)),
2945
])
3046

47+
const [GPUChart, VmstatChart] = await Promise.all([
48+
import("../../components/GPUChart").then((_) => _.default),
49+
import("../../components/VmstatChart").then((_) => _.default),
50+
])
51+
52+
const nodes = Array.from(new Set(Object.keys(gpuData).concat(Object.keys(cpuData))))
53+
54+
const linearized = nodes.map((node) => {
55+
const gpuForNode = gpuData[node]
56+
const cpuForNode = cpuData[node]
57+
return [
58+
!gpuForNode ? noData(node, "GPU Utilization") : <GPUChart logs={{ [node]: gpuForNode }} />,
59+
!cpuForNode ? noData(node, "CPU Utilization") : <VmstatChart logs={{ [node]: cpuData[node] }} />,
60+
]
61+
})
62+
3163
return {
32-
react: <div className="codeflare-chart-grid flex-fill">{charts.flatMap((_) => _.react)}</div>,
64+
react: <div className="codeflare-chart-grid flex-fill">{linearized.flatMap((_) => _)}</div>,
3365
}
3466
}

plugins/plugin-codeflare/src/controller/charts/gpu.tsx

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,18 @@ import stripAnsi from "strip-ansi"
1919
import { Arguments } from "@kui-shell/core"
2020

2121
import { expand } from "../../lib/util"
22+
import LogRecord, { toHostMap } from "./LogRecord"
23+
2224
import GPUChart from "../../components/GPUChart"
25+
import ChartGrid from "../../components/ChartGrid"
2326

24-
export type Log = {
25-
cluster: string
26-
timestamp: number
27+
export type Log = LogRecord<{
2728
gpuType: string
2829
utilizationGPU: number
2930
utilizationMemory: number
3031
totalMemory: number
3132
temperatureGPU: number
32-
}
33+
}>
3334

3435
function formatLogs(logs: string) {
3536
return logs
@@ -40,15 +41,15 @@ function formatLogs(logs: string) {
4041

4142
function formatLogObject(logLine: string[]) {
4243
const splittedLine = logLine[0].split(/\s|\t\t/gi)
43-
const cluster = splittedLine[splittedLine.length - 3]
44+
const hostname = splittedLine[splittedLine.length - 3]
4445
const timestamp = new Date(splittedLine.slice(splittedLine.length - 2).join(" ")).getTime()
4546
const gpuType = splittedLine.slice(0, splittedLine.length - 5).join(" ")
4647

4748
const utilizationData = logLine.map((line) => parseInt(line.trim().split(" ")[0]))
4849
const [, utilizationGPU, utilizationMemory, totalMemory, temperatureGPU] = utilizationData
4950

5051
const newObj: Log = {
51-
cluster,
52+
hostname,
5253
timestamp,
5354
gpuType,
5455
utilizationGPU,
@@ -59,21 +60,27 @@ function formatLogObject(logLine: string[]) {
5960
return newObj
6061
}
6162

62-
export default async function chart(args: Arguments) {
63-
const filepath = args.argvNoOptions[2]
64-
if (!filepath) {
65-
return `Usage chart gpu ${filepath}`
66-
}
67-
68-
const logs = stripAnsi(await args.REPL.qexec<string>(`vfs fslice ${expand(filepath)} 0`))
63+
export async function parse(filepath: string, REPL: Arguments["REPL"]) {
64+
const logs = stripAnsi(await REPL.qexec<string>(`vfs fslice ${expand(filepath)} 0`))
6965
const formattedLogs = formatLogs(logs)
70-
const objLogs = formattedLogs.map((logLine) => formatLogObject(logLine))
66+
return toHostMap(formattedLogs.map((logLine) => formatLogObject(logLine)))
67+
}
7168

69+
export function chart(logs: Awaited<ReturnType<typeof parse>>) {
7270
return {
7371
react: (
74-
<div className="codeflare-chart-grid flex-fill">
75-
<GPUChart logs={objLogs} />
76-
</div>
72+
<ChartGrid>
73+
<GPUChart logs={logs} />
74+
</ChartGrid>
7775
),
7876
}
7977
}
78+
79+
export default async function chartCmd(args: Arguments) {
80+
const filepath = args.argvNoOptions[2]
81+
if (!filepath) {
82+
throw new Error(`Usage chart vmstat ${filepath}`)
83+
}
84+
85+
return chart(await parse(filepath, args.REPL))
86+
}

0 commit comments

Comments
 (0)