feat(cicd-statistics): Added median analysis (alongside average)

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
This commit is contained in:
Gustaf Räntilä
2022-01-21 16:31:00 +01:00
committed by blam
parent cbbab88d9f
commit 2d7f173e07
6 changed files with 100 additions and 74 deletions
@@ -0,0 +1,79 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FilterStatusType } from '../../apis/types';
import { ChartableStageAnalysis, ChartableStageDatapoints } from '../types';
export function getAnalysis(
values: Array<ChartableStageDatapoints>,
status: FilterStatusType,
): ChartableStageAnalysis {
const analysis: ChartableStageAnalysis = {
max: 0,
min: 0,
avg: 0,
med: 0,
};
const definedValues = values
.filter(value => typeof value[status] !== 'undefined')
.map(value => value[status]!)
.sort((a, b) => a - b);
analysis.max = definedValues[definedValues.length - 1] ?? 0;
analysis.min = definedValues[0] ?? 0;
analysis.avg =
definedValues.length === 0
? 0
: definedValues.reduce((prev, cur) => prev + cur, 0) / values.length;
analysis.med = definedValues[Math.ceil(definedValues.length / 2)] ?? 0;
return analysis;
}
export function makeCombinedAnalysis(
analysis: Record<FilterStatusType, ChartableStageAnalysis>,
allDurations: Array<number>,
): ChartableStageAnalysis {
if (analysis.succeeded) {
// If succeeded is a viewed status, it's probably what's expected to see
// overall. Otherwise combine all other.
return analysis.succeeded;
}
const analysisValues = Object.values(analysis);
const max = analysisValues.reduce((prev, cur) => Math.max(prev, cur.max), 0);
const min = analysisValues.reduce(
(prev, cur) => Math.min(prev, cur.min),
max,
);
const avg = !allDurations.length
? 0
: allDurations.reduce((prev, cur) => prev + cur, 0) / allDurations.length;
allDurations.sort((a, b) => a - b);
const med = allDurations[Math.ceil(allDurations.length / 2)] ?? 0;
return {
max,
min,
avg,
med,
};
}
@@ -17,7 +17,7 @@
import { FilterStatusType, statusTypes } from '../../apis/types';
import { Averagify, ChartableStage } from '../types';
import { countBuildsPerDay } from './count-builds-per-day';
import { getAnalysis } from './get-analysis';
import { getAnalysis, makeCombinedAnalysis } from './analysis';
import { average } from './utils';
interface FinalizeStageOptions {
@@ -54,7 +54,7 @@ export function finalizeStage(
countBuildsPerDay(values);
const avgDuration: [duration: number, count: number] = [0, 0];
const allDurations: Array<number> = [];
statusTypes.forEach(status => {
analysis[status] = getAnalysis(values, status);
@@ -70,8 +70,7 @@ export function finalizeStage(
(duration): duration is number => typeof duration !== 'undefined',
);
avgDuration[0] += durationsDense.reduce((prev, cur) => prev + cur, 0);
avgDuration[1] += durationsDense.length;
durationsDense.forEach(dur => allDurations.push(dur));
const averages = durationsDense.map((_, i) =>
average(
@@ -88,16 +87,7 @@ export function finalizeStage(
});
});
const analysisValues = Object.values(analysis);
combinedAnalysis.max = analysisValues.reduce(
(prev, cur) => Math.max(prev, cur.max),
0,
);
combinedAnalysis.min = analysisValues.reduce(
(prev, cur) => Math.min(prev, cur.min),
combinedAnalysis.max,
);
combinedAnalysis.avg = !avgDuration[1] ? 0 : avgDuration[0] / avgDuration[1];
Object.assign(combinedAnalysis, makeCombinedAnalysis(analysis, allDurations));
stage.stages.forEach(subStage => finalizeStage(subStage, options));
}
@@ -1,49 +0,0 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FilterStatusType } from '../../apis/types';
import { ChartableStageAnalysis, ChartableStageDatapoints } from '../types';
export function getAnalysis(
values: Array<ChartableStageDatapoints>,
status: FilterStatusType,
): ChartableStageAnalysis {
const analysis: ChartableStageAnalysis = {
max: 0,
min: 0,
avg: 0,
};
const definedValues = values.filter(
value => typeof value[status] !== 'undefined',
);
analysis.max = definedValues.reduce(
(prev, cur) => Math.max(prev, cur[status]!),
0,
);
analysis.min = definedValues.reduce(
(prev, cur) => Math.min(prev, cur[status]!),
analysis.max,
);
analysis.avg =
definedValues.length === 0
? 0
: definedValues.reduce((prev, cur) => prev + cur[status]!, 0) /
values.length;
return analysis;
}
@@ -38,17 +38,17 @@ export function getOrSetStage(
export function makeStage(name: string): ChartableStage {
return {
analysis: {
unknown: { avg: 0, max: 0, min: 0 },
enqueued: { avg: 0, max: 0, min: 0 },
scheduled: { avg: 0, max: 0, min: 0 },
running: { avg: 0, max: 0, min: 0 },
aborted: { avg: 0, max: 0, min: 0 },
succeeded: { avg: 0, max: 0, min: 0 },
failed: { avg: 0, max: 0, min: 0 },
stalled: { avg: 0, max: 0, min: 0 },
expired: { avg: 0, max: 0, min: 0 },
unknown: { avg: 0, med: 0, max: 0, min: 0 },
enqueued: { avg: 0, med: 0, max: 0, min: 0 },
scheduled: { avg: 0, med: 0, max: 0, min: 0 },
running: { avg: 0, med: 0, max: 0, min: 0 },
aborted: { avg: 0, med: 0, max: 0, min: 0 },
succeeded: { avg: 0, med: 0, max: 0, min: 0 },
failed: { avg: 0, med: 0, max: 0, min: 0 },
stalled: { avg: 0, med: 0, max: 0, min: 0 },
expired: { avg: 0, med: 0, max: 0, min: 0 },
},
combinedAnalysis: { avg: 0, max: 0, min: 0 },
combinedAnalysis: { avg: 0, med: 0, max: 0, min: 0 },
statusSet: new Set<FilterStatusType>(),
name,
values: [],
@@ -104,7 +104,8 @@ export function StageChart(props: StageChartProps) {
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>
{stage.name} (avg {formatDuration(stage.combinedAnalysis.avg)})
{stage.name} (med {formatDuration(stage.combinedAnalysis.med)}, avg{' '}
{formatDuration(stage.combinedAnalysis.avg)})
</Typography>
</AccordionSummary>
<AccordionDetails>
@@ -30,9 +30,14 @@ export type ChartableStageDatapoints = {
};
export interface ChartableStageAnalysis {
/** Maximum duration */
max: number;
/** Minimum duration */
min: number;
/** Average duration */
avg: number;
/** Median duration */
med: number;
}
export interface ChartableStage {