diff --git a/python/ray/dashboard/client/src/api.ts b/python/ray/dashboard/client/src/api.ts index d2e78160f..7ed6b5ad6 100644 --- a/python/ray/dashboard/client/src/api.ts +++ b/python/ray/dashboard/client/src/api.ts @@ -160,7 +160,11 @@ export type RayletActorInfo = numObjectIdsInScope: number; pid: number; port: number; - state: 0 | 1 | 2; + state: + | ActorState.Creating + | ActorState.Alive + | ActorState.Restarting + | ActorState.Dead; taskQueueLength: number; timestamp: number; usedObjectStoreMemory: number; @@ -173,10 +177,20 @@ export type RayletActorInfo = actorId: string; actorTitle: string; requiredResources: { [key: string]: number }; - state: -1; - invalidStateType?: "infeasibleActor" | "pendingActor"; + state: ActorState.Invalid; + invalidStateType?: InvalidStateType; }; +export type InvalidStateType = "infeasibleActor" | "pendingActor"; + +export enum ActorState { + Invalid = -1, + Creating = 0, + Alive = 1, + Restarting = 2, + Dead = 3, +} + export type RayletInfoResponse = { nodes: { [ip: string]: { 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 index 46d161b51..3d21d4e5f 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx @@ -8,6 +8,7 @@ import { } from "@material-ui/core"; import React from "react"; import { + ActorState, checkProfilingStatus, CheckProfilingStatusResponse, getProfilingResultURL, @@ -15,8 +16,11 @@ import { launchProfiling, RayletActorInfo, } from "../../../api"; +import ActorDetailsPane from "./ActorDetailsPane"; import Actors from "./Actors"; +const memoryDebuggingDocLink = + "https://docs.ray.io/en/latest/memory-management.html#debugging-using-ray-memory"; const styles = (theme: Theme) => createStyles({ root: { @@ -43,14 +47,7 @@ const styles = (theme: Theme) => invalidStateTypePendingActor: { color: theme.palette.secondary.main, }, - information: { - fontSize: "0.875rem", - }, - datum: { - "&:not(:first-child)": { - marginLeft: theme.spacing(2), - }, - }, + webuiDisplay: { fontSize: "0.875rem", }, @@ -86,7 +83,7 @@ class Actor extends React.Component, State> { handleProfilingClick = (duration: number) => async () => { const actor = this.props.actor; - if (actor.state !== -1) { + if (actor.state !== ActorState.Invalid) { const profilingId = await launchProfiling( actor.nodeId, actor.pid, @@ -119,7 +116,10 @@ class Actor extends React.Component, State> { killActor = () => { const actor = this.props.actor; - if (actor.state === 0) { + if ( + actor.state === ActorState.Creating || + actor.state === ActorState.Alive + ) { launchKillActor(actor.actorId, actor.ipAddress, actor.port); } }; @@ -129,16 +129,8 @@ class Actor extends React.Component, State> { const { expanded, profiling } = this.state; const information = - actor.state !== -1 + actor.state !== ActorState.Invalid ? [ - { - label: "ActorTitle", - value: actor.actorTitle, - }, - { - label: "State", - value: actor.state.toLocaleString(), - }, { label: "Resources", value: @@ -149,34 +141,48 @@ class Actor extends React.Component, State> { .join(", "), }, { - label: "Pending", + label: "Number of pending tasks", value: actor.taskQueueLength.toLocaleString(), + tooltip: + "The number of tasks that are currently pending to execute on this actor. If this number " + + "remains consistently high, it may indicate that this actor is a bottleneck in your application.", }, { - label: "Executed", + label: "Number of executed tasks", value: actor.numExecutedTasks.toLocaleString(), + tooltip: + "The number of tasks this actor has executed throughout its lifetimes.", }, { - label: "NumObjectIdsInScope", + label: "Number of ObjectIDs in scope", value: actor.numObjectIdsInScope.toLocaleString(), + tooltip: + "The number of ObjectIDs that this actor is keeping in scope via its internal state. " + + "This does not imply that the objects are in active use or colocated on the node with the actor " + + `currently. This can be useful for debugging memory leaks. See the docs at ${memoryDebuggingDocLink} ` + + "for more information.", }, { - label: "NumLocalObjects", + label: "Number of local objects", value: actor.numLocalObjects.toLocaleString(), + tooltip: + "The number of small objects that this actor has stored in its local in-process memory store. This can be useful for " + + `debugging memory leaks. See the docs at ${memoryDebuggingDocLink} for more information`, }, { - label: "UsedLocalObjectMemory", + label: "Object store memory used (MiB)", value: actor.usedObjectStoreMemory.toLocaleString(), + tooltip: + "The total amount of memory that this actor is occupying in the Ray object store. " + + "If this number is increasing without bounds, you might have a memory leak. See " + + `the docs at: ${memoryDebuggingDocLink} for more information.`, }, - // { - // label: "Task", - // value: actor.currentTaskFuncDesc.join(".") - // } ] : [ { - label: "ID", + label: "Actor ID", value: actor.actorId, + tooltip: "", }, { label: "Required resources", @@ -186,12 +192,13 @@ class Actor extends React.Component, State> { .sort((a, b) => a[0].localeCompare(b[0])) .map(([key, value]) => `${value.toLocaleString()} ${key}`) .join(", "), + tooltip: "", }, ]; // Construct the custom message from the actor. let actorCustomDisplay: JSX.Element[] = []; - if (actor.state !== -1 && actor.webuiDisplay) { + if (actor.state !== ActorState.Invalid && actor.webuiDisplay) { actorCustomDisplay = Object.keys(actor.webuiDisplay) .sort() .map((key, _, __) => { @@ -228,7 +235,7 @@ class Actor extends React.Component, State> { return (
- {actor.state !== -1 ? ( + {actor.state !== ActorState.Invalid ? ( Actor {actor.actorId}{" "} {Object.entries(actor.children).length > 0 && ( @@ -289,8 +296,8 @@ class Actor extends React.Component, State> { ) : actor.invalidStateType === "infeasibleActor" ? ( - {actor.actorTitle} is infeasible. (This actor cannot be created - because the Ray cluster cannot satisfy its resource requirements.) + {actor.actorTitle} cannot be created because the Ray cluster + cannot satisfy its resource requirements.) ) : ( @@ -298,20 +305,12 @@ class Actor extends React.Component, State> { )} - - {information.map( - ({ label, value }) => - value && - value.length > 0 && ( - - - {label}: {value} - {" "} - - ), - )} - - {actor.state !== -1 && ( + + {actor.state !== ActorState.Invalid && ( {actorCustomDisplay.length > 0 && ( {actorCustomDisplay} diff --git a/python/ray/dashboard/client/src/pages/dashboard/logical-view/ActorDetailsPane.tsx b/python/ray/dashboard/client/src/pages/dashboard/logical-view/ActorDetailsPane.tsx new file mode 100644 index 000000000..1ed47d5ef --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/ActorDetailsPane.tsx @@ -0,0 +1,155 @@ +import { + createStyles, + Divider, + Grid, + makeStyles, + Theme, + Tooltip, +} from "@material-ui/core"; +import React from "react"; +import { ActorState, InvalidStateType } from "../../../api"; + +type LabeledDatumProps = { + label: string; + datum: any; + tooltip?: string; +}; + +const useLabeledDatumStyles = makeStyles({ + label: { + textDecorationLine: "underline", + textDecorationColor: "#a6c3e3", + textDecorationThickness: "1px", + textDecorationStyle: "dotted", + cursor: "help", + }, +}); + +const LabeledDatum: React.FC = ({ + label, + datum, + tooltip, +}) => { + const classes = useLabeledDatumStyles(); + const innerHtml = ( + + + {label} + + + {datum} + + + ); + return tooltip ? {innerHtml} : innerHtml; +}; + +type ActorStateReprProps = { + state: ActorState; + ist?: InvalidStateType; +}; + +const actorStateReprStyles = makeStyles((theme: Theme) => + createStyles({ + infeasible: { + color: theme.palette.error.light, + }, + pending: { + color: theme.palette.warning.light, + }, + unknown: { + color: theme.palette.warning.light, + }, + creating: { + color: theme.palette.success.light, + }, + alive: { + color: theme.palette.success.dark, + }, + restarting: { + color: theme.palette.warning.light, + }, + dead: { + color: "#cccccc", + }, + }), +); + +const ActorStateRepr: React.FC = ({ state, ist }) => { + const classes = actorStateReprStyles(); + const { Alive, Dead, Creating, Restarting, Invalid } = ActorState; + switch (state) { + case Invalid: + if (ist === "infeasibleActor") { + return
Infeasible
; + } + if (ist === "pendingActor") { + return
Pending Resources
; + } + return
Unknown
; + case Creating: + return
Creating
; + case Alive: + return
Alive
; + case Restarting: + return
Restarting
; + case Dead: + return
Dead
; + } +}; + +type ActorDetailsPaneProps = { + actorTitle: string; + invalidStateType?: InvalidStateType; + actorState: ActorState; + actorDetails: { + label: string; + value: any; + tooltip?: string; + }[]; +}; + +const useStyles = makeStyles((theme: Theme) => ({ + divider: { + width: "100%", + margin: "0 auto", + }, + actorTitleWrapper: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + fontWeight: "bold", + fontSize: "130%", + }, + detailsPane: { + margin: theme.spacing(1), + }, +})); + +const ActorDetailsPane: React.FC = ({ + actorTitle, + actorDetails, + actorState, + invalidStateType, +}) => { + const classes = useStyles(); + return ( + +
+
{actorTitle}
+ +
+ + + {actorDetails.map( + ({ label, value, tooltip }) => + value && + value.length > 0 && ( + + ), + )} + +
+ ); +}; + +export default ActorDetailsPane; 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 index c8b2652c5..20e21673e 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx @@ -1,5 +1,5 @@ import React, { Fragment } from "react"; -import { RayletInfoResponse } from "../../../api"; +import { ActorState, RayletInfoResponse } from "../../../api"; import Actor from "./Actor"; type ActorProps = { @@ -8,10 +8,20 @@ type ActorProps = { const Actors = (props: ActorProps) => { const { actors } = props; - - const actorChildren = Object.entries(actors).map(([actorId, actor]) => ( - - )); + const actorChildren = Object.values(actors) + .sort((actor1, actor2) => { + if ( + actor1.state === ActorState.Dead && + actor2.state === ActorState.Dead + ) { + return 0; + } else if (actor2.state === ActorState.Dead) { + return -1; + } else { + return 1; + } + }) + .map((actor) => ); return {actorChildren}; }; 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 index f7d45a7ae..0e5e0a44c 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx @@ -7,7 +7,7 @@ import { } from "@material-ui/core"; import React, { useState } from "react"; import { connect } from "react-redux"; -import { RayletActorInfo, RayletInfoResponse } from "../../../api"; +import { ActorState, RayletActorInfo, RayletInfoResponse } from "../../../api"; import { StoreState } from "../../../store"; import Actors from "./Actors"; @@ -29,7 +29,7 @@ const getNestedActorTitles = (actor: RayletActorInfo): string[] => { const actorTitle = actor.actorTitle; const titles: string[] = actorTitle ? [actorTitle] : []; // state of -1 indicates an actor data record that does not have children. - if (actor.state === -1) { + if (actor.state === ActorState.Invalid) { return titles; } const children = actor["children"]; @@ -53,7 +53,7 @@ type LogicalViewProps = { rayletInfo: RayletInfoResponse | null; } & ReturnType; -const LogicalView = ({ rayletInfo }: LogicalViewProps) => { +const LogicalView: React.FC = ({ rayletInfo }) => { const [nameFilter, setNameFilter] = useState(""); if (rayletInfo === null) {