ui/job-view/details/tabs/PerformanceTab.jsx (271 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Alert, Button } from 'reactstrap'; import { faInfoCircle, faExternalLinkAlt, faRedo, faTable, faFilm, } from '@fortawesome/free-solid-svg-icons'; import { faYoutube } from '@fortawesome/free-brands-svg-icons'; import { getPerfCompareChooserUrl, 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'; const PROFILE_ZIP_RELEVANCE = 4; const PROFILE_RESOURCE_RELEVANCE = 3; const PROFILE_BUILD_RELEVANCE = 2; const PROFILE_JSON_RELEVANCE = 1; const NO_PROFILE_RELEVANCE = 0; const NON_PERFTEST_RELEVANCES = [ PROFILE_RESOURCE_RELEVANCE, PROFILE_BUILD_RELEVANCE, ]; /** * The performance tab shows performance-oriented information about a test run. * It helps users interact with the Firefox Profiler, and summarizes test * timing information. */ class PerformanceTab extends React.PureComponent { constructor(props) { super(props); const { selectedJobFull } = this.props; this.state = { triggeredGeckoProfiles: 0, showSideBySide: selectedJobFull.job_type_symbol.includes(sxsTaskName), }; } createGeckoProfile = async () => { const { selectedJobFull, notify, decisionTaskMap, currentRepo, } = this.props; 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, ); }; getProfileRelevance = (jobDetail) => { const { url, value } = jobDetail; if (!url) { return NO_PROFILE_RELEVANCE; } if (!value.startsWith('profile_')) { return NO_PROFILE_RELEVANCE; } if (value.endsWith('.zip')) { return PROFILE_ZIP_RELEVANCE; } if (!value.endsWith('.json')) { return NO_PROFILE_RELEVANCE; } if (value === 'profile_resource-usage.json') { return PROFILE_RESOURCE_RELEVANCE; } if (value === 'profile_build_resources.json') { return PROFILE_BUILD_RELEVANCE; } return PROFILE_JSON_RELEVANCE; }; // Returns profile-related job details, ordered by the relevance. getProfiles = (perfTestOnly) => { const profiles = []; for (const jobDetail of this.props.jobDetails) { const relevance = this.getProfileRelevance(jobDetail); if (relevance === NO_PROFILE_RELEVANCE) { continue; } if (perfTestOnly) { if (NON_PERFTEST_RELEVANCES.includes(relevance)) { continue; } } profiles.push({ jobDetail, relevance }); } return profiles.sort((a, b) => b.relevance - a.relevance); }; maybeGetFirefoxProfilerLink = (perfTestOnly) => { const profiles = this.getProfiles(perfTestOnly); if (profiles.length === 0) { return null; } // Use the most relevant profile. const { jobDetail } = profiles[0]; return ( <a title={jobDetail.value} href={getPerfAnalysisUrl(jobDetail.url)} className="btn btn-darker-secondary btn-sm" target="_blank" rel="noopener noreferrer" data-testid="open-profiler" > <FontAwesomeIcon icon={faExternalLinkAlt} className="mr-2" /> Open in Firefox Profiler </a> ); }; render() { const { repoName, revision, selectedJobFull, jobDetails, perfJobDetail, } = this.props; const { triggeredGeckoProfiles, showSideBySide } = this.state; // Just to be safe, use the same isPerfTest check the other // "Create Gecko Profile" button uses in the action menu. const perfTest = isPerfTest(selectedJobFull); const profilerLink = this.maybeGetFirefoxProfilerLink(perfTest); return ( <div className="performance-panel h-100 overflow-auto" role="region" aria-label="Performance" > <div className="performance-panel-actions d-flex"> { // If there is a profiler link, show this first. This is most likely // the primary action of the user here. profilerLink } {perfTest ? ( <Button className={`btn ${ // Only make this primary if there is no profiler link. profilerLink ? 'btn-outline-darker-secondary' : 'btn-darker-secondary' } btn-sm`} onClick={this.createGeckoProfile} title={ 'Trigger another run of this test with the profiler enabled. The ' + 'profile can then be viewed in the Firefox Profiler.' } data-testid="generate-profile" > <FontAwesomeIcon icon={faRedo} className="mr-2" /> {profilerLink ? 'Re-trigger performance profile' : 'Generate performance profile'} </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> )} {perfTest && !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={getPerfCompareChooserUrl({ newRepo: repoName, newRev: revision, frameworkName: perfJobDetail[0].frameworkName, })} target="_blank" rel="noopener noreferrer" className="btn btn-outline-darker-secondary btn-sm" > <FontAwesomeIcon icon={faTable} className="mr-2" /> Compare against another revision </a> </div> { // It can be confusing after triggering a profile what happens next. The // job list only gets populated later. This notification will help the // user know the next action. triggeredGeckoProfiles > 0 ? ( <Alert color="info" className="m-1"> <FontAwesomeIcon icon={faInfoCircle} className="mr-1" /> {triggeredGeckoProfiles === 1 ? `Triggered ${triggeredGeckoProfiles} profiler run. It will show up ` + `as a new entry in the job list once the task has been scheduled.` : `Triggered ${triggeredGeckoProfiles} profiler runs. They will show up ` + `as new entries in the job list once the task has been scheduled.`} </Alert> ) : null } {perfJobDetail.length !== 0 && ( <PerfData perfJobDetail={perfJobDetail} selectedJobFull={selectedJobFull} /> )} {showSideBySide && <SideBySide jobDetails={jobDetails} />} </div> ); } } PerformanceTab.propTypes = { repoName: PropTypes.string.isRequired, jobDetails: PropTypes.arrayOf(PropTypes.shape({})), perfJobDetail: PropTypes.arrayOf(PropTypes.shape({})), revision: PropTypes.string, decisionTaskMap: PropTypes.shape({}).isRequired, }; PerformanceTab.defaultProps = { jobDetails: [], perfJobDetail: [], revision: '', }; const mapStateToProps = (state) => ({ decisionTaskMap: state.pushes.decisionTaskMap, }); const mapDispatchToProps = { notify }; export default connect(mapStateToProps, mapDispatchToProps)(PerformanceTab);