Commit 956bd6a4 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ajax-requests-in-performance-bar' into 'master'

Show Ajax requests in performance bar

Closes #43925

See merge request gitlab-org/gitlab-ce!17742
parents 09ae0071 a200619d
......@@ -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
......
......@@ -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)
......
......@@ -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'));
}
}
......
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 "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 {
......
- 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