Files
impala/www/query_plan.tmpl
Surya Hebbar 37d4a4eb56 IMPALA-13389: Refactor webUI scripts to use ES6 syntax
Currently, the scripts in the entire webUI contain variable and function
declarations using the ES5 standard.

In such declarations, all the variables are attached to the browser's
window object, polluting the global scope. Additionally, many unnamed
functions can be represented with cleaner and concise syntax.

This patch refactors such declarations, using the ES6 syntax.
-> Replacing 'var' declarations with 'let' and 'const'
  - To improve browser's memory utilization
  - For better scoping, immutability and readability
-> Replacing unnamed function declarations with arrow functions
  - Better scoping by binding 'this' object to the surrounding context

These improve maintainability and browser's memory utilization.

Across many instances within the webUI scripts, no particular naming
scheme is being followed for variable and function identifiers.
Hence, they have been revised with the following naming scheme.
-> All function names have been declared using camel case.
-> All constant primitive values and strings have been declared
   in uppercase.
-> All other types of variables have been declared using snake case.

This naming scheme allows easier distinction between functions,
constants and other variables, based on the identifiers.

These changes to code style are further enforced during code review
through IMPALA-13473.

Change-Id: Ie38f2c642ede14956a2c6d551a58e42538204768
Reviewed-on: http://gerrit.cloudera.org:8080/21851
Reviewed-by: Impala Public Jenkins <impala-public-jenkins@cloudera.com>
Tested-by: Impala Public Jenkins <impala-public-jenkins@cloudera.com>
2025-04-10 20:33:49 +00:00

299 lines
10 KiB
Cheetah

<!--
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="page_export_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");
const g = new dagreD3.graphlib.Graph().setGraph({rankDir: "BT"});
const svg = d3.select("svg");
const inner = svg.select("g");
const export_link = document.getElementById("export_link");
const export_filename = document.getElementById("export_filename");
const export_format = document.getElementById("export_format");
export_filename.value = export_filename.value.replace(/\W/g,'_');
// Set up zoom support
const zoom = d3.behavior.zoom().on("zoom", () => {
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.
const 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
const 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) {
const 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 (let 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;
}
let is_first = true;
function renderGraph(ignored_arg) {
if (req.status != 200) return;
const json = JSON.parse(req.responseText);
if (json.error) {
clearInterval(interval_id);
return;
}
refresh_record(json.record_json);
const plan = json["plan_json"];
const inflight = json["inflight"];
if (!inflight) {
clearInterval(interval_id);
}
const states = []
const edges = []
let colour_idx = 0;
let max_node_time = 0;
plan["plan_nodes"].forEach((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.
const states_by_name = { }
states.forEach((state) => {
// Build the label for the node from the name and the detail
let 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>";
let 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) {
let 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((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((v) => {
const node = g.node(v);
node.rx = node.ry = 5;
});
// Create the renderer
const 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) {
const initial_scale = 0.75;
zoom.translate([(svg.attr("width") - g.graph().width * initial_scale) / 2, 20])
.scale(initial_scale)
.event(svg);
svg.attr('height', Math.max(g.graph().height * initial_scale + 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', (event) => {
if (export_format.value === ".html") {
const svg_viewport = document.querySelector("svg");
const export_style = document.getElementById("page_export_css");
const 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();
const interval_id = setInterval(refresh, 2000);
</script>