From 2b20b387e2b2ff2cf20e3806248b6ad5ccd22365 Mon Sep 17 00:00:00 2001 From: Akshay Joshi Date: Fri, 6 Jul 2018 13:13:14 +0100 Subject: [PATCH] Add support for Trigger and JIT stats in the graphical query plan viewer. Fixes #3397 --- docs/en_US/release_notes_3_2.rst | 2 +- web/pgadmin/feature_tests/query_tool_tests.py | 63 +++++++++++++ .../misc/static/explain/css/explain.css | 22 ++++- web/pgadmin/misc/static/explain/js/explain.js | 38 +++++++- .../static/explain/js/explain_statistics.js | 90 +++++++++++++++++++ .../misc/explain/explain_statistics_spec.js | 87 ++++++++++++++++++ 6 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 web/pgadmin/misc/static/explain/js/explain_statistics.js create mode 100644 web/regression/javascript/misc/explain/explain_statistics_spec.js diff --git a/docs/en_US/release_notes_3_2.rst b/docs/en_US/release_notes_3_2.rst index e2682cb8e..b0f96bf2c 100644 --- a/docs/en_US/release_notes_3_2.rst +++ b/docs/en_US/release_notes_3_2.rst @@ -10,7 +10,7 @@ This release contains a number of features and fixes reported since the release Features ******** -| `Feature #???? `_ - Add support for foo! +| `Feature #3397 `_ - Add support for Trigger and JIT stats in the graphical query plan viewer. Bug fixes diff --git a/web/pgadmin/feature_tests/query_tool_tests.py b/web/pgadmin/feature_tests/query_tool_tests.py index ac463d35f..226b32313 100644 --- a/web/pgadmin/feature_tests/query_tool_tests.py +++ b/web/pgadmin/feature_tests/query_tool_tests.py @@ -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): diff --git a/web/pgadmin/misc/static/explain/css/explain.css b/web/pgadmin/misc/static/explain/css/explain.css index d549f8548..98a077599 100644 --- a/web/pgadmin/misc/static/explain/css/explain.css +++ b/web/pgadmin/misc/static/explain/css/explain.css @@ -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; -} \ No newline at end of file +} diff --git a/web/pgadmin/misc/static/explain/js/explain.js b/web/pgadmin/misc/static/explain/js/explain.js index e02877428..e27d810d7 100644 --- a/web/pgadmin/misc/static/explain/js/explain.js +++ b/web/pgadmin/misc/static/explain/js/explain.js @@ -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 = $('
', { + class: 'pg-explain-stats-area btn-group hidden', + role: 'group', + }).appendTo(container); + + $('', { + id: 'btn-explain-stats', + class: 'btn pg-explain-stats-btn badge', + title: 'Statistics', + tabindex: 0, + }).appendTo(statsArea).append( + $('', { + class: 'fa fa-line-chart', + })); + // Main div to be drawn all images on var planDiv = $('
', { class: 'pgadmin-explain-container', diff --git a/web/pgadmin/misc/static/explain/js/explain_statistics.js b/web/pgadmin/misc/static/explain/js/explain_statistics.js new file mode 100644 index 000000000..369a6717c --- /dev/null +++ b/web/pgadmin/misc/static/explain/js/explain_statistics.js @@ -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 = $('
', { + class: 'pgadmin-tooltip-table', + }).appendTo(toolTipContainer); + + if (Object.keys(jit_stats).length > 0){ + tooltip.append('JIT:'); + _.each(jit_stats, function(value, key) { + tooltip.append('  ' + + key + '' + + value + ''); + }); + } + + if (Object.keys(triggers_stats).length > 0){ + tooltip.append('Triggers:'); + _.each(triggers_stats, function(triggers, key_id) { + if (triggers instanceof Object) { + _.each(triggers, function(value, key) { + if (key === 'Trigger Name') { + tooltip.append('  ' + + key + '' + + value + ''); + } else { + tooltip.append('    ' + + key + '' + + value + ''); + } + }); + } + else { + tooltip.append('  ' + + key_id + '' + + triggers + ''); + } + }); + } + + // 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; diff --git a/web/regression/javascript/misc/explain/explain_statistics_spec.js b/web/regression/javascript/misc/explain/explain_statistics_spec.js new file mode 100644 index 000000000..0b2ec67c1 --- /dev/null +++ b/web/regression/javascript/misc/explain/explain_statistics_spec.js @@ -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 = ''; + tooltipContainer = $('
', { + 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'); + }); + }); +});