From a200619d14bf1d90c21503ec358a30ca84d5337f Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 19 Mar 2018 19:06:09 +0000 Subject: [PATCH] Show Ajax requests in performance bar But first, rewrite the performance bar in Vue: 1. Remove the peek-host gem and replace it with existing code. This also allows us to include the host in the JSON response, rather than in the page HTML. 2. Leave the line profiler parts as here-be-dragons: nicer would be a separate endpoint for these, so we could use them on Ajax requests too. 3. The performance bar is too fiddly to rewrite right now, so apply the same logic to that. Then, add features! All requests made through Axios are able to be tracked. To keep a lid on memory usage, only the first two requests for a given URL are tracked, though. Each request that's tracked has the same data as the initial page load, with the exception of the performance bar and the line profiler, as explained above. --- Gemfile | 1 - Gemfile.lock | 3 - app/assets/javascripts/dispatcher.js | 12 +- app/assets/javascripts/performance_bar.js | 57 ------ .../components/detailed_metric.vue | 78 +++++++ .../components/performance_bar_app.vue | 191 ++++++++++++++++++ .../components/request_selector.vue | 52 +++++ .../components/simple_metric.vue | 30 +++ .../components/upstream_performance_bar.vue | 18 ++ .../javascripts/performance_bar/index.js | 37 ++++ .../services/performance_bar_service.js | 24 +++ .../stores/performance_bar_store.js | 39 ++++ app/assets/stylesheets/performance_bar.scss | 29 ++- app/views/peek/_bar.html.haml | 12 ++ app/views/peek/views/_gitaly.html.haml | 17 -- app/views/peek/views/_host.html.haml | 2 - app/views/peek/views/_mysql2.html.haml | 4 - app/views/peek/views/_pg.html.haml | 4 - app/views/peek/views/_rblineprof.html.haml | 7 - app/views/peek/views/_sql.html.haml | 14 -- .../ajax-requests-in-performance-bar.yml | 5 + config/routes.rb | 2 +- .../monitoring/performance/performance_bar.md | 6 +- lib/peek/views/host.rb | 9 + .../user_can_display_performance_bar_spec.rb | 10 +- .../components/detailed_metric_spec.js | 88 ++++++++ .../components/performance_bar_app_spec.js | 88 ++++++++ .../components/request_selector_spec.js | 47 +++++ .../components/simple_metric_spec.js | 47 +++++ vendor/assets/javascripts/peek.js | 86 -------- 30 files changed, 807 insertions(+), 212 deletions(-) delete mode 100644 app/assets/javascripts/performance_bar.js create mode 100644 app/assets/javascripts/performance_bar/components/detailed_metric.vue create mode 100644 app/assets/javascripts/performance_bar/components/performance_bar_app.vue create mode 100644 app/assets/javascripts/performance_bar/components/request_selector.vue create mode 100644 app/assets/javascripts/performance_bar/components/simple_metric.vue create mode 100644 app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue create mode 100644 app/assets/javascripts/performance_bar/index.js create mode 100644 app/assets/javascripts/performance_bar/services/performance_bar_service.js create mode 100644 app/assets/javascripts/performance_bar/stores/performance_bar_store.js create mode 100644 app/views/peek/_bar.html.haml delete mode 100644 app/views/peek/views/_gitaly.html.haml delete mode 100644 app/views/peek/views/_host.html.haml delete mode 100644 app/views/peek/views/_mysql2.html.haml delete mode 100644 app/views/peek/views/_pg.html.haml delete mode 100644 app/views/peek/views/_rblineprof.html.haml delete mode 100644 app/views/peek/views/_sql.html.haml create mode 100644 changelogs/unreleased/ajax-requests-in-performance-bar.yml create mode 100644 lib/peek/views/host.rb create mode 100644 spec/javascripts/performance_bar/components/detailed_metric_spec.js create mode 100644 spec/javascripts/performance_bar/components/performance_bar_app_spec.js create mode 100644 spec/javascripts/performance_bar/components/request_selector_spec.js create mode 100644 spec/javascripts/performance_bar/components/simple_metric_spec.js delete mode 100644 vendor/assets/javascripts/peek.js diff --git a/Gemfile b/Gemfile index 41b5fe07d82..e423f4ba32f 100644 --- a/Gemfile +++ b/Gemfile @@ -276,7 +276,6 @@ gem 'batch-loader', '~> 1.2.1' # Perf bar gem 'peek', '~> 1.0.1' gem 'peek-gc', '~> 0.0.2' -gem 'peek-host', '~> 1.0.0' gem 'peek-mysql2', '~> 1.1.0', group: :mysql gem 'peek-performance_bar', '~> 1.3.0' gem 'peek-pg', '~> 1.3.0', group: :postgres diff --git a/Gemfile.lock b/Gemfile.lock index 8a37b3c4152..1c6c7edb1a0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -593,8 +593,6 @@ GEM railties (>= 4.0.0) peek-gc (0.0.2) peek - peek-host (1.0.0) - peek peek-mysql2 (1.1.0) atomic (>= 1.0.0) mysql2 @@ -1124,7 +1122,6 @@ DEPENDENCIES org-ruby (~> 0.9.12) peek (~> 1.0.1) peek-gc (~> 0.0.2) - peek-host (~> 1.0.0) peek-mysql2 (~> 1.1.0) peek-performance_bar (~> 1.3.0) peek-pg (~> 1.3.0) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 42ecc415173..72f21f13860 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,8 +53,12 @@ function initPageShortcuts(page) { function initGFMInput() { $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { - const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); + const gfm = new GfmAutoComplete( + gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources, + ); + const enableGFM = convertPermissionToBoolean( + el.dataset.supportsAutocomplete, + ); gfm.setup($(el), { emojis: true, members: enableGFM, @@ -67,9 +71,9 @@ function initGFMInput() { } function initPerformanceBar() { - if (document.querySelector('#peek')) { + if (document.querySelector('#js-peek')) { import('./performance_bar') - .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap + .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap .catch(() => Flash('Error loading performance bar module')); } } diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js deleted file mode 100644 index c22598ee665..00000000000 --- a/app/assets/javascripts/performance_bar.js +++ /dev/null @@ -1,57 +0,0 @@ -import $ from 'jquery'; -import 'vendor/peek'; -import 'vendor/peek.performance_bar'; -import { getParameterValues } from './lib/utils/url_utility'; - -export default class PerformanceBar { - constructor(opts) { - if (!PerformanceBar.singleton) { - this.init(opts); - PerformanceBar.singleton = this; - } - return PerformanceBar.singleton; - } - - init(opts) { - const $container = $(opts.container); - this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile'); - this.$lineProfileModal = $('#modal-peek-line-profile'); - this.initEventListeners(); - this.showModalOnLoad(); - } - - initEventListeners() { - this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e)); - $(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile); - } - - showModalOnLoad() { - // When a lineprofiler query-string param is present, we show the line - // profiler modal upon page load - if (/lineprofiler/.test(window.location.search)) { - PerformanceBar.toggleModal(this.$lineProfileModal); - } - } - - handleLineProfileLink(e) { - const lineProfilerParameter = getParameterValues('lineprofiler'); - const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`); - const shouldToggleModal = lineProfilerParameter.length > 0 && - lineProfilerParameterRegex.test(e.currentTarget.href); - - if (shouldToggleModal) { - e.preventDefault(); - PerformanceBar.toggleModal(this.$lineProfileModal); - } - } - - static toggleModal($modal) { - if ($modal.length) { - $modal.modal('toggle'); - } - } - - static toggleLineProfileFile(e) { - $(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle(); - } -} diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue new file mode 100644 index 00000000000..145465f4ee9 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -0,0 +1,78 @@ + + diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue new file mode 100644 index 00000000000..88345cf2ad9 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -0,0 +1,191 @@ + + diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue new file mode 100644 index 00000000000..2f360ea6f6c --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -0,0 +1,52 @@ + + diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue new file mode 100644 index 00000000000..b654bc66249 --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue @@ -0,0 +1,30 @@ + + diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue new file mode 100644 index 00000000000..d438b1ec27b --- /dev/null +++ b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue @@ -0,0 +1,18 @@ + + diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js new file mode 100644 index 00000000000..fca488120f6 --- /dev/null +++ b/app/assets/javascripts/performance_bar/index.js @@ -0,0 +1,37 @@ +import 'vendor/peek.performance_bar'; + +import Vue from 'vue'; +import performanceBarApp from './components/performance_bar_app.vue'; +import PerformanceBarStore from './stores/performance_bar_store'; + +export default () => + new Vue({ + el: '#js-peek', + components: { + performanceBarApp, + }, + data() { + const performanceBarData = document.querySelector(this.$options.el) + .dataset; + const store = new PerformanceBarStore(); + + return { + store, + env: performanceBarData.env, + requestId: performanceBarData.requestId, + peekUrl: performanceBarData.peekUrl, + profileUrl: performanceBarData.profileUrl, + }; + }, + render(createElement) { + return createElement('performance-bar-app', { + props: { + store: this.store, + env: this.env, + requestId: this.requestId, + peekUrl: this.peekUrl, + profileUrl: this.profileUrl, + }, + }); + }, + }); diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js new file mode 100644 index 00000000000..d8e792446c3 --- /dev/null +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -0,0 +1,24 @@ +import axios from '../../lib/utils/axios_utils'; + +export default class PerformanceBarService { + static fetchRequestDetails(peekUrl, requestId) { + return axios.get(peekUrl, { params: { request_id: requestId } }); + } + + static registerInterceptor(peekUrl, callback) { + return axios.interceptors.response.use(response => { + const requestId = response.headers['x-request-id']; + const requestUrl = response.config.url; + + if (requestUrl !== peekUrl && requestId) { + callback(requestId, requestUrl); + } + + return response; + }); + } + + static removeInterceptor(interceptor) { + axios.interceptors.response.eject(interceptor); + } +} diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js new file mode 100644 index 00000000000..c6b2f55243c --- /dev/null +++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js @@ -0,0 +1,39 @@ +export default class PerformanceBarStore { + constructor() { + this.requests = []; + } + + addRequest(requestId, requestUrl, requestDetails) { + if (!this.findRequest(requestId)) { + this.requests.push({ + id: requestId, + url: requestUrl, + details: requestDetails, + }); + } + + return this.requests; + } + + findRequest(requestId) { + return this.requests.find(request => request.id === requestId); + } + + addRequestDetails(requestId, requestDetails) { + const request = this.findRequest(requestId); + + request.details = requestDetails; + + return request; + } + + requestsWithDetails() { + return this.requests.filter(request => request.details); + } + + canTrackRequest(requestUrl) { + return ( + this.requests.filter(request => request.url === requestUrl).length < 2 + ); + } +} diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 6e539e39ca1..d06148a7bf8 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -1,8 +1,8 @@ -@import "framework/variables"; -@import "peek/views/performance_bar"; -@import "peek/views/rblineprof"; +@import 'framework/variables'; +@import 'peek/views/performance_bar'; +@import 'peek/views/rblineprof'; -#peek { +#js-peek { position: fixed; left: 0; top: 0; @@ -21,14 +21,26 @@ &.production { background-color: $perf-bar-production; + + select { + background: $perf-bar-production; + } } &.staging { background-color: $perf-bar-staging; + + select { + background: $perf-bar-staging; + } } &.development { background-color: $perf-bar-development; + + select { + background: $perf-bar-development; + } } .wrapper { @@ -42,11 +54,12 @@ background: $perf-bar-bucket-bg; display: inline-block; padding: 4px 6px; - font-family: Consolas, "Liberation Mono", Courier, monospace; + font-family: Consolas, 'Liberation Mono', Courier, monospace; line-height: 1; color: $perf-bar-bucket-color; border-radius: 3px; - box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to; + box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, + inset 0 1px 2px $perf-bar-bucket-box-shadow-to; .hidden { display: none; @@ -94,6 +107,10 @@ max-width: 10000px !important; } } + + .performance-bar-modal .modal-footer { + display: none; + } } #modal-peek-pg-queries-content { diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml new file mode 100644 index 00000000000..14dafa197b5 --- /dev/null +++ b/app/views/peek/_bar.html.haml @@ -0,0 +1,12 @@ +- return unless peek_enabled? + +#js-peek{ data: { env: Peek.env, + request_id: Peek.request_id, + peek_url: peek_routes.results_url, + profile_url: url_for(params.merge(lineprofiler: 'true')) }, + class: Peek.env } + +#peek-view-performance-bar + = render_server_response_time + %span#serverstats + %ul.performance-bar diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml deleted file mode 100644 index 945bb287429..00000000000 --- a/app/views/peek/views/_gitaly.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- local_assigns.fetch(:view) - -%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-gitaly-details' } } - %span{ data: { defer_to: "#{view.defer_key}-duration" } }... - \/ - %span{ data: { defer_to: "#{view.defer_key}-calls" } }... -#modal-peek-gitaly-details.modal{ tabindex: -1, role: 'dialog' } - .modal-dialog.modal-full - .modal-content - .modal-header - %button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' } - %span{ 'aria-hidden' => 'true' } - × - %h4 - Gitaly requests - .modal-body{ data: { defer_to: "#{view.defer_key}-details" } }... -gitaly diff --git a/app/views/peek/views/_host.html.haml b/app/views/peek/views/_host.html.haml deleted file mode 100644 index 40769b5c6f6..00000000000 --- a/app/views/peek/views/_host.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%span.current-host - = truncate(view.hostname) diff --git a/app/views/peek/views/_mysql2.html.haml b/app/views/peek/views/_mysql2.html.haml deleted file mode 100644 index ac811a10ef5..00000000000 --- a/app/views/peek/views/_mysql2.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- local_assigns.fetch(:view) - -= render 'peek/views/sql', view: view -mysql diff --git a/app/views/peek/views/_pg.html.haml b/app/views/peek/views/_pg.html.haml deleted file mode 100644 index ee94c2f3274..00000000000 --- a/app/views/peek/views/_pg.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- local_assigns.fetch(:view) - -= render 'peek/views/sql', view: view -pg diff --git a/app/views/peek/views/_rblineprof.html.haml b/app/views/peek/views/_rblineprof.html.haml deleted file mode 100644 index 6c037930ca9..00000000000 --- a/app/views/peek/views/_rblineprof.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -Profile: - -= link_to 'all', url_for(lineprofiler: 'true'), class: 'js-toggle-modal-peek-line-profile' -\/ -= link_to 'app & lib', url_for(lineprofiler: 'app'), class: 'js-toggle-modal-peek-line-profile' -\/ -= link_to 'views', url_for(lineprofiler: 'views'), class: 'js-toggle-modal-peek-line-profile' diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml deleted file mode 100644 index 36583df898a..00000000000 --- a/app/views/peek/views/_sql.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-pg-queries' } } - %span{ data: { defer_to: "#{view.defer_key}-duration" } }... - \/ - %span{ data: { defer_to: "#{view.defer_key}-calls" } }... -#modal-peek-pg-queries.modal{ tabindex: -1 } - .modal-dialog.modal-full - .modal-content - .modal-header - %button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' } - %span{ 'aria-hidden' => 'true' } - × - %h4 - SQL queries - .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }... diff --git a/changelogs/unreleased/ajax-requests-in-performance-bar.yml b/changelogs/unreleased/ajax-requests-in-performance-bar.yml new file mode 100644 index 00000000000..88cc3678c2b --- /dev/null +++ b/changelogs/unreleased/ajax-requests-in-performance-bar.yml @@ -0,0 +1,5 @@ +--- +title: Allow viewing timings for AJAX requests in the performance bar +merge_request: +author: +type: changed diff --git a/config/routes.rb b/config/routes.rb index 35fd76fb119..8769f433c39 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -44,7 +44,7 @@ Rails.application.routes.draw do get 'readiness' => 'health#readiness' post 'storage_check' => 'health#storage_check' resources :metrics, only: [:index] - mount Peek::Railtie => '/peek' + mount Peek::Railtie => '/peek', as: 'peek_routes' # Boards resources shared between group and projects resources :boards, only: [] do diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md index ec1cbce1bad..dc4f685d843 100644 --- a/doc/administration/monitoring/performance/performance_bar.md +++ b/doc/administration/monitoring/performance/performance_bar.md @@ -13,12 +13,16 @@ It allows you to see (from left to right): ![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png) - time taken and number of [Gitaly] calls, click through for details of these calls ![Gitaly profiling using the Performance Bar](img/performance_bar_gitaly_calls.png) -- profile of the code used to generate the page, line by line for either _all_, _app & lib_ , or _views_. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)). +- profile of the code used to generate the page, line by line. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)). ![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png) - time taken and number of calls to Redis - time taken and number of background jobs created by Sidekiq - time taken and number of Ruby GC calls +On the far right is a request selector that allows you to view the same metrics +(excluding the page timing and line profiler) for any requests made while the +page was open. Only the first two requests per unique URL are captured. + ## Enable the Performance Bar via the Admin panel GitLab Performance Bar is disabled by default. To enable it for a given group, diff --git a/lib/peek/views/host.rb b/lib/peek/views/host.rb new file mode 100644 index 00000000000..43c8a35c7ea --- /dev/null +++ b/lib/peek/views/host.rb @@ -0,0 +1,9 @@ +module Peek + module Views + class Host < View + def results + { hostname: Gitlab::Environment.hostname } + end + end + end +end diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb index 975c157bcf5..e069c2fddd1 100644 --- a/spec/features/user_can_display_performance_bar_spec.rb +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe 'User can display performance bar', :js do shared_examples 'performance bar cannot be displayed' do it 'does not show the performance bar by default' do - expect(page).not_to have_css('#peek') + expect(page).not_to have_css('#js-peek') end context 'when user press `pb`' do @@ -12,14 +12,14 @@ describe 'User can display performance bar', :js do end it 'does not show the performance bar by default' do - expect(page).not_to have_css('#peek') + expect(page).not_to have_css('#js-peek') end end end shared_examples 'performance bar can be displayed' do it 'does not show the performance bar by default' do - expect(page).not_to have_css('#peek') + expect(page).not_to have_css('#js-peek') end context 'when user press `pb`' do @@ -28,7 +28,7 @@ describe 'User can display performance bar', :js do end it 'shows the performance bar' do - expect(page).to have_css('#peek') + expect(page).to have_css('#js-peek') end end end @@ -41,7 +41,7 @@ describe 'User can display performance bar', :js do it 'shows the performance bar by default' do refresh # Because we're stubbing Rails.env after the 1st visit to root_path - expect(page).to have_css('#peek') + expect(page).to have_css('#js-peek') end end diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js new file mode 100644 index 00000000000..eee0210a2a9 --- /dev/null +++ b/spec/javascripts/performance_bar/components/detailed_metric_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import detailedMetric from '~/performance_bar/components/detailed_metric.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('detailedMetric', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('when the current request has no details', () => { + beforeEach(() => { + vm = mountComponent(Vue.extend(detailedMetric), { + currentRequest: {}, + metric: 'gitaly', + header: 'Gitaly calls', + details: 'details', + keys: ['feature', 'request'], + }); + }); + + it('does not display details', () => { + expect(vm.$el.innerText).not.toContain('/'); + }); + + it('does not display the modal', () => { + expect(vm.$el.querySelector('.performance-bar-modal')).toBeNull(); + }); + + it('displays the metric name', () => { + expect(vm.$el.innerText).toContain('gitaly'); + }); + }); + + describe('when the current request has details', () => { + const requestDetails = [ + { duration: '100', feature: 'find_commit', request: 'abcdef' }, + { duration: '23', feature: 'rebase_in_progress', request: '' }, + ]; + + beforeEach(() => { + vm = mountComponent(Vue.extend(detailedMetric), { + currentRequest: { + details: { + gitaly: { + duration: '123ms', + calls: '456', + details: requestDetails, + }, + }, + }, + metric: 'gitaly', + header: 'Gitaly calls', + details: 'details', + keys: ['feature', 'request'], + }); + }); + + it('diplays details', () => { + expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456'); + }); + + it('adds a modal with a table of the details', () => { + vm.$el + .querySelectorAll('.performance-bar-modal td strong') + .forEach((duration, index) => { + expect(duration.innerText).toContain(requestDetails[index].duration); + }); + + vm.$el + .querySelectorAll('.performance-bar-modal td:nth-child(2)') + .forEach((feature, index) => { + expect(feature.innerText).toContain(requestDetails[index].feature); + }); + + vm.$el + .querySelectorAll('.performance-bar-modal td:nth-child(3)') + .forEach((request, index) => { + expect(request.innerText).toContain(requestDetails[index].request); + }); + }); + + it('displays the metric name', () => { + expect(vm.$el.innerText).toContain('gitaly'); + }); + }); +}); diff --git a/spec/javascripts/performance_bar/components/performance_bar_app_spec.js b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js new file mode 100644 index 00000000000..9ab9ab1c9f4 --- /dev/null +++ b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import axios from '~/lib/utils/axios_utils'; +import performanceBarApp from '~/performance_bar/components/performance_bar_app.vue'; +import PerformanceBarService from '~/performance_bar/services/performance_bar_service'; +import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store'; + +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import MockAdapter from 'axios-mock-adapter'; + +describe('performance bar', () => { + let mock; + let vm; + + beforeEach(() => { + const store = new PerformanceBarStore(); + + mock = new MockAdapter(axios); + + mock.onGet('/-/peek/results').reply( + 200, + { + data: { + gc: { + invokes: 0, + invoke_time: '0.00', + use_size: 0, + total_size: 0, + total_object: 0, + gc_time: '0.00', + }, + host: { hostname: 'web-01' }, + }, + }, + {}, + ); + + vm = mountComponent(Vue.extend(performanceBarApp), { + store, + env: 'development', + requestId: '123', + peekUrl: '/-/peek/results', + profileUrl: '?lineprofiler=true', + }); + }); + + afterEach(() => { + vm.$destroy(); + mock.restore(); + }); + + it('sets the class to match the environment', () => { + expect(vm.$el.getAttribute('class')).toContain('development'); + }); + + describe('loadRequestDetails', () => { + beforeEach(() => { + spyOn(vm.store, 'addRequest').and.callThrough(); + }); + + it('does nothing if the request cannot be tracked', () => { + spyOn(vm.store, 'canTrackRequest').and.callFake(() => false); + + vm.loadRequestDetails('123', 'https://gitlab.com/'); + + expect(vm.store.addRequest).not.toHaveBeenCalled(); + }); + + it('adds the request immediately', () => { + vm.loadRequestDetails('123', 'https://gitlab.com/'); + + expect(vm.store.addRequest).toHaveBeenCalledWith( + '123', + 'https://gitlab.com/', + ); + }); + + it('makes an HTTP request for the request details', () => { + spyOn(PerformanceBarService, 'fetchRequestDetails').and.callThrough(); + + vm.loadRequestDetails('456', 'https://gitlab.com/'); + + expect(PerformanceBarService.fetchRequestDetails).toHaveBeenCalledWith( + '/-/peek/results', + '456', + ); + }); + }); +}); diff --git a/spec/javascripts/performance_bar/components/request_selector_spec.js b/spec/javascripts/performance_bar/components/request_selector_spec.js new file mode 100644 index 00000000000..6108a29f8c4 --- /dev/null +++ b/spec/javascripts/performance_bar/components/request_selector_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import requestSelector from '~/performance_bar/components/request_selector.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('request selector', () => { + const requests = [ + { id: '123', url: 'https://gitlab.com/' }, + { + id: '456', + url: 'https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1', + }, + { + id: '789', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1.json?serializer=widget', + }, + ]; + + let vm; + + beforeEach(() => { + vm = mountComponent(Vue.extend(requestSelector), { + requests, + currentRequest: requests[1], + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + function optionText(requestId) { + return vm.$el.querySelector(`[value='${requestId}']`).innerText.trim(); + } + + it('displays the last component of the path', () => { + expect(optionText(requests[2].id)).toEqual('1.json?serializer=widget'); + }); + + it('keeps the last two components of the path when the last component is numeric', () => { + expect(optionText(requests[1].id)).toEqual('merge_requests/1'); + }); + + it('ignores trailing slashes', () => { + expect(optionText(requests[0].id)).toEqual('gitlab.com'); + }); +}); diff --git a/spec/javascripts/performance_bar/components/simple_metric_spec.js b/spec/javascripts/performance_bar/components/simple_metric_spec.js new file mode 100644 index 00000000000..98b843e9711 --- /dev/null +++ b/spec/javascripts/performance_bar/components/simple_metric_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import simpleMetric from '~/performance_bar/components/simple_metric.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('simpleMetric', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('when the current request has no details', () => { + beforeEach(() => { + vm = mountComponent(Vue.extend(simpleMetric), { + currentRequest: {}, + metric: 'gitaly', + }); + }); + + it('does not display details', () => { + expect(vm.$el.innerText).not.toContain('/'); + }); + + it('displays the metric name', () => { + expect(vm.$el.innerText).toContain('gitaly'); + }); + }); + + describe('when the current request has details', () => { + beforeEach(() => { + vm = mountComponent(Vue.extend(simpleMetric), { + currentRequest: { + details: { gitaly: { duration: '123ms', calls: '456' } }, + }, + metric: 'gitaly', + }); + }); + + it('diplays details', () => { + expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456'); + }); + + it('displays the metric name', () => { + expect(vm.$el.innerText).toContain('gitaly'); + }); + }); +}); diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js deleted file mode 100644 index 7c6d226fa6a..00000000000 --- a/vendor/assets/javascripts/peek.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * this is a modified version of https://github.com/peek/peek/blob/master/app/assets/javascripts/peek.js - * - * - Removed the dependency on jquery.tipsy - * - Removed the initializeTipsy and toggleBar functions - * - Customized updatePerformanceBar to handle SQL query and Gitaly call lists - * - Changed /peek/results to /-/peek/results - * - Removed the keypress, pjax:end, page:change, and turbolinks:load handlers - */ -(function($) { - var fetchRequestResults, getRequestId, peekEnabled, updatePerformanceBar, createTable, createTableRow; - getRequestId = function() { - return $('#peek').data('requestId'); - }; - peekEnabled = function() { - return $('#peek').length; - }; - updatePerformanceBar = function(results) { - Object.keys(results.data).forEach(function(key) { - Object.keys(results.data[key]).forEach(function(label) { - var data = results.data[key][label]; - var table = createTable(key, label, data); - var target = $('[data-defer-to="' + key + '-' + label + '"]'); - - if (table) { - target.html(table); - } else { - target.text(data); - } - }); - }); - return $(document).trigger('peek:render', [getRequestId(), results]); - }; - createTable = function(key, label, data) { - if (label !== 'queries' && label !== 'details') { - return; - } - - var table = document.createElement('table'); - - for (var i = 0; i < data.length; i += 1) { - table.appendChild(createTableRow(data[i])); - } - - table.className = 'table'; - - return table; - }; - createTableRow = function(row) { - var tr = document.createElement('tr'); - var durationTd = document.createElement('td'); - var strong = document.createElement('strong'); - - strong.append(row['duration'] + 'ms'); - durationTd.appendChild(strong); - tr.appendChild(durationTd); - - ['sql', 'feature', 'enabled', 'request'].forEach(function(key) { - if (!row[key]) { return; } - - var td = document.createElement('td'); - - td.appendChild(document.createTextNode(row[key])); - tr.appendChild(td); - }); - - return tr; - }; - fetchRequestResults = function() { - return $.ajax('/-/peek/results', { - data: { - request_id: getRequestId() - }, - success: function(data, textStatus, xhr) { - return updatePerformanceBar(data); - }, - error: function(xhr, textStatus, error) {} - }); - }; - $(document).on('peek:update', fetchRequestResults); - return $(function() { - if (peekEnabled()) { - return $(this).trigger('peek:update'); - } - }); -})(jQuery);