[Dashboard] Add logical view displaying actor tree (#6810)

* [Dashboard] Add logical view displaying actor tree

* Fix key error in test_raylet_info_endpoint
This commit is contained in:
Mitchell Stern
2020-01-17 10:25:27 -08:00
committed by Philipp Moritz
parent 5f36e6eacb
commit 9f96091aef
12 changed files with 384 additions and 98 deletions
+34 -6
View File
@@ -87,12 +87,40 @@ export interface NodeInfoResponse {
export const getNodeInfo = () => get<NodeInfoResponse>("/api/node_info", {});
export interface RayletInfoResponse {
[ip: string]: {
extraInfo?: string;
workersStats: {
isDriver?: boolean;
pid: number;
}[];
nodes: {
[ip: string]: {
extraInfo?: string;
workersStats: {
pid: number;
isDriver?: boolean;
}[];
};
};
actors: {
[actorId: string]:
| {
actorId: string;
children: RayletInfoResponse["actors"];
ipAddress: string;
isDirectCall: boolean;
jobId: string;
numLocalObjects: number;
numObjectIdsInScope: number;
port: number;
state: 0 | 1 | 2;
taskQueueLength: number;
usedObjectStoreMemory: number;
usedResources: { [key: string]: number };
currentTaskDesc?: string;
currentTaskFuncDesc?: string[];
numPendingTasks?: number;
webuiDisplay?: string;
}
| {
actorId: string;
requiredResources: { [key: string]: number };
state: -1;
};
};
}
@@ -1,10 +1,18 @@
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 Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { connect } from "react-redux";
import { getNodeInfo, getRayletInfo } from "../../api";
import { StoreState } from "../../store";
import LastUpdated from "./LastUpdated";
import LogicalView from "./logical-view/LogicalView";
import NodeInfo from "./node-info/NodeInfo";
import RayConfig from "./ray-config/RayConfig";
import { dashboardActions } from "./state";
const styles = (theme: Theme) =>
createStyles({
@@ -14,20 +22,78 @@ const styles = (theme: Theme) =>
"& > :not(:first-child)": {
marginTop: theme.spacing(4)
}
},
tabs: {
borderBottomColor: theme.palette.divider,
borderBottomStyle: "solid",
borderBottomWidth: 1
}
});
class Dashboard extends React.Component<WithStyles<typeof styles>> {
const mapStateToProps = (state: StoreState) => ({
tab: state.dashboard.tab
});
const mapDispatchToProps = dashboardActions;
class Dashboard extends React.Component<
WithStyles<typeof styles> &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps
> {
refreshNodeAndRayletInfo = async () => {
try {
const [nodeInfo, rayletInfo] = await Promise.all([
getNodeInfo(),
getRayletInfo()
]);
this.props.setNodeAndRayletInfo({ nodeInfo, rayletInfo });
this.props.setError(null);
} catch (error) {
this.props.setError(error.toString());
} finally {
setTimeout(this.refreshNodeAndRayletInfo, 1000);
}
};
async componentDidMount() {
await this.refreshNodeAndRayletInfo();
}
handleTabChange = (event: React.ChangeEvent<{}>, value: number) => {
this.props.setTab(value);
};
render() {
const { classes } = this.props;
const { classes, tab } = this.props;
const tabs = [
{ label: "Machine view", component: NodeInfo },
{ label: "Logical view", component: LogicalView },
{ label: "Ray config", component: RayConfig }
];
const SelectedComponent = tabs[tab].component;
return (
<div className={classes.root}>
<Typography variant="h5">Ray Dashboard</Typography>
<NodeInfo />
<RayConfig />
<Tabs
className={classes.tabs}
indicatorColor="primary"
onChange={this.handleTabChange}
textColor="primary"
value={tab}
>
{tabs.map(({ label }) => (
<Tab key={label} label={label} />
))}
</Tabs>
<SelectedComponent />
<LastUpdated />
</div>
);
}
}
export default withStyles(styles)(Dashboard);
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Dashboard));
@@ -4,7 +4,7 @@ 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";
import { StoreState } from "../../store";
const styles = (theme: Theme) =>
createStyles({
@@ -0,0 +1,162 @@
import Typography from "@material-ui/core/Typography";
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 React from "react";
import { RayletInfoResponse } from "../../../api";
import Actors from "./Actors";
import Collapse from "@material-ui/core/Collapse";
const styles = (theme: Theme) =>
createStyles({
root: {
borderColor: theme.palette.divider,
borderStyle: "solid",
borderWidth: 1,
marginTop: theme.spacing(2),
padding: theme.spacing(2)
},
title: {
color: theme.palette.text.secondary,
fontSize: "0.75rem"
},
infeasible: {
color: theme.palette.error.main
},
information: {
fontSize: "0.875rem"
},
datum: {
"&:not(:first-child)": {
marginLeft: theme.spacing(2)
}
},
webuiDisplay: {
fontSize: "0.875rem"
},
expandCollapseButton: {
color: theme.palette.primary.main,
"&:hover": {
cursor: "pointer"
}
}
});
interface Props {
actor: RayletInfoResponse["actors"][keyof RayletInfoResponse["actors"]];
}
interface State {
expanded: boolean;
}
class Actor extends React.Component<Props & WithStyles<typeof styles>, State> {
state: State = {
expanded: true
};
setExpanded = (expanded: boolean) => () => {
this.setState({ expanded });
};
render() {
const { classes, actor } = this.props;
const { expanded } = this.state;
const information =
actor.state !== -1
? [
{
label: "ID",
value: actor.actorId
},
{
label: "Resources",
value:
Object.entries(actor.usedResources).length > 0 &&
Object.entries(actor.usedResources)
.map(([key, value]) => `${value.toLocaleString()} ${key}`)
.join(", ")
},
{
label: "Pending",
value:
actor.taskQueueLength !== undefined &&
actor.taskQueueLength > 0 &&
actor.taskQueueLength.toLocaleString()
},
{
label: "Task",
value:
actor.currentTaskFuncDesc && actor.currentTaskFuncDesc.join(".")
}
]
: [
{
label: "ID",
value: actor.actorId
},
{
label: "Required resources",
value:
Object.entries(actor.requiredResources).length > 0 &&
Object.entries(actor.requiredResources)
.map(([key, value]) => `${value.toLocaleString()} ${key}`)
.join(", ")
}
];
return (
<div className={classes.root}>
<Typography className={classes.title}>
{actor.state !== -1 ? (
<React.Fragment>
Actor {actor.actorId}{" "}
{Object.entries(actor.children).length > 0 && (
<React.Fragment>
(
<span
className={classes.expandCollapseButton}
onClick={this.setExpanded(!expanded)}
>
{expanded ? "Collapse" : "Expand"}
</span>
)
</React.Fragment>
)}
</React.Fragment>
) : (
<span className={classes.infeasible}>Infeasible actor</span>
)}
</Typography>
<Typography className={classes.information}>
{information.map(
({ label, value }) =>
value &&
value.length > 0 && (
<React.Fragment key={label}>
<span className={classes.datum}>
{label}: {value}
</span>{" "}
</React.Fragment>
)
)}
</Typography>
{actor.state !== -1 && (
<React.Fragment>
{actor.webuiDisplay && (
<Typography className={classes.webuiDisplay}>
{actor.webuiDisplay}
</Typography>
)}
<Collapse in={expanded}>
<Actors actors={actor.children} />
</Collapse>
</React.Fragment>
)}
</div>
);
}
}
export default withStyles(styles)(Actor);
@@ -0,0 +1,23 @@
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 React from "react";
import { RayletInfoResponse } from "../../../api";
import Actor from "./Actor";
const styles = (theme: Theme) => createStyles({});
interface Props {
actors: RayletInfoResponse["actors"];
}
class Actors extends React.Component<Props & WithStyles<typeof styles>> {
render() {
const { actors } = this.props;
return Object.entries(actors).map(([actorId, actor]) => (
<Actor actor={actor} key={actorId} />
));
}
}
export default withStyles(styles)(Actors);
@@ -0,0 +1,38 @@
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";
import Actors from "./Actors";
const styles = (theme: Theme) => createStyles({});
const mapStateToProps = (state: StoreState) => ({
rayletInfo: state.dashboard.rayletInfo
});
class LogicalView extends React.Component<
WithStyles<typeof styles> & ReturnType<typeof mapStateToProps>
> {
render() {
const { rayletInfo } = this.props;
if (rayletInfo === null) {
return <Typography color="textSecondary">Loading...</Typography>;
}
if (Object.entries(rayletInfo.actors).length === 0) {
return <Typography color="textSecondary">No actors found.</Typography>;
}
return (
<div>
<Actors actors={rayletInfo.actors} />
</div>
);
}
}
export default connect(mapStateToProps)(withStyles(styles)(LogicalView));
@@ -9,22 +9,12 @@ 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, getRayletInfo } 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)
},
@@ -42,41 +32,14 @@ const mapStateToProps = (state: StoreState) => ({
rayletInfo: state.dashboard.rayletInfo
});
const mapDispatchToProps = dashboardActions;
class NodeInfo extends React.Component<
WithStyles<typeof styles> &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps
WithStyles<typeof styles> & ReturnType<typeof mapStateToProps>
> {
refreshNodeInfo = async () => {
try {
const [nodeInfo, rayletInfo] = await Promise.all([
getNodeInfo(),
getRayletInfo()
]);
this.props.setNodeInfoAndRayletInfo({ nodeInfo, rayletInfo });
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, rayletInfo } = this.props;
if (nodeInfo === null || rayletInfo === null) {
return (
<Typography className={classes.root} color="textSecondary">
Loading...
</Typography>
);
return <Typography color="textSecondary">Loading...</Typography>;
}
const logCounts: {
@@ -125,49 +88,46 @@ class NodeInfo extends React.Component<
}
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}
raylet={client.ip in rayletInfo ? rayletInfo[client.ip] : null}
logCounts={logCounts[client.ip]}
errorCounts={errorCounts[client.ip]}
initialExpanded={nodeInfo.clients.length <= 4}
/>
))}
<TotalRow
nodes={nodeInfo.clients}
logCounts={logCounts}
errorCounts={errorCounts}
<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}
raylet={
client.ip in rayletInfo.nodes
? rayletInfo.nodes[client.ip]
: null
}
logCounts={logCounts[client.ip]}
errorCounts={errorCounts[client.ip]}
initialExpanded={nodeInfo.clients.length <= 4}
/>
</TableBody>
</Table>
<LastUpdated />
</div>
))}
<TotalRow
nodes={nodeInfo.clients}
logCounts={logCounts}
errorCounts={errorCounts}
/>
</TableBody>
</Table>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(NodeInfo));
export default connect(mapStateToProps)(withStyles(styles)(NodeInfo));
@@ -47,7 +47,7 @@ type Node = ArrayType<NodeInfoResponse["clients"]>;
interface Props {
node: Node;
raylet: RayletInfoResponse[keyof RayletInfoResponse] | null;
raylet: RayletInfoResponse["nodes"][keyof RayletInfoResponse["nodes"]] | null;
logCounts: {
perWorker: { [pid: string]: number };
total: number;
@@ -64,7 +64,11 @@ class RayConfig extends React.Component<
const { classes, rayConfig } = this.props;
if (rayConfig === null) {
return null;
return (
<Typography color="textSecondary">
No Ray configuration detected.
</Typography>
);
}
const formattedRayConfig = [
@@ -8,6 +8,7 @@ import {
const name = "dashboard";
interface State {
tab: number;
rayConfig: RayConfigResponse | null;
nodeInfo: NodeInfoResponse | null;
rayletInfo: RayletInfoResponse | null;
@@ -16,6 +17,7 @@ interface State {
}
const initialState: State = {
tab: 0,
rayConfig: null,
nodeInfo: null,
rayletInfo: null,
@@ -27,10 +29,13 @@ const slice = createSlice({
name,
initialState,
reducers: {
setTab: (state, action: PayloadAction<number>) => {
state.tab = action.payload;
},
setRayConfig: (state, action: PayloadAction<RayConfigResponse>) => {
state.rayConfig = action.payload;
},
setNodeInfoAndRayletInfo: (
setNodeAndRayletInfo: (
state,
action: PayloadAction<{
nodeInfo: NodeInfoResponse;
+2 -2
View File
@@ -239,8 +239,8 @@ class Dashboard(object):
to_print.append(line +
(max_line_length - len(line)) * " ")
data["extraInfo"] += "\n" + "\n".join(to_print)
D["actorInfo"] = actor_tree
return await json_response(result=D)
result = {"nodes": D, "actors": actor_tree}
return await json_response(result=result)
async def logs(req) -> aiohttp.web.Response:
hostname = req.query.get("hostname")
+1 -1
View File
@@ -149,7 +149,7 @@ def test_raylet_info_endpoint(shutdown_only):
webui_url = addresses["webui_url"]
webui_url = webui_url.replace("localhost", "http://127.0.0.1")
raylet_info = requests.get(webui_url + "/api/raylet_info").json()
actor_info = raylet_info["result"]["actorInfo"]
actor_info = raylet_info["result"]["actors"]
try:
assert len(actor_info) == 1
_, parent_actor_info = actor_info.popitem()