Result
<%if(pipeline.failed) {%>
Failed in stage <%=stages.flatten().reverse().find { list -> list.find {!it.succeeded} }?.context?.stageName%>
<%} else {%>
Succeeded
<%}%>
Timeline
<%
log = { System.out.println(it) }
def nodeSize = { Node n ->
n.children().count { it.attributes().type == "stage" } + n.children().grep { it.attributes().type == "pipeline" }.sum { nodeSize(it) }
}
def depthf = nodes.depthFirst()
def breadf = nodes.breadthFirst()
def nstages = depthf.grep { it.attributes().type == 'stage' && !it.attributes().stage.synthetic }
%>
<%
// To let us render the whole pipeline proportionally, we do it in two phases.
// The first phase constructs a list of rows, which end up as vertically displayed rows.
// Each row contains a list of 'blocks', that are represent the actual activity of the
// branch of the pipeline being rendered in that row. The block is actually a
// Map describing the type of block to be rendered and other attributes. So the data structure
// here is List
>
levels = []
layoutNode = { parent, depth ->
if(levels.size()<=depth)
levels.add([])
float usedWidth = 0.0f
def childWidths = []
def childDepth = depth+1
for(Node n in parent.children()) {
Map attr = n.attributes()
if(attr.type == 'branchpoint') {
def branchWidths = []
for(Node cn in n.children()) { // each child is a pipeline node
while(levels.size()<=childDepth) levels.add([])
levels[childDepth].add([label:'', width:usedWidth, type:'empty'])
def w = layoutNode(cn, childDepth)
branchWidths.add(w)
// println " child pipeline returned width $w now widths are $childWidths"
if(levels[childDepth])
childDepth+=1
}
def bwMax = branchWidths.max()
if(bwMax != null) {
childWidths.add(branchWidths.max())
usedWidth+=branchWidths.max()
}
}
else
if(attr.type == 'stage' && !attr.stage.synthetic) {
if(childDepth > depth+1) {
levels.add([[type:'empty', label:'',width: usedWidth]])
depth = levels.size()-1
childDepth = depth+1
}
def stage = attr.stage
List outputs = attr.outputs
def maxStopTime = outputs*.stopTimeMs.max()
def minStartTime = outputs*.startTimeMs.min()
def timeMs = 0
if(maxStopTime && minStartTime) {
timeMs = maxStopTime - minStartTime
}
else {
timeMs = stage?.context?.documentation?.elapsedMs?:0
maxStopTime = timeMs;
minStartTime = 0;
}
float width = timeMs
def duration = groovy.time.TimeCategory.minus(new Date(maxStopTime), new Date(minStartTime)).toString().replaceAll("seconds","s")
levels[depth].add( [label: stage.stageName, width: width, type: 'block', anno: duration, stage: stage ])
usedWidth += (width)
}
}
return usedWidth
}
layoutNode(nodes,0)
def rowWidths = levels.collect { it.sum { it.width } }
def maxRowWidth = rowWidths.max()
// Downscale to ensure total width = 95%
levels.each { level -> level.each { it.width = Math.max(0.05,it.width * 0.95/maxRowWidth) } }
// Because of the max in the previous downscaling, we might still have overflowed!
rowWidths = levels.collect { it.sum { it.width } }
maxRowWidth = rowWidths.max()
if(maxRowWidth > 0.97)
levels.each { level -> level.each { it.width = it.width * 0.95/maxRowWidth } }
%>
<%
int branchCount = 1;
levels.eachWithIndex { level, index -> %>
<%
def prevBlock = [ type: 'none' ]
def arrows = [ downright : '⤵', right: '→' ]
level.eachWithIndex { block, blockIndex -> %>
<%
def nextBlock = (blockIndex < level.size()-1) ? level[blockIndex+1] : [type:'none'];
if(prevBlock.type == 'block') {
if(nextBlock.type=='block') { %>
${arrows.right}
<%} else if(nextBlock.type != 'none') { %>
${arrows.downright}
<% } %>
<% } %>
<% prevBlock = block; if(block.type=="block") ++branchCount; %>
<%}
if(prevBlock.type != 'none' && index
'+arrows.downright+'');
%>
<% level.each { block -> %>
<%=block.anno ?: '' %>
<% } %>
<% //log( index + ": " + level.collect { it.label+"($it.type) " +String.valueOf(it.width) }.join("|")) %>
<% } %>
Pipeline Stages
Total Runtime = <%=groovy.time.TimeCategory.minus(pipeline.documentation.finishedAt,pipeline.documentation.startedAt)%>
<%
branchCount = 0;
levels.each { level ->
level.each { block ->
if(!block.stage)
return;
def stage = block.stage;
def ctx = stage.context;
++branchCount;
%>
-
<%if(ctx.documentation) {%>
<% def doc = ctx.documentation; %>
<%if(doc.desc) {%>
${doc.desc}
<%}%>
<%if(doc.author) {%>
Author |
${doc.author} |
<%}%>
<%if(doc.inputs || ctx.inputs) {%>
Inputs |
<%if(ctx.inputs) {%>
|
<%} else {%>
<% if(doc.inputs instanceof List) { %>
<% } else { %>
| ${doc.inputs} |
<% } %>
<% } %>
<%}%>
Commands | ${ctx.trackedOutputs*.value*.command.join(" ")} |
<%if(doc.outputs || ctx.outputs) {%>
Outputs |
<% if(ctx.outputs) { %>
<%ctx.outputs.each { output ->%>
- ${output.replaceAll("^\\./","")}
<%}%>
|
<% } else { %>
<% if(doc.outputs instanceof List) { %>
<% doc.outputs.each { inp -> %>
- ${inp}
<% } %>
|
<% } else { %>
${doc.outputs} |
<% } %>
<% } %>
<%}%>
<%if(doc.tools) {%>
Tools |
|
<%}%>
<%if(doc?.constraints) {%>
Constraints |
${doc.constraints} |
<%}%>
<%if(doc.startedAt) {%>
Execution Time |
<%if(doc.elapsedMs<1000 || !doc.finishedAt) { %>
${dateFormat.format(doc.startedAt)}
<% } else {%>
${dateFormat.format(doc.startedAt)} - ${dateFormat.format(doc.finishedAt)}
<% } %>
<%if(doc.finishedAt) use(groovy.time.TimeCategory) { %>
(${doc.finishedAt - doc.startedAt})
<% } %>
|
<%}%>
<%}%>
<%}%>
<%}%>