Bug 1142680 - initial work to get a compare-talos equivalent stood up

This commit is contained in:
Joel Maher 2015-04-10 13:53:45 -04:00 коммит произвёл William Lachance
Родитель 87325652ad
Коммит fd0f772aa6
6 изменённых файлов: 649 добавлений и 2 удалений

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

@ -51,9 +51,17 @@ module.exports = function(grunt) {
dest:'dist'
}
},
compare: {
src:'webapp/app/compare.html',
nonull: true,
options:{
dest:'dist'
}
},
},
usemin:{ html:['dist/index.html', 'dist/help.html', 'dist/logviewer.html', 'dist/perf.html'] },
usemin:{ html:['dist/index.html', 'dist/help.html', 'dist/logviewer.html',
'dist/perf.html', 'dist/compare.html'] },
'cache-busting': {
indexjs: {
@ -74,6 +82,12 @@ module.exports = function(grunt) {
file: 'dist/js/perf.min.js',
cleanup: true
},
comparejs: {
replace: ['dist/**/*.html'],
replacement: 'compare.min.js',
file: 'dist/js/compare.min.js',
cleanup: true
},
indexcss: {
replace: ['dist/**/*.html'],
replacement: 'index.min.css',
@ -107,7 +121,8 @@ module.exports = function(grunt) {
{ src:'webapp/app/index.html', dest:'dist/index.html', nonull: true },
{ src:'webapp/app/help.html', dest:'dist/help.html', nonull: true },
{ src:'webapp/app/logviewer.html', dest:'dist/logviewer.html', nonull: true },
{ src:'webapp/app/perf.html', dest:'dist/perf.html', nonull: true }
{ src:'webapp/app/perf.html', dest:'dist/perf.html', nonull: true },
{ src:'webapp/app/compare.html', dest:'dist/compare.html', nonull: true }
]
},
// Copy img dir
@ -215,6 +230,26 @@ module.exports = function(grunt) {
keepClosingSlash: true
}
}
},
compare: {
cwd: 'webapp/app',
src: 'partials/perf/*.html',
dest: 'dist/js/compare.min.js',
options: {
usemin: 'dist/js/compare.min.js',
append: true,
htmlmin: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true
}
}
}
}
});

40
ui/compareperf.html Normal file
Просмотреть файл

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html ng-app="compare">
<head>
<meta charset="iso-8859-1" />
<title>Compare Performance</title>
<!-- build:css css/perf.min.css -->
<link href="css/bootstrap.css" rel="stylesheet" media="screen">
<link href="css/perf.css" rel="stylesheet" media="screen">
<!-- endbuild -->
<link id="favicon" type="image/png" rel="shortcut icon" href="img/tree_open.png">
<style>
</style>
</head>
<body>
<section ui-view>
</section>
<script src="js/config/local.conf.js"></script>
<!-- build:js js/compare.min.js -->
<script src="vendor/jquery-2.0.3.js"></script>
<script src="vendor/underscore-min.js"></script>
<script src="vendor/angular/angular.js"></script>
<script src="vendor/angular/angular-route.js"></script>
<script src="vendor/angular/angular-resource.js"></script>
<script src="vendor/angular/angular-cookies.js"></script>
<script src="vendor/angular/angular-ui-router.js"></script>
<script src="vendor/angular/angular-sanitize.min.js"></script>
<script src="vendor/angular-local-storage.min.js"></script>
<script src="vendor/ui-bootstrap-tpls-0.11.2.min.js"></script>
<script src="vendor/bootstrap.js"></script>
<script src="vendor/mousetrap.min.js"></script>
<script src="js/treeherder.js"></script>
<script src="js/providers.js"></script>
<script src="js/compareperf.js"></script>
<!-- endbuild -->
</body>
</html>

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

@ -165,9 +165,67 @@ to { opacity: 0; }
}
<<<<<<< HEAD
<<<<<<< HEAD
.subtest-header {
=======
.subtestheader {
>>>>>>> initial work to get compare-talos stood up inside of perfherder
=======
.subtest-header {
>>>>>>> addressed all feedback except a common library, a few TODO items left in the code
background-image: linear-gradient(to bottom, #6A7B86, #424F5A);
color: white;
font-weight: bold;
}
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> addressed all feedback except a common library, a few TODO items left in the code
=======
>>>>>>> addressed review feedback: whitespace, blank lines, s/compare.js/compareperf.js/, cleaned up angular classes and extra <div>'s, reduced a forEach(..) call to a _.where(...) call, etc.
.subtest-improvement {
background-color: green;
color: white;
font-weight: bold;
}
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> addressed all feedback except a common library, a few TODO items left in the code
=======
>>>>>>> addressed review feedback: whitespace, blank lines, s/compare.js/compareperf.js/, cleaned up angular classes and extra <div>'s, reduced a forEach(..) call to a _.where(...) call, etc.
.subtest-regression {
background-color: red;
color: white;
font-weight: bold;
}
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> addressed all feedback except a common library, a few TODO items left in the code
=======
>>>>>>> addressed review feedback: whitespace, blank lines, s/compare.js/compareperf.js/, cleaned up angular classes and extra <div>'s, reduced a forEach(..) call to a _.where(...) call, etc.
.subtest-empty {
display: none;
}
<<<<<<< HEAD
=======
>>>>>>> initial work to get compare-talos stood up inside of perfherder
=======
>>>>>>> addressed all feedback except a common library, a few TODO items left in the code
.subtest-result button {
display: none;
}
.subtest-result:hover button {
display: inline-block;
}
@ -178,3 +236,15 @@ to { opacity: 0; }
text-overflow: ellipsis;
overflow: hidden;
}
.compare-improvement {
background-color: green;
color: white;
font-weight: bold;
}
.compare-regression {
background-color: red;
color: white;
font-weight: bold;
}

Двоичные данные
ui/img/dancing_cat.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 58 KiB

399
ui/js/compareperf.js Normal file
Просмотреть файл

@ -0,0 +1,399 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// -------------------------------------------------------------------------
// Utility Functions
// -------------------------------------------------------------------------
/**
* Compute the standard deviation for an array of values.
*
* @param values
* An array of numbers.
* @param avg
* Average of the values.
* @return a number (the standard deviation)
*/
function stddev(values, avg) {
if (values.length <= 1) {
return 0;
}
return Math.sqrt(
values.map(function (v) { return Math.pow(v - avg, 2); })
.reduce(function (a, b) { return a + b; }) / (values.length - 1));
}
// -------------------------------------------------------------------------
// End Utility Functions
// -------------------------------------------------------------------------
var compare = angular.module("compare", ['ui.router', 'ui.bootstrap', 'treeherder']);
//TODO: make getSeriesSummary part of a common library
compare.factory('getSeriesSummary', [ function() {
return function(signature, signatureProps, optionCollectionMap, pgo, e10s) {
var platform = signatureProps.machine_platform + " " +
signatureProps.machine_architecture;
var extra = "";
if (signatureProps.job_group_symbol === "T-e10s") {
extra = " e10s";
}
var testName = signatureProps.test;
var subtestSignatures;
if (testName === undefined) {
testName = "summary";
subtestSignatures = signatureProps.subtest_signatures;
}
var name = signatureProps.suite + " " + testName +
" " + optionCollectionMap[signatureProps.option_collection_hash] + extra;
//Only keep summary signatures, filter in/out e10s and pgo
if (name.indexOf('summary') <= 0) {
return null;
}
if (e10s && (name.indexOf('e10s') <= 0)) {
return null;
} else if (!e10s && (name.indexOf('e10s') > 0)) {
return null;
}
//TODO: pgo is linux/windows only- what about osx and android
if (pgo && (name.indexOf('pgo') <= 1)) {
return null;
} else if (!pgo && (name.indexOf('pgo') > 0)) {
return null;
}
return { name: name, signature: signature, platform: platform,
subtestSignatures: subtestSignatures };
};
}]);
compare.controller('CompareCtrl', [ '$state', '$stateParams', '$scope', '$rootScope', '$location',
'thServiceDomain', '$http', '$q', '$timeout', 'getSeriesSummary',
function CompareCtrl($state, $stateParams, $scope, $rootScope, $location,
thServiceDomain, $http, $q, $timeout, getSeriesSummary) {
function displayComparision() {
//TODO: determine the dates of the two revisions and only grab what we need
$scope.timeRange = 2592000; // last 30 days
$scope.testList = [];
$scope.platformList = [];
var signatureURL = thServiceDomain + '/api/project/' + $scope.originalProject +
'/performance-data/0/get_performance_series_summary/?interval=' +
$scope.timeRange;
$http.get(signatureURL).then(
function(response) {
var seriesList = [];
Object.keys(response.data).forEach(function(signature) {
var seriesSummary = getSeriesSummary(signature,
response.data[signature],
optionCollectionMap,
$stateParams.pgo,
$stateParams.e10s);
if (seriesSummary != null && seriesSummary.signature !== undefined) {
seriesList.push(seriesSummary);
if ($scope.platformList.indexOf(seriesSummary.platform) === -1) {
$scope.platformList.push(seriesSummary.platform);
}
if ($scope.testList.indexOf(seriesSummary.name) === -1) {
$scope.testList.push(seriesSummary.name);
}
}
});
// find summary results for all tests/platforms for the original rev
var signatureURL = thServiceDomain + '/api/project/' +
$scope.originalProject + '/performance-data/0/' +
'get_performance_data/?interval_seconds=' + $scope.timeRange;
// TODO: figure how how to reduce these maps
var rawResultsMap = {};
$q.all(seriesList.map(function(series) {
return $http.get(signatureURL + "&signatures=" + series.signature).then(function(response) {
response.data.forEach(function(data) {
rawResultsMap[data.series_signature] = calculateStats(data.blob, $scope.originalResultSetID);
rawResultsMap[data.series_signature].name = series.name;
rawResultsMap[data.series_signature].platform = series.platform;
});
});
})).then(function () {
// find summary results for all tests/platforms for the original rev
var signatureURL = thServiceDomain + '/api/project/' +
$scope.newProject + '/performance-data/0/' +
'get_performance_data/?interval_seconds=' + $scope.timeRange;
//ok, now get the new revision
var signatureListURL = thServiceDomain + '/api/project/' + $scope.newProject +
'/performance-data/0/get_performance_series_summary/?interval=' +
$scope.timeRange;
var newRawResultsMap = {};
var newSeriesList = [];
$http.get(signatureListURL).then(function(response) {
Object.keys(response.data).forEach(function(signature) {
var seriesSummary = getSeriesSummary(signature,
response.data[signature],
optionCollectionMap,
$stateParams.pgo,
$stateParams.e10s);
if (seriesSummary != null && seriesSummary.signature !== undefined) {
newSeriesList.push(seriesSummary);
if ($scope.platformList.indexOf(seriesSummary.platform) === -1) {
$scope.platformList.push(seriesSummary.platform);
}
if ($scope.testList.indexOf(seriesSummary.name) === -1) {
$scope.testList.push(seriesSummary.name);
}
}
});
$scope.testList.sort();
$scope.platformList.sort();
$q.all(newSeriesList.map(function(series) {
return $http.get(signatureURL + "&signatures=" + series.signature).then(function(response) {
response.data.forEach(function(data) {
newRawResultsMap[data.series_signature] = calculateStats(data.blob, $scope.newResultSetID);
newRawResultsMap[data.series_signature].name = series.name;
newRawResultsMap[data.series_signature].platform = series.platform;
});
});
})).then(function () {
$scope.dataLoading = false;
displayResults(rawResultsMap, newRawResultsMap);
});
});
});
}
);
}
//TODO: put this into a generic library
function calculateStats(perfData, resultSetID) {
var geomeans = [];
var total = 0;
_.where(perfData, { result_set_id: resultSetID }).forEach(function(pdata) {
geomeans.push(pdata.geomean);
total += pdata.geomean;
});
var avg = total / geomeans.length;
var sigma = stddev(geomeans, avg);
return {geomean: avg.toFixed(2), stddev: sigma.toFixed(2), runs: geomeans.length};
}
//TODO: put this into a generic library
function isReverseTest(testName) {
var reverseTests = ['dromaeo_dom', 'dromaeo_css', 'v8_7', 'canvasmark'];
var found = false;
reverseTests.forEach(function(rt) {
if (testName.indexOf(rt) >= 0) {
found = true;
}
});
return found;
}
function displayResults(rawResultsMap, newRawResultsMap) {
var counter = 0;
var compareResultsMap = {};
$scope.testList.forEach(function(testName) {
if (counter > 0 && compareResultsMap[(counter-1)].headerColumns==2) {
counter--;
}
//TODO: figure out a cleaner method for making the names a header row
compareResultsMap[counter++] = {'name': testName.replace(' summary', ''),
'isEmpty': true, 'isMinor': false, 'headerColumns': 2,
'originalGeoMean': 'Old Rev', 'originalStddev': 'StdDev',
'newGeoMean': 'New Rev', 'newStddev': 'StdDev',
'delta': 'Delta', 'deltaPercentage': 'Delta'};
$scope.platformList.forEach(function(platform) {
var cmap = {'originalGeoMean': NaN, 'originalRuns': 0, 'originalStddev': NaN,
'newGeoMean': NaN, 'newRuns': 0, 'newStddev': NaN, 'headerColumns': 1,
'delta': NaN, 'deltaPercentage': NaN, 'isEmpty': false,
'isRegression': false, 'isImprovement': false, 'isMinor': true};
var oldSig = _.find(Object.keys(rawResultsMap), function (sig) {
return (rawResultsMap[sig].name == testName && rawResultsMap[sig].platform == platform)});
var newSig = _.find(Object.keys(newRawResultsMap), function (sig) {
return (newRawResultsMap[sig].name == testName && newRawResultsMap[sig].platform == platform)});
if (oldSig) {
var originalData = rawResultsMap[oldSig];
cmap.originalGeoMean = originalData.geomean;
cmap.originalRuns = originalData.runs;
cmap.originalStddev = originalData.stddev;
cmap.originalStddevPct = ((originalData.stddev / originalData.geomean) * 100).toFixed(2);
}
if (newSig) {
var newData = newRawResultsMap[newSig];
cmap.newGeoMean = newData.geomean;
cmap.newRuns = newData.runs;
cmap.newStddev = newData.stddev;
cmap.newStddevPct = ((newData.stddev / newData.geomean) * 100).toFixed(2);
}
if ((cmap.originalRuns == 0 && cmap.newRuns == 0) ||
(testName == 'tp5n summary opt')) {
// We don't generate numbers for tp5n, just counters
cmap.isEmpty = true;
} else {
cmap.delta = (cmap.newGeoMean - cmap.originalGeoMean).toFixed(2);
cmap.deltaPercentage = (cmap.delta / cmap.originalGeoMean * 100).toFixed(2);
if (cmap.deltaPercentage > 2.0) {
cmap.isMinor = false;
isReverseTest(testName) ? cmap.isImprovement = true : cmap.isRegression = true;
} else if (cmap.deltaPercentage < -2.0) {
cmap.isMinor = false;
isReverseTest(testName) ? cmap.isRegression = true : cmap.isImprovement = true;
}
//TODO: do we need zoom? can we have >1 highlighted revision?
var originalSeries = encodeURIComponent(JSON.stringify(
{ project: $scope.originalProject,
signature: oldSig,
visible: true}));
var newSeries = encodeURIComponent(JSON.stringify(
{ project: $scope.newProject,
signature: newSig,
visible: true}));
var detailsLink = thServiceDomain + '/perf.html#/graphs?timerange=' +
$scope.timeRange + '&series=' + newSeries;
if (oldSig != newSig) {
detailsLink += '&series=' + originalSeries;
}
detailsLink += '&highlightedRevision=' + $scope.newRevision;
cmap.detailsLink = detailsLink;
cmap.name = platform;
compareResultsMap[counter++] = cmap;
}
});
});
$scope.compareResults = Object.keys(compareResultsMap).map(function(k) {
return compareResultsMap[k];
});
}
function verifyRevision(project, revision, rsid) {
var uri = thServiceDomain + '/api/project/' + project +
'/resultset/?format=json&full=false&with_jobs=false&revision=' +
revision;
return $http.get(uri).then(function(response) {
var results = response.data.results;
if (results.length > 0) {
//TODO: this is a bit hacky to pass in 'original' as a text string
if (rsid == 'original') {
$scope.originalResultSetID = results[0].id;
} else {
$scope.newResultSetID = results[0].id;
}
}
});
}
function updateURL() {
$state.transitionTo('compare', { 'originalProject': $scope.originalProject,
'originalRevision': $scope.originalRevision,
'newProject': $scope.newProject,
'newRevision': $scope.newRevision},
{location: true, inherit: true, notify: false, relative: $state.$current});
}
var optionCollectionMap = {};
$scope.dataLoading = true;
$http.get(thServiceDomain + '/api/optioncollectionhash').then(
function(response) {
response.data.forEach(function(dict) {
optionCollectionMap[dict.option_collection_hash] =
dict.options.map(function(option) {
return option.name; }).join(" ");
});
}).then(function() {
$stateParams.pgo = Boolean($stateParams.pgo);
$stateParams.e10s = Boolean($stateParams.e10s);
$scope.hideMinorChanges = Boolean($stateParams.hideMinorChanges);
// TODO: validate projects and revisions
$scope.originalProject = $stateParams.originalProject;
$scope.newProject = $stateParams.newProject;
$scope.newRevision = $stateParams.newRevision;
$scope.originalRevision = $stateParams.originalRevision;
if (!$scope.originalProject ||
!$scope.newProject ||
!$scope.originalRevision ||
!$scope.newRevision) {
//TODO: get an error to the UI
return;
}
verifyRevision($scope.originalProject, $scope.originalRevision, "original").then(function () {
verifyRevision($scope.newProject, $scope.newRevision, "new").then(function () {
$http.get(thServiceDomain + '/api/repository/').then(function(response) {
$scope.projects = response.data;
});
});
});
displayComparision();
});
}]);
compare.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.deferIntercept(); // so we don't reload on url change
$stateProvider.state('compare', {
templateUrl: 'partials/perf/comparectrl.html',
url: '/compare?originalProject&originalRevision&newProject&newRevision&hideMinorChanges&e10s&pgo',
controller: 'CompareCtrl'
});
$urlRouterProvider.otherwise('/compare');
})
// define the interception
.run(function ($rootScope, $urlRouter, $location, $state) {
$rootScope.$on('$locationChangeSuccess', function(e, newUrl, oldUrl) {
// Prevent $urlRouter's default handler from firing
e.preventDefault();
if ($state.current.name !== 'compare') {
// here for first time, synchronize
$urlRouter.sync();
}
});
// Configures $urlRouter's listener *after* custom listener
$urlRouter.listen();
})

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

@ -0,0 +1,103 @@
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<<<<<<< HEAD
<<<<<<< HEAD
<a class="navbar-brand" href="#">Perfherder Compare</a>
=======
<a class="navbar-brand" href="#">Perfherder Compare Talos</a>
>>>>>>> initial work to get compare-talos stood up inside of perfherder
=======
<a class="navbar-brand" href="#">Perfherder Compare</a>
>>>>>>> addressed review feedback: whitespace, blank lines, s/compare.js/compareperf.js/, cleaned up angular classes and extra <div>'s, reduced a forEach(..) call to a _.where(...) call, etc.
</div>
</div>
</nav>
<<<<<<< HEAD
<<<<<<< HEAD
<div class="container-fluid">
<div id="datapoint-detail" ng-show="originalRevision && newRevision">
<div ng-if="dataLoading">
Loading all results, please wait a minute...
<img src="img/dancing_cat.gif" />
</div>
<div id="subtest-summary" ng-if="!dataLoading">
<h4>Compare all data for revision {{newProject}}: {{newRevision}} to {{originalProject}}: {{originalRevision}}</h4>
<p class="help-block">Hover over each entry for more options.</p>
<table class="table">
<tbody>
<tr ng-class="{'subtest-header': compareResult.headerColumns==2, 'subtest-empty': compareResult.isEmpty && !compareResult.headerColumns==2, 'subtest-empty': (hideMinorChanges && compareResult.isMinor)}" ng-repeat="compareResult in compareResults">
<td colspan="{{compareResult.headerColumns}}">{{compareResult.name}}</td>
<td ng-class="{'subtest-empty': compareResult.headerColumns==2}"><a ng-href="{{compareResult.detailsLink}}">Details</a></td>
<td ng-attr-title="runs: {{compareResult.originalRuns}}">{{compareResult.originalGeoMean}}</td>
<td>+/-{{compareResult.originalStddev}} ({{compareResult.originalStddevPct}}%)</td>
<td ng-attr-title="runs: {{compareResult.newRuns}}">{{compareResult.newGeoMean}}</td>
<td>+/-{{compareResult.newStddev}} ({{compareResult.newStddevPct}}%)</td>
<td ng-class="{'subtest-regression': compareResult.isRegression, 'subtest-improvement': compareResult.isImprovement}">{{compareResult.delta}}</td>
<td ng-class="{'subtest-regression': compareResult.isRegression, 'subtest-improvement': compareResult.isImprovement}">{{compareResult.deltaPercentage}}%</td>
=======
<div class="ph-horizontal-layout">
<div id="revision-chooser">
&nbsp;
</div>
<div id="data-display">
=======
<div class="container-fluid">
>>>>>>> addressed review feedback: whitespace, blank lines, s/compare.js/compareperf.js/, cleaned up angular classes and extra <div>'s, reduced a forEach(..) call to a _.where(...) call, etc.
<div id="datapoint-detail" ng-show="originalRevision && newRevision">
<div ng-if="dataLoading">
Loading all results, please wait a minute...
<img src="img/dancing_cat.gif" />
</div>
<div id="subtest-summary" ng-if="!dataLoading">
<h4>Compare all data for revision {{newProject}}: {{newRevision}} to {{originalProject}}: {{originalRevision}}</h4>
<p class="help-block">Hover over each entry for more options.</p>
<table class="table">
<tbody>
<<<<<<< HEAD
<tr ng-class="{'subtest-header': compareResult.isHeader, 'subtest-empty': compareResult.isEmpty && !compareResult.isHeader}" ng-repeat="compareResult in compareResults">
<td><a ng-class="{'subtest-empty': compareResult.isHeader}" ng-href="{{compareResult.detailsLink}}">Details</a></td>
<td>{{compareResult.name}}</td>
<<<<<<< HEAD
<td ng-attr-title="runs: {{compareResult.originalRuns}}">{{compareResult.originalGeoMean}}</td><td>{{compareResult.originalVariation}}</td>
<td ng-attr-title="runs: {{compareResult.newRuns}}">{{compareResult.newGeoMean}}</td><td>{{compareResult.newVariation}}</td>
<td ng-style="compareResult.type === 'regression' && {'background-color': 'red', 'color': 'white', 'font-weight': 'bold'} ||
compareResult.type === 'improvement' && {'background-color': 'green', 'color': 'white', 'font-weight': 'bold'}">{{compareResult.delta}}</td>
<td ng-style="compareResult.type === 'regression' && {'background-color': 'red', 'color': 'white', 'font-weight': 'bold'} ||
compareResult.type === 'improvement' && {'background-color': 'green', 'color': 'white', 'font-weight': 'bold'}">{{compareResult.deltaPercentage}}</td>
>>>>>>> initial work to get compare-talos stood up inside of perfherder
=======
=======
<tr ng-class="{'subtest-header': compareResult.headerColumns==2, 'subtest-empty': compareResult.isEmpty && !compareResult.headerColumns==2, 'subtest-empty': (hideMinorChanges && compareResult.isMinor)}" ng-repeat="compareResult in compareResults">
<td colspan="{{compareResult.headerColumns}}">{{compareResult.name}}</td>
<td ng-class="{'subtest-empty': compareResult.headerColumns==2}"><a ng-href="{{compareResult.detailsLink}}">Details</a></td>
>>>>>>> additional cleanup: details after name, header column for testname is colspan=2, support url param hideMinorChanges
<td ng-attr-title="runs: {{compareResult.originalRuns}}">{{compareResult.originalGeoMean}}</td>
<td>+/-{{compareResult.originalStddev}} ({{compareResult.originalStddevPct}}%)</td>
<td ng-attr-title="runs: {{compareResult.newRuns}}">{{compareResult.newGeoMean}}</td>
<td>+/-{{compareResult.newStddev}} ({{compareResult.newStddevPct}}%)</td>
<<<<<<< HEAD
<td ng-class="{'subtest-regression': compareResult.isRegression, 'subtest-improvement': compareResult.isImprovement, 'subtest-header': compareResult.isHeader}">{{compareResult.delta}}</td>
<td ng-class="{'subtest-regression': compareResult.isRegression, 'subtest-improvement': compareResult.isImprovement, 'subtest-header': compareResult.isHeader}">{{compareResult.deltaPercentage}}%</td>
>>>>>>> addressed all feedback except a common library, a few TODO items left in the code
=======
<td ng-class="{'subtest-regression': compareResult.isRegression, 'subtest-improvement': compareResult.isImprovement}">{{compareResult.delta}}</td>
<td ng-class="{'subtest-regression': compareResult.isRegression, 'subtest-improvement': compareResult.isImprovement}">{{compareResult.deltaPercentage}}%</td>
>>>>>>> additional cleanup: details after name, header column for testname is colspan=2, support url param hideMinorChanges
</tr>
</tbody>
</table>
</div>
</div>
<<<<<<< HEAD
<<<<<<< HEAD
=======
</div>
<div class="graph-right-padding"></div>
>>>>>>> initial work to get compare-talos stood up inside of perfherder
=======
>>>>>>> addressed review feedback: whitespace, blank lines, s/compare.js/compareperf.js/, cleaned up angular classes and extra <div>'s, reduced a forEach(..) call to a _.where(...) call, etc.
</div>