diff --git a/mars/services/task/supervisor/processor.py b/mars/services/task/supervisor/processor.py
index 9632af6bd0..105d94286e 100644
--- a/mars/services/task/supervisor/processor.py
+++ b/mars/services/task/supervisor/processor.py
@@ -512,7 +512,13 @@ def get_tileable_graph_as_dict(self):
node_list = []
edge_list = []
+ visited = set()
+
for chunk in tileable_graph:
+ if chunk.key in visited:
+ continue
+ visited.add(chunk.key)
+
node_name = str(chunk.op)
node_list.append({
@@ -527,6 +533,7 @@ def get_tileable_graph_as_dict(self):
'toTileableId': chunk.key,
'linkType': 1 if is_pure_dep else 0,
})
+
graph_dict = {
'tileables': node_list,
'dependencies': edge_list
diff --git a/mars/services/web/ui/src/task_info/TaskTileableGraph.js b/mars/services/web/ui/src/task_info/TaskTileableGraph.js
index 674daafe79..4761005566 100644
--- a/mars/services/web/ui/src/task_info/TaskTileableGraph.js
+++ b/mars/services/web/ui/src/task_info/TaskTileableGraph.js
@@ -34,6 +34,49 @@ export default class TaskTileableGraph extends React.Component {
selectedTileable: null,
tileables: [],
dependencies: [],
+ tileableDetails: {},
+ tileableStatus: [
+ {
+ text: 'Pending',
+ color: '#FFFFFF',
+ legendDotXLoc: '430',
+ legendDotYLoc: '20',
+ legendTextXLoc: '440',
+ legendTextYLoc: '21',
+ },
+ {
+ text: 'Running',
+ color: '#F4B400',
+ legendDotXLoc: '10',
+ legendDotYLoc: '20',
+ legendTextXLoc: '20',
+ legendTextYLoc: '21',
+ },
+ {
+ text: 'Succeeded',
+ color: '#00CD95',
+ legendDotXLoc: '105',
+ legendDotYLoc: '20',
+ legendTextXLoc: '115',
+ legendTextYLoc: '21',
+ },
+ {
+ text: 'Failed',
+ color: '#E74C3C',
+ legendDotXLoc: '345',
+ legendDotYLoc: '20',
+ legendTextXLoc: '355',
+ legendTextYLoc: '21',
+ },
+ {
+ text: 'Cancelled',
+ color: '#BFC9CA',
+ legendDotXLoc: '225',
+ legendDotYLoc: '20',
+ legendTextXLoc: '235',
+ legendTextYLoc: '21',
+ },
+ ]
};
}
@@ -51,39 +94,165 @@ export default class TaskTileableGraph extends React.Component {
});
}
+ fetchTileableDetail() {
+ const { sessionId, taskId } = this.props;
+
+ fetch(`api/session/${sessionId}/task/${taskId
+ }/tileable_detail`)
+ .then(res => res.json())
+ .then((res) => {
+ this.setState({
+ tileableDetails: res,
+ });
+ });
+ }
+
componentDidMount() {
this.g = new dagGraphLib.Graph().setGraph({});
+
+ if (this.interval !== undefined) {
+ clearInterval(this.interval);
+ }
+ this.interval = setInterval(() => this.fetchTileableDetail(), 5000);
+ this.fetchTileableDetail();
this.fetchGraphDetail();
}
+
+
+ /**
+ * Creates one status entry for the legend of DAG
+ *
+ * @param {*} svgContainer - The SVG container that the legend will be placed in
+ * @param {*} dotX - X coordinate of the colored dot for the legend entry
+ * @param {*} dotY - Y coordinate of the colored dot for the legend entry
+ * @param {*} textX - X coordinate of the label for the legend entry
+ * @param {*} textY - Y coordinate of the label for the legend entry
+ * @param {*} color - Status color for the legend entry
+ * @param {*} text - Label for the legend entry
+ */
+ generateGraphLegendItem(svgContainer, dotX, dotY, textX, textY, color, text) {
+ if (color === '#FFFFFF') {
+ // add an additional stroke so
+ // the white color can be visited
+ svgContainer
+ .append('circle')
+ .attr('cx', dotX)
+ .attr('cy', dotY)
+ .attr('r', 6)
+ .attr('stroke', '#333')
+ .style('fill', color);
+ } else {
+ svgContainer
+ .append('circle')
+ .attr('cx', dotX)
+ .attr('cy', dotY)
+ .attr('r', 6)
+ .style('fill', color);
+ }
+
+ svgContainer
+ .append('text')
+ .attr('x', textX)
+ .attr('y', textY)
+ .text(text)
+ .style('font-size', '15px')
+ .attr('alignment-baseline', 'middle');
+ }
+
/* eslint no-unused-vars: ["error", { "args": "none" }] */
componentDidUpdate(prevProps, prevStates, snapshot) {
+ if (Object.keys(this.state.tileableDetails).length !== this.state.tileables.length) {
+ return;
+ }
+
+ /**
+ * If the tileables and dependencies are different, this is a
+ * new DAG, so we will erase everything from the canvas and
+ * generate a new dag
+ */
if (prevStates.tileables !== this.state.tileables
- && prevStates.dependencies !== this.state.dependencies) {
+ && prevStates.dependencies !== this.state.dependencies) {
d3Select('#svg-canvas').selectAll('*').remove();
+ // Set up an SVG group so that we can translate the final graph.
+ const svg = d3Select('#svg-canvas'),
+ inner = svg.append('g');
+
this.g = new dagGraphLib.Graph().setGraph({});
+
+ // Create the legend for DAG
+ const legendSVG = d3Select('#legend');
+ this.state.tileableStatus.forEach((status) => this.generateGraphLegendItem(
+ legendSVG,
+ status.legendDotXLoc,
+ status.legendDotYLoc,
+ status.legendTextXLoc,
+ status.legendTextYLoc,
+ status.color,
+ status.text
+ ));
+
+ // Add the tileables to DAG
this.state.tileables.forEach((tileable) => {
const value = { tileable };
+ const tileableDetail = this.state.tileableDetails[tileable.tileableId];
+ const nameEndIndex = tileable.tileableName.indexOf('key') - 1;
- let nameEndIndex = tileable.tileableName.indexOf('key') - 1;
value.label = tileable.tileableName.substring(0, nameEndIndex);
value.rx = value.ry = 5;
this.g.setNode(tileable.tileableId, value);
- // In future fill color based on progress
+ /**
+ * Add the progress color using SVG linear gradient. The offset on
+ * the first stop on the linear gradient marks how much of the node
+ * should be filled with color. The second stop adds a white color to
+ * the rest of the node
+ */
+ let nodeProgressGradient = inner.append('linearGradient')
+ .attr('id', 'progress-' + tileable.tileableId);
+
+ nodeProgressGradient.append('stop')
+ .attr('id', 'progress-' + tileable.tileableId + '-stop')
+ .attr('stop-color', this.state.tileableStatus[tileableDetail.status].color)
+ .attr('offset', tileableDetail.progress);
+
+ nodeProgressGradient.append('stop')
+ .attr('stop-color', '#FFFFFF')
+ .attr('offset', '0');
+
+ /**
+ * apply the linear gradient and other css properties
+ * to nodes.
+ */
const node = this.g.node(tileable.tileableId);
- node.style = 'fill: #f4b400; cursor: pointer;';
+ node.style = 'cursor: pointer; stroke: #333; fill: url(#progress-' + tileable.tileableId + ')';
node.labelStyle = 'cursor: pointer';
});
+ /**
+ * Adds edges to the DAG. If an edge has a linkType of 1,
+ * the edge will be a dashed line.
+ */
this.state.dependencies.forEach((dependency) => {
- // In future label may be named based on linkType?
- this.g.setEdge(
- dependency.fromTileableId,
- dependency.toTileableId,
- { label: '' }
- );
+ if (dependency.linkType === 1) {
+ this.g.setEdge(
+ dependency.fromTileableId,
+ dependency.toTileableId,
+ {
+ style: 'stroke: #333; fill: none; stroke-dasharray: 5, 5;'
+ }
+ );
+ } else {
+ this.g.setEdge(
+ dependency.fromTileableId,
+ dependency.toTileableId,
+ {
+ style: 'stroke: #333; fill: none;'
+ }
+ );
+ }
+
});
let gInstance = this.g;
@@ -93,30 +262,15 @@ export default class TaskTileableGraph extends React.Component {
node.rx = node.ry = 5;
});
- //makes the lines smooth
- gInstance.edges().forEach(function (e) {
- const edge = gInstance.edge(e.v, e.w);
- edge.style = 'stroke: #333; fill: none';
- });
-
// Create the renderer
const render = new DagRender();
- // Set up an SVG group so that we can translate the final graph.
- const svg = d3Select('#svg-canvas'),
- inner = svg.append('g');
-
- // Set up zoom support
- const zoom = d3Zoom().on('zoom', function (e) {
- inner.attr('transform', e.transform);
- });
- svg.call(zoom);
-
if (this.state.tileables.length !== 0) {
// Run the renderer. This is what draws the final graph.
render(inner, this.g);
}
+ // onClick function for the tileable
const handleClick = (e, node) => {
if (this.props.onTileableClick) {
const selectedTileable = this.state.tileables.filter(
@@ -129,8 +283,23 @@ export default class TaskTileableGraph extends React.Component {
inner.selectAll('g.node').on('click', handleClick);
// Center the graph
- const initialScale = 0.9;
+ const bounds = inner.node().getBBox();
+ const parent = inner.node().parentElement;
+ const width = bounds.width,
+ height = bounds.height;
+ const fullWidth = parent.clientWidth,
+ fullHeight = parent.clientHeight;
+ const initialScale = fullHeight >= height ? 1 : fullHeight / height;
+
+ d3Select('.output').attr('transform', 'translate(' + (fullWidth - width * initialScale) / 2 + ', ' + (fullHeight - height * initialScale) / 2 + ')');
+
+ // Set up zoom support
+ const zoom = d3Zoom().on('zoom', function (e) {
+ inner.attr('transform', e.transform);
+ });
+
svg.call(
+ zoom,
zoom.transform,
d3ZoomIdentity.scale(initialScale)
);
@@ -141,14 +310,39 @@ export default class TaskTileableGraph extends React.Component {
svg.attr('height', 40);
}
}
+
+ /**
+ * If the tileables and dependencies didn't change and
+ * only the tileable status changed, we know this is the
+ * old graph with updated tileable status, so we just
+ * need to update the color of nodes and the progress bar
+ */
+ if (prevStates.tileables === this.state.tileables
+ && prevStates.dependencies === this.state.dependencies
+ && prevStates.tileableDetails !== this.state.tileableDetails) {
+ const svg = d3Select('#svg-canvas');
+ this.state.tileables.forEach((tileable) => {
+ const tileableDetail = this.state.tileableDetails[tileable.tileableId];
+
+ svg.select('#progress-' + tileable.tileableId + '-stop')
+ .attr('stop-color', this.state.tileableStatus[tileableDetail.status].color)
+ .attr('offset', tileableDetail.progress);
+ });
+ }
}
render() {
return (
-
+
+
+
+
);
}
}