From b0fdb25f20b301f0dc96f9bbf0e63d5da5d7c486 Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 10 Apr 2024 09:18:37 -0600 Subject: [PATCH] feat: add telemetry metric for process memory usage (#43) --- lib/buffy/throttle.ex | 35 ++++++++++++++++++++++++- lib/buffy/throttle_and_timed.ex | 36 +++++++++++++++++++++++++- test/buffy/throttle_and_timed_test.exs | 14 ++++++++++ test/buffy/throttle_test.exs | 14 ++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/lib/buffy/throttle.ex b/lib/buffy/throttle.ex index a0a720e..885e785 100644 --- a/lib/buffy/throttle.ex +++ b/lib/buffy/throttle.ex @@ -90,6 +90,14 @@ defmodule Buffy.Throttle do - `:result` - The return value of the `handle_throttle/1` function. + ### Memory Leaks + + With any sort of debounce and Elixir processes, you need to be careful about handling too many processes, or having to much state in memory at the same time. If you handle large amounts of data there is a good chance you'll end up with high memory usage and possibly affect other parts of your system. + + To help monitor this usage, Buffy has a telemetry metric that measures the Elixir process memory usage. If you summarize this metric you should get a good view into your buffy throttle processes. + + summary("buffy.throttle.total_heap_size", tags: [:module]) + """ @typedoc """ @@ -216,7 +224,32 @@ defmodule Buffy.Throttle do @spec init(Buffy.Throttle.state()) :: {:ok, Buffy.Throttle.state()} def init({key, args}) do Process.send_after(self(), :timeout, unquote(throttle)) - {:ok, {key, args}} + {:ok, {key, args}, {:continue, :measure_memory}} + end + + @doc false + @impl GenServer + @spec handle_continue(:measure_memory, Buffy.Throttle.state()) :: {:noreply, Buffy.Throttle.state()} + def handle_continue(:measure_memory, {key, args} = state) do + case Process.info(self(), [:total_heap_size]) do + [{:total_heap_size, total_heap_size}] -> + :telemetry.execute( + [:buffy, :throttle], + %{ + total_heap_size: total_heap_size + }, + %{ + args: args, + key: key, + module: __MODULE__ + } + ) + + _ -> + nil + end + + {:noreply, state} end @doc false diff --git a/lib/buffy/throttle_and_timed.ex b/lib/buffy/throttle_and_timed.ex index b0f8a57..f249ba2 100644 --- a/lib/buffy/throttle_and_timed.ex +++ b/lib/buffy/throttle_and_timed.ex @@ -189,6 +189,14 @@ defmodule Buffy.ThrottleAndTimed do - `:result` - The return value of the `handle_throttle/1` function. + ### Memory Leaks + + With any sort of debounce and Elixir processes, you need to be careful about handling too many processes, or having to much state in memory at the same time. If you handle large amounts of data there is a good chance you'll end up with high memory usage and possibly affect other parts of your system. + + To help monitor this usage, Buffy has a telemetry metric that measures the Elixir process memory usage. If you summarize this metric you should get a good view into your buffy throttle processes. + + summary("buffy.throttle.total_heap_size", tags: [:module]) + """ require Logger @@ -339,7 +347,33 @@ defmodule Buffy.ThrottleAndTimed do @impl GenServer @spec init({Buffy.ThrottleAndTimed.key(), Buffy.ThrottleAndTimed.args()}) :: {:ok, Buffy.ThrottleAndTimed.state()} def init({key, args}) do - {:ok, schedule_throttle_and_update_state(%{key: key, args: args, timer_ref: nil})} + {:ok, schedule_throttle_and_update_state(%{key: key, args: args, timer_ref: nil}), {:continue, :measure_memory}} + end + + @doc false + @impl GenServer + @spec handle_continue(:measure_memory, Buffy.ThrottleAndTimed.state()) :: + {:noreply, Buffy.ThrottleAndTimed.state()} + def handle_continue(:measure_memory, state) do + case Process.info(self(), [:total_heap_size]) do + [{:total_heap_size, total_heap_size}] -> + :telemetry.execute( + [:buffy, :throttle], + %{ + total_heap_size: total_heap_size + }, + %{ + args: state.args, + key: state.key, + module: __MODULE__ + } + ) + + _ -> + nil + end + + {:noreply, state} end @doc """ diff --git a/test/buffy/throttle_and_timed_test.exs b/test/buffy/throttle_and_timed_test.exs index d5a7f63..51f3d9a 100644 --- a/test/buffy/throttle_and_timed_test.exs +++ b/test/buffy/throttle_and_timed_test.exs @@ -248,6 +248,7 @@ defmodule Buffy.ThrottleAndTimedTest do describe ":telemetry" do setup do :telemetry_test.attach_event_handlers(self(), [ + [:buffy, :throttle], [:buffy, :throttle, :throttle], [:buffy, :throttle, :timeout], [:buffy, :throttle, :handle, :start], @@ -260,6 +261,19 @@ defmodule Buffy.ThrottleAndTimedTest do :ok end + test "emits [:buffy, :throttle] total_heap_size" do + MyZeroThrottler.throttle(:foo) + + assert_receive {[:buffy, :throttle], _ref, %{total_heap_size: heap}, + %{ + args: :foo, + key: _, + module: MyZeroThrottler + }} + + assert heap > 0 + end + test "emits [:buffy, :throttle, :throttle]" do MyZeroThrottler.throttle(:foo) diff --git a/test/buffy/throttle_test.exs b/test/buffy/throttle_test.exs index 56ac9f9..cc46c17 100644 --- a/test/buffy/throttle_test.exs +++ b/test/buffy/throttle_test.exs @@ -31,6 +31,7 @@ defmodule Buffy.ThrottleTest do setup do _ref = :telemetry_test.attach_event_handlers(self(), [ + [:buffy, :throttle], [:buffy, :throttle, :throttle], [:buffy, :throttle, :handle, :jitter], [:buffy, :throttle, :handle, :start], @@ -41,6 +42,19 @@ defmodule Buffy.ThrottleTest do :ok end + test "emits [:buffy, :throttle] total_heap_size" do + MyZeroThrottler.throttle(:foo) + + assert_receive {[:buffy, :throttle], _ref, %{total_heap_size: heap}, + %{ + args: :foo, + key: _, + module: MyZeroThrottler + }} + + assert heap > 0 + end + test "emits [:buffy, :throttle, :throttle]" do MyZeroThrottler.throttle(:foo)