diff --git a/neutrino-custom/base.js b/neutrino-custom/base.js index a061401dd..95de55537 100644 --- a/neutrino-custom/base.js +++ b/neutrino-custom/base.js @@ -36,8 +36,7 @@ module.exports = neutrino => { 'angular', 'angular-cookies', 'angular-local-storage', 'angular-resource', 'angular-route', 'angular-sanitize', 'angular-toarrayfilter', 'angular-ui-bootstrap', 'angular-ui-router', 'bootstrap/dist/js/bootstrap', 'hawk', 'jquery', 'jquery.scrollto', - 'js-yaml', 'mousetrap', 'react', 'react-dom', 'taskcluster-client', 'numeral' - ]; + 'js-yaml', 'mousetrap', 'react', 'react-dom', 'taskcluster-client', 'numeral', 'metrics-graphics']; jsDeps.map(dep => neutrino.config.entry('vendor').add(dep) ); diff --git a/neutrino-custom/lint.js b/neutrino-custom/lint.js index c5f99d52b..13cd23a7e 100644 --- a/neutrino-custom/lint.js +++ b/neutrino-custom/lint.js @@ -93,7 +93,8 @@ module.exports = neutrino => { globals: ['angular', '$', '_', 'treeherder', 'perf', 'treeherderApp', 'failureViewerApp', 'logViewerApp', 'userguideApp', 'admin', 'Mousetrap', 'jQuery', 'React', - 'hawk', 'jsonSchemaDefaults', 'SERVICE_DOMAIN', 'numeral' + 'hawk', 'jsonSchemaDefaults', 'SERVICE_DOMAIN', 'numeral', + 'metrics-graphics' ] } })); diff --git a/package.json b/package.json index 217d6f4c9..d98cef70b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "angular-ui-bootstrap": "1.3.3", "angular-ui-router": "0.4.2", "bootstrap": "3.3.7", + "d3": "^4.9.1", "deepmerge": "1.3.2", "font-awesome": "4.7.0", "hawk": "6.0.1", @@ -29,6 +30,7 @@ "json-e": "^2.1.1", "json-schema-defaults": "0.3.0", "lodash": "4.17.4", + "metrics-graphics": "^2.11.0", "mousetrap": "1.6.1", "neutrino": "4.3.1", "neutrino-lint-base": "4.3.1", diff --git a/ui/css/perf.css b/ui/css/perf.css index aa96992f0..917861ac7 100644 --- a/ui/css/perf.css +++ b/ui/css/perf.css @@ -471,3 +471,17 @@ td.alert-confidence { width: 200px; border: 1px solid white; } + +.replicate-graph .mg-header { + font-family: "Helvetica"; + font-size: 15px; + font-weight: bold; +} + +.replicate-graph .mg-x-axis text { + font-size: 0px; +} + +.replicate-graph .mg-category-guides line { + stroke: #ffffff; +} diff --git a/ui/entry-perf.js b/ui/entry-perf.js index 44f5f8f7d..a4843ccd4 100644 --- a/ui/entry-perf.js +++ b/ui/entry-perf.js @@ -10,6 +10,7 @@ require('font-awesome/css/font-awesome.css'); require('./css/treeherder-navbar.css'); require('./css/perf.css'); require('./css/treeherder-loading-overlay.css'); +require('metrics-graphics/dist/metricsgraphics.css'); // Vendor JS require('angular'); @@ -22,6 +23,7 @@ require('mousetrap'); require('bootstrap/dist/js/bootstrap'); require('angular-ui-bootstrap'); require('numeral'); +require('metrics-graphics'); require('./vendor/angular-clipboard.js'); // The jquery flot package does not seem to be updated on npm, so we use a local version: require('./vendor/jquery.flot.js'); diff --git a/ui/js/controllers/perf/compare.js b/ui/js/controllers/perf/compare.js index f3d9712e1..dc2cb1b7c 100644 --- a/ui/js/controllers/perf/compare.js +++ b/ui/js/controllers/perf/compare.js @@ -511,12 +511,13 @@ perf.controller('CompareSubtestResultsCtrl', [ '$state', '$stateParams', '$scope', '$rootScope', '$location', 'thServiceDomain', 'ThRepositoryModel', 'ThResultSetModel', '$http', '$q', '$timeout', 'PhSeries', 'math', - 'PhCompare', 'phTimeRanges', 'compareBaseLineDefaultTimeRange', + 'PhCompare', 'phTimeRanges', 'compareBaseLineDefaultTimeRange', '$httpParamSerializer', function CompareSubtestResultsCtrl($state, $stateParams, $scope, $rootScope, $location, thServiceDomain, ThRepositoryModel, ThResultSetModel, $http, $q, $timeout, PhSeries, math, - PhCompare, phTimeRanges, compareBaseLineDefaultTimeRange) { + PhCompare, phTimeRanges, compareBaseLineDefaultTimeRange, + $httpParamSerializer) { //TODO: duplicated from comparectrl function verifyRevision(project, revision, rsid) { return ThResultSetModel.getResultSetsFromRevision(project.name, revision).then( @@ -588,6 +589,20 @@ perf.controller('CompareSubtestResultsCtrl', [ }; }), [$scope.originalResultSet, $scope.newResultSet]) }]; + //replicate distribution is added only for talos + if ($scope.filterOptions.framework === '1') { + cmap.links.push({ + title: 'replicate', + href: 'perf.html#/comparesubtestdistribution?' + $httpParamSerializer({ + originalProject: $scope.originalProject.name, + newProject: $scope.newProject.name, + originalRevision: $scope.originalRevision, + newRevision: $scope.newRevision, + originalSubtestSignature: oldSig, + newSubtestSignature: newSig + }) + }); + } } else { @@ -605,7 +620,6 @@ perf.controller('CompareSubtestResultsCtrl', [ }), [$scope.newResultSet], $scope.selectedTimeRange.value) }]; } - $scope.compareResults[testName].push(cmap); }); } @@ -853,3 +867,113 @@ perf.controller('CompareSubtestResultsCtrl', [ }); }); }]); + +perf.controller('CompareSubtestDistributionCtrl', ['$scope', '$stateParams', '$q', 'ThRepositoryModel', + 'PhSeries', 'ThResultSetModel', 'metricsgraphics', + function CompareSubtestDistributionCtrl($scope, $stateParams, $q, ThRepositoryModel, + PhSeries, ThResultSetModel, metricsgraphics) { + $scope.originalRevision = $stateParams.originalRevision; + $scope.newRevision = $stateParams.newRevision; + $scope.originalSubtestSignature = $stateParams.originalSubtestSignature; + $scope.newSubtestSignature = $stateParams.newSubtestSignature; + $scope.dataLoading = true; + let loadRepositories = ThRepositoryModel.load(); + const fetchAndDrawReplicateGraph = function (project, revision, subtestSignature, target) { + let replicateData = {}; + return ThResultSetModel.getResultSetsFromRevision(project, revision).then( + (revisionData) => { + replicateData.resultSet = revisionData[0]; + return PhSeries.getSeriesData(project, { + signatures: subtestSignature, + push_id: replicateData.resultSet.id + }); + }).then((perfDatumList) => { + if (!perfDatumList[subtestSignature]) { + replicateData.replicateDataError = true; + return; + } + const numRuns = perfDatumList[subtestSignature].length; + let replicatePromises = perfDatumList[subtestSignature].map( + (value) => {return PhSeries.getReplicateData({job_id: value.job_id}); + }); + return $q.all(replicatePromises).then((replicateData) => { + let replicateValues = replicateData.concat.apply([], + replicateData.map((data) => { + let testSuite = data.suites.find((suite) => { + return suite.name === $scope.testSuite; + }); + let subtest = testSuite.subtests.find((subtest) =>{ + return subtest.name === $scope.subtest; + }); + return subtest.replicates; + }) + ); + //metrics-graphics doesn't accept "0" as x_accesor + replicateValues = replicateValues.map((value, index) => ({ + "replicate": (index + 1).toString(), + "value": value + })); + metricsgraphics.data_graphic({ + title: `${target} Replicates over ${numRuns} run${(numRuns > 1) ? 's' : ''}`, + chart_type: "bar", + data: replicateValues, + y_accessor: "value", + x_accessor: "replicate", + height: 275, + width: 1000, + target: `#${target}` + }); + }, + () =>{ + replicateData.replicateDataError = true; + }); + }).then(() =>{ + if (replicateData.replicateDataError) { + metricsgraphics.data_graphic({ + title: `${target} Replicates`, + chart_type: 'missing-data', + missing_text: 'No Data Found', + target: `#${target}`, + width: 1000, + height: 275 + }); + } + return replicateData; + }); + }; + + $q.all([loadRepositories]).then(() => { + $scope.originalProject = ThRepositoryModel.getRepo( + $stateParams.originalProject); + $scope.newProject = ThRepositoryModel.getRepo( + $stateParams.newProject); + PhSeries.getSeriesList($scope.originalProject.name, {signature: $scope.originalSubtestSignature}).then( + (seriesData) => { + $scope.testSuite = seriesData[0].suite; + $scope.subtest = seriesData[0].test; + $scope.testName = seriesData[0].name; + $scope.platform = seriesData[0].platform; + return fetchAndDrawReplicateGraph($scope.originalProject.name, + $scope.originalRevision, + $scope.originalSubtestSignature, + 'Base'); + }).then((result) => { + $scope.originalResultSet = result.resultSet; + $scope.originalReplicateError = result.replicateDataError; + if ($scope.originalReplicateError) + $scope.noResult = "base"; + return fetchAndDrawReplicateGraph($scope.newProject.name, + $scope.newRevision, + $scope.newSubtestSignature, + 'New'); + }).then((result) => { + $scope.newResultSet = result.resultSet; + $scope.newReplicateError = result.replicateDataError; + if ($scope.newReplicateError) + $scope.noResult = "new"; + window.document.title = `${$scope.platform}: ${$scope.testName}`; + $scope.dataLoading = false; + }); + }); + } +]); diff --git a/ui/js/models/perf/series.js b/ui/js/models/perf/series.js index 83f056234..050ecfa3d 100644 --- a/ui/js/models/perf/series.js +++ b/ui/js/models/perf/series.js @@ -81,12 +81,26 @@ treeherder.factory('PhSeries', ['$http', 'thServiceDomain', 'ThOptionCollectionM }, getSeriesData: function (projectName, params) { return $http.get(thServiceDomain + '/api/project/' + projectName + '/performance/data/', - { params: params }).then(function (response) { + {params: params}).then(function (response) { if (response.data) { return response.data; } return $q.reject("No series data found"); }); + }, + getReplicateData: function (params) { + params.value = 'perfherder-data.json'; + return $http.get(thServiceDomain + '/api/jobdetail/' + , {params: params}).then( + function (response) { + if (response.data.results[0]) { + let url = response.data.results[0].url; + return $http.get(url).then(function (response) { + return response.data; + }); + } + return $q.reject("No replicate data found"); + }); } }; }]); diff --git a/ui/js/perfapp.js b/ui/js/perfapp.js index aa72dcc44..2790261e4 100644 --- a/ui/js/perfapp.js +++ b/ui/js/perfapp.js @@ -55,6 +55,11 @@ perf.config(['$compileProvider', '$httpProvider', '$stateProvider', '$urlRouterP templateUrl: 'partials/perf/dashboardsubtest.html', url: '/dashboardsubtest?topic&filter&showOnlyImportant&showOnlyConfident&baseSignature&variantSignature&repo&timerange&revision', controller: 'dashSubtestCtrl' + }).state('comparesubtestdistribution', { + title: 'Compare Subtest Distribution', + templateUrl: 'partials/perf/comparesubtestdistribution.html', + url: '/comparesubtestdistribution?originalProject&newProject&originalRevision&newRevision&originalSubtestSignature?newSubtestSignature', + controller: 'CompareSubtestDistributionCtrl' }); $urlRouterProvider.otherwise('/graphs'); }]).run(['$rootScope', '$state', '$stateParams', function ($rootScope, $state, $stateParams) { diff --git a/ui/js/services/main.js b/ui/js/services/main.js index b5e3b8a80..327dd476d 100755 --- a/ui/js/services/main.js +++ b/ui/js/services/main.js @@ -218,3 +218,8 @@ treeherder.factory('numeral', [ function () { return require('numeral'); }]); + +treeherder.factory('metricsgraphics', [ + function () { + return require('metrics-graphics'); + }]); diff --git a/ui/partials/perf/comparesubtestdistribution.html b/ui/partials/perf/comparesubtestdistribution.html new file mode 100644 index 000000000..98f532532 --- /dev/null +++ b/ui/partials/perf/comparesubtestdistribution.html @@ -0,0 +1,16 @@ +
No results to show for these two revisions.
+