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:
Sarah Clements 2019-03-25 18:54:53 -07:00 коммит произвёл GitHub
Родитель 03bea62b50
Коммит 1f6809a751
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
30 изменённых файлов: 1376 добавлений и 1014 удалений

Просмотреть файл

@ -6,7 +6,7 @@ import {
waitForElement,
} from 'react-testing-library';
import CompareTableControls from '../../../../ui/perfherder/CompareTableControls';
import CompareTableControls from '../../../../ui/perfherder/compare/CompareTableControls';
import { filterText } from '../../../../ui/perfherder/constants';
// TODO addtional tests:

Просмотреть файл

@ -98,28 +98,3 @@ ul {
list-style-type: none;
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: 1200px;
max-width: 1250px;
}
/* Custom widths for compare table cells */
@ -481,3 +481,32 @@ h4 {
.dropdown-menu-height {
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
import './js/filters';
import './js/controllers/perf/compare';
import './js/controllers/perf/graphs';
import './js/controllers/perf/alerts';
import './js/components/perf/compare';
import './js/components/loading';
import './js/perfapp';
import './perfherder/CompareSelectorView';
import './perfherder/RevisionInformation';
import './perfherder/CompareSubtestDistributionView';
import './perfherder/CompareTableControls';
import './perfherder/compare/CompareSelectorView';
import './perfherder/compare/CompareView';
import './perfherder/compare/CompareSubtestDistributionView';
import './perfherder/compare/CompareSubtestsView';
config.showMissingIcons = true;

Просмотреть файл

@ -221,7 +221,10 @@ export const phAlertStatusMap = {
CONFIRMING: { id: 5, text: 'confirming' },
};
export const compareDefaultTimeRange = 86400 * 2;
export const compareDefaultTimeRange = {
value: 86400 * 2,
text: 'Last 2 days',
};
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',
template: compareCtrlTemplate,
url: '/compare?originalProject&originalRevision?&newProject&newRevision&hideMinorChanges&framework&filter&showOnlyComparable&showOnlyImportant&showOnlyConfident&selectedTimeRange&showOnlyNoise?',
controller: 'CompareResultsCtrl',
})
.state('comparesubtest', {
title: 'Compare - Subtests',
template: compareSubtestCtrlTemplate,
url: '/comparesubtest?originalProject&originalRevision?&newProject&newRevision&originalSignature&newSignature&filter&showOnlyComparable&showOnlyImportant&showOnlyConfident&framework&selectedTimeRange&showOnlyNoise?',
controller: 'CompareSubtestResultsCtrl',
})
.state('comparechooser', {
title: 'Compare Chooser',

Просмотреть файл

@ -1,50 +1 @@
<div class="container-fluid">
<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>
<compare-view />

Просмотреть файл

@ -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">
<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>
<compare-subtests-view />

Просмотреть файл

@ -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 { Container, Col, Row, Button } from 'reactstrap';
import perf from '../js/perf';
import { getApiUrl, repoEndpoint } from '../helpers/url';
import { getData } from '../helpers/http';
import ErrorMessages from '../shared/ErrorMessages';
import perf from '../../js/perf';
import { getApiUrl, repoEndpoint } from '../../helpers/url';
import { getData } from '../../helpers/http';
import ErrorMessages from '../../shared/ErrorMessages';
import {
compareDefaultTimeRange,
genericErrorMessage,
errorMessageClass,
} from '../helpers/constants';
import ErrorBoundary from '../shared/ErrorBoundary';
} from '../../helpers/constants';
import ErrorBoundary from '../../shared/ErrorBoundary';
import SelectorCard from './SelectorCard';
@ -64,7 +64,7 @@ export default class CompareSelectorView extends React.Component {
originalProject,
newProject,
newRevision,
selectedTimeRange: compareDefaultTimeRange,
selectedTimeRange: compareDefaultTimeRange.value,
});
}
};

Просмотреть файл

@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import { react2angular } from 'react2angular/index.es2015';
import { Container, Row } from 'reactstrap';
import perf from '../js/perf';
import RepositoryModel from '../models/repository';
import PushModel from '../models/push';
import { getData } from '../helpers/http';
import { createApiUrl, perfSummaryEndpoint } from '../helpers/url';
import perf from '../../js/perf';
import RepositoryModel from '../../models/repository';
import PushModel from '../../models/push';
import { getData } from '../../helpers/http';
import { createApiUrl, perfSummaryEndpoint } from '../../helpers/url';
import RevisionInformation from './RevisionInformation';
import ReplicatesGraph from './ReplicatesGraph';
@ -160,28 +161,27 @@ export default class CompareSubtestDistributionView extends React.Component {
return (
originalRevision &&
newRevision && (
<div className="container-fluid">
<Container fluid className="max-width-default justify-content-center">
{dataLoading ? (
<div className="loading">
Loading all results, please wait a minute...
<FontAwesomeIcon icon={faCog} size="4x" spin />
</div>
) : (
<div>
<Row className="justify-content-center mt-4">
<React.Fragment>
<h2>
{platform}: {testName} replicate distribution
</h2>
<RevisionInformation
originalProject={originalProject}
originalProject={originalProject.name}
originalRevision={originalRevision}
originalResultSet={originalResultSet}
newProject={newProject}
newProject={newProject.name}
newRevision={newRevision}
newResultSet={newResultSet}
/>
</React.Fragment>
<div>
<div className="pt-5">
<ReplicatesGraph
title="Base"
projectName={originalProject.name}
@ -197,9 +197,9 @@ export default class CompareSubtestDistributionView extends React.Component {
filters={this.state.filters}
/>
</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,
} 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';
export default class CompareTable extends React.Component {
export default class CompareTable extends React.PureComponent {
getColorClass = (data, type) => {
const { className, isRegression, isImprovement } = data;
if (type === 'bar' && !isRegression && !isImprovement) return 'secondary';
@ -30,7 +30,7 @@ export default class CompareTable extends React.Component {
render() {
const { data, testName } = this.props;
return (
<Table sz="small" className="compare-table mb-0" key={testName}>
<Table sz="small" className="compare-table mb-0 px-0" key={testName}>
<thead>
<tr className="subtest-header bg-lightgray">
<th className="text-left">
@ -40,7 +40,7 @@ export default class CompareTable extends React.Component {
{/* empty for less than/greater than data */}
<th className="table-width-sm" />
<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) */}
<th className="table-width-lg" />
<th className="table-width-lg">Confidence</th>

Просмотреть файл

@ -1,15 +1,12 @@
import React from 'react';
import { react2angular } from 'react2angular/index.es2015';
import PropTypes from 'prop-types';
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 CompareTable from './CompareTable';
import { filterText } from './constants';
export default class CompareTableControls extends React.Component {
constructor(props) {
@ -32,45 +29,24 @@ export default class CompareTableControls extends React.Component {
componentDidUpdate(prevProps) {
const { compareResults } = this.props;
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();
}
}
// TODO update usage of $stateParams when switched to react-router
convertParams = value =>
Boolean(
this.props.$stateParams[value] !== undefined &&
parseInt(this.props.$stateParams[value], 10),
this.props.validated[value] !== undefined &&
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 => {
this.setState({ filterText }, () => this.updateFilteredResults());
};
updateFilter = filter => {
// TODO create callback to update queryParams with filter change if not undefined
this.setState(
prevState => ({ [filter]: !prevState[filter] }),
() => {
// TODO noise panel might be best moved into this table (displayed beneath controls)
if (filter === 'showNoise') {
this.props.updateNoiseAlert();
}
this.updateFilteredResults();
},
() => this.updateFilteredResults(),
);
};
@ -112,6 +88,8 @@ export default class CompareTableControls extends React.Component {
showNoise,
} = this.state;
const { compareResults } = this.props;
if (
!filterText &&
!hideUncomparable &&
@ -119,28 +97,31 @@ export default class CompareTableControls extends React.Component {
!hideUncertain &&
!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 =>
this.filterResult(testName, result),
);
if (filteredValues.length) {
newResults.set(testName, filteredValues);
filteredResults.set(testName, filteredValues);
} else {
newResults.delete(testName);
filteredResults.delete(testName);
}
}
this.setState({ results: newResults });
this.setState({ results: filteredResults });
};
render() {
const { filterByFramework, frameworks, titles, filterOptions } = this.props;
const {
frameworkOptions,
dateRangeOptions,
showTestsWithNoise,
} = this.props;
const {
hideUncomparable,
hideUncertain,
@ -149,25 +130,14 @@ export default class CompareTableControls extends React.Component {
results,
} = this.state;
const frameworkNames =
frameworks && frameworks.length
? frameworks.map(framework => framework.name)
: null;
return (
<Container fluid>
<Container fluid className="my-3 px-0">
<Row className="p-3 justify-content-left">
{filterByFramework && frameworkNames && (
<Col sm="auto" className="p-2">
<DropdownButton
data={frameworkNames}
defaultText={filterOptions.framework.name}
updateData={this.updateFramework}
defaultTextClass="mr-0 text-nowrap"
/>
</Col>
)}
<Col sm="2" className="p-2">
{frameworkOptions}
{dateRangeOptions}
</Row>
<Row className="pb-3 pl-3 justify-content-left">
<Col className="py-2 pl-0 pr-2 col-3">
<InputFilter updateFilterText={this.updateFilterText} />
</Col>
<Col sm="auto" className="p-2">
@ -231,15 +201,11 @@ export default class CompareTableControls extends React.Component {
/>
</Col>
</Row>
{showNoise && showTestsWithNoise}
{results.size > 0 ? (
Array.from(results).map(([testName, data]) => (
<CompareTable
key={testName}
data={data}
testName={testName}
title={titles}
/>
<CompareTable key={testName} data={data} testName={testName} />
))
) : (
<p className="lead text-center">No results to show</p>
@ -250,55 +216,29 @@ export default class CompareTableControls extends React.Component {
}
CompareTableControls.propTypes = {
titles: PropTypes.shape({}),
compareResults: PropTypes.shape({}).isRequired,
frameworks: PropTypes.arrayOf(PropTypes.shape({})),
filterOptions: PropTypes.shape({
framework: PropTypes.oneOfType([
PropTypes.shape({
name: PropTypes.string,
}),
PropTypes.string,
]),
}).isRequired,
filterByFramework: PropTypes.number,
updateData: PropTypes.func,
updateNoiseAlert: PropTypes.func,
$stateParams: PropTypes.shape({
frameworkOptions: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
dateRangeOptions: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
validated: PropTypes.shape({
showOnlyImportant: PropTypes.string,
showOnlyComparable: PropTypes.string,
showOnlyConfident: PropTypes.string,
showOnlyNoise: PropTypes.string,
}),
showTestsWithNoise: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.bool,
]),
};
CompareTableControls.defaultProps = {
filterByFramework: null,
frameworks: null,
updateData: null,
titles: null,
updateNoiseAlert: null,
$stateParams: {
frameworkOptions: null,
dateRangeOptions: null,
validated: {
showOnlyImportant: undefined,
showOnlyComparable: undefined,
showOnlyConfident: 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 { InputGroup, InputGroupAddon, Input, Button } from 'reactstrap';
import { filterText } from './constants';
import { filterText } from '../constants';
export default class InputFilter extends React.Component {
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 PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import perf from '../js/perf';
import { errorMessageClass } from '../helpers/constants';
import ErrorBoundary from '../shared/ErrorBoundary';
import Graph from '../shared/Graph';
import PerfSeriesModel from '../models/perfSeries';
import { getData } from '../helpers/http';
import { createApiUrl, perfSummaryEndpoint } from '../helpers/url';
import { errorMessageClass } from '../../helpers/constants';
import ErrorBoundary from '../../shared/ErrorBoundary';
import Graph from '../../shared/Graph';
import PerfSeriesModel from '../../models/perfSeries';
import { getData } from '../../helpers/http';
import { createApiUrl, perfSummaryEndpoint } from '../../helpers/url';
// TODO remove $stateParams after switching to react router
export default class ReplicatesGraph extends React.Component {
@ -119,7 +117,6 @@ export default class ReplicatesGraph extends React.Component {
render() {
const { graphSpecs, drawingData, dataLoading } = this.state;
const { title } = this.props;
const data =
drawingData && drawingData.replicateValues
? drawingData.replicateValues
@ -127,7 +124,6 @@ export default class ReplicatesGraph extends React.Component {
return dataLoading ? (
<div className="loading">
Loading {title.toLowerCase()} results, please wait a minute...
<FontAwesomeIcon icon={faCog} size="4x" spin />
</div>
) : (
@ -151,5 +147,3 @@ ReplicatesGraph.propTypes = {
subtest: PropTypes.string.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 PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import { ListGroup, ListGroupItem } from 'reactstrap';
import perf from '../js/perf';
import { getJobsUrl } from '../helpers/url';
import { getJobsUrl } from '../../helpers/url';
function getRevisionSpecificDetails(
revision,
@ -20,14 +18,15 @@ function getRevisionSpecificDetails(
<React.Fragment>
<strong>{baselineOrNew}</strong> -&nbsp;
{revision ? (
<a href={getJobsUrl({ repo: project.name, revision })}>
<a href={getJobsUrl({ repo: project, revision })}>
{truncatedRevision}
</a>
) : (
truncatedRevision
)}
&nbsp;({project.name}) -&nbsp;
{resultSet ? resultSet.author : selectedTimeRange.text}
&nbsp;({project}) -&nbsp;
{resultSet && resultSet.author}
{!resultSet && selectedTimeRange && selectedTimeRange.text}
{isBaseline && ' - '}
{resultSet ? <span>{resultSet.comments}</span> : ''}
</React.Fragment>
@ -83,9 +82,9 @@ export default function RevisionInformation(props) {
}
RevisionInformation.propTypes = {
originalProject: PropTypes.object,
originalProject: PropTypes.string,
originalRevision: PropTypes.string,
newProject: PropTypes.object,
newProject: PropTypes.string,
newRevision: PropTypes.string,
originalResultSet: PropTypes.object,
newResultSet: PropTypes.object,
@ -93,28 +92,11 @@ RevisionInformation.propTypes = {
};
RevisionInformation.defaultProps = {
originalProject: {},
originalProject: '',
originalRevision: '',
originalResultSet: {},
newProject: {},
newProject: '',
newRevision: '',
newResultSet: {},
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 { faCheck } from '@fortawesome/free-solid-svg-icons';
import PushModel from '../models/push';
import { genericErrorMessage } from '../helpers/constants';
import PushModel from '../../models/push';
import { genericErrorMessage } from '../../helpers/constants';
export default class SelectorCard extends React.Component {
constructor(props) {

Просмотреть файл

@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import SimpleTooltip from '../shared/SimpleTooltip';
import SimpleTooltip from '../../shared/SimpleTooltip';
import { displayNumber } from '../helpers';
import TooltipGraph from './TooltipGraph';
import { displayNumber } from './helpers';
const TableAverage = ({ value, stddev, stddevpct, replicates }) => {
let tooltipText;

Просмотреть файл

@ -72,13 +72,13 @@ export default class TooltipGraph extends React.Component {
{(minValue || maxValue) && (
<tbody>
<tr>
<td className="value-column">
<td className="value-column text-white">
{this.abbreviatedNumber(minValue)}
</td>
<td className="distribution-column">
<canvas ref={this.canvasRef} width={190} height={30} />
</td>
<td className="value-column">
<td className="value-column text-white">
{this.abbreviatedNumber(maxValue)}
</td>
</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 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 { getSeriesName, getTestName } from '../models/perfSeries';
import OptionCollectionModel from '../models/optionCollection';
@ -264,53 +264,6 @@ export const getCounterMap = function getCounterMap(
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(
seriesList,
resultSets,
@ -345,6 +298,60 @@ export const getGraphsLink = function getGraphsLink(
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
// TODO change all usage of signature_hash to signature.id
// for originalSignature and newSignature query params

Просмотреть файл

@ -8,7 +8,7 @@ const ErrorMessages = ({ failureMessage, errorMessages }) => {
return (
<div>
{messages.map(message => (
<Alert color="danger" key={message}>
<Alert color="danger" key={message} className="text-center">
{message}
</Alert>
))}