blob: ef82aa851b75232c65f634eba848c88373b1ab43 [file] [log] [blame]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
{{> www/common-header.tmpl }}
</div>
<div class="container" style="width:1200px;margin:0 auto;">
<style id="css">
/* Text style for graph nodes */
.node {
color: white;
font-size: 14px;
font-weight: 700;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
}
.node rect {
stroke: #333;
fill: #fff;
}
.edgePath path {
stroke: #333;
fill: #333;
stroke-width: 1.5px;
}
.nodes, .edgeLabel {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, "Noto Sans", sans-serif;
}
</style>
{{> www/query_detail_tabs.tmpl }}
{{?plan_metadata_unavailable}}
<h3>Plan not yet available. Page will update when query planning completes.</h3>
{{/plan_metadata_unavailable}}
{{^plan_metadata_unavailable}}
<div style="display:flex; justify-content:space-between;">
<h3>Plan</h3>
<label>
<h4 style="display:inline;"> Download : </h4>
<input type="button" class="btn btn-primary" data-toggle="modal" value="HTML"
data-target="#export_modal" role="button"/>
</label>
</div>
<label>
<input type="checkbox" checked="true" id="colour_scheme" onClick="refresh()"/>
Shade nodes according to time spent (if unchecked, shade according to plan fragment)
</label>
<svg style="border: 1px solid darkgray" width=1200 height=600 class="panel"><g/></svg>
{{/plan_metadata_unavailable}}
<div id="export_modal" style="transition-duration: 0.15s;" class="modal fade"
role="dialog" data-keyboard="true" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5> Download Plan </h5>
<input class="btn btn-primary" type="button" value="X" data-dismiss="modal"/>
</div>
<div class="modal-body">
<h6 class="d-inline"> Filename: </h6>
<input id="export_filename" class="form-control-sm" type="text"
value="{{query_id}}_plan"/>
<select id="export_format" class="form-control-sm btn btn-primary">
<option selected>.html</option>
</select>
</div>
<div class="modal-footer">
<a id="export_link" class="btn btn-primary" data-dismiss="modal" href="#"
role="button"> Download </a>
</div>
</div>
</div>
</div>
{{> www/common-footer.tmpl }}
<script src="{{ __common__.host-url }}/www/d3.v3.min.js" charset="utf-8"></script>
<script src="{{ __common__.host-url }}/www/dagre-d3.min.js"></script>
<!-- Builds and then renders a plan graph using Dagre / D3. The JSON for the current query
is retrieved by an HTTP call, and then the graph of nodes and edges is built by walking
over each plan fragment in turn. Plan fragments are connected wherever a node has a
data_stream_target attribute. -->
<script>
$("#plan-tab").addClass("active");
var g = new dagreD3.graphlib.Graph().setGraph({rankDir: "BT"});
var svg = d3.select("svg");
var inner = svg.select("g");
var export_link = document.getElementById("export_link");
var export_filename = document.getElementById("export_filename");
var export_format = document.getElementById("export_format");
export_filename.value = export_filename.value.replace(/\W/g,'_');
// Set up zoom support
var zoom = d3.behavior.zoom().on("zoom", function() {
inner.attr("transform", "translate(" + d3.event.translate + ")" +
"scale(" + d3.event.scale + ")");
});
svg.call(zoom);
// Set of colours to use, with the same colour used for every node in the same plan
// fragment.
var colours = ["#A9A9A9", "#FF8C00", "#8A2BE2", "#A52A2A", "#00008B", "#006400",
"#228B22", "#4B0082", "#DAA520", "#008B8B", "#000000", "#DC143C"]
// Shades of red in order of intensity, used for colouring nodes by time taken
var cols_by_time = ["#000000", "#1A0500", "#330A00", "#4C0F00", "#661400", "#801A00",
"#991F00", "#B22400", "#CC2900", "#E62E00", "#FF3300", "#FF4719"];
// Recursively build a list of edges and states that comprise the plan graph
function build(node, parent, edges, states, colour_idx, max_node_time) {
states.push({ "name": node["label"],
"detail": node["label_detail"],
"num_instances": node["num_instances"],
"num_active": node["num_active"],
"max_time": node["max_time"],
"avg_time": node["avg_time"],
"is_broadcast": node["is_broadcast"],
"max_time_val": node["max_time_val"],
"style": "fill: " + colours[colour_idx]});
if (parent != null) {
var label_val = "" + node["output_card"].toLocaleString();
edges.push({ start: node["label"], end: parent,
style: { label: label_val }});
}
// Add an inter-fragment edges
if (node["data_stream_target"]) {
// Use a red dashed line to show a streaming data boundary
edges.push({ "start": node["label"],
"end": node["data_stream_target"],
"style": { label: "" + node["output_card"].toLocaleString(),
style: "fill:none; stroke: #c00000; stroke-dasharray: 5, 5;"}});
} else if (node["join_build_target"]) {
// Use a green dashed line to show a join build boundary
edges.push({ "start": node["label"],
"end": node["join_build_target"],
"style": { label: "" + node["output_card"].toLocaleString(),
style: "fill: none; stroke: #00c000; stroke-dasharray: 5, 5;"}
});
}
max_node_time = Math.max(node["max_time_val"], max_node_time)
for (var i = 0; i < node["children"].length; ++i) {
max_node_time = build(
node["children"][i], node["label"], edges, states, colour_idx, max_node_time);
}
return max_node_time;
}
var is_first = true;
function renderGraph(ignored_arg) {
if (req.status != 200) return;
var json = JSON.parse(req.responseText);
if (json.error) {
clearInterval(intervalId);
return;
}
refresh_record(json.record_json);
var plan = json["plan_json"];
var inflight = json["inflight"];
if (!inflight) {
clearInterval(intervalId);
}
var states = []
var edges = []
var colour_idx = 0;
var max_node_time = 0;
plan["plan_nodes"].forEach(function(parent) {
max_node_time = Math.max(
build(parent, null, edges, states, colour_idx, max_node_time));
// Pick a new colour for each plan fragment
colour_idx = (colour_idx + 1) % colours.length;
});
// Keep a map of names to states for use when processing edges.
var states_by_name = { }
states.forEach(function(state) {
// Build the label for the node from the name and the detail
var html = "<span>" + state.name + "</span><br/>";
html += "<span>" + state.detail + "</span><br/>";
html += "<span>" + state.num_instances + " instance";
if (state.num_instances > 1) {
html += "s";
}
html += "</span><br/>";
html += "<span>Max: " + state.max_time + ", avg: " + state.avg_time + "</span>";
var style = state.style;
// If colouring nodes by total time taken, choose a shade in the cols_by_time list
// with idx proportional to the max time of the node divided by the max time over all
// nodes.
if (document.getElementById("colour_scheme").checked) {
var idx = (cols_by_time.length - 1) * (state.max_time_val / (1.0 * max_node_time));
style = "fill: " + cols_by_time[Math.floor(idx)];
}
g.setNode(state.name, { "label": html,
"labelType": "html",
"style": style });
states_by_name[state.name] = state;
});
edges.forEach(function(edge) {
// Impala marks 'broadcast' as a property of the receiver, not the sender. We use
// '(BCAST)' to denote that a node is duplicating its output to all receivers.
if (states_by_name[edge.end].is_broadcast) {
edge.style.label += " \n(BCAST * " + states_by_name[edge.end].num_instances + ")";
}
g.setEdge(edge.start, edge.end, edge.style);
});
g.nodes().forEach(function(v) {
var node = g.node(v);
node.rx = node.ry = 5;
});
// Create the renderer
var render = new dagreD3.render();
// Run the renderer. This is what draws the final graph.
render(inner, g);
// Center the graph, but only the first time through (so as to not lose user zooms).
if (is_first) {
var initialScale = 0.75;
zoom.translate([(svg.attr("width") - g.graph().width * initialScale) / 2, 20])
.scale(initialScale)
.event(svg);
svg.attr('height', Math.max(g.graph().height * initialScale + 40, 600));
is_first = false;
}
}
// Called periodically, fetches the plan JSON from Impala and passes it to renderGraph()
// for display.
function refresh() {
req = new XMLHttpRequest();
req.onload = renderGraph;
req.open("GET", make_url("/query_plan?query_id={{query_id}}&json"), true);
req.send();
}
// Attaches a blob of the current SVG viewport to the associated link
export_link.addEventListener('click', function(event) {
if (export_format.value === ".html") {
var svg_viewport = document.querySelector("svg");
var export_style = document.getElementById("css");
var html_blob = new Blob([`<!DOCTYPE html><body>`,
`<h1 style="font-family:monospace;">Query {{query_id}}</h1>`,
export_style.outerHTML, svg_viewport.outerHTML, `</body></html>`],
{type: "text/html;charset=utf-8"});
export_link.href = URL.createObjectURL(html_blob);
}
export_link.download = `${export_filename.value}${export_format.value}`;
export_link.click();
});
// Force one refresh before starting the timer.
refresh();
var intervalId = setInterval(refresh, 2000);
</script>