From e4db319e41522503927436b7c944752dece0a605 Mon Sep 17 00:00:00 2001 From: Florin Bilt Date: Fri, 3 Oct 2025 17:24:02 +0300 Subject: [PATCH 1/5] Bug 1976036 - Add an API endpoint that can reduce the two treeherder performance tab requests into a single --- treeherder/webapp/api/performance_data.py | 56 ++++++++++ treeherder/webapp/api/urls.py | 4 + ui/job-view/details/DetailsPanel.jsx | 121 +++++++++------------- ui/models/perfSeries.js | 23 ++++ 4 files changed, 131 insertions(+), 73 deletions(-) diff --git a/treeherder/webapp/api/performance_data.py b/treeherder/webapp/api/performance_data.py index f42b20f9301..40efc63d6c5 100644 --- a/treeherder/webapp/api/performance_data.py +++ b/treeherder/webapp/api/performance_data.py @@ -213,6 +213,62 @@ class PerformanceFrameworkViewSet(viewsets.ReadOnlyModelViewSet): ordering = "id" +class PerfomanceJobViewSet(viewsets.ReadOnlyModelViewSet): + def list(self, request, project): + # Expect exactly one job_id in query params + job_id_str = request.query_params.get("job_id") + if job_id_str is None: + return Response( + {"message": "Parameter 'job_id' is required."}, + status=HTTP_400_BAD_REQUEST, + ) + # Validate that job_id is an integer + try: + job_id = int(job_id_str) + except ValueError: + return Response( + {"message": "Parameter 'job_id' must be an integer."}, + status=HTTP_400_BAD_REQUEST, + ) + # Fetch the first PerformanceDatum for this job_id + datum = ( + PerformanceDatum.objects.filter(job_id=job_id) + .select_related("signature", "push") + .first() + ) + if not datum: + return Response( + {"message": f"No data found for job_id={job_id}"}, + status=HTTP_400_BAD_REQUEST, + ) + # Build a single response object + result = { + "id": datum.id, + "signature_data": self.get_signature_data(datum.signature_id), + "job_id": datum.job_id, + "push_id": datum.push_id, + "revision": datum.push.revision, + "push_timestamp": int(time.mktime(datum.push_timestamp.timetuple())), + "value": round(datum.value, 2), + } + return Response(result) + + def get_signature_data(self, signature_id): + obj = PerformanceSignature.objects.select_related( + "option_collection", "platform", "parent_signature" + ).get(id=signature_id) + return { + "id": obj.id, + "signature_hash": obj.signature_hash, + "framework_id": obj.framework_id, + "option_collection_hash": obj.option_collection.option_collection_hash, + "machine_platform": obj.platform.platform, + "suite": obj.suite, + "should_alert": obj.should_alert, + "has_subtests": True if obj.has_subtests else False, + } + + class PerformanceDatumViewSet(viewsets.ViewSet): """ This view serves performance test result data diff --git a/treeherder/webapp/api/urls.py b/treeherder/webapp/api/urls.py index 9724b49f77f..4f4223cb99f 100644 --- a/treeherder/webapp/api/urls.py +++ b/treeherder/webapp/api/urls.py @@ -85,6 +85,10 @@ r"performance/data", performance_data.PerformanceDatumViewSet, basename="performance-data" ) +project_bound_router.register( + r"performance/job-data", performance_data.PerfomanceJobViewSet, basename="performance-job-data" +) + project_bound_router.register( r"performance/signatures", performance_data.PerformanceSignatureViewSet, diff --git a/ui/job-view/details/DetailsPanel.jsx b/ui/job-view/details/DetailsPanel.jsx index c4a16ddc5c4..c59e76bce07 100644 --- a/ui/job-view/details/DetailsPanel.jsx +++ b/ui/job-view/details/DetailsPanel.jsx @@ -185,6 +185,7 @@ class DetailsPanel extends React.Component { ...artifactsParams, ...{ artifactPath: 'public/build/built_from.json' }, }), + this.selectJobController.signal, ); } @@ -193,86 +194,60 @@ class DetailsPanel extends React.Component { this.selectJobController.signal, ); - const performancePromise = PerfSeriesModel.getSeriesData( + const performancePromise = PerfSeriesModel.getJobData( currentRepo.name, - { - job_id: selectedJob.id, - }, - ).then(async (phSeriesResult) => { - const performanceData = Object.values(phSeriesResult).reduce( - (a, b) => [...a, ...b], - [], - ); - let perfJobDetail = []; - - if (performanceData.length) { - const signatureIds = [ - ...new Set(performanceData.map((perf) => perf.signature_id)), - ]; - const seriesListList = await Promise.all( - chunk(signatureIds, 20).map((signatureIdChunk) => - PerfSeriesModel.getSeriesList(currentRepo.name, { - id: signatureIdChunk, - }), - ), - ); - const mappedFrameworks = {}; - frameworks.forEach((element) => { - mappedFrameworks[element.id] = element.name; - }); - - const seriesList = seriesListList - .map((item) => item.data) - .reduce((a, b) => [...a, ...b], []); - - perfJobDetail = performanceData - .map((d) => ({ - series: seriesList.find((s) => d.signature_id === s.id), - ...d, - })) - .map((d) => ({ - url: `/perfherder/graphs?series=${[ - currentRepo.name, - d.signature_id, - 1, - d.series.frameworkId, - ]}&selected=${[d.signature_id, d.id]}`, - shouldAlert: d.series.should_alert, - value: d.value, - measurementUnit: d.series.measurementUnit, - lowerIsBetter: d.series.lowerIsBetter, - title: d.series.name, - suite: d.series.suite, - options: d.series.options.join(' '), - frameworkName: mappedFrameworks[d.series.frameworkId], - perfdocs: new Perfdocs( - mappedFrameworks[d.series.frameworkId], - d.series.suite, - d.series.platform, - d.series.name, - ), - })); + { job_id: selectedJob.id }, + ).then((rowOrResponse) => { + const jobData = + rowOrResponse && rowOrResponse.data + ? rowOrResponse.data + : rowOrResponse; + if (!jobData || jobData.failureStatus) { + this.setState({ perfJobDetail: [] }); + return; } + + const rows = Array.isArray(jobData) ? jobData : [jobData]; + const mappedFrameworks = {}; + frameworks.forEach((element) => { + mappedFrameworks[element.id] = element.name; + }); + + const perfJobDetail = rows.map((jobData) => { + const signature = jobData.signature_data; + return { + url: `/perfherder/graphs?series=${[ + currentRepo.name, + signature.id, + 1, + signature.frameworkId, + ]}&selected=${[signature.id, jobData.id]}`, + shouldAlert: signature.should_alert, + value: jobData.value, + measurementUnit: signature.measurementUnit, + lowerIsBetter: signature.lowerIsBetter, + title: signature.name, + suite: signature.suite, + options: signature.options.join(' '), + frameworkName: mappedFrameworks[signature.frameworkId], + perfdocs: new Perfdocs( + mappedFrameworks[signature.frameworkId], + signature.suite, + signature.platform, + signature.name, + ), + }; + }); perfJobDetail.sort((a, b) => { // Sort perfJobDetails by value of shouldAlert in a particular order: // first true values, after that null values and then false. - if (a.shouldAlert === true) { - return -1; - } - if (a.shouldAlert === false) { - return 1; - } - if (a.shouldAlert === null && b.shouldAlert === true) { - return 1; - } - if (a.shouldAlert === null && b.shouldAlert === false) { - return -1; - } + if (a.shouldAlert === true) return -1; + if (a.shouldAlert === false) return 1; + if (a.shouldAlert === null && b.shouldAlert === true) return 1; + if (a.shouldAlert === null && b.shouldAlert === false) return -1; return 0; }); - this.setState({ - perfJobDetail, - }); + this.setState({ perfJobDetail }); }); Promise.all([ diff --git a/ui/models/perfSeries.js b/ui/models/perfSeries.js index 07791a652d4..693cb51b8c8 100644 --- a/ui/models/perfSeries.js +++ b/ui/models/perfSeries.js @@ -112,6 +112,29 @@ export default class PerfSeriesModel { return { data, failureStatus: null }; } + static async getJobData(projectName, params) { + if (!this.optionCollectionMap) { + this.optionCollectionMap = await OptionCollectionModel.getMap(); + } + const url = + `${getProjectUrl('/performance/job-data/', projectName)}?` + + queryString.stringify(params); + const response = await getData(url); + if (response.failureStatus) { + return response; + } + const { data } = response; + + data.signature_data = await getSeriesSummary( + projectName, + data.signature_data.id, + data.signature_data, + this.optionCollectionMap, + ); + + return data; + } + static getPlatformList(projectName, params) { return getData( `${getProjectUrl( From 4d8c83b6e0d09fa535e334a82618112463bc5734 Mon Sep 17 00:00:00 2001 From: Florin Bilt Date: Mon, 6 Oct 2025 16:03:07 +0300 Subject: [PATCH 2/5] Bug 1976036 - Add an API endpoint that can reduce the two treeherder performance tab requests into a single --- ui/job-view/details/DetailsPanel.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/job-view/details/DetailsPanel.jsx b/ui/job-view/details/DetailsPanel.jsx index c59e76bce07..7fa30f8e6ca 100644 --- a/ui/job-view/details/DetailsPanel.jsx +++ b/ui/job-view/details/DetailsPanel.jsx @@ -185,7 +185,6 @@ class DetailsPanel extends React.Component { ...artifactsParams, ...{ artifactPath: 'public/build/built_from.json' }, }), - this.selectJobController.signal, ); } From e028ec6d0e1a7e32281c578439ebe021143f62b1 Mon Sep 17 00:00:00 2001 From: Florin Bilt Date: Mon, 6 Oct 2025 16:24:19 +0300 Subject: [PATCH 3/5] fix lint error --- ui/job-view/details/DetailsPanel.jsx | 1 - ui/models/perfSeries.js | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/job-view/details/DetailsPanel.jsx b/ui/job-view/details/DetailsPanel.jsx index 7fa30f8e6ca..79b62b5b19e 100644 --- a/ui/job-view/details/DetailsPanel.jsx +++ b/ui/job-view/details/DetailsPanel.jsx @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import chunk from 'lodash/chunk'; import { connect } from 'react-redux'; import { Queue } from 'taskcluster-client-web'; diff --git a/ui/models/perfSeries.js b/ui/models/perfSeries.js index 693cb51b8c8..ec54daf9840 100644 --- a/ui/models/perfSeries.js +++ b/ui/models/perfSeries.js @@ -116,9 +116,10 @@ export default class PerfSeriesModel { if (!this.optionCollectionMap) { this.optionCollectionMap = await OptionCollectionModel.getMap(); } - const url = - `${getProjectUrl('/performance/job-data/', projectName)}?` + - queryString.stringify(params); + const url = `${getProjectUrl( + '/performance/job-data/', + projectName, + )}?${queryString.stringify(params)}`; const response = await getData(url); if (response.failureStatus) { return response; From a41a7bef2e564a5a16d346b29368a002d3894391 Mon Sep 17 00:00:00 2001 From: Florin Bilt Date: Mon, 6 Oct 2025 16:24:19 +0300 Subject: [PATCH 4/5] fix lint error --- tests/ui/job-view/App_test.jsx | 2 +- tests/ui/job-view/Filtering_test.jsx | 2 +- tests/ui/job-view/details/PinBoard_test.jsx | 2 +- ui/job-view/details/DetailsPanel.jsx | 10 ++++---- ui/models/perfSeries.js | 26 ++++++++++++++------- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/tests/ui/job-view/App_test.jsx b/tests/ui/job-view/App_test.jsx index bee417a56e2..2af781ae5cd 100644 --- a/tests/ui/job-view/App_test.jsx +++ b/tests/ui/job-view/App_test.jsx @@ -104,7 +104,7 @@ describe('App', () => { [], ); fetchMock.get( - `begin:${getProjectUrl('/performance/data/?job_id=', repoName)}`, + `begin:${getProjectUrl('/performance/job-data/?job_id=', repoName)}`, [], ); fetchMock.get(`begin:${getApiUrl('/jobs/')}`, jobListFixtureOne); diff --git a/tests/ui/job-view/Filtering_test.jsx b/tests/ui/job-view/Filtering_test.jsx index 1d09dbc4cce..43e61336228 100644 --- a/tests/ui/job-view/Filtering_test.jsx +++ b/tests/ui/job-view/Filtering_test.jsx @@ -266,7 +266,7 @@ describe('Filtering', () => { [], ); fetchMock.get( - getProjectUrl('/performance/data/?job_id=259537372', 'autoland'), + getProjectUrl('/performance/job-data/?job_id=259537372', 'autoland'), [], ); fetchMock.get( diff --git a/tests/ui/job-view/details/PinBoard_test.jsx b/tests/ui/job-view/details/PinBoard_test.jsx index 4b1d72a980d..473907c2cc2 100644 --- a/tests/ui/job-view/details/PinBoard_test.jsx +++ b/tests/ui/job-view/details/PinBoard_test.jsx @@ -66,7 +66,7 @@ describe('DetailsPanel', () => { [], ); fetchMock.get( - getProjectUrl(`/performance/data/?job_id=${selectedJobId}`, repoName), + getProjectUrl(`/performance/job-data/?job_id=${selectedJobId}`, repoName), [], ); fetchMock.get( diff --git a/ui/job-view/details/DetailsPanel.jsx b/ui/job-view/details/DetailsPanel.jsx index 7fa30f8e6ca..f87b8e8c1be 100644 --- a/ui/job-view/details/DetailsPanel.jsx +++ b/ui/job-view/details/DetailsPanel.jsx @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import chunk from 'lodash/chunk'; import { connect } from 'react-redux'; import { Queue } from 'taskcluster-client-web'; @@ -197,11 +196,10 @@ class DetailsPanel extends React.Component { currentRepo.name, { job_id: selectedJob.id }, ).then((rowOrResponse) => { - const jobData = - rowOrResponse && rowOrResponse.data - ? rowOrResponse.data - : rowOrResponse; - if (!jobData || jobData.failureStatus) { + const jobData = !rowOrResponse.failureStatus + ? rowOrResponse.data + : rowOrResponse; + if (jobData.failureStatus) { this.setState({ perfJobDetail: [] }); return; } diff --git a/ui/models/perfSeries.js b/ui/models/perfSeries.js index 693cb51b8c8..f7f3391be0c 100644 --- a/ui/models/perfSeries.js +++ b/ui/models/perfSeries.js @@ -113,17 +113,25 @@ export default class PerfSeriesModel { } static async getJobData(projectName, params) { + const url = `${getProjectUrl( + '/performance/job-data/', + projectName, + )}?${queryString.stringify(params)}`; + const response = await getData(url); + + if ( + !response || + response.failureStatus || + !response.data || + !response.data.signature_data + ) { + return { failureStatus: true, data: ['No data for this job'] }; + } + const data = response.data; + if (!this.optionCollectionMap) { this.optionCollectionMap = await OptionCollectionModel.getMap(); } - const url = - `${getProjectUrl('/performance/job-data/', projectName)}?` + - queryString.stringify(params); - const response = await getData(url); - if (response.failureStatus) { - return response; - } - const { data } = response; data.signature_data = await getSeriesSummary( projectName, @@ -132,7 +140,7 @@ export default class PerfSeriesModel { this.optionCollectionMap, ); - return data; + return { failureStatus: false, data: data }; } static getPlatformList(projectName, params) { From 7b4c894f2e3d280fada22a541aeb6cf009683356 Mon Sep 17 00:00:00 2001 From: Florin Bilt Date: Tue, 7 Oct 2025 19:41:05 +0300 Subject: [PATCH 5/5] fix lint error --- ui/models/perfSeries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/models/perfSeries.js b/ui/models/perfSeries.js index f7f3391be0c..947347f4aa8 100644 --- a/ui/models/perfSeries.js +++ b/ui/models/perfSeries.js @@ -127,7 +127,7 @@ export default class PerfSeriesModel { ) { return { failureStatus: true, data: ['No data for this job'] }; } - const data = response.data; + const { data } = response; if (!this.optionCollectionMap) { this.optionCollectionMap = await OptionCollectionModel.getMap(); @@ -140,7 +140,7 @@ export default class PerfSeriesModel { this.optionCollectionMap, ); - return { failureStatus: false, data: data }; + return { failureStatus: false, data }; } static getPlatformList(projectName, params) {