From 9f95ba8f6502d442e89f96b856ebf77fecda9ef4 Mon Sep 17 00:00:00 2001 From: Jordan Bracco Date: Thu, 14 May 2020 22:14:09 +0200 Subject: Rename to ConcurrentLimiter --- README.md | 4 +- lib/concurrent_limiter.ex | 152 +++++++++++++++++++++++++++++++++ lib/limiter.ex | 150 -------------------------------- mix.exs | 4 +- test/concurrent_limiter_test.exs | 42 +++++++++ test/limiter_test.exs | 42 --------- test/samples/limiter.exs | 24 +++--- test/samples/multi_limiter.exs | 88 +++++++++++-------- test/samples/results_multi_limiter.txt | 60 ++++++------- test/samples/update_counter.exs | 27 +++--- 10 files changed, 307 insertions(+), 286 deletions(-) create mode 100644 lib/concurrent_limiter.ex delete mode 100644 lib/limiter.ex create mode 100644 test/concurrent_limiter_test.exs delete mode 100644 test/limiter_test.exs diff --git a/README.md b/README.md index 2e214c3..5886716 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# Limiter +# Concurrent Limiter -See the docs in `lib/limiter.ex`. +See the docs in `lib/concurrent_limiter.ex`. diff --git a/lib/concurrent_limiter.ex b/lib/concurrent_limiter.ex new file mode 100644 index 0000000..9776377 --- /dev/null +++ b/lib/concurrent_limiter.ex @@ -0,0 +1,152 @@ +defmodule ConcurrentLimiter do + require Logger + + @moduledoc """ + # Concurrent Limiter + + A concurrency limiter. Limits the number of concurrent invocations possible, without using a worker pool or different processes. + + It can be useful in cases where you don't need a worker pool but still being able to limit concurrent calls without much overhead. As it internally uses `persistent_term` to store metadata, and can fallback to ETS tables, it is however not made for a large number of limiters and cannot be used for things like a per-user rate limiter. + + It supports two storage methods: + + * **[atomics](https://erlang.org/doc/man/atomics.html)** recommended and default if your OTP is > 21.2. + * **[ets](https://erlang.org/doc/man/ets.html)** either with a single table per limiter (faster) or a shared table. + + You would almost always want to use atomics, ets is mostly there for backwards compatibility. + """ + + @doc """ + Initializes a `ConcurrentLimiter`. + """ + + @spec new(name, max_running, max_waiting, options) :: :ok | {:error, :existing} when name: atom(), + max_running: non_neg_integer(), + max_waiting: non_neg_integer() | :infinity, + options: [option], + option: {:wait, non_neg_integer()} | backend, + backend: :atomics | ets_backend, + ets_backend: :ets | {:ets, atom()} | {:ets, atom(), ets_options :: []} + def new(name, max_running, max_waiting, options \\ []) do + name = prefix_name(name) + if defined?(name) do + {:error, :existing} + else + wait = Keyword.get(options, :wait, 150) + backend = Keyword.get(options, :backend, default_backend()) + {:ok, backend} = setup_backend(backend) + :persistent_term.put(name, {__MODULE__, max_running, max_waiting, backend, wait}) + :ok + end + end + + @spec set(name, new_max_running, new_max_waiting, options) :: :ok | :error when name: atom(), + new_max_running: non_neg_integer(), + new_max_waiting: non_neg_integer() | :infinity, + options: [option], + option: {:wait, non_neg_integer()} + @doc "Adjust the limiter limits at runtime" + def set(name, new_max_running, new_max_waiting, options \\ []) do + name = prefix_name(name) + if defined?(name) do + new_wait = Keyword.get(options, :wait) + {__MODULE__, max_running, max_waiting, backend, wait} = :persistent_term.get(name) + new = {__MODULE__, new_max_running || max_running, new_max_waiting || max_waiting, backend, new_wait || wait} + :persistent_term.put(name, new) + :ok + else + :error + end + end + + @spec limit(atom(), function()) :: {:error, :overload} | any() + @doc "Limits invocation of `fun`." + def limit(name, fun) do + do_limit(prefix_name(name), fun) + end + + defp do_limit(name, fun) do + {__MODULE__, max_running, max_waiting, backend, wait} = :persistent_term.get(name) + max = max_running + max_waiting + counter = inc(backend, name) + + cond do + counter <= max_running -> + try do + fun.() + after + dec(backend, name) + end + + counter > max -> + dec(backend, name) + {:error, :overload} + + counter > max_running -> + wait(backend, name, wait, fun) + end + end + + defp wait(backend, name, wait, fun) do + Process.sleep(wait) + dec(backend, name) + do_limit(name, fun) + end + + defp inc({:ets, ets}, name) do + :ets.update_counter(ets, name, {2, 1}, {name, 0}) + end + + defp inc({:atomics, ref}, _) do + :atomics.add_get(ref, 1, 1) + end + + defp dec({:ets, ets}, name) do + :ets.update_counter(ets, name, {2, -1}, {name, 0}) + end + + defp dec({:atomics, ref}, _) do + :atomics.sub_get(ref, 1, 1) + end + + defp prefix_name(suffix), do: Module.concat(__MODULE__, suffix) + + defp defined?(name) do + {__MODULE__, _, _, _, _, _} = :persistent_term.get(name) + true + rescue + _ -> false + end + + defp default_backend() do + if Code.ensure_loaded?(:atomics) do + :atomics + else + Logger.debug("ConcurrentLimiter: atomics not available, using ETS backend") + :ets + end + end + + defp setup_backend(:ets) do + setup_backend({:ets, ETS}) + end + + defp setup_backend({:ets, name}) do + setup_backend({:ets, name, [{:write_concurrency, true}, {:read_concurrency, true}]}) + end + + defp setup_backend({:ets, name, options}) do + ets_name = prefix_name(name) + + case :ets.whereis(ets_name) do + :undefined -> :ets.new(ets_name, [:public, :named_table] ++ options) + _ -> nil + end + {:ok, {:ets, ets_name}} + end + + defp setup_backend(:atomics) do + {:ok, {:atomics, :atomics.new(1, [signed: true])}} + end + +end diff --git a/lib/limiter.ex b/lib/limiter.ex deleted file mode 100644 index f8789a4..0000000 --- a/lib/limiter.ex +++ /dev/null @@ -1,150 +0,0 @@ -defmodule Limiter do - require Logger - - @moduledoc """ - # Limiter - - A concurrency limiter. Limits the number of concurrent invocations possible, without using a worker pool or different processes. - - It supports two storage methods: - - * **[atomics](https://erlang.org/doc/man/atomics.html)** recommended and default if your OTP is > 21.2. - * **[ets](https://erlang.org/doc/man/ets.html)** either with a single table per Limiter (faster) or a shared table. - - You would however always want to use atomics, ets is mostly there for backwards compatibility. - """ - - @doc """ - Initializes a `Limiter`. - """ - - @spec new(name, max_running, max_waiting, options) :: :ok | {:error, :existing} when name: atom(), - max_running: non_neg_integer(), - max_waiting: non_neg_integer() | :infinity, - options: [option], - option: {:wait, non_neg_integer()} | backend, - backend: :atomics | ets_backend, - ets_backend: :ets | {:ets, atom()} | {:ets, ets_name :: atom(), ets_options :: []} - def new(name, max_running, max_waiting, options \\ []) do - name = atom_name(name) - if defined?(name) do - {:error, :existing} - else - wait = Keyword.get(options, :wait, 150) - backend = Keyword.get(options, :backend, default_backend()) - {:ok, backend} = setup_backend(backend) - :persistent_term.put(name, {__MODULE__, max_running, max_waiting, backend, wait}) - :ok - end - end - - @spec set(name, new_max_running, new_max_waiting, options) :: :ok | :error when name: atom(), - new_max_running: non_neg_integer(), - new_max_waiting: non_neg_integer() | :infinity, - options: [option], - option: {:wait, non_neg_integer()} - @doc "Adjust the limiter limits at runtime" - def set(name, new_max_running, new_max_waiting, options \\ []) do - name = atom_name(name) - if defined?(name) do - new_wait = Keyword.get(options, :wait) - {__MODULE__, max_running, max_waiting, backend, wait} = :persistent_term.get(name) - new = {__MODULE__, new_max_running || max_running, new_max_waiting || max_waiting, backend, new_wait || wait} - :persistent_term.put(name, new) - :ok - else - :error - end - end - - @spec limit(atom(), function()) :: {:error, :overload} | any() - @doc "Limits invocation of `fun`." - def limit(name, fun) do - do_limit(atom_name(name), fun) - end - - defp do_limit(name, fun) do - {__MODULE__, max_running, max_waiting, backend, wait} = :persistent_term.get(name) - max = max_running + max_waiting - counter = inc(backend, name) - - cond do - counter <= max_running -> - try do - fun.() - after - dec(backend, name) - end - - counter > max -> - dec(backend, name) - {:error, :overload} - - counter > max_running -> - wait(backend, name, wait, fun) - end - end - - defp wait(backend, name, wait, fun) do - Process.sleep(wait) - dec(backend, name) - do_limit(name, fun) - end - - defp inc({:ets, ets}, name) do - :ets.update_counter(ets, name, {2, 1}, {name, 0}) - end - - defp inc({:atomics, ref}, _) do - :atomics.add_get(ref, 1, 1) - end - - defp dec({:ets, ets}, name) do - :ets.update_counter(ets, name, {2, -1}, {name, 0}) - end - - defp dec({:atomics, ref}, _) do - :atomics.sub_get(ref, 1, 1) - end - - defp atom_name(suffix), do: Module.concat(__MODULE__, suffix) - - defp defined?(name) do - {__MODULE__, _, _, _, _, _} = :persistent_term.get(name) - true - rescue - _ -> false - end - - defp default_backend() do - if Code.ensure_loaded?(:atomics) do - :atomics - else - Logger.debug("Limiter: atomics not available, using ETS backend") - :ets - end - end - - defp setup_backend(:ets) do - setup_backend({:ets, ETS}) - end - - defp setup_backend({:ets, name}) do - setup_backend({:ets, name, [{:write_concurrency, true}, {:read_concurrency, true}]}) - end - - defp setup_backend({:ets, name, options}) do - ets_name = atom_name(name) - - case :ets.whereis(ets_name) do - :undefined -> :ets.new(ets_name, [:public, :named_table] ++ options) - _ -> nil - end - {:ok, {:ets, ets_name}} - end - - defp setup_backend(:atomics) do - {:ok, {:atomics, :atomics.new(1, [signed: true])}} - end - -end diff --git a/mix.exs b/mix.exs index 0ff4802..49378b6 100644 --- a/mix.exs +++ b/mix.exs @@ -1,9 +1,9 @@ -defmodule Limiter.MixProject do +defmodule ConcurrentLimiter.MixProject do use Mix.Project def project do [ - app: :limiter, + app: :concurrent_limiter, version: "0.1.0", elixir: "~> 1.10", start_permanent: Mix.env() == :prod, diff --git a/test/concurrent_limiter_test.exs b/test/concurrent_limiter_test.exs new file mode 100644 index 0000000..e1d281e --- /dev/null +++ b/test/concurrent_limiter_test.exs @@ -0,0 +1,42 @@ +defmodule ConcurrentLimiterTest do + use ExUnit.Case + doctest ConcurrentLimiter + + test "limiter ets is atomic" do + name = "test1" + ConcurrentLimiter.new(name, 2, 2) + atomic_test(name) + end + + test "limiter atomics is atomic" do + name = "test2" + ConcurrentLimiter.new(name, 2, 2, backend: :atomics) + atomic_test(name) + end + + defp atomic_test(name) do + self = self() + + sleepy = fn sleep -> + case ConcurrentLimiter.limit(name, fn -> + send(self, :ok) + Process.sleep(sleep) + :ok + end) do + :ok -> :ok + other -> send(self, other) + end + end + + spawn_link(fn -> sleepy.(500) end) + spawn_link(fn -> sleepy.(500) end) + spawn_link(fn -> sleepy.(500) end) + spawn_link(fn -> sleepy.(500) end) + spawn_link(fn -> sleepy.(500) end) + assert_receive :ok, 2000 + assert_receive :ok, 2000 + assert_receive {:error, :overload}, 2000 + assert_receive :ok, 2000 + assert_receive :ok, 2000 + end +end diff --git a/test/limiter_test.exs b/test/limiter_test.exs deleted file mode 100644 index 1b81ead..0000000 --- a/test/limiter_test.exs +++ /dev/null @@ -1,42 +0,0 @@ -defmodule LimiterTest do - use ExUnit.Case - doctest Limiter - - test "limiter ets is atomic" do - name = "test1" - Limiter.new(name, 2, 2) - atomic_test(name) - end - - test "limiter atomics is atomic" do - name = "test2" - Limiter.new(name, 2, 2, backend: :atomics) - atomic_test(name) - end - - defp atomic_test(name) do - self = self() - - sleepy = fn sleep -> - case Limiter.limit(name, fn -> - send(self, :ok) - Process.sleep(sleep) - :ok - end) do - :ok -> :ok - other -> send(self, other) - end - end - - spawn_link(fn -> sleepy.(500) end) - spawn_link(fn -> sleepy.(500) end) - spawn_link(fn -> sleepy.(500) end) - spawn_link(fn -> sleepy.(500) end) - spawn_link(fn -> sleepy.(500) end) - assert_receive :ok, 2000 - assert_receive :ok, 2000 - assert_receive {:error, :overload}, 2000 - assert_receive :ok, 2000 - assert_receive :ok, 2000 - end -end diff --git a/test/samples/limiter.exs b/test/samples/limiter.exs index 785c85f..f903658 100644 --- a/test/samples/limiter.exs +++ b/test/samples/limiter.exs @@ -1,24 +1,24 @@ infinite = 1_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000 -Limiter.new(:bench, infinite, 0) -Limiter.new(:bench_s, infinite, 0, ets: LimiterTest) +ConcurrentLimiter.new(:bench, infinite, 0) +ConcurrentLimiter.new(:bench_s, infinite, 0, ets: ConcurrentLimiterTest) concurrent = [{:read_concurrency, true}, {:write_concurrency, true}] -Limiter.new(:bench_rw, infinite, 0) -Limiter.new(:bench_s_rw, infinite, 0, ets: LimiterTest, ets_opts: concurrent) +ConcurrentLimiter.new(:bench_rw, infinite, 0) +ConcurrentLimiter.new(:bench_s_rw, infinite, 0, ets: ConcurrentLimiterTest, ets_opts: concurrent) single = %{ - "Limiter.limit/2" => fn -> - Limiter.limit(:bench, fn -> :ok end) + "ConcurrentLimiter.limit/2" => fn -> + ConcurrentLimiter.limit(:bench, fn -> :ok end) end, - "Limiter.limit/2 with concurrency" => fn -> - Limiter.limit(:bench_rw, fn -> :ok end) + "ConcurrentLimiter.limit/2 with concurrency" => fn -> + ConcurrentLimiter.limit(:bench_rw, fn -> :ok end) end, - "Limiter:limit/2 with shared ets" => fn -> - Limiter.limit(:bench_s, fn -> :ok end) + "ConcurrentLimiter:limit/2 with shared ets" => fn -> + ConcurrentLimiter.limit(:bench_s, fn -> :ok end) end, - "Limiter:limit/2 with shared ets and concurrency" => fn -> - Limiter.limit(:bench_s_rw, fn -> :ok end) + "ConcurrentLimiter:limit/2 with shared ets and concurrency" => fn -> + ConcurrentLimiter.limit(:bench_s_rw, fn -> :ok end) end } diff --git a/test/samples/multi_limiter.exs b/test/samples/multi_limiter.exs index abec65d..f56cb6c 100644 --- a/test/samples/multi_limiter.exs +++ b/test/samples/multi_limiter.exs @@ -1,52 +1,70 @@ infinite = 1_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000 +parallel = case Integer.parse(System.get_env("PARALLEL", "")) do + {int, _} -> int + _ -> System.schedulers_online()/2 +end -Limiter.new(:bench_u_0, infinite, 0, backend: {:ets, LimiterTest0, []}) -Limiter.new(:bench_u_1, infinite, 0, backend: {:ets, LimiterTest1, []}) -Limiter.new(:bench_u_2, infinite, 0, backend: {:ets, LimiterTest2, []}) -Limiter.new(:bench_u_3, infinite, 0, backend: {:ets, LimiterTest3, []}) +multi_count = case Integer.parse(System.get_env("MULTI", "")) do + {int, _} -> int + _ -> parallel +end -Limiter.new(:bench_a_0, infinite, 0, backend: :atomics) -Limiter.new(:bench_a_1, infinite, 0, backend: :atomics) -Limiter.new(:bench_a_2, infinite, 0, backend: :atomics) -Limiter.new(:bench_a_3, infinite, 0, backend: :atomics) +names = fn(prefix) -> + for i <- 1..multi_count do + Module.concat(MultiConcurrentLimiterBenchmark, "#{prefix}#{i}") + end +end + + +bench_unique = for name <- names.("u") do + ConcurrentLimiter.new(name, infinite, 0, backend: {:ets, name, []}) + name +end + +IO.inspect(bench_unique) + +bench_atomics = for name <- names.("a") do + ConcurrentLimiter.new(name, infinite, 0, backend: :atomics) + name +end -Limiter.new(:bench_s_0, infinite, 0, backend: {:ets, LimiterTest, []}) -Limiter.new(:bench_s_1, infinite, 0, backend: {:ets, LimiterTest, []}) -Limiter.new(:bench_s_2, infinite, 0, backend: {:ets, LimiterTest, []}) -Limiter.new(:bench_s_3, infinite, 0, backend: {:ets, LimiterTest, []}) +bench_shared = for name <- names.("s") do + ConcurrentLimiter.new(name, infinite, 0, backend: {:ets, ConcurrentLimiterTest, []}) + name +end rw = [{:read_concurrency, true}, {:write_concurrency, true}] -Limiter.new(:bench_u_rw0, infinite, 0, backend: {:ets, LimiterTestRW0, rw}) -Limiter.new(:bench_u_rw1, infinite, 0, backend: {:ets, LimiterTestRW1, rw}) -Limiter.new(:bench_u_rw2, infinite, 0, backend: {:ets, LimiterTestRW2, rw}) -Limiter.new(:bench_u_rw3, infinite, 0, backend: {:ets, LimiterTestRW3, rw}) +bench_unique_rw = for name <- names.("u_rw") do + ConcurrentLimiter.new(name, infinite, 0, backend: {:ets, name, rw}) + name +end -Limiter.new(:bench_s_rw0, infinite, 0, backend: {:ets, LimiterTestRW, rw}) -Limiter.new(:bench_s_rw1, infinite, 0, backend: {:ets, LimiterTestRW, rw}) -Limiter.new(:bench_s_rw2, infinite, 0, backend: {:ets, LimiterTestRW, rw}) -Limiter.new(:bench_s_rw3, infinite, 0, backend: {:ets, LimiterTestRW, rw}) +bench_shared_rw = for name <- names.("s_rw") do + ConcurrentLimiter.new(name, infinite, 0, backend: {:ets, ConcurrentLimiterTestRW, rw}) + name +end multiple = %{ - "Limiter.limit/2 unique ets" => fn -> - limiter = Enum.random([:bench_u_0, :bench_u_1, :bench_u_2, :bench_u_3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter.limit/2 unique ets" => fn -> + limiter = Enum.random(bench_unique) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end, - "Limiter:limit/2 shared ets" => fn -> - limiter = Enum.random([:bench_s_0, :bench_s_1, :bench_s_2, :bench_s_3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter:limit/2 shared ets" => fn -> + limiter = Enum.random(bench_shared) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end, - "Limiter.limit/2 unique ets, concurrency" => fn -> - limiter = Enum.random([:bench_u_rw0, :bench_u_rw1, :bench_u_rw2, :bench_u_rw3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter.limit/2 unique ets, concurrency" => fn -> + limiter = Enum.random(bench_unique_rw) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end, - "Limiter:limit/2 shared ets, concurrency" => fn -> - limiter = Enum.random([:bench_s_rw0, :bench_s_rw1, :bench_s_rw2, :bench_s_rw3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter:limit/2 shared ets, concurrency" => fn -> + limiter = Enum.random(bench_shared_rw) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end, - "Limiter:limit/2 atomics" => fn -> - limiter = Enum.random([:bench_a_0, :bench_a_1, :bench_a_2, :bench_a_3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter:limit/2 atomics" => fn -> + limiter = Enum.random(bench_atomics) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end } diff --git a/test/samples/results_multi_limiter.txt b/test/samples/results_multi_limiter.txt index 112c325..75704da 100644 --- a/test/samples/results_multi_limiter.txt +++ b/test/samples/results_multi_limiter.txt @@ -22,25 +22,25 @@ parallel: 1 inputs: none specified Estimated total run time: 35 s -Benchmarking Limiter.limit/2 unique ets... -Benchmarking Limiter.limit/2 unique ets, concurrency... -Benchmarking Limiter:limit/2 atomics... -Benchmarking Limiter:limit/2 shared ets... -Benchmarking Limiter:limit/2 shared ets, concurrency... +Benchmarking ConcurrentLimiter.limit/2 unique ets... +Benchmarking ConcurrentLimiter.limit/2 unique ets, concurrency... +Benchmarking ConcurrentLimiter:limit/2 atomics... +Benchmarking ConcurrentLimiter:limit/2 shared ets... +Benchmarking ConcurrentLimiter:limit/2 shared ets, concurrency... Name ips average deviation median 99th % -Limiter:limit/2 atomics 491.88 K 2.03 μs ±1506.30% 1.55 μs 3.48 μs -Limiter.limit/2 unique ets 414.63 K 2.41 μs ±1169.34% 1.97 μs 4.53 μs -Limiter:limit/2 shared ets 411.43 K 2.43 μs ±1286.95% 1.96 μs 3.66 μs -Limiter.limit/2 unique ets, concurrency 406.50 K 2.46 μs ±1006.31% 2.06 μs 4.34 μs -Limiter:limit/2 shared ets, concurrency 384.04 K 2.60 μs ±1293.25% 2.12 μs 4.37 μs +ConcurrentLimiter:limit/2 atomics 491.88 K 2.03 μs ±1506.30% 1.55 μs 3.48 μs +ConcurrentLimiter.limit/2 unique ets 414.63 K 2.41 μs ±1169.34% 1.97 μs 4.53 μs +ConcurrentLimiter:limit/2 shared ets 411.43 K 2.43 μs ±1286.95% 1.96 μs 3.66 μs +ConcurrentLimiter.limit/2 unique ets, concurrency 406.50 K 2.46 μs ±1006.31% 2.06 μs 4.34 μs +ConcurrentLimiter:limit/2 shared ets, concurrency 384.04 K 2.60 μs ±1293.25% 2.12 μs 4.37 μs Comparison: -Limiter:limit/2 atomics 491.88 K -Limiter.limit/2 unique ets 414.63 K - 1.19x slower +0.38 μs -Limiter:limit/2 shared ets 411.43 K - 1.20x slower +0.40 μs -Limiter.limit/2 unique ets, concurrency 406.50 K - 1.21x slower +0.43 μs -Limiter:limit/2 shared ets, concurrency 384.04 K - 1.28x slower +0.57 μs +ConcurrentLimiter:limit/2 atomics 491.88 K +ConcurrentLimiter.limit/2 unique ets 414.63 K - 1.19x slower +0.38 μs +ConcurrentLimiter:limit/2 shared ets 411.43 K - 1.20x slower +0.40 μs +ConcurrentLimiter.limit/2 unique ets, concurrency 406.50 K - 1.21x slower +0.43 μs +ConcurrentLimiter:limit/2 shared ets, concurrency 384.04 K - 1.28x slower +0.57 μs @@ -65,22 +65,22 @@ parallel: 8 inputs: none specified Estimated total run time: 35 s -Benchmarking Limiter.limit/2 unique ets... -Benchmarking Limiter.limit/2 unique ets, concurrency... -Benchmarking Limiter:limit/2 atomics... -Benchmarking Limiter:limit/2 shared ets... -Benchmarking Limiter:limit/2 shared ets, concurrency... +Benchmarking ConcurrentLimiter.limit/2 unique ets... +Benchmarking ConcurrentLimiter.limit/2 unique ets, concurrency... +Benchmarking ConcurrentLimiter:limit/2 atomics... +Benchmarking ConcurrentLimiter:limit/2 shared ets... +Benchmarking ConcurrentLimiter:limit/2 shared ets, concurrency... Name ips average deviation median 99th % -Limiter:limit/2 atomics 307.84 K 3.25 μs ±1113.62% 2.09 μs 10.24 μs -Limiter.limit/2 unique ets, concurrency 95.56 K 10.46 μs ±391.37% 2.93 μs 163.02 μs -Limiter:limit/2 shared ets, concurrency 92.39 K 10.82 μs ±374.36% 2.92 μs 158.97 μs -Limiter.limit/2 unique ets 80.68 K 12.39 μs ±362.74% 2.85 μs 160.66 μs -Limiter:limit/2 shared ets 6.04 K 165.66 μs ±17.23% 167.48 μs 237.96 μs +ConcurrentLimiter:limit/2 atomics 307.84 K 3.25 μs ±1113.62% 2.09 μs 10.24 μs +ConcurrentLimiter.limit/2 unique ets, concurrency 95.56 K 10.46 μs ±391.37% 2.93 μs 163.02 μs +ConcurrentLimiter:limit/2 shared ets, concurrency 92.39 K 10.82 μs ±374.36% 2.92 μs 158.97 μs +ConcurrentLimiter.limit/2 unique ets 80.68 K 12.39 μs ±362.74% 2.85 μs 160.66 μs +ConcurrentLimiter:limit/2 shared ets 6.04 K 165.66 μs ±17.23% 167.48 μs 237.96 μs Comparison: -Limiter:limit/2 atomics 307.84 K -Limiter.limit/2 unique ets, concurrency 95.56 K - 3.22x slower +7.22 μs -Limiter:limit/2 shared ets, concurrency 92.39 K - 3.33x slower +7.57 μs -Limiter.limit/2 unique ets 80.68 K - 3.82x slower +9.15 μs -Limiter:limit/2 shared ets 6.04 K - 51.00x slower +162.41 μs +ConcurrentLimiter:limit/2 atomics 307.84 K +ConcurrentLimiter.limit/2 unique ets, concurrency 95.56 K - 3.22x slower +7.22 μs +ConcurrentLimiter:limit/2 shared ets, concurrency 92.39 K - 3.33x slower +7.57 μs +ConcurrentLimiter.limit/2 unique ets 80.68 K - 3.82x slower +9.15 μs +ConcurrentLimiter:limit/2 shared ets 6.04 K - 51.00x slower +162.41 μs diff --git a/test/samples/update_counter.exs b/test/samples/update_counter.exs index 1768735..6310c57 100644 --- a/test/samples/update_counter.exs +++ b/test/samples/update_counter.exs @@ -1,19 +1,20 @@ :ets.new(:limiter_bench, [:public, :named_table]) +:ets.new(:limiter_bench_concurrent, [:public, :named_table, {:read_concurrency, false}, {:write_concurrency, true}]) +atomics = :atomics.new(1, []) -Benchee.run( +update_counter = %{ "ets:update_counter" => fn -> :ets.update_counter(:limiter_bench, "bench", {2, 1}, {"bench", 0}) - end - }, - parallel: 1 -) - -Benchee.run( - %{ - "ets:update_counter" => fn -> + end, + "ets:update_counter concurrent" => fn -> :ets.update_counter(:limiter_bench, "bench", {2, 1}, {"bench", 0}) - end - }, - parallel: System.schedulers_online() -) + end, + "atomics:add_get" => fn -> + :atomics.add_get(atomics, 1, 1) + end, + } + +Benchee.run(update_counter, parallel: 1) +Benchee.run(update_counter, parallel: System.schedulers_online()) + -- cgit v1.2.3