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