Commit a55b7853 authored by David O'Regan's avatar David O'Regan

Merge branch 'qmnguyen0711/improve-performance-bar' into 'master'

Improve the performance bar

See merge request gitlab-org/gitlab!56830
parents bd1042a3 89d438d4
<script> <script>
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui';
import { s__ } from '~/locale';
import { sortOrders, sortOrderOptions } from '../constants';
import RequestWarning from './request_warning.vue'; import RequestWarning from './request_warning.vue';
export default { export default {
...@@ -7,6 +10,7 @@ export default { ...@@ -7,6 +10,7 @@ export default {
RequestWarning, RequestWarning,
GlButton, GlButton,
GlModal, GlModal,
GlSegmentedControl,
}, },
directives: { directives: {
'gl-modal': GlModalDirective, 'gl-modal': GlModalDirective,
...@@ -39,6 +43,7 @@ export default { ...@@ -39,6 +43,7 @@ export default {
data() { data() {
return { return {
openedBacktraces: [], openedBacktraces: [],
sortOrder: sortOrders.DURATION,
}; };
}, },
computed: { computed: {
...@@ -48,13 +53,37 @@ export default { ...@@ -48,13 +53,37 @@ export default {
metricDetails() { metricDetails() {
return this.currentRequest.details[this.metric]; return this.currentRequest.details[this.metric];
}, },
metricDetailsSummary() {
return {
[s__('Total')]: this.metricDetails.calls,
[s__('PerformanceBar|Total duration')]: this.metricDetails.duration,
...(this.metricDetails.summary || {}),
};
},
metricDetailsLabel() { metricDetailsLabel() {
return this.metricDetails.duration if (this.metricDetails.duration && this.metricDetails.calls) {
? `${this.metricDetails.duration} / ${this.metricDetails.calls}` return `${this.metricDetails.duration} / ${this.metricDetails.calls}`;
: this.metricDetails.calls; } else if (this.metricDetails.calls) {
return this.metricDetails.calls;
}
return '0';
},
displaySortOrder() {
return (
this.metricDetails.details.length !== 0 &&
this.metricDetails.details.every((item) => item.start)
);
}, },
detailsList() { detailsList() {
return this.metricDetails.details; return this.metricDetails.details.map((item, index) => ({ ...item, id: index }));
},
sortedList() {
if (this.sortOrder === sortOrders.CHRONOLOGICAL) {
return this.detailsList.slice().sort(this.sortDetailChronologically);
}
return this.detailsList.slice().sort(this.sortDetailByDuration);
}, },
warnings() { warnings() {
return this.metricDetails.warnings || []; return this.metricDetails.warnings || [];
...@@ -82,7 +111,17 @@ export default { ...@@ -82,7 +111,17 @@ export default {
itemHasOpenedBacktrace(toggledIndex) { itemHasOpenedBacktrace(toggledIndex) {
return this.openedBacktraces.find((openedIndex) => openedIndex === toggledIndex) >= 0; return this.openedBacktraces.find((openedIndex) => openedIndex === toggledIndex) >= 0;
}, },
changeSortOrder(order) {
this.sortOrder = order;
},
sortDetailByDuration(a, b) {
return a.duration < b.duration ? 1 : -1;
},
sortDetailChronologically(a, b) {
return a.start < b.start ? -1 : 1;
}, },
},
sortOrderOptions,
}; };
</script> </script>
<template> <template>
...@@ -93,18 +132,41 @@ export default { ...@@ -93,18 +132,41 @@ export default {
data-qa-selector="detailed_metric_content" data-qa-selector="detailed_metric_content"
> >
<gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link"> <gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link">
<span class="gl-text-blue-300 gl-font-weight-bold">{{ metricDetailsLabel }}</span> <span
class="gl-text-blue-300 gl-font-weight-bold"
data-testid="performance-bar-details-label"
>
{{ metricDetailsLabel }}
</span>
</gl-button> </gl-button>
<gl-modal :modal-id="modalId" :title="header" size="lg" footer-class="d-none" scrollable> <gl-modal :modal-id="modalId" :title="header" size="lg" footer-class="d-none" scrollable>
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
<div class="gl-display-flex gl-align-items-center" data-testid="performance-bar-summary">
<div v-for="(value, name) in metricDetailsSummary" :key="name" class="gl-pr-8">
<div v-if="value" data-testid="performance-bar-summary-item">
<div>{{ name }}</div>
<div class="gl-font-size-h1 gl-font-weight-bold">{{ value }}</div>
</div>
</div>
</div>
<gl-segmented-control
v-if="displaySortOrder"
data-testid="performance-bar-sort-order"
:options="$options.sortOrderOptions"
:checked="sortOrder"
@input="changeSortOrder"
/>
</div>
<hr />
<table class="table gl-table"> <table class="table gl-table">
<template v-if="detailsList.length"> <template v-if="sortedList.length">
<tr v-for="(item, index) in detailsList" :key="index"> <tr v-for="item in sortedList" :key="item.id">
<td> <td data-testid="performance-item-duration">
<span v-if="item.duration">{{ <span v-if="item.duration">{{
sprintf(__('%{duration}ms'), { duration: item.duration }) sprintf(__('%{duration}ms'), { duration: item.duration })
}}</span> }}</span>
</td> </td>
<td> <td data-testid="performance-item-content">
<div> <div>
<div <div
v-for="(key, keyIndex) in keys" v-for="(key, keyIndex) in keys"
...@@ -121,12 +183,12 @@ export default { ...@@ -121,12 +183,12 @@ export default {
variant="default" variant="default"
icon="ellipsis_h" icon="ellipsis_h"
size="small" size="small"
:selected="itemHasOpenedBacktrace(index)" :selected="itemHasOpenedBacktrace(item.id)"
:aria-label="__('Toggle backtrace')" :aria-label="__('Toggle backtrace')"
@click="toggleBacktrace(index)" @click="toggleBacktrace(item.id)"
/> />
</div> </div>
<pre v-if="itemHasOpenedBacktrace(index)" class="backtrace-row mt-2">{{ <pre v-if="itemHasOpenedBacktrace(item.id)" class="backtrace-row gl-mt-3">{{
item.backtrace item.backtrace
}}</pre> }}</pre>
</div> </div>
...@@ -135,7 +197,7 @@ export default { ...@@ -135,7 +197,7 @@ export default {
</template> </template>
<template v-else> <template v-else>
<tr> <tr>
<td> <td data-testid="performance-bar-empty-detail-notice">
{{ sprintf(__('No %{header} for this request.'), { header: header.toLowerCase() }) }} {{ sprintf(__('No %{header} for this request.'), { header: header.toLowerCase() }) }}
</td> </td>
</tr> </tr>
......
...@@ -40,7 +40,7 @@ export default { ...@@ -40,7 +40,7 @@ export default {
metric: 'active-record', metric: 'active-record',
title: 'pg', title: 'pg',
header: s__('PerformanceBar|SQL queries'), header: s__('PerformanceBar|SQL queries'),
keys: ['sql', 'cached', 'db_role'], keys: ['sql', 'cached', 'transaction', 'db_role'],
}, },
{ {
metric: 'bullet', metric: 'bullet',
...@@ -69,6 +69,7 @@ export default { ...@@ -69,6 +69,7 @@ export default {
}, },
{ {
metric: 'external-http', metric: 'external-http',
title: 'external',
header: s__('PerformanceBar|External Http calls'), header: s__('PerformanceBar|External Http calls'),
keys: ['label', 'code', 'proxy', 'error'], keys: ['label', 'code', 'proxy', 'error'],
}, },
...@@ -157,15 +158,17 @@ export default { ...@@ -157,15 +158,17 @@ export default {
class="view" class="view"
> >
<a class="gl-text-blue-300" :href="currentRequest.details.tracing.tracing_url">{{ <a class="gl-text-blue-300" :href="currentRequest.details.tracing.tracing_url">{{
s__('PerformanceBar|trace') s__('PerformanceBar|Trace')
}}</a> }}</a>
</div> </div>
<add-request v-on="$listeners" />
<div v-if="currentRequest.details" id="peek-download" class="view"> <div v-if="currentRequest.details" id="peek-download" class="view">
<a class="gl-text-blue-300" :download="downloadName" :href="downloadPath">{{ <a class="gl-text-blue-300" :download="downloadName" :href="downloadPath">{{
s__('PerformanceBar|Download') s__('PerformanceBar|Download')
}}</a> }}</a>
</div> </div>
<a v-if="statsUrl" class="gl-text-blue-300 view" :href="statsUrl">{{
s__('PerformanceBar|Stats')
}}</a>
<request-selector <request-selector
v-if="currentRequest" v-if="currentRequest"
:current-request="currentRequest" :current-request="currentRequest"
...@@ -173,9 +176,7 @@ export default { ...@@ -173,9 +176,7 @@ export default {
class="ml-auto" class="ml-auto"
@change-current-request="changeCurrentRequest" @change-current-request="changeCurrentRequest"
/> />
<div v-if="statsUrl" id="peek-stats" class="view"> <add-request v-on="$listeners" />
<a class="gl-text-blue-300" :href="statsUrl">{{ s__('PerformanceBar|Stats') }}</a>
</div>
</div> </div>
</div> </div>
</template> </template>
import { s__ } from '~/locale';
export const sortOrders = {
DURATION: 'duration',
CHRONOLOGICAL: 'chronological',
};
export const sortOrderOptions = [
{
value: sortOrders.DURATION,
text: s__('PerformanceBar|Sort by duration'),
},
{
value: sortOrders.CHRONOLOGICAL,
text: s__('PerformanceBar|Sort chronologically'),
},
];
---
title: Allow details to be sorted chronologically in the performance bar
merge_request: 56830
author:
type: changed
...@@ -11,11 +11,21 @@ module EE ...@@ -11,11 +11,21 @@ module EE
detail = super detail = super
if ::Gitlab::Database::LoadBalancing.enable? if ::Gitlab::Database::LoadBalancing.enable?
detail[:db_role] = ::Gitlab::Database::LoadBalancing.db_role_for_connection(data[:connection]) detail[:db_role] = ::Gitlab::Database::LoadBalancing.db_role_for_connection(data[:connection]).to_s.capitalize
end end
detail detail
end end
override :count_summary
def count_summary(item, count)
super
if ::Gitlab::Database::LoadBalancing.enable?
count[item[:db_role]] ||= 0
count[item[:db_role]] += 1
end
end
end end
end end
end end
......
...@@ -6,14 +6,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ...@@ -6,14 +6,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } } subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } }
let(:connection_replica) { double(:connection_replica) } let(:connection_replica) { double(:connection_replica) }
let(:connection_primary) { double(:connection_primary) } let(:connection_primary_1) { double(:connection_primary) }
let(:connection_primary_2) { double(:connection_primary) }
let(:event_1) do let(:event_1) do
{ {
name: 'SQL', name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10', sql: 'SELECT * FROM users WHERE id = 10',
cached: false, cached: false,
connection: connection_primary connection: connection_primary_1
} }
end end
...@@ -31,12 +32,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ...@@ -31,12 +32,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL', name: 'SQL',
sql: 'UPDATE users SET admin = true WHERE id = 10', sql: 'UPDATE users SET admin = true WHERE id = 10',
cached: false, cached: false,
connection: connection_primary connection: connection_primary_2
} }
end end
before do before do
allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true) allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
allow(connection_replica).to receive(:transaction_open?).and_return(false)
allow(connection_primary_1).to receive(:transaction_open?).and_return(false)
allow(connection_primary_2).to receive(:transaction_open?).and_return(true)
end end
context 'when database load balancing is not enabled' do context 'when database load balancing is not enabled' do
...@@ -48,22 +52,32 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ...@@ -48,22 +52,32 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
end end
expect(subject.results).to match( expect(subject.results).to match(
calls: '3 (1 cached)', calls: 3,
summary: {
"Cached" => 1,
"In a transaction" => 1
},
duration: '6000.00ms', duration: '6000.00ms',
warnings: ["active-record duration: 6000.0 over 3000"], warnings: ["active-record duration: 6000.0 over 3000"],
details: contain_exactly( details: contain_exactly(
a_hash_including( a_hash_including(
start: be_a(Time),
cached: '', cached: '',
transaction: '',
duration: 1000.0, duration: 1000.0,
sql: 'SELECT * FROM users WHERE id = 10' sql: 'SELECT * FROM users WHERE id = 10'
), ),
a_hash_including( a_hash_including(
cached: 'cached', start: be_a(Time),
cached: 'Cached',
transaction: '',
duration: 2000.0, duration: 2000.0,
sql: 'SELECT * FROM users WHERE id = 10' sql: 'SELECT * FROM users WHERE id = 10'
), ),
a_hash_including( a_hash_including(
start: be_a(Time),
cached: '', cached: '',
transaction: 'In a transaction',
duration: 3000.0, duration: 3000.0,
sql: 'UPDATE users SET admin = true WHERE id = 10' sql: 'UPDATE users SET admin = true WHERE id = 10'
) )
...@@ -76,7 +90,8 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ...@@ -76,7 +90,8 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
before do before do
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true) allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_replica).and_return(:replica) allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_replica).and_return(:replica)
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_primary).and_return(:primary) allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_primary_1).and_return(:primary)
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_primary_2).and_return(:primary)
end end
it 'includes db role data' do it 'includes db role data' do
...@@ -87,27 +102,39 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ...@@ -87,27 +102,39 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
end end
expect(subject.results).to match( expect(subject.results).to match(
calls: '3 (1 cached)', calls: 3,
summary: {
"Cached" => 1,
"In a transaction" => 1,
"Primary" => 2,
"Replica" => 1
},
duration: '6000.00ms', duration: '6000.00ms',
warnings: ["active-record duration: 6000.0 over 3000"], warnings: ["active-record duration: 6000.0 over 3000"],
details: contain_exactly( details: contain_exactly(
a_hash_including( a_hash_including(
start: be_a(Time),
cached: '', cached: '',
transaction: '',
duration: 1000.0, duration: 1000.0,
sql: 'SELECT * FROM users WHERE id = 10', sql: 'SELECT * FROM users WHERE id = 10',
db_role: :primary db_role: 'Primary'
), ),
a_hash_including( a_hash_including(
cached: 'cached', start: be_a(Time),
cached: 'Cached',
transaction: '',
duration: 2000.0, duration: 2000.0,
sql: 'SELECT * FROM users WHERE id = 10', sql: 'SELECT * FROM users WHERE id = 10',
db_role: :replica db_role: 'Replica'
), ),
a_hash_including( a_hash_including(
start: be_a(Time),
cached: '', cached: '',
transaction: 'In a transaction',
duration: 3000.0, duration: 3000.0,
sql: 'UPDATE users SET admin = true WHERE id = 10', sql: 'UPDATE users SET admin = true WHERE id = 10',
db_role: :primary db_role: 'Primary'
) )
) )
) )
......
...@@ -17,22 +17,32 @@ module Peek ...@@ -17,22 +17,32 @@ module Peek
} }
}.freeze }.freeze
def results
super.merge(calls: detailed_calls)
end
def self.thresholds def self.thresholds
@thresholds ||= THRESHOLDS.fetch(Rails.env.to_sym, DEFAULT_THRESHOLDS) @thresholds ||= THRESHOLDS.fetch(Rails.env.to_sym, DEFAULT_THRESHOLDS)
end end
def results
super.merge(summary: summary)
end
private private
def detailed_calls def summary
"#{calls} (#{cached_calls} cached)" detail_store.each_with_object({}) do |item, count|
count_summary(item, count)
end
end end
def cached_calls def count_summary(item, count)
detail_store.count { |item| item[:cached] == 'cached' } if item[:cached].present?
count[item[:cached]] ||= 0
count[item[:cached]] += 1
end
if item[:transaction].present?
count[item[:transaction]] ||= 0
count[item[:transaction]] += 1
end
end end
def setup_subscribers def setup_subscribers
...@@ -45,10 +55,12 @@ module Peek ...@@ -45,10 +55,12 @@ module Peek
def generate_detail(start, finish, data) def generate_detail(start, finish, data)
{ {
start: start,
duration: finish - start, duration: finish - start,
sql: data[:sql].strip, sql: data[:sql].strip,
backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller), backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller),
cached: data[:cached] ? 'cached' : '' cached: data[:cached] ? 'Cached' : '',
transaction: data[:connection].transaction_open? ? 'In a transaction' : ''
} }
end end
end end
......
...@@ -22327,10 +22327,19 @@ msgstr "" ...@@ -22327,10 +22327,19 @@ msgstr ""
msgid "PerformanceBar|SQL queries" msgid "PerformanceBar|SQL queries"
msgstr "" msgstr ""
msgid "PerformanceBar|Sort by duration"
msgstr ""
msgid "PerformanceBar|Sort chronologically"
msgstr ""
msgid "PerformanceBar|Stats" msgid "PerformanceBar|Stats"
msgstr "" msgstr ""
msgid "PerformanceBar|trace" msgid "PerformanceBar|Total duration"
msgstr ""
msgid "PerformanceBar|Trace"
msgstr "" msgstr ""
msgid "Permissions" msgid "Permissions"
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DetailedMetric from '~/performance_bar/components/detailed_metric.vue'; import DetailedMetric from '~/performance_bar/components/detailed_metric.vue';
import RequestWarning from '~/performance_bar/components/request_warning.vue'; import RequestWarning from '~/performance_bar/components/request_warning.vue';
import { sortOrders } from '~/performance_bar/constants';
describe('detailedMetric', () => { describe('detailedMetric', () => {
let wrapper; let wrapper;
const createComponent = (props) => { const defaultProps = {
wrapper = shallowMount(DetailedMetric, { currentRequest: {},
propsData: { metric: 'gitaly',
...props, header: 'Gitaly calls',
}, keys: ['feature', 'request'],
}); };
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
shallowMount(DetailedMetric, {
propsData: { ...defaultProps, ...props },
}),
);
}; };
const findAllTraceBlocks = () => wrapper.findAll('pre'); const findAllTraceBlocks = () => wrapper.findAll('pre');
const findTraceBlockAtIndex = (index) => findAllTraceBlocks().at(index); const findTraceBlockAtIndex = (index) => findAllTraceBlocks().at(index);
const findExpandBacktraceBtns = () => wrapper.findAll('[data-testid="backtrace-expand-btn"]'); const findExpandBacktraceBtns = () => wrapper.findAllByTestId('backtrace-expand-btn');
const findExpandedBacktraceBtnAtIndex = (index) => findExpandBacktraceBtns().at(index); const findExpandedBacktraceBtnAtIndex = (index) => findExpandBacktraceBtns().at(index);
const findDetailsLabel = () => wrapper.findByTestId('performance-bar-details-label');
const findSortOrderSwitcher = () => wrapper.findByTestId('performance-bar-sort-order');
const findEmptyDetailNotice = () => wrapper.findByTestId('performance-bar-empty-detail-notice');
const findAllDetailDurations = () =>
wrapper.findAllByTestId('performance-item-duration').wrappers.map((w) => w.text());
const findAllSummaryItems = () =>
wrapper.findAllByTestId('performance-bar-summary-item').wrappers.map((w) => w.text());
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -26,13 +42,7 @@ describe('detailedMetric', () => { ...@@ -26,13 +42,7 @@ describe('detailedMetric', () => {
describe('when the current request has no details', () => { describe('when the current request has no details', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent();
currentRequest: {},
metric: 'gitaly',
header: 'Gitaly calls',
details: 'details',
keys: ['feature', 'request'],
});
}); });
it('does not render the element', () => { it('does not render the element', () => {
...@@ -42,36 +52,104 @@ describe('detailedMetric', () => { ...@@ -42,36 +52,104 @@ describe('detailedMetric', () => {
describe('when the current request has details', () => { describe('when the current request has details', () => {
const requestDetails = [ const requestDetails = [
{ duration: '100', feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
{ {
duration: '23', duration: 23,
feature: 'rebase_in_progress', feature: 'rebase_in_progress',
request: '', request: '',
backtrace: ['other', 'example'], backtrace: ['other', 'example'],
}, },
{ duration: 100, feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
]; ];
describe('with a default metric name', () => { describe('with an empty detail', () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '0ms',
calls: 0,
details: [],
warnings: [],
},
},
},
});
});
it('displays an empty title', () => {
expect(findDetailsLabel().text()).toBe('0');
});
it('displays an empty modal', () => {
expect(findEmptyDetailNotice().text()).toContain('No gitaly calls for this request');
});
it('does not display sort by switcher', () => {
expect(findSortOrderSwitcher().exists()).toBe(false);
});
});
describe('when the details have a summary field', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
currentRequest: { currentRequest: {
details: { details: {
gitaly: { gitaly: {
duration: '123ms', duration: '123ms',
calls: '456', calls: 456,
details: requestDetails,
warnings: ['gitaly calls: 456 over 30'],
summary: {
'In controllers': 100,
'In middlewares': 20,
},
},
},
},
});
});
it('displays a summary section', () => {
expect(findAllSummaryItems()).toEqual([
'Total 456',
'Total duration 123ms',
'In controllers 100',
'In middlewares 20',
]);
});
});
describe("when the details don't have a start field", () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: 456,
details: requestDetails, details: requestDetails,
warnings: ['gitaly calls: 456 over 30'], warnings: ['gitaly calls: 456 over 30'],
}, },
}, },
}, },
metric: 'gitaly',
header: 'Gitaly calls',
keys: ['feature', 'request'],
}); });
}); });
it('displays details', () => { it('displays details header', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain('123ms / 456'); expect(findDetailsLabel().text()).toBe('123ms / 456');
});
it('displays a basic summary section', () => {
expect(findAllSummaryItems()).toEqual(['Total 456', 'Total duration 123ms']);
});
it('sorts the details by descending duration order', () => {
expect(findAllDetailDurations()).toEqual(['100ms', '23ms']);
});
it('does not display sort by switcher', () => {
expect(findSortOrderSwitcher().exists()).toBe(false);
}); });
it('adds a modal with a table of the details', () => { it('adds a modal with a table of the details', () => {
...@@ -119,17 +197,75 @@ describe('detailedMetric', () => { ...@@ -119,17 +197,75 @@ describe('detailedMetric', () => {
findExpandedBacktraceBtnAtIndex(0).vm.$emit('click'); findExpandedBacktraceBtnAtIndex(0).vm.$emit('click');
await nextTick(); await nextTick();
expect(findAllTraceBlocks()).toHaveLength(1); expect(findAllTraceBlocks()).toHaveLength(1);
expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[0].backtrace[0]); expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[1].backtrace[0]);
secondExpandButton.vm.$emit('click'); secondExpandButton.vm.$emit('click');
await nextTick(); await nextTick();
expect(findAllTraceBlocks()).toHaveLength(2); expect(findAllTraceBlocks()).toHaveLength(2);
expect(findTraceBlockAtIndex(1).text()).toContain(requestDetails[1].backtrace[0]); expect(findTraceBlockAtIndex(1).text()).toContain(requestDetails[0].backtrace[0]);
secondExpandButton.vm.$emit('click'); secondExpandButton.vm.$emit('click');
await nextTick(); await nextTick();
expect(findAllTraceBlocks()).toHaveLength(1); expect(findAllTraceBlocks()).toHaveLength(1);
expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[0].backtrace[0]); expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[1].backtrace[0]);
});
});
describe('when the details have a start field', () => {
const requestDetailsWithStart = [
{
start: '2021-03-18 11:41:49.846356 +0700',
duration: 23,
feature: 'rebase_in_progress',
request: '',
},
{
start: '2021-03-18 11:42:11.645711 +0700',
duration: 75,
feature: 'find_commit',
request: 'abcdef',
},
{
start: '2021-03-18 11:42:10.645711 +0700',
duration: 100,
feature: 'find_commit',
request: 'abcdef',
},
];
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: 456,
details: requestDetailsWithStart,
warnings: ['gitaly calls: 456 over 30'],
},
},
},
metric: 'gitaly',
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
});
it('sorts the details by descending duration order', () => {
expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']);
});
it('displays sort by switcher', () => {
expect(findSortOrderSwitcher().exists()).toBe(true);
});
it('allows switch sorting orders', async () => {
findSortOrderSwitcher().vm.$emit('input', sortOrders.CHRONOLOGICAL);
await nextTick();
expect(findAllDetailDurations()).toEqual(['23ms', '100ms', '75ms']);
findSortOrderSwitcher().vm.$emit('input', sortOrders.DURATION);
await nextTick();
expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']);
}); });
}); });
...@@ -145,10 +281,7 @@ describe('detailedMetric', () => { ...@@ -145,10 +281,7 @@ describe('detailedMetric', () => {
}, },
}, },
}, },
metric: 'gitaly',
title: 'custom', title: 'custom',
header: 'Gitaly calls',
keys: ['feature', 'request'],
}); });
}); });
...@@ -156,7 +289,6 @@ describe('detailedMetric', () => { ...@@ -156,7 +289,6 @@ describe('detailedMetric', () => {
expect(wrapper.text()).toContain('custom'); expect(wrapper.text()).toContain('custom');
}); });
}); });
});
describe('when the details has no duration', () => { describe('when the details has no duration', () => {
beforeEach(() => { beforeEach(() => {
...@@ -175,12 +307,21 @@ describe('detailedMetric', () => { ...@@ -175,12 +307,21 @@ describe('detailedMetric', () => {
}); });
}); });
it('displays calls in the label', () => {
expect(findDetailsLabel().text()).toBe('456');
});
it('displays a basic summary section', () => {
expect(findAllSummaryItems()).toEqual(['Total 456']);
});
it('renders only the number of calls', async () => { it('renders only the number of calls', async () => {
expect(trimText(wrapper.text())).toEqual('456 notification bullet'); expect(trimText(wrapper.text())).toContain('notification bullet');
findExpandedBacktraceBtnAtIndex(0).vm.$emit('click'); findExpandedBacktraceBtnAtIndex(0).vm.$emit('click');
await nextTick(); await nextTick();
expect(trimText(wrapper.text())).toEqual('456 notification backtrace bullet'); expect(trimText(wrapper.text())).toContain('notification backtrace bullet');
});
}); });
}); });
}); });
...@@ -5,14 +5,16 @@ require 'spec_helper' ...@@ -5,14 +5,16 @@ require 'spec_helper'
RSpec.describe Peek::Views::ActiveRecord, :request_store do RSpec.describe Peek::Views::ActiveRecord, :request_store do
subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } } subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } }
let(:connection) { double(:connection) } let(:connection_1) { double(:connection) }
let(:connection_2) { double(:connection) }
let(:connection_3) { double(:connection) }
let(:event_1) do let(:event_1) do
{ {
name: 'SQL', name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10', sql: 'SELECT * FROM users WHERE id = 10',
cached: false, cached: false,
connection: connection connection: connection_1
} }
end end
...@@ -21,7 +23,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ...@@ -21,7 +23,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL', name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10', sql: 'SELECT * FROM users WHERE id = 10',
cached: true, cached: true,
connection: connection connection: connection_2
} }
end end
...@@ -30,12 +32,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ...@@ -30,12 +32,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL', name: 'SQL',
sql: 'UPDATE users SET admin = true WHERE id = 10', sql: 'UPDATE users SET admin = true WHERE id = 10',
cached: false, cached: false,
connection: connection connection: connection_3
} }
end end
before do before do
allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true) allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
allow(connection_1).to receive(:transaction_open?).and_return(false)
allow(connection_2).to receive(:transaction_open?).and_return(false)
allow(connection_3).to receive(:transaction_open?).and_return(true)
end end
it 'subscribes and store data into peek views' do it 'subscribes and store data into peek views' do
...@@ -46,22 +51,32 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ...@@ -46,22 +51,32 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
end end
expect(subject.results).to match( expect(subject.results).to match(
calls: '3 (1 cached)', calls: 3,
summary: {
"Cached" => 1,
"In a transaction" => 1
},
duration: '6000.00ms', duration: '6000.00ms',
warnings: ["active-record duration: 6000.0 over 3000"], warnings: ["active-record duration: 6000.0 over 3000"],
details: contain_exactly( details: contain_exactly(
a_hash_including( a_hash_including(
start: be_a(Time),
cached: '', cached: '',
transaction: '',
duration: 1000.0, duration: 1000.0,
sql: 'SELECT * FROM users WHERE id = 10' sql: 'SELECT * FROM users WHERE id = 10'
), ),
a_hash_including( a_hash_including(
cached: 'cached', start: be_a(Time),
cached: 'Cached',
transaction: '',
duration: 2000.0, duration: 2000.0,
sql: 'SELECT * FROM users WHERE id = 10' sql: 'SELECT * FROM users WHERE id = 10'
), ),
a_hash_including( a_hash_including(
start: be_a(Time),
cached: '', cached: '',
transaction: 'In a transaction',
duration: 3000.0, duration: 3000.0,
sql: 'UPDATE users SET admin = true WHERE id = 10' sql: 'UPDATE users SET admin = true WHERE id = 10'
) )
......
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