From 714714189568941894b5614c9e60f65582357d9a Mon Sep 17 00:00:00 2001 From: Wenjun Si Date: Mon, 23 Aug 2021 19:30:47 +0800 Subject: [PATCH] [BACKPORT] Display tileable progress, status and dependency link type on task detail page (#2360) (#2377) --- mars/services/task/supervisor/processor.py | 7 + .../web/ui/src/task_info/TaskTileableGraph.js | 256 +++++++++++++++--- 2 files changed, 232 insertions(+), 31 deletions(-) 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 ( - + + + + ); } }