summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukas Eipert <leipert@gitlab.com>2018-11-06 11:20:49 +0100
committerLukas Eipert <leipert@gitlab.com>2018-11-06 11:39:11 +0100
commit26ab92d3f38eea724728d71bf033895e1e783d1d (patch)
tree7cedaf4a007e17dec699e03cb28269a62b506319
parent84f562e7040f1da818157853de84874d57135ca9 (diff)
downloadgitlab-ce-7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries.tar.gz
Instead of rendering all report items in 4 big lists, we make use of vue-virtual-scroll-list and render only few dozens at once. This improves the performance in several metrics: - Initial load time - Memory Pressure - CPU Load - DOM node count In an example with around 11k reported security vulnerabilities: - Initial load time: 27s -> 4.1s - Memory Pressure: ~750 MB -> ~270 MB - CPU Load (time spent on executing JS/Rendering): 22s -> 2.5s - DOM node count: 430k -> 7k up to 30k while scrolling
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue85
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue (renamed from app/assets/javascripts/reports/components/report_issues.vue)44
-rw-r--r--app/assets/javascripts/vue_shared/components/smart_virtual_list.vue42
-rw-r--r--changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml5
-rw-r--r--spec/javascripts/reports/components/grouped_test_reports_app_spec.js4
-rw-r--r--spec/javascripts/reports/components/report_section_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/smart_virtual_list_spec.js83
7 files changed, 197 insertions, 68 deletions
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue
index 3b425ee2fed..f4243522ef8 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/reports/components/issues_list.vue
@@ -1,18 +1,31 @@
<script>
-import IssuesBlock from '~/reports/components/report_issues.vue';
-import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants';
+import ReportItem from '~/reports/components/report_item.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+
+const wrapIssueWithState = (status, isNew = false) => issue => ({
+ status: issue.status || status,
+ isNew,
+ issue,
+});
/**
* Renders block of issues
*/
-
export default {
components: {
- IssuesBlock,
+ SmartVirtualList,
+ ReportItem,
},
- success: STATUS_SUCCESS,
- failed: STATUS_FAILED,
- neutral: STATUS_NEUTRAL,
+ // Typical height of a report item in px
+ typicalReportItemHeight: 32,
+ /*
+ The maximum amount of shown issues. This is calculated by
+ ( max-height of report-block-list / typicalReportItemHeight ) + some safety margin
+ We will use VirtualList if we have more items than this number.
+ For entries lower than this number, the virtual scroll list calculates the total height of the element wrongly.
+ */
+ maxShownReportItems: 20,
props: {
newIssues: {
type: Array,
@@ -40,42 +53,34 @@ export default {
default: '',
},
},
+ computed: {
+ issuesWithState() {
+ return [
+ ...this.newIssues.map(wrapIssueWithState(STATUS_FAILED, true)),
+ ...this.unresolvedIssues.map(wrapIssueWithState(STATUS_FAILED)),
+ ...this.neutralIssues.map(wrapIssueWithState(STATUS_NEUTRAL)),
+ ...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)),
+ ];
+ },
+ },
};
</script>
<template>
- <div class="report-block-container">
-
- <issues-block
- v-if="newIssues.length"
- :component="component"
- :issues="newIssues"
- class="js-mr-code-new-issues"
- status="failed"
- is-new
- />
-
- <issues-block
- v-if="unresolvedIssues.length"
- :component="component"
- :issues="unresolvedIssues"
- :status="$options.failed"
- class="js-mr-code-new-issues"
- />
-
- <issues-block
- v-if="neutralIssues.length"
- :component="component"
- :issues="neutralIssues"
- :status="$options.neutral"
- class="js-mr-code-non-issues"
- />
-
- <issues-block
- v-if="resolvedIssues.length"
+ <smart-virtual-list
+ :length="issuesWithState.length"
+ :remain="$options.maxShownReportItems"
+ :size="$options.typicalReportItemHeight"
+ class="report-block-container"
+ wtag="ul"
+ wclass="report-block-list"
+ >
+ <report-item
+ v-for="(wrapped, index) in issuesWithState"
+ :key="index"
+ :issue="wrapped.issue"
+ :status="wrapped.status"
:component="component"
- :issues="resolvedIssues"
- :status="$options.success"
- class="js-mr-code-resolved-issues"
+ :is-new="wrapped.isNew"
/>
- </div>
+ </smart-virtual-list>
</template>
diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_item.vue
index a2a03945ae3..01e6d357a21 100644
--- a/app/assets/javascripts/reports/components/report_issues.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -3,14 +3,14 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { components, componentNames } from '~/reports/components/issue_body';
export default {
- name: 'ReportIssues',
+ name: 'ReportItem',
components: {
IssueStatusIcon,
...components,
},
props: {
- issues: {
- type: Array,
+ issue: {
+ type: Object,
required: true,
},
component: {
@@ -33,27 +33,21 @@ export default {
};
</script>
<template>
- <div>
- <ul class="report-block-list">
- <li
- v-for="(issue, index) in issues"
- :key="index"
- :class="{ 'is-dismissed': issue.isDismissed }"
- class="report-block-list-issue"
- >
- <issue-status-icon
- :status="issue.status || status"
- class="append-right-5"
- />
+ <li
+ :class="{ 'is-dismissed': issue.isDismissed }"
+ class="report-block-list-issue"
+ >
+ <issue-status-icon
+ :status="status"
+ class="append-right-5"
+ />
- <component
- :is="component"
- v-if="component"
- :issue="issue"
- :status="issue.status || status"
- :is-new="isNew"
- />
- </li>
- </ul>
- </div>
+ <component
+ :is="component"
+ v-if="component"
+ :issue="issue"
+ :status="status"
+ :is-new="isNew"
+ />
+ </li>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue
new file mode 100644
index 00000000000..63034a45f77
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue
@@ -0,0 +1,42 @@
+<script>
+import VirtualList from 'vue-virtual-scroll-list';
+
+export default {
+ name: 'SmartVirtualList',
+ components: { VirtualList },
+ props: {
+ size: { type: Number, required: true },
+ length: { type: Number, required: true },
+ remain: { type: Number, required: true },
+ rtag: { type: String, default: 'div' },
+ wtag: { type: String, default: 'div' },
+ wclass: { type: String, default: null },
+ },
+};
+</script>
+<template>
+ <virtual-list
+ v-if="length > remain"
+ v-bind="$attrs"
+ :size="remain"
+ :remain="remain"
+ :rtag="rtag"
+ :wtag="wtag"
+ :wclass="wclass"
+ class="js-virtual-list"
+ >
+ <slot></slot>
+ </virtual-list>
+ <component
+ :is="rtag"
+ v-else
+ class="js-plain-element"
+ >
+ <component
+ :is="wtag"
+ :class="wclass"
+ >
+ <slot></slot>
+ </component>
+ </component>
+</template>
diff --git a/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml b/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml
new file mode 100644
index 00000000000..aaae8feb220
--- /dev/null
+++ b/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of rendering large reports
+merge_request: 22835
+author:
+type: performance
diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
index f58515daa4f..69767d9cf1c 100644
--- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
+++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
@@ -151,11 +151,11 @@ describe('Grouped Test Reports App', () => {
it('renders resolved failures', done => {
setTimeout(() => {
- expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain(
+ expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
resolvedFailures.suites[0].resolved_failures[0].name,
);
- expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain(
+ expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
resolvedFailures.suites[0].resolved_failures[1].name,
);
done();
diff --git a/spec/javascripts/reports/components/report_section_spec.js b/spec/javascripts/reports/components/report_section_spec.js
index eb7307605d7..b02af8baaec 100644
--- a/spec/javascripts/reports/components/report_section_spec.js
+++ b/spec/javascripts/reports/components/report_section_spec.js
@@ -120,7 +120,7 @@ describe('Report section', () => {
'Code quality improved on 1 point and degraded on 1 point',
);
- expect(vm.$el.querySelectorAll('.js-mr-code-resolved-issues li').length).toEqual(
+ expect(vm.$el.querySelectorAll('.report-block-container li').length).toEqual(
resolvedIssues.length,
);
});
diff --git a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js
new file mode 100644
index 00000000000..e723fead65e
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js
@@ -0,0 +1,83 @@
+import Vue from 'vue';
+import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('Toggle Button', () => {
+ let vm;
+
+ const createComponent = ({ length, remain }) => {
+ const smartListProperties = {
+ rtag: 'section',
+ wtag: 'ul',
+ wclass: 'test-class',
+ // Size in pixels does not matter for our tests here
+ size: 35,
+ length,
+ remain,
+ };
+
+ const Component = Vue.extend({
+ components: {
+ SmartVirtualScrollList,
+ },
+ smartListProperties,
+ items: Array(length).fill(1),
+ template: `
+ <smart-virtual-scroll-list v-bind="$options.smartListProperties">
+ <li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li>
+ </smart-virtual-scroll-list>`,
+ });
+
+ return mountComponent(Component);
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('if the list is shorter than the maximum shown elements', () => {
+ const listLength = 10;
+
+ beforeEach(() => {
+ vm = createComponent({ length: listLength, remain: 20 });
+ });
+
+ it('renders without the vue-virtual-scroll-list component', () => {
+ expect(vm.$el.classList).not.toContain('js-virtual-list');
+ expect(vm.$el.classList).toContain('js-plain-element');
+ });
+
+ it('renders list with provided tags and classes for the wrapper elements', () => {
+ expect(vm.$el.tagName).toEqual('SECTION');
+ expect(vm.$el.firstChild.tagName).toEqual('UL');
+ expect(vm.$el.firstChild.classList).toContain('test-class');
+ });
+
+ it('renders all children list elements', () => {
+ expect(vm.$el.querySelectorAll('li').length).toEqual(listLength);
+ });
+ });
+
+ describe('if the list is longer than the maximum shown elements', () => {
+ const maxItemsShown = 20;
+
+ beforeEach(() => {
+ vm = createComponent({ length: 1000, remain: maxItemsShown });
+ });
+
+ it('uses the vue-virtual-scroll-list component', () => {
+ expect(vm.$el.classList).toContain('js-virtual-list');
+ expect(vm.$el.classList).not.toContain('js-plain-element');
+ });
+
+ it('renders list with provided tags and classes for the wrapper elements', () => {
+ expect(vm.$el.tagName).toEqual('SECTION');
+ expect(vm.$el.firstChild.tagName).toEqual('UL');
+ expect(vm.$el.firstChild.classList).toContain('test-class');
+ });
+
+ it('renders at max twice the maximum shown elements', () => {
+ expect(vm.$el.querySelectorAll('li').length).toBeLessThanOrEqual(2 * maxItemsShown);
+ });
+ });
+});