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:
alexandru-io 2023-06-19 11:42:32 +03:00 коммит произвёл GitHub
Родитель d1ae8de25c
Коммит 09ae7bd381
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 263 добавлений и 117 удалений

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

@ -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),
);