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.
This commit is contained in:
parent
cd4ddee0d6
commit
a200619d14
30 changed files with 807 additions and 212 deletions
1
Gemfile
1
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
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<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>
|
|
@ -0,0 +1,191 @@
|
|||
<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>
|
|
@ -0,0 +1,52 @@
|
|||
<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>
|
|
@ -0,0 +1,30 @@
|
|||
<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>
|
|
@ -0,0 +1,18 @@
|
|||
<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>
|
37
app/assets/javascripts/performance_bar/index.js
Normal file
37
app/assets/javascripts/performance_bar/index.js
Normal file
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
12
app/views/peek/_bar.html.haml
Normal file
12
app/views/peek/_bar.html.haml
Normal file
|
@ -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
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
%span.current-host
|
||||
= truncate(view.hostname)
|
|
@ -1,4 +0,0 @@
|
|||
- local_assigns.fetch(:view)
|
||||
|
||||
= render 'peek/views/sql', view: view
|
||||
mysql
|
|
@ -1,4 +0,0 @@
|
|||
- local_assigns.fetch(:view)
|
||||
|
||||
= render 'peek/views/sql', view: view
|
||||
pg
|
|
@ -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'
|
|
@ -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" } }...
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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
|
|||
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
|
||||
|
|
|
@ -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,
|
||||
|
|
9
lib/peek/views/host.rb
Normal file
9
lib/peek/views/host.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module Peek
|
||||
module Views
|
||||
class Host < View
|
||||
def results
|
||||
{ hostname: Gitlab::Environment.hostname }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
86
vendor/assets/javascripts/peek.js
vendored
86
vendor/assets/javascripts/peek.js
vendored
|
@ -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);
|
Loading…
Reference in a new issue