From ebce9d00bec9be7c22713ae7b396cba6a975fc40 Mon Sep 17 00:00:00 2001 From: Ukaza Perdana Date: Mon, 18 Jul 2022 11:21:46 +0700 Subject: [PATCH] Approximate percentiles using t-digest --- lib/mobius/summary.ex | 29 ++++++++++++++++++++++++----- mix.exs | 3 ++- mix.lock | 2 ++ test/mobius/summary_test.exs | 25 +++++++++++++++++++------ 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/mobius/summary.ex b/lib/mobius/summary.ex index c6d8c37..0bc502c 100644 --- a/lib/mobius/summary.ex +++ b/lib/mobius/summary.ex @@ -4,7 +4,16 @@ defmodule Mobius.Summary do @typedoc """ Calculated summary statistics """ - @type t() :: %{min: integer(), max: integer(), average: float(), std_dev: float()} + @type t() :: %{ + min: integer(), + max: integer(), + average: float(), + std_dev: float(), + p50: float(), + p75: float(), + p95: float(), + p99: float() + } @typedoc """ A data type to store snapshot information about a summary in order @@ -15,7 +24,8 @@ defmodule Mobius.Summary do max: integer(), accumulated: integer(), accumulated_sqrd: integer(), - reports: non_neg_integer() + reports: non_neg_integer(), + t_digest: map() } @doc """ @@ -28,7 +38,8 @@ defmodule Mobius.Summary do max: metric_value, accumulated: metric_value, accumulated_sqrd: metric_value * metric_value, - reports: 1 + reports: 1, + t_digest: TDigest.new() |> TDigest.update(metric_value) } end @@ -42,7 +53,8 @@ defmodule Mobius.Summary do max: max(summary_data.max, new_metric_value), accumulated: summary_data.accumulated + new_metric_value, accumulated_sqrd: summary_data.accumulated_sqrd + new_metric_value * new_metric_value, - reports: summary_data.reports + 1 + reports: summary_data.reports + 1, + t_digest: TDigest.update(summary_data.t_digest, new_metric_value) } end @@ -56,7 +68,11 @@ defmodule Mobius.Summary do max: summary_data.max, average: summary_data.accumulated / summary_data.reports, std_dev: - std_dev(summary_data.accumulated, summary_data.accumulated_sqrd, summary_data.reports) + std_dev(summary_data.accumulated, summary_data.accumulated_sqrd, summary_data.reports), + p50: percentile(summary_data.t_digest, 0.5), + p75: percentile(summary_data.t_digest, 0.75), + p95: percentile(summary_data.t_digest, 0.95), + p99: percentile(summary_data.t_digest, 0.99) } end @@ -67,4 +83,7 @@ defmodule Mobius.Summary do ((sum_sqrd - sum * sum / n) / (n - 1)) |> :math.sqrt() end + + # Approximate percentiles using t-digest + defdelegate percentile(t, p), to: TDigest end diff --git a/mix.exs b/mix.exs index b203f78..0c8cff9 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,8 @@ defmodule Mobius.MixProject do {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, {:telemetry, "~> 0.4.3 or ~> 1.0"}, {:telemetry_metrics, "~> 0.6.0"}, - {:circular_buffer, "~> 0.4.0"} + {:circular_buffer, "~> 0.4.0"}, + {:t_digest, "~> 0.1.1"} ] end diff --git a/mix.lock b/mix.lock index 48ec6b3..c20a2fe 100644 --- a/mix.lock +++ b/mix.lock @@ -12,6 +12,8 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "shorter_maps": {:hex, :shorter_maps, "1.2.0", "c017bf9f9555f394942970d4c9bda4958c9c7acd326e28c7e535ac2f2e8ecd9c", [:mix], [], "hexpm", "ba43e31fd25745c6193ca8af7def203e6daebbc9c489d8399a5e40cc1f44f766"}, + "t_digest": {:hex, :t_digest, "0.1.1", "3e9d72eacb70ea270c6b09364497359ec54199fda144c9ada09b7931d8c40220", [:mix], [{:shorter_maps, "~> 1.0", [hex: :shorter_maps, repo: "hexpm", optional: false]}], "hexpm", "2f57ade419cec604f58dd8fab9ca195bad1ed65f985867c6ceab41b0c0a267b2"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, } diff --git a/test/mobius/summary_test.exs b/test/mobius/summary_test.exs index 01346fb..ce44598 100644 --- a/test/mobius/summary_test.exs +++ b/test/mobius/summary_test.exs @@ -9,7 +9,8 @@ defmodule Mobius.SummaryTest do accumulated: 100, accumulated_sqrd: 10000, min: 100, - max: 100 + max: 100, + t_digest: TDigest.new() |> TDigest.update(100) } assert expected_summary_data == Summary.new(100) @@ -21,19 +22,31 @@ defmodule Mobius.SummaryTest do accumulated: 100, accumulated_sqrd: 10000, min: 100, - max: 100 + max: 100, + t_digest: TDigest.new() |> TDigest.update(100) } assert expected_summary_data == Summary.new(100) end test "calculate summary from summary data" do - expected_summary = %{min: 100, max: 400, average: 250, std_dev: 212.13203435596427} + expected_summary = %{ + min: 10, + max: 750, + average: 382, + std_dev: 301.1016808691413, + p50: 350, + p75: 750, + p95: 750, + p99: 750 + } + + [first_value | tail_values] = [10, 10, 100, 200, 300, 400, 600, 700, 750, 750] summary_data = - 100 - |> Summary.new() - |> Summary.update(400) + for metric_value <- tail_values, reduce: Summary.new(first_value) do + acc -> Summary.update(acc, metric_value) + end assert expected_summary == Summary.calculate(summary_data) end