diff --git a/python/ray/dashboard/client/src/api.ts b/python/ray/dashboard/client/src/api.ts index afe6c9358..d650f6fef 100644 --- a/python/ray/dashboard/client/src/api.ts +++ b/python/ray/dashboard/client/src/api.ts @@ -87,12 +87,40 @@ export interface NodeInfoResponse { export const getNodeInfo = () => get("/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; + }; }; } diff --git a/python/ray/dashboard/client/src/pages/dashboard/Dashboard.tsx b/python/ray/dashboard/client/src/pages/dashboard/Dashboard.tsx index 899ac5518..49fdc3fd1 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/Dashboard.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/Dashboard.tsx @@ -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> { +const mapStateToProps = (state: StoreState) => ({ + tab: state.dashboard.tab +}); + +const mapDispatchToProps = dashboardActions; + +class Dashboard extends React.Component< + WithStyles & + ReturnType & + 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 (
Ray Dashboard - - + + {tabs.map(({ label }) => ( + + ))} + + +
); } } -export default withStyles(styles)(Dashboard); +export default connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(Dashboard)); diff --git a/python/ray/dashboard/client/src/pages/dashboard/node-info/LastUpdated.tsx b/python/ray/dashboard/client/src/pages/dashboard/LastUpdated.tsx similarity index 96% rename from python/ray/dashboard/client/src/pages/dashboard/node-info/LastUpdated.tsx rename to python/ray/dashboard/client/src/pages/dashboard/LastUpdated.tsx index 130178c05..40d3c1ab5 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/node-info/LastUpdated.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/LastUpdated.tsx @@ -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({ diff --git a/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx new file mode 100644 index 000000000..364adfe18 --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx @@ -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, 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 ( +
+ + {actor.state !== -1 ? ( + + Actor {actor.actorId}{" "} + {Object.entries(actor.children).length > 0 && ( + + ( + + {expanded ? "Collapse" : "Expand"} + + ) + + )} + + ) : ( + Infeasible actor + )} + + + {information.map( + ({ label, value }) => + value && + value.length > 0 && ( + + + {label}: {value} + {" "} + + ) + )} + + {actor.state !== -1 && ( + + {actor.webuiDisplay && ( + + {actor.webuiDisplay} + + )} + + + + + )} +
+ ); + } +} + +export default withStyles(styles)(Actor); diff --git a/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx new file mode 100644 index 000000000..16be65a9c --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx @@ -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> { + render() { + const { actors } = this.props; + return Object.entries(actors).map(([actorId, actor]) => ( + + )); + } +} + +export default withStyles(styles)(Actors); diff --git a/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx b/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx new file mode 100644 index 000000000..f528274ca --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx @@ -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 & ReturnType +> { + render() { + const { rayletInfo } = this.props; + + if (rayletInfo === null) { + return Loading...; + } + + if (Object.entries(rayletInfo.actors).length === 0) { + return No actors found.; + } + + return ( +
+ +
+ ); + } +} + +export default connect(mapStateToProps)(withStyles(styles)(LogicalView)); diff --git a/python/ray/dashboard/client/src/pages/dashboard/node-info/NodeInfo.tsx b/python/ray/dashboard/client/src/pages/dashboard/node-info/NodeInfo.tsx index 0cbf6bf88..a84b92e87 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/node-info/NodeInfo.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/node-info/NodeInfo.tsx @@ -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 & - ReturnType & - typeof mapDispatchToProps + WithStyles & ReturnType > { - 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 ( - - Loading... - - ); + return Loading...; } const logCounts: { @@ -125,49 +88,46 @@ class NodeInfo extends React.Component< } return ( -
- Node information: - - - - - Host - Workers - Uptime - CPU - RAM - Disk - Sent - Received - Logs - Errors - - - - {nodeInfo.clients.map(client => ( - - ))} - + + + + Host + Workers + Uptime + CPU + RAM + Disk + Sent + Received + Logs + Errors + + + + {nodeInfo.clients.map(client => ( + - -
- -
+ ))} + + + ); } } -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(NodeInfo)); +export default connect(mapStateToProps)(withStyles(styles)(NodeInfo)); diff --git a/python/ray/dashboard/client/src/pages/dashboard/node-info/NodeRowGroup.tsx b/python/ray/dashboard/client/src/pages/dashboard/node-info/NodeRowGroup.tsx index 56dc43f1b..53785dfa2 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/node-info/NodeRowGroup.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/node-info/NodeRowGroup.tsx @@ -47,7 +47,7 @@ type Node = ArrayType; interface Props { node: Node; - raylet: RayletInfoResponse[keyof RayletInfoResponse] | null; + raylet: RayletInfoResponse["nodes"][keyof RayletInfoResponse["nodes"]] | null; logCounts: { perWorker: { [pid: string]: number }; total: number; diff --git a/python/ray/dashboard/client/src/pages/dashboard/ray-config/RayConfig.tsx b/python/ray/dashboard/client/src/pages/dashboard/ray-config/RayConfig.tsx index 6a3419e4b..c0a90a4d4 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/ray-config/RayConfig.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/ray-config/RayConfig.tsx @@ -64,7 +64,11 @@ class RayConfig extends React.Component< const { classes, rayConfig } = this.props; if (rayConfig === null) { - return null; + return ( + + No Ray configuration detected. + + ); } const formattedRayConfig = [ diff --git a/python/ray/dashboard/client/src/pages/dashboard/state.ts b/python/ray/dashboard/client/src/pages/dashboard/state.ts index 6e51c548e..a81bfa214 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/state.ts +++ b/python/ray/dashboard/client/src/pages/dashboard/state.ts @@ -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) => { + state.tab = action.payload; + }, setRayConfig: (state, action: PayloadAction) => { state.rayConfig = action.payload; }, - setNodeInfoAndRayletInfo: ( + setNodeAndRayletInfo: ( state, action: PayloadAction<{ nodeInfo: NodeInfoResponse; diff --git a/python/ray/dashboard/dashboard.py b/python/ray/dashboard/dashboard.py index eeb6a8aac..a355cbf45 100644 --- a/python/ray/dashboard/dashboard.py +++ b/python/ray/dashboard/dashboard.py @@ -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") diff --git a/python/ray/tests/test_metrics.py b/python/ray/tests/test_metrics.py index 563a2eaf1..3c3d48bb6 100644 --- a/python/ray/tests/test_metrics.py +++ b/python/ray/tests/test_metrics.py @@ -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()