mirror of
https://github.com/wassname/ray.git
synced 2026-06-28 00:29:38 +08:00
[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:
committed by
Philipp Moritz
parent
5f36e6eacb
commit
9f96091aef
@@ -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));
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user