Entropic Thoughts

Build Failure Rate from Build Times

Build Failure Rate from Build Times

Looking through the articles I’ve started but never finished, I stumbled over a cool experiment from a few years ago: When a build in a ci environment fails, it usually does so quicker than the time required for a full, successful build. This means we ought to be able to find the fraction of failed builds from the build times alone.1 Don’t ask when this would be useful. I don’t remember.

We can pretend build times are a two-component Gaussian mixture. The Gaussian is probably an awful fit for failed builds, which tend to have multi-modal times until failure, but for a quick-and-dirty approximation it might be enough. And the maths are easier than if we did more complicated modeling.

To evaluate this approach lacking real data, we’ll have to generate a bunch of fake build data. This can be done by fitting theoretical distributions to real build times2 In my case, I used the normal distribution for successful builds, and a scaled beta distribution for failed ones.. In the code below, the simulated build failure rate is 28 %, and the fitted model estimates the failure rate as 24 %. Given how shaky the theoretical arguments for doing this are, that’s actually somewhat impressive!

Experienced C# developers will notice that this was written with an older version of C#, since it could have used many of the conveniences introduced in recent years.3 I did switch from block-scoped to file-scoped namespace just to reduce the indentation for viewing here on the web. Despite that, I’m actually a little surprised how nicely numeric code with Math.NET can read.4 I expected this to be somewhat hard to read, but it is very clear to me what goes on.

namespace buildtimefun;

using System;
using System.Linq;
using MathNet.Numerics;
using MathNet.Numerics.Distributions;
using MathNet.Numerics.LinearAlgebra;
using MathNet.Numerics.Random;
using MathNet.Numerics.Statistics;

class Program
{
    public const int N = 50;

    public static void Main(string[] args)
    {
        Diagnose();
        double[] buildTimes = Build(N);
        PrintRaw(buildTimes);
        PrintDensity(buildTimes);

        // Run 500 iterations of expectation maximisation, before printing
        // results.
        var components = new Iteration(buildTimes, 2);
        for (int i = 0; i < 500; i++)
        {
            components = EM(components);
        }

        Console.WriteLine($"p = {components.Fraction[1]}");
        Console.WriteLine("Mu:\n" + components.Mean);
        Console.WriteLine("Var:\n" + components.Variance);
    }

    // The input and output to one iteration of expectation maximisation.
    // The Values vector contains the underlying data, and the other vectors
    // contain parameter estimations for each of the estimated components.
    private struct Iteration
    {
        public Vector<double> Values;
        public Vector<double> Fraction;
        public Vector<double> Mean;
        public Vector<double> Variance;

        // Create initial data for the first iteration, given some values and a
        // desire to estimate n Gaussian components.
        public Iteration(double[] values, int n)
        {
            this.Values = Vector.Dense(values);
            this.Fraction = Vector.Dense(n, 1.0 / n);

            double[] means = values.SelectCombination(n).ToArray();
            this.Mean = Vector.Dense(means);

            double variance = Statistics.Variance(values)/20.0;
            this.Variance = Vector.Dense(n, variance);
        }
    }

    // Get the likelihoods of observing the values given this iteration of
    // estimated parameters for the Gaussian components.
    private static Matrix<Double> Evaluate(Iteration components)
    {
        return Matrix.Dense(
            components.Values.Count,
            components.Fraction.Count,
            (i, d) => Normal.PDF(
                components.Mean[d],
                Math.Sqrt(components.Variance[d]),
                components.Values[i]
            )
        );
    }

    private static Matrix<double> Rows(Vector<double> row, int n)
    {
        return Matrix.DenseOfRowVectors(
            Enumerable.Repeat(row, n)
        );
    }

    private static Matrix<double> Columns(Vector<double> column, int n)
    {
        return Matrix.DenseOfColumnVectors(
            Enumerable.Repeat(column, n)
        );
    }

    // Perform one iteration of expectation maximisation.
    private static Iteration EM(Iteration components)
    {
        var n = components.Values.Count;
        var d = components.Fraction.Count;

        var probability = Evaluate(components);
        var gamma = probability.PointwiseMultiply(
            Rows(components.Fraction, n)
        ).NormalizeRows(1);

        var fraction = gamma.ColumnSums() / n;

        var mean = gamma.Transpose().Multiply(
            components.Values.ToColumnMatrix()
        ).PointwiseDivide(
            gamma.ColumnSums().ToColumnMatrix()
        );

        var means = Columns(mean.Column(0), n);
        var remainders = Rows(components.Values, d)
            .Subtract(means)
            .PointwisePower(2);
        var variance = gamma.Transpose().PointwiseMultiply(
            remainders
        ).RowSums().ToColumnMatrix().PointwiseDivide(
            gamma.ColumnSums().ToColumnMatrix()
        );

        return new Iteration
        {
            Values = components.Values,
            Fraction = fraction,
            Mean = mean.Column(0),
            Variance = variance.Column(0)
        };
    }

    private static void PrintRaw(double[] times)
    {
        Console.WriteLine("====== RAW ======");
        foreach (var time in times)
        {
            Console.WriteLine($"{time:0.0}");
        }
    }

    private static void PrintDensity(double[] times)
    {
        Console.WriteLine("====== DENSITY ======");
        Array.Sort(times);
        var buckets = times.GroupBy(obs => (int) obs);
        foreach (var bucket in buckets)
        {
            Console.Write($"{bucket.Key}|");
            foreach (var time in bucket)
            {
                var tenth = (time - bucket.Key)*10;
                Console.Write($"{tenth:0}");
            }
            Console.WriteLine("");
        }
    }

    private static double[] Build(int samples)
    {
        return BuildSuccess.Samples()
            .Take(samples)
            .Select(d => BuildTime[d].Sample())
            .ToArray();
    }

    private static void Diagnose()
    {
        Console.WriteLine("====== DIAGNOSTICS ======");
        foreach (var distribution in BuildTime)
        {
            Console.WriteLine(
                $"Distribution = {distribution.GetType()}"
            );
            PrintDensity(
                distribution.Samples().Take(500).ToArray()
            );
        }
    }

    private static readonly
    IContinuousDistribution[] BuildTime =
        new IContinuousDistribution[]
    {
        new BetaScaled(a: 3.0, b: 5.0, location: 0.0, scale: 43.0),
        new Normal(mean: 37.0, stddev: 1.5)
    };

    private static readonly Bernoulli BuildSuccess =
        new Bernoulli(p: 0.72);

    private static readonly MatrixBuilder<double> Matrix =
        Matrix<Double>.Build;

    private static readonly VectorBuilder<double> Vector =
        Vector<Double>.Build;

}

This prints something like

====== DENSITY ======
7|7
11|56
12|0
13|10
17|6
18|8
19|7
20|27
22|0
28|9
34|188
35|11355678
36|23356810
37|0122335569
38|0123558
39|456

p = 0.759557687254126

Mu:
DenseVector 2-Double
17.0866
36.9441

Var:
DenseVector 2-Double
32.1969
1.94443