Closed
Bug 1450020
Opened 7 years ago
Closed 6 years ago
Convert Perf Details tab to ReactJS
Categories
(Tree Management :: Treeherder: Frontend, enhancement, P1)
Tree Management
Treeherder: Frontend
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
Assignee | ||
Updated•7 years ago
|
Blocks: treeherder-react
Assignee | ||
Updated•7 years ago
|
Priority: -- → P2
Assignee | ||
Updated•7 years ago
|
Assignee: nobody → cdawson
Status: NEW → ASSIGNED
Priority: P2 → P1
Comment 1•6 years ago
|
||
Assignee | ||
Comment 2•6 years ago
|
||
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
Comment 3•6 years ago
|
||
Assignee | ||
Updated•6 years ago
|
Attachment #8971040 -
Flags: review?(emorley)
Comment 4•6 years ago
|
||
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+
Comment 5•6 years ago
|
||
Commit pushed to master at https://github.com/mozilla/treeherder
https://github.com/mozilla/treeherder/commit/7663ae2b671418262b5fb0f8847c9ec3f517f0c9
Bug 1450020 - Convert Perf Details tab to ReactJS
Assignee | ||
Updated•6 years ago
|
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.
Description
•