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
|
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
|
Bug fixes
|
||||||
|
|||||||
@@ -104,6 +104,16 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
|||||||
self._query_tool_notify_statements()
|
self._query_tool_notify_statements()
|
||||||
self._clear_query_tool()
|
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):
|
def after(self):
|
||||||
self.page.remove_server(self.server)
|
self.page.remove_server(self.server)
|
||||||
connection = test_utils.get_db_connection(
|
connection = test_utils.get_db_connection(
|
||||||
@@ -660,9 +670,62 @@ SELECT 1, pg_sleep(300)"""
|
|||||||
wait.until(WaitForAnyElementWithText(
|
wait.until(WaitForAnyElementWithText(
|
||||||
(By.CSS_SELECTOR, 'td.payload'), "Hello"))
|
(By.CSS_SELECTOR, 'td.payload'), "Hello"))
|
||||||
print("OK.", file=sys.stderr)
|
print("OK.", file=sys.stderr)
|
||||||
|
self._clear_query_tool()
|
||||||
else:
|
else:
|
||||||
print("Skipped.", file=sys.stderr)
|
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):
|
class WaitForAnyElementWithText(object):
|
||||||
def __init__(self, locator, text):
|
def __init__(self, locator, text):
|
||||||
|
|||||||
@@ -17,6 +17,24 @@
|
|||||||
opacity: 1;
|
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 {
|
.explain-tooltip {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -37,8 +55,6 @@ td.explain-tooltip-val {
|
|||||||
|
|
||||||
.pgadmin-explain-tooltip {
|
.pgadmin-explain-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding:5px;
|
|
||||||
border: 1px solid white;
|
|
||||||
opacity:0;
|
opacity:0;
|
||||||
color: cornsilk;
|
color: cornsilk;
|
||||||
background-color: #010125;
|
background-color: #010125;
|
||||||
@@ -55,4 +71,4 @@ td.explain-tooltip-val {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
define('pgadmin.misc.explain', [
|
define('pgadmin.misc.explain', [
|
||||||
'sources/url_for', 'jquery', 'underscore', 'underscore.string',
|
'sources/url_for', 'jquery', 'underscore', 'underscore.string',
|
||||||
'sources/pgadmin', 'backbone', 'snapsvg',
|
'sources/pgadmin', 'backbone', 'snapsvg', 'explain_statistics',
|
||||||
], function(url_for, $, _, S, pgAdmin, Backbone, Snap) {
|
], function(url_for, $, _, S, pgAdmin, Backbone, Snap, StatisticsModel) {
|
||||||
|
|
||||||
pgAdmin = pgAdmin || window.pgAdmin || {};
|
pgAdmin = pgAdmin || window.pgAdmin || {};
|
||||||
|
|
||||||
@@ -615,6 +615,9 @@ define('pgadmin.misc.explain', [
|
|||||||
});
|
});
|
||||||
toolTipContainer.css('left', toolTipX);
|
toolTipContainer.css('left', toolTipX);
|
||||||
toolTipContainer.css('top', toolTipY);
|
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
|
// Remove tooltip when mouse is out from node's area
|
||||||
@@ -696,6 +699,7 @@ define('pgadmin.misc.explain', [
|
|||||||
},
|
},
|
||||||
initialize: function() {
|
initialize: function() {
|
||||||
this.set('Plan', new PlanModel());
|
this.set('Plan', new PlanModel());
|
||||||
|
this.set('Statistics', new StatisticsModel());
|
||||||
},
|
},
|
||||||
|
|
||||||
// Parse the JSON data and fetch its children plans
|
// Parse the JSON data and fetch its children plans
|
||||||
@@ -718,6 +722,17 @@ define('pgadmin.misc.explain', [
|
|||||||
delete data['Plan'];
|
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;
|
return data;
|
||||||
},
|
},
|
||||||
toJSON: function() {
|
toJSON: function() {
|
||||||
@@ -745,6 +760,10 @@ define('pgadmin.misc.explain', [
|
|||||||
plan.draw(
|
plan.draw(
|
||||||
g, xpos, ypos, undefined, undefined, graphContainer, toolTipContainer
|
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',
|
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
|
// Main div to be drawn all images on
|
||||||
var planDiv = $('<div></div>', {
|
var planDiv = $('<div></div>', {
|
||||||
class: 'pgadmin-explain-container',
|
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