mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Add support for Trigger and JIT stats in the graphical query plan viewer. Fixes #3397
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
@@ -55,4 +71,4 @@ td.explain-tooltip-val {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
90
web/pgadmin/misc/static/explain/js/explain_statistics.js
Normal file
90
web/pgadmin/misc/static/explain/js/explain_statistics.js
Normal 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">  '
|
||||
+ 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"> '
|
||||
+ key + '</td><td class="label explain-tooltip-val">'
|
||||
+ value + '</td></tr>');
|
||||
} else {
|
||||
tooltip.append('<tr><td class="label explain-tooltip"> '
|
||||
+ key + '</td><td class="label explain-tooltip-val">'
|
||||
+ value + '</td></tr>');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
tooltip.append('<tr><td class="label explain-tooltip"> '
|
||||
+ 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;
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user