mirror of
https://github.com/wassname/ray.git
synced 2026-06-28 14:48:54 +08:00
[Dashboard] Add initial version of new dashboard (#5730)
This commit is contained in:
committed by
Robert Nishihara
parent
56ab9a00bb
commit
98dcc1d440
@@ -38,6 +38,10 @@ NUMPY_VERSIONS=("1.14.5"
|
||||
mkdir -p $DOWNLOAD_DIR
|
||||
mkdir -p .whl
|
||||
|
||||
# Use the latest version of Node.js in order to build the dashboard.
|
||||
source $HOME/.nvm/nvm.sh
|
||||
nvm use node
|
||||
|
||||
for ((i=0; i<${#PY_VERSIONS[@]}; ++i)); do
|
||||
PY_VERSION=${PY_VERSIONS[i]}
|
||||
PY_INST=${PY_INSTS[i]}
|
||||
@@ -58,10 +62,16 @@ for ((i=0; i<${#PY_VERSIONS[@]}; ++i)); do
|
||||
PIP_CMD="$(dirname $PYTHON_EXE)/pip$PY_MM"
|
||||
|
||||
pushd /tmp
|
||||
# Install latest version of pip to avoid brownouts
|
||||
# Install latest version of pip to avoid brownouts.
|
||||
curl https://bootstrap.pypa.io/get-pip.py | $PYTHON_EXE
|
||||
popd
|
||||
|
||||
# Build the dashboard so its static assets can be included in the wheel.
|
||||
pushd python/ray/dashboard/client
|
||||
npm ci
|
||||
npm run build
|
||||
popd
|
||||
|
||||
pushd python
|
||||
# Setuptools on CentOS is too old to install arrow 0.9.0, therefore we upgrade.
|
||||
$PIP_CMD install --upgrade setuptools
|
||||
|
||||
@@ -35,14 +35,28 @@ export PATH=$PATH:/root/bin
|
||||
rm -f /usr/bin/python2
|
||||
ln -s /opt/python/cp27-cp27m/bin/python2 /usr/bin/python2
|
||||
|
||||
# Install and use the latest version of Node.js in order to build the dashboard.
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
|
||||
source $HOME/.nvm/nvm.sh
|
||||
nvm install node
|
||||
nvm use node
|
||||
|
||||
mkdir .whl
|
||||
for ((i=0; i<${#PYTHONS[@]}; ++i)); do
|
||||
PYTHON=${PYTHONS[i]}
|
||||
NUMPY_VERSION=${NUMPY_VERSIONS[i]}
|
||||
|
||||
# The -f flag is passed twice to also run git clean in the arrow subdirectory.
|
||||
# The -d flag removes directories. The -x flag ignores the .gitignore file,
|
||||
# and the -e flag ensures that we don't remove the .whl directory.
|
||||
git clean -f -f -x -d -e .whl
|
||||
|
||||
# Build the dashboard so its static assets can be included in the wheel.
|
||||
pushd python/ray/dashboard/client
|
||||
npm ci
|
||||
npm run build
|
||||
popd
|
||||
|
||||
pushd python
|
||||
# Fix the numpy version because this will be the oldest numpy version we can
|
||||
# support.
|
||||
|
||||
@@ -88,6 +88,9 @@ setup_commands:
|
||||
# Install basics.
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y build-essential curl unzip
|
||||
# Install Node.js in order to build the dashboard.
|
||||
- curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash
|
||||
- sudo apt-get install -y nodejs
|
||||
# Install Anaconda.
|
||||
- wget https://repo.continuum.io/archive/Anaconda3-5.0.1-Linux-x86_64.sh || true
|
||||
- bash Anaconda3-5.0.1-Linux-x86_64.sh -b -p $HOME/anaconda3 || true
|
||||
@@ -95,7 +98,8 @@ setup_commands:
|
||||
# Build Ray.
|
||||
- git clone https://github.com/ray-project/ray || true
|
||||
- ray/ci/travis/install-bazel.sh
|
||||
- pip install boto3==1.4.8 cython==0.29.0
|
||||
- cd ray/python/ray/dashboard/client; npm ci; npm run build
|
||||
- pip install boto3==1.4.8 cython==0.29.0 aiohttp psutil setproctitle
|
||||
- cd ray/python; pip install -e . --verbose
|
||||
|
||||
# Custom commands that will be run on the head node after common setup.
|
||||
@@ -107,7 +111,7 @@ worker_setup_commands: []
|
||||
# Command to start ray on the head node. You don't need to change this.
|
||||
head_start_ray_commands:
|
||||
- ray stop
|
||||
- ulimit -n 65536; ray start --head --num-redis-shards=10 --redis-port=6379 --autoscaling-config=~/ray_bootstrap_config.yaml
|
||||
- ulimit -n 65536; ray start --head --include-webui --num-redis-shards=10 --redis-port=6379 --autoscaling-config=~/ray_bootstrap_config.yaml
|
||||
|
||||
# Command to start ray on worker nodes. You don't need to change this.
|
||||
worker_start_ray_commands:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,65 @@
|
||||
This project was bootstrapped with `Create React App
|
||||
<https://github.com/facebook/create-react-app>`__.
|
||||
|
||||
Available Scripts
|
||||
-----------------
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
``npm start``
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Runs the app in the development mode. Open `http://localhost:3000
|
||||
<http://localhost:3000>`__ to view it in the browser.
|
||||
|
||||
The page will reload if you make edits. You will also see any lint errors in the
|
||||
console.
|
||||
|
||||
``npm test``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Launches the test runner in the interactive watch mode. See the section about
|
||||
`running tests
|
||||
<https://facebook.github.io/create-react-app/docs/running-tests>`__ for more
|
||||
information.
|
||||
|
||||
``npm run build``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Builds the app for production to the ``build`` folder. It correctly bundles
|
||||
React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes. Your app is ready to
|
||||
be deployed!
|
||||
|
||||
See the section about `deployment
|
||||
<https://facebook.github.io/create-react-app/docs/deployment>`__ for more
|
||||
information.
|
||||
|
||||
``npm run eject``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Note: this is a one-way operation. Once you ``eject``, you can’t go back!
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can
|
||||
``eject`` at any time. This command will remove the single build dependency from
|
||||
your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive
|
||||
dependencies (Webpack, Babel, ESLint, etc) right into your project so you have
|
||||
full control over them. All of the commands except ``eject`` will still work,
|
||||
but they will point to the copied scripts so you can tweak them. At this point
|
||||
you’re on your own.
|
||||
|
||||
You don’t have to ever use ``eject``. The curated feature set is suitable for
|
||||
small and middle deployments, and you shouldn’t feel obligated to use this
|
||||
feature. However we understand that this tool wouldn’t be useful if you couldn’t
|
||||
customize it when you are ready for it.
|
||||
|
||||
Learn More
|
||||
----------
|
||||
|
||||
You can learn more in the `Create React App documentation
|
||||
<https://facebook.github.io/create-react-app/docs/getting-started>`__.
|
||||
|
||||
To learn React, check out the `React documentation <https://reactjs.org/>`__.
|
||||
+13467
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.3.3",
|
||||
"@material-ui/icons": "^4.2.1",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/jest": "24.0.18",
|
||||
"@types/node": "12.7.2",
|
||||
"@types/react": "16.9.2",
|
||||
"@types/react-dom": "16.9.0",
|
||||
"@types/react-router-dom": "^4.3.5",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-scripts": "3.1.1",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"typescript": "3.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^1.18.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Ray Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||
import React from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import Dashboard from "./Dashboard";
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<CssBaseline />
|
||||
<Dashboard />
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,492 @@
|
||||
import Link from "@material-ui/core/Link";
|
||||
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 AddIcon from "@material-ui/icons/Add";
|
||||
import RemoveIcon from "@material-ui/icons/Remove";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { Route } from "react-router";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import Errors from "./Errors";
|
||||
import Logs from "./Logs";
|
||||
import UsageBar from "./UsageBar";
|
||||
|
||||
const formatByteAmount = (amount: number, unit: "mebibyte" | "gibibyte") =>
|
||||
`${(
|
||||
amount / (unit === "mebibyte" ? Math.pow(1024, 2) : Math.pow(1024, 3))
|
||||
).toFixed(1)} ${unit === "mebibyte" ? "MiB" : "GiB"}`;
|
||||
|
||||
const formatUsage = (
|
||||
used: number,
|
||||
total: number,
|
||||
unit: "mebibyte" | "gibibyte"
|
||||
) => {
|
||||
const usedFormatted = formatByteAmount(used, unit);
|
||||
const totalFormatted = formatByteAmount(total, unit);
|
||||
const percent = (100 * used) / total;
|
||||
return `${usedFormatted} / ${totalFormatted} (${percent.toFixed(0)}%)`;
|
||||
};
|
||||
|
||||
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);
|
||||
const pad = (value: number) => value.toString().padStart(2, "0");
|
||||
return [
|
||||
uptimeDays ? `${uptimeDays}d` : "",
|
||||
`${pad(uptimeHours)}h`,
|
||||
`${pad(uptimeMinutes)}m`,
|
||||
`${pad(uptimeSeconds)}s`
|
||||
].join(" ");
|
||||
};
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
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)
|
||||
}
|
||||
},
|
||||
expandCollapseCell: {
|
||||
cursor: "pointer"
|
||||
},
|
||||
expandCollapseIcon: {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: "1.5em",
|
||||
verticalAlign: "middle"
|
||||
},
|
||||
cpuUsage: {
|
||||
minWidth: 60
|
||||
},
|
||||
secondary: {
|
||||
color: theme.palette.text.secondary
|
||||
}
|
||||
});
|
||||
|
||||
// TODO(mitchellstern): Add JSON schema validation for the node info.
|
||||
interface NodeInfo {
|
||||
clients: Array<{
|
||||
now: number;
|
||||
hostname: string;
|
||||
ip: string;
|
||||
boot_time: number;
|
||||
cpu: number;
|
||||
cpus: [number, number];
|
||||
mem: [number, number, number];
|
||||
disk: {
|
||||
[path: string]: {
|
||||
total: number;
|
||||
free: number;
|
||||
used: number;
|
||||
percent: number;
|
||||
};
|
||||
};
|
||||
load_avg: [[number, number, number], [number, number, number]];
|
||||
net: [number, number];
|
||||
workers: Array<{
|
||||
pid: number;
|
||||
create_time: number;
|
||||
name: string;
|
||||
cmdline: string[];
|
||||
cpu_percent: number;
|
||||
cpu_times: {
|
||||
system: number;
|
||||
children_system: number;
|
||||
user: number;
|
||||
children_user: number;
|
||||
};
|
||||
memory_info: {
|
||||
pageins: number;
|
||||
pfaults: number;
|
||||
vms: number;
|
||||
rss: number;
|
||||
};
|
||||
memory_full_info: null;
|
||||
}>;
|
||||
}>;
|
||||
logs: {
|
||||
[ip: string]: {
|
||||
[pid: string]: string[];
|
||||
};
|
||||
};
|
||||
errors: {
|
||||
[jobId: string]: Array<{
|
||||
message: string;
|
||||
timestamp: number;
|
||||
type: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface State {
|
||||
response: {
|
||||
result: NodeInfo;
|
||||
timestamp: number;
|
||||
} | null;
|
||||
error: string | null;
|
||||
expanded: {
|
||||
[hostname: string]: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
class Component extends React.Component<WithStyles<typeof styles>, State> {
|
||||
state: State = {
|
||||
response: null,
|
||||
error: null,
|
||||
expanded: {}
|
||||
};
|
||||
|
||||
fetchNodeInfo = async () => {
|
||||
try {
|
||||
const url = new URL(
|
||||
"/api/node_info",
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:8080"
|
||||
: window.location.href
|
||||
);
|
||||
const response = await fetch(url.toString());
|
||||
const json = await response.json();
|
||||
this.setState({ response: json, error: null });
|
||||
} catch (error) {
|
||||
this.setState({ response: null, error: error.toString() });
|
||||
} finally {
|
||||
setTimeout(this.fetchNodeInfo, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
toggleExpand = (hostname: string) => () => {
|
||||
this.setState(state => ({
|
||||
expanded: {
|
||||
...state.expanded,
|
||||
[hostname]: !state.expanded[hostname]
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
await this.fetchNodeInfo();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const { response, error, expanded } = this.state;
|
||||
|
||||
if (error !== null) {
|
||||
return (
|
||||
<Typography className={classes.root} color="error">
|
||||
{error}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
if (response === null) {
|
||||
return (
|
||||
<Typography className={classes.root} color="textSecondary">
|
||||
Loading...
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const { result, timestamp } = response;
|
||||
|
||||
const logCounts: {
|
||||
[hostname: string]: {
|
||||
perWorker: {
|
||||
[pid: string]: number;
|
||||
};
|
||||
total: number;
|
||||
};
|
||||
} = {};
|
||||
|
||||
const errorCounts: {
|
||||
[hostname: string]: {
|
||||
perWorker: {
|
||||
[pid: string]: number;
|
||||
};
|
||||
total: number;
|
||||
};
|
||||
} = {};
|
||||
|
||||
for (const client of result.clients) {
|
||||
logCounts[client.hostname] = { perWorker: {}, total: 0 };
|
||||
errorCounts[client.hostname] = { perWorker: {}, total: 0 };
|
||||
for (const worker of client.workers) {
|
||||
logCounts[client.hostname].perWorker[worker.pid] = 0;
|
||||
errorCounts[client.hostname].perWorker[worker.pid] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (const ip of Object.keys(result.logs)) {
|
||||
let hostname: string | null = null;
|
||||
for (const client of result.clients) {
|
||||
if (ip === client.ip) {
|
||||
hostname = client.hostname;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hostname !== null) {
|
||||
for (const pid of Object.keys(result.logs[ip])) {
|
||||
const logCount = result.logs[ip][pid].length;
|
||||
if (pid in logCounts[hostname].perWorker) {
|
||||
logCounts[hostname].perWorker[pid] = logCount;
|
||||
}
|
||||
logCounts[hostname].total += logCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const jobErrors of Object.values(result.errors)) {
|
||||
for (const error of jobErrors) {
|
||||
const match = error.message.match(/\(pid=(\d+), host=(.*?)\)/);
|
||||
if (match !== null) {
|
||||
const pid = match[1];
|
||||
const hostname = match[2];
|
||||
if (hostname in errorCounts) {
|
||||
if (pid in errorCounts[hostname].perWorker) {
|
||||
errorCounts[hostname].perWorker[pid]++;
|
||||
}
|
||||
errorCounts[hostname].total++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ipToHostname: { [ip: string]: string } = {};
|
||||
for (const client of result.clients) {
|
||||
ipToHostname[client.ip] = client.hostname;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h5">Ray Dashboard</Typography>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cell} />
|
||||
<TableCell className={classes.cell}>Hostname</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>
|
||||
{result.clients.map(client => {
|
||||
return (
|
||||
<React.Fragment key={client.hostname}>
|
||||
<TableRow hover>
|
||||
<TableCell
|
||||
className={classNames(
|
||||
classes.cell,
|
||||
classes.expandCollapseCell
|
||||
)}
|
||||
onClick={this.toggleExpand(client.hostname)}
|
||||
>
|
||||
{!expanded[client.hostname] ? (
|
||||
<AddIcon className={classes.expandCollapseIcon} />
|
||||
) : (
|
||||
<RemoveIcon className={classes.expandCollapseIcon} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{client.hostname}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{client.workers.length}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{formatUptime(client.boot_time)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<div className={classes.cpuUsage}>
|
||||
<UsageBar
|
||||
percent={client.cpu}
|
||||
text={`${client.cpu.toFixed(1)}%`}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<UsageBar
|
||||
percent={
|
||||
(100 * (client.mem[0] - client.mem[1])) /
|
||||
client.mem[0]
|
||||
}
|
||||
text={formatUsage(
|
||||
client.mem[0] - client.mem[1],
|
||||
client.mem[0],
|
||||
"gibibyte"
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<UsageBar
|
||||
percent={
|
||||
(100 * client.disk["/"].used) / client.disk["/"].total
|
||||
}
|
||||
text={formatUsage(
|
||||
client.disk["/"].used,
|
||||
client.disk["/"].total,
|
||||
"gibibyte"
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
{/*<TableCell className={classes.cell}>{(client.net[0] / Math.pow(1024, 2)).toFixed(3)} MiB/s</TableCell>*/}
|
||||
{/*<TableCell className={classes.cell}>{(client.net[1] / Math.pow(1024, 2)).toFixed(3)} MiB/s</TableCell>*/}
|
||||
<TableCell className={classes.cell}>
|
||||
{logCounts[client.hostname].total === 0 ? (
|
||||
<span className={classes.secondary}>No logs</span>
|
||||
) : (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/logs/${client.hostname}`}
|
||||
>
|
||||
View all logs (
|
||||
{logCounts[client.hostname].total.toLocaleString()}{" "}
|
||||
{logCounts[client.hostname].total === 1
|
||||
? "line"
|
||||
: "lines"}
|
||||
)
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{errorCounts[client.hostname].total === 0 ? (
|
||||
<span className={classes.secondary}>No errors</span>
|
||||
) : (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/errors/${client.hostname}`}
|
||||
>
|
||||
View all errors (
|
||||
{errorCounts[client.hostname].total.toLocaleString()})
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{expanded[client.hostname] &&
|
||||
client.workers.map((worker, index: number) => (
|
||||
<TableRow hover key={index}>
|
||||
<TableCell className={classes.cell} />
|
||||
<TableCell className={classes.cell}>
|
||||
{worker.cmdline[0].split(":", 2)[0]} (PID:{" "}
|
||||
{worker.pid})
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{worker.cmdline[0].split(":", 2)[1] || (
|
||||
<span className={classes.secondary}>Idle</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{formatUptime(worker.create_time)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<UsageBar
|
||||
percent={worker.cpu_percent}
|
||||
text={`${worker.cpu_percent.toFixed(1)}%`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<UsageBar
|
||||
percent={
|
||||
(100 * worker.memory_info.rss) / client.mem[0]
|
||||
}
|
||||
text={formatByteAmount(
|
||||
worker.memory_info.rss,
|
||||
"mebibyte"
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<span className={classes.secondary}>
|
||||
Not available
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{logCounts[client.hostname].perWorker[worker.pid] ===
|
||||
0 ? (
|
||||
<span className={classes.secondary}>No logs</span>
|
||||
) : (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/logs/${client.hostname}/${worker.pid}`}
|
||||
>
|
||||
View log (
|
||||
{logCounts[client.hostname].perWorker[
|
||||
worker.pid
|
||||
].toLocaleString()}{" "}
|
||||
{logCounts[client.hostname].perWorker[
|
||||
worker.pid
|
||||
] === 1
|
||||
? "line"
|
||||
: "lines"}
|
||||
)
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{errorCounts[client.hostname].perWorker[
|
||||
worker.pid
|
||||
] === 0 ? (
|
||||
<span className={classes.secondary}>No errors</span>
|
||||
) : (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/errors/${client.hostname}/${worker.pid}`}
|
||||
>
|
||||
View errors (
|
||||
{errorCounts[client.hostname].perWorker[
|
||||
worker.pid
|
||||
].toLocaleString()}
|
||||
)
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Typography align="center">
|
||||
Last updated: {new Date(timestamp * 1000).toLocaleString()}
|
||||
</Typography>
|
||||
<Route
|
||||
path="/logs/:hostname/:pid?"
|
||||
render={props => (
|
||||
<Logs {...props} ipToHostname={ipToHostname} logs={result.logs} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/errors/:hostname/:pid?"
|
||||
render={props => <Errors {...props} errors={result.errors} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
@@ -0,0 +1,135 @@
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
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 CloseIcon from "@material-ui/icons/Close";
|
||||
import React from "react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import NumberedLines from "./NumberedLines";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
padding: theme.spacing(3)
|
||||
},
|
||||
closeButton: {
|
||||
position: "absolute",
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
zIndex: 1
|
||||
},
|
||||
title: {
|
||||
borderBottomColor: theme.palette.divider,
|
||||
borderBottomStyle: "solid",
|
||||
borderBottomWidth: 1,
|
||||
lineHeight: 1,
|
||||
marginBottom: theme.spacing(3),
|
||||
paddingBottom: theme.spacing(3),
|
||||
position: "relative",
|
||||
"&:not(:first-of-type)": {
|
||||
marginTop: theme.spacing(6)
|
||||
}
|
||||
},
|
||||
error: {
|
||||
"&:not(:last-child)": {
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
},
|
||||
timestamp: {
|
||||
marginBottom: theme.spacing(1)
|
||||
}
|
||||
});
|
||||
|
||||
interface Props {
|
||||
errors: {
|
||||
[jobId: string]: Array<{
|
||||
message: string;
|
||||
timestamp: number;
|
||||
type: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
class Component extends React.Component<
|
||||
Props &
|
||||
WithStyles<typeof styles> &
|
||||
RouteComponentProps<{ hostname: string; pid: string | undefined }>
|
||||
> {
|
||||
handleClose = () => {
|
||||
this.props.history.push("/");
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, errors, match } = this.props;
|
||||
const { hostname, pid } = match.params;
|
||||
|
||||
let errorsForHost: {
|
||||
[pid: string]: Array<{
|
||||
lines: string[];
|
||||
timestamp: number;
|
||||
}>;
|
||||
} = {};
|
||||
|
||||
for (const jobErrors of Object.values(errors)) {
|
||||
for (const error of jobErrors) {
|
||||
const match = error.message.match(/\(pid=(\d+), host=(.*?)\)/);
|
||||
if (match !== null && match[2] === hostname) {
|
||||
const pid = match[1];
|
||||
if (!(pid in errorsForHost)) {
|
||||
errorsForHost[pid] = [];
|
||||
}
|
||||
errorsForHost[pid].push({
|
||||
lines: error.message
|
||||
.replace(/\u001b\[\d+m/g, "") // eslint-disable-line no-control-regex
|
||||
.trim()
|
||||
.split("\n"),
|
||||
timestamp: error.timestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const errorsToDisplay =
|
||||
pid === undefined
|
||||
? errorsForHost
|
||||
: { [pid]: pid in errorsForHost ? errorsForHost[pid] : [] };
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
classes={{ paper: classes.paper }}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
onClose={this.handleClose}
|
||||
open
|
||||
scroll="body"
|
||||
>
|
||||
<IconButton className={classes.closeButton} onClick={this.handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
{Object.entries(errorsToDisplay).map(([pid, errors]) => (
|
||||
<React.Fragment key={pid}>
|
||||
<Typography className={classes.title}>
|
||||
{hostname} (PID: {pid})
|
||||
</Typography>
|
||||
{errors.length > 0 ? (
|
||||
errors.map(({ lines, timestamp }, index) => (
|
||||
<div className={classes.error} key={index}>
|
||||
<Typography className={classes.timestamp}>
|
||||
Error at {new Date(timestamp * 1000).toLocaleString()}
|
||||
</Typography>
|
||||
<NumberedLines lines={lines} />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<Typography color="textSecondary">No errors found.</Typography>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
@@ -0,0 +1,108 @@
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
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 CloseIcon from "@material-ui/icons/Close";
|
||||
import React from "react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import NumberedLines from "./NumberedLines";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
padding: theme.spacing(3)
|
||||
},
|
||||
closeButton: {
|
||||
position: "absolute",
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
zIndex: 1
|
||||
},
|
||||
title: {
|
||||
borderBottomColor: theme.palette.divider,
|
||||
borderBottomStyle: "solid",
|
||||
borderBottomWidth: 1,
|
||||
lineHeight: 1,
|
||||
marginBottom: theme.spacing(3),
|
||||
paddingBottom: theme.spacing(3),
|
||||
position: "relative",
|
||||
"&:not(:first-of-type)": {
|
||||
marginTop: theme.spacing(3)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interface Props {
|
||||
ipToHostname: {
|
||||
[ip: string]: string;
|
||||
};
|
||||
logs: {
|
||||
[ip: string]: {
|
||||
[pid: string]: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
class Component extends React.Component<
|
||||
Props &
|
||||
WithStyles<typeof styles> &
|
||||
RouteComponentProps<{ hostname: string; pid: string | undefined }>
|
||||
> {
|
||||
handleClose = () => {
|
||||
this.props.history.push("/");
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, ipToHostname, logs, match } = this.props;
|
||||
const { hostname, pid } = match.params;
|
||||
|
||||
let logsForHost: {
|
||||
[pid: string]: string[];
|
||||
} = {};
|
||||
|
||||
for (const ip of Object.keys(ipToHostname)) {
|
||||
if (ipToHostname[ip] === hostname) {
|
||||
if (ip in logs) {
|
||||
logsForHost = logs[ip];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const logsToDisplay =
|
||||
pid === undefined
|
||||
? logsForHost
|
||||
: { [pid]: pid in logsForHost ? logsForHost[pid] : [] };
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
classes={{ paper: classes.paper }}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
onClose={this.handleClose}
|
||||
open
|
||||
scroll="body"
|
||||
>
|
||||
<IconButton className={classes.closeButton} onClick={this.handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
{Object.entries(logsToDisplay).map(([pid, lines]) => (
|
||||
<React.Fragment key={pid}>
|
||||
<Typography className={classes.title}>
|
||||
{hostname} (PID: {pid})
|
||||
</Typography>
|
||||
{lines.length > 0 ? (
|
||||
<NumberedLines lines={lines} />
|
||||
) : (
|
||||
<Typography color="textSecondary">No logs found.</Typography>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
@@ -0,0 +1,69 @@
|
||||
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 TableRow from "@material-ui/core/TableRow";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
overflowX: "auto"
|
||||
},
|
||||
cell: {
|
||||
borderWidth: 0,
|
||||
fontFamily: "SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace",
|
||||
padding: 0,
|
||||
"&:last-child": {
|
||||
paddingRight: 0
|
||||
}
|
||||
},
|
||||
lineNumber: {
|
||||
color: theme.palette.text.secondary,
|
||||
paddingRight: theme.spacing(2),
|
||||
textAlign: "right",
|
||||
verticalAlign: "top",
|
||||
width: "1%",
|
||||
// Use a ::before pseudo-element for the line number so that it won't
|
||||
// interact with user selections or searching.
|
||||
"&::before": {
|
||||
content: "attr(data-line-number)"
|
||||
}
|
||||
},
|
||||
line: {
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-wrap"
|
||||
}
|
||||
});
|
||||
|
||||
interface Props {
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
class Component extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
render() {
|
||||
const { classes, lines } = this.props;
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
{lines.map((line, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell
|
||||
className={classNames(classes.cell, classes.lineNumber)}
|
||||
data-line-number={index + 1}
|
||||
/>
|
||||
<TableCell className={classNames(classes.cell, classes.line)}>
|
||||
{line}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
@@ -0,0 +1,64 @@
|
||||
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";
|
||||
|
||||
const blend = (
|
||||
[r1, g1, b1]: number[],
|
||||
[r2, g2, b2]: number[],
|
||||
ratio: number
|
||||
) => [
|
||||
r1 * (1 - ratio) + r2 * ratio,
|
||||
g1 * (1 - ratio) + g2 * ratio,
|
||||
b1 * (1 - ratio) + b2 * ratio
|
||||
];
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
borderColor: theme.palette.divider,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1
|
||||
}
|
||||
});
|
||||
|
||||
interface Props {
|
||||
percent: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
class Component extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
render() {
|
||||
const { classes, text } = this.props;
|
||||
|
||||
let { percent } = this.props;
|
||||
percent = Math.max(percent, 0);
|
||||
percent = Math.min(percent, 100);
|
||||
|
||||
const minColor = [0, 255, 0];
|
||||
const maxColor = [255, 0, 0];
|
||||
|
||||
const leftColor = minColor;
|
||||
const rightColor = blend(minColor, maxColor, percent / 100);
|
||||
const alpha = 0.2;
|
||||
|
||||
const gradient = `
|
||||
linear-gradient(
|
||||
to right,
|
||||
rgba(${leftColor.join(",")}, ${alpha}) 0%,
|
||||
rgba(${rightColor.join(",")}, ${alpha}) ${percent}%,
|
||||
transparent ${percent}%
|
||||
)
|
||||
`;
|
||||
|
||||
// Use a nested `div` here because the right border is affected by the
|
||||
// gradient background otherwise.
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div style={{ background: gradient }}>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "typeface-roboto";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -4,8 +4,8 @@ from __future__ import print_function
|
||||
|
||||
try:
|
||||
import aiohttp.web
|
||||
except ModuleNotFoundError:
|
||||
print("The reporter requires aiohttp to run.")
|
||||
except ImportError:
|
||||
print("The dashboard requires aiohttp to run.")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
@@ -20,9 +20,11 @@ import yaml
|
||||
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
from typing import Dict
|
||||
|
||||
import ray
|
||||
import ray.ray_constants as ray_constants
|
||||
import ray.utils
|
||||
|
||||
@@ -60,7 +62,15 @@ class Dashboard(object):
|
||||
self.temp_dir = temp_dir
|
||||
self.node_stats = NodeStats(redis_address, redis_password)
|
||||
|
||||
self.app = aiohttp.web.Application(middlewares=[self.auth_middleware])
|
||||
# Setting the environment variable RAY_DASHBOARD_DEV=1 disables some
|
||||
# security checks in the dashboard server to ease development while
|
||||
# using the React dev server. Specifically, when this option is set, we
|
||||
# disable the token-based authentication mechanism and allow
|
||||
# cross-origin requests to be made.
|
||||
self.is_dev = os.environ.get("RAY_DASHBOARD_DEV") == "1"
|
||||
|
||||
self.app = aiohttp.web.Application(
|
||||
middlewares=[] if self.is_dev else [self.auth_middleware])
|
||||
self.setup_routes()
|
||||
|
||||
@aiohttp.web.middleware
|
||||
@@ -100,34 +110,27 @@ class Dashboard(object):
|
||||
return forbidden()
|
||||
|
||||
async def get_index(req) -> aiohttp.web.Response:
|
||||
return aiohttp.web.FileResponse(
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "index.html"))
|
||||
|
||||
async def get_resource(req) -> aiohttp.web.Response:
|
||||
try:
|
||||
path = req.match_info["x"]
|
||||
except KeyError:
|
||||
return forbidden()
|
||||
|
||||
if path not in ["main.css", "main.js"]:
|
||||
return forbidden()
|
||||
|
||||
return aiohttp.web.FileResponse(
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"res/{}".format(path)))
|
||||
"client/build/index.html"))
|
||||
|
||||
async def json_response(result=None, error=None,
|
||||
ts=None) -> aiohttp.web.Response:
|
||||
if ts is None:
|
||||
ts = datetime.datetime.utcnow()
|
||||
|
||||
return aiohttp.web.json_response({
|
||||
"result": result,
|
||||
"timestamp": to_unix_time(ts),
|
||||
"error": error,
|
||||
})
|
||||
headers = None
|
||||
if self.is_dev:
|
||||
headers = {"Access-Control-Allow-Origin": "*"}
|
||||
|
||||
return aiohttp.web.json_response(
|
||||
{
|
||||
"result": result,
|
||||
"timestamp": to_unix_time(ts),
|
||||
"error": error,
|
||||
},
|
||||
headers=headers)
|
||||
|
||||
async def ray_config(_) -> aiohttp.web.Response:
|
||||
try:
|
||||
@@ -163,12 +166,17 @@ class Dashboard(object):
|
||||
return await json_response(result=D, ts=now)
|
||||
|
||||
self.app.router.add_get("/", get_index)
|
||||
self.app.router.add_get("/index.html", get_index)
|
||||
self.app.router.add_get("/index.htm", get_index)
|
||||
self.app.router.add_get("/res/{x}", get_resource)
|
||||
|
||||
static_dir = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "client/build/static")
|
||||
if not os.path.isdir(static_dir):
|
||||
raise ValueError(
|
||||
"Dashboard static asset directory not found at '{}'. If "
|
||||
"installing from source, please follow the additional steps "
|
||||
"required to build the dashboard.".format(static_dir))
|
||||
self.app.router.add_static("/static", static_dir)
|
||||
|
||||
self.app.router.add_get("/api/node_info", node_info)
|
||||
self.app.router.add_get("/api/super_client_table", node_info)
|
||||
self.app.router.add_get("/api/ray_config", ray_config)
|
||||
|
||||
self.app.router.add_get("/{_}", get_forbidden)
|
||||
@@ -193,6 +201,12 @@ class NodeStats(threading.Thread):
|
||||
|
||||
self._node_stats = {}
|
||||
self._node_stats_lock = threading.Lock()
|
||||
|
||||
# Mapping from IP address to PID to list of log lines
|
||||
self._logs = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
ray.init(redis_address=redis_address, redis_password=redis_password)
|
||||
|
||||
super().__init__()
|
||||
|
||||
def calculate_totals(self) -> Dict:
|
||||
@@ -263,18 +277,30 @@ class NodeStats(threading.Thread):
|
||||
"totals": self.calculate_totals(),
|
||||
"tasks": self.calculate_tasks(),
|
||||
"clients": node_stats,
|
||||
"logs": self._logs,
|
||||
"errors": ray.errors(all_jobs=True),
|
||||
}
|
||||
|
||||
def run(self):
|
||||
p = self.redis_client.pubsub(ignore_subscribe_messages=True)
|
||||
|
||||
p.psubscribe(self.redis_key)
|
||||
logger.info("NodeStats: subscribed to {}".format(self.redis_key))
|
||||
|
||||
log_channel = ray.gcs_utils.LOG_FILE_CHANNEL
|
||||
p.subscribe(log_channel)
|
||||
logger.info("NodeStats: subscribed to {}".format(log_channel))
|
||||
|
||||
for x in p.listen():
|
||||
try:
|
||||
D = json.loads(ray.utils.decode(x["data"]))
|
||||
with self._node_stats_lock:
|
||||
self._node_stats[D["hostname"]] = D
|
||||
channel = ray.utils.decode(x["channel"])
|
||||
if channel == log_channel:
|
||||
D = json.loads(ray.utils.decode(x["data"]))
|
||||
self._logs[D["ip"]][D["pid"]].extend(D["lines"])
|
||||
else:
|
||||
D = json.loads(ray.utils.decode(x["data"]))
|
||||
self._node_stats[D["hostname"]] = D
|
||||
except Exception:
|
||||
logger.exception(traceback.format_exc())
|
||||
continue
|
||||
@@ -327,15 +353,14 @@ if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
ray.utils.setup_logger(args.logging_level, args.logging_format)
|
||||
|
||||
dashboard = Dashboard(
|
||||
args.redis_address,
|
||||
args.http_port,
|
||||
args.token,
|
||||
args.temp_dir,
|
||||
redis_password=args.redis_password,
|
||||
)
|
||||
|
||||
try:
|
||||
dashboard = Dashboard(
|
||||
args.redis_address,
|
||||
args.http_port,
|
||||
args.token,
|
||||
args.temp_dir,
|
||||
redis_password=args.redis_password,
|
||||
)
|
||||
dashboard.run()
|
||||
except Exception as e:
|
||||
# Something went wrong, so push an error to all drivers.
|
||||
|
||||
+7
-11
@@ -13,7 +13,7 @@ from socket import AddressFamily
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except ModuleNotFoundError:
|
||||
except ImportError:
|
||||
print("The reporter requires psutil to run.")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
@@ -48,14 +48,8 @@ def jsonify_asdict(o):
|
||||
return json.dumps(recursive_asdict(o))
|
||||
|
||||
|
||||
def running_worker(s):
|
||||
if "ray_worker" not in s:
|
||||
return False
|
||||
|
||||
if s == "ray_worker":
|
||||
return False
|
||||
|
||||
return True
|
||||
def is_worker(cmdline):
|
||||
return cmdline and cmdline[0].startswith("ray_")
|
||||
|
||||
|
||||
def determine_ip_address():
|
||||
@@ -127,8 +121,10 @@ class Reporter(object):
|
||||
def get_workers():
|
||||
return [
|
||||
x.as_dict(attrs=[
|
||||
"pid", "create_time", "cpu_times", "name", "memory_full_info"
|
||||
]) for x in psutil.process_iter() if running_worker(x.name())
|
||||
"pid", "create_time", "cpu_percent", "cpu_times", "name",
|
||||
"cmdline", "memory_info", "memory_full_info"
|
||||
]) for x in psutil.process_iter(attrs=["cmdline"])
|
||||
if is_worker(x.info["cmdline"])
|
||||
]
|
||||
|
||||
def get_load_avg(self):
|
||||
|
||||
@@ -978,10 +978,11 @@ def start_dashboard(redis_address,
|
||||
try:
|
||||
import aiohttp # noqa: F401
|
||||
import psutil # noqa: F401
|
||||
import setproctitle # noqa: F401
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Failed to start the dashboard. The dashboard requires Python 3 "
|
||||
"as well as 'pip install aiohttp psutil'.")
|
||||
"as well as 'pip install aiohttp psutil setproctitle'.")
|
||||
|
||||
process_info = start_ray_process(
|
||||
command,
|
||||
|
||||
@@ -3,15 +3,33 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
import ray
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 5, 3), reason="requires python3.5.3 or higher")
|
||||
def test_get_webui():
|
||||
addresses = ray.init(include_webui=True)
|
||||
def test_get_webui(shutdown_only):
|
||||
addresses = ray.init(include_webui=True, num_cpus=1)
|
||||
webui_url = addresses["webui_url"]
|
||||
assert ray.worker.get_webui_url() == webui_url
|
||||
assert ray.get_webui_url() == webui_url
|
||||
|
||||
ray.shutdown()
|
||||
base, token = webui_url.split("?")
|
||||
assert token.startswith("token=")
|
||||
|
||||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
node_info = requests.get(base + "api/node_info?" + token).json()
|
||||
break
|
||||
except requests.exceptions.ConnectionError:
|
||||
if time.time() > start_time + 30:
|
||||
raise Exception(
|
||||
"Timed out while waiting for dashboard to start.")
|
||||
assert node_info["error"] is None
|
||||
assert node_info["result"] is not None
|
||||
assert isinstance(node_info["timestamp"], float)
|
||||
|
||||
+14
-7
@@ -2,6 +2,7 @@ from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -27,9 +28,6 @@ ray_files = [
|
||||
"ray/core/src/ray/raylet/raylet_monitor",
|
||||
"ray/core/src/ray/raylet/raylet",
|
||||
"ray/dashboard/dashboard.py",
|
||||
"ray/dashboard/index.html",
|
||||
"ray/dashboard/res/main.css",
|
||||
"ray/dashboard/res/main.js",
|
||||
]
|
||||
|
||||
# These are the directories where automatically generated Python protobuf
|
||||
@@ -52,6 +50,18 @@ ray_project_files = [
|
||||
"ray/projects/templates/requirements.txt"
|
||||
]
|
||||
|
||||
ray_dashboard_files = [
|
||||
"ray/dashboard/client/build/favicon.ico",
|
||||
"ray/dashboard/client/build/index.html",
|
||||
]
|
||||
for dirname in ["css", "js", "media"]:
|
||||
ray_dashboard_files += glob.glob(
|
||||
"ray/dashboard/client/build/static/{}/*".format(dirname))
|
||||
|
||||
optional_ray_files += ray_autoscaler_files
|
||||
optional_ray_files += ray_project_files
|
||||
optional_ray_files += ray_dashboard_files
|
||||
|
||||
if "RAY_USE_NEW_GCS" in os.environ and os.environ["RAY_USE_NEW_GCS"] == "on":
|
||||
ray_files += [
|
||||
"ray/core/src/credis/build/src/libmember.so",
|
||||
@@ -59,15 +69,12 @@ if "RAY_USE_NEW_GCS" in os.environ and os.environ["RAY_USE_NEW_GCS"] == "on":
|
||||
"ray/core/src/credis/redis/src/redis-server"
|
||||
]
|
||||
|
||||
optional_ray_files += ray_autoscaler_files
|
||||
optional_ray_files += ray_project_files
|
||||
|
||||
extras = {
|
||||
"rllib": [
|
||||
"pyyaml", "gym[atari]", "opencv-python-headless", "lz4", "scipy"
|
||||
],
|
||||
"debug": ["psutil", "setproctitle", "py-spy"],
|
||||
"dashboard": ["psutil", "aiohttp"],
|
||||
"dashboard": ["aiohttp", "psutil", "setproctitle"],
|
||||
"serve": ["uvicorn", "pygments", "werkzeug"],
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user