blob: 704c2ddba3d85de99079c8a955c8d7aac72b9978 [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.
-->
<#include "*/generic.ftl">
<#macro page_head>
<script src="/static/js/d3.v3.js"></script>
<script src="/static/js/dagre-d3.min.js"></script>
<script src="/static/js/graph.js"></script>
<script src="/static/js/jquery.dataTables-1.10.16.min.js"></script>
<script src="/static/js/jquery.form.js"></script>
<script src="/static/js/querySubmission.js"></script>
<!-- Ace Libraries for Syntax Formatting -->
<script src="/static/js/ace-code-editor/ace.js" type="text/javascript" charset="utf-8"></script>
<!-- Disabled in favour of dynamic: script src="/static/js/ace-code-editor/mode-sql.js" type="text/javascript" charset="utf-8" -->
<script src="/dynamic/mode-sql.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/js/ace-code-editor/ext-language_tools.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/js/ace-code-editor/theme-sqlserver.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/js/ace-code-editor/snippets/sql.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/js/ace-code-editor/mode-snippets.js" type="text/javascript" charset="utf-8"></script>
<link href="/static/css/drill-dataTables.sortable.css" rel="stylesheet">
<script>
var globalconfig = {
"queryid" : "${model.getQueryId()}",
"operators" : ${model.getOperatorsJSON()?no_esc}
};
$(document).ready(function() {
$(".sortable").DataTable({
"searching": false,
"lengthChange": false,
"paging": false,
"info": false
});
//Enable Warnings by making it visible
checkForWarnings();
});
//Check for Warnings
function checkForWarnings() {
//No Progress Warning
let noProgressFragmentCount = document.querySelectorAll('td[class=no-progress-tag]').length;
let majorFragmentCount = document.querySelectorAll('#fragment-overview table tbody tr').length;
if (majorFragmentCount > 0) { // For fast-failed queries majorFragmentCount=0
toggleWarning("noProgressWarning", majorFragmentCount, noProgressFragmentCount);
}
//Spill To Disk Warnings
let spillCount = document.querySelectorAll('td[class=spill-tag]').length;
toggleWarning("spillToDiskWarning", true, (spillCount > 0));
//Slow Scan Warnings
let longScanWaitCount = document.querySelectorAll('td[class=scan-wait-tag]').length;
toggleWarning("longScanWaitWarning", true, (longScanWaitCount > 0));
}
//Show Warnings
function toggleWarning(warningElemId, expectedVal, actualVal) {
if (expectedVal == actualVal) {
document.getElementById(warningElemId).style.display="block";
} else {
closeWarning(warningElemId);
}
}
//Close Warning
function closeWarning(warningElemId) {
document.getElementById(warningElemId).style.display="none";
}
//Injects Estimated Rows
function injectEstimatedRows() {
Object.keys(opRowCountMap).forEach(key => {
var tgtElem = $("td.estRowsAnchor[key='" + key + "']");
var status = tgtElem.append("<div class='estRows' title='Estimated'>(" + opRowCountMap[key] + ")</div>");
});
}
//Toggle Estimates' visibility
function toggleEstimates(tgtColumn) {
var colClass = '.est' + tgtColumn;
var estimates = $(colClass);
if (estimates.filter(":visible").length > 0) {
$(colClass).each(function () {
$(this).attr("style", "display:none");
});
} else {
$(colClass).each(function () {
$(this).attr("style", "display:block");
});
}
}
//Close the cancellation status popup
function refreshStatus() {
//Close PopUp Modal
$("#queryCancelModal").modal("hide");
location.reload(true);
}
//Cancel query & show cancellation status
function cancelQuery() {
document.getElementById("cancelTitle").innerHTML = "Drillbit on " + location.hostname + " says";
$.get("/profiles/cancel/" + globalconfig.queryid, function(data, status){/*Not Tracking Response*/});
//Show PopUp Modal
$("#queryCancelModal").modal("show");
};
</script>
</#macro>
<#macro page_body>
<div class="page-header">
</div>
<h3>Query and Planning</h3>
<ul id="query-tabs" class="nav nav-tabs" role="tablist">
<li><a href="#query-query" role="tab" data-toggle="tab">Query</a></li>
<li><a href="#query-physical" role="tab" data-toggle="tab">Physical Plan</a></li>
<li><a href="#query-visual" role="tab" data-toggle="tab">Visualized Plan</a></li>
<li><a href="#query-edit" role="tab" data-toggle="tab">Edit Query</a></li>
<#if model.hasError()>
<li><a href="#query-error" role="tab" data-toggle="tab">Error</a></li>
</#if>
</ul>
<div id="query-content" class="tab-content">
<div id="query-query" class="tab-pane" style="background-color: #ffffff">
<p><pre id="query-text" name="query-text" style="background-color: #f5f5f5; font-family:courier,monospace">${model.getProfile().query}</pre></p>
<#if model.hasAutoLimit()>
<div name="autoLimitWarning" style="cursor:help" class="panel panel-warning" title="WebUI Queries allow restricting the number of rows returned to avoid the server to hold excessive number of results rows in memory.&#10;This helps maintain server stability with faster response if the entire resultset will not be visualized in the browser">
<div class="panel-heading">
<span class="glyphicon glyphicon-pushpin" style="font-size:125%"></span>
<b>WARNING:</b> Query result was <b>automatically</b> limited to <span style="font-style:italic;font-weight:bold">${model.getAutoLimit()} rows</span>
</div>
</div>
</#if>
</div>
<div id="query-physical" class="tab-pane">
<p><pre>${model.profile.plan}</pre></p>
</div>
<div id="query-visual" class="tab-pane">
<div style='padding: 15px 15px;'>
<button type='button' class='btn btn-default' onclick='popUpAndPrintPlan();'><span class="glyphicon glyphicon-print"></span> Print Plan</button>
</div>
<div>
<svg id="query-visual-canvas" class="center-block"></svg>
</div>
</div>
<div id="query-edit" class="tab-pane">
<p>
<#if model.isOnlyImpersonationEnabled()>
<div class="form-group">
<label for="userName">User Name</label>
<input type="text" size="30" name="userName" id="userName" placeholder="User Name" value="${model.getProfile().user}">
</div>
</#if>
<form role="form" id="queryForm" action="/query" method="POST">
<div id="query-editor" class="form-group">${model.getProfile().query}</div>
<input class="form-control" id="query" name="query" type="hidden" value="${model.getProfile().query}"/>
<div style="padding:5px"><b>Hint: </b>Use <div id="keyboardHint" style="display:inline-block; font-style:italic"></div> to submit</div>
<div class="form-group">
<div class="radio-inline">
<label>
<input type="radio" name="queryType" id="sql" value="SQL" checked>
SQL
</label>
</div>
<div class="radio-inline">
<label>
<input type="radio" name="queryType" id="physical" value="PHYSICAL">
PHYSICAL
</label>
</div>
<div class="radio-inline">
<label>
<input type="radio" name="queryType" id="logical" value="LOGICAL">
LOGICAL
</label>
</div>
</div>
<div>
<button class="btn btn-default" type="button" id="rerunButton" onclick="<#if model.isOnlyImpersonationEnabled()>doSubmitQueryWithUserName()<#else>doSubmitQueryWithAutoLimit()</#if>">
Re-run query
</button>
<input type="checkbox" name="forceLimit" value="limit" <#if model.hasAutoLimit()>checked</#if>> Limit results to <input type="text" id="autoLimit" name="autoLimit" min="0" value="<#if model.hasAutoLimit()>${model.getAutoLimit()?c}<#else>${model.getDefaultAutoLimit()?c}</#if>" size="6" pattern="[0-9]*"> rows <span class="glyphicon glyphicon-info-sign" title="Limits the number of records retrieved in the query. Ignored if query has a limit already" style="cursor:pointer"></span>
</div>
<input type="hidden" name="csrfToken" value="${model.getCsrfToken()}">
</form>
</p>
<#include "*/alertModals.ftl">
<#include "*/runningQuery.ftl">
<p>
<form action="/profiles/cancel/${model.queryId}" method="GET">
<div class="form-group">
<button type="submit" class="btn btn-default">Cancel query</button>
</div>
</form>
</p>
<#include "*/runningQuery.ftl">
</div>
<#if model.hasError()>
<div id="query-error" class="tab-pane fade">
<p><pre>${model.getProfile().error?trim}</pre></p>
<p>Failure node: ${model.getProfile().errorNode}</p>
<p>Error ID: ${model.getProfile().errorId}</p>
<div class="page-header"></div>
<h3>Verbose Error Message</h3>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#error-message-overview">
Overview
</a>
</h4>
</div>
<div id="error-message-overview" class="panel-collapse collapse">
<div class="panel-body">
<pre>${model.getProfile().verboseError?trim}</pre>
</div>
</div>
</div>
</div>
</#if>
</div>
<#assign queueName = model.getProfile().getQueueName() />
<#assign queued = queueName != "" && queueName != "-" />
<div class="page-header"></div>
<h3>Query Profile: <span style='font-size:85%'>${model.getQueryId()}</span>
<#if model.getQueryStateDisplayName() == "Prepared" || model.getQueryStateDisplayName() == "Planning" || model.getQueryStateDisplayName() == "Enqueued" || model.getQueryStateDisplayName() == "Starting" || model.getQueryStateDisplayName() == "Running">
<div style="display: inline-block;">
<button type="button" id="cancelBtn" class="btn btn-warning btn-sm" onclick="cancelQuery()" > Cancel </button>
</div>
<!-- Cancellation Modal -->
<div class="modal fade" id="queryCancelModal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" onclick="refreshStatus()">&times;</button>
<h4 class="modal-title" id="cancelTitle"></h4>
</div>
<div class="modal-body" style="line-height:2">
Cancellation issued for Query ID:<br>${model.getQueryId()}
</div>
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="refreshStatus()">Close</button></div>
</div>
</div>
</div>
</#if>
</h3>
<div class="panel-group" id="query-profile-accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#query-profile-overview">
Overview
</a>
</h4>
</div>
<div id="query-profile-overview" class="panel-collapse collapse in">
<div class="panel-body">
<table class="table table-bordered">
<thead>
<tr>
<th>State</th>
<th>Foreman</th>
<th>Total Fragments</th>
<#if queued>
<th>Total Cost</th>
<th>Queue</th>
</#if>
</tr>
</thead>
<tbody>
<tr>
<td>${model.getQueryStateDisplayName()}</td>
<td>${model.getProfile().getForeman().getAddress()}</td>
<td>${model.getProfile().getTotalFragments()}</td>
<#if queued>
<td>${model.getProfile().getTotalCost()}</td>
<td>${queueName}</td>
</#if>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#query-profile-duration">
Duration
</a>
</h4>
</div>
<div id="query-profile-duration" class="panel-collapse collapse in">
<div class="panel-body">
<table class="table table-bordered">
<thead>
<tr>
<th>Planning</th>
<#if queued>
<th>Queued</th>
</#if>
<th>Execution</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>${model.getPlanningDuration()}</td>
<#if queued>
<td>${model.getQueuedDuration()}</td>
</#if>
<td>${model.getExecutionDuration()}</td>
<td>${model.getProfileDuration()}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<#assign sessionOptions = model.getSessionOptions()>
<#assign queryOptions = model.getQueryOptions()>
<#if (sessionOptions?keys?size > 0 || queryOptions?keys?size > 0) >
<div class="page-header"></div>
<h3>Options</h3>
<div class="panel-group" id="options-accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#options-overview">
Overview
</a>
</h4>
</div>
<div id="options-overview" class="panel-collapse collapse in">
<@list_options options=sessionOptions scope="Session" />
<@list_options options=queryOptions scope="Query" />
</div>
</div>
</div>
</#if>
<div class="page-header"></div>
<h3>Fragment Profiles</h3>
<div class="panel-group" id="fragment-accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#fragment-overview">
Overview
</a>
</h4>
</div>
<div id="fragment-overview" class="panel-collapse collapse">
<div class="panel-body">
<svg id="fragment-overview-canvas" class="center-block"></svg>
<div id="noProgressWarning" style="display:none;cursor:help" class="panel panel-warning">
<div class="panel-heading" title="Check if any of the Drillbits are waiting for data from a SCAN operator, or might actually be hung with its VM thread being busy." style="cursor:pointer">
<span class="glyphicon glyphicon-alert" style="font-size:125%">&#xe209;</span> <b>WARNING:</b> No fragments have made any progress in the last <b>${model.getNoProgressWarningThreshold()}</b> seconds. (See <span style="font-style:italic;font-weight:bold">Last Progress</span> below)
</div>
</div>
${model.getFragmentsOverview()?no_esc}
</div>
</div>
<#list model.getFragmentProfiles() as frag>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#${frag.getId()}">
${frag.getDisplayName()}
</a>
</h4>
</div>
<div id="${frag.getId()}" class="panel-collapse collapse">
<div class="panel-body">
${frag.getContent()?no_esc}
</div>
</div>
</div>
</#list>
</div>
</div>
<div class="page-header"></div>
<h3>Operator Profiles
<button onclick="toggleEstimates('Rows')" class="btn" style="font-size:60%; float:right">Show/Hide Estimated Rows</button></h3>
<style>
.estRows {
color:navy;
font-style:italic;
font-size: 80%;
display:<#if model.showEstimatedRows()>block<#else>none</#if>;
}
</style>
<div class="panel-group" id="operator-accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#operator-overview">
Overview
</a>
</h4>
</div>
<div id="operator-overview" class="panel-collapse collapse">
<div class="panel-body">
<#if model.hasAutoLimit()>
<div name="autoLimitWarning" style="cursor:help" class="panel panel-warning" title="WebUI Queries allow restricting the number of rows returned to avoid the server to hold excessive number of results rows in memory.&#10;This helps maintain server stability with faster response if the entire resultset will not be visualized in the browser">
<div class="panel-heading">
<span class="glyphicon glyphicon-pushpin" style="font-size:125%"></span>
<b>WARNING:</b> Query result was <b>automatically</b> limited to <span style="font-style:italic;font-weight:bold">${model.getAutoLimit()} rows</span>
</div>
</div>
</#if>
<div id="spillToDiskWarning" style="display:none;cursor:help" class="panel panel-warning" title="Spills occur because a buffered operator didn't get enough memory to hold data in memory. Increase the memory or ensure that number of spills &lt; 2">
<div class="panel-heading"><span class="glyphicon glyphicon-alert" style="font-size:125%">&#xe209;</span> <b>WARNING:</b> Some operators have data spilled to disk. This will result in performance loss. (See <span style="font-style:italic;font-weight:bold">Avg Peak Memory</span> and <span style="font-style:italic;font-weight:bold">Max Peak Memory</span> below)
<button type="button" class="close" onclick="closeWarning('spillToDiskWarning')" style="font-size:180%">&times;</button>
</div>
</div>
<div id="longScanWaitWarning" style="display:none;cursor:help" class="panel panel-warning">
<div class="panel-heading" title="Check if any of the Drillbits are waiting for data from a SCAN operator, or might actually be hung with its VM thread being busy." style="cursor:pointer">
<span class="glyphicon glyphicon-alert" style="font-size:125%">&#xe209;</span> <b>WARNING:</b> Some of the SCAN operators spent more time waiting for the data than processing it. (See <span style="font-style:italic;font-weight:bold">Avg Wait Time</span> as compared to <span style="font-style:italic;font-weight:bold">Average Process Time</span> for the <b>SCAN</b> operators below)
<button type="button" class="close" onclick="closeWarning('longScanWaitWarning')" style="font-size:180%">&times;</button>
</div>
</div>
${model.getOperatorsOverview()?no_esc}
</div>
</div>
</div>
<#list model.getOperatorProfiles() as op>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#${op.getId()}">
${op.getDisplayName()}
</a>
</h4>
</div>
<div id="${op.getId()}" class="panel-collapse collapse">
<div class="panel-body">
${op.getContent()?no_esc}
</div>
<div class="panel panel-info">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#${op.getId()}-metrics">
Operator Metrics
</a>
</h4>
</div>
<div id="${op.getId()}-metrics" class="panel-collapse collapse">
<div class="panel-body" style="display:block;overflow-x:auto">
${op.getMetricsTable()?no_esc}
</div>
</div>
</div>
</div>
</div>
</#list>
</div>
<div class="page-header"></div>
<h3>Full JSON Profile</h3>
<div class="span4 collapse-group" id="full-json-profile">
<a class="btn btn-default" data-toggle="collapse" data-target="#full-json-profile-json">JSON profile</a>
<br> <br>
<pre class="collapse" id="full-json-profile-json">
</pre>
</div>
<div class="page-header">
</div> <br>
<script>
//Inject Spilled Tags
$(window).on('load', function () {
injectIconByClass("spill-tag","glyphicon-download-alt");
injectIconByClass("time-skew-tag","glyphicon-time");
injectSlowScanIcon();
//Building RowCount
buildRowCountMap();
injectEstimatedRows();
});
//Inject Glyphicon by Class tag
function injectIconByClass(tagLabel, tagIcon) {
//Inject Spill icons
var tagElemList = document.getElementsByClassName(tagLabel);
var i;
for (i = 0; i < tagElemList.length; i++) {
var content = tagElemList[i].innerHTML;
tagElemList[i].innerHTML = "<span class=\"glyphicon " + tagIcon + "\">&nbsp;</span>" + content;
}
}
//Inject PNG icon for slow
function injectSlowScanIcon() {
//Inject Spill icons
var tagElemList = document.getElementsByClassName("scan-wait-tag");
var i;
for (i = 0; i < tagElemList.length; i++) {
var content = tagElemList[i].innerHTML;
tagElemList[i].innerHTML = "<img src='/static/img/turtle.png' alt='slow'> " + content;
}
}
//Configuration for Query Viewer in Profile
ace.require("ace/ext/language_tools");
var viewer = ace.edit("query-text");
viewer.setAutoScrollEditorIntoView(true);
viewer.setOption("minLines", 3);
viewer.setOption("maxLines", 20);
viewer.renderer.setShowGutter(false);
viewer.renderer.setOption('showLineNumbers', false);
viewer.renderer.setOption('showPrintMargin', false);
viewer.renderer.setOption("vScrollBarAlwaysVisible", true);
viewer.renderer.setOption("hScrollBarAlwaysVisible", true);
viewer.renderer.setScrollMargin(10, 10, 10, 10);
viewer.getSession().setMode("ace/mode/sql");
viewer.setTheme("ace/theme/sqlserver");
//CSS Formatting
document.getElementById('query-query').style.fontSize='13px';
document.getElementById('query-query').style.fontFamily='courier,monospace';
document.getElementById('query-query').style.lineHeight='1.5';
document.getElementById('query-query').style.width='98%';
document.getElementById('query-query').style.margin='auto';
viewer.resize();
viewer.setReadOnly(true);
viewer.setOptions({
enableBasicAutocompletion: false,
enableSnippets: false,
enableLiveAutocompletion: false
});
//Configuration for Query Editor in Profile
ace.require("ace/ext/language_tools");
var editor = ace.edit("query-editor");
//Hidden text input for form-submission
var queryText = $('input[name="query"]');
editor.getSession().on("change", function () {
queryText.val(editor.getSession().getValue());
});
editor.setAutoScrollEditorIntoView(true);
editor.setOption("maxLines", 16);
editor.setOption("minLines", 10);
editor.renderer.setShowGutter(true);
editor.renderer.setOption('showLineNumbers', true);
editor.renderer.setOption('showPrintMargin', false);
editor.renderer.setOption("vScrollBarAlwaysVisible", true);
editor.renderer.setOption("hScrollBarAlwaysVisible", true);;
editor.renderer.setScrollMargin(10, 10, 10, 10);
editor.getSession().setMode("ace/mode/sql");
editor.getSession().setTabSize(4);
editor.getSession().setUseSoftTabs(true);
editor.setTheme("ace/theme/sqlserver");
editor.$blockScrolling = "Infinity";
//CSS Formatting
document.getElementById('query-editor').style.fontSize='13px';
document.getElementById('query-editor').style.fontFamily='courier,monospace';
document.getElementById('query-editor').style.lineHeight='1.5';
document.getElementById('query-editor').style.width='98%';
document.getElementById('query-editor').style.margin='auto';
document.getElementById('query-editor').style.backgroundColor='#ffffff';
editor.setOptions({
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: false
});
//Pops out a new window and provide prompt to print
var popUpAndPrintPlan = function() {
var srcSvg = $('#query-visual-canvas');
var screenRatio=0.9;
let printWindow = window.open('', 'PlanPrint', 'width=' + (screenRatio*screen.width) + ',height=' + (screenRatio*screen.height));
printWindow.document.writeln($(srcSvg).parent().html());
printWindow.print();
};
//Provides hint based on OS
var browserOS = navigator.platform.toLowerCase();
if ((browserOS.indexOf("mac") > -1)) {
document.getElementById('keyboardHint').innerHTML="Meta+Enter";
} else {
document.getElementById('keyboardHint').innerHTML="Ctrl+Enter";
}
// meta+enter / ctrl+enter to submit query
document.getElementById('queryForm')
.addEventListener('keydown', function(e) {
if (!(e.keyCode == 13 && (e.metaKey || e.ctrlKey))) return;
if (e.target.form)
<#if model.isOnlyImpersonationEnabled()>doSubmitQueryWithUserName()<#else>doSubmitQueryWithAutoLimit()</#if>;
});
// Extract estimated rowcount map
var opRowCountMap = {};
// Get OpId-Rowocunt Map
function buildRowCountMap() {
var phyText = $('#query-physical').find('pre').text();
var opLines = phyText.split("\n");
opLines.forEach(line => {
if (line.trim().length > 0) {
var opId = line.match(/\d+-\d+/g)[0];
var opRowCount = line.match(/rowcount = ([^,]+)/)[1];
opRowCountMap[opId] = Number(opRowCount).toLocaleString('en');
}
});
}
</script>
</#macro>
<#macro list_options options scope>
<#if (options?keys?size > 0) >
<div class="panel-body">
<h4>${scope} Options</h4>
<table id="${scope}_options_table" class="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<#list options?keys as name>
<tr>
<td>${name}</td>
<td>${options[name]}</td>
</tr>
</#list>
</tbody>
</table>
</div>
</#if>
</#macro>
<@page_html/>