diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index ea45b5e3ec7..015f0519c72 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -39,10 +39,10 @@ export default { required: false, default: false, }, - pipelineLayers: { - type: Array, + computedPipelineInfo: { + type: Object, required: false, - default: () => [], + default: () => ({}), }, type: { type: String, @@ -81,7 +81,10 @@ export default { layout() { return this.isStageView ? this.pipeline.stages - : generateColumnsFromLayersListMemoized(this.pipeline, this.pipelineLayers); + : generateColumnsFromLayersListMemoized( + this.pipeline, + this.computedPipelineInfo.pipelineLayers, + ); }, hasDownstreamPipelines() { return Boolean(this.pipeline?.downstream?.length > 0); @@ -92,6 +95,9 @@ export default { isStageView() { return this.viewType === STAGE_VIEW; }, + linksData() { + return this.computedPipelineInfo?.linksData ?? null; + }, metricsConfig() { return { path: this.configPaths.metricsPath, @@ -188,6 +194,7 @@ export default { :container-id="containerId" :container-measurements="measurements" :highlighted-job="hoveredJobName" + :links-data="linksData" :metrics-config="metricsConfig" :show-links="showJobLinks" :view-type="viewType" diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index 5d51d97eaee..8462fb752b7 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -9,11 +9,11 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants'; import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql'; import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql'; import { reportToSentry, reportMessageToSentry } from '../../utils'; -import { listByLayers } from '../parsing_utils'; import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants'; import PipelineGraph from './graph_component.vue'; import GraphViewSelector from './graph_view_selector.vue'; import { + calculatePipelineLayersInfo, getQueryHeaders, serializeLoadErrors, toggleQueryPollingByVisibility, @@ -51,10 +51,10 @@ export default { return { alertType: null, callouts: [], + computedPipelineInfo: null, currentViewType: STAGE_VIEW, canRefetchHeaderPipeline: false, pipeline: null, - pipelineLayers: null, showAlert: false, showLinks: false, }; @@ -214,12 +214,16 @@ export default { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); }, methods: { - getPipelineLayers() { - if (this.currentViewType === LAYER_VIEW && !this.pipelineLayers) { - this.pipelineLayers = listByLayers(this.pipeline); + getPipelineInfo() { + if (this.currentViewType === LAYER_VIEW && !this.computedPipelineInfo) { + this.computedPipelineInfo = calculatePipelineLayersInfo( + this.pipeline, + this.$options.name, + this.metricsPath, + ); } - return this.pipelineLayers; + return this.computedPipelineInfo; }, handleTipDismissal() { try { @@ -288,7 +292,7 @@ export default { v-if="pipeline" :config-paths="configPaths" :pipeline="pipeline" - :pipeline-layers="getPipelineLayers()" + :computed-pipeline-info="getPipelineInfo()" :show-links="showLinks" :view-type="graphViewType" @error="reportFailure" diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 52ee40bd982..d251e0d8bd8 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -2,10 +2,10 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import { LOAD_FAILURE } from '../../constants'; import { reportToSentry } from '../../utils'; -import { listByLayers } from '../parsing_utils'; import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants'; import LinkedPipeline from './linked_pipeline.vue'; import { + calculatePipelineLayersInfo, getQueryHeaders, serializeLoadErrors, toggleQueryPollingByVisibility, @@ -138,7 +138,11 @@ export default { }, getPipelineLayers(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]; @@ -223,7 +227,7 @@ export default { class="d-inline-block gl-mt-n2" :config-paths="configPaths" :pipeline="currentPipeline" - :pipeline-layers="getPipelineLayers(pipeline.id)" + :computed-pipeline-info="getPipelineLayers(pipeline.id)" :show-links="showLinks" :is-linked-pipeline="true" :view-type="graphViewType" diff --git a/app/assets/javascripts/pipelines/components/graph/perf_utils.js b/app/assets/javascripts/pipelines/components/graph/perf_utils.js new file mode 100644 index 00000000000..3737a209f5c --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/perf_utils.js @@ -0,0 +1,50 @@ +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); + }); +}; diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js index 163b3898c28..3acfc10108b 100644 --- a/app/assets/javascripts/pipelines/components/graph/utils.js +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -1,7 +1,10 @@ import { isEmpty } from 'lodash'; import Visibility from 'visibilityjs'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { reportToSentry } from '../../utils'; +import { listByLayers } from '../parsing_utils'; import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils'; +import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils'; const addMulti = (mainPipelineProjectPath, linkedPipeline) => { return { @@ -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 */ const getQueryHeaders = (etagResource) => { return { @@ -106,6 +131,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0; export { + calculatePipelineLayersInfo, getQueryHeaders, serializeGqlErr, serializeLoadErrors, diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js index 83f2466f0bf..d6d9ea94c13 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js +++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js @@ -13,7 +13,7 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam * @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); return links.map((link) => { diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue index 5c775df7b48..1189c2ebad8 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue @@ -17,8 +17,8 @@ export default { type: Object, required: true, }, - parsedData: { - type: Object, + linksData: { + type: Array, required: true, }, pipelineId: { @@ -95,7 +95,7 @@ export default { highlightedJobs(jobs) { this.$emit('highlightedJobsChange', jobs); }, - parsedData() { + linksData() { this.calculateLinkData(); }, viewType() { @@ -112,7 +112,7 @@ export default { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); }, mounted() { - if (!isEmpty(this.parsedData)) { + if (!isEmpty(this.linksData)) { this.calculateLinkData(); } }, @@ -122,7 +122,7 @@ export default { }, calculateLinkData() { try { - this.links = generateLinksData(this.parsedData, this.containerId, `-${this.pipelineId}`); + this.links = generateLinksData(this.linksData, this.containerId, `-${this.pipelineId}`); } catch (err) { this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false }); reportToSentry(this.$options.name, err); diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue index 81409752621..ef24694e494 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue @@ -1,20 +1,16 @@