Improve on-demand scans polling performances

This reduces the GraphQL polling overhead in the on-demand scans page
by...
* ...including an ETag header in the query to prevent hitting the
  database on every request.
* ...putting the query behind a visibility check to pause the polling
  when the browser tab is not visible.
* ...increasing the polling interval from 1 to 3 seconds.

Changelog: performance
EE: true
parent 802314d3
...@@ -14,6 +14,10 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; ...@@ -14,6 +14,10 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { DAST_SHORT_NAME } from '~/security_configuration/components/constants'; import { DAST_SHORT_NAME } from '~/security_configuration/components/constants';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
getQueryHeaders,
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import handlesErrors from '../../mixins/handles_errors'; import handlesErrors from '../../mixins/handles_errors';
import Actions from '../actions.vue'; import Actions from '../actions.vue';
import EmptyState from '../empty_state.vue'; import EmptyState from '../empty_state.vue';
...@@ -45,7 +49,7 @@ export default { ...@@ -45,7 +49,7 @@ export default {
EmptyState, EmptyState,
}, },
mixins: [handlesErrors], mixins: [handlesErrors],
inject: ['projectPath'], inject: ['projectPath', 'projectOnDemandScanCountsEtag'],
props: { props: {
isActive: { isActive: {
type: Boolean, type: Boolean,
...@@ -95,6 +99,9 @@ export default { ...@@ -95,6 +99,9 @@ export default {
...this.cursor, ...this.cursor,
}; };
}, },
context() {
return getQueryHeaders(this.projectOnDemandScanCountsEtag);
},
update(data) { update(data) {
const pipelines = data?.project?.pipelines; const pipelines = data?.project?.pipelines;
if (!pipelines?.nodes?.length && (this.cursor.after || this.cursor.before)) { if (!pipelines?.nodes?.length && (this.cursor.after || this.cursor.before)) {
...@@ -160,6 +167,9 @@ export default { ...@@ -160,6 +167,9 @@ export default {
} }
}, },
}, },
mounted() {
toggleQueryPollingByVisibility(this.$apollo.queries.pipelines, PIPELINES_POLL_INTERVAL);
},
methods: { methods: {
resetCursor() { resetCursor() {
this.cursor = { ...defaultCursor }; this.cursor = { ...defaultCursor };
......
...@@ -11,8 +11,8 @@ export const LEARN_MORE_TEXT = s__( ...@@ -11,8 +11,8 @@ export const LEARN_MORE_TEXT = s__(
export const PIPELINE_TABS_KEYS = ['all', 'running', 'finished', 'scheduled', 'saved']; export const PIPELINE_TABS_KEYS = ['all', 'running', 'finished', 'scheduled', 'saved'];
export const PIPELINES_PER_PAGE = 20; export const PIPELINES_PER_PAGE = 20;
export const PIPELINES_POLL_INTERVAL = 1000; export const PIPELINES_POLL_INTERVAL = 3000;
export const PIPELINES_COUNT_POLL_INTERVAL = 1000; export const PIPELINES_COUNT_POLL_INTERVAL = 3000;
// Pipeline scopes // Pipeline scopes
export const PIPELINES_SCOPE_RUNNING = 'RUNNING'; export const PIPELINES_SCOPE_RUNNING = 'RUNNING';
......
...@@ -18,6 +18,8 @@ import { scrollToElement } from '~/lib/utils/common_utils'; ...@@ -18,6 +18,8 @@ import { scrollToElement } from '~/lib/utils/common_utils';
import setWindowLocation from 'helpers/set_window_location_helper'; import setWindowLocation from 'helpers/set_window_location_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { BASE_TABS_TABLE_FIELDS, PIPELINES_POLL_INTERVAL } from 'ee/on_demand_scans/constants'; import { BASE_TABS_TABLE_FIELDS, PIPELINES_POLL_INTERVAL } from 'ee/on_demand_scans/constants';
import * as graphQlUtils from '~/pipelines/components/graph/utils';
import { PROJECT_ON_DEMAND_SCAN_COUNTS_ETAG_MOCK } from '../../mocks';
jest.mock('~/lib/utils/common_utils'); jest.mock('~/lib/utils/common_utils');
...@@ -86,6 +88,7 @@ describe('BaseTab', () => { ...@@ -86,6 +88,7 @@ describe('BaseTab', () => {
}, },
provide: { provide: {
projectPath, projectPath,
projectOnDemandScanCountsEtag: PROJECT_ON_DEMAND_SCAN_COUNTS_ETAG_MOCK,
}, },
stubs: { stubs: {
GlTab: stubComponent(GlTab, { GlTab: stubComponent(GlTab, {
...@@ -122,6 +125,16 @@ describe('BaseTab', () => { ...@@ -122,6 +125,16 @@ describe('BaseTab', () => {
}); });
describe('when the app loads', () => { describe('when the app loads', () => {
it('controls the pipelines query with a visibility check', () => {
jest.spyOn(graphQlUtils, 'toggleQueryPollingByVisibility');
createComponent();
expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
wrapper.vm.$apollo.queries.pipelines,
PIPELINES_POLL_INTERVAL,
);
});
it('fetches the pipelines', () => { it('fetches the pipelines', () => {
createComponent(); createComponent();
...@@ -134,6 +147,15 @@ describe('BaseTab', () => { ...@@ -134,6 +147,15 @@ describe('BaseTab', () => {
}); });
}); });
it('computes the ETag header', () => {
jest.spyOn(graphQlUtils, 'getQueryHeaders');
createComponent();
expect(graphQlUtils.getQueryHeaders).toHaveBeenCalledWith(
PROJECT_ON_DEMAND_SCAN_COUNTS_ETAG_MOCK,
);
});
it('polls for pipelines as long as the tab is active', async () => { it('polls for pipelines as long as the tab is active', async () => {
createComponent(); createComponent();
......
...@@ -15,6 +15,7 @@ import { s__ } from '~/locale'; ...@@ -15,6 +15,7 @@ import { s__ } from '~/locale';
import ScanTypeBadge from 'ee/security_configuration/dast_profiles/components/dast_scan_type_badge.vue'; import ScanTypeBadge from 'ee/security_configuration/dast_profiles/components/dast_scan_type_badge.vue';
import flushPromises from 'helpers/flush_promises'; import flushPromises from 'helpers/flush_promises';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { PROJECT_ON_DEMAND_SCAN_COUNTS_ETAG_MOCK } from '../../mocks';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -86,6 +87,7 @@ describe('Saved tab', () => { ...@@ -86,6 +87,7 @@ describe('Saved tab', () => {
}, },
provide: { provide: {
projectPath, projectPath,
projectOnDemandScanCountsEtag: PROJECT_ON_DEMAND_SCAN_COUNTS_ETAG_MOCK,
}, },
stubs: { stubs: {
BaseTab, BaseTab,
......
...@@ -13,6 +13,7 @@ import { SCHEDULED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT } from 'ee/on_demand_scans/ ...@@ -13,6 +13,7 @@ import { SCHEDULED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT } from 'ee/on_demand_scans/
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { stripTimezoneFromISODate } from '~/lib/utils/datetime/date_format_utility'; import { stripTimezoneFromISODate } from '~/lib/utils/datetime/date_format_utility';
import DastScanSchedule from 'ee/security_configuration/dast_profiles/components/dast_scan_schedule.vue'; import DastScanSchedule from 'ee/security_configuration/dast_profiles/components/dast_scan_schedule.vue';
import { PROJECT_ON_DEMAND_SCAN_COUNTS_ETAG_MOCK } from '../../mocks';
jest.mock('~/lib/utils/common_utils'); jest.mock('~/lib/utils/common_utils');
...@@ -51,6 +52,7 @@ describe('Scheduled tab', () => { ...@@ -51,6 +52,7 @@ describe('Scheduled tab', () => {
}, },
provide: { provide: {
projectPath, projectPath,
projectOnDemandScanCountsEtag: PROJECT_ON_DEMAND_SCAN_COUNTS_ETAG_MOCK,
timezones: mockTimezones, timezones: mockTimezones,
}, },
stubs: { stubs: {
......
export const PROJECT_ON_DEMAND_SCAN_COUNTS_ETAG_MOCK = `/api/graphql:on_demand_scan/counts/namespace/project`;
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