Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/ui/job-view/App_test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/job-view/Filtering_test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/job-view/details/PinBoard_test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
56 changes: 56 additions & 0 deletions treeherder/webapp/api/performance_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions treeherder/webapp/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
120 changes: 46 additions & 74 deletions ui/job-view/details/DetailsPanel.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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([
Expand Down
31 changes: 31 additions & 0 deletions ui/models/perfSeries.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down