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/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..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'; @@ -193,86 +192,59 @@ 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.failureStatus + ? rowOrResponse.data + : rowOrResponse; + if (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..947347f4aa8 100644 --- a/ui/models/perfSeries.js +++ b/ui/models/perfSeries.js @@ -112,6 +112,37 @@ export default class PerfSeriesModel { return { data, failureStatus: null }; } + 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; + + if (!this.optionCollectionMap) { + this.optionCollectionMap = await OptionCollectionModel.getMap(); + } + + data.signature_data = await getSeriesSummary( + projectName, + data.signature_data.id, + data.signature_data, + this.optionCollectionMap, + ); + + return { failureStatus: false, data }; + } + static getPlatformList(projectName, params) { return getData( `${getProjectUrl(