Skip to content

Commit

Permalink
[BACKPORT] Display tileable progress, status and dependency link type…
Browse files Browse the repository at this point in the history
… on task detail page (#2360) (#2377)
  • Loading branch information
wjsi authored Aug 23, 2021
1 parent c497a1e commit 7147141
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 31 deletions.
7 changes: 7 additions & 0 deletions mars/services/task/supervisor/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand Down
256 changes: 225 additions & 31 deletions mars/services/web/ui/src/task_info/TaskTileableGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
]
};
}

Expand All @@ -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;
Expand All @@ -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(
Expand All @@ -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)
);
Expand All @@ -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 (
<svg
id="svg-canvas"
style={{ margin: 30, width: '90%' }}
/>
<React.Fragment>
<svg
id='legend'
style={{ marginLeft: '6%', width: '90%', height: '10%' }}
/>
<svg
id='svg-canvas'
style={{ margin: 30, width: '90%', height: '80%' }}
/>
</React.Fragment>
);
}
}
Expand Down

0 comments on commit 7147141

Please sign in to comment.