зеркало из https://github.com/mozilla/treeherder.git
Bug 1465589 - Convert Perfherder services to functions (#4243)
This commit is contained in:
Родитель
4a6095784c
Коммит
c6502e5bff
|
@ -128,7 +128,7 @@ module.exports = {
|
||||||
neutrino.config.performance
|
neutrino.config.performance
|
||||||
.hints('error')
|
.hints('error')
|
||||||
.maxAssetSize(1.20 * 1024 * 1024)
|
.maxAssetSize(1.20 * 1024 * 1024)
|
||||||
.maxEntrypointSize(1.63 * 1024 * 1024);
|
.maxEntrypointSize(1.64 * 1024 * 1024);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -27,8 +27,6 @@ import './js/filters';
|
||||||
import './js/models/perf/issue_tracker';
|
import './js/models/perf/issue_tracker';
|
||||||
import './js/models/perf/performance_framework';
|
import './js/models/perf/performance_framework';
|
||||||
import './js/models/perf/alerts';
|
import './js/models/perf/alerts';
|
||||||
import './js/services/perf/math';
|
|
||||||
import './js/services/perf/compare';
|
|
||||||
import './js/controllers/perf/compare';
|
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';
|
||||||
|
|
|
@ -34,3 +34,19 @@ export const destroy = function deleteRecord(uri) {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getData = async function getData(url) {
|
||||||
|
let failureStatus = null;
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
failureStatus = response.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.headers.get('content-type') === 'text/html' && failureStatus) {
|
||||||
|
return { data: { [failureStatus]: response.statusText }, failureStatus };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { data, failureStatus };
|
||||||
|
};
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { updateQueryParams, validateQueryParams, getData, mergeData, formatBugs } from './helpers';
|
import { updateQueryParams, validateQueryParams, mergeData, formatBugs } from './helpers';
|
||||||
import { graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl,
|
import { graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl,
|
||||||
bugzillaBugsApi } from '../helpers/url';
|
bugzillaBugsApi } from '../helpers/url';
|
||||||
|
import { getData } from '../helpers/http';
|
||||||
|
|
||||||
const withView = defaultState => WrappedComponent =>
|
const withView = defaultState => WrappedComponent =>
|
||||||
class View extends React.Component {
|
class View extends React.Component {
|
||||||
|
|
|
@ -119,22 +119,6 @@ export const validateQueryParams = function validateQueryParams(params, bugRequi
|
||||||
return messages;
|
return messages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getData = async function getData(url) {
|
|
||||||
let failureStatus = null;
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
failureStatus = response.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.headers.get('content-type') === 'text/html' && failureStatus) {
|
|
||||||
return { data: { [failureStatus]: response.statusText }, failureStatus };
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return { data, failureStatus };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tableRowStyling = function tableRowStyling(state, bug) {
|
export const tableRowStyling = function tableRowStyling(state, bug) {
|
||||||
if (bug) {
|
if (bug) {
|
||||||
const style = { color: '#aaa' };
|
const style = { color: '#aaa' };
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {
|
||||||
import PushModel from '../../../models/push';
|
import PushModel from '../../../models/push';
|
||||||
import RepositoryModel from '../../../models/repository';
|
import RepositoryModel from '../../../models/repository';
|
||||||
import PerfSeriesModel from '../../../models/perfSeries';
|
import PerfSeriesModel from '../../../models/perfSeries';
|
||||||
|
import { getCounterMap, getInterval, validateQueryParams, getResultsMap,
|
||||||
|
getGraphsLink } from '../../../perfherder/helpers';
|
||||||
|
|
||||||
perf.controller('CompareChooserCtrl', [
|
perf.controller('CompareChooserCtrl', [
|
||||||
'$state', '$stateParams', '$scope', '$q',
|
'$state', '$stateParams', '$scope', '$q',
|
||||||
|
@ -139,11 +141,9 @@ perf.controller('CompareChooserCtrl', [
|
||||||
perf.controller('CompareResultsCtrl', [
|
perf.controller('CompareResultsCtrl', [
|
||||||
'$state', '$stateParams', '$scope',
|
'$state', '$stateParams', '$scope',
|
||||||
'$httpParamSerializer', '$q', 'PhFramework',
|
'$httpParamSerializer', '$q', 'PhFramework',
|
||||||
'PhCompare',
|
|
||||||
function CompareResultsCtrl($state, $stateParams, $scope,
|
function CompareResultsCtrl($state, $stateParams, $scope,
|
||||||
$httpParamSerializer,
|
$httpParamSerializer,
|
||||||
$q, PhFramework,
|
$q, PhFramework) {
|
||||||
PhCompare) {
|
|
||||||
function displayResults(rawResultsMap, newRawResultsMap) {
|
function displayResults(rawResultsMap, newRawResultsMap) {
|
||||||
$scope.compareResults = {};
|
$scope.compareResults = {};
|
||||||
$scope.titles = {};
|
$scope.titles = {};
|
||||||
|
@ -174,7 +174,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
newRawResultsMap[sig].name === testName && newRawResultsMap[sig].platform === platform,
|
newRawResultsMap[sig].name === testName && newRawResultsMap[sig].platform === platform,
|
||||||
);
|
);
|
||||||
|
|
||||||
const cmap = PhCompare.getCounterMap(testName, rawResultsMap[oldSig], newRawResultsMap[newSig]);
|
const cmap = getCounterMap(testName, rawResultsMap[oldSig], newRawResultsMap[newSig]);
|
||||||
if (cmap.isEmpty) {
|
if (cmap.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -214,7 +214,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
|
|
||||||
cmap.links.push({
|
cmap.links.push({
|
||||||
title: 'graph',
|
title: 'graph',
|
||||||
href: PhCompare.getGraphsLink([...new Set(
|
href: getGraphsLink([...new Set(
|
||||||
[$scope.originalProject, $scope.newProject])].map(project => ({
|
[$scope.originalProject, $scope.newProject])].map(project => ({
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
signature: oldSig,
|
signature: oldSig,
|
||||||
|
@ -242,7 +242,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
|
|
||||||
cmap.links.push({
|
cmap.links.push({
|
||||||
title: 'graph',
|
title: 'graph',
|
||||||
href: PhCompare.getGraphsLink([...new Set(
|
href: getGraphsLink([...new Set(
|
||||||
[$scope.originalProject, $scope.newProject])].map(project => ({
|
[$scope.originalProject, $scope.newProject])].map(project => ({
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
signature: oldSig,
|
signature: oldSig,
|
||||||
|
@ -263,7 +263,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
const noiseMetricTestName = 'Noise Metric';
|
const noiseMetricTestName = 'Noise Metric';
|
||||||
$scope.compareResults[noiseMetricTestName] = [];
|
$scope.compareResults[noiseMetricTestName] = [];
|
||||||
$scope.platformList.forEach(function (platform) {
|
$scope.platformList.forEach(function (platform) {
|
||||||
const cmap = PhCompare.getCounterMap(noiseMetricTestName, $scope.oldStddevVariance[platform], $scope.newStddevVariance[platform]);
|
const cmap = getCounterMap(noiseMetricTestName, $scope.oldStddevVariance[platform], $scope.newStddevVariance[platform]);
|
||||||
if (cmap.isEmpty) {
|
if (cmap.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -288,7 +288,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
$scope.platformList = [];
|
$scope.platformList = [];
|
||||||
|
|
||||||
if ($scope.originalRevision) {
|
if ($scope.originalRevision) {
|
||||||
const timeRange = PhCompare.getInterval($scope.originalResultSet.push_timestamp, $scope.newResultSet.push_timestamp);
|
const timeRange = getInterval($scope.originalResultSet.push_timestamp, $scope.newResultSet.push_timestamp);
|
||||||
// Optimization - if old/new branches are the same collect data in one pass
|
// Optimization - if old/new branches are the same collect data in one pass
|
||||||
const resultSetIds = (isEqual($scope.originalProject, $scope.newProject)) ?
|
const resultSetIds = (isEqual($scope.originalProject, $scope.newProject)) ?
|
||||||
[$scope.originalResultSet.id, $scope.newResultSet.id] : [$scope.originalResultSet.id];
|
[$scope.originalResultSet.id, $scope.newResultSet.id] : [$scope.originalResultSet.id];
|
||||||
|
@ -302,7 +302,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
originalSeriesList.map(series => series.platform))];
|
originalSeriesList.map(series => series.platform))];
|
||||||
$scope.testList = [...new Set(
|
$scope.testList = [...new Set(
|
||||||
originalSeriesList.map(series => series.name))];
|
originalSeriesList.map(series => series.name))];
|
||||||
return PhCompare.getResultsMap($scope.originalProject.name,
|
return getResultsMap($scope.originalProject.name,
|
||||||
originalSeriesList,
|
originalSeriesList,
|
||||||
{ push_id: resultSetIds });
|
{ push_id: resultSetIds });
|
||||||
}).then((resultMaps) => {
|
}).then((resultMaps) => {
|
||||||
|
@ -329,7 +329,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
...$scope.testList,
|
...$scope.testList,
|
||||||
...new Set(newSeriesList.map(series => series.name)),
|
...new Set(newSeriesList.map(series => series.name)),
|
||||||
])];
|
])];
|
||||||
return PhCompare.getResultsMap($scope.newProject.name,
|
return getResultsMap($scope.newProject.name,
|
||||||
newSeriesList,
|
newSeriesList,
|
||||||
{ push_id: [$scope.newResultSet.id] });
|
{ push_id: [$scope.newResultSet.id] });
|
||||||
}).then((resultMaps) => {
|
}).then((resultMaps) => {
|
||||||
|
@ -349,7 +349,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
const startDateMs = ($scope.newResultSet.push_timestamp -
|
const startDateMs = ($scope.newResultSet.push_timestamp -
|
||||||
$scope.selectedTimeRange.value) * 1000;
|
$scope.selectedTimeRange.value) * 1000;
|
||||||
const endDateMs = $scope.newResultSet.push_timestamp * 1000;
|
const endDateMs = $scope.newResultSet.push_timestamp * 1000;
|
||||||
return PhCompare.getResultsMap(
|
return getResultsMap(
|
||||||
$scope.originalProject.name, originalSeriesList, {
|
$scope.originalProject.name, originalSeriesList, {
|
||||||
start_date: new Date(startDateMs).toISOString().slice(0, -5),
|
start_date: new Date(startDateMs).toISOString().slice(0, -5),
|
||||||
end_date: new Date(endDateMs).toISOString().slice(0, -5),
|
end_date: new Date(endDateMs).toISOString().slice(0, -5),
|
||||||
|
@ -368,7 +368,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
...$scope.testList,
|
...$scope.testList,
|
||||||
...new Set(newSeriesList.map(series => series.name)),
|
...new Set(newSeriesList.map(series => series.name)),
|
||||||
])];
|
])];
|
||||||
return PhCompare.getResultsMap($scope.newProject.name,
|
return getResultsMap($scope.newProject.name,
|
||||||
newSeriesList,
|
newSeriesList,
|
||||||
{ push_id: [$scope.newResultSet.id] });
|
{ push_id: [$scope.newResultSet.id] });
|
||||||
}).then((resultMaps) => {
|
}).then((resultMaps) => {
|
||||||
|
@ -447,10 +447,7 @@ perf.controller('CompareResultsCtrl', [
|
||||||
$scope.errors = [];
|
$scope.errors = [];
|
||||||
// validation works only for revision to revision comparison
|
// validation works only for revision to revision comparison
|
||||||
if ($stateParams.originalRevision) {
|
if ($stateParams.originalRevision) {
|
||||||
$scope.errors = PhCompare.validateInput($stateParams.originalProject,
|
$scope.errors = validateQueryParams($stateParams);
|
||||||
$stateParams.newProject,
|
|
||||||
$stateParams.originalRevision,
|
|
||||||
$stateParams.newRevision);
|
|
||||||
|
|
||||||
if ($scope.errors.length > 0) {
|
if ($scope.errors.length > 0) {
|
||||||
$scope.dataLoading = false;
|
$scope.dataLoading = false;
|
||||||
|
@ -515,9 +512,8 @@ perf.controller('CompareResultsCtrl', [
|
||||||
|
|
||||||
perf.controller('CompareSubtestResultsCtrl', [
|
perf.controller('CompareSubtestResultsCtrl', [
|
||||||
'$state', '$stateParams', '$scope', '$q',
|
'$state', '$stateParams', '$scope', '$q',
|
||||||
'PhCompare', '$httpParamSerializer',
|
'$httpParamSerializer',
|
||||||
function CompareSubtestResultsCtrl($state, $stateParams, $scope, $q,
|
function CompareSubtestResultsCtrl($state, $stateParams, $scope, $q,
|
||||||
PhCompare,
|
|
||||||
$httpParamSerializer) {
|
$httpParamSerializer) {
|
||||||
// TODO: duplicated from comparectrl
|
// TODO: duplicated from comparectrl
|
||||||
function verifyRevision(project, revision, rsid) {
|
function verifyRevision(project, revision, rsid) {
|
||||||
|
@ -573,7 +569,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
const oldSig = mapsigs[0];
|
const oldSig = mapsigs[0];
|
||||||
const newSig = mapsigs[1];
|
const newSig = mapsigs[1];
|
||||||
|
|
||||||
const cmap = PhCompare.getCounterMap(testName, rawResultsMap[oldSig], newRawResultsMap[newSig]);
|
const cmap = getCounterMap(testName, rawResultsMap[oldSig], newRawResultsMap[newSig]);
|
||||||
if (oldSig === $scope.originalSignature ||
|
if (oldSig === $scope.originalSignature ||
|
||||||
oldSig === $scope.newSignature ||
|
oldSig === $scope.newSignature ||
|
||||||
newSig === $scope.originalSignature ||
|
newSig === $scope.originalSignature ||
|
||||||
|
@ -596,7 +592,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
if ($scope.originalRevision) {
|
if ($scope.originalRevision) {
|
||||||
cmap.links = [{
|
cmap.links = [{
|
||||||
title: 'graph',
|
title: 'graph',
|
||||||
href: PhCompare.getGraphsLink([...new Set([
|
href: getGraphsLink([...new Set([
|
||||||
$scope.originalProject,
|
$scope.originalProject,
|
||||||
$scope.newProject,
|
$scope.newProject,
|
||||||
])].map(project => ({
|
])].map(project => ({
|
||||||
|
@ -622,7 +618,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
} else {
|
} else {
|
||||||
cmap.links = [{
|
cmap.links = [{
|
||||||
title: 'graph',
|
title: 'graph',
|
||||||
href: PhCompare.getGraphsLink([...new Set([
|
href: getGraphsLink([...new Set([
|
||||||
$scope.originalProject,
|
$scope.originalProject,
|
||||||
$scope.newProject,
|
$scope.newProject,
|
||||||
])].map(project => ({
|
])].map(project => ({
|
||||||
|
@ -637,7 +633,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
|
|
||||||
const noiseMetricTestName = 'Noise Metric';
|
const noiseMetricTestName = 'Noise Metric';
|
||||||
$scope.compareResults[noiseMetricTestName] = [];
|
$scope.compareResults[noiseMetricTestName] = [];
|
||||||
const cmap = PhCompare.getCounterMap(noiseMetricTestName, $scope.oldStddevVariance, $scope.newStddevVariance);
|
const cmap = getCounterMap(noiseMetricTestName, $scope.oldStddevVariance, $scope.newStddevVariance);
|
||||||
if (!cmap.isEmpty) {
|
if (!cmap.isEmpty) {
|
||||||
cmap.name = testName;
|
cmap.name = testName;
|
||||||
cmap.isNoiseMetric = true;
|
cmap.isNoiseMetric = true;
|
||||||
|
@ -655,12 +651,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
RepositoryModel.getList().then((repos) => {
|
RepositoryModel.getList().then((repos) => {
|
||||||
$scope.errors = [];
|
$scope.errors = [];
|
||||||
if ($stateParams.originalRevision) {
|
if ($stateParams.originalRevision) {
|
||||||
$scope.errors = PhCompare.validateInput($stateParams.originalProject,
|
$scope.errors = validateQueryParams($stateParams);
|
||||||
$stateParams.newProject,
|
|
||||||
$stateParams.originalRevision,
|
|
||||||
$stateParams.newRevision,
|
|
||||||
$stateParams.originalSignature,
|
|
||||||
$stateParams.newSignature);
|
|
||||||
|
|
||||||
if ($scope.errors.length > 0) {
|
if ($scope.errors.length > 0) {
|
||||||
$scope.dataLoading = false;
|
$scope.dataLoading = false;
|
||||||
|
@ -772,7 +763,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
}).then(function (originalSubtestList) {
|
}).then(function (originalSubtestList) {
|
||||||
$scope.pageList = originalSubtestList.map(subtest => subtest.name);
|
$scope.pageList = originalSubtestList.map(subtest => subtest.name);
|
||||||
$scope.platformList = [...new Set(originalSubtestList.map(subtest => subtest.platform))];
|
$scope.platformList = [...new Set(originalSubtestList.map(subtest => subtest.platform))];
|
||||||
return PhCompare.getResultsMap($scope.originalProject.name,
|
return getResultsMap($scope.originalProject.name,
|
||||||
originalSubtestList,
|
originalSubtestList,
|
||||||
{ push_id: resultSetIds });
|
{ push_id: resultSetIds });
|
||||||
}),
|
}),
|
||||||
|
@ -813,7 +804,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
...newSeriesList.map(series => series.name),
|
...newSeriesList.map(series => series.name),
|
||||||
])];
|
])];
|
||||||
|
|
||||||
return PhCompare.getResultsMap($scope.newProject.name,
|
return getResultsMap($scope.newProject.name,
|
||||||
newSeriesList,
|
newSeriesList,
|
||||||
{ push_id: [$scope.newResultSet.id] });
|
{ push_id: [$scope.newResultSet.id] });
|
||||||
}).then(function (newSeriesMaps) {
|
}).then(function (newSeriesMaps) {
|
||||||
|
@ -857,7 +848,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
const startDateMs = ($scope.newResultSet.push_timestamp -
|
const startDateMs = ($scope.newResultSet.push_timestamp -
|
||||||
$scope.selectedTimeRange.value) * 1000;
|
$scope.selectedTimeRange.value) * 1000;
|
||||||
const endDateMs = $scope.newResultSet.push_timestamp * 1000;
|
const endDateMs = $scope.newResultSet.push_timestamp * 1000;
|
||||||
return PhCompare.getResultsMap(
|
return getResultsMap(
|
||||||
$scope.originalProject.name,
|
$scope.originalProject.name,
|
||||||
originalSubtestList, {
|
originalSubtestList, {
|
||||||
start_date: new Date(startDateMs).toISOString().slice(0, -5),
|
start_date: new Date(startDateMs).toISOString().slice(0, -5),
|
||||||
|
@ -882,7 +873,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
||||||
...newSeriesList.map(series => series.name),
|
...newSeriesList.map(series => series.name),
|
||||||
])];
|
])];
|
||||||
|
|
||||||
return PhCompare.getResultsMap($scope.newProject.name,
|
return getResultsMap($scope.newProject.name,
|
||||||
newSeriesList,
|
newSeriesList,
|
||||||
{ push_id: [$scope.newResultSet.id] });
|
{ push_id: [$scope.newResultSet.id] });
|
||||||
}).then(function (newSeriesMaps) {
|
}).then(function (newSeriesMaps) {
|
||||||
|
|
|
@ -1,301 +0,0 @@
|
||||||
// Remove the eslint-disable when rewriting this file during the React conversion.
|
|
||||||
/* eslint-disable func-names, no-nested-ternary, object-shorthand, prefer-arrow-callback, prefer-template */
|
|
||||||
import forIn from 'lodash/forIn';
|
|
||||||
import chunk from 'lodash/chunk';
|
|
||||||
|
|
||||||
import treeherder from '../../treeherder';
|
|
||||||
import { getApiUrl } from '../../../helpers/url';
|
|
||||||
import { phTimeRanges } from '../../../helpers/constants';
|
|
||||||
import PerfSeriesModel from '../../../models/perfSeries';
|
|
||||||
|
|
||||||
treeherder.factory('PhCompare', [
|
|
||||||
'$q', '$http', '$httpParamSerializer', 'math',
|
|
||||||
function ($q, $http, $httpParamSerializer, math) {
|
|
||||||
|
|
||||||
// Used for t_test: default stddev if both sets have only a single value - 15%.
|
|
||||||
// Should be rare case and it's unreliable, but at least have something.
|
|
||||||
const STDDEV_DEFAULT_FACTOR = 0.15;
|
|
||||||
|
|
||||||
const RATIO_CARE_MIN = 1.02; // We don't care about less than ~2% diff
|
|
||||||
const T_VALUE_CARE_MIN = 3; // Anything below this is "low" in confidence
|
|
||||||
const T_VALUE_CONFIDENT = 5; // Anything above this is "high" in confidence
|
|
||||||
|
|
||||||
function getClassName(newIsBetter, oldVal, newVal, abs_t_value) {
|
|
||||||
// NOTE: we care about general ratio rather than how much is new compared
|
|
||||||
// to old - this could end up with slightly higher or lower threshold
|
|
||||||
// in practice than indicated by DIFF_CARE_MIN. E.g.:
|
|
||||||
// - If old is 10 and new is 5, then new = old -50%
|
|
||||||
// - If old is 5 and new is 10, then new = old + 100%
|
|
||||||
// And if the threshold was 75% then one would matter and the other wouldn't.
|
|
||||||
// Instead, we treat both cases as 2.0 (general ratio), and both would matter
|
|
||||||
// if our threshold was 75% (i.e. DIFF_CARE_MIN = 1.75).
|
|
||||||
if (!oldVal || !newVal) {
|
|
||||||
// handle null case
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
let ratio = newVal / oldVal;
|
|
||||||
if (ratio < 1) {
|
|
||||||
ratio = 1 / ratio; // Direction agnostic and always >= 1.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ratio < RATIO_CARE_MIN || abs_t_value < T_VALUE_CARE_MIN) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abs_t_value < T_VALUE_CONFIDENT) {
|
|
||||||
// Since we (currently) have only one return value to indicate uncertainty,
|
|
||||||
// let's use it for regressions only. (Improvement would just not be marked).
|
|
||||||
return newIsBetter ? '' : 'compare-notsure';
|
|
||||||
}
|
|
||||||
|
|
||||||
return newIsBetter ? 'compare-improvement' : 'compare-regression';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Aggregates two sets of values into a "comparison object" which is later used
|
|
||||||
// to display a single line of comparison.
|
|
||||||
// The result object has the following properties:
|
|
||||||
// - .isEmpty: true if no data for either side.
|
|
||||||
// If !isEmpty, for originalData/newData (if the data exists)
|
|
||||||
// - .[original|new]Value // Average of the values
|
|
||||||
// - .[original|new]Stddev // stddev
|
|
||||||
// - .[original|new]StddevPct // stddev as percentage of the average
|
|
||||||
// - .[original|new]Runs // Display data: number of runs and their values
|
|
||||||
// If both originalData/newData exist, comparison data:
|
|
||||||
// - .newIsBetter // is new result better or worse (even if unsure)
|
|
||||||
// - .isImprovement // is new result better + we're confident about it
|
|
||||||
// - .isRegression // is new result worse + we're confident about it
|
|
||||||
// - .delta
|
|
||||||
// - .deltaPercentage
|
|
||||||
// - .confidence // t-test value
|
|
||||||
// - .confidenceText // 'low'/'med'/'high'
|
|
||||||
// - .confidenceTextLong // more explanation on what confidenceText means
|
|
||||||
// - .isMeaningful // for highlighting - bool over t-test threshold
|
|
||||||
// And some data to help formatting of the comparison:
|
|
||||||
// - .className
|
|
||||||
// - .magnitude
|
|
||||||
// - .marginDirection
|
|
||||||
getCounterMap: function getDisplayLineData(testName, originalData, newData) {
|
|
||||||
|
|
||||||
function numericCompare(a, b) {
|
|
||||||
return a < b ? -1 : a > b ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some statistics for a single set of values
|
|
||||||
function analyzeSet(values, testName) {
|
|
||||||
let average;
|
|
||||||
let stddev;
|
|
||||||
|
|
||||||
if (testName === 'Noise Metric') {
|
|
||||||
average = Math.sqrt(values.map(x => x ** 2).reduce((a, b) => a + b, 0));
|
|
||||||
stddev = 1;
|
|
||||||
} else {
|
|
||||||
average = math.average(values);
|
|
||||||
stddev = math.stddev(values, average);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
average: average,
|
|
||||||
stddev: stddev,
|
|
||||||
stddevPct: Math.round(math.percentOf(stddev, average) * 100) / 100,
|
|
||||||
|
|
||||||
// We use slice to keep the original values at their original order
|
|
||||||
// in case the order is important elsewhere.
|
|
||||||
runs: values.slice().sort(numericCompare),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eventually the result object, after setting properties as required.
|
|
||||||
const cmap = { isEmpty: true };
|
|
||||||
|
|
||||||
// It's possible to get an object with empty values, so check for that too.
|
|
||||||
const hasOrig = originalData && originalData.values.length;
|
|
||||||
const hasNew = newData && newData.values.length;
|
|
||||||
|
|
||||||
if (!hasOrig && !hasNew) {
|
|
||||||
return cmap; // No data for either side
|
|
||||||
}
|
|
||||||
|
|
||||||
cmap.isEmpty = false;
|
|
||||||
|
|
||||||
if (hasOrig) {
|
|
||||||
const orig = analyzeSet(originalData.values, testName);
|
|
||||||
cmap.originalValue = orig.average;
|
|
||||||
cmap.originalRuns = orig.runs;
|
|
||||||
cmap.originalStddev = orig.stddev;
|
|
||||||
cmap.originalStddevPct = orig.stddevPct;
|
|
||||||
} else {
|
|
||||||
cmap.originalRuns = [];
|
|
||||||
}
|
|
||||||
if (hasNew) {
|
|
||||||
const newd = analyzeSet(newData.values, testName);
|
|
||||||
cmap.newValue = newd.average;
|
|
||||||
cmap.newRuns = newd.runs;
|
|
||||||
cmap.newStddev = newd.stddev;
|
|
||||||
cmap.newStddevPct = newd.stddevPct;
|
|
||||||
} else {
|
|
||||||
cmap.newRuns = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasOrig || !hasNew) {
|
|
||||||
return cmap; // No comparison, just display for one side.
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep the framework id so we can filter by that later, if necessary
|
|
||||||
cmap.frameworkId = originalData.frameworkId;
|
|
||||||
|
|
||||||
// Compare the sides.
|
|
||||||
// Normally tests are "lower is better", can be over-ridden with a series option
|
|
||||||
cmap.delta = (cmap.newValue - cmap.originalValue);
|
|
||||||
cmap.newIsBetter = (originalData.lowerIsBetter && cmap.delta < 0) ||
|
|
||||||
(!originalData.lowerIsBetter && cmap.delta > 0);
|
|
||||||
|
|
||||||
// delta percentage (for display)
|
|
||||||
cmap.deltaPercentage = math.percentOf(cmap.delta, cmap.originalValue);
|
|
||||||
// arbitrary scale from 0-20% multiplied by 5, capped
|
|
||||||
// at 100 (so 20% regression === 100% bad)
|
|
||||||
cmap.magnitude = Math.min(Math.abs(cmap.deltaPercentage) * 5, 100);
|
|
||||||
|
|
||||||
const abs_t_value = Math.abs(math.t_test(originalData.values, newData.values, STDDEV_DEFAULT_FACTOR));
|
|
||||||
cmap.className = getClassName(cmap.newIsBetter, cmap.originalValue, cmap.newValue, abs_t_value);
|
|
||||||
cmap.confidence = abs_t_value;
|
|
||||||
cmap.confidenceTextLong = 'Result of running t-test on base versus new result distribution: ';
|
|
||||||
if (abs_t_value < T_VALUE_CARE_MIN) {
|
|
||||||
cmap.confidenceText = 'low';
|
|
||||||
cmap.confidenceTextLong += "A value of 'low' suggests less confidence that there is a sustained, significant change between the two revisions.";
|
|
||||||
} else if (abs_t_value < T_VALUE_CONFIDENT) {
|
|
||||||
cmap.confidenceText = 'med';
|
|
||||||
cmap.confidenceTextLong += "A value of 'med' indicates uncertainty that there is a significant change. If you haven't already, consider retriggering the job to be more sure.";
|
|
||||||
} else {
|
|
||||||
cmap.confidenceText = 'high';
|
|
||||||
cmap.confidenceTextLong += "A value of 'high' indicates more confidence that there is a significant change, however you should check the historical record for the test by looking at the graph to be more sure (some noisy tests can provide inconsistent results).";
|
|
||||||
}
|
|
||||||
cmap.isRegression = (cmap.className === 'compare-regression');
|
|
||||||
cmap.isImprovement = (cmap.className === 'compare-improvement');
|
|
||||||
cmap.isMeaningful = (cmap.className !== '');
|
|
||||||
cmap.isComplete = (cmap.originalRuns.length &&
|
|
||||||
cmap.newRuns.length);
|
|
||||||
cmap.isConfident = ((cmap.originalRuns.length > 1 &&
|
|
||||||
cmap.newRuns.length > 1 &&
|
|
||||||
abs_t_value >= T_VALUE_CONFIDENT) ||
|
|
||||||
(cmap.originalRuns.length >= 6 &&
|
|
||||||
cmap.newRuns.length >= 6 &&
|
|
||||||
abs_t_value >= T_VALUE_CARE_MIN));
|
|
||||||
cmap.needsMoreRuns = (cmap.isComplete && !cmap.isConfident &&
|
|
||||||
cmap.originalRuns.length < 6);
|
|
||||||
cmap.isNoiseMetric = false;
|
|
||||||
|
|
||||||
return cmap;
|
|
||||||
},
|
|
||||||
|
|
||||||
getInterval: function (oldTimestamp, newTimestamp) {
|
|
||||||
const now = (new Date()).getTime() / 1000;
|
|
||||||
let timeRange = Math.min(oldTimestamp, newTimestamp);
|
|
||||||
timeRange = Math.round(now - timeRange);
|
|
||||||
|
|
||||||
// now figure out which predefined set of data we can query from
|
|
||||||
const phTimeRange = phTimeRanges.find(i => timeRange <= i.value);
|
|
||||||
return phTimeRange.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
validateInput: function (originalProject, newProject,
|
|
||||||
originalRevision, newRevision,
|
|
||||||
originalSignature, newSignature) {
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
$http.get(getApiUrl('/repository/')).then(function (response) {
|
|
||||||
if (!response.data.find(project => project.name === originalProject)) {
|
|
||||||
errors.push("Invalid project, doesn't exist: " + originalProject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.data.find(project => project.name === newProject)) {
|
|
||||||
errors.push("Invalid project, doesn't exist: " + newProject);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return errors;
|
|
||||||
},
|
|
||||||
|
|
||||||
getResultsMap: (projectName, seriesList, params) => {
|
|
||||||
const resultsMap = {};
|
|
||||||
return $q.all(chunk(seriesList, 40).map(
|
|
||||||
seriesChunk => PerfSeriesModel.getSeriesData(
|
|
||||||
projectName, {
|
|
||||||
signature_id: seriesChunk.map(series => series.id),
|
|
||||||
framework: [...new Set(seriesChunk.map(series => series.frameworkId))],
|
|
||||||
...params,
|
|
||||||
}).then((seriesData) => {
|
|
||||||
// Aggregates data from the server on a single group of values which
|
|
||||||
// will be compared later to another group. Ends up with an object
|
|
||||||
// with description (name/platform) and values.
|
|
||||||
// The values are later processed at getCounterMap as the data arguments.
|
|
||||||
forIn(seriesData, (data, signatureHash) => {
|
|
||||||
const signature = seriesList.find(series =>
|
|
||||||
series.signature === signatureHash);
|
|
||||||
if (signature) {
|
|
||||||
// helper method to either return the push
|
|
||||||
// index (if getting per-push results) or
|
|
||||||
// just the main results map object otherwise
|
|
||||||
const _getResultMapEntry = (datum) => {
|
|
||||||
if (params.push_id) {
|
|
||||||
if (!resultsMap[datum.push_id]) {
|
|
||||||
resultsMap[datum.push_id] = {};
|
|
||||||
}
|
|
||||||
return resultsMap[datum.push_id];
|
|
||||||
}
|
|
||||||
return resultsMap;
|
|
||||||
};
|
|
||||||
data.forEach((datum) => {
|
|
||||||
const entry = _getResultMapEntry(datum);
|
|
||||||
if (!entry[signatureHash]) {
|
|
||||||
entry[signatureHash] = {
|
|
||||||
...signature,
|
|
||||||
values: [datum.value],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
entry[signatureHash].values.push(datum.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
)).then(() => resultsMap);
|
|
||||||
},
|
|
||||||
|
|
||||||
getGraphsLink: function (seriesList, resultSets, timeRange) {
|
|
||||||
let graphsLink = 'perf.html#/graphs?' + $httpParamSerializer({
|
|
||||||
series: seriesList.map(series => ([
|
|
||||||
series.projectName,
|
|
||||||
series.signature, 1,
|
|
||||||
series.frameworkId,
|
|
||||||
])),
|
|
||||||
highlightedRevisions: resultSets.map(resultSet => (
|
|
||||||
resultSet.revision.slice(0, 12)
|
|
||||||
)),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resultSets) {
|
|
||||||
if (!timeRange) {
|
|
||||||
graphsLink += '&timerange=' + Math.max(
|
|
||||||
...resultSets.map(resultSet =>
|
|
||||||
phTimeRanges.map(range => range.value).find(t =>
|
|
||||||
((Date.now() / 1000.0) -
|
|
||||||
resultSet.push_timestamp) < t),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
graphsLink += '&timerange=' + timeRange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return graphsLink;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -1,78 +0,0 @@
|
||||||
// Remove the eslint-disable when rewriting this file during the React conversion.
|
|
||||||
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
|
|
||||||
import treeherder from '../../treeherder';
|
|
||||||
|
|
||||||
treeherder.factory('math', [
|
|
||||||
function () {
|
|
||||||
function percentOf(a, b) {
|
|
||||||
return b ? 100 * a / b : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function average(values) {
|
|
||||||
if (values.length < 1) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stddev(values, avg) {
|
|
||||||
if (values.length < 2) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!avg) avg = average(values);
|
|
||||||
|
|
||||||
return Math.sqrt(
|
|
||||||
values.map(v => (v - avg) ** 2)
|
|
||||||
.reduce(function (a, b) { return a + b; }) / (values.length - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a set has only one value, assume average-ish-plus sddev, which
|
|
||||||
// will manifest as smaller t-value the less items there are at the group
|
|
||||||
// (so quite small for 1 value). This default value is a parameter.
|
|
||||||
// C/T mean control/test group (in our case original/new data).
|
|
||||||
function t_test(valuesC, valuesT, stddev_default_factor) {
|
|
||||||
const lenC = valuesC.length;
|
|
||||||
const lenT = valuesT.length;
|
|
||||||
|
|
||||||
// We must have at least one value at each set
|
|
||||||
if (lenC < 1 || lenT < 1) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgC = average(valuesC);
|
|
||||||
const avgT = average(valuesT);
|
|
||||||
|
|
||||||
// Use actual stddev if possible, or stddev_default_factor if one sample
|
|
||||||
let stddevC = (lenC > 1 ? stddev(valuesC, avgC) : stddev_default_factor * avgC);
|
|
||||||
let stddevT = (lenT > 1 ? stddev(valuesT, avgT) : stddev_default_factor * avgT);
|
|
||||||
|
|
||||||
// If one of the sets has only a single sample, assume its stddev is
|
|
||||||
// the same as that of the other set (in percentage). If both sets
|
|
||||||
// have only one sample, both will use stddev_default_factor.
|
|
||||||
if (lenC === 1) {
|
|
||||||
stddevC = valuesC[0] * stddevT / avgT;
|
|
||||||
} else if (lenT === 1) {
|
|
||||||
stddevT = valuesT[0] * stddevC / avgC;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delta = avgT - avgC;
|
|
||||||
const stdDiffErr = (
|
|
||||||
Math.sqrt(
|
|
||||||
stddevC * stddevC / lenC // control-variance / control-size
|
|
||||||
+
|
|
||||||
stddevT * stddevT / lenT, // ...
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return delta / stdDiffErr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
percentOf: percentOf,
|
|
||||||
average: average,
|
|
||||||
stddev: stddev,
|
|
||||||
t_test: t_test,
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const tValueCareMin = 3; // Anything below this is "low" in confidence
|
||||||
|
export const tValueConfidence = 5; // Anything above this is "high" in confidence
|
|
@ -0,0 +1,334 @@
|
||||||
|
import chunk from 'lodash/chunk';
|
||||||
|
|
||||||
|
import { tValueCareMin, tValueConfidence } from './constants';
|
||||||
|
import { getApiUrl, createQueryParams } from '../helpers/url';
|
||||||
|
import { getData } from '../helpers/http';
|
||||||
|
import PerfSeriesModel from '../models/perfSeries';
|
||||||
|
import { phTimeRanges } from '../helpers/constants';
|
||||||
|
|
||||||
|
export const calcPercentOf = function calcPercentOf(a, b) {
|
||||||
|
return b ? 100 * a / b : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calcAverage = function calcAverage(values) {
|
||||||
|
if (!values.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStdDev = function getStandardDeviation(values, avg) {
|
||||||
|
if (values.length < 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!avg) avg = calcAverage(values);
|
||||||
|
|
||||||
|
return Math.sqrt(
|
||||||
|
values.map(v => (v - avg) ** 2).reduce((a, b) => a + b) / (values.length - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// If a set has only one value, assume average-ish-plus standard deviation, which
|
||||||
|
// will manifest as smaller t-value the less items there are at the group
|
||||||
|
// (so quite small for 1 value). This default value is a parameter.
|
||||||
|
// C/T mean control/test group (in our case original/new data).
|
||||||
|
export const getTTest = function getTTest(valuesC, valuesT, stddev_default_factor) {
|
||||||
|
const lenC = valuesC.length;
|
||||||
|
const lenT = valuesT.length;
|
||||||
|
|
||||||
|
if (!lenC || !lenT) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avgC = calcAverage(valuesC);
|
||||||
|
const avgT = calcAverage(valuesT);
|
||||||
|
let stddevC = (lenC > 1 ? getStdDev(valuesC, avgC) : stddev_default_factor * avgC);
|
||||||
|
let stddevT = (lenT > 1 ? getStdDev(valuesT, avgT) : stddev_default_factor * avgT);
|
||||||
|
|
||||||
|
if (lenC === 1) {
|
||||||
|
stddevC = valuesC[0] * stddevT / avgT;
|
||||||
|
} else if (lenT === 1) {
|
||||||
|
stddevT = valuesT[0] * stddevC / avgC;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = avgT - avgC;
|
||||||
|
const stdDiffErr = (
|
||||||
|
Math.sqrt(
|
||||||
|
(stddevC * stddevC / lenC) // control-variance / control-size
|
||||||
|
+
|
||||||
|
(stddevT * stddevT / lenT),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return delta / stdDiffErr;
|
||||||
|
};
|
||||||
|
// TODO many of these are only used in one controller so can likely be moved
|
||||||
|
// into the appropriate react component
|
||||||
|
|
||||||
|
const numericCompare = (a, b) => {
|
||||||
|
if (a < b) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a > b) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const analyzeSet = (values, testName) => {
|
||||||
|
let average;
|
||||||
|
let stddev = 1;
|
||||||
|
|
||||||
|
if (testName === 'Noise Metric') {
|
||||||
|
average = Math.sqrt(values.map(x => x ** 2).reduce((a, b) => a + b, 0));
|
||||||
|
} else {
|
||||||
|
average = calcAverage(values);
|
||||||
|
stddev = getStdDev(values, average);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
average,
|
||||||
|
stddev,
|
||||||
|
stddevPct: Math.round(calcPercentOf(stddev, average) * 100) / 100,
|
||||||
|
// TODO verify this is needed
|
||||||
|
// We use slice to keep the original values at their original order
|
||||||
|
// in case the order is important elsewhere.
|
||||||
|
runs: values.slice().sort(numericCompare),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClassName = (newIsBetter, oldVal, newVal, absTValue) => {
|
||||||
|
// Returns a class name, if any, based on a relative change in the absolute value
|
||||||
|
if (!oldVal || !newVal) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let ratio = newVal / oldVal;
|
||||||
|
if (ratio < 1) {
|
||||||
|
ratio = 1 / ratio; // Direction agnostic and always >= 1.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratio < 1.02 || absTValue < tValueCareMin) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absTValue < tValueConfidence) {
|
||||||
|
return newIsBetter ? '' : 'compare-notsure';
|
||||||
|
}
|
||||||
|
|
||||||
|
return newIsBetter ? 'compare-improvement' : 'compare-regression';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aggregates two sets of values into a "comparison object" which is later used
|
||||||
|
// to display a single line of comparison.
|
||||||
|
// The result object has the following properties:
|
||||||
|
// - .isEmpty: true if no data for either side.
|
||||||
|
// If !isEmpty, for originalData/newData (if the data exists)
|
||||||
|
// - .[original|new]Value // Average of the values
|
||||||
|
// - .[original|new]Stddev // stddev
|
||||||
|
// - .[original|new]StddevPct // stddev as percentage of the average
|
||||||
|
// - .[original|new]Runs // Display data: number of runs and their values
|
||||||
|
// If both originalData/newData exist, comparison data:
|
||||||
|
// - .newIsBetter // is new result better or worse (even if unsure)
|
||||||
|
// - .isImprovement // is new result better + we're confident about it
|
||||||
|
// - .isRegression // is new result worse + we're confident about it
|
||||||
|
// - .delta
|
||||||
|
// - .deltaPercentage
|
||||||
|
// - .confidence // t-test value
|
||||||
|
// - .confidenceText // 'low'/'med'/'high'
|
||||||
|
// - .confidenceTextLong // more explanation on what confidenceText means
|
||||||
|
// - .isMeaningful // for highlighting - bool over t-test threshold
|
||||||
|
// And some data to help formatting of the comparison:
|
||||||
|
// - .className
|
||||||
|
// - .magnitude
|
||||||
|
// - .marginDirection
|
||||||
|
|
||||||
|
export const getCounterMap = function getCounterMap(testName, originalData, newData) {
|
||||||
|
// TODO setting this value seems a bit odd, look into how its being used
|
||||||
|
const cmap = { isEmpty: false };
|
||||||
|
const hasOrig = originalData && originalData.values.length;
|
||||||
|
const hasNew = newData && newData.values.length;
|
||||||
|
|
||||||
|
if (!hasOrig && !hasNew) {
|
||||||
|
cmap.isEmpty = true;
|
||||||
|
return cmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOrig) {
|
||||||
|
const orig = analyzeSet(originalData.values, testName);
|
||||||
|
cmap.originalValue = orig.average;
|
||||||
|
cmap.originalRuns = orig.runs;
|
||||||
|
cmap.originalStddev = orig.stddev;
|
||||||
|
cmap.originalStddevPct = orig.stddevPct;
|
||||||
|
} else {
|
||||||
|
cmap.originalRuns = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNew) {
|
||||||
|
const newd = analyzeSet(newData.values, testName);
|
||||||
|
cmap.newValue = newd.average;
|
||||||
|
cmap.newRuns = newd.runs;
|
||||||
|
cmap.newStddev = newd.stddev;
|
||||||
|
cmap.newStddevPct = newd.stddevPct;
|
||||||
|
} else {
|
||||||
|
cmap.newRuns = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasOrig || !hasNew) {
|
||||||
|
return cmap; // No comparison, just display for one side.
|
||||||
|
}
|
||||||
|
|
||||||
|
cmap.frameworkId = originalData.frameworkId;
|
||||||
|
// Normally tests are "lower is better", can be over-ridden with a series option
|
||||||
|
cmap.delta = (cmap.newValue - cmap.originalValue);
|
||||||
|
cmap.newIsBetter = (originalData.lowerIsBetter && cmap.delta < 0) ||
|
||||||
|
(!originalData.lowerIsBetter && cmap.delta > 0);
|
||||||
|
|
||||||
|
cmap.deltaPercentage = calcPercentOf(cmap.delta, cmap.originalValue);
|
||||||
|
// arbitrary scale from 0-20% multiplied by 5, capped
|
||||||
|
// at 100 (so 20% regression === 100% bad)
|
||||||
|
cmap.magnitude = Math.min(Math.abs(cmap.deltaPercentage) * 5, 100);
|
||||||
|
|
||||||
|
// 0.15 is used for getTTest: default stddev if both sets have only a single value - 15%.
|
||||||
|
// Should be rare case and it's unreliable, but at least have something.
|
||||||
|
const absTValue = Math.abs(getTTest(originalData.values, newData.values, 0.15));
|
||||||
|
cmap.className = getClassName(cmap.newIsBetter, cmap.originalValue, cmap.newValue, absTValue);
|
||||||
|
cmap.confidence = absTValue;
|
||||||
|
cmap.confidenceTextLong = 'Result of running t-test on base versus new result distribution: ';
|
||||||
|
|
||||||
|
if (absTValue < tValueCareMin) {
|
||||||
|
cmap.confidenceText = 'low';
|
||||||
|
cmap.confidenceTextLong += "A value of 'low' suggests less confidence that there is a sustained, significant change between the two revisions.";
|
||||||
|
} else if (absTValue < tValueConfidence) {
|
||||||
|
cmap.confidenceText = 'med';
|
||||||
|
cmap.confidenceTextLong += "A value of 'med' indicates uncertainty that there is a significant change. If you haven't already, consider retriggering the job to be more sure.";
|
||||||
|
} else {
|
||||||
|
cmap.confidenceText = 'high';
|
||||||
|
cmap.confidenceTextLong += "A value of 'high' indicates more confidence that there is a significant change, however you should check the historical record for the test by looking at the graph to be more sure (some noisy tests can provide inconsistent results).";
|
||||||
|
}
|
||||||
|
cmap.isRegression = (cmap.className === 'compare-regression');
|
||||||
|
cmap.isImprovement = (cmap.className === 'compare-improvement');
|
||||||
|
cmap.isMeaningful = (cmap.className !== '');
|
||||||
|
|
||||||
|
cmap.isComplete = (cmap.originalRuns.length &&
|
||||||
|
cmap.newRuns.length);
|
||||||
|
cmap.isConfident = ((cmap.originalRuns.length > 1 &&
|
||||||
|
cmap.newRuns.length > 1 &&
|
||||||
|
absTValue >= tValueConfidence) ||
|
||||||
|
(cmap.originalRuns.length >= 6 &&
|
||||||
|
cmap.newRuns.length >= 6 &&
|
||||||
|
absTValue >= tValueCareMin));
|
||||||
|
cmap.needsMoreRuns = (cmap.isComplete && !cmap.isConfident &&
|
||||||
|
cmap.originalRuns.length < 6);
|
||||||
|
cmap.isNoiseMetric = false;
|
||||||
|
|
||||||
|
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('/repository/'));
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResultMapEntry = (datum, resultsMap, params) => {
|
||||||
|
if (params.push_id) {
|
||||||
|
if (!resultsMap[datum.push_id]) {
|
||||||
|
resultsMap[datum.push_id] = {};
|
||||||
|
}
|
||||||
|
return resultsMap[datum.push_id];
|
||||||
|
}
|
||||||
|
return resultsMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getResultsMap = function getResultsMap(projectName, seriesList, params) {
|
||||||
|
const resultsMap = {};
|
||||||
|
|
||||||
|
return Promise.all(chunk(seriesList, 150).map(
|
||||||
|
seriesChunk => PerfSeriesModel.getSeriesData(
|
||||||
|
projectName, {
|
||||||
|
signature_id: seriesChunk.map(series => series.id),
|
||||||
|
framework: [...new Set(seriesChunk.map(series => series.frameworkId))],
|
||||||
|
...params,
|
||||||
|
}).then((seriesData) => {
|
||||||
|
// Aggregates data from a single group of values and returns an object containing
|
||||||
|
// description (name/platform) and values; these are later processed in getCounterMap.
|
||||||
|
for (const [signatureHash, data] of Object.entries(seriesData)) {
|
||||||
|
const signature = seriesList.find(series => series.signature === signatureHash);
|
||||||
|
|
||||||
|
if (signature) {
|
||||||
|
data.forEach((datum) => {
|
||||||
|
const entry = getResultMapEntry(datum, resultsMap, params);
|
||||||
|
if (!entry[signatureHash]) {
|
||||||
|
entry[signatureHash] = {
|
||||||
|
...signature,
|
||||||
|
values: [datum.value],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
entry[signatureHash].values.push(datum.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)).then(() => resultsMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGraphsLink = function getGraphsLink(seriesList, resultSets, timeRange) {
|
||||||
|
const params = {
|
||||||
|
series: seriesList.map(series => ([
|
||||||
|
series.projectName,
|
||||||
|
series.signature, 1,
|
||||||
|
series.frameworkId,
|
||||||
|
])),
|
||||||
|
highlightedRevisions: resultSets.map(resultSet => (
|
||||||
|
resultSet.revision.slice(0, 12)
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resultSets && !timeRange) {
|
||||||
|
params.timerange = Math.max(
|
||||||
|
...resultSets.map(resultSet =>
|
||||||
|
phTimeRanges.map(range => range.value).find(t =>
|
||||||
|
((Date.now() / 1000.0) - resultSet.push_timestamp) < t),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeRange) {
|
||||||
|
params.timerange = timeRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `perf.html#/graphs${createQueryParams(params)}`;
|
||||||
|
};
|
Загрузка…
Ссылка в новой задаче