Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {RunInfoFromReportServer} from '../../../../../runner/shared-interfaces';
import {BuildResultStatus} from '../../../../../runner/workers/builder/builder-types';
import {ScoreCssVariable} from '../../shared/scoring';
import {StackedBarChartData} from '../../shared/visualization/stacked-bar-chart/stacked-bar-chart';

/**
* Calculates the average number of repair attempts performed in a run.
*/
export function calculateAverageRepairAttempts(report: RunInfoFromReportServer) {
let totalRepairs = 0;
let count = 0;

for (const result of report.results) {
// Only consider successful builds that required repairs.
if (
result.finalAttempt.buildResult.status === BuildResultStatus.SUCCESS &&
result.repairAttempts > 0
) {
totalRepairs += result.repairAttempts;
count++;
}
}

return count > 0 ? totalRepairs / count : null;
}

/**
* Creates graph data for the "repair attempt" graph, from a given run report.
*/
export function createRepairAttemptGraphData(report: RunInfoFromReportServer) {
const repairsToAppCount = new Map<number | 'failed', number>();

// Map repair count to how many applications shared that count.
let maxRepairCount = 0;
for (const result of report.results) {
if (result.finalAttempt.buildResult.status === BuildResultStatus.ERROR) {
repairsToAppCount.set('failed', (repairsToAppCount.get('failed') || 0) + 1);
} else {
const repairs = result.repairAttempts;
// For this graph, we ignore applications that required no repair.
if (repairs > 0) {
repairsToAppCount.set(repairs, (repairsToAppCount.get(repairs) || 0) + 1);
maxRepairCount = Math.max(maxRepairCount, repairs);
}
}
}

const data: StackedBarChartData = [];

// All the numeric keys, sorted by value.
const intermediateRepairKeys = Array.from(repairsToAppCount.keys())
.filter((k): k is number => typeof k === 'number')
.sort((a, b) => a - b);

// This graph might involve a bunch of sections. We want to scale them among all the possible color "grades".

for (let repairCount = 1; repairCount <= maxRepairCount; repairCount++) {
const applicationCount = repairsToAppCount.get(repairCount);
if (!applicationCount) continue;

data.push({
label: labelByRepairCount(repairCount),
color: colorByRepairCount(repairCount),
value: applicationCount,
});
}

// Handle 'Build failed even after all retries' - always maps to the "failure" grade.
const failedCount = repairsToAppCount.get('failed') || 0;
if (failedCount > 0) {
data.push({
label: 'Build failed even after all retries',
color: ScoreCssVariable.poor,
value: failedCount,
});
}
return data;
}

function labelByRepairCount(repairCount: number): string {
switch (repairCount) {
case 1:
return '1 repair';
case 2:
case 3:
case 4:
return `${repairCount} repairs`;
default:
return '5+ repairs';
}
}

function colorByRepairCount(repairCount: number): string {
// We're using mediocre1-5 since these are essentially *all* bad so we don't want green in this
// graph.
switch (repairCount) {
case 1:
return ScoreCssVariable.mediocre1;
case 2:
return ScoreCssVariable.mediocre2;
case 3:
return ScoreCssVariable.mediocre3;
case 4:
return ScoreCssVariable.mediocre4;
default:
return ScoreCssVariable.mediocre5;
}
}
26 changes: 26 additions & 0 deletions report-app/src/app/pages/report-viewer/report-viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@ <h3 class="chart-title">
<stacked-bar-chart [data]="buildsAsGraphData(overview.stats.builds)" [compact]="true" />
</div>
</div>
@if (hasSuccessfulResultWithMoreThanOneBuildAttempt()) {
<div class="chart-container repair-attempts">
<h3>
<span class="material-symbols-outlined">build_circle</span>
<span>Repair attempts</span>
<span
class="material-symbols-outlined has-tooltip multiline-tooltip chart-title-tooltip-icon"
data-tooltip="For applications that required repairs to be built, this displays the distribution of how many repair attempts were required."
>info</span
>
@if (averageRepairAttempts() !== null) {
<span class="chart-title-right-label"
>Avg: {{ averageRepairAttempts() | number: '1.2-2' }}</span
>
<span
class="material-symbols-outlined has-tooltip multiline-tooltip chart-title-tooltip-icon"
data-tooltip="Average repair count among applications that were successfully built after repairs."
>info</span
>
}
</h3>
<div class="summary-card-item">
<stacked-bar-chart [data]="repairAttemptsAsGraphData()" [compact]="true" />
</div>
</div>
}
@if (overview.stats.tests) {
<div class="chart-container test-results-details">
<h3 class="chart-title">
Expand Down
11 changes: 11 additions & 0 deletions report-app/src/app/pages/report-viewer/report-viewer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ lighthouse-category + lighthouse-category {
align-items: center;
}

.chart-title-tooltip-icon {
font-size: 18px;
cursor: help;
}

.chart-title-right-label {
margin-left: auto;
font-size: 0.9rem;
font-weight: 500;
}

.axe-violations ul {
padding: 0px 20px;
}
Expand Down
54 changes: 42 additions & 12 deletions report-app/src/app/pages/report-viewer/report-viewer.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import {Clipboard} from '@angular/cdk/clipboard';
import {DatePipe, DecimalPipe} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {
afterNextRender,
Component,
computed,
ElementRef,
inject,
input,
resource,
signal,
viewChild,
} from '@angular/core';
import {afterNextRender, Component, computed, inject, input, resource, signal} from '@angular/core';
import {NgxJsonViewerModule} from 'ngx-json-viewer';
import {BuildErrorType} from '../../../../../runner/workers/builder/builder-types';
import {
BuildErrorType,
BuildResultStatus,
} from '../../../../../runner/workers/builder/builder-types';
import {
AssessmentResult,
AssessmentResultFromReportServer,
Expand Down Expand Up @@ -46,6 +39,10 @@ import {AiAssistant} from '../../shared/ai-assistant/ai-assistant';
import {LighthouseCategory} from './lighthouse-category';
import {MultiSelect} from '../../shared/multi-select/multi-select';
import {FileCodeViewer} from '../../shared/file-code-viewer/file-code-viewer';
import {
calculateAverageRepairAttempts,
createRepairAttemptGraphData,
} from './repair-attempt-graph-builder';

const localReportRegex = /-l\d+$/;

Expand Down Expand Up @@ -283,6 +280,39 @@ export class ReportViewer {
];
}

protected hasSuccessfulResultWithMoreThanOneBuildAttempt = computed(() => {
if (!this.selectedReport.hasValue()) {
return false;
}
for (const result of this.selectedReport.value().results) {
if (
result.finalAttempt.buildResult.status === BuildResultStatus.SUCCESS &&
result.repairAttempts > 1
) {
return true;
}
}
return false;
});

protected averageRepairAttempts = computed<number | null>(() => {
const report = this.selectedReportWithSortedResults();
if (!report) {
return null;
}

return calculateAverageRepairAttempts(report);
});

protected repairAttemptsAsGraphData = computed<StackedBarChartData>(() => {
const report = this.selectedReportWithSortedResults();
if (!report) {
return [];
}

return createRepairAttemptGraphData(report);
});

protected testsAsGraphData(tests: RunSummaryTests): StackedBarChartData {
return [
{
Expand Down
6 changes: 6 additions & 0 deletions report-app/src/app/shared/scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export enum ScoreCssVariable {
good = 'var(--status-fill-good)',
poor = 'var(--status-fill-poor)',
neutral = 'var(--status-fill-neutral)',
// When we need more refined gradiant between "good" and "poor".
mediocre1 = 'var(--status-fill-mediocre-1)',
mediocre2 = 'var(--status-fill-mediocre-2)',
mediocre3 = 'var(--status-fill-mediocre-3)',
mediocre4 = 'var(--status-fill-mediocre-4)',
mediocre5 = 'var(--status-fill-mediocre-5)',
}

const CACHED_COLORS = {
Expand Down
1 change: 1 addition & 0 deletions report-app/src/app/shared/styles/tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

&.multiline-tooltip::before {
white-space: normal;
width: max-content;
max-width: 400px;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@

.legend {
display: flex;
justify-content: center;
gap: 1.5rem;
flex-wrap: wrap;
justify-content: flex-start;
column-gap: 1.5rem;
}

.legend-item {
Expand All @@ -66,6 +67,7 @@
font-size: 14px;
color: var(--text-secondary);
white-space: nowrap;
margin-top: 0.5rem;
}

.legend-color {
Expand Down
7 changes: 7 additions & 0 deletions report-app/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
--status-fill-poor: #ef4444;
--status-fill-neutral: #aaa;

/* When we need a more gradiant spread of "meh". */
--status-fill-mediocre-1: #fbbc04; /* Yellow 500 */
--status-fill-mediocre-2: #f9ab00; /* Yellow 600 */
--status-fill-mediocre-3: #f29900; /* Yellow 700 */
--status-fill-mediocre-4: #ea8600; /* Yellow 800 */
--status-fill-mediocre-5: #e37400; /* Yellow 900 */

--status-text-excellent: #0c855d;
--status-text-great: #0c855d; // TODO: do we want to differentiate from `excellent`?
--status-text-good: #c57f08;
Expand Down
Loading