Commit 171202e4 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo Committed by Andrew Fontaine

Pipeline Graph: Stop calling parseData twice

parent 6c24fa0b
...@@ -39,10 +39,10 @@ export default { ...@@ -39,10 +39,10 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
pipelineLayers: { computedPipelineInfo: {
type: Array, type: Object,
required: false, required: false,
default: () => [], default: () => ({}),
}, },
type: { type: {
type: String, type: String,
...@@ -81,7 +81,10 @@ export default { ...@@ -81,7 +81,10 @@ export default {
layout() { layout() {
return this.isStageView return this.isStageView
? this.pipeline.stages ? this.pipeline.stages
: generateColumnsFromLayersListMemoized(this.pipeline, this.pipelineLayers); : generateColumnsFromLayersListMemoized(
this.pipeline,
this.computedPipelineInfo.pipelineLayers,
);
}, },
hasDownstreamPipelines() { hasDownstreamPipelines() {
return Boolean(this.pipeline?.downstream?.length > 0); return Boolean(this.pipeline?.downstream?.length > 0);
...@@ -92,6 +95,9 @@ export default { ...@@ -92,6 +95,9 @@ export default {
isStageView() { isStageView() {
return this.viewType === STAGE_VIEW; return this.viewType === STAGE_VIEW;
}, },
linksData() {
return this.computedPipelineInfo?.linksData ?? null;
},
metricsConfig() { metricsConfig() {
return { return {
path: this.configPaths.metricsPath, path: this.configPaths.metricsPath,
...@@ -188,6 +194,7 @@ export default { ...@@ -188,6 +194,7 @@ export default {
:container-id="containerId" :container-id="containerId"
:container-measurements="measurements" :container-measurements="measurements"
:highlighted-job="hoveredJobName" :highlighted-job="hoveredJobName"
:links-data="linksData"
:metrics-config="metricsConfig" :metrics-config="metricsConfig"
:show-links="showJobLinks" :show-links="showJobLinks"
:view-type="viewType" :view-type="viewType"
......
...@@ -9,11 +9,11 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants'; ...@@ -9,11 +9,11 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql'; import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql'; import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql';
import { reportToSentry, reportMessageToSentry } from '../../utils'; import { reportToSentry, reportMessageToSentry } from '../../utils';
import { listByLayers } from '../parsing_utils';
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants'; import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
import PipelineGraph from './graph_component.vue'; import PipelineGraph from './graph_component.vue';
import GraphViewSelector from './graph_view_selector.vue'; import GraphViewSelector from './graph_view_selector.vue';
import { import {
calculatePipelineLayersInfo,
getQueryHeaders, getQueryHeaders,
serializeLoadErrors, serializeLoadErrors,
toggleQueryPollingByVisibility, toggleQueryPollingByVisibility,
...@@ -51,10 +51,10 @@ export default { ...@@ -51,10 +51,10 @@ export default {
return { return {
alertType: null, alertType: null,
callouts: [], callouts: [],
computedPipelineInfo: null,
currentViewType: STAGE_VIEW, currentViewType: STAGE_VIEW,
canRefetchHeaderPipeline: false, canRefetchHeaderPipeline: false,
pipeline: null, pipeline: null,
pipelineLayers: null,
showAlert: false, showAlert: false,
showLinks: false, showLinks: false,
}; };
...@@ -214,12 +214,16 @@ export default { ...@@ -214,12 +214,16 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
}, },
methods: { methods: {
getPipelineLayers() { getPipelineInfo() {
if (this.currentViewType === LAYER_VIEW && !this.pipelineLayers) { if (this.currentViewType === LAYER_VIEW && !this.computedPipelineInfo) {
this.pipelineLayers = listByLayers(this.pipeline); this.computedPipelineInfo = calculatePipelineLayersInfo(
this.pipeline,
this.$options.name,
this.metricsPath,
);
} }
return this.pipelineLayers; return this.computedPipelineInfo;
}, },
handleTipDismissal() { handleTipDismissal() {
try { try {
...@@ -288,7 +292,7 @@ export default { ...@@ -288,7 +292,7 @@ export default {
v-if="pipeline" v-if="pipeline"
:config-paths="configPaths" :config-paths="configPaths"
:pipeline="pipeline" :pipeline="pipeline"
:pipeline-layers="getPipelineLayers()" :computed-pipeline-info="getPipelineInfo()"
:show-links="showLinks" :show-links="showLinks"
:view-type="graphViewType" :view-type="graphViewType"
@error="reportFailure" @error="reportFailure"
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { LOAD_FAILURE } from '../../constants'; import { LOAD_FAILURE } from '../../constants';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import { listByLayers } from '../parsing_utils';
import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants'; import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants';
import LinkedPipeline from './linked_pipeline.vue'; import LinkedPipeline from './linked_pipeline.vue';
import { import {
calculatePipelineLayersInfo,
getQueryHeaders, getQueryHeaders,
serializeLoadErrors, serializeLoadErrors,
toggleQueryPollingByVisibility, toggleQueryPollingByVisibility,
...@@ -138,7 +138,11 @@ export default { ...@@ -138,7 +138,11 @@ export default {
}, },
getPipelineLayers(id) { getPipelineLayers(id) {
if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) { if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) {
this.pipelineLayers[id] = listByLayers(this.currentPipeline); this.pipelineLayers[id] = calculatePipelineLayersInfo(
this.currentPipeline,
this.$options.name,
this.configPaths.metricsPath,
);
} }
return this.pipelineLayers[id]; return this.pipelineLayers[id];
...@@ -223,7 +227,7 @@ export default { ...@@ -223,7 +227,7 @@ export default {
class="d-inline-block gl-mt-n2" class="d-inline-block gl-mt-n2"
:config-paths="configPaths" :config-paths="configPaths"
:pipeline="currentPipeline" :pipeline="currentPipeline"
:pipeline-layers="getPipelineLayers(pipeline.id)" :computed-pipeline-info="getPipelineLayers(pipeline.id)"
:show-links="showLinks" :show-links="showLinks"
:is-linked-pipeline="true" :is-linked-pipeline="true"
:view-type="graphViewType" :view-type="graphViewType"
......
import {
PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
PIPELINES_DETAIL_LINK_DURATION,
PIPELINES_DETAIL_LINKS_TOTAL,
PIPELINES_DETAIL_LINKS_JOB_RATIO,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { reportPerformance } from '../graph_shared/api';
export const beginPerfMeasure = () => {
performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
};
export const finishPerfMeasureAndSend = (numLinks, numGroups, metricsPath) => {
performanceMarkAndMeasure({
mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
measures: [
{
name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
},
],
});
window.requestAnimationFrame(() => {
const duration = window.performance.getEntriesByName(
PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
)[0]?.duration;
if (!duration) {
return;
}
const data = {
histograms: [
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
{ name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
{
name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
value: numLinks / numGroups,
},
],
};
reportPerformance(metricsPath, data);
});
};
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { reportToSentry } from '../../utils';
import { listByLayers } from '../parsing_utils';
import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils'; import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils';
const addMulti = (mainPipelineProjectPath, linkedPipeline) => { const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
return { return {
...@@ -10,6 +13,28 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => { ...@@ -10,6 +13,28 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
}; };
}; };
const calculatePipelineLayersInfo = (pipeline, componentName, metricsPath) => {
const shouldCollectMetrics = Boolean(metricsPath.length);
if (shouldCollectMetrics) {
beginPerfMeasure();
}
let layers = null;
try {
layers = listByLayers(pipeline);
if (shouldCollectMetrics) {
finishPerfMeasureAndSend(layers.linksData.length, layers.numGroups, metricsPath);
}
} catch (err) {
reportToSentry(componentName, err);
}
return layers;
};
/* eslint-disable @gitlab/require-i18n-strings */ /* eslint-disable @gitlab/require-i18n-strings */
const getQueryHeaders = (etagResource) => { const getQueryHeaders = (etagResource) => {
return { return {
...@@ -106,6 +131,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { ...@@ -106,6 +131,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => {
const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0; const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
export { export {
calculatePipelineLayersInfo,
getQueryHeaders, getQueryHeaders,
serializeGqlErr, serializeGqlErr,
serializeLoadErrors, serializeLoadErrors,
......
...@@ -13,7 +13,7 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam ...@@ -13,7 +13,7 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam
* @returns {Array} Links that contain all the information about them * @returns {Array} Links that contain all the information about them
*/ */
export const generateLinksData = ({ links }, containerID, modifier = '') => { export const generateLinksData = (links, containerID, modifier = '') => {
const containerEl = document.getElementById(containerID); const containerEl = document.getElementById(containerID);
return links.map((link) => { return links.map((link) => {
......
...@@ -17,8 +17,8 @@ export default { ...@@ -17,8 +17,8 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
parsedData: { linksData: {
type: Object, type: Array,
required: true, required: true,
}, },
pipelineId: { pipelineId: {
...@@ -95,7 +95,7 @@ export default { ...@@ -95,7 +95,7 @@ export default {
highlightedJobs(jobs) { highlightedJobs(jobs) {
this.$emit('highlightedJobsChange', jobs); this.$emit('highlightedJobsChange', jobs);
}, },
parsedData() { linksData() {
this.calculateLinkData(); this.calculateLinkData();
}, },
viewType() { viewType() {
...@@ -112,7 +112,7 @@ export default { ...@@ -112,7 +112,7 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
}, },
mounted() { mounted() {
if (!isEmpty(this.parsedData)) { if (!isEmpty(this.linksData)) {
this.calculateLinkData(); this.calculateLinkData();
} }
}, },
...@@ -122,7 +122,7 @@ export default { ...@@ -122,7 +122,7 @@ export default {
}, },
calculateLinkData() { calculateLinkData() {
try { try {
this.links = generateLinksData(this.parsedData, this.containerId, `-${this.pipelineId}`); this.links = generateLinksData(this.linksData, this.containerId, `-${this.pipelineId}`);
} catch (err) { } catch (err) {
this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false }); this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false });
reportToSentry(this.$options.name, err); reportToSentry(this.$options.name, err);
......
<script> <script>
import { isEmpty } from 'lodash'; import { memoize } from 'lodash';
import { __ } from '~/locale';
import {
PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
PIPELINES_DETAIL_LINK_DURATION,
PIPELINES_DETAIL_LINKS_TOTAL,
PIPELINES_DETAIL_LINKS_JOB_RATIO,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import { parseData } from '../parsing_utils'; import { parseData } from '../parsing_utils';
import { reportPerformance } from './api';
import LinksInner from './links_inner.vue'; import LinksInner from './links_inner.vue';
const parseForLinksBare = (pipeline) => {
const arrayOfJobs = pipeline.flatMap(({ groups }) => groups);
return parseData(arrayOfJobs).links;
};
const parseForLinks = memoize(parseForLinksBare);
export default { export default {
name: 'LinksLayer', name: 'LinksLayer',
components: { components: {
...@@ -29,10 +25,10 @@ export default { ...@@ -29,10 +25,10 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
metricsConfig: { linksData: {
type: Object, type: Array,
required: false, required: false,
default: () => ({}), default: () => [],
}, },
showLinks: { showLinks: {
type: Boolean, type: Boolean,
...@@ -40,30 +36,16 @@ export default { ...@@ -40,30 +36,16 @@ export default {
default: true, default: true,
}, },
}, },
data() {
return {
alertDismissed: false,
parsedData: {},
showLinksOverride: false,
};
},
i18n: {
showLinksAnyways: __('Show links anyways'),
tooManyJobs: __(
'This graph has a large number of jobs and showing the links between them may have performance implications.',
),
},
computed: { computed: {
containerZero() { containerZero() {
return !this.containerMeasurements.width || !this.containerMeasurements.height; return !this.containerMeasurements.width || !this.containerMeasurements.height;
}, },
numGroups() { getLinksData() {
return this.pipelineData.reduce((acc, { groups }) => { if (this.linksData.length > 0) {
return acc + Number(groups.length); return this.linksData;
}, 0); }
},
shouldCollectMetrics() { return parseForLinks(this.pipelineData);
return this.metricsConfig.collectMetrics && this.metricsConfig.path;
}, },
showLinkedLayers() { showLinkedLayers() {
return this.showLinks && !this.containerZero; return this.showLinks && !this.containerZero;
...@@ -72,77 +54,14 @@ export default { ...@@ -72,77 +54,14 @@ export default {
errorCaptured(err, _vm, info) { errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
}, },
mounted() {
if (!isEmpty(this.pipelineData)) {
window.requestAnimationFrame(() => {
this.prepareLinkData();
});
}
},
methods: {
beginPerfMeasure() {
if (this.shouldCollectMetrics) {
performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
}
},
finishPerfMeasureAndSend(numLinks) {
if (this.shouldCollectMetrics) {
performanceMarkAndMeasure({
mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
measures: [
{
name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
},
],
});
}
window.requestAnimationFrame(() => {
const duration = window.performance.getEntriesByName(
PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
)[0]?.duration;
if (!duration) {
return;
}
const data = {
histograms: [
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
{ name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
{
name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
value: numLinks / this.numGroups,
},
],
};
reportPerformance(this.metricsConfig.path, data);
});
},
prepareLinkData() {
this.beginPerfMeasure();
let numLinks;
try {
const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
this.parsedData = parseData(arrayOfJobs);
numLinks = this.parsedData.links.length;
} catch (err) {
reportToSentry(this.$options.name, err);
}
this.finishPerfMeasureAndSend(numLinks);
},
},
}; };
</script> </script>
<template> <template>
<links-inner <links-inner
v-if="showLinkedLayers" v-if="showLinkedLayers"
:container-measurements="containerMeasurements" :container-measurements="containerMeasurements"
:parsed-data="parsedData" :links-data="getLinksData"
:pipeline-data="pipelineData" :pipeline-data="pipelineData"
:total-groups="numGroups"
v-bind="$attrs" v-bind="$attrs"
v-on="$listeners" v-on="$listeners"
> >
......
...@@ -175,7 +175,7 @@ export const listByLayers = ({ stages }) => { ...@@ -175,7 +175,7 @@ export const listByLayers = ({ stages }) => {
const parsedData = parseData(arrayOfJobs); const parsedData = parseData(arrayOfJobs);
const dataWithLayers = createSankey()(parsedData); const dataWithLayers = createSankey()(parsedData);
return dataWithLayers.nodes.reduce((acc, { layer, name }) => { const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
/* sort groups by layer */ /* sort groups by layer */
if (!acc[layer]) { if (!acc[layer]) {
...@@ -186,6 +186,12 @@ export const listByLayers = ({ stages }) => { ...@@ -186,6 +186,12 @@ export const listByLayers = ({ stages }) => {
return acc; return acc;
}, []); }, []);
return {
linksData: parsedData.links,
numGroups: arrayOfJobs.length,
pipelineLayers,
};
}; };
export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipelineLayers) => { export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipelineLayers) => {
......
...@@ -30537,9 +30537,6 @@ msgstr "" ...@@ -30537,9 +30537,6 @@ msgstr ""
msgid "Show latest version" msgid "Show latest version"
msgstr "" msgstr ""
msgid "Show links anyways"
msgstr ""
msgid "Show list" msgid "Show list"
msgstr "" msgstr ""
...@@ -33877,9 +33874,6 @@ msgstr "" ...@@ -33877,9 +33874,6 @@ msgstr ""
msgid "This field is required." msgid "This field is required."
msgstr "" msgstr ""
msgid "This graph has a large number of jobs and showing the links between them may have performance implications."
msgstr ""
msgid "This group" msgid "This group"
msgstr "" msgstr ""
......
...@@ -4,8 +4,8 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; ...@@ -4,8 +4,8 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue'; import JobItem from '~/pipelines/components/graph/job_item.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import { calculatePipelineLayersInfo } from '~/pipelines/components/graph/utils';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import { listByLayers } from '~/pipelines/components/parsing_utils';
import { import {
generateResponse, generateResponse,
mockPipelineResponse, mockPipelineResponse,
...@@ -150,7 +150,7 @@ describe('graph component', () => { ...@@ -150,7 +150,7 @@ describe('graph component', () => {
}, },
props: { props: {
viewType: LAYER_VIEW, viewType: LAYER_VIEW,
pipelineLayers: listByLayers(defaultProps.pipeline), computedPipelineInfo: calculatePipelineLayersInfo(defaultProps.pipeline, 'layer', ''),
}, },
}); });
}); });
......
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql'; import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
import axios from '~/lib/utils/axios_utils';
import {
PIPELINES_DETAIL_LINK_DURATION,
PIPELINES_DETAIL_LINKS_TOTAL,
PIPELINES_DETAIL_LINKS_JOB_RATIO,
} from '~/performance/constants';
import * as perfUtils from '~/performance/utils';
import { import {
IID_FAILURE, IID_FAILURE,
LAYER_VIEW, LAYER_VIEW,
...@@ -16,9 +24,11 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; ...@@ -16,9 +24,11 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue'; import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue'; import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import * as Api from '~/pipelines/components/graph_shared/api';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils'; import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
import * as sentryUtils from '~/pipelines/utils';
import { mockRunningPipelineHeaderData } from '../mock_data'; import { mockRunningPipelineHeaderData } from '../mock_data';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data'; import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
...@@ -480,4 +490,112 @@ describe('Pipeline graph wrapper', () => { ...@@ -480,4 +490,112 @@ describe('Pipeline graph wrapper', () => {
}); });
}); });
}); });
describe('performance metrics', () => {
const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json';
let markAndMeasure;
let reportToSentry;
let reportPerformance;
let mock;
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure');
reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry');
reportPerformance = jest.spyOn(Api, 'reportPerformance');
});
describe('with no metrics path', () => {
beforeEach(async () => {
createComponentWithApollo();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
});
it('is not called', () => {
expect(markAndMeasure).not.toHaveBeenCalled();
expect(reportToSentry).not.toHaveBeenCalled();
expect(reportPerformance).not.toHaveBeenCalled();
});
});
describe('with metrics path', () => {
const duration = 875;
const numLinks = 7;
const totalGroups = 8;
const metricsData = {
histograms: [
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
{ name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
{
name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
value: numLinks / totalGroups,
},
],
};
describe('when no duration is obtained', () => {
beforeEach(async () => {
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
return [];
});
createComponentWithApollo({
provide: {
metricsPath,
glFeatures: {
pipelineGraphLayersView: true,
},
},
data: {
currentViewType: LAYER_VIEW,
},
});
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
});
it('attempts to collect metrics', () => {
expect(markAndMeasure).toHaveBeenCalled();
expect(reportPerformance).not.toHaveBeenCalled();
expect(reportToSentry).not.toHaveBeenCalled();
});
});
describe('with duration and no error', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onPost(metricsPath).reply(200, {});
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
return [{ duration }];
});
createComponentWithApollo({
provide: {
metricsPath,
glFeatures: {
pipelineGraphLayersView: true,
},
},
data: {
currentViewType: LAYER_VIEW,
},
});
});
afterEach(() => {
mock.restore();
});
it('it calls reportPerformance with expected arguments', () => {
expect(markAndMeasure).toHaveBeenCalled();
expect(reportPerformance).toHaveBeenCalled();
expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
expect(reportToSentry).not.toHaveBeenCalled();
});
});
});
});
}); });
...@@ -31,7 +31,7 @@ describe('Links Inner component', () => { ...@@ -31,7 +31,7 @@ describe('Links Inner component', () => {
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
parsedData: parseData(currentPipelineData.flatMap(({ groups }) => groups)), linksData: parseData(currentPipelineData.flatMap(({ groups }) => groups)).links,
}, },
}); });
}; };
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
PIPELINES_DETAIL_LINK_DURATION,
PIPELINES_DETAIL_LINKS_TOTAL,
PIPELINES_DETAIL_LINKS_JOB_RATIO,
} from '~/performance/constants';
import * as perfUtils from '~/performance/utils';
import * as Api from '~/pipelines/components/graph_shared/api';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue'; import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as sentryUtils from '~/pipelines/utils';
import { generateResponse, mockPipelineResponse } from '../graph/mock_data'; import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
describe('links layer component', () => { describe('links layer component', () => {
...@@ -94,139 +84,4 @@ describe('links layer component', () => { ...@@ -94,139 +84,4 @@ describe('links layer component', () => {
expect(findLinksInner().exists()).toBe(false); expect(findLinksInner().exists()).toBe(false);
}); });
}); });
describe('performance metrics', () => {
const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json';
let markAndMeasure;
let reportToSentry;
let reportPerformance;
let mock;
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure');
reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry');
reportPerformance = jest.spyOn(Api, 'reportPerformance');
});
describe('with no metrics config object', () => {
beforeEach(() => {
createComponent();
});
it('is not called', () => {
expect(markAndMeasure).not.toHaveBeenCalled();
expect(reportToSentry).not.toHaveBeenCalled();
expect(reportPerformance).not.toHaveBeenCalled();
});
});
describe('with metrics config set to false', () => {
beforeEach(() => {
createComponent({
props: {
metricsConfig: {
collectMetrics: false,
metricsPath: '/path/to/metrics',
},
},
});
});
it('is not called', () => {
expect(markAndMeasure).not.toHaveBeenCalled();
expect(reportToSentry).not.toHaveBeenCalled();
expect(reportPerformance).not.toHaveBeenCalled();
});
});
describe('with no metrics path', () => {
beforeEach(() => {
createComponent({
props: {
metricsConfig: {
collectMetrics: true,
metricsPath: '',
},
},
});
});
it('is not called', () => {
expect(markAndMeasure).not.toHaveBeenCalled();
expect(reportToSentry).not.toHaveBeenCalled();
expect(reportPerformance).not.toHaveBeenCalled();
});
});
describe('with metrics path and collect set to true', () => {
const duration = 875;
const numLinks = 7;
const totalGroups = 8;
const metricsData = {
histograms: [
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
{ name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
{
name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
value: numLinks / totalGroups,
},
],
};
describe('when no duration is obtained', () => {
beforeEach(() => {
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
return [];
});
createComponent({
props: {
metricsConfig: {
collectMetrics: true,
path: metricsPath,
},
},
});
});
it('attempts to collect metrics', () => {
expect(markAndMeasure).toHaveBeenCalled();
expect(reportPerformance).not.toHaveBeenCalled();
expect(reportToSentry).not.toHaveBeenCalled();
});
});
describe('with duration and no error', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onPost(metricsPath).reply(200, {});
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
return [{ duration }];
});
createComponent({
props: {
metricsConfig: {
collectMetrics: true,
path: metricsPath,
},
},
});
});
afterEach(() => {
mock.restore();
});
it('it calls reportPerformance with expected arguments', () => {
expect(markAndMeasure).toHaveBeenCalled();
expect(reportPerformance).toHaveBeenCalled();
expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
expect(reportToSentry).not.toHaveBeenCalled();
});
});
});
});
}); });
...@@ -120,8 +120,8 @@ describe('DAG visualization parsing utilities', () => { ...@@ -120,8 +120,8 @@ describe('DAG visualization parsing utilities', () => {
describe('generateColumnsFromLayersList', () => { describe('generateColumnsFromLayersList', () => {
const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo'); const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
const layers = listByLayers(pipeline); const { pipelineLayers } = listByLayers(pipeline);
const columns = generateColumnsFromLayersListBare(pipeline, layers); const columns = generateColumnsFromLayersListBare(pipeline, pipelineLayers);
it('returns stage-like objects with default name, id, and status', () => { it('returns stage-like objects with default name, id, and status', () => {
columns.forEach((col, idx) => { columns.forEach((col, idx) => {
...@@ -136,7 +136,7 @@ describe('DAG visualization parsing utilities', () => { ...@@ -136,7 +136,7 @@ describe('DAG visualization parsing utilities', () => {
it('creates groups that match the list created in listByLayers', () => { it('creates groups that match the list created in listByLayers', () => {
columns.forEach((col, idx) => { columns.forEach((col, idx) => {
const groupNames = col.groups.map(({ name }) => name); const groupNames = col.groups.map(({ name }) => name);
expect(groupNames).toEqual(layers[idx]); expect(groupNames).toEqual(pipelineLayers[idx]);
}); });
}); });
......
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