blob: c677e905ce89b40d9ac0137788b0a040a5e56eb9 [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;
}
</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}}
<h3>Plan</h3>
<div>
<label>
<input type="checkbox" checked="true" id="colour_scheme"/>
Shade nodes according to time spent (if unchecked, shade according to plan fragment)
</label>
</div>
<svg style="border: 1px solid darkgray" width=1200 height=600 class="panel"><g/></svg>
{{/plan_metadata_unavailable}}
{{> 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");
// 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 edge. We use a red dashed line to show that rows are crossing
// the fragment boundary.
if (node["data_stream_target"]) {
edges.push({ "start": node["label"],
"end": node["data_stream_target"],
"style": { label: "" + node["output_card"].toLocaleString(),
style: "stroke: #f66; 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);
var plan = json["plan_json"];
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();
}
// Force one refresh before starting the timer.
refresh();
setInterval(refresh, 1000);
</script>