[Dashboard] Add remaining features from old dashboard (#6489)

* [Dashboard] Add remaining features from old dashboard

* Fix linting errors

* Set cluster uptime statistic to N/A

* Use proper singular or plural words for workers column

* Ignore .js, .jsx, .ts, .tsx files in check-git-clang-format-output.sh

* Fix bash quote issue
This commit is contained in:
Mitchell Stern
2019-12-16 11:21:18 -08:00
committed by Simon Mo
parent b7d5c8f220
commit 1531c21dbd
33 changed files with 838 additions and 314 deletions
+2 -2
View File
@@ -3,8 +3,8 @@ import React from "react";
import { Provider } from "react-redux";
import { BrowserRouter, Route } from "react-router-dom";
import Dashboard from "./pages/dashboard/Dashboard";
import Errors from "./pages/errors/Errors";
import Logs from "./pages/logs/Logs";
import Errors from "./pages/dashboard/dialogs/errors/Errors";
import Logs from "./pages/dashboard/dialogs/logs/Logs";
import { store } from "./store";
class App extends React.Component {
+12 -1
View File
@@ -22,6 +22,18 @@ const get = async <T>(path: string, params: { [key: string]: any }) => {
return result as T;
};
export interface RayConfigResponse {
min_workers: number;
max_workers: number;
initial_workers: number;
autoscaling_mode: string;
idle_timeout_minutes: number;
head_type: string;
worker_type: string;
}
export const getRayConfig = () => get<RayConfigResponse>("/api/ray_config", {});
export interface NodeInfoResponse {
clients: Array<{
now: number;
@@ -44,7 +56,6 @@ export interface NodeInfoResponse {
workers: Array<{
pid: number;
create_time: number;
name: string;
cmdline: string[];
cpu_percent: number;
cpu_times: {
@@ -43,7 +43,7 @@ interface Props {
lines: string[];
}
class Component extends React.Component<Props & WithStyles<typeof styles>> {
class NumberedLines extends React.Component<Props & WithStyles<typeof styles>> {
render() {
const { classes, lines } = this.props;
return (
@@ -66,4 +66,4 @@ class Component extends React.Component<Props & WithStyles<typeof styles>> {
}
}
export default withStyles(styles)(Component);
export default withStyles(styles)(NumberedLines);
@@ -19,6 +19,10 @@ const styles = (theme: Theme) =>
borderColor: theme.palette.divider,
borderStyle: "solid",
borderWidth: 1
},
inner: {
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1)
}
});
@@ -27,7 +31,7 @@ interface Props {
text: string;
}
class Component extends React.Component<Props & WithStyles<typeof styles>> {
class UsageBar extends React.Component<Props & WithStyles<typeof styles>> {
render() {
const { classes, text } = this.props;
@@ -55,10 +59,12 @@ class Component extends React.Component<Props & WithStyles<typeof styles>> {
// gradient background otherwise.
return (
<div className={classes.root}>
<div style={{ background: gradient }}>{text}</div>
<div className={classes.inner} style={{ background: gradient }}>
{text}
</div>
</div>
);
}
}
export default withStyles(styles)(Component);
export default withStyles(styles)(UsageBar);
@@ -17,17 +17,16 @@ export const formatUsage = (
return `${usedFormatted} / ${totalFormatted} (${percent.toFixed(0)}%)`;
};
export const formatUptime = (bootTime: number) => {
const uptimeSecondsTotal = Date.now() / 1000 - bootTime;
const uptimeSeconds = Math.floor(uptimeSecondsTotal) % 60;
const uptimeMinutes = Math.floor(uptimeSecondsTotal / 60) % 60;
const uptimeHours = Math.floor(uptimeSecondsTotal / 60 / 60) % 24;
const uptimeDays = Math.floor(uptimeSecondsTotal / 60 / 60 / 24);
export const formatDuration = (durationInSeconds: number) => {
const durationSeconds = Math.floor(durationInSeconds) % 60;
const durationMinutes = Math.floor(durationInSeconds / 60) % 60;
const durationHours = Math.floor(durationInSeconds / 60 / 60) % 24;
const durationDays = Math.floor(durationInSeconds / 60 / 60 / 24);
const pad = (value: number) => value.toString().padStart(2, "0");
return [
uptimeDays ? `${uptimeDays}d` : "",
`${pad(uptimeHours)}h`,
`${pad(uptimeMinutes)}m`,
`${pad(uptimeSeconds)}s`
durationDays ? `${durationDays}d` : "",
`${pad(durationHours)}h`,
`${pad(durationMinutes)}m`,
`${pad(durationSeconds)}s`
].join(" ");
};
@@ -1,18 +1,10 @@
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import createStyles from "@material-ui/core/styles/createStyles";
import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { connect } from "react-redux";
import { getNodeInfo } from "../../api";
import { StoreState } from "../../store";
import NodeRowGroup from "./NodeRowGroup";
import { dashboardActions } from "./state";
import NodeInfo from "./node-info/NodeInfo";
import RayConfig from "./ray-config/RayConfig";
const styles = (theme: Theme) =>
createStyles({
@@ -20,151 +12,22 @@ const styles = (theme: Theme) =>
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2),
"& > :not(:first-child)": {
marginTop: theme.spacing(2)
}
},
cell: {
padding: theme.spacing(1),
textAlign: "center",
"&:last-child": {
paddingRight: theme.spacing(1)
marginTop: theme.spacing(4)
}
}
});
const mapStateToProps = (state: StoreState) => ({
nodeInfo: state.dashboard.nodeInfo,
lastUpdatedAt: state.dashboard.lastUpdatedAt,
error: state.dashboard.error
});
const mapDispatchToProps = dashboardActions;
class Dashboard extends React.Component<
WithStyles<typeof styles> &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps
> {
refresh = async () => {
try {
const nodeInfo = await getNodeInfo();
this.props.setNodeInfo(nodeInfo);
} catch (error) {
this.props.setError(error.toString());
} finally {
setTimeout(this.refresh, 1000);
}
};
async componentDidMount() {
await this.refresh();
}
class Dashboard extends React.Component<WithStyles<typeof styles>> {
render() {
const { classes, nodeInfo, lastUpdatedAt, error } = this.props;
if (error !== null) {
return (
<Typography className={classes.root} color="error">
{error}
</Typography>
);
}
if (nodeInfo === null) {
return (
<Typography className={classes.root} color="textSecondary">
Loading...
</Typography>
);
}
const logCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
} = {};
const errorCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
} = {};
for (const client of nodeInfo.clients) {
logCounts[client.ip] = { perWorker: {}, total: 0 };
errorCounts[client.ip] = { perWorker: {}, total: 0 };
for (const worker of client.workers) {
logCounts[client.ip].perWorker[worker.pid] = 0;
errorCounts[client.ip].perWorker[worker.pid] = 0;
}
}
for (const ip of Object.keys(nodeInfo.log_counts)) {
if (ip in logCounts) {
for (const [pid, count] of Object.entries(nodeInfo.log_counts[ip])) {
logCounts[ip].perWorker[pid] = count;
logCounts[ip].total += count;
}
}
}
for (const ip of Object.keys(nodeInfo.error_counts)) {
if (ip in errorCounts) {
for (const [pid, count] of Object.entries(nodeInfo.error_counts[ip])) {
errorCounts[ip].perWorker[pid] = count;
errorCounts[ip].total += count;
}
}
}
const { classes } = this.props;
return (
<div className={classes.root}>
<Typography variant="h5">Ray Dashboard</Typography>
<Table>
<TableHead>
<TableRow>
<TableCell className={classes.cell} />
<TableCell className={classes.cell}>Host</TableCell>
<TableCell className={classes.cell}>Workers</TableCell>
<TableCell className={classes.cell}>Uptime</TableCell>
<TableCell className={classes.cell}>CPU</TableCell>
<TableCell className={classes.cell}>RAM</TableCell>
<TableCell className={classes.cell}>Disk</TableCell>
{/*<TableCell className={classes.cell}>Sent</TableCell>*/}
{/*<TableCell className={classes.cell}>Received</TableCell>*/}
<TableCell className={classes.cell}>Logs</TableCell>
<TableCell className={classes.cell}>Errors</TableCell>
</TableRow>
</TableHead>
<TableBody>
{nodeInfo.clients.map(client => (
<NodeRowGroup
key={client.ip}
node={client}
logCounts={logCounts[client.ip]}
errorCounts={errorCounts[client.ip]}
/>
))}
</TableBody>
</Table>
{lastUpdatedAt !== null && (
<Typography align="center">
Last updated: {new Date(lastUpdatedAt).toLocaleString()}
</Typography>
)}
<NodeInfo />
<RayConfig />
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Dashboard));
export default withStyles(styles)(Dashboard);
@@ -5,9 +5,9 @@ import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { RouteComponentProps } from "react-router";
import { ErrorsResponse, getErrors } from "../../api";
import DialogWithTitle from "../../common/DialogWithTitle";
import NumberedLines from "../../common/NumberedLines";
import { ErrorsResponse, getErrors } from "../../../../api";
import DialogWithTitle from "../../../../common/DialogWithTitle";
import NumberedLines from "../../../../common/NumberedLines";
const styles = (theme: Theme) =>
createStyles({
@@ -5,9 +5,9 @@ import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { RouteComponentProps } from "react-router";
import { getLogs, LogsResponse } from "../../api";
import DialogWithTitle from "../../common/DialogWithTitle";
import NumberedLines from "../../common/NumberedLines";
import { getLogs, LogsResponse } from "../../../../api";
import DialogWithTitle from "../../../../common/DialogWithTitle";
import NumberedLines from "../../../../common/NumberedLines";
const styles = (theme: Theme) =>
createStyles({
@@ -1,18 +0,0 @@
import React from "react";
import UsageBar from "../../../common/UsageBar";
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
export const NodeCPU: NodeFeatureComponent = ({ node }) => (
<div style={{ minWidth: 60 }}>
<UsageBar percent={node.cpu} text={`${node.cpu.toFixed(1)}%`} />
</div>
);
export const WorkerCPU: WorkerFeatureComponent = ({ worker }) => (
<div style={{ minWidth: 60 }}>
<UsageBar
percent={worker.cpu_percent}
text={`${worker.cpu_percent.toFixed(1)}%`}
/>
</div>
);
@@ -1,20 +0,0 @@
import Typography from "@material-ui/core/Typography";
import React from "react";
import { formatUsage } from "../../../common/formatUtils";
import UsageBar from "../../../common/UsageBar";
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
export const NodeDisk: NodeFeatureComponent = ({ node }) => (
<React.Fragment>
<UsageBar
percent={(100 * node.disk["/"].used) / node.disk["/"].total}
text={formatUsage(node.disk["/"].used, node.disk["/"].total, "gibibyte")}
/>
</React.Fragment>
);
export const WorkerDisk: WorkerFeatureComponent = () => (
<Typography color="textSecondary" component="span" variant="inherit">
Not available
</Typography>
);
@@ -1,18 +0,0 @@
import React from "react";
import { formatByteAmount, formatUsage } from "../../../common/formatUtils";
import UsageBar from "../../../common/UsageBar";
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
export const NodeRAM: NodeFeatureComponent = ({ node }) => (
<UsageBar
percent={(100 * (node.mem[0] - node.mem[1])) / node.mem[0]}
text={formatUsage(node.mem[0] - node.mem[1], node.mem[0], "gibibyte")}
/>
);
export const WorkerRAM: WorkerFeatureComponent = ({ node, worker }) => (
<UsageBar
percent={(100 * worker.memory_info.rss) / node.mem[0]}
text={formatByteAmount(worker.memory_info.rss, "mebibyte")}
/>
);
@@ -1,11 +0,0 @@
import React from "react";
import { formatUptime } from "../../../common/formatUtils";
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
export const NodeUptime: NodeFeatureComponent = ({ node }) => (
<React.Fragment>{formatUptime(node.boot_time)}</React.Fragment>
);
export const WorkerUptime: WorkerFeatureComponent = ({ worker }) => (
<React.Fragment>{formatUptime(worker.create_time)}</React.Fragment>
);
@@ -1,13 +0,0 @@
import React from "react";
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
export const NodeWorkers: NodeFeatureComponent = ({ node }) => (
<React.Fragment>{node.workers.length}</React.Fragment>
);
// Ray worker process titles have one of the following forms: `ray::IDLE`,
// `ray::function()`, `ray::Class`, or `ray::Class.method()`. We extract the
// second portion here for display in the "Workers" column.
export const WorkerWorkers: WorkerFeatureComponent = ({ worker }) => (
<React.Fragment>{worker.cmdline[0].split("::", 2)[1]}</React.Fragment>
);
@@ -0,0 +1,51 @@
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import createStyles from "@material-ui/core/styles/createStyles";
import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { connect } from "react-redux";
import { StoreState } from "../../../store";
const styles = (theme: Theme) =>
createStyles({
root: {
marginTop: theme.spacing(2)
},
lastUpdated: {
color: theme.palette.text.secondary,
fontSize: "0.8125rem",
textAlign: "center"
},
error: {
color: theme.palette.error.main,
fontSize: "0.8125rem",
textAlign: "center"
}
});
const mapStateToProps = (state: StoreState) => ({
lastUpdatedAt: state.dashboard.lastUpdatedAt,
error: state.dashboard.error
});
class LastUpdated extends React.Component<
WithStyles<typeof styles> & ReturnType<typeof mapStateToProps>
> {
render() {
const { classes, lastUpdatedAt, error } = this.props;
return (
<div className={classes.root}>
{lastUpdatedAt !== null && (
<Typography className={classes.lastUpdated}>
Last updated: {new Date(lastUpdatedAt).toLocaleString()}
</Typography>
)}
{error !== null && (
<Typography className={classes.error}>{error}</Typography>
)}
</div>
);
}
}
export default connect(mapStateToProps)(withStyles(styles)(LastUpdated));
@@ -0,0 +1,167 @@
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import createStyles from "@material-ui/core/styles/createStyles";
import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { connect } from "react-redux";
import { getNodeInfo } from "../../../api";
import { StoreState } from "../../../store";
import { dashboardActions } from "../state";
import LastUpdated from "./LastUpdated";
import NodeRowGroup from "./NodeRowGroup";
import TotalRow from "./TotalRow";
const styles = (theme: Theme) =>
createStyles({
root: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2),
"& > :not(:first-child)": {
marginTop: theme.spacing(2)
}
},
table: {
marginTop: theme.spacing(1)
},
cell: {
padding: theme.spacing(1),
textAlign: "center",
"&:last-child": {
paddingRight: theme.spacing(1)
}
}
});
const mapStateToProps = (state: StoreState) => ({
nodeInfo: state.dashboard.nodeInfo
});
const mapDispatchToProps = dashboardActions;
class NodeInfo extends React.Component<
WithStyles<typeof styles> &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps
> {
refreshNodeInfo = async () => {
try {
const nodeInfo = await getNodeInfo();
this.props.setNodeInfo(nodeInfo);
this.props.setError(null);
} catch (error) {
this.props.setError(error.toString());
} finally {
setTimeout(this.refreshNodeInfo, 1000);
}
};
async componentDidMount() {
await this.refreshNodeInfo();
}
render() {
const { classes, nodeInfo } = this.props;
if (nodeInfo === null) {
return (
<Typography className={classes.root} color="textSecondary">
Loading...
</Typography>
);
}
const logCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
} = {};
const errorCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
} = {};
for (const client of nodeInfo.clients) {
logCounts[client.ip] = { perWorker: {}, total: 0 };
errorCounts[client.ip] = { perWorker: {}, total: 0 };
for (const worker of client.workers) {
logCounts[client.ip].perWorker[worker.pid] = 0;
errorCounts[client.ip].perWorker[worker.pid] = 0;
}
}
for (const ip of Object.keys(nodeInfo.log_counts)) {
if (ip in logCounts) {
for (const [pid, count] of Object.entries(nodeInfo.log_counts[ip])) {
logCounts[ip].perWorker[pid] = count;
logCounts[ip].total += count;
}
}
}
for (const ip of Object.keys(nodeInfo.error_counts)) {
if (ip in errorCounts) {
for (const [pid, count] of Object.entries(nodeInfo.error_counts[ip])) {
errorCounts[ip].perWorker[pid] = count;
errorCounts[ip].total += count;
}
}
}
return (
<div>
<Typography>Node information:</Typography>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.cell} />
<TableCell className={classes.cell}>Host</TableCell>
<TableCell className={classes.cell}>Workers</TableCell>
<TableCell className={classes.cell}>Uptime</TableCell>
<TableCell className={classes.cell}>CPU</TableCell>
<TableCell className={classes.cell}>RAM</TableCell>
<TableCell className={classes.cell}>Disk</TableCell>
<TableCell className={classes.cell}>Sent</TableCell>
<TableCell className={classes.cell}>Received</TableCell>
<TableCell className={classes.cell}>Logs</TableCell>
<TableCell className={classes.cell}>Errors</TableCell>
</TableRow>
</TableHead>
<TableBody>
{nodeInfo.clients.map(client => (
<NodeRowGroup
key={client.ip}
node={client}
logCounts={logCounts[client.ip]}
errorCounts={errorCounts[client.ip]}
/>
))}
<TotalRow
nodes={nodeInfo.clients}
logCounts={logCounts}
errorCounts={errorCounts}
/>
</TableBody>
</Table>
<LastUpdated />
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(NodeInfo));
@@ -7,13 +7,15 @@ import AddIcon from "@material-ui/icons/Add";
import RemoveIcon from "@material-ui/icons/Remove";
import classNames from "classnames";
import React from "react";
import { NodeInfoResponse } from "../../api";
import { NodeInfoResponse } from "../../../api";
import { NodeCPU, WorkerCPU } from "./features/CPU";
import { NodeDisk, WorkerDisk } from "./features/Disk";
import { makeNodeErrors, makeWorkerErrors } from "./features/Errors";
import { NodeHost, WorkerHost } from "./features/Host";
import { makeNodeLogs, makeWorkerLogs } from "./features/Logs";
import { NodeRAM, WorkerRAM } from "./features/RAM";
import { NodeReceived, WorkerReceived } from "./features/Received";
import { NodeSent, WorkerSent } from "./features/Sent";
import { NodeUptime, WorkerUptime } from "./features/Uptime";
import { NodeWorkers, WorkerWorkers } from "./features/Workers";
@@ -80,6 +82,8 @@ class NodeRowGroup extends React.Component<
{ NodeFeature: NodeCPU, WorkerFeature: WorkerCPU },
{ NodeFeature: NodeRAM, WorkerFeature: WorkerRAM },
{ NodeFeature: NodeDisk, WorkerFeature: WorkerDisk },
{ NodeFeature: NodeSent, WorkerFeature: WorkerSent },
{ NodeFeature: NodeReceived, WorkerFeature: WorkerReceived },
{
NodeFeature: makeNodeLogs(logCounts),
WorkerFeature: makeWorkerLogs(logCounts)
@@ -103,8 +107,8 @@ class NodeRowGroup extends React.Component<
<RemoveIcon className={classes.expandCollapseIcon} />
)}
</TableCell>
{features.map(({ NodeFeature }) => (
<TableCell className={classes.cell}>
{features.map(({ NodeFeature }, index) => (
<TableCell className={classes.cell} key={index}>
<NodeFeature node={node} />
</TableCell>
))}
@@ -113,8 +117,8 @@ class NodeRowGroup extends React.Component<
node.workers.map((worker, index: number) => (
<TableRow hover key={index}>
<TableCell className={classes.cell} />
{features.map(({ WorkerFeature }) => (
<TableCell className={classes.cell}>
{features.map(({ WorkerFeature }, index) => (
<TableCell className={classes.cell} key={index}>
<WorkerFeature node={node} worker={worker} />
</TableCell>
))}
@@ -0,0 +1,87 @@
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import createStyles from "@material-ui/core/styles/createStyles";
import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import LayersIcon from "@material-ui/icons/Layers";
import React from "react";
import { NodeInfoResponse } from "../../../api";
import { ClusterCPU } from "./features/CPU";
import { ClusterDisk } from "./features/Disk";
import { makeClusterErrors } from "./features/Errors";
import { ClusterHost } from "./features/Host";
import { makeClusterLogs } from "./features/Logs";
import { ClusterRAM } from "./features/RAM";
import { ClusterReceived } from "./features/Received";
import { ClusterSent } from "./features/Sent";
import { ClusterUptime } from "./features/Uptime";
import { ClusterWorkers } from "./features/Workers";
const styles = (theme: Theme) =>
createStyles({
cell: {
borderTopColor: theme.palette.divider,
borderTopStyle: "solid",
borderTopWidth: 2,
padding: theme.spacing(1),
textAlign: "center",
"&:last-child": {
paddingRight: theme.spacing(1)
}
},
totalIcon: {
color: theme.palette.text.secondary,
fontSize: "1.5em",
verticalAlign: "middle"
}
});
interface Props {
nodes: NodeInfoResponse["clients"];
logCounts: {
[ip: string]: {
perWorker: { [pid: string]: number };
total: number;
};
};
errorCounts: {
[ip: string]: {
perWorker: { [pid: string]: number };
total: number;
};
};
}
class TotalRow extends React.Component<Props & WithStyles<typeof styles>> {
render() {
const { classes, nodes, logCounts, errorCounts } = this.props;
const features = [
{ ClusterFeature: ClusterHost },
{ ClusterFeature: ClusterWorkers },
{ ClusterFeature: ClusterUptime },
{ ClusterFeature: ClusterCPU },
{ ClusterFeature: ClusterRAM },
{ ClusterFeature: ClusterDisk },
{ ClusterFeature: ClusterSent },
{ ClusterFeature: ClusterReceived },
{ ClusterFeature: makeClusterLogs(logCounts) },
{ ClusterFeature: makeClusterErrors(errorCounts) }
];
return (
<TableRow hover>
<TableCell className={classes.cell}>
<LayersIcon className={classes.totalIcon} />
</TableCell>
{features.map(({ ClusterFeature }, index) => (
<TableCell className={classes.cell} key={index}>
<ClusterFeature nodes={nodes} />
</TableCell>
))}
</TableRow>
);
}
}
export default withStyles(styles)(TotalRow);
@@ -0,0 +1,55 @@
import React from "react";
import UsageBar from "../../../../common/UsageBar";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
const getWeightedAverage = (
input: {
weight: number;
value: number;
}[]
) => {
if (input.length === 0) {
return 0;
}
let totalWeightTimesValue = 0;
let totalWeight = 0;
for (const { weight, value } of input) {
totalWeightTimesValue += weight * value;
totalWeight += weight;
}
return totalWeightTimesValue / totalWeight;
};
export const ClusterCPU: ClusterFeatureComponent = ({ nodes }) => {
const cpuWeightedAverage = getWeightedAverage(
nodes.map(node => ({ weight: node.cpus[0], value: node.cpu }))
);
return (
<div style={{ minWidth: 60 }}>
<UsageBar
percent={cpuWeightedAverage}
text={`${cpuWeightedAverage.toFixed(1)}%`}
/>
</div>
);
};
export const NodeCPU: NodeFeatureComponent = ({ node }) => (
<div style={{ minWidth: 60 }}>
<UsageBar percent={node.cpu} text={`${node.cpu.toFixed(1)}%`} />
</div>
);
export const WorkerCPU: WorkerFeatureComponent = ({ worker }) => (
<div style={{ minWidth: 60 }}>
<UsageBar
percent={worker.cpu_percent}
text={`${worker.cpu_percent.toFixed(1)}%`}
/>
</div>
);
@@ -0,0 +1,37 @@
import Typography from "@material-ui/core/Typography";
import React from "react";
import { formatUsage } from "../../../../common/formatUtils";
import UsageBar from "../../../../common/UsageBar";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
export const ClusterDisk: ClusterFeatureComponent = ({ nodes }) => {
let used = 0;
let total = 0;
for (const node of nodes) {
used += node.disk["/"].used;
total += node.disk["/"].total;
}
return (
<UsageBar
percent={(100 * used) / total}
text={formatUsage(used, total, "gibibyte")}
/>
);
};
export const NodeDisk: NodeFeatureComponent = ({ node }) => (
<UsageBar
percent={(100 * node.disk["/"].used) / node.disk["/"].total}
text={formatUsage(node.disk["/"].used, node.disk["/"].total, "gibibyte")}
/>
);
export const WorkerDisk: WorkerFeatureComponent = () => (
<Typography color="textSecondary" component="span" variant="inherit">
N/A
</Typography>
);
@@ -2,7 +2,37 @@ import Link from "@material-ui/core/Link";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { Link as RouterLink } from "react-router-dom";
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
export const makeClusterErrors = (errorCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
}): ClusterFeatureComponent => ({ nodes }) => {
let totalErrorCount = 0;
for (const node of nodes) {
if (node.ip in errorCounts) {
totalErrorCount += errorCounts[node.ip].total;
}
}
return totalErrorCount === 0 ? (
<Typography color="textSecondary" component="span" variant="inherit">
No errors
</Typography>
) : (
<React.Fragment>
{totalErrorCount.toLocaleString()}{" "}
{totalErrorCount === 1 ? "error" : "errors"}
</React.Fragment>
);
};
export const makeNodeErrors = (errorCounts: {
perWorker: { [pid: string]: number };
@@ -1,5 +1,16 @@
import React from "react";
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
export const ClusterHost: ClusterFeatureComponent = ({ nodes }) => (
<React.Fragment>
Totals ({nodes.length.toLocaleString()}{" "}
{nodes.length === 1 ? "host" : "hosts"})
</React.Fragment>
);
export const NodeHost: NodeFeatureComponent = ({ node }) => (
<React.Fragment>
@@ -2,7 +2,36 @@ import Link from "@material-ui/core/Link";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { Link as RouterLink } from "react-router-dom";
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
export const makeClusterLogs = (logCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
}): ClusterFeatureComponent => ({ nodes }) => {
let totalLogCount = 0;
for (const node of nodes) {
if (node.ip in logCounts) {
totalLogCount += logCounts[node.ip].total;
}
}
return totalLogCount === 0 ? (
<Typography color="textSecondary" component="span" variant="inherit">
No logs
</Typography>
) : (
<React.Fragment>
{totalLogCount.toLocaleString()} {totalLogCount === 1 ? "line" : "lines"}
</React.Fragment>
);
};
export const makeNodeLogs = (logCounts: {
perWorker: { [pid: string]: number };
@@ -0,0 +1,37 @@
import React from "react";
import { formatByteAmount, formatUsage } from "../../../../common/formatUtils";
import UsageBar from "../../../../common/UsageBar";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
export const ClusterRAM: ClusterFeatureComponent = ({ nodes }) => {
let used = 0;
let total = 0;
for (const node of nodes) {
used += node.mem[0] - node.mem[1];
total += node.mem[0];
}
return (
<UsageBar
percent={(100 * used) / total}
text={formatUsage(used, total, "gibibyte")}
/>
);
};
export const NodeRAM: NodeFeatureComponent = ({ node }) => (
<UsageBar
percent={(100 * (node.mem[0] - node.mem[1])) / node.mem[0]}
text={formatUsage(node.mem[0] - node.mem[1], node.mem[0], "gibibyte")}
/>
);
export const WorkerRAM: WorkerFeatureComponent = ({ node, worker }) => (
<UsageBar
percent={(100 * worker.memory_info.rss) / node.mem[0]}
text={formatByteAmount(worker.memory_info.rss, "mebibyte")}
/>
);
@@ -0,0 +1,30 @@
import Typography from "@material-ui/core/Typography";
import React from "react";
import { formatByteAmount } from "../../../../common/formatUtils";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
export const ClusterReceived: ClusterFeatureComponent = ({ nodes }) => {
let totalReceived = 0;
for (const node of nodes) {
totalReceived += node.net[1];
}
return (
<React.Fragment>
{formatByteAmount(totalReceived, "mebibyte")}/s
</React.Fragment>
);
};
export const NodeReceived: NodeFeatureComponent = ({ node }) => (
<React.Fragment>{formatByteAmount(node.net[1], "mebibyte")}/s</React.Fragment>
);
export const WorkerReceived: WorkerFeatureComponent = () => (
<Typography color="textSecondary" component="span" variant="inherit">
N/A
</Typography>
);
@@ -0,0 +1,28 @@
import Typography from "@material-ui/core/Typography";
import React from "react";
import { formatByteAmount } from "../../../../common/formatUtils";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
export const ClusterSent: ClusterFeatureComponent = ({ nodes }) => {
let totalSent = 0;
for (const node of nodes) {
totalSent += node.net[0];
}
return (
<React.Fragment>{formatByteAmount(totalSent, "mebibyte")}/s</React.Fragment>
);
};
export const NodeSent: NodeFeatureComponent = ({ node }) => (
<React.Fragment>{formatByteAmount(node.net[0], "mebibyte")}/s</React.Fragment>
);
export const WorkerSent: WorkerFeatureComponent = () => (
<Typography color="textSecondary" component="span" variant="inherit">
N/A
</Typography>
);
@@ -0,0 +1,26 @@
import Typography from "@material-ui/core/Typography";
import React from "react";
import { formatDuration } from "../../../../common/formatUtils";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
const getUptime = (bootTime: number) => Date.now() / 1000 - bootTime;
export const ClusterUptime: ClusterFeatureComponent = ({ nodes }) => (
<Typography color="textSecondary" component="span" variant="inherit">
N/A
</Typography>
);
export const NodeUptime: NodeFeatureComponent = ({ node }) => (
<React.Fragment>{formatDuration(getUptime(node.boot_time))}</React.Fragment>
);
export const WorkerUptime: WorkerFeatureComponent = ({ worker }) => (
<React.Fragment>
{formatDuration(getUptime(worker.create_time))}
</React.Fragment>
);
@@ -0,0 +1,40 @@
import React from "react";
import {
ClusterFeatureComponent,
NodeFeatureComponent,
WorkerFeatureComponent
} from "./types";
export const ClusterWorkers: ClusterFeatureComponent = ({ nodes }) => {
let totalWorkers = 0;
let totalCpus = 0;
for (const node of nodes) {
totalWorkers += node.workers.length;
totalCpus += node.cpus[0];
}
return (
<React.Fragment>
{totalWorkers.toLocaleString()}{" "}
{totalWorkers === 1 ? "worker" : "workers"} / {totalCpus.toLocaleString()}{" "}
{totalCpus === 1 ? "core" : "cores"}
</React.Fragment>
);
};
export const NodeWorkers: NodeFeatureComponent = ({ node }) => {
const workers = node.workers.length;
const cpus = node.cpus[0];
return (
<React.Fragment>
{workers.toLocaleString()} {workers === 1 ? "worker" : "workers"} /{" "}
{cpus.toLocaleString()} {cpus === 1 ? "core" : "cores"}
</React.Fragment>
);
};
// Ray worker process titles have one of the following forms: `ray::IDLE`,
// `ray::function()`, `ray::Class`, or `ray::Class.method()`. We extract the
// second portion here for display in the "Workers" column.
export const WorkerWorkers: WorkerFeatureComponent = ({ worker }) => (
<React.Fragment>{worker.cmdline[0].split("::", 2)[1]}</React.Fragment>
);
@@ -1,13 +1,17 @@
import React from "react";
import { NodeInfoResponse } from "../../../api";
import { NodeInfoResponse } from "../../../../api";
type ArrayType<T> = T extends Array<infer U> ? U : never;
type Node = ArrayType<NodeInfoResponse["clients"]>;
type Worker = ArrayType<Node["workers"]>;
type ClusterFeatureData = { nodes: Node[] };
type NodeFeatureData = { node: Node };
type WorkerFeatureData = { node: Node; worker: Worker };
export type ClusterFeatureComponent = (
data: ClusterFeatureData
) => React.ReactElement;
export type NodeFeatureComponent = (
data: NodeFeatureData
) => React.ReactElement;
@@ -0,0 +1,132 @@
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import createStyles from "@material-ui/core/styles/createStyles";
import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography";
import classNames from "classnames";
import React from "react";
import { connect } from "react-redux";
import { getRayConfig } from "../../../api";
import { StoreState } from "../../../store";
import { dashboardActions } from "../state";
const styles = (theme: Theme) =>
createStyles({
table: {
marginTop: theme.spacing(1),
width: "auto"
},
cell: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3),
textAlign: "center",
"&:last-child": {
paddingRight: theme.spacing(3)
}
},
key: {
color: theme.palette.text.secondary
}
});
const mapStateToProps = (state: StoreState) => ({
rayConfig: state.dashboard.rayConfig
});
const mapDispatchToProps = dashboardActions;
class RayConfig extends React.Component<
WithStyles<typeof styles> &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps
> {
refreshRayConfig = async () => {
try {
const rayConfig = await getRayConfig();
this.props.setRayConfig(rayConfig);
} catch (error) {
} finally {
setTimeout(this.refreshRayConfig, 10 * 1000);
}
};
async componentDidMount() {
await this.refreshRayConfig();
}
render() {
const { classes, rayConfig } = this.props;
if (rayConfig === null) {
return null;
}
const formattedRayConfig = [
{
key: "Autoscaling mode",
value: rayConfig.autoscaling_mode
},
{
key: "Head node type",
value: rayConfig.head_type
},
{
key: "Worker node type",
value: rayConfig.worker_type
},
{
key: "Min worker nodes",
value: rayConfig.min_workers
},
{
key: "Initial worker nodes",
value: rayConfig.initial_workers
},
{
key: "Max worker nodes",
value: rayConfig.max_workers
},
{
key: "Idle timeout",
value: `${rayConfig.idle_timeout_minutes} ${
rayConfig.idle_timeout_minutes === 1 ? "minute" : "minutes"
}`
}
];
return (
<div>
<Typography>Ray cluster configuration:</Typography>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.cell}>Setting</TableCell>
<TableCell className={classes.cell}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{formattedRayConfig.map(({ key, value }, index) => (
<TableRow key={index}>
<TableCell className={classNames(classes.cell, classes.key)}>
{key}
</TableCell>
<TableCell className={classes.cell}>{value}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(RayConfig));
@@ -1,15 +1,17 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { NodeInfoResponse } from "../../api";
import { NodeInfoResponse, RayConfigResponse } from "../../api";
const name = "dashboard";
interface State {
rayConfig: RayConfigResponse | null;
nodeInfo: NodeInfoResponse | null;
lastUpdatedAt: number | null;
error: string | null;
}
const initialState: State = {
rayConfig: null,
nodeInfo: null,
lastUpdatedAt: null,
error: null
@@ -19,11 +21,14 @@ const slice = createSlice({
name,
initialState,
reducers: {
setRayConfig: (state, action: PayloadAction<RayConfigResponse>) => {
state.rayConfig = action.payload;
},
setNodeInfo: (state, action: PayloadAction<NodeInfoResponse>) => {
state.nodeInfo = action.payload;
state.lastUpdatedAt = Date.now();
},
setError: (state, action: PayloadAction<string>) => {
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
}
}
+2 -51
View File
@@ -21,8 +21,6 @@ import time
import traceback
import yaml
from pathlib import Path
from collections import Counter
from collections import defaultdict
from operator import itemgetter
from typing import Dict
@@ -113,8 +111,8 @@ class Dashboard(object):
async def ray_config(_) -> aiohttp.web.Response:
try:
with open(
Path("~/ray_bootstrap_config.yaml").expanduser()) as f:
config_path = os.path.expanduser("~/ray_bootstrap_config.yaml")
with open(config_path) as f:
cfg = yaml.safe_load(f)
except Exception:
return await json_response(error="No config")
@@ -213,51 +211,6 @@ class NodeStats(threading.Thread):
super().__init__()
def calculate_totals(self) -> Dict:
total_boot_time = 0
total_cpus = 0
total_workers = 0
total_load = [0.0, 0.0, 0.0]
total_storage_avail = 0
total_storage_total = 0
total_ram_avail = 0
total_ram_total = 0
total_sent = 0
total_recv = 0
for v in self._node_stats.values():
total_boot_time += v["boot_time"]
total_cpus += v["cpus"][0]
total_workers += len(v["workers"])
total_load[0] += v["load_avg"][0][0]
total_load[1] += v["load_avg"][0][1]
total_load[2] += v["load_avg"][0][2]
total_storage_avail += v["disk"]["/"]["free"]
total_storage_total += v["disk"]["/"]["total"]
total_ram_avail += v["mem"][1]
total_ram_total += v["mem"][0]
total_sent += v["net"][0]
total_recv += v["net"][1]
return {
"boot_time": total_boot_time,
"n_workers": total_workers,
"n_cores": total_cpus,
"m_avail": total_ram_avail,
"m_total": total_ram_total,
"d_avail": total_storage_avail,
"d_total": total_storage_total,
"load": total_load,
"n_sent": total_sent,
"n_recv": total_recv,
}
def calculate_tasks(self) -> Counter:
return Counter(
(x["name"]
for y in (v["workers"] for v in self._node_stats.values())
for x in y))
def calculate_log_counts(self):
return {
ip: {
@@ -296,8 +249,6 @@ class NodeStats(threading.Thread):
(v for v in self._node_stats.values()),
key=itemgetter("boot_time"))
return {
"totals": self.calculate_totals(),
"tasks": self.calculate_tasks(),
"clients": node_stats,
"log_counts": self.calculate_log_counts(),
"error_counts": self.calculate_error_counts(),
+2 -2
View File
@@ -112,8 +112,8 @@ class Reporter(object):
def get_workers():
return [
x.as_dict(attrs=[
"pid", "create_time", "cpu_percent", "cpu_times", "name",
"cmdline", "memory_info", "memory_full_info"
"pid", "create_time", "cpu_percent", "cpu_times", "cmdline",
"memory_info", "memory_full_info"
]) for x in psutil.process_iter(attrs=["cmdline"])
if is_worker(x.info["cmdline"])
]