зеркало из https://github.com/mozilla/treeherder.git
Bug 1822513 - Add links to side by side in Performance Tab of page load job (#7719)
Bug 1822513 - Add links to side by side in Performance Tab of page load job
This commit is contained in:
Родитель
d1ae8de25c
Коммит
09ae7bd381
|
@ -263,3 +263,9 @@ export const genericErrorMessage = 'Something went wrong';
|
|||
export const mercurialDatetimeFormat = 'ddd MMM DD HH:mm:ss YYYY ZZ';
|
||||
|
||||
export const alertsViewDatetimeFormat = 'ddd MMM DD HH:mm YYYY';
|
||||
|
||||
export const sxsJobTypeName = 'perftest-linux-side-by-side';
|
||||
|
||||
export const sxsTaskName = 'side-by-side';
|
||||
|
||||
export const geckoProfileTaskName = 'geckoprofile';
|
||||
|
|
|
@ -52,10 +52,9 @@ export const isReftest = function isReftest(job) {
|
|||
export const isPerfTest = function isPerfTest(job) {
|
||||
return [job.job_group_name, job.job_type_name].some(
|
||||
(name) =>
|
||||
(name.toLowerCase().includes('talos') ||
|
||||
name.toLowerCase().includes('raptor') ||
|
||||
name.toLowerCase().includes('browsertime')) &&
|
||||
!name.toLowerCase().includes('side-by-side'),
|
||||
name.toLowerCase().includes('talos') ||
|
||||
name.toLowerCase().includes('raptor') ||
|
||||
name.toLowerCase().includes('browsertime'),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -15,30 +15,31 @@ import { getAction } from './taskcluster';
|
|||
* @param {Object} decisionTaskMap - Object that maps a job push ID to a decision task.
|
||||
* @param {string} currentRepo - The name of the current repo, e.g. "try"
|
||||
*/
|
||||
export async function triggerGeckoProfileTask(
|
||||
export async function triggerTask(
|
||||
selectedJobFull,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
currentRepo,
|
||||
taskName,
|
||||
) {
|
||||
const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id];
|
||||
|
||||
TaskclusterModel.load(decisionTaskId, selectedJobFull, currentRepo).then(
|
||||
(results) => {
|
||||
try {
|
||||
const geckoprofile = getAction(results.actions, 'geckoprofile');
|
||||
const action = getAction(results.actions, taskName);
|
||||
|
||||
if (
|
||||
geckoprofile === undefined ||
|
||||
!Object.prototype.hasOwnProperty.call(geckoprofile, 'kind')
|
||||
action === undefined ||
|
||||
!Object.prototype.hasOwnProperty.call(action, 'kind')
|
||||
) {
|
||||
return notify(
|
||||
'Job was scheduled without taskcluster support for GeckoProfiles',
|
||||
`Job was scheduled without taskcluster support for ${taskName}`,
|
||||
);
|
||||
}
|
||||
|
||||
TaskclusterModel.submit({
|
||||
action: geckoprofile,
|
||||
action,
|
||||
decisionTaskId,
|
||||
taskId: results.originalTaskId,
|
||||
task: results.originalTask,
|
||||
|
|
|
@ -218,7 +218,10 @@ class DetailsPanel extends React.Component {
|
|||
// of all the extra fields in ``selectedJobFull``. It's not that much for just one job, but as
|
||||
// one selects job after job, over the course of a day, it can add up. Therefore, we keep
|
||||
// selectedJobFull data as transient only when the job is selected.
|
||||
const selectedJobFull = jobResult;
|
||||
const selectedJobFull = {
|
||||
...jobResult,
|
||||
hasSideBySide: selectedJob.hasSideBySide,
|
||||
};
|
||||
const jobRevision = push ? push.revision : null;
|
||||
|
||||
addAggregateFields(selectedJobFull);
|
||||
|
|
|
@ -18,8 +18,12 @@ import {
|
|||
faCrosshairs,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { thEvents } from '../../../helpers/constants';
|
||||
import { triggerGeckoProfileTask } from '../../../helpers/performance';
|
||||
import {
|
||||
geckoProfileTaskName,
|
||||
sxsTaskName,
|
||||
thEvents,
|
||||
} from '../../../helpers/constants';
|
||||
import { triggerTask } from '../../../helpers/performance';
|
||||
import { formatTaskclusterError } from '../../../helpers/errorMessage';
|
||||
import {
|
||||
isReftest,
|
||||
|
@ -97,11 +101,28 @@ class ActionBar extends React.PureComponent {
|
|||
decisionTaskMap,
|
||||
currentRepo,
|
||||
} = this.props;
|
||||
return triggerGeckoProfileTask(
|
||||
return triggerTask(
|
||||
selectedJobFull,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
currentRepo,
|
||||
geckoProfileTaskName,
|
||||
);
|
||||
};
|
||||
|
||||
createSideBySide = async () => {
|
||||
const {
|
||||
selectedJobFull,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
currentRepo,
|
||||
} = this.props;
|
||||
await triggerTask(
|
||||
selectedJobFull,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
currentRepo,
|
||||
sxsTaskName,
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -469,6 +490,16 @@ class ActionBar extends React.PureComponent {
|
|||
Create Gecko Profile
|
||||
</DropdownItem>
|
||||
)}
|
||||
{isPerfTest(selectedJobFull) &&
|
||||
!selectedJobFull.hasSideBySide && (
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
className="py-2"
|
||||
onClick={this.createSideBySide}
|
||||
>
|
||||
Generate side-by-side
|
||||
</DropdownItem>
|
||||
)}
|
||||
{canConfirmFailure(selectedJobFull) && (
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { notify } from '../../redux/stores/notifications';
|
||||
|
||||
class PerfData extends React.PureComponent {
|
||||
render() {
|
||||
const { perfJobDetail, selectedJobFull } = this.props;
|
||||
|
||||
const sortedDetails = perfJobDetail.slice();
|
||||
|
||||
// These styles are shared across all of the table cells.
|
||||
const cellClassName = 'nowrap pl-2 pr-2';
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="font-size-16 mt-3 mb-2">
|
||||
Results for: {selectedJobFull.job_type_name}
|
||||
</h3>
|
||||
<table className="table table-sm performance-panel-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className={`text-right ${cellClassName}`}>
|
||||
Value
|
||||
</th>
|
||||
<th scope="col" className={cellClassName}>
|
||||
Unit
|
||||
</th>
|
||||
<th scope="col" className={cellClassName}>
|
||||
Better
|
||||
</th>
|
||||
<th scope="col" className={cellClassName}>
|
||||
History
|
||||
</th>
|
||||
<th scope="col" className={cellClassName}>
|
||||
Name
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedDetails.map(
|
||||
(
|
||||
{
|
||||
value,
|
||||
url,
|
||||
measurementUnit,
|
||||
lowerIsBetter,
|
||||
title,
|
||||
suite,
|
||||
perfdocs,
|
||||
},
|
||||
idx,
|
||||
) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<tr key={idx}>
|
||||
{/* Ensure the value and measurement are visually next to each
|
||||
other in the chart, by aligning the value to the right. */}
|
||||
<td className={`text-right ${cellClassName}`}>{value}</td>
|
||||
<td className={cellClassName}>{measurementUnit || '–'}</td>
|
||||
<td className={cellClassName}>
|
||||
{lowerIsBetter ? 'Lower' : 'Higher'}
|
||||
</td>
|
||||
<td className={cellClassName}>
|
||||
<a
|
||||
href={url}
|
||||
className="btn btn-outline-darker-secondary btn-sm performance-panel-view-button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
<td className="w-100">
|
||||
{perfdocs.hasDocumentation() ? (
|
||||
<div>
|
||||
<a
|
||||
href={perfdocs.documentationURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{`${suite} `}
|
||||
</a>
|
||||
{`${perfdocs.remainingName}`}
|
||||
</div>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
),
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PerfData.propTypes = {
|
||||
perfJobDetail: PropTypes.arrayOf(PropTypes.object),
|
||||
};
|
||||
|
||||
PerfData.defaultProps = {
|
||||
perfJobDetail: [],
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
decisionTaskMap: state.pushes.decisionTaskMap,
|
||||
});
|
||||
const mapDispatchToProps = { notify };
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PerfData);
|
|
@ -8,15 +8,22 @@ import {
|
|||
faExternalLinkAlt,
|
||||
faRedo,
|
||||
faTable,
|
||||
faFilm,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faYoutube } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
import { getCompareChooserUrl, getPerfAnalysisUrl } from '../../../helpers/url';
|
||||
import { triggerGeckoProfileTask } from '../../../helpers/performance';
|
||||
import {
|
||||
getCompareChooserUrl,
|
||||
getJobsUrl,
|
||||
getPerfAnalysisUrl,
|
||||
} from '../../../helpers/url';
|
||||
import { triggerTask } from '../../../helpers/performance';
|
||||
import { notify } from '../../redux/stores/notifications';
|
||||
import { isPerfTest } from '../../../helpers/job';
|
||||
import { geckoProfileTaskName, sxsTaskName } from '../../../helpers/constants';
|
||||
|
||||
import SideBySide from './SideBySide';
|
||||
|
||||
import PerfData from './PerfData';
|
||||
/**
|
||||
* The performance tab shows performance-oriented information about a test run.
|
||||
* It helps users interact with the Firefox Profiler, and summarizes test
|
||||
|
@ -28,9 +35,7 @@ class PerformanceTab extends React.PureComponent {
|
|||
const { selectedJobFull } = this.props;
|
||||
this.state = {
|
||||
triggeredGeckoProfiles: 0,
|
||||
showSideBySide: selectedJobFull.job_type_name.includes(
|
||||
'perftest-linux-side-by-side',
|
||||
),
|
||||
showSideBySide: selectedJobFull.job_type_symbol.includes(sxsTaskName),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -41,17 +46,34 @@ class PerformanceTab extends React.PureComponent {
|
|||
decisionTaskMap,
|
||||
currentRepo,
|
||||
} = this.props;
|
||||
await triggerGeckoProfileTask(
|
||||
await triggerTask(
|
||||
selectedJobFull,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
currentRepo,
|
||||
geckoProfileTaskName,
|
||||
);
|
||||
this.setState((state) => ({
|
||||
triggeredGeckoProfiles: state.triggeredGeckoProfiles + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
createSideBySide = async () => {
|
||||
const {
|
||||
selectedJobFull,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
currentRepo,
|
||||
} = this.props;
|
||||
await triggerTask(
|
||||
selectedJobFull,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
currentRepo,
|
||||
sxsTaskName,
|
||||
);
|
||||
};
|
||||
|
||||
maybeGetFirefoxProfilerLink() {
|
||||
// Look for a profiler artifact.
|
||||
const jobDetail = this.props.jobDetails.find(
|
||||
|
@ -79,102 +101,14 @@ class PerformanceTab extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
maybeRenderPerfData() {
|
||||
const { perfJobDetail, selectedJobFull } = this.props;
|
||||
if (perfJobDetail.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortedDetails = perfJobDetail.slice();
|
||||
|
||||
// These styles are shared across all of the table cells.
|
||||
const cellClassName = 'nowrap pl-2 pr-2';
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="font-size-16 mt-3 mb-2">
|
||||
Results for: {selectedJobFull.job_type_name}
|
||||
</h3>
|
||||
<table className="table table-sm performance-panel-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className={`text-right ${cellClassName}`}>
|
||||
Value
|
||||
</th>
|
||||
<th scope="col" className={cellClassName}>
|
||||
Unit
|
||||
</th>
|
||||
<th scope="col" className={cellClassName}>
|
||||
Better
|
||||
</th>
|
||||
<th scope="col" className={cellClassName}>
|
||||
History
|
||||
</th>
|
||||
<th scope="col" className={cellClassName}>
|
||||
Name
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedDetails.map(
|
||||
(
|
||||
{
|
||||
value,
|
||||
url,
|
||||
measurementUnit,
|
||||
lowerIsBetter,
|
||||
title,
|
||||
suite,
|
||||
perfdocs,
|
||||
},
|
||||
idx,
|
||||
) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<tr key={idx}>
|
||||
{/* Ensure the value and measurement are visually next to each
|
||||
other in the chart, by aligning the value to the right. */}
|
||||
<td className={`text-right ${cellClassName}`}>{value}</td>
|
||||
<td className={cellClassName}>{measurementUnit || '–'}</td>
|
||||
<td className={cellClassName}>
|
||||
{lowerIsBetter ? 'Lower' : 'Higher'}
|
||||
</td>
|
||||
<td className={cellClassName}>
|
||||
<a
|
||||
href={url}
|
||||
className="btn btn-outline-darker-secondary btn-sm performance-panel-view-button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
<td className="w-100">
|
||||
{perfdocs.hasDocumentation() ? (
|
||||
<div>
|
||||
<a
|
||||
href={perfdocs.documentationURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{`${suite} `}
|
||||
</a>
|
||||
{`${perfdocs.remainingName}`}
|
||||
</div>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
),
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { repoName, revision, selectedJobFull, jobDetails } = this.props;
|
||||
const {
|
||||
repoName,
|
||||
revision,
|
||||
selectedJobFull,
|
||||
jobDetails,
|
||||
perfJobDetail,
|
||||
} = this.props;
|
||||
const { triggeredGeckoProfiles, showSideBySide } = this.state;
|
||||
const profilerLink = this.maybeGetFirefoxProfilerLink();
|
||||
|
||||
|
@ -214,6 +148,36 @@ class PerformanceTab extends React.PureComponent {
|
|||
</Button>
|
||||
) : null
|
||||
}
|
||||
{selectedJobFull.hasSideBySide && (
|
||||
<a
|
||||
title="Open side-by-side job"
|
||||
href={getJobsUrl({
|
||||
repo: repoName,
|
||||
revision,
|
||||
searchStr: selectedJobFull.hasSideBySide,
|
||||
group_state: 'expanded',
|
||||
})}
|
||||
className="btn btn-darker-secondary btn-sm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} className="mr-2" />
|
||||
<FontAwesomeIcon icon={faYoutube} className="mr-2" />
|
||||
Open side-by-side job
|
||||
</a>
|
||||
)}
|
||||
{isPerfTest(selectedJobFull) &&
|
||||
!showSideBySide &&
|
||||
!selectedJobFull.hasSideBySide && (
|
||||
<Button
|
||||
className="btn btn-darker-secondary btn-sm"
|
||||
onClick={this.createSideBySide}
|
||||
title="Generate side-by-side"
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilm} className="mr-2" />
|
||||
Generate side-by-side
|
||||
</Button>
|
||||
)}
|
||||
<a
|
||||
href={getCompareChooserUrl({
|
||||
newProject: repoName,
|
||||
|
@ -242,7 +206,12 @@ class PerformanceTab extends React.PureComponent {
|
|||
</Alert>
|
||||
) : null
|
||||
}
|
||||
{this.maybeRenderPerfData()}
|
||||
{perfJobDetail.length !== 0 && (
|
||||
<PerfData
|
||||
perfJobDetail={perfJobDetail}
|
||||
selectedJobFull={selectedJobFull}
|
||||
/>
|
||||
)}
|
||||
{showSideBySide && <SideBySide jobDetails={jobDetails} />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@ import sortBy from 'lodash/sortBy';
|
|||
import { Col } from 'reactstrap';
|
||||
|
||||
import {
|
||||
sxsTaskName,
|
||||
thEvents,
|
||||
thOptionOrder,
|
||||
thPlatformMap,
|
||||
|
@ -316,6 +317,29 @@ class Push extends React.PureComponent {
|
|||
job.task_run = getTaskRunStr(job);
|
||||
return job;
|
||||
});
|
||||
const sideBySideJobs = newJobList.filter((sxsJob) =>
|
||||
sxsJob.job_type_symbol.includes(sxsTaskName),
|
||||
);
|
||||
// If the pageload job has a side-by-side comparison associated
|
||||
// add job.hasSideBySide containing sxsTaskName ("side-by-side")
|
||||
newJobList.forEach((job) => {
|
||||
if (job.job_type_name.includes('browsertime')) {
|
||||
const matchingSxsJobs = sideBySideJobs.filter(
|
||||
(sxsJob) =>
|
||||
sxsJob.job_type_name.includes(
|
||||
job.job_type_name.split('/opt-')[0],
|
||||
) && // platform
|
||||
sxsJob.job_type_name.includes(
|
||||
job.job_type_name.split('/opt-')[1],
|
||||
), // testName
|
||||
);
|
||||
if (matchingSxsJobs.length > 0) {
|
||||
job.hasSideBySide = matchingSxsJobs[0].job_type_name;
|
||||
} else {
|
||||
job.hasSideBySide = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
const platforms = this.sortGroupedJobs(
|
||||
this.groupJobByPlatform(newJobList),
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче