Closed Bug 1450020 Opened 7 years ago Closed 6 years ago

Convert Perf Details tab to ReactJS

Categories

(Tree Management :: Treeherder: Frontend, enhancement, P1)

enhancement

Tracking

(Not tracked)

RESOLVED FIXED

People

(Reporter: camd, Assigned: camd)

References

Details

Attachments

(1 file, 1 obsolete file)

Part of the overall conversion to ReactJS. Estimated difficulty of 3
Priority: -- → P2
Assignee: nobody → cdawson
Status: NEW → ASSIGNED
Priority: P2 → P1
Comment on attachment 8971023 [details] Link to GitHub pull-request: https://github.com/mozilla/treeherder/pull/3485 >diff --git a/ui/details-panel/PerformanceTab.jsx b/ui/details-panel/PerformanceTab.jsx >new file mode 100644 >index 0000000000..201d9bdaac >--- /dev/null >+++ b/ui/details-panel/PerformanceTab.jsx >@@ -0,0 +1,119 @@ >+import PropTypes from 'prop-types'; >+import { react2angular } from "react2angular/index.es2015"; >+ >+import treeherder from '../js/treeherder'; >+import PerfSeriesModel from '../models/PerfSeriesModel'; >+ >+class PerformanceTab extends React.Component { >+ >+ static getDerivedStateFromProps(nextProps) { >+ if (nextProps.job) { >+ return { canFetchPerfData: true }; >+ } >+ return { canFetchPerfData: false }; >+ } >+ >+ constructor(props) { >+ super(props); >+ >+ this.state = { >+ perfSeriesModel: new PerfSeriesModel(), >+ canFetchPerfData: false, >+ }; >+ } >+ >+ async componentDidMount() { >+ const { repoName, job } = this.props; >+ const { perfSeriesModel, canFetchPerfData } = this.state; >+ >+ if (!canFetchPerfData) { >+ console.log("can't fetch data"); >+ return null; >+ } >+ >+ const seriesData = await perfSeriesModel.getSeriesData(repoName, { job_id: job.id }); >+ const performanceData = _.flatten(Object.values(seriesData)); >+ console.log("series and perf", seriesData, performanceData); >+ >+ if (performanceData) { >+ await this.fetchPerfJobDetail(performanceData); >+ } >+ >+ } >+ >+ async fetchPerfJobDetail(performanceData) { >+ const { tabService, repoName, job } = this.props; >+ const { perfSeriesModel } = this.state; >+ console.log("we have performanceData"); >+ const signatureIds = _.uniq(_.map(performanceData, 'signature_id')); >+ const seriesListList = await Promise.all(_.chunk(signatureIds, 20).map( >+ signatureIdChunk => perfSeriesModel.getSeriesList(repoName, { id: signatureIdChunk }) >+ )); >+ >+ console.log("promises are done"); >+ const seriesList = _.flatten(seriesListList); >+ const perfJobDetail = performanceData.map(d => ({ >+ series: seriesList.find(s => d.signature_id === s.id), >+ ...d >+ })).filter(d => !d.series.parentSignature).map(d => ({ >+ url: `/perf.html#/graphs?series=${[repoName, d.signature_id, 1, d.series.frameworkId]}&selected=${[repoName, d.signature_id, job.result_set_id, d.id]}`, >+ value: d.value, >+ title: d.series.name >+ })); >+ console.log("perfJobDetail set now", perfJobDetail); >+ tabService.tabs.perfDetails.enabled = true; >+ this.setState({ perfJobDetail }); >+ >+ } >+ >+ render() { >+ const { repoName, revision } = this.props; >+ const { perfJobDetail } = this.state; >+ console.log("render perfJobDetail", perfJobDetail, this.props.job); >+ const sortedDetails = perfJobDetail ? perfJobDetail.slice() : []; >+ sortedDetails.sort((a, b) => a.title.localeCompare(b.title)); >+ >+ return ( >+ <div className="performance-panel"> >+ {!!sortedDetails.length && <ul> >+ <li>Perfherder: >+ {sortedDetails.map((detail, idx) => ( >+ <ul >+ key={idx} // eslint-disable-line react/no-array-index-key >+ > >+ <li>{detail.title}: >+ <a href={detail.url}>{detail.value}</a> >+ </li> >+ </ul> >+ ))} >+ </li> >+ </ul>} >+ <ul> >+ <li> >+ <a >+ href={`perf.html#/comparechooser?newProject=${repoName}&newRevision=${revision}`} >+ target="_blank" >+ rel="noopener" >+ >Compare result against another revision</a> >+ </li> >+ </ul> >+ </div> >+ ); >+ } >+} >+ >+PerformanceTab.propTypes = { >+ // tabService: PropTypes.object.isRequired, >+ repoName: PropTypes.string.isRequired, >+ revision: PropTypes.string, >+ job: PropTypes.object, >+}; >+ >+PerformanceTab.defaultProps = { >+ revision: '', >+ job: null, >+}; >+ >+treeherder.component('performanceTab', react2angular( >+ PerformanceTab, >+ ['tabService', 'repoName', 'revision', 'job', 'perfJobDetail'])); >diff --git a/ui/entry-index.js b/ui/entry-index.js >index d9c3201551..7935bb1a22 100644 >--- a/ui/entry-index.js >+++ b/ui/entry-index.js >@@ -73,4 +73,5 @@ import './details-panel/FailureSummaryTab'; > import './details-panel/AutoclassifyTab'; > import './details-panel/AnnotationsTab'; > import './details-panel/SimilarJobsTab'; >+import './details-panel/PerformanceTab'; > import './js/filters'; >diff --git a/ui/models/OptionCollectionModel.js b/ui/models/OptionCollectionModel.js >new file mode 100644 >index 0000000000..86ef9a51a2 >--- /dev/null >+++ b/ui/models/OptionCollectionModel.js >@@ -0,0 +1,23 @@ >+import { getApiUrl } from '../helpers/urlHelper'; >+ >+export default class ThOptionCollectionModel { >+ constructor() { >+ this.optionCollectionMap = null; >+ } >+ >+ async loadMap() { >+ const resp = await fetch(getApiUrl("/optioncollectionhash/")); >+ const optionCollections = await resp.json(); >+ // return a map of option collection hashes to a string >+ // representation of their contents >+ // (e.g. 102210fe594ee9b33d82058545b1ed14f4c8206e -> opt) >+ this.optionCollectionMap = optionCollections.reduce((acc, optColl) => ( >+ { [optColl.option_collection_hash]: [...new Set(optColl.options.map(opt => opt.name))].sort().join() } >+ ), {}); >+ return this.optionCollectionMap; >+ } >+ >+ async getMap() { >+ return !this.optionCollectionMap ? this.loadMap() : this.optionCollectionMap; >+ } >+} >diff --git a/ui/models/PerfSeriesModel.js b/ui/models/PerfSeriesModel.js >new file mode 100644 >index 0000000000..5d27728f6c >--- /dev/null >+++ b/ui/models/PerfSeriesModel.js >@@ -0,0 +1,107 @@ >+import { getApiUrl, getProjectUrl } from "../helpers/urlHelper"; >+import ThOptionCollectionModel from './OptionCollectionModel'; >+ >+export default class PerfSeriesModel { >+ constructor() { >+ this.optionCollectionModel = new ThOptionCollectionModel(); >+ } >+ >+ getTestName(signatureProps) { >+ // only return suite name if testname is identical, and handle >+ // undefined test name >+ return _.uniq(_.filter([signatureProps.suite, signatureProps.test])).join(" "); >+ } >+ >+ getSeriesOptions(signatureProps, optionCollectionMap) { >+ let options = [optionCollectionMap[signatureProps.option_collection_hash]]; >+ if (signatureProps.extra_options) { >+ options = options.concat(signatureProps.extra_options); >+ } >+ return _.uniq(options); >+ } >+ >+ getSeriesName(signatureProps, optionCollectionMap, >+ displayOptions) { >+ const platform = signatureProps.machine_platform; >+ let name = this.getTestName(signatureProps); >+ >+ if (displayOptions && displayOptions.includePlatformInName) { >+ name = name + " " + platform; >+ } >+ const options = this.getSeriesOptions(signatureProps, optionCollectionMap); >+ return `${name} ${options.join(' ')}`; >+ } >+ >+ getSeriesSummary(projectName, signature, signatureProps, >+ optionCollectionMap) { >+ const platform = signatureProps.machine_platform; >+ const options = this.getSeriesOptions(signatureProps, optionCollectionMap); >+ >+ return { >+ id: signatureProps.id, >+ name: this.getSeriesName(signatureProps, optionCollectionMap), >+ testName: this.getTestName(signatureProps), // unadorned with platform/option info >+ suite: signatureProps.suite, >+ test: signatureProps.test || null, >+ signature: signature, >+ hasSubtests: signatureProps.has_subtests || false, >+ parentSignature: signatureProps.parent_signature || null, >+ projectName: projectName, >+ platform: platform, >+ options: options, >+ frameworkId: signatureProps.framework_id, >+ lowerIsBetter: (signatureProps.lower_is_better === undefined || >+ signatureProps.lower_is_better) >+ }; >+ } >+ >+ getSeriesList(projectName, params) { >+ return this.optionCollectionModel.getMap().then(optionCollectionMap => ( >+ fetch(getProjectUrl('/performance/signatures/', projectName), { params }) >+ .then(response => ( >+ _.map(response.data, (signatureProps, signature) => ( >+ this.getSeriesSummary(projectName, signature, >+ signatureProps, >+ optionCollectionMap) >+ )) >+ )) >+ )); >+ } >+ >+ getPlatformList(projectName, params) { >+ return fetch( >+ getProjectUrl('/performance/platforms/', projectName), >+ { params }).then(function (response) { >+ return response.data; >+ }); >+ } >+ >+ getSeriesData(repoName, params) { >+ return fetch( >+ getProjectUrl('/performance/data/', repoName), >+ { params }).then(function (response) { >+ if (response.data) { >+ return response.data; >+ } >+ return Promise.reject(new Error("No series data found")); >+ }); >+ } >+ >+ getReplicateData(params) { >+ params.value = 'perfherder-data.json'; >+ return fetch( >+ getApiUrl('/jobdetail/'), >+ { params }).then( >+ function (response) { >+ if (response.data.results[0]) { >+ const url = response.data.results[0].url; >+ return fetch(url).then(function (response) { >+ return response.data; >+ }); >+ } >+ return Promise.reject(new Error("No replicate data found")); >+ }); >+ } >+ >+ >+} >diff --git a/ui/plugins/controller.js b/ui/plugins/controller.js >index bf47e67f9c..7a1fa4622e 100644 >--- a/ui/plugins/controller.js >+++ b/ui/plugins/controller.js >@@ -69,7 +69,7 @@ treeherder.controller('PluginCtrl', [ > failTab = "autoClassification"; > } > >- $scope.tabService.tabs.perfDetails.enabled = hasPerformanceData; >+ // $scope.tabService.tabs.perfDetails.enabled = hasPerformanceData; > // the success tabs should be "performance" if job was not a build > const jobType = job.job_type_name; > if (hasPerformanceData && jobType !== "Build" && jobType !== "Nightly" && >@@ -193,6 +193,7 @@ treeherder.controller('PluginCtrl', [ > > $scope.job = {}; > $scope.job_details = []; >+ $scope.perfJobDetail = []; > const jobPromise = ThJobModel.get( > $scope.repoName, job.id, > { timeout: selectJobPromise }); >@@ -205,14 +206,14 @@ treeherder.controller('PluginCtrl', [ > job.id, > { timeout: selectJobPromise }); > >- const phSeriesPromise = PhSeries.getSeriesData( >- $scope.repoName, { job_id: job.id }); >+ // const phSeriesPromise = PhSeries.getSeriesData( >+ // $scope.repoName, { job_id: job.id }); > > return $q.all([ > jobPromise, > jobDetailPromise, > jobLogUrlPromise, >- phSeriesPromise >+ // phSeriesPromise > ]).then(function (results) { > > //the first result comes from the job promise >@@ -261,26 +262,27 @@ treeherder.controller('PluginCtrl', [ > $scope.reftestUrl = `${getReftestUrl($scope.job_log_urls[0].url)}&only_show_unexpected=1`; > } > >- const performanceData = _.flatten(Object.values(results[3])); >- if (performanceData) { >- const signatureIds = _.uniq(_.map(performanceData, 'signature_id')); >- $q.all(_.chunk(signatureIds, 20).map( >- signatureIdChunk => PhSeries.getSeriesList($scope.repoName, { id: signatureIdChunk }) >- )).then((seriesListList) => { >- const seriesList = _.flatten(seriesListList); >- $scope.perfJobDetail = performanceData.map(d => ({ >- series: seriesList.find(s => d.signature_id === s.id), >- ...d >- })).filter(d => !d.series.parentSignature).map(d => ({ >- url: `/perf.html#/graphs?series=${[$scope.repoName, d.signature_id, 1, d.series.frameworkId]}&selected=${[$scope.repoName, d.signature_id, $scope.job.result_set_id, d.id]}`, >- value: d.value, >- title: d.series.name >- })); >- }); >- } >+ // const performanceData = _.flatten(Object.values(results[3])); >+ // if (performanceData) { >+ // const signatureIds = _.uniq(_.map(performanceData, 'signature_id')); >+ // $q.all(_.chunk(signatureIds, 20).map( >+ // signatureIdChunk => PhSeries.getSeriesList($scope.repoName, { id: signatureIdChunk }) >+ // )).then((seriesListList) => { >+ // const seriesList = _.flatten(seriesListList); >+ // $timeout($scope.perfJobDetail = performanceData.map(d => ({ >+ // series: seriesList.find(s => d.signature_id === s.id), >+ // ...d >+ // })).filter(d => !d.series.parentSignature).map(d => ({ >+ // url: `/perf.html#/graphs?series=${[$scope.repoName, d.signature_id, 1, d.series.frameworkId]}&selected=${[$scope.repoName, d.signature_id, $scope.job.result_set_id, d.id]}`, >+ // value: d.value, >+ // title: d.series.name >+ // }))); >+ // console.log("perfJobDetail set now", $scope.perfJobDetail); >+ // }); >+ // } > > // set the tab options and selections based on the selected job >- initializeTabs($scope.job, (Object.keys(performanceData).length > 0)); >+ initializeTabs($scope.job); > > $scope.updateClassifications(); > $scope.updateBugs(); >diff --git a/ui/plugins/perf_details/main.html b/ui/plugins/perf_details/main.html >index fb9b431562..5d4aa0f49c 100755 >--- a/ui/plugins/perf_details/main.html >+++ b/ui/plugins/perf_details/main.html >@@ -1,17 +1,10 @@ >-<div class="performance-panel"> >- <ul ng-show="perfJobDetail"> >- <li>Perfherder: >- <ul ng-repeat="detail in perfJobDetail | toArray | orderBy: 'title'"> >- <li>{{detail.title}}: <a href="{{::detail.url}}">{{::detail.value|displayNumber}}</a> >- </li> >- </ul> >- </li> >- </ul> >- <ul> >- <li> >- <a href="perf.html#/comparechooser?newProject={{repoName}}&newRevision={{jobRevision}}" >- target="_blank" rel="noopener">Compare result against another revision</a> >- </li> >- </ul> >- >+<div> >+ <div> >+ <performance-tab >+ job="selectedJob" >+ tab-service="tabService" >+ repo-name="repoName" >+ revision="jobRevision" >+ /> >+ </div> > </div> >diff --git a/ui/plugins/tabs.js b/ui/plugins/tabs.js >index 6354760a7f..093956b1fb 100644 >--- a/ui/plugins/tabs.js >+++ b/ui/plugins/tabs.js >@@ -40,7 +40,7 @@ treeherder.factory('thTabs', [ > title: "Performance", > description: "performance details", > content: "plugins/perf_details/main.html", >- enabled: false >+ enabled: true > } > }, > tabOrder: [
Attachment #8971023 - Attachment is obsolete: true
Attachment #8971040 - Flags: review?(emorley)
Comment on attachment 8971040 [details] Link to GitHub pull-request: https://github.com/mozilla/treeherder/pull/3486 Making great progress through these! :-)
Attachment #8971040 - Flags: review?(emorley) → review+
Status: ASSIGNED → RESOLVED
Closed: 6 years ago
Resolution: --- → FIXED
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: