зеркало из https://github.com/mozilla/treeherder.git
Bug 1509216 - Convert Perfherder compare and comparesubtests views to react (#4811)
Move compare files to new directory, set up validation HOC and create components for compare view and compare subtests views
This commit is contained in:
Родитель
03bea62b50
Коммит
1f6809a751
|
@ -6,7 +6,7 @@ import {
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from 'react-testing-library';
|
} from 'react-testing-library';
|
||||||
|
|
||||||
import CompareTableControls from '../../../../ui/perfherder/CompareTableControls';
|
import CompareTableControls from '../../../../ui/perfherder/compare/CompareTableControls';
|
||||||
import { filterText } from '../../../../ui/perfherder/constants';
|
import { filterText } from '../../../../ui/perfherder/constants';
|
||||||
|
|
||||||
// TODO addtional tests:
|
// TODO addtional tests:
|
||||||
|
|
|
@ -98,28 +98,3 @@ ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 999;
|
|
||||||
height: 2em;
|
|
||||||
width: 2em;
|
|
||||||
overflow: show;
|
|
||||||
margin: auto;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* overlay */
|
|
||||||
.loading:before {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(20, 19, 19, 0.3);
|
|
||||||
}
|
|
||||||
|
|
|
@ -458,7 +458,7 @@ h4 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-width-default {
|
.max-width-default {
|
||||||
max-width: 1200px;
|
max-width: 1250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom widths for compare table cells */
|
/* Custom widths for compare table cells */
|
||||||
|
@ -481,3 +481,32 @@ h4 {
|
||||||
.dropdown-menu-height {
|
.dropdown-menu-height {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999;
|
||||||
|
height: 2em;
|
||||||
|
width: 2em;
|
||||||
|
overflow: show;
|
||||||
|
margin: auto;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overlay */
|
||||||
|
.loading:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(20, 19, 19, 0.3);
|
||||||
|
}
|
||||||
|
|
|
@ -52,16 +52,14 @@ import './js/perf';
|
||||||
|
|
||||||
// Perf JS
|
// Perf JS
|
||||||
import './js/filters';
|
import './js/filters';
|
||||||
import './js/controllers/perf/compare';
|
|
||||||
import './js/controllers/perf/graphs';
|
import './js/controllers/perf/graphs';
|
||||||
import './js/controllers/perf/alerts';
|
import './js/controllers/perf/alerts';
|
||||||
import './js/components/perf/compare';
|
|
||||||
import './js/components/loading';
|
import './js/components/loading';
|
||||||
import './js/perfapp';
|
import './js/perfapp';
|
||||||
import './perfherder/CompareSelectorView';
|
import './perfherder/compare/CompareSelectorView';
|
||||||
import './perfherder/RevisionInformation';
|
import './perfherder/compare/CompareView';
|
||||||
import './perfherder/CompareSubtestDistributionView';
|
import './perfherder/compare/CompareSubtestDistributionView';
|
||||||
import './perfherder/CompareTableControls';
|
import './perfherder/compare/CompareSubtestsView';
|
||||||
|
|
||||||
config.showMissingIcons = true;
|
config.showMissingIcons = true;
|
||||||
|
|
||||||
|
|
|
@ -221,7 +221,10 @@ export const phAlertStatusMap = {
|
||||||
CONFIRMING: { id: 5, text: 'confirming' },
|
CONFIRMING: { id: 5, text: 'confirming' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const compareDefaultTimeRange = 86400 * 2;
|
export const compareDefaultTimeRange = {
|
||||||
|
value: 86400 * 2,
|
||||||
|
text: 'Last 2 days',
|
||||||
|
};
|
||||||
|
|
||||||
export const thBugSuggestionLimit = 20;
|
export const thBugSuggestionLimit = 20;
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import treeherder from '../../treeherder';
|
|
||||||
import compareErrorTemplate from '../../../partials/perf/comparerror.html';
|
|
||||||
|
|
||||||
treeherder.component('compareError', {
|
|
||||||
template: compareErrorTemplate,
|
|
||||||
bindings: {
|
|
||||||
errors: '<',
|
|
||||||
originalProject: '<',
|
|
||||||
originalRevision: '<',
|
|
||||||
newProject: '<',
|
|
||||||
newRevision: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,597 +0,0 @@
|
||||||
// Remove the eslint-disable when rewriting this file during the React conversion.
|
|
||||||
/* eslint-disable func-names, object-shorthand, prefer-template, radix */
|
|
||||||
import difference from 'lodash/difference';
|
|
||||||
|
|
||||||
import perf from '../../perf';
|
|
||||||
import { endpoints, noiseMetricTitle } from '../../../perfherder/constants';
|
|
||||||
import {
|
|
||||||
phTimeRanges,
|
|
||||||
compareDefaultTimeRange,
|
|
||||||
} from '../../../helpers/constants';
|
|
||||||
import PushModel from '../../../models/push';
|
|
||||||
import RepositoryModel from '../../../models/repository';
|
|
||||||
import { getCounterMap, getInterval, validateQueryParams, getGraphsLink } from '../../../perfherder/helpers';
|
|
||||||
import { getApiUrl, createApiUrl, perfSummaryEndpoint } from '../../../helpers/url';
|
|
||||||
import { getData } from '../../../helpers/http';
|
|
||||||
|
|
||||||
const createNoiseMetric = (cmap, name, compareResults) => {
|
|
||||||
cmap.name = name;
|
|
||||||
cmap.isNoiseMetric = true;
|
|
||||||
|
|
||||||
if (compareResults.has(noiseMetricTitle)) {
|
|
||||||
compareResults.get(noiseMetricTitle).push(cmap);
|
|
||||||
} else {
|
|
||||||
compareResults.set(noiseMetricTitle, [cmap]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyRevision(project, revision, rsid, $scope) {
|
|
||||||
const { data, failureStatus } = await PushModel.getList({ repo: project.name, commit_revision: revision })
|
|
||||||
if (failureStatus) {
|
|
||||||
return $scope.errors.push(data);
|
|
||||||
}
|
|
||||||
if (!data.results.length) {
|
|
||||||
return $scope.errors.push('No results found for this revision');
|
|
||||||
}
|
|
||||||
const resultSet = data.results[0];
|
|
||||||
// TODO: this is a bit hacky to pass in 'original' as a text string
|
|
||||||
if (rsid === 'original') {
|
|
||||||
$scope.originalResultSet = resultSet;
|
|
||||||
} else {
|
|
||||||
$scope.newResultSet = resultSet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
perf.controller('CompareResultsCtrl', [
|
|
||||||
'$state', '$stateParams', '$scope',
|
|
||||||
'$httpParamSerializer', '$q',
|
|
||||||
function CompareResultsCtrl($state, $stateParams, $scope,
|
|
||||||
$httpParamSerializer, $q) {
|
|
||||||
function displayResults(rawResultsMap, newRawResultsMap) {
|
|
||||||
|
|
||||||
$scope.compareResults = new Map();
|
|
||||||
$scope.titles = {};
|
|
||||||
if ($scope.originalRevision) {
|
|
||||||
window.document.title = `Comparison between ${$scope.originalRevision} (${$scope.originalProject.name}) and ${$scope.newRevision} (${$scope.newProject.name})`;
|
|
||||||
} else {
|
|
||||||
window.document.title = `Comparison between ${$scope.originalProject.name} and ${$scope.newRevision} (${$scope.newProject.name})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.oldStddevVariance = {};
|
|
||||||
$scope.newStddevVariance = {};
|
|
||||||
$scope.testsTooVariable = [{ platform: 'Platform', testname: 'Testname', baseStddev: 'Base Stddev', newStddev: 'New Stddev' }];
|
|
||||||
|
|
||||||
$scope.testList.forEach(function (testName) {
|
|
||||||
$scope.titles[testName] = testName;
|
|
||||||
$scope.platformList.forEach(function (platform) {
|
|
||||||
if (Object.keys($scope.oldStddevVariance).indexOf(platform) < 0) {
|
|
||||||
$scope.oldStddevVariance[platform] = { values: [], lowerIsBetter: true, frameworkID: $scope.filterOptions.framework.id };
|
|
||||||
}
|
|
||||||
if (Object.keys($scope.newStddevVariance).indexOf(platform) < 0) {
|
|
||||||
$scope.newStddevVariance[platform] = { values: [], lowerIsBetter: true, frameworkID: $scope.filterOptions.framework.id };
|
|
||||||
}
|
|
||||||
const oldResults = rawResultsMap.find(sig =>
|
|
||||||
sig.name === testName && sig.platform === platform
|
|
||||||
);
|
|
||||||
const newResults = newRawResultsMap.find(sig =>
|
|
||||||
sig.name === testName && sig.platform === platform
|
|
||||||
);
|
|
||||||
const cmap = getCounterMap(testName, oldResults, newResults);
|
|
||||||
if (cmap.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No results for one or both data points
|
|
||||||
if (cmap.originalStddevPct !== undefined && cmap.newStddevPct !== undefined) {
|
|
||||||
// TODO: ideally anything >10.0 is bad, but should we ignore anything?
|
|
||||||
if (cmap.originalStddevPct < 50.0 && cmap.newStddevPct < 50.0) {
|
|
||||||
$scope.oldStddevVariance[platform].values.push(Math.round(cmap.originalStddevPct * 100) / 100);
|
|
||||||
$scope.newStddevVariance[platform].values.push(Math.round(cmap.newStddevPct * 100) / 100);
|
|
||||||
} else {
|
|
||||||
$scope.testsTooVariable.push({ platform: platform, testname: testName, baseStddev: cmap.originalStddevPct, newStddev: cmap.newStddevPct });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmap.links = [];
|
|
||||||
const hasSubtests = ((oldResults && oldResults.has_subtests) || (newResults && newResults.has_subtests));
|
|
||||||
|
|
||||||
if ($scope.originalRevision) {
|
|
||||||
if (hasSubtests) {
|
|
||||||
let detailsLink = 'perf.html#/comparesubtest?';
|
|
||||||
detailsLink += $httpParamSerializer({
|
|
||||||
originalProject: $scope.originalProject.name,
|
|
||||||
originalRevision: $scope.originalRevision,
|
|
||||||
newProject: $scope.newProject.name,
|
|
||||||
newRevision: $scope.newRevision,
|
|
||||||
originalSignature: oldResults ? oldResults.signature_id : null,
|
|
||||||
newSignature: newResults ? newResults.signature_id : null,
|
|
||||||
framework: $scope.filterOptions.framework.id,
|
|
||||||
});
|
|
||||||
cmap.links.push({
|
|
||||||
title: 'subtests',
|
|
||||||
href: detailsLink,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cmap.links.push({
|
|
||||||
title: 'graph',
|
|
||||||
href: getGraphsLink([...new Set(
|
|
||||||
[$scope.originalProject, $scope.newProject])].map(project => ({
|
|
||||||
projectName: project.name,
|
|
||||||
signature: !oldResults ? newResults.signature_hash : oldResults.signature_hash,
|
|
||||||
frameworkId: $scope.filterOptions.framework.id,
|
|
||||||
})),
|
|
||||||
[$scope.originalResultSet, $scope.newResultSet]),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (hasSubtests) {
|
|
||||||
let detailsLink = 'perf.html#/comparesubtest?';
|
|
||||||
detailsLink += $httpParamSerializer({
|
|
||||||
originalProject: $scope.originalProject.name,
|
|
||||||
newProject: $scope.newProject.name,
|
|
||||||
newRevision: $scope.newRevision,
|
|
||||||
originalSignature: oldResults ? oldResults.signature_id : null,
|
|
||||||
newSignature: newResults ? newResults.signature_id : null,
|
|
||||||
framework: $scope.filterOptions.framework.id,
|
|
||||||
selectedTimeRange: $scope.selectedTimeRange.value,
|
|
||||||
});
|
|
||||||
cmap.links.push({
|
|
||||||
title: 'subtests',
|
|
||||||
href: detailsLink,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
cmap.links.push({
|
|
||||||
title: 'graph',
|
|
||||||
href: getGraphsLink([...new Set(
|
|
||||||
[$scope.originalProject, $scope.newProject])].map(project => ({
|
|
||||||
projectName: project.name,
|
|
||||||
signature: !oldResults ? newResults.signature_hash : oldResults.signature_hash,
|
|
||||||
frameworkId: $scope.filterOptions.framework.id,
|
|
||||||
})),
|
|
||||||
[$scope.newResultSet], $scope.selectedTimeRange.value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cmap.name = platform;
|
|
||||||
if ($scope.compareResults.has(testName)) {
|
|
||||||
$scope.compareResults.get(testName).push(cmap);
|
|
||||||
} else {
|
|
||||||
$scope.compareResults.set(testName, [cmap]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.platformList.forEach(function (platform) {
|
|
||||||
const cmap = getCounterMap(noiseMetricTitle, $scope.oldStddevVariance[platform], $scope.newStddevVariance[platform]);
|
|
||||||
if (cmap.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createNoiseMetric(cmap, platform, $scope.compareResults);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove the tests with no data, report them as well; not needed for subtests
|
|
||||||
const resultsArr = Array.from($scope.compareResults.keys());
|
|
||||||
$scope.testNoResults = difference($scope.testList, resultsArr).sort().join();
|
|
||||||
$scope.testList = resultsArr.sort().concat([noiseMetricTitle]);
|
|
||||||
$scope.compareResults = new Map([...$scope.compareResults.entries()].sort());
|
|
||||||
|
|
||||||
// call $apply explicitly so we don't have to worry about when promises
|
|
||||||
// get resolved (see bug 1470600)
|
|
||||||
$scope.$apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
const createQueryParams = (repository, interval) => ({
|
|
||||||
repository,
|
|
||||||
framework: $scope.filterOptions.framework.id,
|
|
||||||
interval,
|
|
||||||
no_subtests: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
$scope.dataLoading = true;
|
|
||||||
$scope.testList = [];
|
|
||||||
$scope.platformList = [];
|
|
||||||
let originalParams;
|
|
||||||
let interval;
|
|
||||||
|
|
||||||
if ($scope.originalRevision) {
|
|
||||||
interval = getInterval($scope.originalResultSet.push_timestamp, $scope.newResultSet.push_timestamp);
|
|
||||||
originalParams = createQueryParams($scope.originalProject.name, interval);
|
|
||||||
originalParams.revision = $scope.originalResultSet.revision;
|
|
||||||
} else {
|
|
||||||
interval = $scope.selectedTimeRange.value;
|
|
||||||
const startDateMs = ($scope.newResultSet.push_timestamp - interval) * 1000;
|
|
||||||
const endDateMs = $scope.newResultSet.push_timestamp * 1000;
|
|
||||||
|
|
||||||
originalParams = createQueryParams($scope.originalProject.name, interval);
|
|
||||||
originalParams.startday = new Date(startDateMs).toISOString().slice(0, -5);
|
|
||||||
originalParams.endday = new Date(endDateMs).toISOString().slice(0, -5);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newParams = createQueryParams($scope.newProject.name, interval);
|
|
||||||
newParams.revision = $scope.newResultSet.revision;
|
|
||||||
|
|
||||||
const [originalResults, newResults] = await Promise.all([getData(createApiUrl(perfSummaryEndpoint, originalParams)),
|
|
||||||
getData(createApiUrl(perfSummaryEndpoint, newParams))]);
|
|
||||||
|
|
||||||
$scope.dataLoading = false;
|
|
||||||
|
|
||||||
const data = [...originalResults.data, ...newResults.data];
|
|
||||||
|
|
||||||
$scope.platformList = [...new Set(data.map(item => item.platform))].sort();
|
|
||||||
$scope.testList = [...new Set(data.map(item => item.name))].sort();
|
|
||||||
|
|
||||||
return displayResults(originalResults.data, newResults.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateURL() {
|
|
||||||
const params = {
|
|
||||||
framework: $scope.filterOptions.framework.id,
|
|
||||||
filter: $scope.filterOptions.filter,
|
|
||||||
showOnlyImportant: $scope.filterOptions.showOnlyImportant ? 1 : undefined,
|
|
||||||
showOnlyComparable: $scope.filterOptions.showOnlyComparable ? 1 : undefined,
|
|
||||||
showOnlyConfident: $scope.filterOptions.showOnlyConfident ? 1 : undefined,
|
|
||||||
showOnlyNoise: $scope.filterOptions.showOnlyNoise ? 1 : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($scope.originalRevision === undefined) {
|
|
||||||
params.selectedTimeRange = $scope.selectedTimeRange.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$state.transitionTo('compare', params, {
|
|
||||||
location: true,
|
|
||||||
inherit: true,
|
|
||||||
relative: $state.$current,
|
|
||||||
notify: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.updateData = function(framework) {
|
|
||||||
$scope.filterOptions.framework = framework;
|
|
||||||
updateURL();
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.updateNoiseAlert = function() {
|
|
||||||
$scope.filterOptions.showOnlyNoise = !$scope.filterOptions.showOnlyNoise;
|
|
||||||
$scope.$apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.timeRangeChanged = function (selectedTimeRange) {
|
|
||||||
// This function is used to alter
|
|
||||||
// $scope.selectedTimeRange for baseline comparison.
|
|
||||||
// selectedTimeRange is passed as parameter
|
|
||||||
// because angular assigns it to a different scope
|
|
||||||
$scope.selectedTimeRange = selectedTimeRange;
|
|
||||||
updateURL();
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
$scope.dataLoading = true;
|
|
||||||
|
|
||||||
const loadRepositories = RepositoryModel.getList();
|
|
||||||
const loadFrameworks = getData(getApiUrl(endpoints.frameworks)).then(({ data: frameworks }) => {
|
|
||||||
$scope.frameworks = frameworks;
|
|
||||||
});
|
|
||||||
|
|
||||||
$q.all([loadRepositories, loadFrameworks]).then(async function ([repos]) {
|
|
||||||
$scope.errors = [];
|
|
||||||
// validation works only for revision to revision comparison
|
|
||||||
if ($stateParams.originalRevision) {
|
|
||||||
$scope.errors = await validateQueryParams($stateParams);
|
|
||||||
|
|
||||||
if ($scope.errors.length > 0) {
|
|
||||||
$scope.dataLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$scope.filterOptions = {
|
|
||||||
framework: $scope.frameworks.find(fw =>
|
|
||||||
fw.id === parseInt($stateParams.framework),
|
|
||||||
) || $scope.frameworks[0],
|
|
||||||
filter: $stateParams.filter || '',
|
|
||||||
showOnlyImportant: Boolean($stateParams.showOnlyImportant !== undefined &&
|
|
||||||
parseInt($stateParams.showOnlyImportant)),
|
|
||||||
showOnlyComparable: Boolean($stateParams.showOnlyComparable !== undefined &&
|
|
||||||
parseInt($stateParams.showOnlyComparable)),
|
|
||||||
showOnlyConfident: Boolean($stateParams.showOnlyConfident !== undefined &&
|
|
||||||
parseInt($stateParams.showOnlyConfident)),
|
|
||||||
showOnlyNoise: Boolean($stateParams.showOnlyNoise !== undefined &&
|
|
||||||
parseInt($stateParams.showOnlyNoise)),
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.originalProject = RepositoryModel.getRepo(
|
|
||||||
$stateParams.originalProject, repos);
|
|
||||||
$scope.newProject = RepositoryModel.getRepo(
|
|
||||||
$stateParams.newProject, repos);
|
|
||||||
$scope.newRevision = $stateParams.newRevision;
|
|
||||||
|
|
||||||
// always need to verify the new revision, only sometimes the original
|
|
||||||
const verifyPromises = [verifyRevision($scope.newProject, $scope.newRevision, 'new', $scope)];
|
|
||||||
if ($stateParams.originalRevision) {
|
|
||||||
$scope.originalRevision = $stateParams.originalRevision;
|
|
||||||
verifyPromises.push(verifyRevision($scope.originalProject, $scope.originalRevision, 'original', $scope));
|
|
||||||
} else {
|
|
||||||
$scope.timeRanges = phTimeRanges;
|
|
||||||
$scope.selectedTimeRange = $scope.timeRanges.find(timeRange =>
|
|
||||||
timeRange.value === ($stateParams.selectedTimeRange ? parseInt($stateParams.selectedTimeRange) : compareDefaultTimeRange),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$q.all(verifyPromises).then(function () {
|
|
||||||
if ($scope.errors.length > 0) {
|
|
||||||
$scope.dataLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$scope.$watchGroup(['filterOptions.filter',
|
|
||||||
'filterOptions.showOnlyImportant',
|
|
||||||
'filterOptions.showOnlyComparable',
|
|
||||||
'filterOptions.showOnlyConfident',
|
|
||||||
'filterOptions.showOnlyNoise'],
|
|
||||||
updateURL);
|
|
||||||
|
|
||||||
$scope.$watch('filterOptions.framework',
|
|
||||||
function (newValue, oldValue) {
|
|
||||||
if (newValue.id !== oldValue.id) {
|
|
||||||
updateURL();
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
load();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}]);
|
|
||||||
|
|
||||||
perf.controller('CompareSubtestResultsCtrl', [
|
|
||||||
'$state', '$stateParams', '$scope', '$q',
|
|
||||||
'$httpParamSerializer',
|
|
||||||
function CompareSubtestResultsCtrl($state, $stateParams, $scope, $q,
|
|
||||||
$httpParamSerializer) {
|
|
||||||
|
|
||||||
function displayResults(rawResultsMap, newRawResultsMap) {
|
|
||||||
|
|
||||||
$scope.compareResults = new Map();
|
|
||||||
$scope.titles = {};
|
|
||||||
|
|
||||||
const testName = $scope.subtestTitle;
|
|
||||||
|
|
||||||
$scope.titles[testName] = `${$scope.platformList[0]}: ${testName}`;
|
|
||||||
|
|
||||||
$scope.subtestTitle = $scope.titles[testName];
|
|
||||||
window.document.title = $scope.subtestTitle;
|
|
||||||
|
|
||||||
$scope.oldStddevVariance = { values: [], lowerIsBetter: true, frameworkID: $scope.filterOptions.framework.id };
|
|
||||||
$scope.newStddevVariance = { values: [], lowerIsBetter: true, frameworkID: $scope.filterOptions.framework.id };
|
|
||||||
$scope.testsTooVariable = [{ testName: 'Testname', baseStddev: 'Base Stddev', newStddev: 'New Stddev' }];
|
|
||||||
$scope.pageList.sort();
|
|
||||||
$scope.pageList.forEach(function (page) {
|
|
||||||
const mapsigs = [];
|
|
||||||
[rawResultsMap, newRawResultsMap].forEach(function (resultsMap) {
|
|
||||||
let tempsig;
|
|
||||||
// If no data for a given platform, or test, display N/A in table
|
|
||||||
if (resultsMap) {
|
|
||||||
tempsig = resultsMap.find(sig => sig.test === page);
|
|
||||||
} else {
|
|
||||||
tempsig = 'undefined';
|
|
||||||
resultsMap = {};
|
|
||||||
resultsMap[tempsig] = {};
|
|
||||||
}
|
|
||||||
mapsigs.push(tempsig);
|
|
||||||
});
|
|
||||||
const oldData = mapsigs[0];
|
|
||||||
const newData = mapsigs[1];
|
|
||||||
|
|
||||||
const cmap = getCounterMap(testName, oldData, newData);
|
|
||||||
if ((oldData && oldData.parent_signature === $scope.originalSignature) ||
|
|
||||||
(oldData && oldData.parent_signature === $scope.newSignature) ||
|
|
||||||
newData.parent_signature === $scope.originalSignature ||
|
|
||||||
newData.parent_signature === $scope.newSignature) {
|
|
||||||
cmap.highlightedTest = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No results for one or both data points
|
|
||||||
if (cmap.originalStddevPct !== undefined && cmap.newStddevPct !== undefined) {
|
|
||||||
// TODO: ideally anything >10.0 is bad, but should we ignore anything?
|
|
||||||
if (cmap.originalStddevPct < 50.0 && cmap.newStddevPct < 50.0) {
|
|
||||||
$scope.oldStddevVariance.values.push(Math.round(cmap.originalStddevPct * 100) / 100);
|
|
||||||
$scope.newStddevVariance.values.push(Math.round(cmap.newStddevPct * 100) / 100);
|
|
||||||
} else {
|
|
||||||
$scope.testsTooVariable.push({ testname: page, basStddev: cmap.originalStddevPct, newStddev: cmap.newStddevPct });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmap.name = page;
|
|
||||||
if ($scope.originalRevision) {
|
|
||||||
cmap.links = [{
|
|
||||||
title: 'graph',
|
|
||||||
href: getGraphsLink([...new Set([
|
|
||||||
$scope.originalProject,
|
|
||||||
$scope.newProject,
|
|
||||||
])].map(project => ({
|
|
||||||
projectName: project.name,
|
|
||||||
signature: !oldData ? newData.signature_id : oldData.signature_id,
|
|
||||||
frameworkId: $scope.filterOptions.framework,
|
|
||||||
})), [$scope.originalResultSet, $scope.newResultSet]),
|
|
||||||
}];
|
|
||||||
// replicate distribution is added only for talos
|
|
||||||
if ($scope.filterOptions.framework === '1') {
|
|
||||||
cmap.links.push({
|
|
||||||
title: 'replicates',
|
|
||||||
href: 'perf.html#/comparesubtestdistribution?' + $httpParamSerializer({
|
|
||||||
originalProject: $scope.originalProject.name,
|
|
||||||
newProject: $scope.newProject.name,
|
|
||||||
originalRevision: $scope.originalRevision,
|
|
||||||
newRevision: $scope.newRevision,
|
|
||||||
originalSubtestSignature: oldData ? oldData.signature_id : null,
|
|
||||||
newSubtestSignature: newData ? newData.signature_id: null,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cmap.links = [{
|
|
||||||
title: 'graph',
|
|
||||||
href: getGraphsLink([...new Set([
|
|
||||||
$scope.originalProject,
|
|
||||||
$scope.newProject,
|
|
||||||
])].map(project => ({
|
|
||||||
projectName: project.name,
|
|
||||||
signature: !oldData ? newData.signature_id : oldData.signature_id,
|
|
||||||
frameworkId: $scope.filterOptions.framework,
|
|
||||||
})), [$scope.newResultSet], $scope.selectedTimeRange.value),
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
if ($scope.compareResults.has(testName)) {
|
|
||||||
$scope.compareResults.get(testName).push(cmap);
|
|
||||||
} else {
|
|
||||||
$scope.compareResults.set(testName, [cmap]);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const cmap = getCounterMap(noiseMetricTitle, $scope.oldStddevVariance, $scope.newStddevVariance);
|
|
||||||
if (!cmap.isEmpty) {
|
|
||||||
createNoiseMetric(cmap, testName, $scope.compareResults)
|
|
||||||
}
|
|
||||||
|
|
||||||
// call $apply explicitly so we don't have to worry about when promises
|
|
||||||
// get resolved (see bug 1470600)
|
|
||||||
$scope.$apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.updateNoiseAlert = function() {
|
|
||||||
$scope.filterOptions.showOnlyNoise = !$scope.filterOptions.showOnlyNoise;
|
|
||||||
$scope.$apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchData() {
|
|
||||||
const createQueryParams = (parent_signature, repository) => ({
|
|
||||||
parent_signature,
|
|
||||||
framework: $scope.filterOptions.framework,
|
|
||||||
repository,
|
|
||||||
});
|
|
||||||
|
|
||||||
const originalParams = createQueryParams($scope.originalSignature, $scope.originalProject.name);
|
|
||||||
|
|
||||||
if ($scope.originalRevision) {
|
|
||||||
originalParams.revision = $scope.originalResultSet.revision;
|
|
||||||
} else {
|
|
||||||
// TODO create a helper for the startday and endday since this is also used in compare view
|
|
||||||
const startDateMs = ($scope.newResultSet.push_timestamp -
|
|
||||||
$scope.selectedTimeRange.value) * 1000;
|
|
||||||
const endDateMs = $scope.newResultSet.push_timestamp * 1000;
|
|
||||||
|
|
||||||
originalParams.startday = new Date(startDateMs).toISOString().slice(0, -5);
|
|
||||||
originalParams.endday = new Date(endDateMs).toISOString().slice(0, -5);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newParams = createQueryParams($scope.newSignature, $scope.newProject.name);
|
|
||||||
newParams.revision = $scope.newResultSet.revision;
|
|
||||||
|
|
||||||
const [originalResults, newResults] = await Promise.all([getData(createApiUrl(perfSummaryEndpoint, originalParams)),
|
|
||||||
getData(createApiUrl(perfSummaryEndpoint, newParams))]);
|
|
||||||
|
|
||||||
$scope.dataLoading = false;
|
|
||||||
|
|
||||||
const results = [...originalResults.data, ...newResults.data];
|
|
||||||
|
|
||||||
const subtestName = results[0].name.split(' ');
|
|
||||||
subtestName.splice(1, 1);
|
|
||||||
$scope.subtestTitle = subtestName.join(' ');
|
|
||||||
|
|
||||||
$scope.pageList = [...new Set(results.map(subtest => subtest.test))].sort();
|
|
||||||
$scope.platformList = [...new Set(results.map(subtest => subtest.platform))].sort();
|
|
||||||
|
|
||||||
return displayResults(originalResults.data, newResults.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.dataLoading = true;
|
|
||||||
|
|
||||||
RepositoryModel.getList().then(async (repos) => {
|
|
||||||
$scope.errors = [];
|
|
||||||
if ($stateParams.originalRevision) {
|
|
||||||
$scope.errors = await validateQueryParams($stateParams);
|
|
||||||
|
|
||||||
if ($scope.errors.length > 0) {
|
|
||||||
$scope.dataLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.originalProject = RepositoryModel.getRepo(
|
|
||||||
$stateParams.originalProject, repos);
|
|
||||||
$scope.newProject = RepositoryModel.getRepo(
|
|
||||||
$stateParams.newProject, repos);
|
|
||||||
$scope.newRevision = $stateParams.newRevision;
|
|
||||||
$scope.originalSignature = $stateParams.originalSignature;
|
|
||||||
$scope.newSignature = $stateParams.newSignature;
|
|
||||||
|
|
||||||
// always need to verify the new revision, only sometimes the original
|
|
||||||
const verifyPromises = [verifyRevision($scope.newProject, $scope.newRevision, 'new', $scope)];
|
|
||||||
if ($stateParams.originalRevision) {
|
|
||||||
$scope.originalRevision = $stateParams.originalRevision;
|
|
||||||
verifyPromises.push(verifyRevision($scope.originalProject, $scope.originalRevision, 'original', $scope));
|
|
||||||
} else {
|
|
||||||
$scope.timeRanges = phTimeRanges;
|
|
||||||
$scope.selectedTimeRange = $scope.timeRanges.find(timeRange =>
|
|
||||||
timeRange.value === ($stateParams.selectedTimeRange ? parseInt($stateParams.selectedTimeRange) : compareDefaultTimeRange),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$q.all(verifyPromises).then(function () {
|
|
||||||
$scope.pageList = [];
|
|
||||||
|
|
||||||
if ($scope.errors.length > 0) {
|
|
||||||
$scope.dataLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.filterOptions = {
|
|
||||||
framework: $stateParams.framework || 1, // 1 == talos
|
|
||||||
filter: $stateParams.filter || '',
|
|
||||||
showOnlyImportant: Boolean($stateParams.showOnlyImportant !== undefined &&
|
|
||||||
parseInt($stateParams.showOnlyImportant)),
|
|
||||||
showOnlyComparable: Boolean($stateParams.showOnlyComparable !== undefined &&
|
|
||||||
parseInt($stateParams.showOnlyComparable)),
|
|
||||||
showOnlyConfident: Boolean($stateParams.showOnlyConfident !== undefined &&
|
|
||||||
parseInt($stateParams.showOnlyConfident)),
|
|
||||||
showOnlyNoise: Boolean($stateParams.showOnlyNoise !== undefined &&
|
|
||||||
parseInt($stateParams.showOnlyNoise)),
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.$watchGroup([
|
|
||||||
'filterOptions.filter',
|
|
||||||
'filterOptions.showOnlyImportant',
|
|
||||||
'filterOptions.showOnlyComparable',
|
|
||||||
'filterOptions.showOnlyConfident',
|
|
||||||
'filterOptions.showOnlyNoise',
|
|
||||||
], function () {
|
|
||||||
$state.transitionTo('comparesubtest', {
|
|
||||||
filter: $scope.filterOptions.filter,
|
|
||||||
showOnlyImportant: $scope.filterOptions.showOnlyImportant ? 1 : undefined,
|
|
||||||
showOnlyComparable: $scope.filterOptions.showOnlyComparable ? 1 : undefined,
|
|
||||||
showOnlyConfident: $scope.filterOptions.showOnlyConfident ? 1 : undefined,
|
|
||||||
showOnlyNoise: $scope.filterOptions.showOnlyNoise ? 1 : undefined,
|
|
||||||
}, {
|
|
||||||
location: true,
|
|
||||||
inherit: true,
|
|
||||||
relative: $state.$current,
|
|
||||||
notify: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.timeRangeChanged = function (selectedTimeRange) {
|
|
||||||
// This function is used to alter
|
|
||||||
// $scope.selectedTimeRange for baseline comparison.
|
|
||||||
// selectedTimeRange is passed as parameter
|
|
||||||
// because angular assigns it to a different scope
|
|
||||||
$scope.selectedTimeRange = selectedTimeRange;
|
|
||||||
$state.go('comparesubtest', {
|
|
||||||
filter: $scope.filterOptions.filter,
|
|
||||||
showOnlyImportant: $scope.filterOptions.showOnlyImportant ? 1 : undefined,
|
|
||||||
showOnlyComparable: $scope.filterOptions.showOnlyComparable ? 1 : undefined,
|
|
||||||
showOnlyConfident: $scope.filterOptions.showOnlyConfident ? 1 : undefined,
|
|
||||||
selectedTimeRange: $scope.selectedTimeRange.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}]);
|
|
|
@ -44,13 +44,11 @@ perf.config(['$compileProvider', '$locationProvider', '$httpProvider', '$statePr
|
||||||
title: 'Compare',
|
title: 'Compare',
|
||||||
template: compareCtrlTemplate,
|
template: compareCtrlTemplate,
|
||||||
url: '/compare?originalProject&originalRevision?&newProject&newRevision&hideMinorChanges&framework&filter&showOnlyComparable&showOnlyImportant&showOnlyConfident&selectedTimeRange&showOnlyNoise?',
|
url: '/compare?originalProject&originalRevision?&newProject&newRevision&hideMinorChanges&framework&filter&showOnlyComparable&showOnlyImportant&showOnlyConfident&selectedTimeRange&showOnlyNoise?',
|
||||||
controller: 'CompareResultsCtrl',
|
|
||||||
})
|
})
|
||||||
.state('comparesubtest', {
|
.state('comparesubtest', {
|
||||||
title: 'Compare - Subtests',
|
title: 'Compare - Subtests',
|
||||||
template: compareSubtestCtrlTemplate,
|
template: compareSubtestCtrlTemplate,
|
||||||
url: '/comparesubtest?originalProject&originalRevision?&newProject&newRevision&originalSignature&newSignature&filter&showOnlyComparable&showOnlyImportant&showOnlyConfident&framework&selectedTimeRange&showOnlyNoise?',
|
url: '/comparesubtest?originalProject&originalRevision?&newProject&newRevision&originalSignature&newSignature&filter&showOnlyComparable&showOnlyImportant&showOnlyConfident&framework&selectedTimeRange&showOnlyNoise?',
|
||||||
controller: 'CompareSubtestResultsCtrl',
|
|
||||||
})
|
})
|
||||||
.state('comparechooser', {
|
.state('comparechooser', {
|
||||||
title: 'Compare Chooser',
|
title: 'Compare Chooser',
|
||||||
|
|
|
@ -1,50 +1 @@
|
||||||
<div class="container-fluid">
|
<compare-view />
|
||||||
<div id="datapoint-detail" ng-show="newRevision">
|
|
||||||
<div ng-if="dataLoading">
|
|
||||||
Loading all results, please wait a minute...
|
|
||||||
<img src="../../img/dancing_cat.gif" />
|
|
||||||
</div>
|
|
||||||
<div id="error" ng-if="!dataLoading && errors.length">
|
|
||||||
<compare-error errors="errors" original-revision="originalRevision" original-project="originalProject" new-revision="newRevision" new-project="newProject"></compare-error>
|
|
||||||
</div>
|
|
||||||
<div id="subtest-summary" ng-if="!dataLoading && !errors.length">
|
|
||||||
<h1>Perfherder Compare Revisions</h1>
|
|
||||||
<revision-information original-project="originalProject" original-revision="originalRevision" original-result-set="originalResultSet" new-project="newProject" new-revision="newRevision" new-result-set="newResultSet" selected-time-range="selectedTimeRange"></revision-information>
|
|
||||||
<div class="alert alert-warning" role="alert" ng-if="testNoResults">
|
|
||||||
<strong>tests with no results:</strong>
|
|
||||||
<p class="notes-preview bg-light rounded mt-2"
|
|
||||||
ng-text-truncate="testNoResults"
|
|
||||||
ng-tt-chars-threshold="250"
|
|
||||||
ng-tt-more-label="Show"
|
|
||||||
ng-tt-less-label="Hide"></p>
|
|
||||||
</div>
|
|
||||||
<div class="alert alert-warning" role="alert" ng-if="testsTooVariable.length > 1 && filterOptions.showOnlyNoise">
|
|
||||||
<strong>Tests with too much noise to be considered in the noise metric:</strong>
|
|
||||||
<table>
|
|
||||||
<tr ng-repeat="tname in testsTooVariable">
|
|
||||||
<td>{{tname.platform}}</td>
|
|
||||||
<td>{{tname.testname}}</td>
|
|
||||||
<td>{{tname.baseStddev}}</td>
|
|
||||||
<td>{{tname.newStddev}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<div class="form-group" ng-if="!originalRevision">
|
|
||||||
Time range to sample (before push):
|
|
||||||
<select ng-model="selectedTimeRange"
|
|
||||||
ng-options="timeRange.text for timeRange in timeRanges track by timeRange.value"
|
|
||||||
ng-change="timeRangeChanged(selectedTimeRange)">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<compare-table-controls
|
|
||||||
compare-results="compareResults"
|
|
||||||
frameworks="frameworks"
|
|
||||||
filter-options="filterOptions"
|
|
||||||
filter-by-framework="1"
|
|
||||||
update-data="updateData"
|
|
||||||
update-noise-alert="updateNoiseAlert">
|
|
||||||
</compare-table-controls>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
<p ng-repeat="error in errors">
|
|
||||||
<span class="fas fa-exclamation-triangle"></span> {{error}}
|
|
||||||
</p>
|
|
||||||
<p><a class="alert-link" href="#/comparechooser?originalProject={{originalProject}}&originalRevision={{originalRevision}}&newProject={{newProject}}&newRevision={{newRevision}}">Modify choice</p>
|
|
||||||
</div>
|
|
|
@ -1,39 +1 @@
|
||||||
<div class="container-fluid">
|
<compare-subtests-view />
|
||||||
<div ng-if="dataLoading">
|
|
||||||
Loading all results, please wait a minute...
|
|
||||||
<img src="../../img/dancing_cat.gif" />
|
|
||||||
<img src="../../img/dancing_cat.gif" ng-repeat="x in [0,1,2,3]" style="transform:scale(0.5); margin-left:-40px; margin-top:40px;"/>
|
|
||||||
</div>
|
|
||||||
<div id="error" ng-if="!dataLoading && errors.length">
|
|
||||||
<compare-error errors="errors" original-revision="originalRevision" original-project="originalProject" new-revision="newRevision" new-project="newProject"></compare-error>
|
|
||||||
</div>
|
|
||||||
<div class="alert alert-warning" role="alert" ng-if="testsTooVariable.length > 1 && filterOptions.showOnlyNoise">
|
|
||||||
<strong>Tests with too much noise to be considered in the noise metric:</strong>
|
|
||||||
<table>
|
|
||||||
<tr ng-repeat="tname in testsTooVariable">
|
|
||||||
<td>{{tname.testname}}</td>
|
|
||||||
<td>{{tname.baseStddev}}</td>
|
|
||||||
<td>{{tname.newStddev}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="subtest-summary" ng-if="!dataLoading && !errors.length">
|
|
||||||
<h1>{{subtestTitle}} subtest summary</h1>
|
|
||||||
<revision-information original-project="originalProject" original-revision="originalRevision" original-result-set="originalResultSet" new-project="newProject" new-revision="newRevision" new-result-set="newResultSet" selected-time-range="selectedTimeRange"></revision-information>
|
|
||||||
<p><a href="perf.html#/compare?originalProject={{originalProject.name}}&originalRevision={{originalRevision}}&newProject={{newProject.name}}&newRevision={{newRevision}}">Show all tests and platforms</a></p>
|
|
||||||
<div class="form-group" ng-if="!originalRevision">
|
|
||||||
Time range to sample (before push):
|
|
||||||
<select ng-model="selectedTimeRange"
|
|
||||||
ng-options="timeRange.text for timeRange in timeRanges track by timeRange.value"
|
|
||||||
ng-change="timeRangeChanged(selectedTimeRange)">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<compare-table-controls
|
|
||||||
titles="titles"
|
|
||||||
compare-results="compareResults"
|
|
||||||
filter-options="filterOptions"
|
|
||||||
update-data="updateData"
|
|
||||||
update-noise-alert="updateNoiseAlert">
|
|
||||||
</compare-table-controls>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { ButtonDropdown, DropdownToggle } from 'reactstrap';
|
|
||||||
|
|
||||||
import DropdownMenuItems from '../shared/DropdownMenuItems';
|
|
||||||
|
|
||||||
export default class DropdownButton extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isOpen: false,
|
|
||||||
selectedItem: this.props.defaultText,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState = selectedItem => {
|
|
||||||
this.setState({ selectedItem });
|
|
||||||
this.props.updateData(selectedItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { selectedItem, isOpen } = this.state;
|
|
||||||
const { data, defaultTextClass } = this.props;
|
|
||||||
return (
|
|
||||||
<ButtonDropdown
|
|
||||||
className={defaultTextClass}
|
|
||||||
isOpen={isOpen}
|
|
||||||
toggle={() =>
|
|
||||||
this.setState(prevState => ({
|
|
||||||
isOpen: !prevState.isOpen,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DropdownToggle caret>{selectedItem}</DropdownToggle>
|
|
||||||
{data && (
|
|
||||||
<DropdownMenuItems
|
|
||||||
options={data}
|
|
||||||
selectedItem={selectedItem}
|
|
||||||
updateData={this.updateState}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ButtonDropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownButton.propTypes = {
|
|
||||||
data: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
defaultText: PropTypes.string.isRequired,
|
|
||||||
updateData: PropTypes.func.isRequired,
|
|
||||||
defaultTextClass: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
DropdownButton.defaultProps = {
|
|
||||||
defaultTextClass: 'text-nowrap',
|
|
||||||
};
|
|
|
@ -3,16 +3,16 @@ import PropTypes from 'prop-types';
|
||||||
import { react2angular } from 'react2angular/index.es2015';
|
import { react2angular } from 'react2angular/index.es2015';
|
||||||
import { Container, Col, Row, Button } from 'reactstrap';
|
import { Container, Col, Row, Button } from 'reactstrap';
|
||||||
|
|
||||||
import perf from '../js/perf';
|
import perf from '../../js/perf';
|
||||||
import { getApiUrl, repoEndpoint } from '../helpers/url';
|
import { getApiUrl, repoEndpoint } from '../../helpers/url';
|
||||||
import { getData } from '../helpers/http';
|
import { getData } from '../../helpers/http';
|
||||||
import ErrorMessages from '../shared/ErrorMessages';
|
import ErrorMessages from '../../shared/ErrorMessages';
|
||||||
import {
|
import {
|
||||||
compareDefaultTimeRange,
|
compareDefaultTimeRange,
|
||||||
genericErrorMessage,
|
genericErrorMessage,
|
||||||
errorMessageClass,
|
errorMessageClass,
|
||||||
} from '../helpers/constants';
|
} from '../../helpers/constants';
|
||||||
import ErrorBoundary from '../shared/ErrorBoundary';
|
import ErrorBoundary from '../../shared/ErrorBoundary';
|
||||||
|
|
||||||
import SelectorCard from './SelectorCard';
|
import SelectorCard from './SelectorCard';
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ export default class CompareSelectorView extends React.Component {
|
||||||
originalProject,
|
originalProject,
|
||||||
newProject,
|
newProject,
|
||||||
newRevision,
|
newRevision,
|
||||||
selectedTimeRange: compareDefaultTimeRange,
|
selectedTimeRange: compareDefaultTimeRange.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { react2angular } from 'react2angular/index.es2015';
|
import { react2angular } from 'react2angular/index.es2015';
|
||||||
|
import { Container, Row } from 'reactstrap';
|
||||||
|
|
||||||
import perf from '../js/perf';
|
import perf from '../../js/perf';
|
||||||
import RepositoryModel from '../models/repository';
|
import RepositoryModel from '../../models/repository';
|
||||||
import PushModel from '../models/push';
|
import PushModel from '../../models/push';
|
||||||
import { getData } from '../helpers/http';
|
import { getData } from '../../helpers/http';
|
||||||
import { createApiUrl, perfSummaryEndpoint } from '../helpers/url';
|
import { createApiUrl, perfSummaryEndpoint } from '../../helpers/url';
|
||||||
|
|
||||||
import RevisionInformation from './RevisionInformation';
|
import RevisionInformation from './RevisionInformation';
|
||||||
import ReplicatesGraph from './ReplicatesGraph';
|
import ReplicatesGraph from './ReplicatesGraph';
|
||||||
|
@ -160,28 +161,27 @@ export default class CompareSubtestDistributionView extends React.Component {
|
||||||
return (
|
return (
|
||||||
originalRevision &&
|
originalRevision &&
|
||||||
newRevision && (
|
newRevision && (
|
||||||
<div className="container-fluid">
|
<Container fluid className="max-width-default justify-content-center">
|
||||||
{dataLoading ? (
|
{dataLoading ? (
|
||||||
<div className="loading">
|
<div className="loading">
|
||||||
Loading all results, please wait a minute...
|
|
||||||
<FontAwesomeIcon icon={faCog} size="4x" spin />
|
<FontAwesomeIcon icon={faCog} size="4x" spin />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<Row className="justify-content-center mt-4">
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<h2>
|
<h2>
|
||||||
{platform}: {testName} replicate distribution
|
{platform}: {testName} replicate distribution
|
||||||
</h2>
|
</h2>
|
||||||
<RevisionInformation
|
<RevisionInformation
|
||||||
originalProject={originalProject}
|
originalProject={originalProject.name}
|
||||||
originalRevision={originalRevision}
|
originalRevision={originalRevision}
|
||||||
originalResultSet={originalResultSet}
|
originalResultSet={originalResultSet}
|
||||||
newProject={newProject}
|
newProject={newProject.name}
|
||||||
newRevision={newRevision}
|
newRevision={newRevision}
|
||||||
newResultSet={newResultSet}
|
newResultSet={newResultSet}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
<div>
|
<div className="pt-5">
|
||||||
<ReplicatesGraph
|
<ReplicatesGraph
|
||||||
title="Base"
|
title="Base"
|
||||||
projectName={originalProject.name}
|
projectName={originalProject.name}
|
||||||
|
@ -197,9 +197,9 @@ export default class CompareSubtestDistributionView extends React.Component {
|
||||||
filters={this.state.filters}
|
filters={this.state.filters}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Row>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Container>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { react2angular } from 'react2angular/index.es2015';
|
||||||
|
import difference from 'lodash/difference';
|
||||||
|
|
||||||
|
import perf from '../../js/perf';
|
||||||
|
import { createQueryParams } from '../../helpers/url';
|
||||||
|
import {
|
||||||
|
createNoiseMetric,
|
||||||
|
getCounterMap,
|
||||||
|
createGraphsLinks,
|
||||||
|
} from '../helpers';
|
||||||
|
import { noiseMetricTitle } from '../constants';
|
||||||
|
|
||||||
|
import withValidation from './Validation';
|
||||||
|
import CompareTableView from './CompareTableView';
|
||||||
|
|
||||||
|
// TODO remove $stateParams and $state after switching to react router
|
||||||
|
export class CompareSubtestsView extends React.PureComponent {
|
||||||
|
createQueryParams = (parent_signature, repository, framework) => ({
|
||||||
|
parent_signature,
|
||||||
|
framework,
|
||||||
|
repository,
|
||||||
|
});
|
||||||
|
|
||||||
|
getQueryParams = (timeRange, framework) => {
|
||||||
|
const {
|
||||||
|
originalProject,
|
||||||
|
newProject,
|
||||||
|
originalRevision,
|
||||||
|
newRevision,
|
||||||
|
newResultSet,
|
||||||
|
originalSignature,
|
||||||
|
newSignature,
|
||||||
|
} = this.props.validated;
|
||||||
|
|
||||||
|
const originalParams = this.createQueryParams(
|
||||||
|
originalSignature,
|
||||||
|
originalProject,
|
||||||
|
framework.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (originalRevision) {
|
||||||
|
originalParams.revision = originalRevision;
|
||||||
|
} else {
|
||||||
|
// can create a helper function for both views
|
||||||
|
const startDateMs =
|
||||||
|
(newResultSet.push_timestamp - timeRange.value) * 1000;
|
||||||
|
const endDateMs = newResultSet.push_timestamp * 1000;
|
||||||
|
originalParams.startday = new Date(startDateMs)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, -5);
|
||||||
|
originalParams.endday = new Date(endDateMs).toISOString().slice(0, -5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newParams = this.createQueryParams(
|
||||||
|
newSignature,
|
||||||
|
newProject,
|
||||||
|
framework.id,
|
||||||
|
);
|
||||||
|
newParams.revision = newRevision;
|
||||||
|
return [originalParams, newParams];
|
||||||
|
};
|
||||||
|
|
||||||
|
createLinks = (oldResults, newResults, timeRange, framework) => {
|
||||||
|
const {
|
||||||
|
originalProject,
|
||||||
|
newProject,
|
||||||
|
originalRevision,
|
||||||
|
newRevision,
|
||||||
|
} = this.props.validated;
|
||||||
|
let links = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
(framework.name === 'talos' ||
|
||||||
|
framework.name === 'raptor' ||
|
||||||
|
framework.name === 'devtools') &&
|
||||||
|
originalRevision
|
||||||
|
) {
|
||||||
|
const params = {
|
||||||
|
originalProject,
|
||||||
|
newProject,
|
||||||
|
originalRevision,
|
||||||
|
newRevision,
|
||||||
|
originalSubtestSignature: oldResults ? oldResults.signature_id : null,
|
||||||
|
newSubtestSignature: newResults ? newResults.signature_id : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
links.push({
|
||||||
|
title: 'replicates',
|
||||||
|
href: `perf.html#/comparesubtestdistribution${createQueryParams(
|
||||||
|
params,
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const signature_hash = !oldResults
|
||||||
|
? newResults.signature_hash
|
||||||
|
: oldResults.signature_hash;
|
||||||
|
|
||||||
|
links = createGraphsLinks(
|
||||||
|
this.props.validated,
|
||||||
|
links,
|
||||||
|
framework,
|
||||||
|
timeRange,
|
||||||
|
signature_hash,
|
||||||
|
);
|
||||||
|
return links;
|
||||||
|
};
|
||||||
|
|
||||||
|
getDisplayResults = (origResultsMap, newResultsMap, state) => {
|
||||||
|
const { originalSignature, newSignature } = this.props.validated;
|
||||||
|
|
||||||
|
const { tableNames, rowNames, framework, timeRange } = state;
|
||||||
|
const testsWithNoise = [];
|
||||||
|
let compareResults = new Map();
|
||||||
|
const parentTestName = tableNames[0];
|
||||||
|
|
||||||
|
const oldStddevVariance = {
|
||||||
|
values: [],
|
||||||
|
lower_is_better: true,
|
||||||
|
frameworkID: framework.id,
|
||||||
|
};
|
||||||
|
const newStddevVariance = {
|
||||||
|
values: [],
|
||||||
|
lower_is_better: true,
|
||||||
|
frameworkID: framework.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
rowNames.forEach(testName => {
|
||||||
|
const oldResults = origResultsMap.find(sig => sig.test === testName);
|
||||||
|
const newResults = newResultsMap.find(sig => sig.test === testName);
|
||||||
|
|
||||||
|
const cmap = getCounterMap(testName, oldResults, newResults);
|
||||||
|
if (cmap.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cmap.name = testName;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(oldResults && oldResults.parent_signature === originalSignature) ||
|
||||||
|
(oldResults && oldResults.parent_signature === newSignature) ||
|
||||||
|
newResults.parent_signature === originalSignature ||
|
||||||
|
newResults.parent_signature === newSignature
|
||||||
|
) {
|
||||||
|
cmap.highlightedTest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cmap.originalStddevPct !== undefined &&
|
||||||
|
cmap.newStddevPct !== undefined
|
||||||
|
) {
|
||||||
|
if (cmap.originalStddevPct < 50.0 && cmap.newStddevPct < 50.0) {
|
||||||
|
oldStddevVariance.values.push(
|
||||||
|
Math.round(cmap.originalStddevPct * 100) / 100,
|
||||||
|
);
|
||||||
|
newStddevVariance.values.push(
|
||||||
|
Math.round(cmap.newStddevPct * 100) / 100,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const noise = {
|
||||||
|
baseStddev: cmap.originalStddevPct,
|
||||||
|
newStddev: cmap.newStddevPct,
|
||||||
|
testname: testName,
|
||||||
|
};
|
||||||
|
testsWithNoise.push(noise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmap.links = this.createLinks(
|
||||||
|
oldResults,
|
||||||
|
newResults,
|
||||||
|
timeRange,
|
||||||
|
framework,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (compareResults.has(parentTestName)) {
|
||||||
|
compareResults.get(parentTestName).push(cmap);
|
||||||
|
} else {
|
||||||
|
compareResults.set(parentTestName, [cmap]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const cmap = getCounterMap(
|
||||||
|
noiseMetricTitle,
|
||||||
|
oldStddevVariance,
|
||||||
|
newStddevVariance,
|
||||||
|
);
|
||||||
|
if (!cmap.isEmpty) {
|
||||||
|
compareResults = createNoiseMetric(cmap, parentTestName, compareResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
compareResults = new Map([...compareResults.entries()].sort());
|
||||||
|
const updates = { compareResults, testsWithNoise, loading: false };
|
||||||
|
|
||||||
|
const resultsArr = compareResults
|
||||||
|
.get(parentTestName)
|
||||||
|
.map(value => value.name);
|
||||||
|
const testsNoResults = difference(rowNames, resultsArr)
|
||||||
|
.sort()
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
if (testsNoResults.length) {
|
||||||
|
updates.testsNoResults = testsNoResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CompareTableView
|
||||||
|
{...this.props}
|
||||||
|
getQueryParams={this.getQueryParams}
|
||||||
|
getDisplayResults={this.getDisplayResults}
|
||||||
|
hasSubtests
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompareSubtestsView.propTypes = {
|
||||||
|
validated: PropTypes.shape({
|
||||||
|
originalResultSet: PropTypes.shape({}),
|
||||||
|
newResultSet: PropTypes.shape({}),
|
||||||
|
newRevision: PropTypes.string,
|
||||||
|
originalProject: PropTypes.string,
|
||||||
|
newProject: PropTypes.string,
|
||||||
|
originalRevision: PropTypes.string,
|
||||||
|
projects: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
updateParams: PropTypes.func.isRequired,
|
||||||
|
newSignature: PropTypes.string,
|
||||||
|
originalSignature: PropTypes.string,
|
||||||
|
}),
|
||||||
|
$stateParams: PropTypes.shape({}),
|
||||||
|
$state: PropTypes.shape({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
CompareSubtestsView.defaultProps = {
|
||||||
|
validated: PropTypes.shape({}),
|
||||||
|
$stateParams: null,
|
||||||
|
$state: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requiredParams = new Set([
|
||||||
|
'originalProject',
|
||||||
|
'newProject',
|
||||||
|
'newRevision',
|
||||||
|
'originalSignature',
|
||||||
|
'newSignature',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const compareSubtestsView = withValidation(requiredParams)(CompareSubtestsView);
|
||||||
|
|
||||||
|
perf.component(
|
||||||
|
'compareSubtestsView',
|
||||||
|
react2angular(compareSubtestsView, [], ['$stateParams', '$state']),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default compareSubtestsView;
|
|
@ -7,12 +7,12 @@ import {
|
||||||
faThumbsUp,
|
faThumbsUp,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import SimpleTooltip from '../shared/SimpleTooltip';
|
import SimpleTooltip from '../../shared/SimpleTooltip';
|
||||||
|
import { displayNumber } from '../helpers';
|
||||||
|
|
||||||
import { displayNumber } from './helpers';
|
|
||||||
import TableAverage from './TableAverage';
|
import TableAverage from './TableAverage';
|
||||||
|
|
||||||
export default class CompareTable extends React.Component {
|
export default class CompareTable extends React.PureComponent {
|
||||||
getColorClass = (data, type) => {
|
getColorClass = (data, type) => {
|
||||||
const { className, isRegression, isImprovement } = data;
|
const { className, isRegression, isImprovement } = data;
|
||||||
if (type === 'bar' && !isRegression && !isImprovement) return 'secondary';
|
if (type === 'bar' && !isRegression && !isImprovement) return 'secondary';
|
||||||
|
@ -30,7 +30,7 @@ export default class CompareTable extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { data, testName } = this.props;
|
const { data, testName } = this.props;
|
||||||
return (
|
return (
|
||||||
<Table sz="small" className="compare-table mb-0" key={testName}>
|
<Table sz="small" className="compare-table mb-0 px-0" key={testName}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="subtest-header bg-lightgray">
|
<tr className="subtest-header bg-lightgray">
|
||||||
<th className="text-left">
|
<th className="text-left">
|
||||||
|
@ -40,7 +40,7 @@ export default class CompareTable extends React.Component {
|
||||||
{/* empty for less than/greater than data */}
|
{/* empty for less than/greater than data */}
|
||||||
<th className="table-width-sm" />
|
<th className="table-width-sm" />
|
||||||
<th className="table-width-lg">New</th>
|
<th className="table-width-lg">New</th>
|
||||||
<th className="table-width-sm">Delta</th>
|
<th className="table-width-lg">Delta</th>
|
||||||
{/* empty for progress bars (magnitude of difference) */}
|
{/* empty for progress bars (magnitude of difference) */}
|
||||||
<th className="table-width-lg" />
|
<th className="table-width-lg" />
|
||||||
<th className="table-width-lg">Confidence</th>
|
<th className="table-width-lg">Confidence</th>
|
|
@ -1,15 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { react2angular } from 'react2angular/index.es2015';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Col, Row, Container, Button } from 'reactstrap';
|
import { Col, Row, Container, Button } from 'reactstrap';
|
||||||
|
|
||||||
import perf from '../js/perf';
|
import SimpleTooltip from '../../shared/SimpleTooltip';
|
||||||
import SimpleTooltip from '../shared/SimpleTooltip';
|
import { filterText } from '../constants';
|
||||||
|
|
||||||
import DropdownButton from './DropdownButton';
|
|
||||||
import InputFilter from './InputFilter';
|
import InputFilter from './InputFilter';
|
||||||
import CompareTable from './CompareTable';
|
import CompareTable from './CompareTable';
|
||||||
import { filterText } from './constants';
|
|
||||||
|
|
||||||
export default class CompareTableControls extends React.Component {
|
export default class CompareTableControls extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -32,45 +29,24 @@ export default class CompareTableControls extends React.Component {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { compareResults } = this.props;
|
const { compareResults } = this.props;
|
||||||
if (prevProps.compareResults !== compareResults) {
|
if (prevProps.compareResults !== compareResults) {
|
||||||
// TODO fetching new data based on a framework change is remounting
|
|
||||||
// the component and removing any previously set state; might be a side effect
|
|
||||||
// of using it with angular
|
|
||||||
this.updateFilteredResults();
|
this.updateFilteredResults();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO update usage of $stateParams when switched to react-router
|
|
||||||
convertParams = value =>
|
convertParams = value =>
|
||||||
Boolean(
|
Boolean(
|
||||||
this.props.$stateParams[value] !== undefined &&
|
this.props.validated[value] !== undefined &&
|
||||||
parseInt(this.props.$stateParams[value], 10),
|
parseInt(this.props.validated[value], 10),
|
||||||
);
|
);
|
||||||
|
|
||||||
updateFramework = selectedFramework => {
|
|
||||||
const { frameworks, updateData } = this.props;
|
|
||||||
// TODO this updates the entire framework object in the compare controller;
|
|
||||||
// look into removing it
|
|
||||||
const framework = frameworks.find(
|
|
||||||
framework => framework.name === selectedFramework,
|
|
||||||
);
|
|
||||||
updateData(framework);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateFilterText = filterText => {
|
updateFilterText = filterText => {
|
||||||
this.setState({ filterText }, () => this.updateFilteredResults());
|
this.setState({ filterText }, () => this.updateFilteredResults());
|
||||||
};
|
};
|
||||||
|
|
||||||
updateFilter = filter => {
|
updateFilter = filter => {
|
||||||
// TODO create callback to update queryParams with filter change if not undefined
|
|
||||||
this.setState(
|
this.setState(
|
||||||
prevState => ({ [filter]: !prevState[filter] }),
|
prevState => ({ [filter]: !prevState[filter] }),
|
||||||
() => {
|
() => this.updateFilteredResults(),
|
||||||
// TODO noise panel might be best moved into this table (displayed beneath controls)
|
|
||||||
if (filter === 'showNoise') {
|
|
||||||
this.props.updateNoiseAlert();
|
|
||||||
}
|
|
||||||
this.updateFilteredResults();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -112,6 +88,8 @@ export default class CompareTableControls extends React.Component {
|
||||||
showNoise,
|
showNoise,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const { compareResults } = this.props;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!filterText &&
|
!filterText &&
|
||||||
!hideUncomparable &&
|
!hideUncomparable &&
|
||||||
|
@ -119,28 +97,31 @@ export default class CompareTableControls extends React.Component {
|
||||||
!hideUncertain &&
|
!hideUncertain &&
|
||||||
!showNoise
|
!showNoise
|
||||||
) {
|
) {
|
||||||
return this.setState({ results: this.props.compareResults });
|
return this.setState({ results: compareResults });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newResults = new Map(this.props.compareResults);
|
const filteredResults = new Map(compareResults);
|
||||||
|
|
||||||
for (const [testName, values] of newResults) {
|
for (const [testName, values] of filteredResults) {
|
||||||
const filteredValues = values.filter(result =>
|
const filteredValues = values.filter(result =>
|
||||||
this.filterResult(testName, result),
|
this.filterResult(testName, result),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filteredValues.length) {
|
if (filteredValues.length) {
|
||||||
newResults.set(testName, filteredValues);
|
filteredResults.set(testName, filteredValues);
|
||||||
} else {
|
} else {
|
||||||
newResults.delete(testName);
|
filteredResults.delete(testName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.setState({ results: filteredResults });
|
||||||
this.setState({ results: newResults });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { filterByFramework, frameworks, titles, filterOptions } = this.props;
|
const {
|
||||||
|
frameworkOptions,
|
||||||
|
dateRangeOptions,
|
||||||
|
showTestsWithNoise,
|
||||||
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
hideUncomparable,
|
hideUncomparable,
|
||||||
hideUncertain,
|
hideUncertain,
|
||||||
|
@ -149,25 +130,14 @@ export default class CompareTableControls extends React.Component {
|
||||||
results,
|
results,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const frameworkNames =
|
|
||||||
frameworks && frameworks.length
|
|
||||||
? frameworks.map(framework => framework.name)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container fluid>
|
<Container fluid className="my-3 px-0">
|
||||||
<Row className="p-3 justify-content-left">
|
<Row className="p-3 justify-content-left">
|
||||||
{filterByFramework && frameworkNames && (
|
{frameworkOptions}
|
||||||
<Col sm="auto" className="p-2">
|
{dateRangeOptions}
|
||||||
<DropdownButton
|
</Row>
|
||||||
data={frameworkNames}
|
<Row className="pb-3 pl-3 justify-content-left">
|
||||||
defaultText={filterOptions.framework.name}
|
<Col className="py-2 pl-0 pr-2 col-3">
|
||||||
updateData={this.updateFramework}
|
|
||||||
defaultTextClass="mr-0 text-nowrap"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
<Col sm="2" className="p-2">
|
|
||||||
<InputFilter updateFilterText={this.updateFilterText} />
|
<InputFilter updateFilterText={this.updateFilterText} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col sm="auto" className="p-2">
|
<Col sm="auto" className="p-2">
|
||||||
|
@ -231,15 +201,11 @@ export default class CompareTableControls extends React.Component {
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
{showNoise && showTestsWithNoise}
|
||||||
|
|
||||||
{results.size > 0 ? (
|
{results.size > 0 ? (
|
||||||
Array.from(results).map(([testName, data]) => (
|
Array.from(results).map(([testName, data]) => (
|
||||||
<CompareTable
|
<CompareTable key={testName} data={data} testName={testName} />
|
||||||
key={testName}
|
|
||||||
data={data}
|
|
||||||
testName={testName}
|
|
||||||
title={titles}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="lead text-center">No results to show</p>
|
<p className="lead text-center">No results to show</p>
|
||||||
|
@ -250,55 +216,29 @@ export default class CompareTableControls extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
CompareTableControls.propTypes = {
|
CompareTableControls.propTypes = {
|
||||||
titles: PropTypes.shape({}),
|
|
||||||
compareResults: PropTypes.shape({}).isRequired,
|
compareResults: PropTypes.shape({}).isRequired,
|
||||||
frameworks: PropTypes.arrayOf(PropTypes.shape({})),
|
frameworkOptions: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
|
||||||
filterOptions: PropTypes.shape({
|
dateRangeOptions: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
|
||||||
framework: PropTypes.oneOfType([
|
validated: PropTypes.shape({
|
||||||
PropTypes.shape({
|
|
||||||
name: PropTypes.string,
|
|
||||||
}),
|
|
||||||
PropTypes.string,
|
|
||||||
]),
|
|
||||||
}).isRequired,
|
|
||||||
filterByFramework: PropTypes.number,
|
|
||||||
updateData: PropTypes.func,
|
|
||||||
updateNoiseAlert: PropTypes.func,
|
|
||||||
$stateParams: PropTypes.shape({
|
|
||||||
showOnlyImportant: PropTypes.string,
|
showOnlyImportant: PropTypes.string,
|
||||||
showOnlyComparable: PropTypes.string,
|
showOnlyComparable: PropTypes.string,
|
||||||
showOnlyConfident: PropTypes.string,
|
showOnlyConfident: PropTypes.string,
|
||||||
showOnlyNoise: PropTypes.string,
|
showOnlyNoise: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
|
showTestsWithNoise: PropTypes.oneOfType([
|
||||||
|
PropTypes.shape({}),
|
||||||
|
PropTypes.bool,
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
CompareTableControls.defaultProps = {
|
CompareTableControls.defaultProps = {
|
||||||
filterByFramework: null,
|
frameworkOptions: null,
|
||||||
frameworks: null,
|
dateRangeOptions: null,
|
||||||
updateData: null,
|
validated: {
|
||||||
titles: null,
|
|
||||||
updateNoiseAlert: null,
|
|
||||||
$stateParams: {
|
|
||||||
showOnlyImportant: undefined,
|
showOnlyImportant: undefined,
|
||||||
showOnlyComparable: undefined,
|
showOnlyComparable: undefined,
|
||||||
showOnlyConfident: undefined,
|
showOnlyConfident: undefined,
|
||||||
showOnlyNoise: undefined,
|
showOnlyNoise: undefined,
|
||||||
},
|
},
|
||||||
|
showTestsWithNoise: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
perf.component(
|
|
||||||
'compareTableControls',
|
|
||||||
react2angular(
|
|
||||||
CompareTableControls,
|
|
||||||
[
|
|
||||||
'compareResults',
|
|
||||||
'titles',
|
|
||||||
'frameworks',
|
|
||||||
'filterOptions',
|
|
||||||
'filterByFramework',
|
|
||||||
'updateData',
|
|
||||||
'updateNoiseAlert',
|
|
||||||
],
|
|
||||||
['$stateParams'],
|
|
||||||
),
|
|
||||||
);
|
|
|
@ -0,0 +1,350 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Col,
|
||||||
|
Row,
|
||||||
|
Container,
|
||||||
|
UncontrolledDropdown,
|
||||||
|
DropdownToggle,
|
||||||
|
} from 'reactstrap';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import ErrorMessages from '../../shared/ErrorMessages';
|
||||||
|
import {
|
||||||
|
genericErrorMessage,
|
||||||
|
errorMessageClass,
|
||||||
|
compareDefaultTimeRange,
|
||||||
|
phTimeRanges,
|
||||||
|
} from '../../helpers/constants';
|
||||||
|
import ErrorBoundary from '../../shared/ErrorBoundary';
|
||||||
|
import { getData } from '../../helpers/http';
|
||||||
|
import {
|
||||||
|
createApiUrl,
|
||||||
|
perfSummaryEndpoint,
|
||||||
|
createQueryParams,
|
||||||
|
} from '../../helpers/url';
|
||||||
|
import DropdownMenuItems from '../../shared/DropdownMenuItems';
|
||||||
|
|
||||||
|
import RevisionInformation from './RevisionInformation';
|
||||||
|
import CompareTableControls from './CompareTableControls';
|
||||||
|
import NoiseTable from './NoiseTable';
|
||||||
|
import ResultsAlert from './ResultsAlert';
|
||||||
|
|
||||||
|
// TODO remove $stateParams and $state after switching to react router
|
||||||
|
export default class CompareTableView extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
compareResults: new Map(),
|
||||||
|
testsNoResults: null,
|
||||||
|
testsWithNoise: [],
|
||||||
|
failureMessage: '',
|
||||||
|
loading: false,
|
||||||
|
timeRange: this.setTimeRange(),
|
||||||
|
framework: this.getFrameworkData(),
|
||||||
|
title: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.getPerformanceData();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props !== prevProps) {
|
||||||
|
this.getPerformanceData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFrameworkData = () => {
|
||||||
|
const { framework, frameworks } = this.props.validated;
|
||||||
|
|
||||||
|
if (framework) {
|
||||||
|
const frameworkObject = frameworks.find(
|
||||||
|
item => item.id === parseInt(framework, 10),
|
||||||
|
);
|
||||||
|
// framework is validated in the withValidation component so
|
||||||
|
// we know this object will always exist
|
||||||
|
return frameworkObject;
|
||||||
|
}
|
||||||
|
return { id: 1, name: 'talos' };
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeRange = () => {
|
||||||
|
const { selectedTimeRange, originalRevision } = this.props.validated;
|
||||||
|
|
||||||
|
if (originalRevision) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeRange;
|
||||||
|
if (selectedTimeRange) {
|
||||||
|
timeRange = phTimeRanges.find(
|
||||||
|
timeRange => timeRange.value === parseInt(selectedTimeRange, 10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeRange || compareDefaultTimeRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
getPerformanceData = async () => {
|
||||||
|
const { getQueryParams, hasSubtests, getDisplayResults } = this.props;
|
||||||
|
const {
|
||||||
|
originalProject,
|
||||||
|
originalRevision,
|
||||||
|
newProject,
|
||||||
|
newRevision,
|
||||||
|
} = this.props.validated;
|
||||||
|
const { framework, timeRange } = this.state;
|
||||||
|
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
const [originalParams, newParams] = getQueryParams(timeRange, framework);
|
||||||
|
|
||||||
|
const [originalResults, newResults] = await Promise.all([
|
||||||
|
getData(createApiUrl(perfSummaryEndpoint, originalParams)),
|
||||||
|
getData(createApiUrl(perfSummaryEndpoint, newParams)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (originalResults.failureStatus) {
|
||||||
|
return this.setState({
|
||||||
|
failureMessage: originalResults.data,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newResults.failureStatus) {
|
||||||
|
return this.setState({
|
||||||
|
failureMessage: newResults.data,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = [...originalResults.data, ...newResults.data];
|
||||||
|
let rowNames;
|
||||||
|
let tableNames;
|
||||||
|
let title;
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
return this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSubtests) {
|
||||||
|
let subtestName = data[0].name.split(' ');
|
||||||
|
subtestName.splice(1, 1);
|
||||||
|
subtestName = subtestName.join(' ');
|
||||||
|
|
||||||
|
title = `${data[0].platform}: ${subtestName}`;
|
||||||
|
tableNames = [subtestName];
|
||||||
|
rowNames = [...new Set(data.map(item => item.test))].sort();
|
||||||
|
} else {
|
||||||
|
tableNames = [...new Set(data.map(item => item.name))].sort();
|
||||||
|
rowNames = [...new Set(data.map(item => item.platform))].sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = originalRevision
|
||||||
|
? `${originalRevision} (${originalProject})`
|
||||||
|
: originalProject;
|
||||||
|
|
||||||
|
window.document.title =
|
||||||
|
title || `Comparison between ${text} and ${newRevision} (${newProject})`;
|
||||||
|
|
||||||
|
const updates = getDisplayResults(originalResults.data, newResults.data, {
|
||||||
|
...this.state,
|
||||||
|
...{ tableNames, rowNames },
|
||||||
|
});
|
||||||
|
updates.title = title;
|
||||||
|
return this.setState(updates);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateFramework = selection => {
|
||||||
|
const { frameworks, updateParams } = this.props.validated;
|
||||||
|
const framework = frameworks.find(item => item.name === selection);
|
||||||
|
|
||||||
|
updateParams({ framework: framework.id });
|
||||||
|
this.setState({ framework }, () => this.getPerformanceData());
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTimeRange = selection => {
|
||||||
|
const { updateParams } = this.props.validated;
|
||||||
|
const timeRange = phTimeRanges.find(item => item.text === selection);
|
||||||
|
|
||||||
|
updateParams({ selectedTimeRange: timeRange.value });
|
||||||
|
this.setState({ timeRange }, () => this.getPerformanceData());
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
originalProject,
|
||||||
|
newProject,
|
||||||
|
originalRevision,
|
||||||
|
newRevision,
|
||||||
|
originalResultSet,
|
||||||
|
newResultSet,
|
||||||
|
frameworks,
|
||||||
|
} = this.props.validated;
|
||||||
|
|
||||||
|
const { filterByFramework, hasSubtests } = this.props;
|
||||||
|
const {
|
||||||
|
compareResults,
|
||||||
|
loading,
|
||||||
|
failureMessage,
|
||||||
|
testsWithNoise,
|
||||||
|
timeRange,
|
||||||
|
testsNoResults,
|
||||||
|
title,
|
||||||
|
framework,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const timeRangeOptions = phTimeRanges.map(option => option.text);
|
||||||
|
const frameworkNames =
|
||||||
|
frameworks && frameworks.length ? frameworks.map(item => item.name) : [];
|
||||||
|
|
||||||
|
const params = { originalProject, newProject, newRevision };
|
||||||
|
|
||||||
|
if (originalRevision) {
|
||||||
|
params.originalRevision = originalRevision;
|
||||||
|
} else if (timeRange) {
|
||||||
|
params.selectedTimeRange = timeRange.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container fluid className="max-width-default">
|
||||||
|
{loading && !failureMessage && (
|
||||||
|
<div className="loading">
|
||||||
|
<FontAwesomeIcon icon={faCog} size="4x" spin />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ErrorBoundary
|
||||||
|
errorClasses={errorMessageClass}
|
||||||
|
message={genericErrorMessage}
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
{hasSubtests && (
|
||||||
|
<p>
|
||||||
|
<a href={`perf.html#/compare${createQueryParams(params)}`}>
|
||||||
|
Show all tests and platforms
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mx-auto">
|
||||||
|
<Row className="justify-content-center">
|
||||||
|
<Col sm="8" className="text-center">
|
||||||
|
{failureMessage && (
|
||||||
|
<ErrorMessages failureMessage={failureMessage} />
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{newRevision && newProject && (originalRevision || timeRange) && (
|
||||||
|
<Row>
|
||||||
|
<Col sm="12" className="text-center pb-1">
|
||||||
|
<h1>
|
||||||
|
{hasSubtests
|
||||||
|
? `${title} subtest summary`
|
||||||
|
: 'Perfherder Compare Revisions'}
|
||||||
|
</h1>
|
||||||
|
<RevisionInformation
|
||||||
|
originalProject={originalProject}
|
||||||
|
originalRevision={originalRevision}
|
||||||
|
originalResultSet={originalResultSet}
|
||||||
|
newProject={newProject}
|
||||||
|
newRevision={newRevision}
|
||||||
|
newResultSet={newResultSet}
|
||||||
|
selectedTimeRange={timeRange}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{testsNoResults && (
|
||||||
|
<ResultsAlert testsNoResults={testsNoResults} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CompareTableControls
|
||||||
|
{...this.props}
|
||||||
|
frameworkOptions={
|
||||||
|
filterByFramework && (
|
||||||
|
<Col sm="auto" className="py-0 pl-0 pr-3">
|
||||||
|
<UncontrolledDropdown className="mr-0 text-nowrap">
|
||||||
|
<DropdownToggle caret>{framework.name}</DropdownToggle>
|
||||||
|
{frameworkNames && (
|
||||||
|
<DropdownMenuItems
|
||||||
|
options={frameworkNames}
|
||||||
|
selectedItem={framework.name}
|
||||||
|
updateData={this.updateFramework}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</UncontrolledDropdown>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateState={state => this.setState(state)}
|
||||||
|
compareResults={compareResults}
|
||||||
|
dateRangeOptions={
|
||||||
|
!originalRevision && (
|
||||||
|
<Col sm="auto" className="p-0">
|
||||||
|
<UncontrolledDropdown className="mr-0 text-nowrap">
|
||||||
|
<DropdownToggle caret>{timeRange.text}</DropdownToggle>
|
||||||
|
<DropdownMenuItems
|
||||||
|
options={timeRangeOptions}
|
||||||
|
selectedItem={timeRange.text}
|
||||||
|
updateData={this.updateTimeRange}
|
||||||
|
/>
|
||||||
|
</UncontrolledDropdown>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showTestsWithNoise={
|
||||||
|
testsWithNoise.length > 0 && (
|
||||||
|
<Row>
|
||||||
|
<Col sm="12" className="text-center">
|
||||||
|
<NoiseTable
|
||||||
|
testsWithNoise={testsWithNoise}
|
||||||
|
hasSubtests={hasSubtests}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompareTableView.propTypes = {
|
||||||
|
validated: PropTypes.shape({
|
||||||
|
originalResultSet: PropTypes.shape({}),
|
||||||
|
newResultSet: PropTypes.shape({}),
|
||||||
|
newRevision: PropTypes.string,
|
||||||
|
originalProject: PropTypes.string,
|
||||||
|
newProject: PropTypes.string,
|
||||||
|
originalRevision: PropTypes.string,
|
||||||
|
projects: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
frameworks: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
selectedTimeRange: PropTypes.string,
|
||||||
|
updateParams: PropTypes.func.isRequired,
|
||||||
|
originalSignature: PropTypes.string,
|
||||||
|
newSignature: PropTypes.string,
|
||||||
|
framework: PropTypes.string,
|
||||||
|
}),
|
||||||
|
dateRangeOptions: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
|
||||||
|
filterByFramework: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
|
||||||
|
getDisplayResults: PropTypes.func.isRequired,
|
||||||
|
getQueryParams: PropTypes.func.isRequired,
|
||||||
|
hasSubtests: PropTypes.bool,
|
||||||
|
$stateParams: PropTypes.shape({}).isRequired,
|
||||||
|
$state: PropTypes.shape({}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
CompareTableView.defaultProps = {
|
||||||
|
dateRangeOptions: null,
|
||||||
|
filterByFramework: null,
|
||||||
|
validated: PropTypes.shape({}),
|
||||||
|
hasSubtests: false,
|
||||||
|
};
|
|
@ -0,0 +1,279 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { react2angular } from 'react2angular/index.es2015';
|
||||||
|
import difference from 'lodash/difference';
|
||||||
|
|
||||||
|
import perf from '../../js/perf';
|
||||||
|
import { createQueryParams } from '../../helpers/url';
|
||||||
|
import { phTimeRanges } from '../../helpers/constants';
|
||||||
|
import {
|
||||||
|
createNoiseMetric,
|
||||||
|
getCounterMap,
|
||||||
|
createGraphsLinks,
|
||||||
|
} from '../helpers';
|
||||||
|
import { noiseMetricTitle } from '../constants';
|
||||||
|
|
||||||
|
import withValidation from './Validation';
|
||||||
|
import CompareTableView from './CompareTableView';
|
||||||
|
|
||||||
|
// TODO remove $stateParams and $state after switching to react router
|
||||||
|
export class CompareView extends React.PureComponent {
|
||||||
|
getInterval = (oldTimestamp, newTimestamp) => {
|
||||||
|
const now = new Date().getTime() / 1000;
|
||||||
|
let timeRange = Math.min(oldTimestamp, newTimestamp);
|
||||||
|
timeRange = Math.round(now - timeRange);
|
||||||
|
const newTimeRange = phTimeRanges.find(time => timeRange <= time.value);
|
||||||
|
return newTimeRange.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
queryParams = (repository, interval, framework) => ({
|
||||||
|
repository,
|
||||||
|
framework,
|
||||||
|
interval,
|
||||||
|
no_subtests: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
getQueryParams = (timeRange, framework) => {
|
||||||
|
const {
|
||||||
|
originalProject,
|
||||||
|
newProject,
|
||||||
|
originalRevision,
|
||||||
|
newRevision,
|
||||||
|
newResultSet,
|
||||||
|
originalResultSet,
|
||||||
|
} = this.props.validated;
|
||||||
|
|
||||||
|
let originalParams;
|
||||||
|
let interval;
|
||||||
|
|
||||||
|
if (originalRevision) {
|
||||||
|
interval = this.getInterval(
|
||||||
|
originalResultSet.push_timestamp,
|
||||||
|
newResultSet.push_timestamp,
|
||||||
|
);
|
||||||
|
originalParams = this.queryParams(
|
||||||
|
originalProject,
|
||||||
|
interval,
|
||||||
|
framework.id,
|
||||||
|
);
|
||||||
|
originalParams.revision = originalRevision;
|
||||||
|
} else {
|
||||||
|
interval = timeRange.value;
|
||||||
|
const startDateMs = (newResultSet.push_timestamp - interval) * 1000;
|
||||||
|
const endDateMs = newResultSet.push_timestamp * 1000;
|
||||||
|
|
||||||
|
originalParams = this.queryParams(
|
||||||
|
originalProject,
|
||||||
|
interval,
|
||||||
|
framework.id,
|
||||||
|
);
|
||||||
|
originalParams.startday = new Date(startDateMs)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, -5);
|
||||||
|
originalParams.endday = new Date(endDateMs).toISOString().slice(0, -5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newParams = this.queryParams(newProject, interval, framework.id);
|
||||||
|
newParams.revision = newRevision;
|
||||||
|
return [originalParams, newParams];
|
||||||
|
};
|
||||||
|
|
||||||
|
createLinks = (oldResults, newResults, timeRange, framework) => {
|
||||||
|
const {
|
||||||
|
originalProject,
|
||||||
|
newProject,
|
||||||
|
originalRevision,
|
||||||
|
newRevision,
|
||||||
|
} = this.props.validated;
|
||||||
|
|
||||||
|
let links = [];
|
||||||
|
const hasSubtests =
|
||||||
|
(oldResults && oldResults.has_subtests) ||
|
||||||
|
(newResults && newResults.has_subtests);
|
||||||
|
|
||||||
|
if (hasSubtests) {
|
||||||
|
const params = {
|
||||||
|
originalProject,
|
||||||
|
newProject,
|
||||||
|
newRevision,
|
||||||
|
originalSignature: oldResults ? oldResults.signature_id : null,
|
||||||
|
newSignature: newResults ? newResults.signature_id : null,
|
||||||
|
framework: framework.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (originalRevision) {
|
||||||
|
params.originalRevision = originalRevision;
|
||||||
|
} else {
|
||||||
|
params.selectedTimeRange = timeRange.value;
|
||||||
|
}
|
||||||
|
const detailsLink = `perf.html#/comparesubtest${createQueryParams(
|
||||||
|
params,
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
links.push({
|
||||||
|
title: 'subtests',
|
||||||
|
href: detailsLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const signature_hash = !oldResults
|
||||||
|
? newResults.signature_hash
|
||||||
|
: oldResults.signature_hash;
|
||||||
|
links = createGraphsLinks(
|
||||||
|
this.props.validated,
|
||||||
|
links,
|
||||||
|
framework,
|
||||||
|
timeRange,
|
||||||
|
signature_hash,
|
||||||
|
);
|
||||||
|
return links;
|
||||||
|
};
|
||||||
|
|
||||||
|
getDisplayResults = (origResultsMap, newResultsMap, state) => {
|
||||||
|
const { rowNames, tableNames, framework, timeRange } = state;
|
||||||
|
|
||||||
|
let compareResults = new Map();
|
||||||
|
const oldStddevVariance = {};
|
||||||
|
const newStddevVariance = {};
|
||||||
|
const testsWithNoise = [];
|
||||||
|
|
||||||
|
tableNames.forEach(testName => {
|
||||||
|
rowNames.forEach(value => {
|
||||||
|
if (!oldStddevVariance[value]) {
|
||||||
|
oldStddevVariance[value] = {
|
||||||
|
values: [],
|
||||||
|
lower_is_better: true,
|
||||||
|
frameworkID: framework.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!newStddevVariance[value]) {
|
||||||
|
newStddevVariance[value] = {
|
||||||
|
values: [],
|
||||||
|
frameworkID: framework.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldResults = origResultsMap.find(
|
||||||
|
sig => sig.name === testName && sig.platform === value,
|
||||||
|
);
|
||||||
|
const newResults = newResultsMap.find(
|
||||||
|
sig => sig.name === testName && sig.platform === value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cmap = getCounterMap(testName, oldResults, newResults);
|
||||||
|
if (cmap.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cmap.name = value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
cmap.originalStddevPct !== undefined &&
|
||||||
|
cmap.newStddevPct !== undefined
|
||||||
|
) {
|
||||||
|
if (cmap.originalStddevPct < 50.0 && cmap.newStddevPct < 50.0) {
|
||||||
|
oldStddevVariance[value].values.push(
|
||||||
|
Math.round(cmap.originalStddevPct * 100) / 100,
|
||||||
|
);
|
||||||
|
newStddevVariance[value].values.push(
|
||||||
|
Math.round(cmap.newStddevPct * 100) / 100,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const noise = {
|
||||||
|
baseStddev: cmap.originalStddevPct,
|
||||||
|
newStddev: cmap.newStddevPct,
|
||||||
|
platform: value,
|
||||||
|
testname: testName,
|
||||||
|
};
|
||||||
|
testsWithNoise.push(noise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmap.links = this.createLinks(
|
||||||
|
oldResults,
|
||||||
|
newResults,
|
||||||
|
timeRange,
|
||||||
|
framework,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (compareResults.has(testName)) {
|
||||||
|
compareResults.get(testName).push(cmap);
|
||||||
|
} else {
|
||||||
|
compareResults.set(testName, [cmap]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rowNames.forEach(value => {
|
||||||
|
const cmap = getCounterMap(
|
||||||
|
noiseMetricTitle,
|
||||||
|
oldStddevVariance[value],
|
||||||
|
newStddevVariance[value],
|
||||||
|
);
|
||||||
|
if (cmap.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
compareResults = createNoiseMetric(cmap, value, compareResults);
|
||||||
|
});
|
||||||
|
|
||||||
|
compareResults = new Map([...compareResults.entries()].sort());
|
||||||
|
const updates = { compareResults, testsWithNoise, loading: false };
|
||||||
|
|
||||||
|
const resultsArr = Array.from(compareResults.keys());
|
||||||
|
const testsNoResults = difference(tableNames, resultsArr)
|
||||||
|
.sort()
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
if (testsNoResults.length) {
|
||||||
|
updates.testsNoResults = testsNoResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CompareTableView
|
||||||
|
{...this.props}
|
||||||
|
getQueryParams={this.getQueryParams}
|
||||||
|
getDisplayResults={this.getDisplayResults}
|
||||||
|
filterByFramework
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompareView.propTypes = {
|
||||||
|
validated: PropTypes.shape({
|
||||||
|
originalResultSet: PropTypes.shape({}),
|
||||||
|
newResultSet: PropTypes.shape({}),
|
||||||
|
newRevision: PropTypes.string,
|
||||||
|
originalProject: PropTypes.string,
|
||||||
|
newProject: PropTypes.string,
|
||||||
|
originalRevision: PropTypes.string,
|
||||||
|
projects: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
frameworks: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
framework: PropTypes.string,
|
||||||
|
updateParams: PropTypes.func.isRequired,
|
||||||
|
}),
|
||||||
|
$stateParams: PropTypes.shape({}),
|
||||||
|
$state: PropTypes.shape({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
CompareView.defaultProps = {
|
||||||
|
validated: PropTypes.shape({}),
|
||||||
|
$stateParams: null,
|
||||||
|
$state: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requiredParams = new Set([
|
||||||
|
'originalProject',
|
||||||
|
'newProject',
|
||||||
|
'newRevision',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const compareView = withValidation(requiredParams)(CompareView);
|
||||||
|
|
||||||
|
perf.component(
|
||||||
|
'compareView',
|
||||||
|
react2angular(compareView, [], ['$stateParams', '$state']),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default compareView;
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { InputGroup, InputGroupAddon, Input, Button } from 'reactstrap';
|
import { InputGroup, InputGroupAddon, Input, Button } from 'reactstrap';
|
||||||
|
|
||||||
import { filterText } from './constants';
|
import { filterText } from '../constants';
|
||||||
|
|
||||||
export default class InputFilter extends React.Component {
|
export default class InputFilter extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Table, Alert } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const NoiseTable = ({ testsWithNoise, hasSubtests }) => {
|
||||||
|
const valueToString = value => {
|
||||||
|
if (Number.isNaN(value)) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert color="info">
|
||||||
|
<p className="font-weight-bold">
|
||||||
|
Tests with too much noise to be considered in the noise metric
|
||||||
|
</p>
|
||||||
|
<Table sz="small" className="text-left" borderless>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{!hasSubtests && <th className="text-left">Platform</th>}
|
||||||
|
<th className="text-left">Testname</th>
|
||||||
|
<th className="text-left">Base Stddev</th>
|
||||||
|
<th className="text-left">New Stddev</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{testsWithNoise.map(test => (
|
||||||
|
<tr key={`${test.testname} ${test.platform || ''}`}>
|
||||||
|
{test.platform && <td>{test.platform}</td>}
|
||||||
|
<td>{test.testname}</td>
|
||||||
|
<td>{valueToString(test.baseStddev)}</td>
|
||||||
|
<td>{valueToString(test.newStddev)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
NoiseTable.propTypes = {
|
||||||
|
testsWithNoise: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||||
|
hasSubtests: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
NoiseTable.defaultProps = {
|
||||||
|
hasSubtests: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoiseTable;
|
|
@ -1,16 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { react2angular } from 'react2angular/index.es2015';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import perf from '../js/perf';
|
import { errorMessageClass } from '../../helpers/constants';
|
||||||
import { errorMessageClass } from '../helpers/constants';
|
import ErrorBoundary from '../../shared/ErrorBoundary';
|
||||||
import ErrorBoundary from '../shared/ErrorBoundary';
|
import Graph from '../../shared/Graph';
|
||||||
import Graph from '../shared/Graph';
|
import PerfSeriesModel from '../../models/perfSeries';
|
||||||
import PerfSeriesModel from '../models/perfSeries';
|
import { getData } from '../../helpers/http';
|
||||||
import { getData } from '../helpers/http';
|
import { createApiUrl, perfSummaryEndpoint } from '../../helpers/url';
|
||||||
import { createApiUrl, perfSummaryEndpoint } from '../helpers/url';
|
|
||||||
|
|
||||||
// TODO remove $stateParams after switching to react router
|
// TODO remove $stateParams after switching to react router
|
||||||
export default class ReplicatesGraph extends React.Component {
|
export default class ReplicatesGraph extends React.Component {
|
||||||
|
@ -119,7 +117,6 @@ export default class ReplicatesGraph extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { graphSpecs, drawingData, dataLoading } = this.state;
|
const { graphSpecs, drawingData, dataLoading } = this.state;
|
||||||
const { title } = this.props;
|
|
||||||
const data =
|
const data =
|
||||||
drawingData && drawingData.replicateValues
|
drawingData && drawingData.replicateValues
|
||||||
? drawingData.replicateValues
|
? drawingData.replicateValues
|
||||||
|
@ -127,7 +124,6 @@ export default class ReplicatesGraph extends React.Component {
|
||||||
|
|
||||||
return dataLoading ? (
|
return dataLoading ? (
|
||||||
<div className="loading">
|
<div className="loading">
|
||||||
Loading {title.toLowerCase()} results, please wait a minute...
|
|
||||||
<FontAwesomeIcon icon={faCog} size="4x" spin />
|
<FontAwesomeIcon icon={faCog} size="4x" spin />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -151,5 +147,3 @@ ReplicatesGraph.propTypes = {
|
||||||
subtest: PropTypes.string.isRequired,
|
subtest: PropTypes.string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
perf.component('replicatesGraph', react2angular(ReplicatesGraph, [], []));
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Col, Row, Alert } from 'reactstrap';
|
||||||
|
|
||||||
|
export default class ResultsAlert extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
showMoreResults: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { testsNoResults } = this.props;
|
||||||
|
const { showMoreResults } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className="pt-5 justify-content-center">
|
||||||
|
<Col small="12" className="px-0 max-width-default">
|
||||||
|
<Alert color="warning">
|
||||||
|
<p className="font-weight-bold">Tests without results</p>
|
||||||
|
<p className={showMoreResults ? '' : 'text-truncate'}>
|
||||||
|
{testsNoResults}
|
||||||
|
</p>
|
||||||
|
{testsNoResults.length > 174 && (
|
||||||
|
<p
|
||||||
|
className="mb-0 text-right font-weight-bold pointer"
|
||||||
|
onClick={() =>
|
||||||
|
this.setState({ showMoreResults: !showMoreResults })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{`show ${showMoreResults ? 'less' : 'more'}`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultsAlert.propTypes = {
|
||||||
|
testsNoResults: PropTypes.string.isRequired,
|
||||||
|
};
|
|
@ -1,10 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { react2angular } from 'react2angular/index.es2015';
|
|
||||||
import { ListGroup, ListGroupItem } from 'reactstrap';
|
import { ListGroup, ListGroupItem } from 'reactstrap';
|
||||||
|
|
||||||
import perf from '../js/perf';
|
import { getJobsUrl } from '../../helpers/url';
|
||||||
import { getJobsUrl } from '../helpers/url';
|
|
||||||
|
|
||||||
function getRevisionSpecificDetails(
|
function getRevisionSpecificDetails(
|
||||||
revision,
|
revision,
|
||||||
|
@ -20,14 +18,15 @@ function getRevisionSpecificDetails(
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<strong>{baselineOrNew}</strong> -
|
<strong>{baselineOrNew}</strong> -
|
||||||
{revision ? (
|
{revision ? (
|
||||||
<a href={getJobsUrl({ repo: project.name, revision })}>
|
<a href={getJobsUrl({ repo: project, revision })}>
|
||||||
{truncatedRevision}
|
{truncatedRevision}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
truncatedRevision
|
truncatedRevision
|
||||||
)}
|
)}
|
||||||
({project.name}) -
|
({project}) -
|
||||||
{resultSet ? resultSet.author : selectedTimeRange.text}
|
{resultSet && resultSet.author}
|
||||||
|
{!resultSet && selectedTimeRange && selectedTimeRange.text}
|
||||||
{isBaseline && ' - '}
|
{isBaseline && ' - '}
|
||||||
{resultSet ? <span>{resultSet.comments}</span> : ''}
|
{resultSet ? <span>{resultSet.comments}</span> : ''}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -83,9 +82,9 @@ export default function RevisionInformation(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
RevisionInformation.propTypes = {
|
RevisionInformation.propTypes = {
|
||||||
originalProject: PropTypes.object,
|
originalProject: PropTypes.string,
|
||||||
originalRevision: PropTypes.string,
|
originalRevision: PropTypes.string,
|
||||||
newProject: PropTypes.object,
|
newProject: PropTypes.string,
|
||||||
newRevision: PropTypes.string,
|
newRevision: PropTypes.string,
|
||||||
originalResultSet: PropTypes.object,
|
originalResultSet: PropTypes.object,
|
||||||
newResultSet: PropTypes.object,
|
newResultSet: PropTypes.object,
|
||||||
|
@ -93,28 +92,11 @@ RevisionInformation.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
RevisionInformation.defaultProps = {
|
RevisionInformation.defaultProps = {
|
||||||
originalProject: {},
|
originalProject: '',
|
||||||
originalRevision: '',
|
originalRevision: '',
|
||||||
originalResultSet: {},
|
originalResultSet: {},
|
||||||
newProject: {},
|
newProject: '',
|
||||||
newRevision: '',
|
newRevision: '',
|
||||||
newResultSet: {},
|
newResultSet: {},
|
||||||
selectedTimeRange: undefined,
|
selectedTimeRange: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
perf.component(
|
|
||||||
'revisionInformation',
|
|
||||||
react2angular(
|
|
||||||
RevisionInformation,
|
|
||||||
[
|
|
||||||
'originalProject',
|
|
||||||
'originalRevision',
|
|
||||||
'originalResultSet',
|
|
||||||
'newProject',
|
|
||||||
'newRevision',
|
|
||||||
'newResultSet',
|
|
||||||
'selectedTimeRange',
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
);
|
|
|
@ -20,8 +20,8 @@ import {
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import PushModel from '../models/push';
|
import PushModel from '../../models/push';
|
||||||
import { genericErrorMessage } from '../helpers/constants';
|
import { genericErrorMessage } from '../../helpers/constants';
|
||||||
|
|
||||||
export default class SelectorCard extends React.Component {
|
export default class SelectorCard extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import SimpleTooltip from '../shared/SimpleTooltip';
|
import SimpleTooltip from '../../shared/SimpleTooltip';
|
||||||
|
import { displayNumber } from '../helpers';
|
||||||
|
|
||||||
import TooltipGraph from './TooltipGraph';
|
import TooltipGraph from './TooltipGraph';
|
||||||
import { displayNumber } from './helpers';
|
|
||||||
|
|
||||||
const TableAverage = ({ value, stddev, stddevpct, replicates }) => {
|
const TableAverage = ({ value, stddev, stddevpct, replicates }) => {
|
||||||
let tooltipText;
|
let tooltipText;
|
|
@ -72,13 +72,13 @@ export default class TooltipGraph extends React.Component {
|
||||||
{(minValue || maxValue) && (
|
{(minValue || maxValue) && (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="value-column">
|
<td className="value-column text-white">
|
||||||
{this.abbreviatedNumber(minValue)}
|
{this.abbreviatedNumber(minValue)}
|
||||||
</td>
|
</td>
|
||||||
<td className="distribution-column">
|
<td className="distribution-column">
|
||||||
<canvas ref={this.canvasRef} width={190} height={30} />
|
<canvas ref={this.canvasRef} width={190} height={30} />
|
||||||
</td>
|
</td>
|
||||||
<td className="value-column">
|
<td className="value-column text-white">
|
||||||
{this.abbreviatedNumber(maxValue)}
|
{this.abbreviatedNumber(maxValue)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
|
@ -0,0 +1,214 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Container } from 'reactstrap';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import { getData } from '../../helpers/http';
|
||||||
|
import { getApiUrl, repoEndpoint } from '../../helpers/url';
|
||||||
|
import PushModel from '../../models/push';
|
||||||
|
import { endpoints } from '../constants';
|
||||||
|
import ErrorMessages from '../../shared/ErrorMessages';
|
||||||
|
|
||||||
|
// TODO once we switch to react-router
|
||||||
|
// 1) use context in this HOC to share state between compare views, by wrapping router component in it;
|
||||||
|
// advantages include:
|
||||||
|
// * no need to check historical location.state to determine if user has navigated to compare or comparesubtest
|
||||||
|
// views from a previous view (thus params have already been validated and resultsets stored in state)
|
||||||
|
// * if user navigates to compareview from compareChooser and decides to change a project via query params
|
||||||
|
// to a different project, projects will already be stored in state so no fetching data again to validate
|
||||||
|
//
|
||||||
|
|
||||||
|
const withValidation = requiredParams => WrappedComponent => {
|
||||||
|
class Validation extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// TODO change $stateParams to location.state once we switch to react-router
|
||||||
|
this.state = {
|
||||||
|
originalProject: null,
|
||||||
|
newProject: null,
|
||||||
|
originalRevision: null,
|
||||||
|
newRevision: null,
|
||||||
|
originalSignature: null,
|
||||||
|
newSignature: null,
|
||||||
|
errorMessages: [],
|
||||||
|
projects: [],
|
||||||
|
originalResultSet: null,
|
||||||
|
newResultSet: null,
|
||||||
|
selectedTimeRange: null,
|
||||||
|
framework: null,
|
||||||
|
frameworks: [],
|
||||||
|
// TODO reset if validateParams method is called from another component
|
||||||
|
validationComplete: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const [projects, frameworks] = await Promise.all([
|
||||||
|
getData(getApiUrl(repoEndpoint)),
|
||||||
|
getData(getApiUrl(endpoints.frameworks)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const updates = {
|
||||||
|
...this.processResponse(projects, 'projects'),
|
||||||
|
...this.processResponse(frameworks, 'frameworks'),
|
||||||
|
};
|
||||||
|
this.setState(updates, () =>
|
||||||
|
this.validateParams(this.props.$stateParams),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
processResponse = (response, state) => {
|
||||||
|
const { data, failureStatus } = response;
|
||||||
|
if (failureStatus) {
|
||||||
|
return { errorMessages: [...this.state.errorMessages, ...data] };
|
||||||
|
}
|
||||||
|
return { [state]: data };
|
||||||
|
};
|
||||||
|
|
||||||
|
updateParams = param => {
|
||||||
|
const { transitionTo, current } = this.props.$state;
|
||||||
|
transitionTo(current.name, param, {
|
||||||
|
inherit: true,
|
||||||
|
notify: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
errorMessage = (param, value) => `${param} ${value} is not valid`;
|
||||||
|
|
||||||
|
async checkRevisions(params) {
|
||||||
|
if (!params.originalRevision) {
|
||||||
|
const newResultResponse = await this.verifyRevision(
|
||||||
|
params.newProject,
|
||||||
|
params.newRevision,
|
||||||
|
'newResultSet',
|
||||||
|
);
|
||||||
|
return this.setState({
|
||||||
|
...params,
|
||||||
|
...newResultResponse,
|
||||||
|
validationComplete: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [newResultResponse, origResultResponse] = await Promise.all([
|
||||||
|
this.verifyRevision(
|
||||||
|
params.newProject,
|
||||||
|
params.newRevision,
|
||||||
|
'newResultSet',
|
||||||
|
),
|
||||||
|
this.verifyRevision(
|
||||||
|
params.originalProject,
|
||||||
|
params.originalRevision,
|
||||||
|
'originalResultSet',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...params,
|
||||||
|
...newResultResponse,
|
||||||
|
...origResultResponse,
|
||||||
|
validationComplete: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyRevision(project, revision, resultSetName) {
|
||||||
|
const { data, failureStatus } = await PushModel.getList({
|
||||||
|
repo: project,
|
||||||
|
commit_revision: revision,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failureStatus) {
|
||||||
|
return {
|
||||||
|
errorMessages: [`Error fetching revision ${revision}: ${data}`],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!data.results.length) {
|
||||||
|
return { errorMessages: [`No results found for revision ${revision}`] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [resultSetName]: data.results[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
validateParams(params) {
|
||||||
|
const { projects, frameworks } = this.state;
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const [param, value] of Object.entries(params)) {
|
||||||
|
if (!value && requiredParams.has(param)) {
|
||||||
|
errors.push(`${param} is required`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 'undefined') {
|
||||||
|
errors.push(this.errorMessage(param, value));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.indexOf('Project') !== -1 && projects.length) {
|
||||||
|
const validProject = projects.find(project => project.name === value);
|
||||||
|
|
||||||
|
if (!validProject) {
|
||||||
|
errors.push(this.errorMessage(param, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param === 'framework' && value && frameworks.length) {
|
||||||
|
const validFramework = frameworks.find(
|
||||||
|
item => item.id === parseInt(value, 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validFramework) {
|
||||||
|
errors.push(this.errorMessage(param, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return this.setState({ errorMessages: errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkRevisions(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const updateParams = { updateParams: this.updateParams };
|
||||||
|
const validatedProps = {
|
||||||
|
...this.state,
|
||||||
|
...updateParams,
|
||||||
|
};
|
||||||
|
const { validationComplete, errorMessages } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{!validationComplete && errorMessages.length === 0 && (
|
||||||
|
<div className="loading">
|
||||||
|
<FontAwesomeIcon icon={faCog} size="4x" spin />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errorMessages.length > 0 && (
|
||||||
|
<Container className="pt-5 max-width-default">
|
||||||
|
<ErrorMessages errorMessages={errorMessages} />
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{validationComplete && !errorMessages.length && (
|
||||||
|
<WrappedComponent validated={validatedProps} {...this.props} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Validation.propTypes = {
|
||||||
|
$stateParams: PropTypes.shape({}).isRequired,
|
||||||
|
$state: PropTypes.shape({
|
||||||
|
transitionTo: PropTypes.func,
|
||||||
|
current: PropTypes.shape({}),
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Validation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withValidation;
|
|
@ -1,7 +1,7 @@
|
||||||
import numeral from 'numeral';
|
import numeral from 'numeral';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
import { getApiUrl, createQueryParams, repoEndpoint } from '../helpers/url';
|
import { getApiUrl, createQueryParams } from '../helpers/url';
|
||||||
import { create, getData, update } from '../helpers/http';
|
import { create, getData, update } from '../helpers/http';
|
||||||
import { getSeriesName, getTestName } from '../models/perfSeries';
|
import { getSeriesName, getTestName } from '../models/perfSeries';
|
||||||
import OptionCollectionModel from '../models/optionCollection';
|
import OptionCollectionModel from '../models/optionCollection';
|
||||||
|
@ -264,53 +264,6 @@ export const getCounterMap = function getCounterMap(
|
||||||
return cmap;
|
return cmap;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: move into a react component as this is only used once (in PhCompare controller)
|
|
||||||
export const getInterval = function getInterval(oldTimestamp, newTimestamp) {
|
|
||||||
const now = new Date().getTime() / 1000;
|
|
||||||
let timeRange = Math.min(oldTimestamp, newTimestamp);
|
|
||||||
timeRange = Math.round(now - timeRange);
|
|
||||||
const newTimeRange = phTimeRanges.find(time => timeRange <= time.value);
|
|
||||||
return newTimeRange.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO possibly break up into different functions and/or move into a component
|
|
||||||
export const validateQueryParams = async function validateQueryParams(params) {
|
|
||||||
const {
|
|
||||||
originalProject,
|
|
||||||
newProject,
|
|
||||||
originalRevision,
|
|
||||||
newRevision,
|
|
||||||
originalSignature,
|
|
||||||
newSignature,
|
|
||||||
} = params;
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (!originalProject) errors.push('Missing input: originalProject');
|
|
||||||
if (!newProject) errors.push('Missing input: newProject');
|
|
||||||
if (!originalRevision) errors.push('Missing input: originalRevision');
|
|
||||||
if (!newRevision) errors.push('Missing input: newRevision');
|
|
||||||
|
|
||||||
if (originalSignature && newSignature) {
|
|
||||||
if (!originalSignature) errors.push('Missing input: originalSignature');
|
|
||||||
if (!newSignature) errors.push('Missing input: newSignature');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, failureStatus } = await getData(getApiUrl(repoEndpoint));
|
|
||||||
|
|
||||||
if (
|
|
||||||
!failureStatus &&
|
|
||||||
!data.find(project => project.name === originalProject)
|
|
||||||
) {
|
|
||||||
errors.push(`Invalid project, doesn't exist ${originalProject}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!failureStatus && !data.find(project => project.name === newProject)) {
|
|
||||||
errors.push(`Invalid project, doesn't exist ${newProject}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGraphsLink = function getGraphsLink(
|
export const getGraphsLink = function getGraphsLink(
|
||||||
seriesList,
|
seriesList,
|
||||||
resultSets,
|
resultSets,
|
||||||
|
@ -345,6 +298,60 @@ export const getGraphsLink = function getGraphsLink(
|
||||||
return `perf.html#/graphs${createQueryParams(params)}`;
|
return `perf.html#/graphs${createQueryParams(params)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createNoiseMetric = function createNoiseMetric(
|
||||||
|
cmap,
|
||||||
|
name,
|
||||||
|
compareResults,
|
||||||
|
) {
|
||||||
|
cmap.name = name;
|
||||||
|
cmap.isNoiseMetric = true;
|
||||||
|
|
||||||
|
if (compareResults.has(noiseMetricTitle)) {
|
||||||
|
compareResults.get(noiseMetricTitle).push(cmap);
|
||||||
|
} else {
|
||||||
|
compareResults.set(noiseMetricTitle, [cmap]);
|
||||||
|
}
|
||||||
|
return compareResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createGraphsLinks = (
|
||||||
|
validatedProps,
|
||||||
|
links,
|
||||||
|
framework,
|
||||||
|
timeRange,
|
||||||
|
signature,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
originalProject,
|
||||||
|
newProject,
|
||||||
|
originalRevision,
|
||||||
|
newResultSet,
|
||||||
|
originalResultSet,
|
||||||
|
} = validatedProps;
|
||||||
|
|
||||||
|
const graphsParams = [...new Set([originalProject, newProject])].map(
|
||||||
|
projectName => ({
|
||||||
|
projectName,
|
||||||
|
signature,
|
||||||
|
frameworkId: framework.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let graphsLink;
|
||||||
|
if (originalRevision) {
|
||||||
|
graphsLink = getGraphsLink(graphsParams, [originalResultSet, newResultSet]);
|
||||||
|
} else {
|
||||||
|
graphsLink = getGraphsLink(graphsParams, [newResultSet], timeRange.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
links.push({
|
||||||
|
title: 'graph',
|
||||||
|
href: graphsLink,
|
||||||
|
});
|
||||||
|
|
||||||
|
return links;
|
||||||
|
};
|
||||||
|
|
||||||
// old PhAlerts' inner workings
|
// old PhAlerts' inner workings
|
||||||
// TODO change all usage of signature_hash to signature.id
|
// TODO change all usage of signature_hash to signature.id
|
||||||
// for originalSignature and newSignature query params
|
// for originalSignature and newSignature query params
|
||||||
|
|
|
@ -8,7 +8,7 @@ const ErrorMessages = ({ failureMessage, errorMessages }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{messages.map(message => (
|
{messages.map(message => (
|
||||||
<Alert color="danger" key={message}>
|
<Alert color="danger" key={message} className="text-center">
|
||||||
{message}
|
{message}
|
||||||
</Alert>
|
</Alert>
|
||||||
))}
|
))}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче