| <!-- |
| 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> |