mirror of
https://github.com/apache/impala.git
synced 2025-12-19 09:58:28 -05:00
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>
299 lines
10 KiB
Cheetah
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>
|