Commit a200619d authored by Sean McGivern's avatar Sean McGivern

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.
parent cd4ddee0
...@@ -276,7 +276,6 @@ gem 'batch-loader', '~> 1.2.1' ...@@ -276,7 +276,6 @@ gem 'batch-loader', '~> 1.2.1'
# Perf bar # Perf bar
gem 'peek', '~> 1.0.1' gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2' gem 'peek-gc', '~> 0.0.2'
gem 'peek-host', '~> 1.0.0'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.3.0' gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres gem 'peek-pg', '~> 1.3.0', group: :postgres
......
...@@ -593,8 +593,6 @@ GEM ...@@ -593,8 +593,6 @@ GEM
railties (>= 4.0.0) railties (>= 4.0.0)
peek-gc (0.0.2) peek-gc (0.0.2)
peek peek
peek-host (1.0.0)
peek
peek-mysql2 (1.1.0) peek-mysql2 (1.1.0)
atomic (>= 1.0.0) atomic (>= 1.0.0)
mysql2 mysql2
...@@ -1124,7 +1122,6 @@ DEPENDENCIES ...@@ -1124,7 +1122,6 @@ DEPENDENCIES
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
peek (~> 1.0.1) peek (~> 1.0.1)
peek-gc (~> 0.0.2) peek-gc (~> 0.0.2)
peek-host (~> 1.0.0)
peek-mysql2 (~> 1.1.0) peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0) peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0) peek-pg (~> 1.3.0)
......
...@@ -53,8 +53,12 @@ function initPageShortcuts(page) { ...@@ -53,8 +53,12 @@ function initPageShortcuts(page) {
function initGFMInput() { function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); const gfm = new GfmAutoComplete(
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
);
const enableGFM = convertPermissionToBoolean(
el.dataset.supportsAutocomplete,
);
gfm.setup($(el), { gfm.setup($(el), {
emojis: true, emojis: true,
members: enableGFM, members: enableGFM,
...@@ -67,9 +71,9 @@ function initGFMInput() { ...@@ -67,9 +71,9 @@ function initGFMInput() {
} }
function initPerformanceBar() { function initPerformanceBar() {
if (document.querySelector('#peek')) { if (document.querySelector('#js-peek')) {
import('./performance_bar') 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')); .catch(() => Flash('Error loading performance bar module'));
} }
} }
......
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();
}
}
<script>
import GlModal from '~/vue_shared/components/gl_modal.vue';
export default {
components: {
GlModal,
},
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
header: {
type: String,
required: true,
},
details: {
type: String,
required: true,
},
keys: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div
:id="`peek-view-${metric}`"
class="view"
>
<button
:data-target="`#modal-peek-${metric}-details`"
class="btn-blank btn-link bold"
type="button"
data-toggle="modal"
>
<span
v-if="currentRequest.details"
class="bold"
>
{{ currentRequest.details[metric].duration }}
/
{{ currentRequest.details[metric].calls }}
</span>
</button>
<gl-modal
v-if="currentRequest.details"
:id="`modal-peek-${metric}-details`"
:header-title-text="header"
class="performance-bar-modal"
>
<table class="table">
<tr
v-for="(item, index) in currentRequest.details[metric][details]"
:key="index"
>
<td><strong>{{ item.duration }}ms</strong></td>
<td
v-for="key in keys"
:key="key"
>
{{ item[key] }}
</td>
</tr>
</table>
<div slot="footer">
</div>
</gl-modal>
{{ metric }}
</div>
</template>
<script>
import $ from 'jquery';
import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash';
export default {
components: {
detailedMetric,
requestSelector,
simpleMetric,
upstreamPerformanceBar,
},
props: {
store: {
type: Object,
required: true,
},
env: {
type: String,
required: true,
},
requestId: {
type: String,
required: true,
},
peekUrl: {
type: String,
required: true,
},
profileUrl: {
type: String,
required: true,
},
},
detailedMetrics: [
{ metric: 'pg', header: 'SQL queries', details: 'queries', keys: ['sql'] },
{
metric: 'gitaly',
header: 'Gitaly calls',
details: 'details',
keys: ['feature', 'request'],
},
],
simpleMetrics: ['redis', 'sidekiq'],
data() {
return { currentRequestId: '' };
},
computed: {
requests() {
return this.store.requestsWithDetails();
},
currentRequest: {
get() {
return this.store.findRequest(this.currentRequestId);
},
set(requestId) {
this.currentRequestId = requestId;
},
},
initialRequest() {
return this.currentRequestId === this.requestId;
},
lineProfileModal() {
return $('#modal-peek-line-profile');
},
},
mounted() {
this.interceptor = PerformanceBarService.registerInterceptor(
this.peekUrl,
this.loadRequestDetails,
);
this.loadRequestDetails(this.requestId, window.location.href);
this.currentRequest = this.requestId;
if (this.lineProfileModal.length) {
this.lineProfileModal.modal('toggle');
}
},
beforeDestroy() {
PerformanceBarService.removeInterceptor(this.interceptor);
},
methods: {
loadRequestDetails(requestId, requestUrl) {
if (!this.store.canTrackRequest(requestUrl)) {
return;
}
this.store.addRequest(requestId, requestUrl);
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data.data);
})
.catch(() =>
Flash(`Error getting performance bar results for ${requestId}`),
);
},
changeCurrentRequest(newRequestId) {
this.currentRequest = newRequestId;
},
},
};
</script>
<template>
<div
id="js-peek"
:class="env"
>
<request-selector
v-if="currentRequest"
:current-request="currentRequest"
:requests="requests"
@change-current-request="changeCurrentRequest"
/>
<div
id="peek-view-host"
class="view prepend-left-5"
>
<span
v-if="currentRequest && currentRequest.details"
class="current-host"
>
{{ currentRequest.details.host.hostname }}
</span>
</div>
<div
v-if="currentRequest"
class="wrapper"
>
<upstream-performance-bar
v-if="initialRequest && currentRequest.details"
/>
<detailed-metric
v-for="metric in $options.detailedMetrics"
:key="metric.metric"
:current-request="currentRequest"
:metric="metric.metric"
:header="metric.header"
:details="metric.details"
:keys="metric.keys"
/>
<div
v-if="initialRequest"
id="peek-view-rblineprof"
class="view"
>
<button
v-if="lineProfileModal.length"
class="btn-link btn-blank"
data-toggle="modal"
data-target="#modal-peek-line-profile"
>
profile
</button>
<a
v-else
:href="profileUrl"
>
profile
</a>
</div>
<simple-metric
v-for="metric in $options.simpleMetrics"
:current-request="currentRequest"
:key="metric"
:metric="metric"
/>
<div
id="peek-view-gc"
class="view"
>
<span
v-if="currentRequest.details"
class="bold"
>
<span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span>ms
/
<span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span>
gc
</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
requests: {
type: Array,
required: true,
},
},
data() {
return {
currentRequestId: this.currentRequest.id,
};
},
watch: {
currentRequestId(newRequestId) {
this.$emit('change-current-request', newRequestId);
},
},
methods: {
truncatedUrl(requestUrl) {
const components = requestUrl.replace(/\/$/, '').split('/');
let truncated = components[components.length - 1];
if (truncated.match(/^\d+$/)) {
truncated = `${components[components.length - 2]}/${truncated}`;
}
return truncated;
},
},
};
</script>
<template>
<div
id="peek-request-selector"
class="append-right-5 pull-right"
>
<select v-model="currentRequestId">
<option
v-for="request in requests"
:key="request.id"
:value="request.id"
>
{{ truncatedUrl(request.url) }}
</option>
</select>
</div>
</template>
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
},
};
</script>
<template>
<div
:id="`peek-view-${metric}`"
class="view"
>
<span
v-if="currentRequest.details"
class="bold"
>
{{ currentRequest.details[metric].duration }}
/
{{ currentRequest.details[metric].calls }}
</span>
{{ metric }}
</div>
</template>
<script>
export default {
mounted() {
const upstreamPerformanceBar = document
.getElementById('peek-view-performance-bar')
.cloneNode(true);
this.$refs.wrapper.appendChild(upstreamPerformanceBar);
},
};
</script>
<template>
<div
id="peek-view-performance-bar-vue"
class="view"
ref="wrapper"
></div>
</template>
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,
},
});
},
});
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);
}
}
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
);
}
}
@import "framework/variables"; @import 'framework/variables';
@import "peek/views/performance_bar"; @import 'peek/views/performance_bar';
@import "peek/views/rblineprof"; @import 'peek/views/rblineprof';
#peek { #js-peek {
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
...@@ -21,14 +21,26 @@ ...@@ -21,14 +21,26 @@
&.production { &.production {
background-color: $perf-bar-production; background-color: $perf-bar-production;
select {
background: $perf-bar-production;
}
} }
&.staging { &.staging {
background-color: $perf-bar-staging; background-color: $perf-bar-staging;
select {
background: $perf-bar-staging;
}
} }
&.development { &.development {
background-color: $perf-bar-development; background-color: $perf-bar-development;
select {
background: $perf-bar-development;
}
} }
.wrapper { .wrapper {
...@@ -42,11 +54,12 @@ ...@@ -42,11 +54,12 @@
background: $perf-bar-bucket-bg; background: $perf-bar-bucket-bg;
display: inline-block; display: inline-block;
padding: 4px 6px; padding: 4px 6px;
font-family: Consolas, "Liberation Mono", Courier, monospace; font-family: Consolas, 'Liberation Mono', Courier, monospace;
line-height: 1; line-height: 1;
color: $perf-bar-bucket-color; color: $perf-bar-bucket-color;
border-radius: 3px; 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 { .hidden {
display: none; display: none;
...@@ -94,6 +107,10 @@ ...@@ -94,6 +107,10 @@
max-width: 10000px !important; max-width: 10000px !important;
} }
} }
.performance-bar-modal .modal-footer {
display: none;
}
} }
#modal-peek-pg-queries-content { #modal-peek-pg-queries-content {
......
- 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
- 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' }
&times;
%h4
Gitaly requests
.modal-body{ data: { defer_to: "#{view.defer_key}-details" } }...
gitaly
%span.current-host
= truncate(view.hostname)
- local_assigns.fetch(:view)
= render 'peek/views/sql', view: view
mysql
- local_assigns.fetch(:view)
= render 'peek/views/sql', view: view
pg
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'
%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' }
&times;
%h4
SQL queries
.modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
---
title: Allow viewing timings for AJAX requests in the performance bar
merge_request:
author:
type: changed
...@@ -44,7 +44,7 @@ Rails.application.routes.draw do ...@@ -44,7 +44,7 @@ Rails.application.routes.draw do
get 'readiness' => 'health#readiness' get 'readiness' => 'health#readiness'
post 'storage_check' => 'health#storage_check' post 'storage_check' => 'health#storage_check'
resources :metrics, only: [:index] resources :metrics, only: [:index]
mount Peek::Railtie => '/peek' mount Peek::Railtie => '/peek', as: 'peek_routes'
# Boards resources shared between group and projects # Boards resources shared between group and projects
resources :boards, only: [] do resources :boards, only: [] do
......
...@@ -13,12 +13,16 @@ It allows you to see (from left to right): ...@@ -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) ![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 - 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) ![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) ![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 calls to Redis
- time taken and number of background jobs created by Sidekiq - time taken and number of background jobs created by Sidekiq
- time taken and number of Ruby GC calls - 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 ## Enable the Performance Bar via the Admin panel
GitLab Performance Bar is disabled by default. To enable it for a given group, GitLab Performance Bar is disabled by default. To enable it for a given group,
......
module Peek
module Views
class Host < View
def results
{ hostname: Gitlab::Environment.hostname }
end
end
end
end
...@@ -3,7 +3,7 @@ require 'rails_helper' ...@@ -3,7 +3,7 @@ require 'rails_helper'
describe 'User can display performance bar', :js do describe 'User can display performance bar', :js do
shared_examples 'performance bar cannot be displayed' do shared_examples 'performance bar cannot be displayed' do
it 'does not show the performance bar by default' 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 end
context 'when user press `pb`' do context 'when user press `pb`' do
...@@ -12,14 +12,14 @@ describe 'User can display performance bar', :js do ...@@ -12,14 +12,14 @@ describe 'User can display performance bar', :js do
end end
it 'does not show the performance bar by default' 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 end
end end
end end
shared_examples 'performance bar can be displayed' do shared_examples 'performance bar can be displayed' do
it 'does not show the performance bar by default' 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 end
context 'when user press `pb`' do context 'when user press `pb`' do
...@@ -28,7 +28,7 @@ describe 'User can display performance bar', :js do ...@@ -28,7 +28,7 @@ describe 'User can display performance bar', :js do
end end
it 'shows the performance bar' do it 'shows the performance bar' do
expect(page).to have_css('#peek') expect(page).to have_css('#js-peek')
end end
end end
end end
...@@ -41,7 +41,7 @@ describe 'User can display performance bar', :js do ...@@ -41,7 +41,7 @@ describe 'User can display performance bar', :js do
it 'shows the performance bar by default' do it 'shows the performance bar by default' do
refresh # Because we're stubbing Rails.env after the 1st visit to root_path 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
end end
......
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');
});
});
});
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',
);
});
});
});
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');
});
});
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');
});
});
});
/*
* 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);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment