Add support for Trigger and JIT stats in the graphical query plan viewer. Fixes #3397

This commit is contained in:
Akshay Joshi
2018-07-06 13:13:14 +01:00
committed by Dave Page
parent 73530c05aa
commit 2b20b387e2
6 changed files with 296 additions and 6 deletions

View File

@@ -10,7 +10,7 @@ This release contains a number of features and fixes reported since the release
Features
********
| `Feature #???? <https://redmine.postgresql.org/issues/????>`_ - Add support for foo!
| `Feature #3397 <https://redmine.postgresql.org/issues/3397>`_ - Add support for Trigger and JIT stats in the graphical query plan viewer.
Bug fixes

View File

@@ -104,6 +104,16 @@ class QueryToolFeatureTest(BaseFeatureTest):
self._query_tool_notify_statements()
self._clear_query_tool()
# explain query with JIT stats
print("Explain query with JIT stats... ",
file=sys.stderr, end="")
if self._supported_jit_on_server():
self._query_tool_explain_check_jit_stats()
print("OK.", file=sys.stderr)
self._clear_query_tool()
else:
print("Skipped.", file=sys.stderr)
def after(self):
self.page.remove_server(self.server)
connection = test_utils.get_db_connection(
@@ -660,9 +670,62 @@ SELECT 1, pg_sleep(300)"""
wait.until(WaitForAnyElementWithText(
(By.CSS_SELECTOR, 'td.payload'), "Hello"))
print("OK.", file=sys.stderr)
self._clear_query_tool()
else:
print("Skipped.", file=sys.stderr)
def _supported_jit_on_server(self):
connection = test_utils.get_db_connection(
self.server['db'],
self.server['username'],
self.server['db_password'],
self.server['host'],
self.server['port'],
self.server['sslmode']
)
pg_cursor = connection.cursor()
pg_cursor.execute('select version()')
version_string = pg_cursor.fetchone()
is_edb = False
if len(version_string) > 0:
is_edb = 'EnterpriseDB' in version_string[0]
connection.close()
return connection.server_version >= 110000 and not is_edb
def _query_tool_explain_check_jit_stats(self):
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with("SET jit_above_cost=10;")
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self._clear_query_tool()
self.page.fill_codemirror_area_with("SELECT count(*) FROM pg_class;")
query_op = self.page.find_by_id("btn-query-dropdown")
query_op.click()
ActionChains(self.driver).move_to_element(
query_op.find_element_by_xpath(
"//li[contains(.,'Explain Options')]")).perform()
self.page.find_by_id("btn-explain-verbose").click()
self.page.find_by_id("btn-explain-costs").click()
self.page.find_by_id("btn-explain-analyze").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
# Search for 'Output' word in result (verbose option)
canvas.find_element_by_xpath("//*[contains(string(), 'JIT')]")
self._clear_query_tool()
class WaitForAnyElementWithText(object):
def __init__(self, locator, text):

View File

@@ -17,6 +17,24 @@
opacity: 1;
}
.pg-explain-stats-area {
position: absolute;
top: 5px;
right: 25px;
opacity: 0.5;
}
.pg-explain-stats-btn {
top: 5px;
min-width: 25px;
border: 1px solid transparent;
pointer-events: none;
}
.pg-explain-stats-area:hover {
opacity: 1;
}
.explain-tooltip {
display: table-cell;
text-align: left;
@@ -37,8 +55,6 @@ td.explain-tooltip-val {
.pgadmin-explain-tooltip {
position: absolute;
padding:5px;
border: 1px solid white;
opacity:0;
color: cornsilk;
background-color: #010125;

View File

@@ -1,7 +1,7 @@
define('pgadmin.misc.explain', [
'sources/url_for', 'jquery', 'underscore', 'underscore.string',
'sources/pgadmin', 'backbone', 'snapsvg',
], function(url_for, $, _, S, pgAdmin, Backbone, Snap) {
'sources/pgadmin', 'backbone', 'snapsvg', 'explain_statistics',
], function(url_for, $, _, S, pgAdmin, Backbone, Snap, StatisticsModel) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -615,6 +615,9 @@ define('pgadmin.misc.explain', [
});
toolTipContainer.css('left', toolTipX);
toolTipContainer.css('top', toolTipY);
$('.pgadmin-explain-tooltip').css('padding', '5px');
$('.pgadmin-explain-tooltip').css('border', '1px solid white');
});
// Remove tooltip when mouse is out from node's area
@@ -696,6 +699,7 @@ define('pgadmin.misc.explain', [
},
initialize: function() {
this.set('Plan', new PlanModel());
this.set('Statistics', new StatisticsModel());
},
// Parse the JSON data and fetch its children plans
@@ -718,6 +722,17 @@ define('pgadmin.misc.explain', [
delete data['Plan'];
}
var statistics = this.get('Statistics');
if (data && 'JIT' in data) {
statistics.set('JIT', data['JIT']);
delete data ['JIT'];
}
if (data && 'Triggers' in data) {
statistics.set('Triggers', data['Triggers']);
delete data ['Triggers'];
}
return data;
},
toJSON: function() {
@@ -745,6 +760,10 @@ define('pgadmin.misc.explain', [
plan.draw(
g, xpos, ypos, undefined, undefined, graphContainer, toolTipContainer
);
//Set the Statistics as tooltip
var statistics = this.get('Statistics');
statistics.set_statistics(toolTipContainer);
},
});
@@ -784,6 +803,21 @@ define('pgadmin.misc.explain', [
class: 'fa fa-search-minus',
}));
var statsArea = $('<div></div>', {
class: 'pg-explain-stats-area btn-group hidden',
role: 'group',
}).appendTo(container);
$('<button></button>', {
id: 'btn-explain-stats',
class: 'btn pg-explain-stats-btn badge',
title: 'Statistics',
tabindex: 0,
}).appendTo(statsArea).append(
$('<i></i>', {
class: 'fa fa-line-chart',
}));
// Main div to be drawn all images on
var planDiv = $('<div></div>', {
class: 'pgadmin-explain-container',

View File

@@ -0,0 +1,90 @@
import $ from 'jquery';
import Backbone from 'backbone';
// Backbone model for other statistics
let StatisticsModel = Backbone.Model.extend({
defaults: {
JIT: [],
Triggers: [],
},
set_statistics: function(toolTipContainer) {
var jit_stats = this.get('JIT'),
triggers_stats = this.get('Triggers');
if (Object.keys(jit_stats).length > 0 ||
Object.keys(triggers_stats).length > 0) {
$('.pg-explain-stats-area').removeClass('hidden');
}
$('.pg-explain-stats-area').on('mouseover', () => {
// Empty the tooltip content if it has any and add new data
toolTipContainer.empty();
if (Object.keys(jit_stats).length == 0 &&
Object.keys(triggers_stats).length == 0) {
return;
}
var tooltip = $('<table></table>', {
class: 'pgadmin-tooltip-table',
}).appendTo(toolTipContainer);
if (Object.keys(jit_stats).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">JIT:</td></tr>');
_.each(jit_stats, function(value, key) {
tooltip.append('<tr><td class="label explain-tooltip">&nbsp&nbsp'
+ key + '</td><td class="label explain-tooltip-val">'
+ value + '</td></tr>');
});
}
if (Object.keys(triggers_stats).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">Triggers:</td></tr>');
_.each(triggers_stats, function(triggers, key_id) {
if (triggers instanceof Object) {
_.each(triggers, function(value, key) {
if (key === 'Trigger Name') {
tooltip.append('<tr><td class="label explain-tooltip">&nbsp;&nbsp;'
+ key + '</td><td class="label explain-tooltip-val">'
+ value + '</td></tr>');
} else {
tooltip.append('<tr><td class="label explain-tooltip">&nbsp;&nbsp;&nbsp;&nbsp;'
+ key + '</td><td class="label explain-tooltip-val">'
+ value + '</td></tr>');
}
});
}
else {
tooltip.append('<tr><td class="label explain-tooltip">&nbsp;&nbsp;'
+ key_id + '</td><td class="label explain-tooltip-val">'
+ triggers + '</td></tr>');
}
});
}
// Show toolTip at respective x,y coordinates
toolTipContainer.css({
'opacity': '0.8',
'left': '',
'right': '65px',
'top': '15px',
});
$('.pgadmin-explain-tooltip').css('padding', '5px');
$('.pgadmin-explain-tooltip').css('border', '1px solid white');
});
// Remove tooltip when mouse is out from node's area
$('.pg-explain-stats-area').on('mouseout', () => {
toolTipContainer.empty();
toolTipContainer.css({
'opacity': '0',
'left': 0,
'top': 0,
});
});
},
});
module.exports = StatisticsModel;

View File

@@ -0,0 +1,87 @@
import StatisticsModel from '../../../../pgadmin/misc/static/explain/js/explain_statistics';
import $ from 'jquery';
describe('ExplainStatistics', () => {
let statsModel;
let statsDiv;
let tooltipContainer;
beforeEach(function() {
statsModel = new StatisticsModel();
statsDiv = '<div class="pg-explain-stats-area btn-group hidden"></div>';
tooltipContainer = $('<div></div>', {
id: 'toolTip',
class: 'pgadmin-explain-tooltip',
});
});
describe('No Statistics', () => {
it('Statistics button should be hidden', () => {
$('body').append(statsDiv);
statsModel.set('JIT', []);
statsModel.set('Triggers', []);
statsModel.set_statistics(tooltipContainer);
expect($('.pg-explain-stats-area').hasClass('hidden')).toBe(true);
});
});
describe('JIT Statistics', () => {
beforeEach(function() {
$('body').append(statsDiv);
statsModel.set('JIT', [{'cost': '100'}]);
statsModel.set('Triggers', []);
statsModel.set_statistics(tooltipContainer);
});
it('Statistics button should be visible', () => {
expect($('.pg-explain-stats-area').hasClass('hidden')).toBe(false);
});
it('Mouse over event should be trigger', () => {
// Trigger mouse over event
var hoverEvent = new $.Event('mouseover');
$('.pg-explain-stats-area').trigger(hoverEvent);
expect(tooltipContainer.css('opacity')).toBe('0.8');
});
it('Mouse out event should be trigger', () => {
// Trigger mouse out event
var hoverEvent = new $.Event('mouseout');
$('.pg-explain-stats-area').trigger(hoverEvent);
expect(tooltipContainer.css('opacity')).toBe('0');
});
});
describe('Triggers Statistics', () => {
beforeEach(function() {
$('body').append(statsDiv);
statsModel.set('JIT', []);
statsModel.set('Triggers', [{'name': 'test_trigger'}]);
statsModel.set_statistics(tooltipContainer);
});
it('Statistics button should be visible', () => {
expect($('.pg-explain-stats-area').hasClass('hidden')).toBe(false);
});
it('Mouse over event should be trigger', () => {
// Trigger mouse over event
var hoverEvent = new $.Event('mouseover');
$('.pg-explain-stats-area').trigger(hoverEvent);
expect(tooltipContainer.css('opacity')).toBe('0.8');
});
it('Mouse out event should be trigger', () => {
// Trigger mouse out event
var hoverEvent = new $.Event('mouseout');
$('.pg-explain-stats-area').trigger(hoverEvent);
expect(tooltipContainer.css('opacity')).toBe('0');
});
});
});