diff --git a/src/elm/Components/BarChart.elm b/src/elm/Components/BarChart.elm index 1fdf76ae0..254b34896 100644 --- a/src/elm/Components/BarChart.elm +++ b/src/elm/Components/BarChart.elm @@ -31,6 +31,10 @@ import TypedSvg.Types exposing (AnchorAlignment(..), Transform(..)) import Utils.Helpers as Util + +-- TYPES + + {-| UnitFormat defines what format the values in the chart should be displayed in. -} type UnitFormat @@ -160,6 +164,10 @@ withNumberUnit v (BarChartConfig config) = BarChartConfig { config | unit = number v } + +-- VIEW + + {-| view: takes title, width (optional), height (optional), data, optional maximum y-axis value, unit as string, and returns a chart. -} diff --git a/src/elm/Metrics/BuildMetrics.elm b/src/elm/Metrics/BuildMetrics.elm index 490538bac..61af562d8 100644 --- a/src/elm/Metrics/BuildMetrics.elm +++ b/src/elm/Metrics/BuildMetrics.elm @@ -3,9 +3,19 @@ SPDX-License-Identifier: Apache-2.0 --} -module Metrics.BuildMetrics exposing (Metrics, calculateAverageRuntime, calculateAverageTimeToRecovery, calculateBuildFrequency, calculateEventBranchMetrics, calculateFailureRate, calculateMetrics) +module Metrics.BuildMetrics exposing + ( Metrics + , calculateAverageRuntime + , calculateAverageTimeToRecovery + , calculateBuildFrequency + , calculateEventBranchMetrics + , calculateFailureRate + , calculateMetrics + , filterCompletedBuilds + ) import Dict exposing (Dict) +import Html exposing (b) import Statistics import Vela @@ -25,17 +35,26 @@ type alias StatusMetrics = type alias OverallMetrics = - { failureRate : Float - , averageQueueTime : Float + { -- frequency metrics + buildFrequency : Int + , deployFrequency : Int + + -- duration metrics , averageRuntime : Float + , stdDeviationRuntime : Float + , medianRuntime : Float , timeUsedOnFailedBuilds : Float + + -- relability , successRate : Float - , medianQueueTime : Float - , medianRuntime : Float - , stdDeviationRuntime : Float - , buildFrequency : Int - , deployFrequency : Int + , failureRate : Float , averageTimeToRecovery : Float + + -- queue metrics + , averageQueueTime : Float + , medianQueueTime : Float + + -- aggregrates , eventBranchMetrics : Dict ( String, String ) EventBranchMetrics } @@ -52,6 +71,9 @@ type alias TimeSeriesData = } +{-| calculateMetrics : calculates metrics based on the list of builds passed in. +returns Nothing when the list is empty. +-} calculateMetrics : List Vela.Build -> Maybe Metrics calculateMetrics builds = if List.isEmpty builds then @@ -59,39 +81,47 @@ calculateMetrics builds = else let - failureRate = - calculateFailureRate builds + completedBuilds = + filterCompletedBuilds builds - averageQueueTime = - calculateAverageQueueTime builds + -- frequency + buildFrequency = + calculateBuildFrequency builds + deployFrequency = + calculateDeployFrequency builds + + -- duration averageRuntime = - calculateAverageRuntime builds + calculateAverageRuntime completedBuilds + + stdDeviationRuntime = + calculateStdDeviationRuntime completedBuilds + + medianRuntime = + calculateMedianRuntime completedBuilds timeUsedOnFailedBuilds = calculateTimeUsedOnFailedBuilds builds + -- reliability successRate = calculateSuccessRate builds - medianQueueTime = - calculateMedianQueueTime builds - - medianRuntime = - calculateMedianRuntime builds - - stdDeviationRuntime = - calculateStdDeviationRuntime builds - - buildFrequency = - calculateBuildFrequency builds - - deployFrequency = - calculateDeployFrequency builds + failureRate = + calculateFailureRate builds averageTimeToRecovery = calculateAverageTimeToRecovery builds + -- queue metrics + averageQueueTime = + calculateAverageQueueTime builds + + medianQueueTime = + calculateMedianQueueTime builds + + -- aggregrates eventBranchMetrics = calculateEventBranchMetrics builds @@ -100,133 +130,192 @@ calculateMetrics builds = in Just { overall = - { failureRate = failureRate - , averageQueueTime = averageQueueTime + { buildFrequency = buildFrequency + , deployFrequency = deployFrequency , averageRuntime = averageRuntime + , stdDeviationRuntime = stdDeviationRuntime + , medianRuntime = medianRuntime , timeUsedOnFailedBuilds = timeUsedOnFailedBuilds , successRate = successRate - , medianQueueTime = medianQueueTime - , medianRuntime = medianRuntime - , stdDeviationRuntime = stdDeviationRuntime - , buildFrequency = buildFrequency - , deployFrequency = deployFrequency + , failureRate = failureRate , averageTimeToRecovery = averageTimeToRecovery + , averageQueueTime = averageQueueTime + , medianQueueTime = medianQueueTime , eventBranchMetrics = eventBranchMetrics } , byStatus = byStatus } -calculateMedianRuntime : List Vela.Build -> Float -calculateMedianRuntime builds = +filterCompletedBuilds : List Vela.Build -> List Vela.Build +filterCompletedBuilds builds = + builds + |> List.filter (\build -> build.status /= Vela.Pending) + |> List.filter (\build -> build.status /= Vela.PendingApproval) + |> List.filter (\build -> build.status /= Vela.Running) + + + +-- frequency calculations + + +calculateBuildFrequency : List Vela.Build -> Int +calculateBuildFrequency builds = let - legitBuilds = - builds - |> List.filter (\build -> build.status /= Vela.Pending) - |> List.filter (\build -> build.status /= Vela.Running) + sortedByCreated = + List.sortBy .created builds + + firstBuildTime = + List.head sortedByCreated |> Maybe.map .created |> Maybe.withDefault 0 + + lastBuildTime = + List.reverse sortedByCreated |> List.head |> Maybe.map .created |> Maybe.withDefault 0 + + totalSeconds = + lastBuildTime - firstBuildTime + + totalDays = + -- if we start a new day, we just count the whole day + max 1 (ceiling (toFloat totalSeconds / (24 * 60 * 60))) - runTimes = - List.map (\build -> toFloat (build.finished - build.started)) legitBuilds + totalBuilds = + List.length builds in - calculateMedian runTimes + if totalDays == 0 then + 0 + else + totalBuilds // totalDays -calculateStdDeviationRuntime : List Vela.Build -> Float -calculateStdDeviationRuntime builds = + +calculateDeployFrequency : List Vela.Build -> Int +calculateDeployFrequency builds = let - legitBuilds = + sortedByCreated = builds - |> List.filter (\build -> build.status /= Vela.Pending) - |> List.filter (\build -> build.status /= Vela.Running) + |> List.filter (\build -> build.event == "deployment") + |> List.sortBy .created + + firstBuildTime = + List.head sortedByCreated |> Maybe.map .created |> Maybe.withDefault 0 - runTimes = - List.map (\build -> toFloat (build.finished - build.started)) legitBuilds + lastBuildTime = + List.reverse sortedByCreated |> List.head |> Maybe.map .created |> Maybe.withDefault 0 + + totalSeconds = + lastBuildTime - firstBuildTime + + totalDays = + max 1 (totalSeconds // (24 * 60 * 60)) + + totalBuilds = + List.length sortedByCreated in - calculateStdDeviation runTimes + if totalDays == 0 then + 0 + else + totalBuilds // totalDays -calculateMedianQueueTime : List Vela.Build -> Float -calculateMedianQueueTime builds = + + +-- duration calculations + + +calculateAverageRuntime : List Vela.Build -> Float +calculateAverageRuntime builds = let - legitBuilds = - builds - |> List.filter (\build -> build.started > 0) + total = + List.foldl (\build acc -> acc + (build.finished - build.started)) 0 builds - queueTimes = - List.map (\build -> toFloat (build.started - build.enqueued)) legitBuilds + count = + List.length builds in - calculateMedian queueTimes + if count == 0 then + 0 + + else + toFloat (total // count) + + +calculateStdDeviationRuntime : List Vela.Build -> Float +calculateStdDeviationRuntime builds = + builds + |> List.map (\build -> toFloat (build.finished - build.started)) + |> calculateStdDeviation + + +calculateMedianRuntime : List Vela.Build -> Float +calculateMedianRuntime builds = + builds + |> List.map (\build -> toFloat (build.finished - build.started)) + |> calculateMedian + + +calculateTimeUsedOnFailedBuilds : List Vela.Build -> Float +calculateTimeUsedOnFailedBuilds builds = + builds + |> List.filter (\build -> build.status == Vela.Failure) + |> List.foldl (\build acc -> acc + (build.finished - build.started)) 0 + |> toFloat + + + +-- reliability calculations calculateSuccessRate : List Vela.Build -> Float calculateSuccessRate builds = let total = - List.length builds + builds + |> List.length + |> toFloat succeeded = - List.length (List.filter (\build -> build.status == Vela.Success) builds) + builds + |> List.filter (\build -> build.status == Vela.Success) + |> List.length + |> toFloat in if total == 0 then 0 else - (toFloat succeeded / toFloat total) * 100 + (succeeded / total) * 100 -calculateMetricsByStatus : List Vela.Build -> Dict String StatusMetrics -calculateMetricsByStatus builds = +calculateFailureRate : List Vela.Build -> Float +calculateFailureRate builds = let - -- Group builds by status - groupedBuilds = - List.foldl - (\build acc -> - let - key = - Vela.statusToString build.status - in - Dict.update key (Maybe.map (\lst -> Just (build :: lst)) >> Maybe.withDefault (Just [ build ])) acc - ) - Dict.empty - builds - - calculateMetricsForGroup b = - let - buildTimes = - List.map (\build -> toFloat (build.finished - build.started)) b - - medianRuntime = - calculateMedian buildTimes - - averageRuntime = - calculateAverageRuntime b - - buildFrequency = - calculateBuildFrequency b + totalFailures = + builds + |> List.filter (\build -> build.status == Vela.Failure) + |> List.length + |> toFloat - eventBranchMetrics = - calculateEventBranchMetrics b - in - { averageRuntime = averageRuntime - , medianRuntime = medianRuntime - , buildFrequency = buildFrequency - , eventBranchMetrics = eventBranchMetrics - } + count = + builds + |> List.length + |> toFloat in - Dict.map (\_ buildss -> calculateMetricsForGroup buildss) groupedBuilds + if count == 0 then + 0 + + else + (totalFailures / count) * 100 calculateAverageTimeToRecovery : List Vela.Build -> Float calculateAverageTimeToRecovery builds = let - -- Filter the builds to get only failed and successful builds failedBuilds = List.filter (\build -> build.status == Vela.Failure) builds successfulBuilds = List.filter (\build -> build.status == Vela.Success) builds - -- Group builds by branch + -- group builds by branch groupByBranch b = List.foldl (\build acc -> @@ -241,7 +330,7 @@ calculateAverageTimeToRecovery builds = groupedSuccessfulBuilds = groupByBranch successfulBuilds - -- Find pairs of failed and subsequent successful builds within each branch + -- find pairs of failed and subsequent successful builds within each branch findRecoveryTimes f s = case ( f, s ) of ( [], _ ) -> @@ -257,7 +346,7 @@ calculateAverageTimeToRecovery builds = else findRecoveryTimes f restSuccess - -- Calculate the time differences for each branch + -- calculate the time differences for each branch calculateBranchRecoveryTimes branch = let f = @@ -268,101 +357,101 @@ calculateAverageTimeToRecovery builds = in findRecoveryTimes (List.sortBy .created f) (List.sortBy .created s) - -- Aggregate recovery times across all branches + -- aggregate recovery times across all branches allRecoveryTimes = Dict.keys groupedFailedBuilds |> List.concatMap calculateBranchRecoveryTimes - -- Compute the average of the time differences + -- compute the average of the time differences totalRecoveryTime = - List.sum allRecoveryTimes + toFloat (List.sum allRecoveryTimes) count = - List.length allRecoveryTimes + toFloat (List.length allRecoveryTimes) in if count == 0 then 0 else - toFloat totalRecoveryTime / toFloat count + totalRecoveryTime / count -calculateBuildFrequency : List Vela.Build -> Int -calculateBuildFrequency builds = - let - sortedByCreated = - List.sortBy .created builds - - firstBuildTime = - List.head sortedByCreated |> Maybe.map .created |> Maybe.withDefault 0 - lastBuildTime = - List.reverse sortedByCreated |> List.head |> Maybe.map .created |> Maybe.withDefault 0 +-- queue time calculations - totalSeconds = - lastBuildTime - firstBuildTime - totalDays = - -- if we start a new day, we just count the whole day - max 1 (ceiling (toFloat totalSeconds / (24 * 60 * 60))) +calculateAverageQueueTime : List Vela.Build -> Float +calculateAverageQueueTime builds = + let + total = + builds + |> List.filter (\build -> build.started > 0) + |> List.foldl (\build acc -> acc + (build.started - build.enqueued)) 0 - totalBuilds = + count = List.length builds in - if totalDays == 0 then + if count == 0 then 0 else - totalBuilds // totalDays - - -calculateDeployFrequency : List Vela.Build -> Int -calculateDeployFrequency builds = - let - sortedByCreated = - builds - |> List.filter (\build -> not (String.isEmpty build.deploy)) - |> List.sortBy .created - - firstBuildTime = - List.head sortedByCreated |> Maybe.map .created |> Maybe.withDefault 0 + toFloat (total // count) - lastBuildTime = - List.reverse sortedByCreated |> List.head |> Maybe.map .created |> Maybe.withDefault 0 - totalSeconds = - lastBuildTime - firstBuildTime +calculateMedianQueueTime : List Vela.Build -> Float +calculateMedianQueueTime builds = + builds + |> List.filter (\build -> build.started > 0) + |> List.map (\build -> toFloat (build.started - build.enqueued)) + |> calculateMedian - totalDays = - max 1 (totalSeconds // (24 * 60 * 60)) - totalBuilds = - List.length sortedByCreated - in - if totalDays == 0 then - 0 +calculateMetricsByStatus : List Vela.Build -> Dict String StatusMetrics +calculateMetricsByStatus builds = + let + -- group builds by status + groupedBuilds = + List.foldl + (\build acc -> + let + key = + Vela.statusToString build.status + in + Dict.update key (Maybe.map (\lst -> Just (build :: lst)) >> Maybe.withDefault (Just [ build ])) acc + ) + Dict.empty + builds - else - totalBuilds // totalDays + calculateMetricsForGroup b = + let + buildTimes = + List.map (\build -> toFloat (build.finished - build.started)) b + medianRuntime = + calculateMedian buildTimes -calculateMedian : List Float -> Float -calculateMedian list = - List.sort list - |> Statistics.quantile 0.5 - |> Maybe.withDefault 0 + averageRuntime = + calculateAverageRuntime b + buildFrequency = + calculateBuildFrequency b -calculateStdDeviation : List Float -> Float -calculateStdDeviation list = - Statistics.deviation list - |> Maybe.withDefault 0 + eventBranchMetrics = + calculateEventBranchMetrics b + in + { averageRuntime = averageRuntime + , medianRuntime = medianRuntime + , buildFrequency = buildFrequency + , eventBranchMetrics = eventBranchMetrics + } + in + Dict.map (\_ buildList -> calculateMetricsForGroup buildList) groupedBuilds calculateEventBranchMetrics : List Vela.Build -> Dict ( String, String ) EventBranchMetrics calculateEventBranchMetrics builds = let - -- Group builds by (event, branch) + -- group builds by (event, branch) groupedBuilds = List.foldl (\build acc -> @@ -375,7 +464,7 @@ calculateEventBranchMetrics builds = Dict.empty builds - -- Calculate metrics for each group + -- calculate metrics for each group calculateMetricsForGroup b = let buildTimes = @@ -396,66 +485,21 @@ calculateEventBranchMetrics builds = , buildTimesOverTime = buildTimesOverTime } in - Dict.map (\_ buildss -> calculateMetricsForGroup buildss) groupedBuilds - - -calculateAverageRuntime : List Vela.Build -> Float -calculateAverageRuntime builds = - let - legitBuilds = - builds - |> List.filter (\build -> build.status /= Vela.Pending) - |> List.filter (\build -> build.status /= Vela.Running) - - total = - legitBuilds - |> List.foldl (\build acc -> acc + (build.finished - build.started)) 0 - - count = - List.length legitBuilds - in - if count == 0 then - 0 - - else - toFloat (total // count) - - -calculateAverageQueueTime : List Vela.Build -> Float -calculateAverageQueueTime builds = - let - total = - builds |> List.filter (\build -> build.started > 0) |> List.foldl (\build acc -> acc + (build.started - build.enqueued)) 0 + Dict.map (\_ buildsList -> calculateMetricsForGroup buildsList) groupedBuilds - count = - List.length builds - in - if count == 0 then - 0 - - else - toFloat (total // count) -calculateFailureRate : List Vela.Build -> Float -calculateFailureRate builds = - let - totalFailures = - builds |> List.filter (\build -> build.status == Vela.Failure) |> List.length +-- generic helpers - count = - List.length builds - in - if count == 0 then - 0 - else - toFloat totalFailures / toFloat count * 100 +calculateMedian : List Float -> Float +calculateMedian list = + List.sort list + |> Statistics.quantile 0.5 + |> Maybe.withDefault 0 -calculateTimeUsedOnFailedBuilds : List Vela.Build -> Float -calculateTimeUsedOnFailedBuilds builds = - builds - |> List.filter (\build -> build.status == Vela.Failure) - |> List.foldl (\build acc -> acc + (build.finished - build.started)) 0 - |> toFloat +calculateStdDeviation : List Float -> Float +calculateStdDeviation list = + Statistics.deviation list + |> Maybe.withDefault 0 diff --git a/src/elm/Pages/Org_/Repo_/Insights.elm b/src/elm/Pages/Org_/Repo_/Insights.elm index 4ca3ccc01..2b6b08e69 100644 --- a/src/elm/Pages/Org_/Repo_/Insights.elm +++ b/src/elm/Pages/Org_/Repo_/Insights.elm @@ -45,7 +45,7 @@ page user shared route = -- LAYOUT -{-| toLayout : takes user, route, model, and passes the deployments page info to Layouts. +{-| toLayout : takes user, route, model, and passes the insights page info to Layouts. -} toLayout : Auth.User -> Route { org : String, repo : String } -> Model -> Layouts.Layout Msg toLayout user route model = @@ -67,12 +67,17 @@ toLayout user route model = -- INIT +{-| Model : alias for a model object for an insights page. +we store the builds and calculated metrics. +-} type alias Model = { builds : WebData (List Vela.Build) , metrics : Maybe Metrics } +{-| init : takes shared model, route, and initializes an insights page input arguments. +-} init : Shared.Model -> Route { org : String, repo : String } -> () -> ( Model, Effect Msg ) init shared route () = let @@ -104,10 +109,14 @@ init shared route () = -- UPDATE +{-| Msg : custom type with possible messages. +-} type Msg = GetRepoBuildsResponse (Result (Http.Detailed.Error String) ( Http.Metadata, List Vela.Build )) +{-| update : takes current models, route, message, and returns an updated model and effect. +-} update : Shared.Model -> Route { org : String, repo : String } -> Msg -> Model -> ( Model, Effect Msg ) update shared route msg model = case msg of @@ -138,6 +147,8 @@ update shared route msg model = -- SUBSCRIPTIONS +{-| subscriptions : takes model and returns the subscriptions. +-} subscriptions : Model -> Sub Msg subscriptions model = Sub.none @@ -147,6 +158,8 @@ subscriptions model = -- VIEW +{-| view : takes models, route, and creates the html for the insights page. +-} view : Shared.Model -> Route { org : String, repo : String } -> Model -> View Msg view shared route model = { title = "Pipeline Insights" @@ -168,13 +181,15 @@ view shared route model = viewEmpty _ -> - viewInsights model shared.time shared.zone + viewInsights model shared.time ) } -viewInsights : Model -> Time.Posix -> Time.Zone -> List (Html Msg) -viewInsights model now time = +{-| viewInsights : take model and current time and renders metrics. +-} +viewInsights : Model -> Time.Posix -> List (Html Msg) +viewInsights model now = case ( model.metrics, model.builds ) of ( Just m, RemoteData.Success builds ) -> let @@ -277,6 +292,8 @@ viewInsights model now time = [ h3 [] [ text "No Metrics to Show" ] ] +{-| viewMetrics : takes a value and description as strings and renders a quick metric. +-} viewMetric : String -> String -> Html msg viewMetric value description = div [ class "metric" ] @@ -285,6 +302,8 @@ viewMetric value description = ] +{-| viewEmpty : renders information when there are no builds returned. +-} viewEmpty : List (Html msg) viewEmpty = [ h3 [ Helpers.testAttribute "no-builds" ] [ text "No builds found" ] @@ -292,6 +311,8 @@ viewEmpty = ] +{-| viewError : renders information when there was an error retrieving the builds. +-} viewError : List (Html msg) viewError = [ h3 [] [ text "There was an error retrieving builds :(" ] diff --git a/tests/BuildMetricsTest.elm b/tests/BuildMetricsTest.elm index 213fbf40d..cbccddc1c 100644 --- a/tests/BuildMetricsTest.elm +++ b/tests/BuildMetricsTest.elm @@ -131,7 +131,7 @@ suite = , createSampleBuild 2 15 0 Vela.Pending "push" "main" ] in - calculateAverageRuntime builds + calculateAverageRuntime (filterCompletedBuilds builds) |> Expect.equal 15 , test "calculates average runtime for varied build run times" <| \_ -> @@ -142,7 +142,7 @@ suite = , createSampleBuild 2 567 0 Vela.Pending "push" "main" ] in - calculateAverageRuntime builds + calculateAverageRuntime (filterCompletedBuilds builds) |> Expect.equal 178 ] , describe "calculateMetrics"