├── config ├── dev.exs ├── prod.exs ├── config.exs └── test.exs ├── lib ├── instruments │ ├── statix.ex │ └── application.ex ├── probe │ ├── value.ex │ ├── errors.ex │ ├── supervisor.ex │ ├── function.ex │ ├── definitions.ex │ └── runner.ex ├── sysmon │ ├── receiver.ex │ ├── supervisor.ex │ ├── receiver │ │ ├── metrics.ex │ │ └── log.ex │ ├── emitter.ex │ └── reporter.ex ├── stats_reporter │ ├── null.ex │ └── logger.ex ├── stats_reporter.ex ├── custom_functions.ex ├── probe.ex ├── macro_helpers.ex ├── probes │ └── schedulers.ex ├── fast_gauge.ex ├── fast_counter.ex ├── rate_tracker.ex └── instruments.ex ├── .gitignore ├── bench ├── fast_counter │ ├── increment.exs │ └── tag_handling.exs ├── fast_gauge │ └── gauge.exs ├── rate_tracker │ └── track.exs └── results │ ├── fast_counter │ ├── increment │ │ ├── strategy-1.txt │ │ ├── strategy-3.txt │ │ └── strategy-2.txt │ └── tag_handling │ │ ├── analysis.txt │ │ ├── run-2.txt │ │ ├── run-1.txt │ │ └── run-3.txt │ ├── rate_tracker │ └── analysis.txt │ └── fast_gauge │ └── analysis.txt ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── pages ├── Performance.md ├── Overview.md ├── Probes.md └── Configuration.md ├── test ├── rate_tracker_test.exs ├── macro_helpers.test.exs ├── sysmon │ ├── reporter_test.exs │ └── emitter_test.exs ├── test_helper.exs ├── support │ └── fake_statsd.ex ├── custom_functions_test.exs ├── instruments_test.exs └── probe │ └── probe_test.exs ├── mix.exs ├── mix.lock └── README.md /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | import_config "#{Mix.env()}.exs" 4 | -------------------------------------------------------------------------------- /lib/instruments/statix.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Statix do 2 | @moduledoc """ 3 | The default stats reporter. Uses the `Statix` library. 4 | """ 5 | use Statix, runtime_config: true 6 | end 7 | -------------------------------------------------------------------------------- /lib/probe/value.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Probe.Value do 2 | @moduledoc false 3 | defstruct value: nil, sample_rate: nil, tags: [] 4 | 5 | def new(value, opts) do 6 | tags = Keyword.get(opts, :tags) 7 | sample_rate = Keyword.get(opts, :sample_rate) 8 | 9 | %__MODULE__{value: value, tags: tags, sample_rate: sample_rate} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/probe/errors.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Probe.Errors do 2 | @moduledoc false 3 | 4 | defmodule ProbeNameTakenError do 5 | defexception taken_names: [] 6 | 7 | def message(%{taken_names: names}) do 8 | formatted_names = Enum.map_join(names, ", ", fn name -> "\"#{name}\"" end) 9 | "You're re-registering the following probes: #{formatted_names}" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/probe/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Probe.Supervisor do 2 | @moduledoc false 3 | use DynamicSupervisor 4 | 5 | def start_link(_ \\ []) do 6 | DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) 7 | end 8 | 9 | def init([]) do 10 | DynamicSupervisor.init(strategy: :one_for_one) 11 | end 12 | 13 | def start_probe(name, type, options, probe_module) do 14 | DynamicSupervisor.start_child(__MODULE__, {Instruments.Probe.Runner, {name, type, options, probe_module}}) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez -------------------------------------------------------------------------------- /lib/sysmon/receiver.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Sysmon.Receiver do 2 | @moduledoc """ 3 | The Receiver behavior defines callbacks that are invoked by the Emitter in 4 | response to system monitor events. 5 | """ 6 | 7 | @type info :: List.t({term(), term()}) 8 | 9 | @callback handle_busy_port(pid(), port()) :: :ok 10 | 11 | @callback handle_busy_dist_port(pid(), port()) :: :ok 12 | 13 | @callback handle_long_gc(pid(), info()) :: :ok 14 | 15 | @callback handle_long_message_queue(pid(), boolean()) :: :ok 16 | 17 | @callback handle_long_schedule(pid(), info()) :: :ok 18 | 19 | @callback handle_large_heap(pid(), info()) :: :ok 20 | end 21 | -------------------------------------------------------------------------------- /lib/probe/function.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Probe.Function do 2 | @moduledoc false 3 | @behaviour Instruments.Probe 4 | 5 | def probe_init(_name, _probe_type, options) do 6 | probe_fn = Keyword.fetch!(options, :function) 7 | 8 | {:ok, {probe_fn, nil}} 9 | end 10 | 11 | def probe_get_value({_, last_value}) do 12 | {:ok, last_value} 13 | end 14 | 15 | def probe_reset({probe_fn, _}) do 16 | {:ok, {probe_fn, nil}} 17 | end 18 | 19 | def probe_sample({probe_fn, _}) do 20 | probe_value = 21 | case probe_fn.() do 22 | {:ok, result} -> result 23 | other -> other 24 | end 25 | 26 | {:ok, {probe_fn, probe_value}} 27 | end 28 | 29 | def probe_handle_message(_, state), do: {:ok, state} 30 | end 31 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # setting the statsd port to something other than the default 4 | # in test so we don't conflict in the build env. 5 | config :instruments, 6 | statsd_port: 15310, 7 | fast_counter_report_interval: 10, 8 | fast_counter_report_jitter_range: 0..0, 9 | fast_gauge_report_interval: 10, 10 | fast_gauge_report_jitter_range: 0..0, 11 | rate_tracker_report_interval: 10, 12 | rate_tracker_report_jitter_range: 0..0, 13 | reporter_module: Instruments.Statix 14 | 15 | config :logger, 16 | compile_time_purge_matching: [ 17 | [level_lower_than: :error] 18 | ] 19 | 20 | config :statix, port: 15310 21 | 22 | config :instruments, 23 | # Don't start the system monitor by default, the tests will start it 24 | enable_sysmon: false 25 | -------------------------------------------------------------------------------- /bench/fast_counter/increment.exs: -------------------------------------------------------------------------------- 1 | Application.ensure_all_started(:instruments) 2 | 3 | Benchee.run( 4 | %{ 5 | "increment" => fn options -> 6 | for _ <- 1..100 do 7 | Instruments.FastCounter.increment("test.counter", 1, options) 8 | end 9 | end 10 | }, 11 | inputs: %{ 12 | "1. No Options" => [], 13 | "2. No Tags" => [sample_rate: 1.0], 14 | "3. Empty Tags" => [sample_rate: 1.0, tags: []], 15 | "4. One Tag" => [sample_rate: 1.0, tags: ["test:tag"]], 16 | "5. Five Tags" => [sample_rate: 1.0, tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag"]], 17 | "6. Ten Tags" => [sample_rate: 1.0, tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag", "test-6:tag", "test-7:tag", "test-8:tag", "test-9:tag", "test-10:tag"]] 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /lib/stats_reporter/null.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.StatsReporter.Null do 2 | @moduledoc """ 3 | A StatsReporter module that throws out all logging messages. 4 | """ 5 | 6 | @behaviour Instruments.StatsReporter 7 | 8 | @doc false 9 | def connect(), do: :ok 10 | 11 | @doc false 12 | def increment(_key, _value \\ 1, _options \\ []), do: :ok 13 | 14 | @doc false 15 | def decrement(_key, _value \\ 1, _options \\ []), do: :ok 16 | 17 | @doc false 18 | def gauge(_key, _value, _options \\ []), do: :ok 19 | 20 | @doc false 21 | def histogram(_key, _value, _options \\ []), do: :ok 22 | 23 | @doc false 24 | def timing(_key, _value, _options \\ []), do: :ok 25 | 26 | @doc false 27 | def measure(_key, _options \\ [], fun), do: fun.() 28 | 29 | @doc false 30 | def set(_key, _value, _options \\ []), do: :ok 31 | end 32 | -------------------------------------------------------------------------------- /lib/instruments/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | alias Instruments.{ 7 | FastCounter, 8 | FastGauge, 9 | Probe, 10 | RateTracker 11 | } 12 | 13 | def start(_type, _args) do 14 | reporter = Application.get_env(:instruments, :reporter_module, Instruments.Statix) 15 | reporter.connect() 16 | 17 | children = [ 18 | FastCounter, 19 | FastGauge, 20 | Probe.Definitions, 21 | Probe.Supervisor, 22 | RateTracker 23 | ] 24 | 25 | children = 26 | if Application.get_env(:instruments, :enable_sysmon, false) do 27 | [Instruments.Sysmon.Supervisor | children] 28 | else 29 | children 30 | end 31 | 32 | Supervisor.start_link(children, strategy: :one_for_one, name: Instruments.Supervisor) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/sysmon/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Sysmon.Supervisor do 2 | @moduledoc """ 3 | The system monitor is broken into three concepts: the Reporter, the Emitter, 4 | and the Receiver. 5 | 6 | The Reporter subscribes to `:erlang.system_monitor` and will forward system 7 | monitor events it receives to subscribers. 8 | 9 | The Emitter is responsible for receiving events from the Reporter and invoking 10 | the appropriate handler on the Receiver. 11 | 12 | Since only one process can subscribe to system monitor events, this is opt-in 13 | and must be enabled by setting `:enable_sysmon` to `true` in the 14 | `:instruments` application environment. 15 | """ 16 | 17 | use Supervisor 18 | 19 | def start_link(_) do 20 | Supervisor.start_link(__MODULE__, []) 21 | end 22 | 23 | def init([]) do 24 | children = [ 25 | Instruments.Sysmon.Reporter, 26 | Instruments.Sysmon.Emitter, 27 | ] 28 | 29 | Supervisor.init(children, strategy: :rest_for_one, name: __MODULE__) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/sysmon/receiver/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Sysmon.Receiver.Metrics do 2 | @moduledoc """ 3 | This module emits system monitor events 4 | """ 5 | require Instruments 6 | 7 | @behaviour Instruments.Sysmon.Receiver 8 | 9 | @impl true 10 | def handle_busy_dist_port(_, _) do 11 | Instruments.increment("erlang.sysmon.busy_dist_port") 12 | end 13 | 14 | @impl true 15 | def handle_busy_port(_, _) do 16 | Instruments.increment("erlang.sysmon.busy_port") 17 | end 18 | 19 | @impl true 20 | def handle_long_gc(_, _) do 21 | Instruments.increment("erlang.sysmon.long_gc") 22 | end 23 | 24 | @impl true 25 | def handle_long_message_queue(_, long) do 26 | Instruments.increment("erlang.sysmon.long_message_queue", tags: ["long:#{long}"]) 27 | end 28 | 29 | @impl true 30 | def handle_long_schedule(_, _) do 31 | Instruments.increment("erlang.sysmon.long_schedule") 32 | end 33 | 34 | @impl true 35 | def handle_large_heap(_, _) do 36 | Instruments.increment("erlang.sysmon.large_heap") 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Discord, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /lib/sysmon/receiver/log.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Sysmon.Receiver.Log do 2 | @moduledoc """ 3 | This module emits system monitor events 4 | """ 5 | 6 | @behaviour Instruments.Sysmon.Receiver 7 | 8 | require Logger 9 | 10 | @impl true 11 | def handle_busy_port(pid, port) do 12 | Logger.warn("Busy port: #{inspect(pid)} #{inspect(port)}") 13 | end 14 | 15 | @impl true 16 | def handle_busy_dist_port(pid, port) do 17 | Logger.warn("Busy dist port: #{inspect(pid)} #{inspect(port)}") 18 | end 19 | 20 | @impl true 21 | def handle_long_gc(pid, info) do 22 | Logger.warn("Long GC: #{inspect(pid)} #{inspect(info)}") 23 | end 24 | 25 | @impl true 26 | def handle_long_message_queue(pid, long) do 27 | if long do 28 | Logger.warn("Long message queue: #{inspect(pid)}") 29 | else 30 | Logger.info("Long message queue resolved: #{inspect(pid)}") 31 | end 32 | end 33 | 34 | @impl true 35 | def handle_long_schedule(pid, info) do 36 | Logger.warn("Long schedule: #{inspect(pid)} #{inspect(info)}") 37 | end 38 | 39 | @impl true 40 | def handle_large_heap(pid, info) do 41 | Logger.warn("Large heap: #{inspect(pid)} #{inspect(info)}") 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/stats_reporter/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.StatsReporter.Logger do 2 | @moduledoc """ 3 | A StatsReporter that logs to the `Logger` module. 4 | """ 5 | 6 | @behaviour Instruments.StatsReporter 7 | require Logger 8 | 9 | @doc false 10 | def connect(), do: :ok 11 | 12 | @doc false 13 | def increment(key, value \\ 1, _options \\ []) do 14 | Logger.info("incrementing #{key} by #{value}") 15 | end 16 | 17 | @doc false 18 | def decrement(key, value \\ 1, _options \\ []) do 19 | Logger.info("decrementing #{key} by #{value}") 20 | end 21 | 22 | @doc false 23 | def gauge(key, value, _options \\ []) do 24 | Logger.info("Setting gauge #{key} to #{value}") 25 | end 26 | 27 | @doc false 28 | def histogram(key, value, _options \\ []) do 29 | Logger.info("Adding #{value} to #{key} histogram") 30 | end 31 | 32 | @doc false 33 | def timing(key, value, _options \\ []) do 34 | Logger.info("#{key} took #{value}ms") 35 | end 36 | 37 | @doc false 38 | def measure(key, _options \\ [], fun) do 39 | {time_in_us, result} = :timer.tc(fun) 40 | Logger.info("#{key} took #{time_in_us / 1000}ms") 41 | result 42 | end 43 | 44 | @doc false 45 | def set(key, value, _options \\ []) do 46 | Logger.info("setting #{key} to #{value}") 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: ubuntu-24.04 13 | strategy: 14 | # This depends on listening on a UDP socket on a fixed port so only 1 at a time. 15 | max-parallel: 1 16 | matrix: 17 | include: 18 | - elixir-version: 1.12.3 19 | otp-version: 24.3 20 | - elixir-version: 1.15.5 21 | otp-version: 25.3 22 | - elixir-version: 1.16.2 23 | otp-version: 25.3 24 | - elixir-version: 1.17.3 25 | otp-version: 25.3 26 | - elixir-version: 1.18.3 27 | otp-version: 25.3 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Set up Elixir 31 | uses: erlef/setup-beam@v1 32 | with: 33 | elixir-version: ${{ matrix.elixir-version }} 34 | otp-version: ${{ matrix.otp-version }} 35 | - name: Restore dependencies cache 36 | uses: actions/cache@v4 37 | with: 38 | path: deps 39 | key: ${{ runner.os }}-${{ matrix.elixir-version }}-${{ matrix.otp-version}}-mix-${{ hashFiles('**/mix.lock') }} 40 | restore-keys: ${{ runner.os }}-${{ matrix.elixir-version}}-${{ matrix.otp-version }}-mix- 41 | - name: Install dependencies 42 | run: mix deps.get 43 | - name: Run tests 44 | run: mix test 45 | -------------------------------------------------------------------------------- /pages/Performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | There are a couple optimizations that keep Instruments fast. 4 | 5 | ### ETS backed counters 6 | Probe counters actually increment or decrement a value in an ETS table, every 7 | `fast_counter_report_interval` milliseconds, the aggregated values are flushed to 8 | statsd. Furthermore, the ETS tables are sharded across schedulers and can be written to 9 | without any concurrency. Because of this, counters are effectively free and with a 10 | conservative flush interval, will put little pressure on your statsd server. 11 | 12 | ### IOData metric names 13 | 14 | Instruments uses macros to implement the metric names, and automatically converts interpolated 15 | strings into IOLists. This means you can have many generated names without increasing the 16 | amount of binary memory you're using. For example: 17 | 18 | ```elixir 19 | def increment_rpc(rpc_name), 20 | do: Instruments.increment("my_module.rpc.#{rpc_name}") 21 | ``` 22 | 23 | will be rewritten to the call: 24 | 25 | ```elixir 26 | def increment_rpc(rpc_name), 27 | do: Instruments.increment(["my_module.rpc.", Kernel.to_string(rpc_name)]) 28 | ``` 29 | 30 | If you wish, you may pass any IOData as the name of a metric. 31 | 32 | ### Sample Rates 33 | For histograms, measure calls and timings, the default sample rate is pegged to 0.1. 34 | This is so you don't accidentally overload your metrics collector. It can be 35 | overridden by passing `sample_rate: float_value` to your metrics call in the 36 | options. 37 | -------------------------------------------------------------------------------- /lib/stats_reporter.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.StatsReporter do 2 | @moduledoc """ 3 | A behavoiur for reporters. 4 | 5 | Reporters emit values back to the underlying reporting system. 6 | Out of the box, Instruments provides `Instruments.Statix`, `Instruments.StatsReporter.Logger`, 7 | and `Instruments.StatsReporter.Null`reporters. 8 | """ 9 | 10 | @type key :: String.t() 11 | @type stats_return :: :ok | {:error, term} 12 | 13 | @doc """ 14 | Connect to the reporter. 15 | This function is called by the system prior to using the reporter, 16 | any connections should be established in this function. 17 | """ 18 | @callback connect() :: :ok 19 | 20 | @doc """ 21 | Increment a key by the specified value 22 | """ 23 | @callback increment(key, integer, keyword) :: stats_return 24 | 25 | @doc """ 26 | Decrement a key by the specified value 27 | """ 28 | @callback decrement(key, integer, keyword) :: stats_return 29 | 30 | @doc """ 31 | Set the value of the key to the specified value 32 | """ 33 | @callback gauge(key, integer, keyword) :: stats_return 34 | 35 | @doc """ 36 | Include the value in the histogram defined by `key` 37 | """ 38 | @callback histogram(key, integer, keyword) :: stats_return 39 | 40 | @doc """ 41 | Include the timing in the `key` 42 | """ 43 | @callback timing(key, integer, keyword) :: stats_return 44 | 45 | @doc """ 46 | Measure the execution time of the provided function and 47 | include it in the metric defined by `key` 48 | """ 49 | @callback measure(key, keyword, (() -> any)) :: any 50 | 51 | @doc """ 52 | Write the value into the set defined by `key` 53 | """ 54 | @callback set(key, integer, keyword) :: stats_return 55 | end 56 | -------------------------------------------------------------------------------- /pages/Overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | You're blind without metrics. Metrics should also be easy to add to you application 4 | and have little performance impact. This module allows you to define metrics 5 | with ease and see inside your application. 6 | 7 | Instruments has the following types of metrics that closely mirror statsd. 8 | 9 | * **Counters**: Allow you to increment or decrement a value. 10 | * **Gauges**: Allow you to report a single value that changes over time 11 | * **Histograms**: Values are grouped into percentiles 12 | * **Timings**: Report a timed value in milliseconds 13 | * **Measurements**: Measure the execution time of a function 14 | * **Sets**: Add a value to a statsd set 15 | * **Events**: Report an event like a deploy using arbitrary keys and values 16 | 17 | 18 | ## Basic Usage 19 | 20 | Reporting a metric is extremely simple; just `use` the Instruments module and call the 21 | appropriate function: 22 | 23 | ```elixir 24 | defmodule ModuleThatNeedsMetrics do 25 | use Instruments 26 | 27 | def other_function() do 28 | Process.sleep(150) 29 | end 30 | 31 | def metrics_function() do 32 | Instruments.increment("my.counter", 3) 33 | Instruments.measure("metrics_function.other_fn_call", &other_function/0) 34 | end 35 | end 36 | ``` 37 | 38 | ## Custom Namespaces 39 | Often, all metrics inside a module have namespaced metrics. This is easy to accomplish 40 | using `CustomFunctions` 41 | 42 | ```elixir 43 | defmodule RpcHandler do 44 | use Instruments.CustomFunctions, prefix: "my_service.rpc" 45 | 46 | def handle(:get, "/foo/bar") do 47 | increment("foo.bar") 48 | end 49 | end 50 | ``` 51 | 52 | The above example will increment the `my_service.rpc.foo.bar` metric by one. 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/rate_tracker_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Instruments.RateTrackerTest do 2 | use ExUnit.Case 3 | 4 | alias Instruments.RateTracker 5 | 6 | setup do 7 | old_threshold = Application.get_env(:instruments, :rate_tracker_callback_threshold) 8 | Application.put_env(:instruments, :rate_tracker_callback_threshold, 5) 9 | 10 | on_exit(fn -> 11 | if old_threshold do 12 | Application.put_env(:instruments, :rate_tracker_callback_threshold, old_threshold) 13 | else 14 | Application.delete_env(:instruments, :rate_tracker_callback_threshold) 15 | end 16 | end) 17 | 18 | :ok 19 | end 20 | 21 | test "calls callback if rate is exceeded" do 22 | me = self() 23 | 24 | RateTracker.subscribe(fn value, _rate -> 25 | send(me, value) 26 | end) 27 | 28 | Enum.each( 29 | 1..1000, 30 | fn _n -> 31 | RateTracker.track("test.metric", tags: ["test_tag_1:abc", "test_tag_2:def"]) 32 | end 33 | ) 34 | 35 | assert_receive {"test.metric", [tags: ["test_tag_1:abc", "test_tag_2:def"]]} 36 | end 37 | 38 | test "does not call calback if rate not exceeded" do 39 | me = self() 40 | 41 | RateTracker.subscribe(fn value, _rate -> 42 | send(me, value) 43 | end) 44 | 45 | refute_receive {"test.metric", [tags: ["test_tag_1:abc", "test_tag_2:def"]]} 46 | end 47 | 48 | test "a highly frequent but low sampled metric won't be reported" do 49 | me = self() 50 | 51 | Enum.each( 52 | 1..1000, 53 | fn _n -> 54 | RateTracker.track("test.metric", tags: ["test_tag_1:abc", "test_tag_2:def"], sample_rate: 0.001) 55 | end 56 | ) 57 | 58 | RateTracker.subscribe(fn value, _rate -> 59 | send(me, value) 60 | end) 61 | 62 | refute_receive {"test.metric", [tags: ["test_tag_1:abc", "test_tag_2:def"]]} 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Mixfile do 2 | use Mix.Project 3 | 4 | @version "2.6.0" 5 | @github_url "https://github.com/discord/instruments" 6 | 7 | def project do 8 | [ 9 | app: :instruments, 10 | name: "Instruments", 11 | version: @version, 12 | elixir: "~> 1.5", 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | elixirc_paths: compile_paths(Mix.env()), 16 | deps: deps(), 17 | docs: docs(), 18 | package: package(), 19 | description: description() 20 | ] 21 | end 22 | 23 | def docs do 24 | [ 25 | extras: [ 26 | "pages/Overview.md", 27 | "pages/Configuration.md", 28 | "pages/Probes.md", 29 | "pages/Performance.md" 30 | ] 31 | ] 32 | end 33 | 34 | def application do 35 | [ 36 | extra_applications: [:logger], 37 | mod: {Instruments.Application, []} 38 | ] 39 | end 40 | 41 | def compile_paths(:test) do 42 | default_compile_path() ++ ["test/support"] 43 | end 44 | 45 | def compile_paths(_), do: default_compile_path() 46 | 47 | defp default_compile_path(), do: ["lib"] 48 | 49 | defp deps do 50 | [ 51 | {:benchee, "~> 1.4", only: :dev}, 52 | {:ex_doc, "~> 0.28", only: :dev, runtime: false}, 53 | {:recon, "~> 2.5.2"}, 54 | {:statix, "~> 1.5.1", hex: :discord_statix}, 55 | {:dialyxir, "~> 1.0", only: :dev, runtime: false} 56 | ] 57 | end 58 | 59 | defp description do 60 | "A small, fast, and unobtrusive metrics library" 61 | end 62 | 63 | defp package do 64 | [ 65 | name: "instruments", 66 | files: ["lib", "pages", "README*", "LICENSE", "mix.exs"], 67 | maintainers: ["Discord Core Infrastructure"], 68 | licenses: ["MIT"], 69 | source_url: @github_url, 70 | links: %{"GitHub" => @github_url} 71 | ] 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /bench/fast_gauge/gauge.exs: -------------------------------------------------------------------------------- 1 | Application.ensure_all_started(:instruments) 2 | 3 | Benchee.run( 4 | %{ 5 | "gauge_non_parallel" => fn options -> 6 | for v <- 1..100 do 7 | Instruments.Statix.gauge("test.gauge", v, options) 8 | end 9 | end, 10 | "fastgauge_non_parallel" => fn options -> 11 | for v <- 1..100 do 12 | Instruments.FastGauge.gauge("test.gauge", v, options) 13 | end 14 | end 15 | }, 16 | inputs: %{ 17 | "1. No Options" => [], 18 | "2. No Tags" => [sample_rate: 1.0], 19 | "3. Empty Tags" => [sample_rate: 1.0, tags: []], 20 | "4. One Tag" => [sample_rate: 1.0, tags: ["test:tag"]], 21 | "5. Five Tags" => [sample_rate: 1.0, tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag"]], 22 | "6. Ten Tags" => [sample_rate: 1.0, tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag", "test-6:tag", "test-7:tag", "test-8:tag", "test-9:tag", "test-10:tag"]] 23 | }, 24 | parallel: 1, 25 | save: [path: "bench/results/fast_gauge/non_parallel.benchee"] 26 | ) 27 | 28 | Benchee.run( 29 | %{ 30 | "gauge_parallel_8" => fn options -> 31 | for v <- 1..100 do 32 | Instruments.Statix.gauge("test.gauge", v, options) 33 | end 34 | end, 35 | "fastgauge_parallel_8" => fn options -> 36 | for v <- 1..100 do 37 | Instruments.FastGauge.gauge("test.gauge", v, options) 38 | end 39 | end 40 | }, 41 | inputs: %{ 42 | "1. No Options" => [], 43 | "2. No Tags" => [sample_rate: 1.0], 44 | "3. Empty Tags" => [sample_rate: 1.0, tags: []], 45 | "4. One Tag" => [sample_rate: 1.0, tags: ["test:tag"]], 46 | "5. Five Tags" => [sample_rate: 1.0, tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag"]], 47 | "6. Ten Tags" => [sample_rate: 1.0, tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag", "test-6:tag", "test-7:tag", "test-8:tag", "test-9:tag", "test-10:tag"]] 48 | }, 49 | parallel: 8, 50 | save: [path: "bench/results/fast_gauge/parallel_8.benchee"] 51 | ) 52 | -------------------------------------------------------------------------------- /test/macro_helpers.test.exs: -------------------------------------------------------------------------------- 1 | defmodule Instruments.MacroHelpersTest do 2 | use ExUnit.Case 3 | import Instruments.MacroHelpers, only: [to_iolist: 1] 4 | 5 | test "should work with a plain string" do 6 | assert to_iolist(quote do: "foo.bar.baz") == "foo.bar.baz" 7 | end 8 | 9 | test "should work with an interpolated string at the end" do 10 | var = Macro.var(:baz, __MODULE__) 11 | assert ["foo.bar.", ^var] = to_iolist(quote do: "foo.bar.#{baz}") 12 | end 13 | 14 | test "should work with an interpolated string in the middle" do 15 | metric_var = Macro.var(:metric_name, __MODULE__) 16 | assert ["foo.", ^metric_var, ".bar"] = to_iolist(quote do: "foo.#{metric_name}.bar") 17 | end 18 | 19 | test "should work with an interpolated string at the beginning" do 20 | metric_var = Macro.var(:metric_1, __MODULE__) 21 | assert [^metric_var, ".second"] = to_iolist(quote do: "#{metric_1}.second") 22 | end 23 | 24 | test "it should work with several interpolated strings" do 25 | prefix_var = Macro.var(:prefix, __MODULE__) 26 | suffix_var = Macro.var(:suffix, __MODULE__) 27 | 28 | assert [^prefix_var, ".something.", ^suffix_var] = 29 | to_iolist(quote do: "#{prefix}.something.#{suffix}") 30 | end 31 | 32 | test "it should work with string concatenation" do 33 | requests_var = Macro.var(:requests, __MODULE__) 34 | 35 | assert [^requests_var, ".suffix"] = to_iolist(quote do: requests <> ".suffix") 36 | end 37 | 38 | test "it should let you pass in iolists" do 39 | metric_var = Macro.var(:metric_name, __MODULE__) 40 | 41 | assert ["foo", ".", "bar", ".", "baz"] == to_iolist(quote do: ["foo", ".", "bar", ".", "baz"]) 42 | 43 | assert ["foo", ".", "bar", ".", ^metric_var] = 44 | to_iolist(quote do: ["foo", ".", "bar", ".", metric_name]) 45 | 46 | assert ["foo", ".", ^metric_var, ".", "baz"] = 47 | to_iolist(quote do: ["foo", ".", metric_name, ".", "baz"]) 48 | 49 | assert [^metric_var, ".", "bar", ".", "baz"] = 50 | to_iolist(quote do: [metric_name, ".", "bar", ".", "baz"]) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /bench/rate_tracker/track.exs: -------------------------------------------------------------------------------- 1 | Application.ensure_all_started(:instruments) 2 | 3 | Benchee.run( 4 | %{ 5 | "track_non_parallel" => fn options -> 6 | for _ <- 1..100 do 7 | Instruments.RateTracker.track("test.histogram", options) 8 | end 9 | end 10 | }, 11 | inputs: %{ 12 | "1. No Options" => [], 13 | "2. No Tags" => [sample_rate: 1.0], 14 | "3. Empty Tags" => [sample_rate: 1.0, tags: []], 15 | "4. One Tag" => [sample_rate: 1.0, tags: ["test:tag"]], 16 | "5. Five Tags" => [ 17 | sample_rate: 1.0, 18 | tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag"] 19 | ], 20 | "6. Ten Tags" => [ 21 | sample_rate: 1.0, 22 | tags: [ 23 | "test-1:tag", 24 | "test-2:tag", 25 | "test-3:tag", 26 | "test-4:tag", 27 | "test-5:tag", 28 | "test-6:tag", 29 | "test-7:tag", 30 | "test-8:tag", 31 | "test-9:tag", 32 | "test-10:tag" 33 | ] 34 | ] 35 | }, 36 | parallel: 1, 37 | save: [path: "bench/results/rate_tracker/non_parallel.benchee"] 38 | ) 39 | 40 | Benchee.run( 41 | %{ 42 | "track_parallel_8" => fn options -> 43 | for _ <- 1..100 do 44 | Instruments.RateTracker.track("test.tracker", options) 45 | end 46 | end 47 | }, 48 | inputs: %{ 49 | "1. No Options" => [], 50 | "2. No Tags" => [sample_rate: 1.0], 51 | "3. Empty Tags" => [sample_rate: 1.0, tags: []], 52 | "4. One Tag" => [sample_rate: 1.0, tags: ["test:tag"]], 53 | "5. Five Tags" => [ 54 | sample_rate: 1.0, 55 | tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag"] 56 | ], 57 | "6. Ten Tags" => [ 58 | sample_rate: 1.0, 59 | tags: [ 60 | "test-1:tag", 61 | "test-2:tag", 62 | "test-3:tag", 63 | "test-4:tag", 64 | "test-5:tag", 65 | "test-6:tag", 66 | "test-7:tag", 67 | "test-8:tag", 68 | "test-9:tag", 69 | "test-10:tag" 70 | ] 71 | ] 72 | }, 73 | parallel: 8, 74 | save: [path: "bench/results/rate_tracker/parallel_8.benchee"] 75 | ) 76 | -------------------------------------------------------------------------------- /bench/results/fast_counter/increment/strategy-1.txt: -------------------------------------------------------------------------------- 1 | v1 Fast Counter Strategy 2 | 3 | Original release Fast Counter Strategy. Creates a semi-stable key by sorting tags and merging them back into the 4 | options. 5 | 6 | Output of Benchmark 7 | 8 | Operating System: macOS 9 | CPU Information: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz 10 | Number of Available Cores: 16 11 | Available memory: 64 GB 12 | Elixir 1.7.4 13 | Erlang 21.3.8.10 14 | 15 | Benchmark suite executing with the following configuration: 16 | warmup: 2 s 17 | time: 5 s 18 | memory time: 0 ns 19 | parallel: 1 20 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 21 | Estimated total run time: 42 s 22 | 23 | Benchmarking increment with input 1. No Options... 24 | Benchmarking increment with input 2. No Tags... 25 | Benchmarking increment with input 3. Empty Tags... 26 | Benchmarking increment with input 4. One Tag... 27 | Benchmarking increment with input 5. Five Tags... 28 | Benchmarking increment with input 6. Ten Tags... 29 | 30 | ##### With input 1. No Options ##### 31 | Name ips average deviation median 99th % 32 | increment 41.87 K 23.89 μs ±40.66% 22 μs 70 μs 33 | 34 | ##### With input 2. No Tags ##### 35 | Name ips average deviation median 99th % 36 | increment 34.17 K 29.26 μs ±42.41% 25 μs 87 μs 37 | 38 | ##### With input 3. Empty Tags ##### 39 | Name ips average deviation median 99th % 40 | increment 19.19 K 52.12 μs ±38.40% 45 μs 140 μs 41 | 42 | ##### With input 4. One Tag ##### 43 | Name ips average deviation median 99th % 44 | increment 17.84 K 56.05 μs ±38.08% 48.00 μs 150 μs 45 | 46 | ##### With input 5. Five Tags ##### 47 | Name ips average deviation median 99th % 48 | increment 11.15 K 89.72 μs ±34.33% 78.00 μs 223.00 μs 49 | 50 | ##### With input 6. Ten Tags ##### 51 | Name ips average deviation median 99th % 52 | increment 6.58 K 151.97 μs ±30.37% 134 μs 353 μs -------------------------------------------------------------------------------- /pages/Probes.md: -------------------------------------------------------------------------------- 1 | # Probes 2 | 3 | A probe is a persistent system metric that's updated at a 4 | consistent rate. 5 | 6 | Probes can be either functions, or a module that implements the 7 | `Instruments.Probe` behaviour. If it's a module, its state is controlled by 8 | another process, all implementers need to worry about is the 9 | state transitions. 10 | 11 | ## Defining probes 12 | 13 | First of all, probes must have unique names so their stats won't conflict 14 | with one another. This is enforced at runtime, since it's possible to 15 | define probes progammatically. 16 | 17 | ### Functions 18 | The simplest way to define a probe is to specify a function: 19 | 20 | ```elixir 21 | Probe.define("erlang.process_count", :gauge, 22 | function: fn -> :erlang.system_info(:process_count) end, 23 | report_interval: 60_000) 24 | ``` 25 | 26 | Since the above definiton doesn't pass in the sample_interval options, 27 | the sample interval is the same as the report interval. The metric will 28 | be sampled and reported every 60 seconds. 29 | 30 | ### MFA 31 | 32 | A simplification of the above example uses the `:mfa` option to specify 33 | a module, function and arguments to be called. 34 | For example, 35 | 36 | ```elixir 37 | Probe.define("erlang.process_count", :gauge, mfa: {:erlang, :system_info, [:process_count]}) 38 | ``` 39 | 40 | You can also have a function that returns a keyword list of stats and 41 | select which keys you want to report. The keys are added to the stat name 42 | 43 | ```elixir 44 | Probe.define("erlang.memory", :gauge, 45 | function: fn -> :erlang.memory() end, 46 | keys: [:total, :atom, :processes], 47 | report_interval: 60_000) 48 | ``` 49 | 50 | While the `:erlang.memory()` returns a keyword list with 9 entries, 51 | the above call will only produce three metrics, `erlang.memory.total`, 52 | `erlang.memory.atom` and `erlang.memory.processes`. 53 | 54 | ## Module based Probes 55 | If more control is desired, you can implement a probe module yourself. 56 | 57 | ```elixir 58 | defmodule MyProbe do 59 | @behaviour Instruments.Probe 60 | # callback implementations... 61 | end 62 | 63 | # and then define the probe: 64 | Probe.define("system.my_probe", :counter, module: MyProbe) 65 | ``` 66 | 67 | Your module is now a registered probe, and will receive all of the `Probe` 68 | callbacks. 69 | -------------------------------------------------------------------------------- /test/sysmon/reporter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Sysmon.ReporterTest do 2 | use ExUnit.Case 3 | 4 | alias Instruments.Sysmon.Reporter 5 | 6 | describe "subscriptions" do 7 | setup do 8 | {:ok, reporter_pid} = start_supervised(Reporter) 9 | Process.link(reporter_pid) 10 | 11 | {:ok, %{ 12 | reporter_pid: reporter_pid 13 | }} 14 | end 15 | 16 | test "allows subscribing", ctx do 17 | :ok = Reporter.subscribe() 18 | state = :sys.get_state(ctx.reporter_pid) 19 | assert Map.values(state.subscribers) == [self()] 20 | end 21 | 22 | test "can subscribe more than once", ctx do 23 | :ok = Reporter.subscribe() 24 | :ok = Reporter.subscribe() 25 | state = :sys.get_state(ctx.reporter_pid) 26 | assert Map.values(state.subscribers) == [self()] 27 | end 28 | 29 | test "allows unsubscribing", ctx do 30 | :ok = Reporter.subscribe() 31 | :ok = Reporter.unsubscribe() 32 | state = :sys.get_state(ctx.reporter_pid) 33 | assert Map.values(state.subscribers) == [] 34 | end 35 | 36 | test "sends events to subscribers" do 37 | Reporter.subscribe() 38 | dummy_port = :erlang.list_to_port(~c"#Port<0.1>") 39 | pid = self() 40 | send(Reporter, {:monitor, pid, :busy_port, dummy_port}) 41 | assert_receive {Reporter, :busy_port, %{pid: ^pid, port: ^dummy_port}} 42 | end 43 | end 44 | 45 | describe "events" do 46 | 47 | test "can be set from environment" do 48 | prev = Application.get_env(:instruments, :sysmon_events) 49 | on_exit(fn -> 50 | Application.put_env(:instruments, :sysmon_events, prev) 51 | end) 52 | Application.put_env((:instruments), :sysmon_events, [:busy_port, :busy_dist_port]) 53 | {:ok, pid} = start_supervised(Reporter) 54 | Process.link(pid) 55 | 56 | assert Reporter.get_events() == [:busy_port, :busy_dist_port] 57 | assert :erlang.system_monitor() == {pid, [:busy_dist_port, :busy_port]} 58 | end 59 | 60 | test "can be reconfigured" do 61 | {:ok, pid} = start_supervised(Reporter) 62 | Process.link(pid) 63 | 64 | Reporter.set_events([:busy_port, :busy_dist_port]) 65 | assert :erlang.system_monitor() == {pid, [:busy_dist_port, :busy_port]} 66 | end 67 | 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /pages/Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ### Statix 4 | 5 | After the underlying `Statix` library is configured, `Instruments` requires little configuration. 6 | To configure `Statix`, include the following in your `config/config.exs` file: 7 | 8 | ```elixir 9 | config :statix, 10 | prefix: "#{Mix.env}", 11 | host: "localhost", 12 | port: 15339 13 | ``` 14 | This should be pretty self-explanatory, other than you'll have a metric prefix of your 15 | Mix.env for all metrics, so if you defined a metric named `my_server.requests_per_second` it would be 16 | converted to `prod.my_server.requests_per_second`. 17 | 18 | More information can be found on the [Statix GitHub page](https://github.com/lexmag/statix#configuration). 19 | 20 | ### Instruments-Specific Config 21 | 22 | There are a couple of `Instruments` specific application variables: 23 | 24 | * `reporter_module`: The `Instruments.StatsReporter` that emits statistics for the application. Defaults 25 | to `Instruments.Statix`. 26 | * `fast_counter_report_interval`: How often counters should send data to the `reporter_module`, in milliseconds. 27 | Defaults to 10 seconds. 28 | * `fast_counter_report_jitter_range`: How much random jitter should be applied to the reporting interval, in milliseconds. 29 | Defaults to half a second before and after the reporting interval. 30 | * `probe_prefix`: A global prefix to apply to all probes. 31 | * `statsd_port`: The port that the statsd server listens on. Should be the same as the port in the statix 32 | configuration above. 33 | * `enable_sysmon`: Enables and registers `Instruments.Sysmon.Reporter` with `:erlang.system_monitor/1` to receive system 34 | monitor events. 35 | * `sysmon_receiver`: The `Instruments.Sysmon.Receiver` that handles sysmon events. Defaults to `Instruments.Sysmon.Receiver.Metrics` 36 | * `sysmon_events`: The list of system monitor events that `Instruments.Sysmon.Reporter` will subscribe to. Defaults to `[]` 37 | 38 | For example: 39 | 40 | config :instruments, 41 | reporter_module: Instruments.StatsReporter.Logger, 42 | fast_counter_report_interval: 30_000, 43 | fast_counter_report_jitter_range: -700..700, 44 | probe_prefix: "probes", 45 | statsd_port: 15339 46 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule MetricsAssertions do 4 | @safe_metric_types [:increment, :decrement, :gauge, :event, :set] 5 | use ExUnit.Case 6 | 7 | def assert_metric_reported(metric_type, metric_name) do 8 | assert_receive {:metric_reported, {^metric_type, ^metric_name, _, _}}, 5000 9 | end 10 | 11 | def assert_metric_reported(metric_type, metric_name, metric_value) 12 | when is_number(metric_value) do 13 | assert_receive {:metric_reported, {^metric_type, ^metric_name, ^metric_value, _}}, 5000 14 | end 15 | 16 | def assert_metric_reported(metric_type, metric_name, expected_metric_value) do 17 | assert_receive {:metric_reported, {^metric_type, ^metric_name, actual_value, _}}, 5000 18 | 19 | cond do 20 | range?(expected_metric_value) -> 21 | do_assert_range(expected_metric_value, actual_value) 22 | 23 | true -> 24 | assert expected_metric_value == actual_value 25 | end 26 | end 27 | 28 | def assert_metric_reported(metric_type, metric_name, metric_value, options) 29 | when is_number(metric_value) do 30 | assert_receive {:metric_reported, {^metric_type, ^metric_name, ^metric_value, actual_options}}, 5000 31 | 32 | do_assert_options(metric_type, options, actual_options) 33 | end 34 | 35 | def assert_metric_reported(metric_type, metric_name, expected_metric_value, options) do 36 | assert_receive {:metric_reported, 37 | {^metric_type, ^metric_name, actual_metric_value, actual_options}}, 5000 38 | 39 | do_assert_options(metric_type, options, actual_options) 40 | 41 | cond do 42 | range?(expected_metric_value) -> 43 | do_assert_range(expected_metric_value, actual_metric_value) 44 | 45 | true -> 46 | assert expected_metric_value == actual_metric_value 47 | end 48 | end 49 | 50 | defp do_assert_range(metric_range, actual_metric_value) do 51 | assert round(actual_metric_value) in metric_range 52 | end 53 | 54 | defp do_assert_options(metric_type, expected, actual) when metric_type in @safe_metric_types, 55 | do: assert(expected == actual) 56 | 57 | defp do_assert_options(_, expected_options, actual_options) do 58 | options_with_sample_rate = Keyword.merge([sample_rate: 1.0], expected_options) 59 | 60 | for {expected_key, expected_value} <- options_with_sample_rate do 61 | assert {expected_key, expected_value} == 62 | {expected_key, Keyword.get(actual_options, expected_key)} 63 | end 64 | end 65 | 66 | defp range?(_.._) do 67 | true 68 | end 69 | 70 | defp range?(_) do 71 | false 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/custom_functions.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.CustomFunctions do 2 | @moduledoc """ 3 | Creates custom prefixed functions 4 | 5 | Often, a module will have functions that all have a common prefix. 6 | It's somewhat tedious to have to put this prefix in every call to 7 | every metric function. Using this module can help somewhat. 8 | 9 | When you `use` this module, it defines custom, module-specific metrics 10 | functions that include your prefix. For example: 11 | 12 | ``` 13 | defmodule Prefixed do 14 | use Instruments.CustomFunctions, prefix: "my.module" 15 | 16 | def do_something() do 17 | increment("do_something_counts") 18 | do_another_thing() 19 | end 20 | 21 | def long_running() do 22 | measure("timed_fn", &compute/0) 23 | end 24 | 25 | defp compute(), do: Process.sleep(10_000) 26 | defp do_another_thing, do: 3 27 | end 28 | ``` 29 | 30 | In the above example, we increment `do_something_counts` and `timed_fn`, yet 31 | the metrics emitted are `my.module.do_something_counts` and `my.module.timed_fn`. 32 | """ 33 | 34 | defmacro __using__(opts) do 35 | prefix = 36 | case Keyword.fetch!(opts, :prefix) do 37 | prefix_string when is_bitstring(prefix_string) -> 38 | prefix_string 39 | 40 | ast -> 41 | {computed_prefix, _} = Code.eval_quoted(ast) 42 | computed_prefix 43 | end 44 | 45 | prefix_with_dot = "#{prefix}." 46 | 47 | quote do 48 | use Instruments 49 | 50 | @doc false 51 | def increment(key, value \\ 1, options \\ []) do 52 | Instruments.increment([unquote(prefix_with_dot), key], value, options) 53 | end 54 | 55 | @doc false 56 | def decrement(key, value \\ 1, options \\ []) do 57 | Instruments.decrement([unquote(prefix_with_dot), key], value, options) 58 | end 59 | 60 | @doc false 61 | def gauge(key, value, options \\ []) do 62 | Instruments.gauge([unquote(prefix_with_dot), key], value, options) 63 | end 64 | 65 | @doc false 66 | def histogram(key, value, options \\ []) do 67 | Instruments.histogram([unquote(prefix_with_dot), key], value, options) 68 | end 69 | 70 | @doc false 71 | def timing(key, value, options \\ []) do 72 | Instruments.timing([unquote(prefix_with_dot), key], value, options) 73 | end 74 | 75 | @doc false 76 | def set(key, value, options \\ []) do 77 | Instruments.set([unquote(prefix_with_dot), key], value, options) 78 | end 79 | 80 | @doc false 81 | def measure(key, options \\ [], func) do 82 | Instruments.measure([unquote(prefix_with_dot), key], options, func) 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/sysmon/emitter.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Sysmon.Emitter do 2 | @moduledoc """ 3 | The Emitter is a simple module that subscribes to the Reporter and will invoke 4 | the corresponding handler on the Receiver. 5 | """ 6 | 7 | use GenServer 8 | 9 | require Logger 10 | 11 | alias Instruments.Sysmon.Reporter 12 | 13 | defstruct [ 14 | receiver_module: nil 15 | ] 16 | 17 | def start_link(opts \\ []) do 18 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 19 | end 20 | 21 | @doc """ 22 | Sets the receiver module to handle system monitor events. Receiver modules must implement the `Instruments.Sysmon.Receiver` behaviour. 23 | """ 24 | @spec set_receiver(term()) :: :ok 25 | def set_receiver(receiver_module) do 26 | GenServer.call(__MODULE__, {:set_receiver, receiver_module}) 27 | end 28 | 29 | @impl true 30 | def init(_) do 31 | Reporter.subscribe() 32 | {:ok, %__MODULE__{ 33 | receiver_module: Application.get_env(:instruments, :sysmon_receiver, Instruments.Sysmon.Receiver.Metrics) 34 | }} 35 | end 36 | 37 | 38 | @impl true 39 | def handle_call({:set_receiver, receiver_module}, _from, %__MODULE__{} = state) do 40 | {:reply, :ok, %__MODULE__{state | receiver_module: receiver_module}} 41 | end 42 | 43 | @impl true 44 | def handle_info({Reporter, event, data}, state) do 45 | handle_event(state, event, data) 46 | {:noreply, state} 47 | end 48 | 49 | def handle_info(unknown, state) do 50 | Logger.error("Emitter received unknown message: #{inspect(unknown)}") 51 | {:noreply, state} 52 | end 53 | 54 | defp handle_event(%__MODULE__{} = state, :busy_dist_port, %{pid: pid, port: port}) do 55 | state.receiver_module.handle_busy_dist_port(pid, port) 56 | end 57 | 58 | defp handle_event(%__MODULE__{} = state, :busy_port, %{pid: pid, port: port}) do 59 | state.receiver_module.handle_busy_port(pid, port) 60 | end 61 | 62 | defp handle_event(%__MODULE__{} = state, :long_gc, %{pid: pid, info: info}) do 63 | state.receiver_module.handle_long_gc(pid, info) 64 | end 65 | 66 | defp handle_event(%__MODULE__{} = state, :long_message_queue, %{pid: pid, info: long}) do 67 | state.receiver_module.handle_long_message_queue(pid, long) 68 | end 69 | 70 | defp handle_event(%__MODULE__{} = state, :long_schedule, %{pid: pid, info: info}) do 71 | state.receiver_module.handle_long_schedule(pid, info) 72 | end 73 | 74 | defp handle_event(%__MODULE__{} = state, :large_heap, %{pid: pid, info: info}) do 75 | state.receiver_module.handle_large_heap(pid, info) 76 | end 77 | 78 | defp handle_event(%__MODULE__{}, event, data) do 79 | Logger.warn("Emitter received unknown event #{inspect(event)} with data #{inspect(data)}") 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/sysmon/emitter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Instruments.EmitterTest do 2 | use ExUnit.Case 3 | 4 | alias Instruments.Sysmon.Emitter 5 | alias Instruments.Sysmon.Reporter 6 | 7 | defmodule TestEmitter do 8 | @behaviour Instruments.Sysmon.Receiver 9 | 10 | @impl true 11 | def handle_busy_dist_port(pid, port) do 12 | send(TestEmitterReceiver, {:busy_dist_port, pid, port}) 13 | end 14 | 15 | @impl true 16 | def handle_busy_port(pid, port) do 17 | send(TestEmitterReceiver, {:busy_port, pid, port}) 18 | end 19 | 20 | @impl true 21 | def handle_long_gc(pid, info) do 22 | send(TestEmitterReceiver, {:long_gc, pid, info}) 23 | end 24 | 25 | @impl true 26 | def handle_long_message_queue(pid, long) do 27 | send(TestEmitterReceiver, {:long_message_queue, pid, long}) 28 | end 29 | 30 | @impl true 31 | def handle_long_schedule(pid, info) do 32 | send(TestEmitterReceiver, {:long_schedule, pid, info}) 33 | end 34 | 35 | @impl true 36 | def handle_large_heap(pid, info) do 37 | send(TestEmitterReceiver, {:large_heap, pid, info}) 38 | end 39 | end 40 | 41 | setup do 42 | Process.register(self(), TestEmitterReceiver) 43 | Application.put_env(:instruments, :sysmon_receiver, TestEmitter) 44 | 45 | {:ok, reporter_pid} = start_supervised(Reporter) 46 | Process.link(reporter_pid) 47 | 48 | {:ok, pid} = start_supervised(Emitter) 49 | Process.link(pid) 50 | 51 | {:ok, pid: pid} 52 | end 53 | 54 | test "handle_busy_dist_port", ctx do 55 | pid = self() 56 | port = :erlang.list_to_port(~c"#Port<0.1>") 57 | send(ctx.pid, {Reporter, :busy_dist_port, %{pid: pid, port: port}}) 58 | assert_receive {:busy_dist_port, ^pid, ^port} 59 | end 60 | 61 | test "handle_busy_port", ctx do 62 | pid = self() 63 | port = :erlang.list_to_port(~c"#Port<0.1>") 64 | send(ctx.pid, {Reporter, :busy_port, %{pid: pid, port: port}}) 65 | assert_receive {:busy_port, ^pid, ^port} 66 | end 67 | 68 | test "handle_long_gc", ctx do 69 | pid = self() 70 | send(ctx.pid, {Reporter, :long_gc, %{ 71 | pid: pid, 72 | info: [] 73 | }}) 74 | assert_receive {:long_gc, ^pid, []} 75 | end 76 | 77 | test "handle_long_message_queue", ctx do 78 | pid = self() 79 | send(ctx.pid, {Reporter, :long_message_queue, %{ 80 | pid: pid, 81 | info: true 82 | }}) 83 | assert_receive {:long_message_queue, ^pid, true} 84 | end 85 | 86 | test "handle_long_schedule", ctx do 87 | pid = self() 88 | send(ctx.pid, {Reporter, :long_schedule, %{ 89 | pid: pid, 90 | info: [] 91 | }}) 92 | assert_receive {:long_schedule, ^pid, []} 93 | end 94 | 95 | test "handle_large_heap", ctx do 96 | pid = self() 97 | send(ctx.pid, {Reporter, :large_heap, %{ 98 | pid: pid, 99 | info: [] 100 | }}) 101 | assert_receive {:large_heap, ^pid, []} 102 | end 103 | 104 | end 105 | -------------------------------------------------------------------------------- /lib/probe.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Probe do 2 | @moduledoc """ 3 | A behavior for a Probe. 4 | 5 | Modules that define probes are expected to implement all of the functions in 6 | this behaviour. 7 | 8 | A probe is created via the call to `c:Instruments.Probe.probe_init/3`, and is 9 | then called every `sample_interval` milliseconds via the 10 | `c:Instruments.Probe.probe_sample/1` function. The probe can then update its 11 | internal state and do any processing it requires. 12 | 13 | Every `report_interval` milliseconds, the probe is expected to emit its metric 14 | value. 15 | 16 | """ 17 | 18 | @type datapoint :: String.t() 19 | @type state :: any 20 | @type probe_value :: number | keyword 21 | @type probe_type :: :counter | :spiral | :gauge | :histogram | :timing | :set 22 | @type probe_options :: [ 23 | {:sample_rate, pos_integer} 24 | | {:tags, [String.t(), ...]} 25 | | {:report_interval, pos_integer} 26 | | {:sample_interval, pos_integer} 27 | | {:function, (() -> {:ok, state})} 28 | | {:mfa, {module(), atom(), [term()]}} 29 | | {:module, module} 30 | | {:keys, [atom]} 31 | ] 32 | 33 | @doc """ 34 | Called when the probe is created. The callback is passed 35 | the name of the probe, what kind of metric it's producing and the options 36 | the probe was created with. 37 | 38 | You must return `{:ok, state}`. The state will be passed back to you on 39 | subsequent callbacks. Any other return values will cancel further 40 | execution of the probe. 41 | """ 42 | @callback probe_init(String.t(), probe_type, probe_options) :: {:ok, state} 43 | 44 | @doc """ 45 | Called every `sample_interval` milliseconds. When called, the probe should 46 | perform its measurement and update its internal state. 47 | 48 | You must return `{:ok, state}`. Any other return values will cancel further 49 | execution of the probe. 50 | """ 51 | @callback probe_sample(state) :: {:ok, state} 52 | 53 | @doc """ 54 | Called at least every `report_interval` milliseconds. This call reads the 55 | value of the probe, which is reported to the underlying statistics system. 56 | 57 | Return values can either take the form of a single numeric value, or a 58 | keyword list keys -> numeric values. Nil values won't be reported to the 59 | statistics system. 60 | """ 61 | @callback probe_get_value(state) :: {:ok, probe_value} 62 | 63 | @doc """ 64 | Resets the probe's state. 65 | 66 | You must return `{:ok, state}`. Any other return values will cancel further 67 | execution of the probe. 68 | """ 69 | @callback probe_reset(state) :: {:ok, state} 70 | 71 | @doc """ 72 | Called when the probe's runner process receives an unknown message. 73 | 74 | You must return `{:ok, state}`. Any other return values will cancel further 75 | execution of the probe. 76 | """ 77 | @callback probe_handle_message(any, state) :: {:ok, state} 78 | 79 | alias Instruments.Probe.Definitions 80 | 81 | defdelegate define(name, type, options), to: Definitions 82 | defdelegate define!(name, type, options), to: Definitions 83 | end 84 | -------------------------------------------------------------------------------- /bench/results/fast_counter/increment/strategy-3.txt: -------------------------------------------------------------------------------- 1 | v3 Fast Counter Strategy 2 | 3 | Same strategy as v2 but code was changed to extract the table_key building into an inline function 4 | 5 | Output of Benchmark 6 | 7 | Operating System: macOS 8 | CPU Information: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz 9 | Number of Available Cores: 16 10 | Available memory: 64 GB 11 | Elixir 1.7.4 12 | Erlang 21.3.8.10 13 | 14 | Benchmark suite executing with the following configuration: 15 | warmup: 2 s 16 | time: 5 s 17 | memory time: 0 ns 18 | parallel: 1 19 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 20 | Estimated total run time: 42 s 21 | 22 | Benchmarking increment with input 1. No Options... 23 | Benchmarking increment with input 2. No Tags... 24 | Benchmarking increment with input 3. Empty Tags... 25 | Benchmarking increment with input 4. One Tag... 26 | Benchmarking increment with input 5. Five Tags... 27 | Benchmarking increment with input 6. Ten Tags... 28 | 29 | ##### With input 1. No Options ##### 30 | Name ips average deviation median 99th % 31 | increment 47.05 K 21.25 μs ±37.14% 19.98 μs 60.98 μs 32 | 33 | Compared to v2 IPS : +0.55K ✅ 34 | Compared to v2 Average: -0.26 μs ❌ 35 | Compared to v2 Median : +1.00 μs ✅ 36 | Compared to v2 99th % : -2.00 μs ✅ 37 | 38 | ##### With input 2. No Tags ##### 39 | Name ips average deviation median 99th % 40 | increment 36.19 K 27.63 μs ±31.73% 25.98 μs 74.98 μs 41 | 42 | Compared to v2 IPS : +1.23K ✅ 43 | Compared to v2 Average: -1.05 μs ✅ 44 | Compared to v2 Median : +/- 0 μs ✅ 45 | Compared to v2 99th % : -5.00 μs ✅ 46 | 47 | ##### With input 3. Empty Tags ##### 48 | Name ips average deviation median 99th % 49 | increment 32.72 K 30.57 μs ±29.42% 28.98 μs 78.98 μs 50 | 51 | Compared to v2 IPS : +2.10K ✅ 52 | Compared to v2 Average: -2.09 μs ✅ 53 | Compared to v2 Median : +/- 0 μs ✅ 54 | Compared to v2 99th % : -13.00 μs ✅ 55 | 56 | ##### With input 4. One Tag ##### 57 | Name ips average deviation median 99th % 58 | increment 29.47 K 33.93 μs ±31.08% 30.98 μs 87.98 μs 59 | 60 | Compared to v2 IPS : +0.87K ✅ 61 | Compared to v2 Average: -1.04 μs ✅ 62 | Compared to v2 Median : -1.00 μs ✅ 63 | Compared to v2 99th % : -9.00 μs ✅ 64 | 65 | ##### With input 5. Five Tags ##### 66 | Name ips average deviation median 99th % 67 | increment 13.81 K 72.42 μs ±22.17% 68.98 μs 157.98 μs 68 | 69 | Compared to v2 IPS : +1.75K ✅ 70 | Compared to v2 Average: -10.47 μs ✅ 71 | Compared to v2 Median : -3.00 μs ✅ 72 | Compared to v2 99th % : -47.00 μs ✅ 73 | 74 | ##### With input 6. Ten Tags ##### 75 | Name ips average deviation median 99th % 76 | increment 7.68 K 130.14 μs ±25.45% 118.98 μs 287.98 μs 77 | 78 | Compared to v2 IPS : +0.27K ✅ 79 | Compared to v2 Average: -4.88 μs ✅ 80 | Compared to v2 Median : +/- 0 μs ✅ 81 | Compared to v2 99th % : -32.00 μs ✅ 82 | -------------------------------------------------------------------------------- /bench/fast_counter/tag_handling.exs: -------------------------------------------------------------------------------- 1 | keyword_merge = fn input -> 2 | for _ <- 1..100 do 3 | case Keyword.get(input, :tags) do 4 | tags when is_list(tags) -> 5 | {:test, Keyword.merge(input, tags: Enum.sort(tags))} 6 | 7 | _ -> 8 | {:test, input} 9 | end 10 | end 11 | end 12 | 13 | keyword_merge_special = fn input -> 14 | for _ <- 1..100 do 15 | case Keyword.get(input, :tags) do 16 | [] -> 17 | {:test, input} 18 | 19 | [_] -> 20 | {:test, input} 21 | 22 | tags when is_list(tags) -> 23 | {:test, Keyword.merge(input, tags: Enum.sort(tags))} 24 | 25 | _ -> 26 | {:test, input} 27 | end 28 | end 29 | end 30 | 31 | keyword_pop_and_put = fn input -> 32 | for _ <- 1..100 do 33 | case Keyword.pop(input, :tags) do 34 | {tags, input} when is_list(tags) -> 35 | {:test, Keyword.put(input, :tags, Enum.sort(tags))} 36 | 37 | _ -> 38 | {:test, input} 39 | end 40 | end 41 | end 42 | 43 | keyword_pop_and_put_special = fn input -> 44 | for _ <- 1..100 do 45 | case Keyword.pop(input, :tags) do 46 | {[], _} -> 47 | {:test, input} 48 | 49 | {[_], _} -> 50 | {:test, input} 51 | 52 | {tags, input} when is_list(tags) -> 53 | {:test, Keyword.put(input, :tags, Enum.sort(tags))} 54 | 55 | _ -> 56 | {:test, input} 57 | end 58 | end 59 | end 60 | 61 | keyword_replace = fn input -> 62 | for _ <- 1..100 do 63 | case Keyword.get(input, :tags) do 64 | tags when is_list(tags) -> 65 | {:test, Keyword.replace!(input, :tags, Enum.sort(tags))} 66 | 67 | _ -> 68 | {:test, input} 69 | end 70 | end 71 | end 72 | 73 | keyword_replace_special = fn input -> 74 | for _ <- 1..100 do 75 | case Keyword.get(input, :tags) do 76 | [] -> 77 | {:test, input} 78 | 79 | [_] -> 80 | {:test, input} 81 | 82 | tags when is_list(tags) -> 83 | {:test, Keyword.replace!(input, :tags, Enum.sort(tags))} 84 | 85 | _ -> 86 | {:test, input} 87 | end 88 | end 89 | end 90 | 91 | 92 | Benchee.run( 93 | %{ 94 | "Keyword.get/2 + Keyword.merge/2" => &keyword_merge.(&1), 95 | "Keyword.pop/2 + Keyword.put/3" => &keyword_pop_and_put.(&1), 96 | "Keyword.get/2 + Keyword.replace!/3" => &keyword_replace.(&1), 97 | "Keyword.get/2 + Keyword.merge/2 with Special Casing" => &keyword_merge_special.(&1), 98 | "Keyword.pop/2 + Keyword.put/3 with Special Casing" => &keyword_pop_and_put_special.(&1), 99 | "Keyword.get/2 + Keyword.replace!/3 with Special Casing" => &keyword_replace_special.(&1), 100 | }, 101 | inputs: %{ 102 | "1. No Options" => [], 103 | "2. No Tags" => [sample_rate: 1.0], 104 | "3. Empty Tags" => [sample_rate: 1.0, tags: []], 105 | "4. One Tag" => [sample_rate: 1.0, tags: ["test:tag"]], 106 | "5. Five Tags" => [sample_rate: 1.0, tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag"]], 107 | "6. Ten Tags" => [sample_rate: 1.0, tags: ["test-1:tag", "test-2:tag", "test-3:tag", "test-4:tag", "test-5:tag", "test-6:tag", "test-7:tag", "test-8:tag", "test-9:tag", "test-10:tag"]] 108 | } 109 | ) 110 | -------------------------------------------------------------------------------- /test/support/fake_statsd.ex: -------------------------------------------------------------------------------- 1 | defmodule FakeStatsd do 2 | @moduledoc """ 3 | A fake stats server. 4 | 5 | This statsd server will parse incoming statsd calls and forward them to the process 6 | send into its start_link function. 7 | """ 8 | use GenServer 9 | 10 | def start_link(test_process) do 11 | GenServer.start_link(__MODULE__, [test_process], name: __MODULE__) 12 | end 13 | 14 | def init([test_process]) do 15 | {:ok, sock} = :gen_udp.open(Instruments.statsd_port(), [:binary, active: true, reuseaddr: true]) 16 | {:ok, {test_process, sock}} 17 | end 18 | 19 | def handle_info({:udp, socket, _ip, _port_info, packet}, {test_process, socket}) do 20 | send(test_process, {:metric_reported, decode(packet)}) 21 | 22 | {:noreply, {test_process, socket}} 23 | end 24 | 25 | defp decode(packet_bytes) do 26 | packet_bytes 27 | |> String.split("|") 28 | |> do_decode 29 | end 30 | 31 | defp do_decode([name_and_val, type | rest]) do 32 | opts = decode_tags_and_sampling(rest) 33 | {name, val} = decode_name_and_value(name_and_val) 34 | do_decode(name, val, type, opts) 35 | end 36 | 37 | defp do_decode(name, val, "g", opts) do 38 | {:gauge, name, to_number(val), opts} 39 | end 40 | 41 | defp do_decode(name, val, "ms", opts) do 42 | {:timing, name, to_number(val), opts} 43 | end 44 | 45 | defp do_decode(name, val, "s", opts) do 46 | {:set, name, to_number(val), opts} 47 | end 48 | 49 | defp do_decode(name, val, "h", opts) do 50 | {:histogram, name, to_number(val), opts} 51 | end 52 | 53 | defp do_decode(name, val, "c", opts) do 54 | {type, numeric_val} = 55 | case to_number(val) do 56 | v when v >= 0 -> 57 | {:increment, v} 58 | 59 | v -> 60 | {:decrement, -v} 61 | end 62 | 63 | {type, name, numeric_val, opts} 64 | end 65 | 66 | defp do_decode(:event, name, val, opts) do 67 | {:event, name, val, opts} 68 | end 69 | 70 | defp decode_tags_and_sampling(tags_and_sampling), 71 | do: decode_tags_and_sampling(tags_and_sampling, []) 72 | 73 | defp decode_tags_and_sampling([], accum) do 74 | accum 75 | end 76 | 77 | defp decode_tags_and_sampling([<<"#", tags::binary>> | rest], accum) do 78 | tag_list = String.split(tags, ",") 79 | decode_tags_and_sampling(rest, Keyword.put(accum, :tags, tag_list)) 80 | end 81 | 82 | defp decode_tags_and_sampling([<<"@", sampling::binary>> | rest], accum) do 83 | sample_rate = String.to_float(sampling) 84 | decode_tags_and_sampling(rest, Keyword.put(accum, :sample_rate, sample_rate)) 85 | end 86 | 87 | defp decode_name_and_value(<<"_e", rest::binary>>) do 88 | [_lengths, title] = String.split(rest, ":") 89 | 90 | {:event, title} 91 | end 92 | 93 | defp decode_name_and_value(name_and_val) do 94 | [name, value] = String.split(name_and_val, ":") 95 | {name, value} 96 | end 97 | 98 | defp to_number(s) do 99 | with {int_val, ""} <- Integer.parse(s) do 100 | int_val 101 | else 102 | _ -> 103 | case Float.parse(s) do 104 | {float_val, ""} -> 105 | float_val 106 | 107 | _ -> 108 | s 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /bench/results/fast_counter/increment/strategy-2.txt: -------------------------------------------------------------------------------- 1 | v2 Fast Counter Strategy 2 | 3 | Minor Changes to how `increment/3` works 4 | 5 | - Add a special case function for when no options are passed that avoids the more complex table key logic 6 | - Use the faster tag handling strategy, see the tag_handling/analysis.txt 7 | 8 | Output of Benchmark 9 | 10 | Operating System: macOS 11 | CPU Information: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz 12 | Number of Available Cores: 16 13 | Available memory: 64 GB 14 | Elixir 1.7.4 15 | Erlang 21.3.8.10 16 | 17 | Benchmark suite executing with the following configuration: 18 | warmup: 2 s 19 | time: 5 s 20 | memory time: 0 ns 21 | parallel: 1 22 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 23 | Estimated total run time: 42 s 24 | 25 | Benchmarking increment with input 1. No Options... 26 | Benchmarking increment with input 2. No Tags... 27 | Benchmarking increment with input 3. Empty Tags... 28 | Benchmarking increment with input 4. One Tag... 29 | Benchmarking increment with input 5. Five Tags... 30 | Benchmarking increment with input 6. Ten Tags... 31 | 32 | ##### With input 1. No Options ##### 33 | Name ips average deviation median 99th % 34 | increment 46.50 K 21.51 μs ±40.73% 18.98 μs 62.98 μs 35 | 36 | Compared to v1 IPS : +4.63K ✅ 37 | Compared to v1 Average: -2.38 μs ✅ 38 | Compared to v1 Median : -3.02 μs ✅ 39 | Compared to v1 99th % : -7.02 μs ✅ 40 | 41 | ##### With input 2. No Tags ##### 42 | Name ips average deviation median 99th % 43 | increment 34.87 K 28.68 μs ±36.49% 25.98 μs 79.98 μs 44 | 45 | Compared to v1 IPS : +0.70K ✅ 46 | Compared to v1 Average: -0.58 μs ✅ 47 | Compared to v1 Median : +0.90 μs ❌ 48 | Compared to v1 99th % : -7.02 μs ✅ 49 | 50 | ##### With input 3. Empty Tags ##### 51 | Name ips average deviation median 99th % 52 | increment 30.62 K 32.66 μs ±38.21% 28.98 μs 91.98 μs 53 | 54 | Compared to v1 IPS : +11.43K ✅ 55 | Compared to v1 Average: -19.46 μs ✅ 56 | Compared to v1 Median : -16.02 μs ✅ 57 | Compared to v1 99th % : -48.02 μs ✅ 58 | 59 | ##### With input 4. One Tag ##### 60 | Name ips average deviation median 99th % 61 | increment 28.60 K 34.97 μs ±36.25% 31.98 μs 96.98 μs 62 | 63 | Compared to v1 IPS : +10.76K ✅ 64 | Compared to v1 Average: -21.08 μs ✅ 65 | Compared to v1 Median : -16.02 μs ✅ 66 | Compared to v1 99th % : -53.02 μs ✅ 67 | 68 | ##### With input 5. Five Tags ##### 69 | Name ips average deviation median 99th % 70 | increment 12.06 K 82.89 μs ±35.25% 71.98 μs 204.98 μs 71 | 72 | Compared to v1 IPS : +0.91K ✅ 73 | Compared to v1 Average: -6.83 μs ✅ 74 | Compared to v1 Median : -6.02 μs ✅ 75 | Compared to v1 99th % : -18.02 μs ✅ 76 | 77 | ##### With input 6. Ten Tags ##### 78 | Name ips average deviation median 99th % 79 | increment 7.41 K 135.02 μs ±30.81% 118.98 μs 319.98 μs 80 | 81 | Compared to v1 IPS : +0.83K ✅ 82 | Compared to v1 Average: -16.95 μs ✅ 83 | Compared to v1 Median : -15.02 μs ✅ 84 | Compared to v1 99th % : -33.02 μs ✅ -------------------------------------------------------------------------------- /test/custom_functions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Instruments.CustomFunctionsTest do 2 | use ExUnit.Case 3 | alias Instruments.CustomFunctions 4 | import MetricsAssertions 5 | 6 | use Instruments 7 | 8 | setup do 9 | {:ok, _fake_statsd} = FakeStatsd.start_link(self()) 10 | :ok 11 | end 12 | 13 | defmodule Custom do 14 | use CustomFunctions, prefix: "custom" 15 | end 16 | 17 | describe "adding a prefix" do 18 | test "to increment calls" do 19 | Custom.increment("foo.bar.baz") 20 | assert_metric_reported(:increment, "custom.foo.bar.baz", 1) 21 | 22 | Custom.increment("foo.bar.baz", 3) 23 | assert_metric_reported(:increment, "custom.foo.bar.baz", 3) 24 | 25 | Custom.increment("foo.bar.baz", 4, tags: ["stinky"]) 26 | assert_metric_reported(:increment, "custom.foo.bar.baz", 4, tags: ["stinky"]) 27 | end 28 | 29 | test "to decrement calls" do 30 | Custom.decrement("foo.bar.bax") 31 | assert_metric_reported(:decrement, "custom.foo.bar.bax", 1) 32 | 33 | Custom.decrement("foo.bar.bax", 3) 34 | assert_metric_reported(:decrement, "custom.foo.bar.bax", 3) 35 | 36 | Custom.decrement("foo.bar.baz", 4, tags: ["stinky"]) 37 | assert_metric_reported(:decrement, "custom.foo.bar.baz", 4, tags: ["stinky"]) 38 | end 39 | 40 | test "to gauge calls" do 41 | Custom.gauge("my.gauge", 384) 42 | assert_metric_reported(:gauge, "custom.my.gauge", 384) 43 | 44 | Custom.gauge("my.gauge", 946, tags: ["sweet_gauge"]) 45 | assert_metric_reported(:gauge, "custom.my.gauge", 946, tags: ["sweet_gauge"]) 46 | end 47 | 48 | test "to histogram calls" do 49 | Custom.histogram("my.histogram", 900, sample_rate: 1.0) 50 | assert_metric_reported(:histogram, "custom.my.histogram", 900) 51 | 52 | Custom.histogram("my.histogram", 901, tags: ["cool_metric"], sample_rate: 1.0) 53 | assert_metric_reported(:histogram, "custom.my.histogram", 901, tags: ["cool_metric"]) 54 | end 55 | 56 | test "to timing calls" do 57 | Custom.timing("my.timing", 900, sample_rate: 1.0) 58 | assert_metric_reported(:timing, "custom.my.timing", 900) 59 | 60 | Custom.timing("my.timing", 901, tags: ["speed:fast"], sample_rate: 1.0) 61 | assert_metric_reported(:timing, "custom.my.timing", 901, tags: ["speed:fast"]) 62 | end 63 | 64 | test "to set calls" do 65 | Custom.set("my.set", 900) 66 | assert_metric_reported(:set, "custom.my.set", 900) 67 | 68 | Custom.set("my.set", 901, tags: ["speed:fast"]) 69 | assert_metric_reported(:set, "custom.my.set", 901, tags: ["speed:fast"]) 70 | end 71 | 72 | test "to measure_calls" do 73 | func = fn -> 74 | :timer.sleep(10) 75 | :done 76 | end 77 | 78 | assert :done == Custom.measure("my.measure", [sample_rate: 1.0], func) 79 | assert_metric_reported(:timing, "custom.my.measure", 10..12) 80 | 81 | assert :done == 82 | Custom.measure("my.measure", [sample_rate: 1.0, tags: ["timing:short"]], func) 83 | 84 | assert_metric_reported(:timing, "custom.my.measure", 10..11, tags: ["timing:short"]) 85 | end 86 | end 87 | 88 | test "setting a runtime prefix" do 89 | defmodule RuntimePrefix do 90 | use CustomFunctions, prefix: Application.get_env(:instruments, :custom_prefix, "foobar") 91 | end 92 | 93 | RuntimePrefix.increment("foo.bar", 3) 94 | assert_metric_reported(:increment, "foobar.foo.bar", 3) 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/macro_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.MacroHelpers do 2 | @moduledoc false 3 | 4 | alias Instruments.RateTracker 5 | 6 | @safe_metric_types [:increment, :decrement, :gauge, :event, :set] 7 | 8 | @metrics_module Application.get_env(:instruments, :reporter_module, Instruments.Statix) 9 | 10 | def build_metric_macro(:measure, caller, metrics_module, key_ast, options_ast, function) do 11 | key = to_iolist(key_ast, caller) 12 | 13 | quote do 14 | safe_opts = unquote(to_safe_options(:measure, options_ast)) 15 | Instruments.MacroHelpers.maybe_track(unquote(key), unquote(metrics_module), safe_opts) 16 | unquote(metrics_module).measure(unquote(key), safe_opts, unquote(function)) 17 | end 18 | end 19 | 20 | def build_metric_macro(type, caller, metrics_module, key_ast, value_ast, options_ast) do 21 | key = to_iolist(key_ast, caller) 22 | 23 | quote do 24 | safe_opts = unquote(to_safe_options(type, options_ast)) 25 | Instruments.MacroHelpers.maybe_track(unquote(key), unquote(metrics_module), safe_opts) 26 | unquote(metrics_module).unquote(type)(unquote(key), unquote(value_ast), safe_opts) 27 | end 28 | end 29 | 30 | @doc """ 31 | Transforms metric keys into iolists. A metric key can be: 32 | 33 | * A list, in which case it's let through unchanged 34 | * A static bitstring, which is let through unchanged 35 | * An interpolated bitstring, which is converted to an iolist 36 | where the interpolated variables are members 37 | * A concatenation operation, which is handled like an interpolated 38 | bitstring 39 | """ 40 | def to_iolist({var_name, [line: line], mod}, caller) when is_atom(var_name) and is_atom(mod) do 41 | raise CompileError, 42 | description: "Metric keys must be defined statically", 43 | line: line, 44 | file: caller.file 45 | end 46 | 47 | def to_iolist(metric, _) when is_bitstring(metric), 48 | do: metric 49 | 50 | def to_iolist(metric, _) when is_list(metric) do 51 | metric 52 | end 53 | 54 | def to_iolist(metric, _) do 55 | {_t, iolist} = Macro.postwalk(metric, [], &parse_iolist/2) 56 | 57 | Enum.reverse(iolist) 58 | end 59 | 60 | # Track if we're using a non-Fast* module. 61 | def maybe_track(key, @metrics_module, opts) do 62 | RateTracker.track(key, opts) 63 | end 64 | 65 | def maybe_track(_key, _module, _opts) do 66 | :ok 67 | end 68 | 69 | # Parses string literals 70 | defp parse_iolist(string_literal = ast, acc) when is_bitstring(string_literal), 71 | do: {ast, [string_literal | acc]} 72 | 73 | # This handles the `Kernel.to_string` call that string interpolation emits 74 | defp parse_iolist({{:., _ctx, [Kernel, :to_string]}, _, [_var]} = to_string_call, acc), 75 | do: {nil, [to_string_call | acc]} 76 | 77 | # this head handles string concatenation with <> 78 | defp parse_iolist({:<>, _, [left, right]}, _) do 79 | # this gets eventually reversed, so we concatenate them in reverse order 80 | {nil, [right, left]} 81 | end 82 | 83 | # If the ast fragment is unknown, return it and the accumulator; 84 | # it will eventually be built up into one of the above cases. 85 | defp parse_iolist(ast, accum), 86 | do: {ast, accum} 87 | 88 | defp to_safe_options(metric_type, options_ast) when metric_type in @safe_metric_types, 89 | do: options_ast 90 | 91 | defp to_safe_options(_metric_type, options_ast) do 92 | quote do 93 | Keyword.merge([sample_rate: 0.1], unquote(options_ast)) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"}, 3 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 4 | "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, 5 | "discord_statix": {:hex, :discord_statix, "1.5.1", "e0904aef402628beb91548f6e1b9d2f2a6923810605bc5a2ac56b22663c82610", [:mix], [], "hexpm", "dff9d1b114204fb7a49b429bdeaf24fe92576f63f2c756d9d89a3eadd2004a33"}, 6 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, 8 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 9 | "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, 10 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 11 | "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"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 14 | "recon": {:hex, :recon, "2.5.2", "cba53fa8db83ad968c9a652e09c3ed7ddcc4da434f27c3eaa9ca47ffb2b1ff03", [:mix, :rebar3], [], "hexpm", "2c7523c8dee91dff41f6b3d63cba2bd49eb6d2fe5bf1eec0df7f87eb5e230e1c"}, 15 | "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, 16 | "statix": {:hex, :discord_statix, "1.5.1", "e0904aef402628beb91548f6e1b9d2f2a6923810605bc5a2ac56b22663c82610", [:mix], [], "hexpm", "dff9d1b114204fb7a49b429bdeaf24fe92576f63f2c756d9d89a3eadd2004a33"}, 17 | } 18 | -------------------------------------------------------------------------------- /lib/probes/schedulers.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Probes.Schedulers do 2 | @moduledoc """ 3 | A probe that reports erlang's internal CPU usage 4 | 5 | Any good system monitoring needs to understand how hard the CPU is working. In 6 | an Erlang ecosystem, this can be somewhat challenging becase when an Erlang 7 | system isn't busy, the BEAM vm keeps its schedulers in tight loops so they don't get 8 | descheduled by the operating system. This can make external CPU metrics like `top` 9 | report that the system is actually much busier than it is. 10 | 11 | This module reports Erlang's internal view of its scheduler utilization and is 12 | a better gauge of how loaded your system is. It reports two values, the total 13 | utilization, and a [weighted utilization](http://erlang.org/doc/man/erlang.html#statistics_scheduler_wall_time), 14 | which can be used as a proxy for CPU usage. 15 | 16 | To use this probe, add the following function somewhwere in your application's 17 | initialization: 18 | 19 | alias Instruments 20 | Probe.define!("erlang.scheduler_utilization", :gauge, module: Probes.Schedulers, keys: ~w(weighted total)) 21 | 22 | The probe will now report two metrics, `erlang.scheduler_utilization.total` and `erlang.scheduler_utilization.total`. 23 | """ 24 | alias Instruments.Probe 25 | 26 | @behaviour Probe 27 | 28 | # Probe behaviour callbacks 29 | 30 | @doc false 31 | def behaviour(), do: :probe 32 | 33 | @doc false 34 | def probe_init(_name, _type, _options) do 35 | :erlang.system_flag(:scheduler_wall_time, true) 36 | wall_time = calculate_wall_time() 37 | {:ok, %{wall_time: wall_time, old_wall_time: wall_time}} 38 | end 39 | 40 | @doc false 41 | def probe_get_value(%{wall_time: new_wall_time, old_wall_time: old_wall_time}) do 42 | {active, total} = 43 | old_wall_time 44 | |> Enum.zip(new_wall_time) 45 | |> Enum.reduce({0, 0}, fn {{_, old_active, old_total}, {_, new_active, new_total}}, 46 | {active, total} -> 47 | {active + (new_active - old_active), total + (new_total - old_total)} 48 | end) 49 | 50 | # this alogrithm taken from http://erlang.org/doc/man/erlang.html#statistics_scheduler_wall_time 51 | stats = 52 | case total do 53 | 0 -> 54 | [weighted: 0.0, total: 0.0] 55 | 56 | _ -> 57 | total_scheduler_utilization = active / total 58 | 59 | weighted_utilization = 60 | total_scheduler_utilization * total_scheduler_count() / logical_processor_count() 61 | 62 | weighted_utilization_percent = Float.round(weighted_utilization * 100, 3) 63 | 64 | [ 65 | weighted: weighted_utilization_percent, 66 | total: Float.round(total_scheduler_utilization * 100, 3) 67 | ] 68 | end 69 | 70 | {:ok, stats} 71 | end 72 | 73 | @doc false 74 | def probe_reset(state), do: {:ok, state} 75 | 76 | @doc false 77 | def probe_sample(%{wall_time: old_wall_time} = state) do 78 | {:ok, %{state | old_wall_time: old_wall_time, wall_time: calculate_wall_time()}} 79 | end 80 | 81 | @doc false 82 | def probe_handle_message(_, state), do: {:ok, state} 83 | 84 | # end probe behaviour callbacks 85 | 86 | # Private 87 | defp calculate_wall_time() do 88 | :scheduler_wall_time 89 | |> :erlang.statistics() 90 | |> Enum.sort() 91 | end 92 | 93 | defp total_scheduler_count() do 94 | :erlang.system_info(:schedulers) + dirty_scheduler_count() 95 | end 96 | 97 | defp dirty_scheduler_count() do 98 | try do 99 | :erlang.system_info(:dirty_cpu_schedulers) 100 | rescue 101 | ArgumentError -> 102 | 0 103 | end 104 | end 105 | 106 | defp logical_processor_count() do 107 | case :erlang.system_info(:logical_processors_available) do 108 | :unknown -> 109 | :erlang.system_info(:logical_processors_online) 110 | 111 | proc_count when is_integer(proc_count) -> 112 | proc_count 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/fast_gauge.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.FastGauge do 2 | @moduledoc false 3 | # Instruments.FastGauge sets up one ETS table per scheduler, and calls to 4 | # `gauge/3` insert into the table for the current scheduler. 5 | # The key for the table entry is built out of the gauge name and options. 6 | # The value for the table entry includes the gauge value as well as the timestamp 7 | # it was recorded at. 8 | # 9 | # The Instruments.FastGauge process periodically reports the most recent value 10 | # for every table key, deleting entries that have been reported or ignored. 11 | @table_prefix :instruments_gauges 12 | @max_tables 128 13 | @report_interval_ms Application.compile_env( 14 | :instruments, 15 | :fast_gauge_report_interval, 16 | 10_000 17 | ) 18 | @report_jitter_range_ms Application.compile_env( 19 | :instruments, 20 | :fast_gauge_report_jitter_range, 21 | -500..500 22 | ) 23 | @compile {:inline, get_table_key: 2, latest_table_entry: 2} 24 | 25 | @type table_entry :: {gauge_value :: number(), recorded_timestamp :: pos_integer()} 26 | 27 | use GenServer 28 | 29 | def start_link(_ \\ []) do 30 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 31 | end 32 | 33 | def init(:ok) do 34 | table_count = :erlang.system_info(:schedulers) 35 | 36 | for scheduler_id <- 1..table_count do 37 | :ets.new(table_name(scheduler_id), [:named_table, :public, :set]) 38 | end 39 | 40 | reporter_module = Application.get_env(:instruments, :reporter_module, Instruments.Statix) 41 | 42 | schedule_report() 43 | {:ok, {reporter_module, table_count}} 44 | end 45 | 46 | ## Public 47 | 48 | @spec gauge(iodata, integer) :: :ok 49 | @spec gauge(iodata, integer, Statix.options()) :: :ok 50 | def gauge(name, value, options \\ []) do 51 | table_key = get_table_key(name, options) 52 | timestamp = System.monotonic_time() 53 | :ets.insert(current_table(), [{table_key, {value, timestamp}}]) 54 | end 55 | 56 | ## GenServer callbacks 57 | def handle_info(:report, {reporter_module, table_count} = state) do 58 | 1..table_count 59 | |> Enum.map(fn scheduler_id -> table_name(scheduler_id) end) 60 | |> Enum.reduce(%{}, fn table_name, acc -> 61 | table_results = 62 | table_name 63 | |> :ets.tab2list() 64 | |> Map.new() 65 | 66 | Enum.each(table_results, &:ets.delete_object(table_name, &1)) 67 | 68 | Map.merge(acc, table_results, fn _key, table_entry_old, table_entry_new -> 69 | latest_table_entry(table_entry_old, table_entry_new) 70 | end) 71 | end) 72 | |> Enum.each(fn {table_key, {value, _recorded_timestamp}} -> 73 | report_stat({table_key, value}, reporter_module) 74 | end) 75 | 76 | schedule_report() 77 | {:noreply, state} 78 | end 79 | 80 | ## Private 81 | defp current_table() do 82 | table_name(:erlang.system_info(:scheduler_id)) 83 | end 84 | 85 | defp get_table_key(name, []) do 86 | {name, []} 87 | end 88 | 89 | defp get_table_key(name, options) do 90 | case Keyword.get(options, :tags) do 91 | [] -> 92 | {name, options} 93 | 94 | [_] -> 95 | {name, options} 96 | 97 | tags when is_list(tags) -> 98 | {name, Keyword.replace!(options, :tags, Enum.sort(tags))} 99 | 100 | _ -> 101 | {name, options} 102 | end 103 | end 104 | 105 | @spec latest_table_entry(table_entry(), table_entry()) :: table_entry() 106 | defp latest_table_entry( 107 | {_gauge_value_left, recorded_timestamp_left} = left, 108 | {_gauge_value_right, recorded_timestamp_right} 109 | ) 110 | when recorded_timestamp_left > recorded_timestamp_right do 111 | left 112 | end 113 | 114 | defp latest_table_entry(_left, right) do 115 | right 116 | end 117 | 118 | defp report_stat({{metric_name, opts}, value}, reporter_module) do 119 | reporter_module.gauge(metric_name, value, opts) 120 | end 121 | 122 | defp schedule_report() do 123 | wait_time = @report_interval_ms + Enum.random(@report_jitter_range_ms) 124 | Process.send_after(self(), :report, wait_time) 125 | end 126 | 127 | for scheduler_id <- 1..@max_tables do 128 | defp table_name(unquote(scheduler_id)) do 129 | unquote(:"#{@table_prefix}_#{scheduler_id}") 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/probe/definitions.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Probe.Definitions do 2 | @moduledoc false 3 | 4 | use GenServer 5 | alias Instruments.Probe 6 | alias Instruments.Probe.Errors 7 | 8 | @type definition_errors :: {:error, {:probe_names_taken, [String.t()]}} 9 | @type definition_response :: {:ok, [String.t()]} | definition_errors 10 | 11 | @probe_prefix Application.get_env(:instruments, :probe_prefix) 12 | @table_name :probe_definitions 13 | 14 | def start_link(_ \\ []), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) 15 | 16 | def init([]) do 17 | table_name = @table_name 18 | ^table_name = :ets.new(table_name, [:named_table, :set, :protected, read_concurrency: true]) 19 | {:ok, nil} 20 | end 21 | 22 | @doc """ 23 | Defines a probe. If the definition fails, an exception is thrown. 24 | @see define/3 25 | """ 26 | @spec define!(String.t(), Probe.probe_type(), Probe.probe_options()) :: [String.t()] 27 | def define!(name, type, options) do 28 | case define(name, type, options) do 29 | {:ok, probe_names} -> 30 | probe_names 31 | 32 | {:error, {:probe_names_taken, taken_names}} -> 33 | raise Errors.ProbeNameTakenError.exception(taken_names: taken_names) 34 | end 35 | end 36 | 37 | @doc """ 38 | Defines a probe. 39 | The probe type can be: 40 | 41 | * `gauge`: A single emitted value 42 | * `counter`: A value that's incremented or decremeted over time. 43 | If the value is negative, a decrement command is issued, 44 | otherwise an increment command is executed. 45 | * `histogram`: A value combined into a series and then listed as percentages. 46 | * `timing`: A millisecond timing value. 47 | 48 | Returns `{:ok, [probe_name]}` or `{:error, reason}`. 49 | """ 50 | @spec define(String.t(), Probe.probe_type(), Probe.probe_options()) :: definition_response 51 | def define(base_name, type, options) do 52 | name = to_probe_name(@probe_prefix, base_name) 53 | 54 | defn_fn = fn -> 55 | cond do 56 | Keyword.has_key?(options, :function) -> 57 | Probe.Supervisor.start_probe(name, type, options, Probe.Function) 58 | 59 | Keyword.has_key?(options, :mfa) -> 60 | {{module, fun, args}, options} = Keyword.pop(options, :mfa) 61 | probe_fn = fn -> :erlang.apply(module, fun, args) end 62 | options = Keyword.put(options, :function, probe_fn) 63 | 64 | Probe.Supervisor.start_probe(name, type, options, Probe.Function) 65 | 66 | Keyword.has_key?(options, :module) -> 67 | probe_module = Keyword.get(options, :module) 68 | Probe.Supervisor.start_probe(name, type, options, probe_module) 69 | end 70 | end 71 | 72 | definitions = 73 | case Keyword.get(options, :keys) do 74 | keys when is_list(keys) -> 75 | Enum.map(keys, fn key -> "#{name}.#{key}" end) 76 | 77 | nil -> 78 | [name] 79 | end 80 | 81 | unique_names = unique_names(definitions, options) 82 | 83 | GenServer.call(__MODULE__, {:define, unique_names, defn_fn}) 84 | end 85 | 86 | def handle_call({:define, probe_names, transaction}, _from, _) do 87 | response = 88 | case used_probe_names(probe_names) do 89 | [] -> 90 | added_probes = 91 | Enum.map(probe_names, fn probe_name -> 92 | true = :ets.insert_new(@table_name, {probe_name, probe_name}) 93 | probe_name 94 | end) 95 | 96 | transaction.() 97 | {:ok, added_probes} 98 | 99 | used_probe_names -> 100 | {:error, {:probe_names_taken, used_probe_names}} 101 | end 102 | 103 | {:reply, response, nil} 104 | end 105 | 106 | @spec unique_names([String.t()], Probe.probe_options()) :: [String.t()] 107 | defp unique_names(probe_names, options) do 108 | case Keyword.get(options, :tags) do 109 | tags when is_list(tags) -> 110 | tag_string = Enum.join(Enum.sort(tags), ",") 111 | for probe_name <- probe_names do 112 | "#{probe_name}.tags:#{tag_string}" 113 | end 114 | 115 | nil -> 116 | probe_names 117 | end 118 | end 119 | 120 | @spec used_probe_names([String.t()]) :: [String.t()] 121 | defp used_probe_names(probe_names) do 122 | probe_names 123 | |> Enum.map(&:ets.match(@table_name, {&1, :"$1"})) 124 | |> List.flatten() 125 | end 126 | 127 | def to_probe_name(nil, base_name), do: base_name 128 | def to_probe_name(probe_prefix, base_name), do: "#{probe_prefix}.#{base_name}" 129 | end 130 | -------------------------------------------------------------------------------- /lib/fast_counter.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.FastCounter do 2 | @moduledoc false 3 | 4 | # A Faster than normal counter. 5 | 6 | # Builds one ETS table per scheduler in the system and sends increment / decrement writes to the local 7 | # scheduler. 8 | # Statistics are reported per scheduler once every `fast_counter_report_interval` milliseconds, with a 9 | # random jitter in the range of `fast_counter_report_jitter_range` milliseconds. 10 | 11 | @table_prefix :instruments_counters 12 | @max_tables 128 13 | @report_interval_ms Application.get_env( 14 | :instruments, 15 | :fast_counter_report_interval, 16 | 10_000 17 | ) 18 | @report_jitter_range_ms Application.get_env( 19 | :instruments, 20 | :fast_counter_report_jitter_range, 21 | -500..500 22 | ) 23 | @compile {:inline, get_table_key: 2} 24 | 25 | use GenServer 26 | 27 | def start_link(_ \\ []) do 28 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 29 | end 30 | 31 | def init(:ok) do 32 | table_count = :erlang.system_info(:schedulers) 33 | 34 | for scheduler_id <- 1..table_count do 35 | :ets.new(table_name(scheduler_id), [:named_table, :public, :set]) 36 | end 37 | 38 | reporter_module = Application.get_env(:instruments, :reporter_module, Instruments.Statix) 39 | 40 | schedule_report() 41 | {:ok, {reporter_module, table_count}} 42 | end 43 | 44 | ## Public 45 | 46 | @spec increment(iodata) :: :ok 47 | @spec increment(iodata, integer) :: :ok 48 | @spec increment(iodata, integer, Statix.options()) :: :ok 49 | def increment(name, amount \\ 1, options \\ []) do 50 | table_key = get_table_key(name, options) 51 | :ets.update_counter(current_table(), table_key, amount, {table_key, 0}) 52 | :ok 53 | end 54 | 55 | @spec decrement(iodata) :: :ok 56 | @spec decrement(iodata, integer) :: :ok 57 | @spec decrement(iodata, integer, Statix.options()) :: :ok 58 | def decrement(name, amount \\ 1, options \\ []), 59 | do: increment(name, -amount, options) 60 | 61 | ## GenServer callbacks 62 | def handle_info(:report, {reporter_module, table_count} = state) do 63 | # dump the scheduler's data and decrement its 64 | # counters by the amount we dumped. 65 | dump_and_clear_data = fn scheduler_id -> 66 | table_name = table_name(scheduler_id) 67 | table_data = :ets.tab2list(table_name) 68 | 69 | Enum.each(table_data, fn {key, val} -> 70 | :ets.update_counter(table_name, key, -val) 71 | end) 72 | 73 | table_data 74 | end 75 | 76 | # aggregates each scheduler's table into one metric 77 | aggregate_stats = fn {key, val}, acc -> 78 | Map.update(acc, key, val, &(&1 + val)) 79 | end 80 | 81 | 1..table_count 82 | |> Enum.flat_map(dump_and_clear_data) 83 | |> Enum.reduce(%{}, aggregate_stats) 84 | |> Enum.each(&report_stat(&1, reporter_module)) 85 | 86 | schedule_report() 87 | {:noreply, state} 88 | end 89 | 90 | ## Private 91 | 92 | defp get_table_key(name, []) do 93 | {name, []} 94 | end 95 | 96 | defp get_table_key(name, options) do 97 | case Keyword.get(options, :tags) do 98 | [] -> 99 | {name, options} 100 | 101 | [_] -> 102 | {name, options} 103 | 104 | tags when is_list(tags) -> 105 | {name, Keyword.replace!(options, :tags, Enum.sort(tags))} 106 | 107 | _ -> 108 | {name, options} 109 | end 110 | end 111 | 112 | defp report_stat({_key, 0}, _), 113 | do: :ok 114 | 115 | defp report_stat({{metric_name, opts}, value}, reporter_module) when value < 0 do 116 | # this -value looks like a bug, but isn't. Since we're aggregating 117 | # counters, the value could be negative, but the decrement 118 | # operation takes positive values. 119 | reporter_module.decrement(metric_name, -value, opts) 120 | end 121 | 122 | defp report_stat({{metric_name, opts}, value}, reporter_module) when value > 0 do 123 | reporter_module.increment(metric_name, value, opts) 124 | end 125 | 126 | defp schedule_report() do 127 | wait_time = @report_interval_ms + Enum.random(@report_jitter_range_ms) 128 | Process.send_after(self(), :report, wait_time) 129 | end 130 | 131 | defp current_table() do 132 | table_name(:erlang.system_info(:scheduler_id)) 133 | end 134 | 135 | for scheduler_id <- 1..@max_tables do 136 | defp table_name(unquote(scheduler_id)) do 137 | unquote(:"#{@table_prefix}_#{scheduler_id}") 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/sysmon/reporter.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Sysmon.Reporter do 2 | @moduledoc """ 3 | Since only one process can subscribe to system monitor events, the Reporter 4 | acts as a relay for system monitor events, allowing multiple subscribers to 5 | receive system monitor events. 6 | 7 | On startup, the Reporter will subscribe to the system monitor events 8 | configured in `:sysmon_events` in the `:instruments` application environment. 9 | If no events are configured, the Reporter will not subscribe to any events. 10 | """ 11 | use GenServer 12 | 13 | require Logger 14 | 15 | @type sysmon_event :: 16 | {:long_gc, pos_integer()} 17 | | {:long_schedule, pos_integer()} 18 | | {:large_heap, pos_integer()} 19 | | :busy_port 20 | | :busy_dist_port 21 | 22 | @type t :: %__MODULE__{ 23 | subscribers: %{reference() => pid()}, 24 | events: [sysmon_event()] 25 | } 26 | 27 | defstruct subscribers: Map.new(), events: [] 28 | 29 | def start_link(opts \\ []) do 30 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 31 | end 32 | 33 | @doc """ 34 | Subscribes the provided pid to configured system monitor events. 35 | """ 36 | @spec subscribe(pid()) :: :ok 37 | def subscribe(pid \\ self()) do 38 | GenServer.call(__MODULE__, {:subscribe, pid}) 39 | end 40 | 41 | @doc """ 42 | Unsubscribes the provided pid from system monitor events. 43 | """ 44 | @spec unsubscribe(pid()) :: :ok 45 | def unsubscribe(pid \\ self()) do 46 | GenServer.call(__MODULE__, {:unsubscribe, pid}) 47 | end 48 | 49 | @doc """ 50 | Sets the system monitor events to subscribe to. If no events are provided, the Reporter will not register itself as the system monitor process. 51 | """ 52 | @spec set_events([sysmon_event()]) :: :ok 53 | def set_events(events) do 54 | GenServer.call(__MODULE__, {:set_events, events}) 55 | end 56 | 57 | @doc """ 58 | Returns the system monitor events the Reporter is subscribed to. 59 | """ 60 | @spec get_events() :: [sysmon_event()] 61 | def get_events() do 62 | GenServer.call(__MODULE__, :get_events) 63 | end 64 | 65 | @impl true 66 | def init(_) do 67 | sysmon_events = Application.get_env(:instruments, :sysmon_events, []) 68 | enable_sysmon(sysmon_events) 69 | 70 | {:ok, 71 | %__MODULE__{ 72 | events: sysmon_events 73 | }} 74 | end 75 | 76 | @impl true 77 | def handle_call({:subscribe, pid}, _from, %__MODULE__{} = state) do 78 | existing = Map.values(state.subscribers) 79 | 80 | state = 81 | if Enum.member?(existing, pid) do 82 | state 83 | else 84 | ref = Process.monitor(pid) 85 | 86 | %__MODULE__{ 87 | state 88 | | subscribers: Map.put(state.subscribers, ref, pid) 89 | } 90 | end 91 | 92 | {:reply, :ok, state} 93 | end 94 | 95 | def handle_call({:unsubscribe, pid}, _from, %__MODULE__{} = state) do 96 | entries = Enum.filter(state.subscribers, fn {_, p} -> p == pid end) 97 | 98 | state = 99 | case entries do 100 | [{ref, _pid}] -> 101 | Process.demonitor(ref) 102 | 103 | %__MODULE__{ 104 | state 105 | | subscribers: Map.delete(state.subscribers, ref) 106 | } 107 | 108 | _ -> 109 | state 110 | end 111 | 112 | {:reply, :ok, state} 113 | end 114 | 115 | def handle_call({:set_events, events}, _from, %__MODULE__{} = state) do 116 | enable_sysmon(events) 117 | {:reply, :ok, %__MODULE__{state | events: events}} 118 | end 119 | 120 | def handle_call(:get_events, _from, %__MODULE__{} = state) do 121 | {:reply, state.events, state} 122 | end 123 | 124 | @impl true 125 | def handle_info({:DOWN, ref, :process, _pid, _reason}, %__MODULE__{} = state) do 126 | state = %__MODULE__{ 127 | state 128 | | subscribers: Map.delete(state.subscribers, ref) 129 | } 130 | 131 | {:noreply, state} 132 | end 133 | 134 | def handle_info(msg, %__MODULE__{} = state) do 135 | to_forward = 136 | case msg do 137 | {:monitor, pid, event, port} when event == :busy_dist_port or event == :busy_port -> 138 | {__MODULE__, event, 139 | %{ 140 | pid: pid, 141 | port: port 142 | }} 143 | 144 | {:monitor, pid, event, info} -> 145 | {__MODULE__, event, 146 | %{ 147 | pid: pid, 148 | info: info 149 | }} 150 | 151 | unknown -> 152 | {__MODULE__, :unknown, unknown} 153 | end 154 | 155 | Enum.each(state.subscribers, fn {_, pid} -> send(pid, to_forward) end) 156 | {:noreply, state} 157 | end 158 | 159 | defp enable_sysmon(nil) do 160 | enable_sysmon([]) 161 | end 162 | 163 | defp enable_sysmon([]) do 164 | :ok 165 | end 166 | 167 | defp enable_sysmon(events) do 168 | # Log if we're going to overwrite an existing system monitor 169 | our_pid = self() 170 | 171 | case :erlang.system_monitor() do 172 | :undefined -> 173 | # No system monitor is configured 174 | :ok 175 | 176 | {^our_pid, _} -> 177 | # We are already receiving system monitor events 178 | :ok 179 | 180 | {pid, _} -> 181 | # Another process is already receiving system monitor events, log a warning 182 | Logger.warn("Overwriting system monitor process: #{inspect(pid)}") 183 | end 184 | 185 | :erlang.system_monitor(our_pid, events) 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /test/instruments_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InstrumentsTest do 2 | use ExUnit.Case 3 | import MetricsAssertions 4 | 5 | use Instruments 6 | 7 | setup do 8 | FakeStatsd.start_link(self()) 9 | :ok 10 | end 11 | 12 | test "setting a gauge value" do 13 | Instruments.gauge("foo.bar", 3) 14 | assert_metric_reported(:gauge, "foo.bar") 15 | 16 | Instruments.gauge("foo.bar", 6, tags: ["my:tag"]) 17 | assert_metric_reported(:gauge, "foo.bar", 6, tags: ["my:tag"]) 18 | 19 | Instruments.gauge("foo.bar", 6, tags: ["my:tag"], sample_rate: 1.0) 20 | assert_metric_reported(:gauge, "foo.bar", 6, tags: ["my:tag"], sample_rate: 1.0) 21 | 22 | gauge_name = "fail_amount" 23 | Instruments.gauge("foo.bar.#{gauge_name}", 284) 24 | assert_metric_reported(:gauge, "foo.bar.fail_amount", 284) 25 | end 26 | 27 | test "incrementing a counter" do 28 | Instruments.increment("my.counter", 6) 29 | assert_metric_reported(:increment, "my.counter", 6) 30 | 31 | Instruments.increment("my.counter", 6, tags: ["my:counter_tag"]) 32 | assert_metric_reported(:increment, "my.counter", 6, tags: ["my:counter_tag"]) 33 | 34 | Instruments.increment("my.counter", 6, tags: ["my:counter_tag"], sample_rate: 1.0) 35 | 36 | assert_metric_reported(:increment, "my.counter", 6, tags: ["my:counter_tag"], sample_rate: 1.0) 37 | 38 | counter_name = :stinky 39 | Instruments.increment("my.counter.#{counter_name}", 6) 40 | assert_metric_reported(:increment, "my.counter.stinky", 6) 41 | end 42 | 43 | test "decrementing a counter" do 44 | Instruments.decrement("my.counter", 6) 45 | assert_metric_reported(:decrement, "my.counter", 6) 46 | 47 | Instruments.decrement("my.counter", 6, tags: ["my:counter_tag"]) 48 | assert_metric_reported(:decrement, "my.counter", 6, tags: ["my:counter_tag"]) 49 | 50 | Instruments.decrement("my.counter", 6, tags: ["my:counter_tag"], sample_rate: 1.0) 51 | 52 | assert_metric_reported(:decrement, "my.counter", 6, tags: ["my:counter_tag"], sample_rate: 1.0) 53 | 54 | counter_name = "decrementer" 55 | Instruments.decrement("my.#{counter_name}.requests", 9) 56 | assert_metric_reported(:decrement, "my.decrementer.requests", 9) 57 | end 58 | 59 | test "sending a timing metric" do 60 | Instruments.timing("my.timer", 3000, sample_rate: 1.0) 61 | assert_metric_reported(:timing, "my.timer", 3000, sample_rate: 1.0) 62 | 63 | Instruments.timing("my.timer", 3000, tags: ["timing:slow"], sample_rate: 1.0) 64 | assert_metric_reported(:timing, "my.timer", 3000, tags: ["timing:slow"], sample_rate: 1.0) 65 | 66 | Instruments.timing("my.timer", 3000, tags: ["timing:slow"], sample_rate: 1.0) 67 | assert_metric_reported(:timing, "my.timer", 3000, tags: ["timing:slow"], sample_rate: 1.0) 68 | 69 | rpc_name = "get_user" 70 | Instruments.timing("rpc.#{rpc_name}.response_time", 29, sample_rate: 1.0) 71 | assert_metric_reported(:timing, "rpc.get_user.response_time", 29) 72 | end 73 | 74 | test "measuring a timed metric" do 75 | pauser = fn -> 76 | :timer.sleep(10) 77 | end 78 | 79 | Instruments.measure("my_timed_metric", [sample_rate: 1.0], pauser) 80 | assert_metric_reported(:timing, "my_timed_metric", 10..15) 81 | 82 | Instruments.measure("my_timed_metric", [sample_rate: 1.0, tags: ["my:pause"]], pauser) 83 | assert_metric_reported(:timing, "my_timed_metric", 10..15, tags: ["my:pause"]) 84 | 85 | Instruments.measure("my_timed_metric", [tags: ["my:pause"], sample_rate: 1.0], pauser) 86 | 87 | assert_metric_reported(:timing, "my_timed_metric", 10..15, 88 | tags: ["my:pause"], 89 | sample_rate: 1.0 90 | ) 91 | 92 | rpc_name = "delete_user" 93 | Instruments.measure("rpc.#{rpc_name}", [sample_rate: 1.0], pauser) 94 | assert_metric_reported(:timing, "rpc.delete_user", 10..15) 95 | end 96 | 97 | test "setting a histogram" do 98 | Instruments.histogram("my.histogram", 29, sample_rate: 1.0) 99 | assert_metric_reported(:histogram, "my.histogram", 29) 100 | 101 | Instruments.histogram("my.histogram", 949, tags: ["rpc:call", "other:data"], sample_rate: 1.0) 102 | assert_metric_reported(:histogram, "my.histogram", 949, tags: ["rpc:call", "other:data"]) 103 | 104 | Instruments.histogram("my.histogram", 949, tags: ["rpc:call", "other:data"], sample_rate: 1.0) 105 | 106 | assert_metric_reported(:histogram, "my.histogram", 949, 107 | tags: ["rpc:call", "other:data"], 108 | sample_rate: 1.0 109 | ) 110 | 111 | histogram_name = "friend_count" 112 | Instruments.histogram("discord.users.#{histogram_name}", 29, sample_rate: 1.0) 113 | assert_metric_reported(:histogram, "discord.users.friend_count", 29) 114 | end 115 | 116 | test "setting a set value" do 117 | Instruments.set("my.set", 629) 118 | assert_metric_reported(:set, "my.set", 629) 119 | 120 | set_name = "custom_set" 121 | Instruments.set("discord.#{set_name}", 830) 122 | assert_metric_reported(:set, "discord.custom_set", 830) 123 | end 124 | 125 | test "sending events" do 126 | Instruments.send_event("my_title", "my text") 127 | 128 | assert_metric_reported(:event, "my_title", "my text") 129 | 130 | set_name = "dirty" 131 | Instruments.send_event("my_stuff.#{set_name}", "clothes") 132 | assert_metric_reported(:event, "my_stuff.dirty", "clothes") 133 | end 134 | 135 | test "sending events with tags" do 136 | Instruments.send_event("my_title", "my text", tags: ["host:any", "another:tag"]) 137 | assert_metric_reported(:event, "my_title", "my text", tags: ["host:any", "another:tag"]) 138 | end 139 | 140 | test "sending events with a title that's a variable blows up" do 141 | quoted = 142 | quote do 143 | use Instruments 144 | 145 | val = "43" 146 | interp = "this is my title #{val}" 147 | Instruments.send_event(interp, "my_text") 148 | end 149 | 150 | assert_raise CompileError, ~r/Metric keys must be defined statically/, fn -> 151 | Code.eval_quoted(quoted) 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instruments: Simple, powerful and fast metrics for Statsd and DataDog 2 | 3 | [![CI](https://github.com/discord/instruments/workflows/CI/badge.svg)](https://github.com/discord/instruments/actions) 4 | [![Hex.pm Version](http://img.shields.io/hexpm/v/instruments.svg?style=flat)](https://hex.pm/packages/instruments) 5 | [![Hex.pm License](http://img.shields.io/hexpm/l/instruments.svg?style=flat)](https://hex.pm/packages/instruments) 6 | [![HexDocs](https://img.shields.io/badge/HexDocs-Yes-blue)](https://hexdocs.pm/instruments) 7 | 8 | You're blind without metrics. Metrics should also be easy to add to you application 9 | and have little performance impact. This module allows you to define metrics 10 | with ease and see inside your application. 11 | 12 | Instruments has the following types of metrics that closely mirror statsd. 13 | 14 | * **Counters**: Allow you to increment or decrement a value. 15 | * **Gauges**: Allow you to report a single value that changes over time 16 | * **Histograms**: Values are grouped into percentiles 17 | * **Timings**: Report a timed value in milliseconds 18 | * **Measurements**: Measure the execution time of a function 19 | * **Sets**: Add a value to a statsd set 20 | * **Events**: Report an event like a deploy using arbitrary keys and values 21 | 22 | 23 | ## Basic Usage 24 | 25 | Reporting a metric is extremely simple; just `use` the Instruments module and call the 26 | appropriate function: 27 | 28 | ```elixir 29 | defmodule ModuleThatNeedsMetrics do 30 | use Instruments 31 | 32 | def other_function() do 33 | Process.sleep(150) 34 | end 35 | 36 | def metrics_function() do 37 | Instruments.increment("my.counter", 3) 38 | Instruments.measure("metrics_function.other_fn_call", &other_function/0) 39 | end 40 | end 41 | ``` 42 | 43 | ### Custom Namespaces 44 | Often, all metrics inside a module have namespaced metrics. This is easy to accomplish 45 | using `CustomFunctions` 46 | 47 | ```elixir 48 | defmodule RpcHandler do 49 | use Instruments.CustomFunctions, prefix: "my_service.rpc" 50 | 51 | def handle(:get, "/foo/bar") do 52 | increment("foo.bar") 53 | end 54 | end 55 | ``` 56 | 57 | The above example will increment the "my_service.rpc.foo.bar" metric by one. 58 | 59 | ## Probes 60 | A probe is a metric that's periodically updated, like memory usage. It can be 61 | tedious to define these on your own, so Instruments automates this process. 62 | There are several different ways to define a probe: 63 | 64 | The first, and easiest is to use the `:mfa` key, which takes a tuple of 65 | `{Module, function, arguments}` 66 | 67 | ```elixir 68 | Probe.define!("erlang.process_count", :gauge, 69 | mfa: {:erlang, :system_info, [:process_count]}) 70 | ``` 71 | 72 | The above will report the process count every ten seconds. 73 | You can also select keys from a value. For example, when reporting memory usage: 74 | 75 | ```elixir 76 | Probe.define("erlang.memory", :gauge, 77 | mfa: {:erlang, :memory, []}, 78 | keys: [:total, :processes]) 79 | ``` 80 | 81 | In the above example, the `:erlang.memory()` function will be called, and it returns a 82 | keyword list like: 83 | 84 | ```elixir 85 | [total: 19371280, processes: 4638128, processes_used: 4633792, system: 14733152, 86 | atom: 264529, atom_used: 250724, binary: 181960, code: 5843599, ets: 383504] 87 | ``` 88 | 89 | From this, the probe extracts the `:total` and `:processes` keys, creates two metrics, 90 | `erlang.memory.total` and `erlang.memory.processes` and reports them. 91 | 92 | You can also define probes via a passed in zero argument function. 93 | 94 | ```elixir 95 | Probe.define!("erlang.memory", :gauge, 96 | function: &:erlang.memory/0, 97 | keys: [:total, :processes]) 98 | ``` 99 | 100 | The above function simplifies the earlier mfa example, above, calling `:erlang.memory()` 101 | and extracting the `:total` and `:processes` keys. 102 | 103 | Finally, if this isn't enough flexibility, you can implement the `Probe` behaviour and 104 | pass in the module of your probe: 105 | 106 | ```elixir 107 | defmodule MyProbe do 108 | @behaviour Instruments.Probe 109 | # implementation of the callbacks 110 | end 111 | 112 | Probe.define!("my.probe", :gauge, module: MyProbe) 113 | ``` 114 | 115 | Your probe module will now experience lifecycle callbacks and can keep its own state. 116 | More information on the `Probe` behaviour is in the `Instruments.Probe` moduledoc. 117 | 118 | Probes also have two other options: 119 | 120 | * `report_interval`: (milliseconds) How often the probe is reported to the 121 | underlying stats package. 122 | 123 | * `sample_interval`: (milliseconds) How often the probe's data is collected. 124 | If not set, this defaults to the `report_interval`. 125 | 126 | ## Performance 127 | 128 | There are a couple optimizations that keep Instruments fast. 129 | 130 | #### ETS backed counters 131 | Probe counters actually increment or decrement a value in an ETS table, every 132 | `fast_counter_report_interval` milliseconds, the aggregated values are flushed to 133 | statsd. Because of this, counters are effectively free and with a conservative flush interval, 134 | will put little pressure on your statsd server. 135 | 136 | #### IOData metric names 137 | 138 | Instruments uses macros to implement the metric names, and automatically converts interpolated 139 | strings into IOLists. This means you can have many generated names without increasing the 140 | amount of binary memory you're using. For example: 141 | 142 | ```elixir 143 | def increment_rpc(rpc_name), 144 | do: Instruments.increment("my_module.rpc.#{rpc_name}") 145 | ``` 146 | 147 | will be rewritten to the call: 148 | 149 | ```elixir 150 | def increment_rpc(rpc_name), 151 | do: Instruments.increment(["my_module.rpc.", Kernel.to_string(rpc_name)]) 152 | ``` 153 | 154 | If you wish, you may pass any IOData as the name of a metric. 155 | 156 | #### Sample Rates 157 | For histograms, measure calls and timings, the default sample rate is pegged to 0.1. 158 | This is so you don't accidentally overload your metrics collector. It can be 159 | overridden by passing `sample_rate: float_value` to your metrics call in the 160 | options. 161 | -------------------------------------------------------------------------------- /bench/results/rate_tracker/analysis.txt: -------------------------------------------------------------------------------- 1 | The "multitable" results represent results with one table per scheduler. 2 | 3 | ##### With input 1. No Options ##### 4 | Name ips average deviation median 99th % 5 | track_non_parallel (2025-7-30--19-7-52-utc) 57.02 K 17.54 μs ±40.55% 16.40 μs 29.50 μs 6 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 54.03 K 18.51 μs ±40.31% 17.50 μs 30.85 μs 7 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 44.47 K 22.49 μs ±46.10% 18.66 μs 39.27 μs 8 | track_parallel_8 (2025-7-30--19-8-35-utc) 1.23 K 809.92 μs ±10.42% 801.34 μs 1112.69 μs 9 | 10 | Comparison: 11 | track_non_parallel (2025-7-30--19-7-52-utc) 57.02 K 12 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 54.03 K - 1.06x slower +0.97 μs 13 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 44.47 K - 1.28x slower +4.95 μs 14 | track_parallel_8 (2025-7-30--19-8-35-utc) 1.23 K - 46.18x slower +792.38 μs 15 | 16 | ##### With input 2. No Tags ##### 17 | Name ips average deviation median 99th % 18 | track_non_parallel (2025-7-30--19-7-52-utc) 42.16 K 23.72 μs ±38.69% 21.62 μs 40.88 μs 19 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 41.70 K 23.98 μs ±34.79% 22.63 μs 39.77 μs 20 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 32.58 K 30.69 μs ±37.52% 25.34 μs 49.99 μs 21 | track_parallel_8 (2025-7-30--19-8-35-utc) 1.24 K 806.80 μs ±7.80% 800.37 μs 1029.23 μs 22 | 23 | Comparison: 24 | track_non_parallel (2025-7-30--19-7-52-utc) 42.16 K 25 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 41.70 K - 1.01x slower +0.26 μs 26 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 32.58 K - 1.29x slower +6.97 μs 27 | track_parallel_8 (2025-7-30--19-8-35-utc) 1.24 K - 34.02x slower +783.08 μs 28 | 29 | ##### With input 3. Empty Tags ##### 30 | Name ips average deviation median 99th % 31 | track_non_parallel (2025-7-30--19-7-52-utc) 37.38 K 26.75 μs ±37.74% 24.10 μs 46.37 μs 32 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 36.71 K 27.24 μs ±38.85% 25.29 μs 47.26 μs 33 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 27.42 K 36.47 μs ±140.98% 31.97 μs 57.37 μs 34 | track_parallel_8 (2025-7-30--19-8-35-utc) 1.18 K 846.45 μs ±22.48% 819.76 μs 1504.00 μs 35 | 36 | Comparison: 37 | track_non_parallel (2025-7-30--19-7-52-utc) 37.38 K 38 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 36.71 K - 1.02x slower +0.49 μs 39 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 27.42 K - 1.36x slower +9.71 μs 40 | track_parallel_8 (2025-7-30--19-8-35-utc) 1.18 K - 31.64x slower +819.69 μs 41 | 42 | ##### With input 4. One Tag ##### 43 | Name ips average deviation median 99th % 44 | track_non_parallel (2025-7-30--19-7-52-utc) 35.04 K 28.54 μs ±33.51% 26.81 μs 47.81 μs 45 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 33.96 K 29.44 μs ±31.72% 27.91 μs 49.09 μs 46 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 28.08 K 35.61 μs ±37.16% 28.55 μs 60.68 μs 47 | track_parallel_8 (2025-7-30--19-8-35-utc) 1.24 K 806.46 μs ±11.67% 796.09 μs 1200.90 μs 48 | 49 | Comparison: 50 | track_non_parallel (2025-7-30--19-7-52-utc) 35.04 K 51 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 33.96 K - 1.03x slower +0.90 μs 52 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 28.08 K - 1.25x slower +7.07 μs 53 | track_parallel_8 (2025-7-30--19-8-35-utc) 1.24 K - 28.26x slower +777.92 μs 54 | 55 | ##### With input 5. Five Tags ##### 56 | Name ips average deviation median 99th % 57 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 18.28 K 54.71 μs ±14.59% 51.88 μs 94.28 μs 58 | track_non_parallel (2025-7-30--19-7-52-utc) 17.64 K 56.68 μs ±21.84% 51.38 μs 101.40 μs 59 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 13.77 K 72.61 μs ±84.96% 58.31 μs 118.09 μs 60 | track_parallel_8 (2025-7-30--19-8-35-utc) 0.91 K 1096.18 μs ±56.20% 968.22 μs 3851.55 μs 61 | 62 | Comparison: 63 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 18.28 K 64 | track_non_parallel (2025-7-30--19-7-52-utc) 17.64 K - 1.04x slower +1.97 μs 65 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 13.77 K - 1.33x slower +17.90 μs 66 | track_parallel_8 (2025-7-30--19-8-35-utc) 0.91 K - 20.04x slower +1041.47 μs 67 | 68 | ##### With input 6. Ten Tags ##### 69 | Name ips average deviation median 99th % 70 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 10.45 K 95.65 μs ±14.43% 91.08 μs 171.52 μs 71 | track_non_parallel (2025-7-30--19-7-52-utc) 10.41 K 96.10 μs ±17.22% 90.35 μs 173.25 μs 72 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 9.16 K 109.22 μs ±28.10% 94.04 μs 193.71 μs 73 | track_parallel_8 (2025-7-30--19-8-35-utc) 0.89 K 1125.36 μs ±9.38% 1105.57 μs 1502.48 μs 74 | 75 | Comparison: 76 | track_non_parallel_multitable (2025-7-30--19-57-8-utc) 10.45 K 77 | track_non_parallel (2025-7-30--19-7-52-utc) 10.41 K - 1.00x slower +0.46 μs 78 | track_parallel_8_multitable (2025-7-30--19-57-50-utc) 9.16 K - 1.14x slower +13.57 μs 79 | track_parallel_8 (2025-7-30--19-8-35-utc) 0.89 K - 11.77x slower +1029.71 μs 80 | -------------------------------------------------------------------------------- /lib/probe/runner.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.Probe.Runner do 2 | @moduledoc false 3 | 4 | # A module tasked with running probes. 5 | # This module is a controller process for running module-based probes. 6 | # It periodically exectues callback functions on the probes, and reports their 7 | # results to statix. 8 | 9 | defmodule State do 10 | @moduledoc false 11 | 12 | @type t :: %{ 13 | name: String.t(), 14 | datapoints: [atom], 15 | reporter_options: Instruments.Probe.probe_options(), 16 | report_integer: pos_integer, 17 | sample_interval: pos_integer, 18 | probe_module: module, 19 | probe_state: any 20 | } 21 | 22 | defstruct name: nil, 23 | type: nil, 24 | datapoints: %{}, 25 | reporter_module: nil, 26 | reporter_options: [], 27 | report_interval: 1_000, 28 | sample_interval: 1_000, 29 | probe_module: nil, 30 | probe_state: nil 31 | 32 | def new(metric_name, type, options, probe_module) do 33 | report_interval = Keyword.get(options, :report_interval, 10_000) 34 | sample_interval = Keyword.get(options, :sample_interval, report_interval) 35 | 36 | datapoints = 37 | if Keyword.has_key?(options, :keys) do 38 | for key <- Keyword.get(options, :keys), into: %{} do 39 | {key, "#{metric_name}.#{key}"} 40 | end 41 | else 42 | %{metric_name => metric_name} 43 | end 44 | 45 | %__MODULE__{ 46 | name: metric_name, 47 | type: type, 48 | datapoints: datapoints, 49 | reporter_module: Application.get_env(:instruments, :reporter_module, Instruments.Statix), 50 | report_interval: report_interval, 51 | sample_interval: sample_interval, 52 | probe_module: probe_module, 53 | reporter_options: sanitize_reporter_options(options) 54 | } 55 | end 56 | 57 | defp sanitize_reporter_options(options) do 58 | [sample_rate: 1.0] 59 | |> Keyword.merge(options) 60 | |> Keyword.take([:sample_rate, :tags]) 61 | end 62 | end 63 | 64 | alias Instruments.Probe 65 | use GenServer 66 | require Logger 67 | 68 | @spec start_link(String.t(), Probe.probe_type(), Probe.probe_options(), module) :: {:ok, pid} 69 | def start_link(name, type, options, probe_module) do 70 | start_link({name, type, options, probe_module}) 71 | end 72 | 73 | @spec start_link({String.t(), Probe.probe_type(), Probe.probe_options(), module}) :: {:ok, pid} 74 | def start_link(args) do 75 | GenServer.start_link(__MODULE__, args) 76 | end 77 | 78 | def flush(probe_pid) do 79 | GenServer.call(probe_pid, :flush) 80 | end 81 | 82 | def init({name, type, options, probe_module}) do 83 | state = State.new(name, type, options, probe_module) 84 | {:ok, probe_state} = probe_module.probe_init(name, type, options) 85 | Process.send_after(self(), :probe_sample, state.sample_interval) 86 | Process.send_after(self(), :probe_update, state.report_interval) 87 | 88 | {:ok, %State{state | probe_state: probe_state}} 89 | end 90 | 91 | def handle_call(:flush, _from, %State{} = state) do 92 | {:ok, new_probe_state} = state.probe_module.probe_sample(state.probe_state) 93 | new_state = %State{state | probe_state: new_probe_state} 94 | do_probe_update(new_state) 95 | 96 | {:reply, :ok, new_state} 97 | end 98 | 99 | def handle_info(:probe_sample, %State{} = state) do 100 | {:ok, new_probe_state} = state.probe_module.probe_sample(state.probe_state) 101 | Process.send_after(self(), :probe_sample, state.sample_interval) 102 | 103 | {:noreply, %State{state | probe_state: new_probe_state}} 104 | end 105 | 106 | def handle_info(:probe_update, %State{} = state) do 107 | do_probe_update(state) 108 | 109 | Process.send_after(self(), :probe_update, state.report_interval) 110 | {:noreply, state} 111 | end 112 | 113 | def handle_info(unknown_message, %{} = state) do 114 | {:ok, new_probe_state} = 115 | state.probe_module.probe_handle_message(unknown_message, state.probe_state) 116 | 117 | {:noreply, %State{state | probe_state: new_probe_state}} 118 | end 119 | 120 | # Private 121 | 122 | defp do_probe_update(%State{} = state) do 123 | {:ok, values} = state.probe_module.probe_get_value(state.probe_state) 124 | 125 | case values do 126 | %Probe.Value{} = value -> 127 | Enum.each(state.datapoints, fn {_, metric_name} -> 128 | send_metric(metric_name, value, state) 129 | end) 130 | 131 | values when is_list(values) -> 132 | Enum.each(state.datapoints, fn {key, metric_name} -> 133 | values 134 | |> Keyword.get_values(key) 135 | |> Enum.each(&send_metric(metric_name, &1, state)) 136 | end) 137 | 138 | value when is_number(value) -> 139 | Enum.each(state.datapoints, fn {_, metric_name} -> 140 | send_metric(metric_name, value, state) 141 | end) 142 | 143 | nil -> 144 | Logger.info("Not Sending #{state.name} due to nil return") 145 | 146 | invalid -> 147 | Logger.warn("Probe #{state.name} has returned an invalid value: #{inspect(invalid)}") 148 | end 149 | end 150 | 151 | defp send_metric(_metric_name, 0, %State{type: :counter}), 152 | do: :ok 153 | 154 | defp send_metric(metric_name, metric_value, %State{type: :counter} = state) 155 | when metric_value > 0 do 156 | send_metric(metric_name, metric_value, %State{state | type: :increment}) 157 | end 158 | 159 | defp send_metric(metric_name, metric_value, %State{type: :counter} = state) 160 | when metric_value < 0 do 161 | send_metric(metric_name, abs(metric_value), %State{state | type: :decrement}) 162 | end 163 | 164 | defp send_metric( 165 | metric_name, 166 | %Probe.Value{value: value, tags: tags, sample_rate: sample_rate}, 167 | %State{} = state 168 | ) do 169 | tags = 170 | case tags do 171 | tags when is_list(tags) -> 172 | Keyword.get(state.reporter_options, :tags, []) ++ tags 173 | 174 | _ -> 175 | Keyword.get(state.reporter_options, :tags, []) 176 | end 177 | 178 | sample_rate = 179 | case sample_rate do 180 | sample_rate when is_float(sample_rate) -> 181 | sample_rate 182 | 183 | _ -> 184 | Keyword.get(state.reporter_options, :sample_rate, 1.0) 185 | end 186 | 187 | new_opts = Keyword.merge(state.reporter_options, tags: tags, sample_rate: sample_rate) 188 | send_metric(metric_name, value, %State{state | reporter_options: new_opts}) 189 | end 190 | 191 | defp send_metric(metric_name, metric_value, %State{} = state) when is_number(metric_value) do 192 | :erlang.apply(state.reporter_module, state.type, [ 193 | metric_name, 194 | metric_value, 195 | state.reporter_options 196 | ]) 197 | 198 | :ok 199 | end 200 | 201 | defp send_metric(_metric_name, _metric_value, _state), 202 | do: :ok 203 | end 204 | -------------------------------------------------------------------------------- /lib/rate_tracker.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments.RateTracker do 2 | @moduledoc """ 3 | RateTracker will track how often you are reporting metrics that are not backed 4 | by a "fast" implementation. 5 | 6 | RateTracker is designed to catch cases where you have inadvertently reported 7 | a metric "too" frequently, as some metrics require hitting statsd directly for 8 | every reported value. Doing so in hot loops can result in your 9 | application slowing significantly. 10 | """ 11 | 12 | @table_prefix :instruments_rate_tracker 13 | @max_tables 128 14 | @report_interval_ms Application.get_env( 15 | :instruments, 16 | :rate_tracker_report_interval, 17 | 10_000 18 | ) 19 | @report_jitter_range_ms Application.get_env( 20 | :instruments, 21 | :rate_tracker_report_jitter_range, 22 | -500..500 23 | ) 24 | 25 | @compile {:inline, get_table_key: 2} 26 | 27 | use GenServer 28 | 29 | @type t :: %__MODULE__{ 30 | last_update_time: integer(), 31 | callbacks: [callback()], 32 | table_count: non_neg_integer() 33 | } 34 | 35 | @type callback :: ({String.t(), Statix.options()}, non_neg_integer() -> term()) 36 | 37 | @enforce_keys [:last_update_time, :table_count] 38 | defstruct [ 39 | :last_update_time, 40 | :table_count, 41 | callbacks: [] 42 | ] 43 | 44 | def start_link(_ \\ []) do 45 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 46 | end 47 | 48 | def init(:ok) do 49 | table_count = :erlang.system_info(:schedulers) 50 | 51 | for scheduler_id <- 1..table_count do 52 | :ets.new(table_name(scheduler_id), [:named_table, :public, :set]) 53 | end 54 | 55 | schedule_report() 56 | 57 | {:ok, 58 | %__MODULE__{ 59 | last_update_time: time(), 60 | table_count: table_count, 61 | callbacks: [] 62 | }} 63 | end 64 | 65 | ## Public 66 | @doc false 67 | @spec track(iodata) :: :ok 68 | @spec track(iodata, Statix.options()) :: :ok 69 | def track(name, options \\ []) do 70 | table_key = get_table_key(name, options) 71 | :ets.update_counter(current_table(), table_key, 1, {table_key, 0}) 72 | 73 | :ok 74 | end 75 | 76 | @doc """ 77 | Add a callback to be notified that you are reporting a metric "too" frequently. 78 | 79 | In order to receive notifications, you must set `:instruments` -> 80 | `:rate_tracker_callback_threshold` to the per-second rate that you want to be 81 | notified at. This value will be different for every system, and will require 82 | experimentation to determine. You can use `dump_rates()` in a remote console 83 | to see what values are currently tracked for your metrics. 84 | 85 | This callback should be short-lived. 86 | """ 87 | @spec subscribe(callback()) :: :ok 88 | def subscribe(callback) do 89 | GenServer.cast(__MODULE__, {:subscribe, callback}) 90 | end 91 | 92 | @doc """ 93 | Dump the currently tracked rates 94 | """ 95 | @spec dump_rates() :: [{{String.t(), Keyword.t()}, non_neg_integer()}] 96 | def dump_rates() do 97 | GenServer.call(__MODULE__, :dump_rates) 98 | end 99 | 100 | ## GenServer callbacks 101 | 102 | def handle_call(:dump_rates, _from, %__MODULE__{} = state) do 103 | time_since_report = time() - state.last_update_time 104 | rates = do_dump_rates(state, time_since_report) 105 | {:reply, rates, state} 106 | end 107 | 108 | def handle_cast({:subscribe, callback}, %__MODULE__{} = state) do 109 | state = %__MODULE__{state | callbacks: [callback | state.callbacks]} 110 | 111 | {:noreply, state} 112 | end 113 | 114 | def handle_info(:report, %__MODULE__{} = state) do 115 | report_time = time() 116 | time_since_report = report_time - state.last_update_time 117 | threshold = Application.get_env(:instruments, :rate_tracker_callback_threshold, nil) 118 | 119 | # Extraordinarily unlikely to be zero, but if it is for some reason, we'll just skip this 120 | # and let the next report get it 121 | if time_since_report > 0 do 122 | counts = dump_and_clear_counts(state) 123 | do_report(state, counts, time_since_report, threshold) 124 | end 125 | 126 | schedule_report() 127 | {:noreply, %__MODULE__{state | last_update_time: report_time}} 128 | end 129 | 130 | ## Private 131 | 132 | defp do_dump_rates(_state, 0) do 133 | [] 134 | end 135 | 136 | defp do_dump_rates(state, time_since_report) do 137 | 1..state.table_count 138 | |> Enum.flat_map(fn scheduler_id -> 139 | scheduler_id 140 | |> table_name() 141 | |> :ets.tab2list() 142 | end) 143 | |> aggregate_stats() 144 | |> Enum.filter(fn 145 | {_key, 0} -> false 146 | {_key, _rate} -> true 147 | end) 148 | |> Enum.map(fn {key, count} -> 149 | {key, count / time_since_report} 150 | end) 151 | |> Enum.to_list() 152 | end 153 | 154 | defp get_table_key(name, []) do 155 | {name, []} 156 | end 157 | 158 | defp get_table_key(name, options) do 159 | case Keyword.get(options, :tags) do 160 | [] -> 161 | {name, options} 162 | 163 | [_] -> 164 | {name, options} 165 | 166 | tags when is_list(tags) -> 167 | {name, Keyword.replace!(options, :tags, Enum.sort(tags))} 168 | 169 | _ -> 170 | {name, options} 171 | end 172 | end 173 | 174 | defp sample_rate_for_key({_name, opts}) do 175 | Keyword.get(opts, :sample_rate, 1) 176 | end 177 | 178 | defp schedule_report() do 179 | wait_time = @report_interval_ms + Enum.random(@report_jitter_range_ms) 180 | Process.send_after(self(), :report, wait_time) 181 | end 182 | 183 | defp time() do 184 | # Dividing so we can get the fractional part 185 | System.monotonic_time(:microsecond) / 1_000_000 186 | end 187 | 188 | defp current_table() do 189 | table_name(:erlang.system_info(:scheduler_id)) 190 | end 191 | 192 | defp do_report(%__MODULE__{} = _state, _aggregated_counts, _time_since_report, nil) do 193 | nil 194 | end 195 | 196 | defp do_report(%__MODULE__{} = state, aggregated_counts, time_since_report, threshold) do 197 | Enum.each(aggregated_counts, fn {key, num_tracked} -> 198 | # Sampling correction is technically approximate (we don't know if Statix or another underlying lib will report this differently) 199 | tracked_per_second = num_tracked / time_since_report * sample_rate_for_key(key) 200 | 201 | if tracked_per_second > threshold do 202 | Enum.each(state.callbacks, fn callback -> callback.(key, tracked_per_second) end) 203 | end 204 | end) 205 | end 206 | 207 | defp dump_and_clear_counts(%__MODULE__{} = state) do 208 | 1..state.table_count 209 | |> Enum.flat_map(fn scheduler_id -> 210 | table_name = table_name(scheduler_id) 211 | table_data = :ets.tab2list(table_name) 212 | 213 | Enum.each(table_data, fn {key, val} -> 214 | :ets.update_counter(table_name, key, -val) 215 | end) 216 | 217 | table_data 218 | end) 219 | |> aggregate_stats() 220 | end 221 | 222 | defp aggregate_stats(table_data) do 223 | Enum.reduce(table_data, %{}, fn {key, val}, acc -> 224 | Map.update(acc, key, val, &(&1 + val)) 225 | end) 226 | end 227 | 228 | for scheduler_id <- 1..@max_tables do 229 | defp table_name(unquote(scheduler_id)) do 230 | unquote(:"#{@table_prefix}_#{scheduler_id}") 231 | end 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /bench/results/fast_counter/tag_handling/analysis.txt: -------------------------------------------------------------------------------- 1 | Values presented are thousands of iterations per second, higher is better. 2 | 3 | For each scenario scores are calculated, scores are calculate with the following formula. 4 | 5 | score = round(((total - worst) / (best - worst)) * 100) 6 | 7 | ##### With input 1. No Options ##### 8 | | Run 1 | Run 2 | Run 3 | Average | Total | Score | 9 | |-------|-------|-------|---------|-------|-------| 10 | Keyword.get/2 + Keyword.merge/2 | 177 | 177 | 166 | 173 | 520 | 100 | 11 | Keyword.get/2 + Keyword.replace!/3 | 158 | 166 | 160 | 161 | 484 | 73 | 12 | Keyword.get/2 + Keyword.merge/2 with Special Casing | 155 | 167 | 158 | 160 | 480 | 70 | 13 | Keyword.get/2 + Keyword.replace!/3 with Special Casing | 150 | 160 | 161 | 157 | 471 | 64 | 14 | Keyword.pop/2 + Keyword.put/3 with Special Casing | 127 | 129 | 130 | 129 | 386 | 1 | 15 | Keyword.pop/2 + Keyword.put/3 | 119 | 135 | 131 | 128 | 385 | 0 | 16 | 17 | ##### With input 2. No Tags ##### 18 | 19 | | Run 1 | Run 2 | Run 3 | Average | Total | Score | 20 | |-------|-------|-------|---------|-------|-------| 21 | Keyword.get/2 + Keyword.merge/2 | 175 | 174 | 156 | 168 | 505 | 100 | 22 | Keyword.get/2 + Keyword.replace!/3 | 151 | 166 | 153 | 156 | 470 | 72 | 23 | Keyword.get/2 + Keyword.merge/2 with Special Casing | 154 | 139 | 154 | 149 | 447 | 54 | 24 | Keyword.get/2 + Keyword.replace!/3 with Special Casing | 145 | 135 | 157 | 146 | 437 | 46 | 25 | Keyword.pop/2 + Keyword.put/3 with Special Casing | 125 | 128 | 128 | 127 | 381 | 1 | 26 | Keyword.pop/2 + Keyword.put/3 | 119 | 132 | 129 | 127 | 380 | 0 | 27 | 28 | ##### With input 3. Empty Tags ##### 29 | 30 | | Run 1 | Run 2 | Run 3 | Average | Total | Score | 31 | |-------|-------|-------|---------|-------|-------| 32 | Keyword.get/2 + Keyword.merge/2 with Special Casing | 147 | 150 | 150 | 149 | 447 | 100 | 33 | Keyword.get/2 + Keyword.replace!/3 with Special Casing | 136 | 147 | 153 | 145 | 436 | 97 | 34 | Keyword.pop/2 + Keyword.put/3 with Special Casing | 73 | 71 | 77 | 74 | 221 | 30 | 35 | Keyword.get/2 + Keyword.replace!/3 | 58 | 65 | 62 | 62 | 185 | 18 | 36 | Keyword.pop/2 + Keyword.put/3 | 50 | 61 | 54 | 55 | 165 | 12 | 37 | Keyword.get/2 + Keyword.merge/2 | 44 | 42 | 40 | 42 | 126 | 0 | 38 | 39 | 40 | 41 | ##### With input 4. One Tag ##### 42 | 43 | | Run 1 | Run 2 | Run 3 | Average | Total | Score | 44 | |-------|-------|-------|---------|-------|-------| 45 | Keyword.get/2 + Keyword.replace!/3 with Special Casing | 144 | 152 | 153 | 150 | 449 | 100 | 46 | Keyword.get/2 + Keyword.merge/2 with Special Casing | 143 | 154 | 150 | 149 | 447 | 99 | 47 | Keyword.pop/2 + Keyword.put/3 with Special Casing | 73 | 74 | 75 | 74 | 222 | 30 | 48 | Keyword.get/2 + Keyword.replace!/3 | 58 | 65 | 59 | 61 | 182 | 17 | 49 | Keyword.pop/2 + Keyword.put/3 | 50 | 57 | 51 | 53 | 158 | 10 | 50 | Keyword.get/2 + Keyword.merge/2 | 43 | 42 | 41 | 42 | 126 | 0 | 51 | 52 | ##### With input 5. Five Tags ##### 53 | 54 | | Run 1 | Run 2 | Run 3 | Average | Total | Score | 55 | |-------|-------|-------|---------|-------|-------| 56 | Keyword.get/2 + Keyword.replace!/3 with Special Casing | 26 | 30 | 29 | 28 | 85 | 100 | 57 | Keyword.get/2 + Keyword.replace!/3 | 27 | 29 | 28 | 28 | 84 | 95 | 58 | Keyword.pop/2 + Keyword.put/3 with Special Casing | 26 | 27 | 27 | 27 | 80 | 76 | 59 | Keyword.pop/2 + Keyword.put/3 | 23 | 27 | 26 | 25 | 76 | 57 | 60 | Keyword.get/2 + Keyword.merge/2 | 24 | 23 | 22 | 23 | 69 | 24 | 61 | Keyword.get/2 + Keyword.merge/2 with Special Casing | 21 | 22 | 21 | 21 | 64 | 0 | 62 | 63 | ##### With input 6. Ten Tags ##### 64 | 65 | | Run 1 | Run 2 | Run 3 | Average | Total | Score | 66 | |-------|-------|-------|---------|-------|-------| 67 | Keyword.pop/2 + Keyword.put/3 with Special Casing | 13 | 13 | 13 | 13 | 39 | 100 | 68 | Keyword.get/2 + Keyword.replace!/3 with Special Casing | 11 | 13 | 13 | 12 | 37 | 71 | 69 | Keyword.get/2 + Keyword.replace!/3 | 11 | 12 | 13 | 12 | 36 | 57 | 70 | Keyword.get/2 + Keyword.merge/2 | 12 | 11 | 11 | 11 | 34 | 29 | 71 | Keyword.pop/2 + Keyword.put/3 | 11 | 12 | 11 | 11 | 34 | 29 | 72 | Keyword.get/2 + Keyword.merge/2 with Special Casing | 11 | 11 | 10 | 11 | 32 | 0 | 73 | 74 | #### Overall Score Analysis #### 75 | 76 | | Case 1 | Case 2 | Case 3 | Case 4 | Case 5 | Case 6 | Total | Average | 77 | |--------|--------|--------|--------|--------|--------|-------|---------| 78 | Keyword.get/2 + Keyword.replace!/3 with Special Casing | 64 | 46 | 97 | 100 | 100 | 71 | 478 | 80 | 79 | Keyword.get/2 + Keyword.replace!/3 | 73 | 72 | 18 | 17 | 95 | 57 | 332 | 55 | 80 | Keyword.get/2 + Keyword.merge/2 with Special Casing | 70 | 54 | 100 | 99 | 0 | 0 | 323 | 54 | 81 | Keyword.get/2 + Keyword.merge/2 | 100 | 100 | 0 | 0 | 24 | 29 | 253 | 42 | 82 | Keyword.pop/2 + Keyword.put/3 with Special Casing | 1 | 1 | 30 | 30 | 76 | 100 | 238 | 39 | 83 | Keyword.pop/2 + Keyword.put/3 | 0 | 0 | 12 | 10 | 57 | 29 | 108 | 18 | 84 | 85 | 86 | #### Results #### 87 | 88 | Keyword.get/2 + Keyword.replace!/3 with Special Casing on average is the best performing algorithm for tag handling 89 | 90 | Performance Report compared to best 91 | 92 | | Run 1 | Run 2 | Run 3 | Average | 93 | |----------|---------|---------|---------| 94 | Case 1 | +0.90μs | +0.59μs | +0.19μs | +0.56μs | 95 | Case 2 | +1.22μs | +1.63μs | -0.01μs | +0.95μs | 96 | Case 3 | +0.55μs | +0.17μs | -0.14μs | +0.19μs | 97 | Case 4 | -0.04μs | +0.07μs | -0.11μs | -0.08μs | 98 | Case 5 | -1.20μs | -1.49μs | -1.43μs | -1.37μs | 99 | Case 6 | +10.04μs | -0.41μs | +1.43μs | +3.69μs | 100 | 101 | Worst underperformance is the outlier Run 1 / Case 6 where this strategy underperformed by 10.04μs. 102 | 103 | Overall the strategy is never worse than +10.04μs than the best strategy and outperforms all other strategies by up to -1.49μs in some scenarios. 104 | 105 | With the outlier removed, the performance envelope for this strategy is within a tolerance of +1.63μs from being the best strategy in all cases. 106 | 107 | -------------------------------------------------------------------------------- /test/probe/probe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Instruments.ProbeTest do 2 | use ExUnit.Case 3 | alias Instruments.Probe 4 | alias Instruments.Probe.Errors.ProbeNameTakenError 5 | import MetricsAssertions 6 | 7 | setup do 8 | {:ok, _fake_statsd} = start_supervised({FakeStatsd, self()}) 9 | 10 | :ok 11 | end 12 | 13 | describe "probes with functions" do 14 | test "it should allow you to define a probe via a function call" do 15 | Probe.define("other_call", :counter, 16 | function: fn -> 3 end, 17 | report_interval: 20 18 | ) 19 | 20 | assert_metric_reported(:increment, "other_call", 3) 21 | end 22 | 23 | test "it should issue a decrement if your function returns a negative value" do 24 | Probe.define("decrement_call", :counter, 25 | function: fn -> -3 end, 26 | report_interval: 20 27 | ) 28 | 29 | assert_metric_reported(:decrement, "decrement_call", 3) 30 | end 31 | 32 | test "it should allow you to select keys from a function call" do 33 | Probe.define("erlang.memory", :gauge, 34 | function: fn -> 35 | [processes: 6, system: 7, atom: 8, binary: 9, ets: 10] 36 | end, 37 | keys: ~w(processes system atom binary ets)a, 38 | report_interval: 20 39 | ) 40 | 41 | assert_metric_reported(:gauge, "erlang.memory.processes", 6) 42 | assert_metric_reported(:gauge, "erlang.memory.system", 7) 43 | assert_metric_reported(:gauge, "erlang.memory.atom", 8) 44 | assert_metric_reported(:gauge, "erlang.memory.binary", 9) 45 | assert_metric_reported(:gauge, "erlang.memory.ets", 10) 46 | end 47 | 48 | test "it should handle :ok tuple values" do 49 | Probe.define("function.with.ok", :gauge, 50 | function: fn -> {:ok, 6} end, 51 | report_interval: 20 52 | ) 53 | 54 | assert_metric_reported(:gauge, "function.with.ok", 6) 55 | end 56 | 57 | test "it should allow flushing" do 58 | Probe.define("function.with.flush", :gauge, 59 | function: fn -> {:ok, 100} end, 60 | report_interval: 20_000 61 | ) 62 | 63 | Instruments.flush_all_probes(false) 64 | assert_metric_reported(:gauge, "function.with.flush", 100) 65 | end 66 | 67 | test "it should allow the function to return probe results in a keyword list" do 68 | Probe.define("complex.returns", :gauge, 69 | function: fn -> 70 | [with_tags: Probe.Value.new(6, tags: ["my.tag"])] 71 | end, 72 | keys: ~w(with_tags)a, 73 | report_interval: 20 74 | ) 75 | 76 | assert_metric_reported(:gauge, "complex.returns.with_tags", 6, 77 | tags: ["my.tag"], 78 | sample_rate: 1.0 79 | ) 80 | end 81 | 82 | test "it should allow the function to return probe results" do 83 | Probe.define("overridden.tags", :gauge, 84 | function: fn -> 85 | Probe.Value.new(100, tags: ["another.tag"]) 86 | end, 87 | report_interval: 20 88 | ) 89 | 90 | assert_metric_reported(:gauge, "overridden.tags", 100, 91 | tags: ["another.tag"], 92 | sample_rate: 1.0 93 | ) 94 | end 95 | 96 | test "it should allow probes to report under the same name with different tags" do 97 | Probe.define("different.tags", :gauge, 98 | function: fn -> 1000 end, 99 | tags: ["tag.a"], 100 | report_interval: 20 101 | ) 102 | 103 | Probe.define("different.tags", :gauge, 104 | function: fn -> 1 end, 105 | tags: ["tag.b"], 106 | report_interval: 20 107 | ) 108 | 109 | assert_metric_reported(:gauge, "different.tags", 1000, tags: ["tag.a"], sample_rate: 1.0) 110 | assert_metric_reported(:gauge, "different.tags", 1, tags: ["tag.b"], sample_rate: 1.0) 111 | end 112 | end 113 | 114 | describe "probes with mfa" do 115 | defmodule QuickStaticMetric do 116 | def probe_value, do: {:ok, 6} 117 | def error_value, do: {:error, :bad_at_metrics} 118 | 119 | def override_value(arg), 120 | do: Probe.Value.new(94, tags: ["overridden_tag", "arg_tag.#{arg}"]) 121 | 122 | def complex_overrides(state) do 123 | [ 124 | clients: Probe.Value.new(state.udp, tags: ["protocol:udp"]), 125 | clients: Probe.Value.new(state.tcp, tags: ["protocol:tcp"]), 126 | "clients.udp": state.udp, 127 | "clients.tcp": state.tcp 128 | ] 129 | end 130 | end 131 | 132 | test "it should allow you to define a probe via mfa" do 133 | Probe.define("erlang.process_count", :gauge, 134 | mfa: {:erlang, :system_info, [:process_count]}, 135 | report_interval: 20 136 | ) 137 | 138 | assert_metric_reported(:gauge, "erlang.process_count") 139 | end 140 | 141 | test "it should handle responses with successes" do 142 | Probe.define("quick.static.metric", :gauge, 143 | mfa: {QuickStaticMetric, :probe_value, []}, 144 | report_interval: 20 145 | ) 146 | 147 | assert_metric_reported(:gauge, "quick.static.metric", 6) 148 | end 149 | 150 | test "it should allow you to specify options" do 151 | Probe.define("quick.static.with_options", :histogram, 152 | mfa: {QuickStaticMetric, :probe_value, []}, 153 | tags: ["MyTag", "other tag"], 154 | report_interval: 20 155 | ) 156 | 157 | assert_metric_reported(:histogram, "quick.static.with_options", 6, 158 | tags: ["MyTag", "other tag"] 159 | ) 160 | end 161 | 162 | test "it should allow flushing" do 163 | Probe.define("quick.static.flush", :gauge, 164 | mfa: {QuickStaticMetric, :probe_value, []}, 165 | report_interval: 10_000 166 | ) 167 | 168 | Instruments.flush_all_probes(false) 169 | assert_metric_reported(:gauge, "quick.static.flush", 6) 170 | end 171 | 172 | test "it should allow the mfa to add tags" do 173 | Probe.define("quick.overridden", :histogram, 174 | mfa: {QuickStaticMetric, :override_value, ["great"]}, 175 | tags: ["default_tag"], 176 | report_interval: 20 177 | ) 178 | 179 | assert_metric_reported(:histogram, "quick.overridden", 94, 180 | tags: ["default_tag", "overridden_tag", "arg_tag.great"] 181 | ) 182 | end 183 | 184 | test "multiple values can be emitted" do 185 | Probe.define("prefix", :gauge, 186 | mfa: {QuickStaticMetric, :complex_overrides, [%{udp: 13, tcp: 128}]}, 187 | keys: [:clients, :"clients.udp", :"clients.tcp"], 188 | report_interval: 30 189 | ) 190 | 191 | assert_metric_reported(:gauge, "prefix.clients.udp", 13) 192 | assert_metric_reported(:gauge, "prefix.clients.tcp", 128) 193 | 194 | assert_metric_reported(:gauge, "prefix.clients", 13, 195 | tags: ["protocol:udp"], 196 | sample_rate: 1.0 197 | ) 198 | 199 | assert_metric_reported(:gauge, "prefix.clients", 128, 200 | tags: ["protocol:tcp"], 201 | sample_rate: 1.0 202 | ) 203 | end 204 | end 205 | 206 | describe "naming conflicts" do 207 | test "it should not let you define two probes with the same name" do 208 | assert_raise ProbeNameTakenError, fn -> 209 | Probe.define!("foo.bar.baz", :gauge, function: fn -> 3 end) 210 | Probe.define!("foo.bar.baz", :gauge, function: fn -> 6 end) 211 | end 212 | end 213 | 214 | test "it should not let you define two probes that conflict via their keys" do 215 | assert_raise ProbeNameTakenError, fn -> 216 | Probe.define!("probe.without.keys", :gauge, function: fn -> 9 end) 217 | 218 | Probe.define!("probe.without", :gauge, 219 | function: fn -> [keys: 10, values: 11] end, 220 | keys: [:keys, :values] 221 | ) 222 | end 223 | end 224 | 225 | test "it should let you define two probes that have the same name but different tags" do 226 | Probe.define!("probe.with.tags", :gauge, function: fn -> 9 end, tags: [:tagA]) 227 | Probe.define!("probe.with.tags", :gauge, function: fn -> 9 end, tags: [:tagB]) 228 | end 229 | 230 | test "it should not let you define two probes that have the same name and the same tags" do 231 | Probe.define!("probe.with.tags", :gauge, function: fn -> 9 end, tags: [:tagC]) 232 | assert_raise ProbeNameTakenError, fn -> 233 | Probe.define!("probe.with.tags", :gauge, function: fn -> 9 end, tags: [:tagC]) 234 | end 235 | end 236 | 237 | test "it should let you define two probes that have the same name and at least one different tag" do 238 | Probe.define!("probe.with.tags", :gauge, function: fn -> 9 end, tags: [:tagE, :tagF]) 239 | Probe.define!("probe.with.tags", :gauge, function: fn -> 9 end, tags: [:tagG, :tagE]) 240 | end 241 | 242 | test "it should not let you define two probes that have the same tags in a different order" do 243 | Probe.define!("probe.with.tags", :gauge, function: fn -> 9 end, tags: [:tagG, :tagH]) 244 | assert_raise ProbeNameTakenError, fn -> 245 | Probe.define!("probe.with.tags", :gauge, function: fn -> 9 end, tags: [:tagH, :tagG]) 246 | end 247 | end 248 | end 249 | 250 | describe "probes in a module" do 251 | defmodule ModuleProbe do 252 | @behaviour Instruments.Probe 253 | 254 | @impl Instruments.Probe 255 | def probe_init(_name, _type, _opts), do: {:ok, 0} 256 | 257 | @impl Instruments.Probe 258 | def probe_get_value(state), do: {:ok, state} 259 | 260 | @impl Instruments.Probe 261 | def probe_reset(_), do: {:ok, 0} 262 | 263 | @impl Instruments.Probe 264 | def probe_sample(state), do: {:ok, state + 1} 265 | 266 | @impl Instruments.Probe 267 | def probe_handle_message(_msg, state), do: {:ok, state} 268 | 269 | def probe_get_datapoints(_), do: [:foo] 270 | end 271 | 272 | defmodule MessageProbe do 273 | @behaviour Instruments.Probe 274 | 275 | @impl Instruments.Probe 276 | def probe_init(_name, _type, _opts), do: {:ok, 1} 277 | 278 | @impl Instruments.Probe 279 | def probe_get_value(state), do: {:ok, state} 280 | 281 | @impl Instruments.Probe 282 | def probe_reset(_), do: {:ok, 0} 283 | 284 | @impl Instruments.Probe 285 | def probe_sample(state) do 286 | send(self(), {:do_update, 6}) 287 | {:ok, state} 288 | end 289 | 290 | @impl Instruments.Probe 291 | def probe_handle_message({:do_update, val}, _state), do: {:ok, val} 292 | 293 | @impl Instruments.Probe 294 | def probe_handle_message(_msg, state), do: {:ok, state} 295 | 296 | def probe_get_datapoints(_), do: [:foo] 297 | end 298 | 299 | test "You should be able to define a probe by passing in a module" do 300 | Probe.define("probe.with.module", :gauge, 301 | module: ModuleProbe, 302 | report_interval: 20 303 | ) 304 | 305 | assert_metric_reported(:gauge, "probe.with.module") 306 | assert_metric_reported(:gauge, "probe.with.module") 307 | end 308 | 309 | test "probes should be able to process messages" do 310 | Probe.define("probe.with.messages", :gauge, 311 | module: MessageProbe, 312 | report_interval: 20 313 | ) 314 | 315 | assert_metric_reported(:gauge, "probe.with.messages", 6) 316 | end 317 | 318 | test "should allow flushing" do 319 | Probe.define!("probe.with.flush", :gauge, 320 | module: MessageProbe, 321 | # this shouldn't automatically report 322 | report_interval: 10_000 323 | ) 324 | 325 | Instruments.flush_all_probes(false) 326 | assert_metric_reported(:gauge, "probe.with.flush") 327 | end 328 | end 329 | end 330 | -------------------------------------------------------------------------------- /lib/instruments.ex: -------------------------------------------------------------------------------- 1 | defmodule Instruments do 2 | @moduledoc """ 3 | Instruments allows you to easily create and emit metrics from your application. 4 | 5 | Getting started with instruments is simple, all you do is `use` this module, and 6 | you're off to the races. 7 | 8 | ``` 9 | defmodule MyModule do 10 | use Instruments 11 | 12 | def compute_something() do 13 | Instruments.increment("computations") 14 | end 15 | end 16 | ``` 17 | 18 | You can also create functions that are custom prefixed to avoide duplication in your code. 19 | See `Instruments.CustomFunctions` for more details. 20 | 21 | ## Metric Options 22 | 23 | All the functions in this module can be given an options keyword list, with one 24 | or both of the following keys: 25 | 26 | * `sample_rate`: A float, determining the percentage chance this metric will be emitted. 27 | * `tags`: A list of String tags that will be applied to this metric. Tags are useful for post-hoc grouping. 28 | For example, you could add instance type as a tag and visualize the difference between timing 29 | metrics of the same statistic across instance types to see which are the fastest. 30 | 31 | Here's an example of using options: 32 | 33 | ```elixir 34 | @type user_type :: :administrator | :employee | :normal 35 | @spec my_function(user_type, [User]) :: :ok 36 | def my_function(user_type, users) do 37 | Instruments.histogram("user_counts", Enum.count(users), sample_rate: 0.5, tags: [\"#\{user_type\}\"]) 38 | end 39 | ``` 40 | 41 | Now you can aggregate user counts by user type without emitting new stats 42 | 43 | ## Performance notes 44 | 45 | If a metric key has interpolation (such as `"my_metric.#\{Mix.env\}"`), the interpolation is removed and the metric name is converted to 46 | IOdata. This will prevent garbage being created in your process. 47 | 48 | """ 49 | 50 | alias Instruments.{ 51 | FastCounter, 52 | FastGauge, 53 | MacroHelpers, 54 | Probe, 55 | Probes 56 | } 57 | 58 | require Logger 59 | 60 | @metrics_module Application.get_env(:instruments, :reporter_module, Instruments.Statix) 61 | @statsd_port Application.get_env(:instruments, :statsd_port, 8125) 62 | 63 | defmacro __using__(_opts) do 64 | quote do 65 | require Instruments 66 | end 67 | end 68 | 69 | @doc false 70 | def statsd_port(), do: @statsd_port 71 | 72 | # metrics macros 73 | @doc false 74 | defdelegate connect(), to: @metrics_module 75 | 76 | @doc """ 77 | Increments a counter 78 | 79 | Increments the counter with name `key` by `value`. 80 | """ 81 | defmacro increment(key, value \\ 1, options \\ []), 82 | do: MacroHelpers.build_metric_macro(:increment, __CALLER__, FastCounter, key, value, options) 83 | 84 | @doc """ 85 | Decrements a counter 86 | 87 | Decrements the counter with the key `key` by `value`. 88 | """ 89 | defmacro decrement(key, value \\ 1, options \\ []), 90 | do: MacroHelpers.build_metric_macro(:decrement, __CALLER__, FastCounter, key, value, options) 91 | 92 | @doc """ 93 | Sets a gauge value 94 | 95 | Sets the Gauge with key `key` to `value`, overwriting the previous value. Gauges are useful 96 | for system metrics that have a specific value at a specific time. 97 | """ 98 | defmacro gauge(key, value, options \\ []), 99 | do: MacroHelpers.build_metric_macro(:gauge, __CALLER__, FastGauge, key, value, options) 100 | 101 | @doc """ 102 | Adds a value to a histogram 103 | 104 | Reports `value` to a histogram with key `key`. A Histogram is useful if you want to see 105 | aggregated percentages, and are often used when recording timings. 106 | """ 107 | defmacro histogram(key, value, options \\ []), 108 | do: 109 | MacroHelpers.build_metric_macro( 110 | :histogram, 111 | __CALLER__, 112 | @metrics_module, 113 | key, 114 | value, 115 | options 116 | ) 117 | 118 | @doc """ 119 | Reports a timed value 120 | 121 | If you're manually timing something, you can use this function to report its value. Timings 122 | are usually added to a histogram and reported as percentages. If you're interested in timing 123 | a function, you should also see `Instruments.measure/3`. 124 | """ 125 | defmacro timing(key, value, options \\ []), 126 | do: MacroHelpers.build_metric_macro(:timing, __CALLER__, @metrics_module, key, value, options) 127 | 128 | @doc """ 129 | Adds `value` to a set 130 | 131 | Statsd supports the notion of [sets](https://github.com/etsy/statsd/blob/master/docs/metric_types.md#sets), 132 | which are unique values in a given flush. This adds `value` 133 | to a set with key `key`. 134 | 135 | """ 136 | defmacro set(key, value, options \\ []), 137 | do: MacroHelpers.build_metric_macro(:set, __CALLER__, @metrics_module, key, value, options) 138 | 139 | @doc """ 140 | Times the function `function` and returns its result 141 | 142 | This function allows you to time a function and send a metric in one call, and can often be 143 | easier to use than the `Instruments.timing/3` function. 144 | 145 | For example this: 146 | def timed_internals() do 147 | {run_time_micros, result} = :timer.tc(&other_fn/0) 148 | Instruments.timing("my.metric", run_time_micros) 149 | result 150 | end 151 | 152 | Can be converted to: 153 | def timed_internals() do 154 | Instruments.measure("my.metric", &other_fn/0) 155 | end 156 | 157 | """ 158 | defmacro measure(key, options \\ [], function), 159 | do: 160 | MacroHelpers.build_metric_macro( 161 | :measure, 162 | __CALLER__, 163 | @metrics_module, 164 | key, 165 | options, 166 | function 167 | ) 168 | 169 | @doc """ 170 | Sends an event to DataDog 171 | 172 | This macro is useful if you want to record one-off events like deploys or metrics values changing. 173 | """ 174 | defmacro send_event(title_ast, text, opts \\ []) do 175 | title_iodata = MacroHelpers.to_iolist(title_ast, __CALLER__) 176 | 177 | quote do 178 | title = unquote(title_iodata) 179 | 180 | header = [ 181 | "_e{", 182 | Integer.to_charlist(IO.iodata_length(title)), 183 | ",", 184 | Integer.to_charlist(IO.iodata_length(unquote(text))), 185 | "}:", 186 | title, 187 | "|", 188 | unquote(text) 189 | ] 190 | 191 | message = 192 | case Keyword.get(unquote(opts), :tags) do 193 | nil -> 194 | header 195 | 196 | tag_list -> 197 | [header, "|#", Enum.intersperse(tag_list, ",")] 198 | end 199 | 200 | # Statix registers a port to the name of the metrics module. 201 | # and this code assumes that the metrics module is bound to 202 | # a port, and sends directly to it. If we move off of Statix, 203 | # this will have to be changed. 204 | unquote(@metrics_module) 205 | |> Process.whereis() 206 | |> :gen_udp.send('localhost', Instruments.statsd_port(), message) 207 | end 208 | end 209 | 210 | @doc false 211 | def flush_all_probes(wait_for_flush \\ true, flush_timeout_ms \\ 10_000) do 212 | Probe.Supervisor 213 | |> Process.whereis() 214 | |> Supervisor.which_children() 215 | |> Enum.each(fn {_, pid, _, _module} -> 216 | Probe.Runner.flush(pid) 217 | end) 218 | 219 | if wait_for_flush do 220 | Process.sleep(flush_timeout_ms) 221 | end 222 | end 223 | 224 | @doc """ 225 | Registers the following probes: 226 | 227 | 1. `erlang.memory`: Reports how much memory is being used by the `process`, `system`, `atom`, `binary` and `ets` carriers. 228 | 1. `erlang.supercarrier`: Reports the total size of the [super carrier](https://www.erlang.org/doc/apps/erts/supercarrier.html), and how much of it is used. 229 | 1. `recon.alloc`: Reports how much memory is being actively used by the VM. 230 | 1. `erlang.system.process_count`: A gauge reporting the number of processes in the VM. 231 | 1. `erlang.system.port_count`: A gauge reporting the number of ports in the VM. 232 | 1. `erlang.statistics.run_queue`: A gauge reporting the VM's run queue. This number should be 0 or very low. A high run queue indicates your system is overloaded. 233 | 1. `erlang.scheduler_utilization`: A gauge that reports the actual utilization of every scheduler in the system. See `Instruments.Probes.Schedulers` for more information 234 | 235 | If some memory allocators are disabled, then the erlang.memory and recon.alloc probes will not be registered as these statistics are unavailable. 236 | """ 237 | @spec register_vm_metrics(pos_integer()) :: :ok 238 | def register_vm_metrics(report_interval \\ 10000) do 239 | try do 240 | # Ensure that we are able to get memory statistics before registering 241 | :erlang.memory() 242 | 243 | # VM memory. 244 | # processes = used by Erlang processes, their stacks and heaps. 245 | # system = used but not directly related to any Erlang process. 246 | # atom = allocated for atoms (included in system). 247 | # binary = allocated for binaries (included in system). 248 | # ets = allocated for ETS tables (included in system). 249 | Probe.define!("erlang.memory", :gauge, 250 | mfa: {:erlang, :memory, []}, 251 | keys: ~w(processes system atom binary ets)a, 252 | report_interval: report_interval 253 | ) 254 | 255 | # Memory actively used by the VM, allocated (should ~match OS allocation), 256 | # unused (i.e. allocated - used), and usage (used / allocated). 257 | alloc_keys = ~w(used allocated unused usage)a 258 | 259 | Probe.define!("recon.alloc", :gauge, 260 | function: fn -> 261 | for type <- alloc_keys, into: Keyword.new() do 262 | {type, :recon_alloc.memory(type)} 263 | end 264 | end, 265 | keys: alloc_keys, 266 | report_interval: report_interval 267 | ) 268 | 269 | cond do 270 | # The supercarrier is part of mseg_alloc (the flags are all under +MMsc*, where the second "M" refers to `mseg_alloc`) 271 | # https://www.erlang.org/doc/apps/erts/erts_alloc.html 272 | has_allocator_feature?(:mseg_alloc) -> 273 | Probe.define!("erlang.supercarrier", :gauge, 274 | function: fn -> 275 | erts_mmap_info = :erlang.system_info({:allocator, :erts_mmap}) 276 | 277 | get_in(erts_mmap_info, [:default_mmap, :supercarrier, :sizes]) || [total: 0, used: 0] 278 | end, 279 | keys: ~w(total used)a, 280 | report_interval: report_interval 281 | ) 282 | 283 | Application.get_env(:instruments, :warn_on_memory_stats_unsupported?, true) -> 284 | Logger.warn("[Instruments] not collecting memory metrics because :mseg_alloc is not enabled") 285 | 286 | true -> :ok 287 | end 288 | rescue 289 | ErlangError -> 290 | if Application.get_env(:instruments, :warn_on_memory_stats_unsupported?, true) do 291 | Logger.warn("[Instruments] not collecting memory metrics because :erlang.memory is unsupported (some allocator disabled?)") 292 | end 293 | end 294 | 295 | 296 | # process_count = current number of processes. 297 | # port_count = current number of ports. 298 | system_keys = ~w(process_count port_count)a 299 | 300 | Probe.define!("erlang.system", :gauge, 301 | function: fn -> 302 | for key <- system_keys do 303 | {key, :erlang.system_info(key)} 304 | end 305 | end, 306 | keys: system_keys, 307 | report_interval: report_interval 308 | ) 309 | 310 | # The number of processes that are ready to run on all available run queues. 311 | Probe.define!("erlang.statistics.run_queue", :gauge, 312 | mfa: {:erlang, :statistics, [:run_queue]}, 313 | report_interval: report_interval 314 | ) 315 | 316 | Probe.define!("erlang.scheduler_utilization", :gauge, 317 | module: Probes.Schedulers, 318 | keys: ~w(weighted total)a, 319 | report_interval: report_interval 320 | ) 321 | 322 | :ok 323 | end 324 | 325 | @spec has_allocator_feature?(atom()) :: boolean() 326 | defp has_allocator_feature?(feature) do 327 | {_allocator, _version, features, _settings} = :erlang.system_info(:allocator) 328 | 329 | Enum.member?(features, feature) 330 | end 331 | end 332 | -------------------------------------------------------------------------------- /bench/results/fast_counter/tag_handling/run-2.txt: -------------------------------------------------------------------------------- 1 | Operating System: macOS 2 | CPU Information: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz 3 | Number of Available Cores: 16 4 | Available memory: 64 GB 5 | Elixir 1.7.4 6 | Erlang 21.3.8.10 7 | 8 | Benchmark suite executing with the following configuration: 9 | warmup: 2 s 10 | time: 5 s 11 | memory time: 0 ns 12 | parallel: 1 13 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 14 | Estimated total run time: 4.20 min 15 | 16 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 1. No Options... 17 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 2. No Tags... 18 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 3. Empty Tags... 19 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 4. One Tag... 20 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 5. Five Tags... 21 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 6. Ten Tags... 22 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 1. No Options... 23 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 2. No Tags... 24 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 3. Empty Tags... 25 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 4. One Tag... 26 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 5. Five Tags... 27 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 6. Ten Tags... 28 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 1. No Options... 29 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 2. No Tags... 30 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 3. Empty Tags... 31 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 4. One Tag... 32 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 5. Five Tags... 33 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 6. Ten Tags... 34 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 1. No Options... 35 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 2. No Tags... 36 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 3. Empty Tags... 37 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 4. One Tag... 38 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 5. Five Tags... 39 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 6. Ten Tags... 40 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 1. No Options... 41 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 2. No Tags... 42 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 3. Empty Tags... 43 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 4. One Tag... 44 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 5. Five Tags... 45 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 6. Ten Tags... 46 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 1. No Options... 47 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 2. No Tags... 48 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 3. Empty Tags... 49 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 4. One Tag... 50 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 5. Five Tags... 51 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 6. Ten Tags... 52 | 53 | ##### With input 1. No Options ##### 54 | Name ips average deviation median 99th % 55 | Keyword.get/2 + Keyword.merge/2 177.05 K 5.65 μs ±219.50% 4.98 μs 26.98 μs 56 | Keyword.get/2 + Keyword.merge/2 with Special Casing 166.87 K 5.99 μs ±224.54% 4.98 μs 26.98 μs 57 | Keyword.get/2 + Keyword.replace!/3 165.87 K 6.03 μs ±243.04% 4.98 μs 27.98 μs 58 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 160.36 K 6.24 μs ±227.06% 4.98 μs 28.98 μs 59 | Keyword.pop/2 + Keyword.put/3 135.37 K 7.39 μs ±194.26% 6.98 μs 28.98 μs 60 | Keyword.pop/2 + Keyword.put/3 with Special Casing 128.69 K 7.77 μs ±126.73% 6.98 μs 30.98 μs 61 | 62 | Comparison: 63 | Keyword.get/2 + Keyword.merge/2 177.05 K 64 | Keyword.get/2 + Keyword.merge/2 with Special Casing 166.87 K - 1.06x slower +0.34 μs 65 | Keyword.get/2 + Keyword.replace!/3 165.87 K - 1.07x slower +0.38 μs 66 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 160.36 K - 1.10x slower +0.59 μs 67 | Keyword.pop/2 + Keyword.put/3 135.37 K - 1.31x slower +1.74 μs 68 | Keyword.pop/2 + Keyword.put/3 with Special Casing 128.69 K - 1.38x slower +2.12 μs 69 | 70 | ##### With input 2. No Tags ##### 71 | Name ips average deviation median 99th % 72 | Keyword.get/2 + Keyword.merge/2 174.23 K 5.74 μs ±221.73% 4.98 μs 26.98 μs 73 | Keyword.get/2 + Keyword.replace!/3 166.08 K 6.02 μs ±266.86% 4.98 μs 28.98 μs 74 | Keyword.get/2 + Keyword.merge/2 with Special Casing 139.07 K 7.19 μs ±144.70% 6.98 μs 28.98 μs 75 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 135.63 K 7.37 μs ±124.85% 6.98 μs 29.98 μs 76 | Keyword.pop/2 + Keyword.put/3 131.75 K 7.59 μs ±172.32% 6.98 μs 29.98 μs 77 | Keyword.pop/2 + Keyword.put/3 with Special Casing 128.07 K 7.81 μs ±120.53% 6.98 μs 30.98 μs 78 | 79 | Comparison: 80 | Keyword.get/2 + Keyword.merge/2 174.23 K 81 | Keyword.get/2 + Keyword.replace!/3 166.08 K - 1.05x slower +0.28 μs 82 | Keyword.get/2 + Keyword.merge/2 with Special Casing 139.07 K - 1.25x slower +1.45 μs 83 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 135.63 K - 1.28x slower +1.63 μs 84 | Keyword.pop/2 + Keyword.put/3 131.75 K - 1.32x slower +1.85 μs 85 | Keyword.pop/2 + Keyword.put/3 with Special Casing 128.07 K - 1.36x slower +2.07 μs 86 | 87 | ##### With input 3. Empty Tags ##### 88 | Name ips average deviation median 99th % 89 | Keyword.get/2 + Keyword.merge/2 with Special Casing 150.27 K 6.65 μs ±143.67% 5.98 μs 28.98 μs 90 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 146.51 K 6.83 μs ±132.87% 5.98 μs 28.98 μs 91 | Keyword.pop/2 + Keyword.put/3 with Special Casing 70.97 K 14.09 μs ±46.01% 12.98 μs 43.98 μs 92 | Keyword.get/2 + Keyword.replace!/3 65.28 K 15.32 μs ±67.04% 12.98 μs 52.98 μs 93 | Keyword.pop/2 + Keyword.put/3 60.67 K 16.48 μs ±39.86% 14.98 μs 50.98 μs 94 | Keyword.get/2 + Keyword.merge/2 42.03 K 23.79 μs ±41.73% 20.98 μs 67.98 μs 95 | 96 | Comparison: 97 | Keyword.get/2 + Keyword.merge/2 with Special Casing 150.27 K 98 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 146.51 K - 1.03x slower +0.171 μs 99 | Keyword.pop/2 + Keyword.put/3 with Special Casing 70.97 K - 2.12x slower +7.44 μs 100 | Keyword.get/2 + Keyword.replace!/3 65.28 K - 2.30x slower +8.66 μs 101 | Keyword.pop/2 + Keyword.put/3 60.67 K - 2.48x slower +9.83 μs 102 | Keyword.get/2 + Keyword.merge/2 42.03 K - 3.58x slower +17.14 μs 103 | 104 | ##### With input 4. One Tag ##### 105 | Name ips average deviation median 99th % 106 | Keyword.get/2 + Keyword.merge/2 with Special Casing 153.97 K 6.49 μs ±125.30% 5.98 μs 27.98 μs 107 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 152.25 K 6.57 μs ±126.39% 5.98 μs 28.98 μs 108 | Keyword.pop/2 + Keyword.put/3 with Special Casing 74.45 K 13.43 μs ±47.01% 11.98 μs 42.98 μs 109 | Keyword.get/2 + Keyword.replace!/3 64.70 K 15.46 μs ±52.38% 12.98 μs 50.98 μs 110 | Keyword.pop/2 + Keyword.put/3 57.05 K 17.53 μs ±47.19% 15.98 μs 56.98 μs 111 | Keyword.get/2 + Keyword.merge/2 41.84 K 23.90 μs ±44.72% 20.98 μs 68.98 μs 112 | 113 | Comparison: 114 | Keyword.get/2 + Keyword.merge/2 with Special Casing 153.97 K 115 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 152.25 K - 1.01x slower +0.0733 μs 116 | Keyword.pop/2 + Keyword.put/3 with Special Casing 74.45 K - 2.07x slower +6.94 μs 117 | Keyword.get/2 + Keyword.replace!/3 64.70 K - 2.38x slower +8.96 μs 118 | Keyword.pop/2 + Keyword.put/3 57.05 K - 2.70x slower +11.03 μs 119 | Keyword.get/2 + Keyword.merge/2 41.84 K - 3.68x slower +17.41 μs 120 | 121 | ##### With input 5. Five Tags ##### 122 | Name ips average deviation median 99th % 123 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 30.19 K 33.13 μs ±36.26% 28.98 μs 91.98 μs 124 | Keyword.get/2 + Keyword.replace!/3 28.88 K 34.62 μs ±41.62% 29.98 μs 99.98 μs 125 | Keyword.pop/2 + Keyword.put/3 with Special Casing 27.26 K 36.68 μs ±33.83% 31.98 μs 94.98 μs 126 | Keyword.pop/2 + Keyword.put/3 26.95 K 37.11 μs ±38.91% 31.98 μs 103.98 μs 127 | Keyword.get/2 + Keyword.merge/2 23.02 K 43.45 μs ±36.66% 37.98 μs 115.98 μs 128 | Keyword.get/2 + Keyword.merge/2 with Special Casing 22.47 K 44.51 μs ±34.87% 38.98 μs 116.98 μs 129 | 130 | Comparison: 131 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 30.19 K 132 | Keyword.get/2 + Keyword.replace!/3 28.88 K - 1.05x slower +1.49 μs 133 | Keyword.pop/2 + Keyword.put/3 with Special Casing 27.26 K - 1.11x slower +3.55 μs 134 | Keyword.pop/2 + Keyword.put/3 26.95 K - 1.12x slower +3.98 μs 135 | Keyword.get/2 + Keyword.merge/2 23.02 K - 1.31x slower +10.32 μs 136 | Keyword.get/2 + Keyword.merge/2 with Special Casing 22.47 K - 1.34x slower +11.39 μs 137 | 138 | ##### With input 6. Ten Tags ##### 139 | Name ips average deviation median 99th % 140 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 12.81 K 78.07 μs ±33.22% 68.98 μs 189.98 μs 141 | Keyword.pop/2 + Keyword.put/3 with Special Casing 12.74 K 78.48 μs ±32.45% 69.98 μs 190.98 μs 142 | Keyword.get/2 + Keyword.replace!/3 12.41 K 80.60 μs ±32.59% 69.98 μs 194.98 μs 143 | Keyword.pop/2 + Keyword.put/3 11.57 K 86.40 μs ±28.26% 81.98 μs 189.98 μs 144 | Keyword.get/2 + Keyword.merge/2 11.10 K 90.10 μs ±31.08% 80.98 μs 215.98 μs 145 | Keyword.get/2 + Keyword.merge/2 with Special Casing 10.78 K 92.73 μs ±27.41% 84.98 μs 207.98 μs 146 | 147 | Comparison: 148 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 12.81 K 149 | Keyword.pop/2 + Keyword.put/3 with Special Casing 12.74 K - 1.01x slower +0.41 μs 150 | Keyword.get/2 + Keyword.replace!/3 12.41 K - 1.03x slower +2.54 μs 151 | Keyword.pop/2 + Keyword.put/3 11.57 K - 1.11x slower +8.33 μs 152 | Keyword.get/2 + Keyword.merge/2 11.10 K - 1.15x slower +12.04 μs 153 | Keyword.get/2 + Keyword.merge/2 with Special Casing 10.78 K - 1.19x slower +14.66 μs -------------------------------------------------------------------------------- /bench/results/fast_counter/tag_handling/run-1.txt: -------------------------------------------------------------------------------- 1 | Operating System: macOS 2 | CPU Information: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz 3 | Number of Available Cores: 16 4 | Available memory: 64 GB 5 | Elixir 1.7.4 6 | Erlang 21.3.8.10 7 | 8 | Benchmark suite executing with the following configuration: 9 | warmup: 2 s 10 | time: 5 s 11 | memory time: 0 ns 12 | parallel: 1 13 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 14 | Estimated total run time: 4.20 min 15 | 16 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 1. No Options... 17 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 2. No Tags... 18 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 3. Empty Tags... 19 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 4. One Tag... 20 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 5. Five Tags... 21 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 6. Ten Tags... 22 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 1. No Options... 23 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 2. No Tags... 24 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 3. Empty Tags... 25 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 4. One Tag... 26 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 5. Five Tags... 27 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 6. Ten Tags... 28 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 1. No Options... 29 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 2. No Tags... 30 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 3. Empty Tags... 31 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 4. One Tag... 32 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 5. Five Tags... 33 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 6. Ten Tags... 34 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 1. No Options... 35 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 2. No Tags... 36 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 3. Empty Tags... 37 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 4. One Tag... 38 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 5. Five Tags... 39 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 6. Ten Tags... 40 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 1. No Options... 41 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 2. No Tags... 42 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 3. Empty Tags... 43 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 4. One Tag... 44 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 5. Five Tags... 45 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 6. Ten Tags... 46 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 1. No Options... 47 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 2. No Tags... 48 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 3. Empty Tags... 49 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 4. One Tag... 50 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 5. Five Tags... 51 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 6. Ten Tags... 52 | 53 | ##### With input 1. No Options ##### 54 | Name ips average deviation median 99th % 55 | Keyword.get/2 + Keyword.merge/2 173.58 K 5.76 μs ±217.77% 5 μs 26 μs 56 | Keyword.get/2 + Keyword.replace!/3 158.46 K 6.31 μs ±144.04% 6 μs 27 μs 57 | Keyword.get/2 + Keyword.merge/2 with Special Casing 155.24 K 6.44 μs ±126.07% 6 μs 29 μs 58 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 150.05 K 6.66 μs ±134.41% 6 μs 28 μs 59 | Keyword.pop/2 + Keyword.put/3 with Special Casing 127.09 K 7.87 μs ±115.74% 7 μs 30 μs 60 | Keyword.pop/2 + Keyword.put/3 119.39 K 8.38 μs ±121.42% 8 μs 30 μs 61 | 62 | Comparison: 63 | Keyword.get/2 + Keyword.merge/2 173.58 K 64 | Keyword.get/2 + Keyword.replace!/3 158.46 K - 1.10x slower +0.55 μs 65 | Keyword.get/2 + Keyword.merge/2 with Special Casing 155.24 K - 1.12x slower +0.68 μs 66 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 150.05 K - 1.16x slower +0.90 μs 67 | Keyword.pop/2 + Keyword.put/3 with Special Casing 127.09 K - 1.37x slower +2.11 μs 68 | Keyword.pop/2 + Keyword.put/3 119.39 K - 1.45x slower +2.61 μs 69 | 70 | ##### With input 2. No Tags ##### 71 | Name ips average deviation median 99th % 72 | Keyword.get/2 + Keyword.merge/2 175.59 K 5.70 μs ±235.28% 5 μs 26 μs 73 | Keyword.get/2 + Keyword.merge/2 with Special Casing 154.30 K 6.48 μs ±138.74% 6 μs 29 μs 74 | Keyword.get/2 + Keyword.replace!/3 151.31 K 6.61 μs ±136.86% 6 μs 28 μs 75 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 144.60 K 6.92 μs ±164.46% 7 μs 17 μs 76 | Keyword.pop/2 + Keyword.put/3 with Special Casing 125.17 K 7.99 μs ±130.25% 7 μs 31 μs 77 | Keyword.pop/2 + Keyword.put/3 119.29 K 8.38 μs ±124.92% 7 μs 34 μs 78 | 79 | Comparison: 80 | Keyword.get/2 + Keyword.merge/2 175.59 K 81 | Keyword.get/2 + Keyword.merge/2 with Special Casing 154.30 K - 1.14x slower +0.79 μs 82 | Keyword.get/2 + Keyword.replace!/3 151.31 K - 1.16x slower +0.91 μs 83 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 144.60 K - 1.21x slower +1.22 μs 84 | Keyword.pop/2 + Keyword.put/3 with Special Casing 125.17 K - 1.40x slower +2.29 μs 85 | Keyword.pop/2 + Keyword.put/3 119.29 K - 1.47x slower +2.69 μs 86 | 87 | ##### With input 3. Empty Tags ##### 88 | Name ips average deviation median 99th % 89 | Keyword.get/2 + Keyword.merge/2 with Special Casing 147.21 K 6.79 μs ±143.28% 6 μs 30 μs 90 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 136.14 K 7.35 μs ±121.45% 7 μs 21 μs 91 | Keyword.pop/2 + Keyword.put/3 with Special Casing 72.95 K 13.71 μs ±48.37% 12 μs 44 μs 92 | Keyword.get/2 + Keyword.replace!/3 58.27 K 17.16 μs ±39.63% 16 μs 38 μs 93 | Keyword.pop/2 + Keyword.put/3 50.46 K 19.82 μs ±51.60% 17 μs 62 μs 94 | Keyword.get/2 + Keyword.merge/2 43.69 K 22.89 μs ±41.07% 21 μs 65 μs 95 | 96 | Comparison: 97 | Keyword.get/2 + Keyword.merge/2 with Special Casing 147.21 K 98 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 136.14 K - 1.08x slower +0.55 μs 99 | Keyword.pop/2 + Keyword.put/3 with Special Casing 72.95 K - 2.02x slower +6.92 μs 100 | Keyword.get/2 + Keyword.replace!/3 58.27 K - 2.53x slower +10.37 μs 101 | Keyword.pop/2 + Keyword.put/3 50.46 K - 2.92x slower +13.02 μs 102 | Keyword.get/2 + Keyword.merge/2 43.69 K - 3.37x slower +16.09 μs 103 | 104 | ##### With input 4. One Tag ##### 105 | Name ips average deviation median 99th % 106 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 143.50 K 6.97 μs ±129.37% 6 μs 30 μs 107 | Keyword.get/2 + Keyword.merge/2 with Special Casing 142.58 K 7.01 μs ±129.65% 6 μs 29 μs 108 | Keyword.pop/2 + Keyword.put/3 with Special Casing 72.57 K 13.78 μs ±50.94% 12 μs 46 μs 109 | Keyword.get/2 + Keyword.replace!/3 58.44 K 17.11 μs ±43.30% 16 μs 37 μs 110 | Keyword.pop/2 + Keyword.put/3 49.73 K 20.11 μs ±49.34% 17 μs 62 μs 111 | Keyword.get/2 + Keyword.merge/2 43.23 K 23.13 μs ±41.80% 21 μs 65 μs 112 | 113 | Comparison: 114 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 143.50 K 115 | Keyword.get/2 + Keyword.merge/2 with Special Casing 142.58 K - 1.01x slower +0.0446 μs 116 | Keyword.pop/2 + Keyword.put/3 with Special Casing 72.57 K - 1.98x slower +6.81 μs 117 | Keyword.get/2 + Keyword.replace!/3 58.44 K - 2.46x slower +10.14 μs 118 | Keyword.pop/2 + Keyword.put/3 49.73 K - 2.89x slower +13.14 μs 119 | Keyword.get/2 + Keyword.merge/2 43.23 K - 3.32x slower +16.16 μs 120 | 121 | ##### With input 5. Five Tags ##### 122 | Name ips average deviation median 99th % 123 | Keyword.get/2 + Keyword.replace!/3 27.19 K 36.78 μs ±39.29% 32 μs 101 μs 124 | Keyword.pop/2 + Keyword.put/3 with Special Casing 26.33 K 37.98 μs ±37.51% 34 μs 105 μs 125 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 25.99 K 38.48 μs ±34.21% 36 μs 98 μs 126 | Keyword.get/2 + Keyword.merge/2 23.85 K 41.93 μs ±30.92% 38 μs 105 μs 127 | Keyword.pop/2 + Keyword.put/3 23.15 K 43.19 μs ±26.28% 40 μs 92 μs 128 | Keyword.get/2 + Keyword.merge/2 with Special Casing 21.33 K 46.88 μs ±36.12% 42 μs 121 μs 129 | 130 | Comparison: 131 | Keyword.get/2 + Keyword.replace!/3 27.19 K 132 | Keyword.pop/2 + Keyword.put/3 with Special Casing 26.33 K - 1.03x slower +1.20 μs 133 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 25.99 K - 1.05x slower +1.70 μs 134 | Keyword.get/2 + Keyword.merge/2 23.85 K - 1.14x slower +5.15 μs 135 | Keyword.pop/2 + Keyword.put/3 23.15 K - 1.17x slower +6.41 μs 136 | Keyword.get/2 + Keyword.merge/2 with Special Casing 21.33 K - 1.27x slower +10.10 μs 137 | 138 | ##### With input 6. Ten Tags ##### 139 | Name ips average deviation median 99th % 140 | Keyword.pop/2 + Keyword.put/3 with Special Casing 12.84 K 77.90 μs ±28.77% 71 μs 185 μs 141 | Keyword.get/2 + Keyword.merge/2 11.58 K 86.37 μs ±30.03% 79 μs 208 μs 142 | Keyword.get/2 + Keyword.replace!/3 11.37 K 87.94 μs ±23.55% 84 μs 177 μs 143 | Keyword.pop/2 + Keyword.put/3 11.34 K 88.20 μs ±32.71% 78 μs 209 μs 144 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 11.19 K 89.35 μs ±24.93% 84 μs 184 μs 145 | Keyword.get/2 + Keyword.merge/2 with Special Casing 10.93 K 91.46 μs ±32.56% 81 μs 219 μs 146 | 147 | Comparison: 148 | Keyword.pop/2 + Keyword.put/3 with Special Casing 12.84 K 149 | Keyword.get/2 + Keyword.merge/2 11.58 K - 1.11x slower +8.47 μs 150 | Keyword.get/2 + Keyword.replace!/3 11.37 K - 1.13x slower +10.04 μs 151 | Keyword.pop/2 + Keyword.put/3 11.34 K - 1.13x slower +10.29 μs 152 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 11.19 K - 1.15x slower +11.44 μs 153 | Keyword.get/2 + Keyword.merge/2 with Special Casing 10.93 K - 1.17x slower +13.55 μs -------------------------------------------------------------------------------- /bench/results/fast_counter/tag_handling/run-3.txt: -------------------------------------------------------------------------------- 1 | Operating System: macOS 2 | CPU Information: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz 3 | Number of Available Cores: 16 4 | Available memory: 64 GB 5 | Elixir 1.7.4 6 | Erlang 21.3.8.10 7 | 8 | Benchmark suite executing with the following configuration: 9 | warmup: 2 s 10 | time: 5 s 11 | memory time: 0 ns 12 | parallel: 1 13 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 14 | Estimated total run time: 4.20 min 15 | 16 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 1. No Options... 17 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 2. No Tags... 18 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 3. Empty Tags... 19 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 4. One Tag... 20 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 5. Five Tags... 21 | Benchmarking Keyword.get/2 + Keyword.merge/2 with input 6. Ten Tags... 22 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 1. No Options... 23 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 2. No Tags... 24 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 3. Empty Tags... 25 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 4. One Tag... 26 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 5. Five Tags... 27 | Benchmarking Keyword.get/2 + Keyword.merge/2 with Special Casing with input 6. Ten Tags... 28 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 1. No Options... 29 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 2. No Tags... 30 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 3. Empty Tags... 31 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 4. One Tag... 32 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 5. Five Tags... 33 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with input 6. Ten Tags... 34 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 1. No Options... 35 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 2. No Tags... 36 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 3. Empty Tags... 37 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 4. One Tag... 38 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 5. Five Tags... 39 | Benchmarking Keyword.get/2 + Keyword.replace!/3 with Special Casing with input 6. Ten Tags... 40 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 1. No Options... 41 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 2. No Tags... 42 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 3. Empty Tags... 43 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 4. One Tag... 44 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 5. Five Tags... 45 | Benchmarking Keyword.pop/2 + Keyword.put/3 with input 6. Ten Tags... 46 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 1. No Options... 47 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 2. No Tags... 48 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 3. Empty Tags... 49 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 4. One Tag... 50 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 5. Five Tags... 51 | Benchmarking Keyword.pop/2 + Keyword.put/3 with Special Casing with input 6. Ten Tags... 52 | 53 | ##### With input 1. No Options ##### 54 | Name ips average deviation median 99th % 55 | Keyword.get/2 + Keyword.merge/2 166.32 K 6.01 μs ±260.69% 5 μs 26 μs 56 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 161.29 K 6.20 μs ±217.41% 5 μs 28 μs 57 | Keyword.get/2 + Keyword.replace!/3 159.78 K 6.26 μs ±251.24% 6 μs 27 μs 58 | Keyword.get/2 + Keyword.merge/2 with Special Casing 158.08 K 6.33 μs ±122.79% 6 μs 29 μs 59 | Keyword.pop/2 + Keyword.put/3 131.11 K 7.63 μs ±189.69% 7 μs 29 μs 60 | Keyword.pop/2 + Keyword.put/3 with Special Casing 129.63 K 7.71 μs ±192.99% 7 μs 31 μs 61 | 62 | Comparison: 63 | Keyword.get/2 + Keyword.merge/2 166.32 K 64 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 161.29 K - 1.03x slower +0.187 μs 65 | Keyword.get/2 + Keyword.replace!/3 159.78 K - 1.04x slower +0.25 μs 66 | Keyword.get/2 + Keyword.merge/2 with Special Casing 158.08 K - 1.05x slower +0.31 μs 67 | Keyword.pop/2 + Keyword.put/3 131.11 K - 1.27x slower +1.61 μs 68 | Keyword.pop/2 + Keyword.put/3 with Special Casing 129.63 K - 1.28x slower +1.70 μs 69 | 70 | ##### With input 2. No Tags ##### 71 | Name ips average deviation median 99th % 72 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 156.50 K 6.39 μs ±128.46% 6 μs 28 μs 73 | Keyword.get/2 + Keyword.merge/2 156.33 K 6.40 μs ±130.17% 6 μs 27 μs 74 | Keyword.get/2 + Keyword.merge/2 with Special Casing 153.62 K 6.51 μs ±136.05% 6 μs 28 μs 75 | Keyword.get/2 + Keyword.replace!/3 153.45 K 6.52 μs ±130.77% 6 μs 26 μs 76 | Keyword.pop/2 + Keyword.put/3 129.45 K 7.73 μs ±176.45% 7 μs 30 μs 77 | Keyword.pop/2 + Keyword.put/3 with Special Casing 128.31 K 7.79 μs ±115.90% 7 μs 31 μs 78 | 79 | Comparison: 80 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 156.50 K 81 | Keyword.get/2 + Keyword.merge/2 156.33 K - 1.00x slower +0.00678 μs 82 | Keyword.get/2 + Keyword.merge/2 with Special Casing 153.62 K - 1.02x slower +0.120 μs 83 | Keyword.get/2 + Keyword.replace!/3 153.45 K - 1.02x slower +0.127 μs 84 | Keyword.pop/2 + Keyword.put/3 129.45 K - 1.21x slower +1.34 μs 85 | Keyword.pop/2 + Keyword.put/3 with Special Casing 128.31 K - 1.22x slower +1.40 μs 86 | 87 | ##### With input 3. Empty Tags ##### 88 | Name ips average deviation median 99th % 89 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 152.84 K 6.54 μs ±118.68% 6 μs 29 μs 90 | Keyword.get/2 + Keyword.merge/2 with Special Casing 149.59 K 6.69 μs ±119.45% 6 μs 29 μs 91 | Keyword.pop/2 + Keyword.put/3 with Special Casing 76.52 K 13.07 μs ±45.99% 12 μs 42 μs 92 | Keyword.get/2 + Keyword.replace!/3 62.39 K 16.03 μs ±54.14% 15 μs 48 μs 93 | Keyword.pop/2 + Keyword.put/3 53.79 K 18.59 μs ±47.89% 16 μs 59 μs 94 | Keyword.get/2 + Keyword.merge/2 39.50 K 25.32 μs ±37.72% 24 μs 65 μs 95 | 96 | Comparison: 97 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 152.84 K 98 | Keyword.get/2 + Keyword.merge/2 with Special Casing 149.59 K - 1.02x slower +0.142 μs 99 | Keyword.pop/2 + Keyword.put/3 with Special Casing 76.52 K - 2.00x slower +6.53 μs 100 | Keyword.get/2 + Keyword.replace!/3 62.39 K - 2.45x slower +9.49 μs 101 | Keyword.pop/2 + Keyword.put/3 53.79 K - 2.84x slower +12.05 μs 102 | Keyword.get/2 + Keyword.merge/2 39.50 K - 3.87x slower +18.77 μs 103 | 104 | ##### With input 4. One Tag ##### 105 | Name ips average deviation median 99th % 106 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 153.09 K 6.53 μs ±145.34% 6 μs 29 μs 107 | Keyword.get/2 + Keyword.merge/2 with Special Casing 150.47 K 6.65 μs ±118.33% 6 μs 29 μs 108 | Keyword.pop/2 + Keyword.put/3 with Special Casing 75.38 K 13.27 μs ±46.02% 12 μs 42 μs 109 | Keyword.get/2 + Keyword.replace!/3 59.09 K 16.92 μs ±54.38% 16 μs 44 μs 110 | Keyword.pop/2 + Keyword.put/3 50.62 K 19.75 μs ±44.88% 17 μs 60 μs 111 | Keyword.get/2 + Keyword.merge/2 40.60 K 24.63 μs ±48.74% 21 μs 76 μs 112 | 113 | Comparison: 114 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 153.09 K 115 | Keyword.get/2 + Keyword.merge/2 with Special Casing 150.47 K - 1.02x slower +0.114 μs 116 | Keyword.pop/2 + Keyword.put/3 with Special Casing 75.38 K - 2.03x slower +6.73 μs 117 | Keyword.get/2 + Keyword.replace!/3 59.09 K - 2.59x slower +10.39 μs 118 | Keyword.pop/2 + Keyword.put/3 50.62 K - 3.02x slower +13.22 μs 119 | Keyword.get/2 + Keyword.merge/2 40.60 K - 3.77x slower +18.10 μs 120 | 121 | ##### With input 5. Five Tags ##### 122 | Name ips average deviation median 99th % 123 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 29.32 K 34.11 μs ±36.48% 30 μs 94 μs 124 | Keyword.get/2 + Keyword.replace!/3 28.14 K 35.54 μs ±40.11% 32 μs 96 μs 125 | Keyword.pop/2 + Keyword.put/3 with Special Casing 27.47 K 36.41 μs ±35.24% 32 μs 99 μs 126 | Keyword.pop/2 + Keyword.put/3 26.17 K 38.22 μs ±32.58% 35 μs 99 μs 127 | Keyword.get/2 + Keyword.merge/2 21.57 K 46.36 μs ±30.62% 44 μs 112 μs 128 | Keyword.get/2 + Keyword.merge/2 with Special Casing 21.12 K 47.35 μs ±28.81% 45 μs 110 μs 129 | 130 | Comparison: 131 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 29.32 K 132 | Keyword.get/2 + Keyword.replace!/3 28.14 K - 1.04x slower +1.43 μs 133 | Keyword.pop/2 + Keyword.put/3 with Special Casing 27.47 K - 1.07x slower +2.30 μs 134 | Keyword.pop/2 + Keyword.put/3 26.17 K - 1.12x slower +4.11 μs 135 | Keyword.get/2 + Keyword.merge/2 21.57 K - 1.36x slower +12.25 μs 136 | Keyword.get/2 + Keyword.merge/2 with Special Casing 21.12 K - 1.39x slower +13.24 μs 137 | 138 | ##### With input 6. Ten Tags ##### 139 | Name ips average deviation median 99th % 140 | Keyword.pop/2 + Keyword.put/3 with Special Casing 13.01 K 76.88 μs ±29.92% 69 μs 186 μs 141 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 12.77 K 78.31 μs ±32.18% 70 μs 192 μs 142 | Keyword.get/2 + Keyword.replace!/3 12.74 K 78.47 μs ±31.99% 70 μs 190 μs 143 | Keyword.pop/2 + Keyword.put/3 11.75 K 85.11 μs ±34.38% 74 μs 207 μs 144 | Keyword.get/2 + Keyword.merge/2 11.09 K 90.14 μs ±30.51% 82 μs 213 μs 145 | Keyword.get/2 + Keyword.merge/2 with Special Casing 10.36 K 96.50 μs ±27.80% 92 μs 209 μs 146 | 147 | Comparison: 148 | Keyword.pop/2 + Keyword.put/3 with Special Casing 13.01 K 149 | Keyword.get/2 + Keyword.replace!/3 with Special Casing 12.77 K - 1.02x slower +1.43 μs 150 | Keyword.get/2 + Keyword.replace!/3 12.74 K - 1.02x slower +1.59 μs 151 | Keyword.pop/2 + Keyword.put/3 11.75 K - 1.11x slower +8.23 μs 152 | Keyword.get/2 + Keyword.merge/2 11.09 K - 1.17x slower +13.26 μs 153 | Keyword.get/2 + Keyword.merge/2 with Special Casing 10.36 K - 1.26x slower +19.62 μs -------------------------------------------------------------------------------- /bench/results/fast_gauge/analysis.txt: -------------------------------------------------------------------------------- 1 | Table of Contents: 2 | Benchee output (final FastGauge implementation vs Statix.Gauge) 3 | Benchee output comparing one ETS table vs one ETS table per scheduler: 4 | 5 | Benchee output (final FastGauge implementation vs Statix.Gauge) 6 | ========================================================================================================= 7 | Operating System: Linux 8 | CPU Information: Intel(R) Xeon(R) CPU @ 3.10GHz 9 | Number of Available Cores: 16 10 | Available memory: 62.79 GB 11 | Elixir 1.15.5 12 | Erlang 25.3.2.7 13 | JIT enabled: true 14 | 15 | Benchmark suite executing with the following configuration: 16 | warmup: 2 s 17 | time: 5 s 18 | memory time: 0 ns 19 | reduction time: 0 ns 20 | parallel: 1 21 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 22 | Estimated total run time: 1 min 24 s 23 | 24 | Benchmarking fastgauge_non_parallel with input 1. No Options ... 25 | Benchmarking fastgauge_non_parallel with input 2. No Tags ... 26 | Benchmarking fastgauge_non_parallel with input 3. Empty Tags ... 27 | Benchmarking fastgauge_non_parallel with input 4. One Tag ... 28 | Benchmarking fastgauge_non_parallel with input 5. Five Tags ... 29 | Benchmarking fastgauge_non_parallel with input 6. Ten Tags ... 30 | Benchmarking gauge_non_parallel with input 1. No Options ... 31 | Benchmarking gauge_non_parallel with input 2. No Tags ... 32 | Benchmarking gauge_non_parallel with input 3. Empty Tags ... 33 | Benchmarking gauge_non_parallel with input 4. One Tag ... 34 | Benchmarking gauge_non_parallel with input 5. Five Tags ... 35 | Benchmarking gauge_non_parallel with input 6. Ten Tags ... 36 | Calculating statistics... 37 | Formatting results... 38 | 39 | ##### With input 1. No Options ##### 40 | Name ips average deviation median 99th % 41 | fastgauge_non_parallel 33.88 K 29.51 μs ±28.23% 28.96 μs 38.57 μs 42 | gauge_non_parallel 1.48 K 676.73 μs ±2.16% 675.19 μs 741.91 μs 43 | 44 | Comparison: 45 | fastgauge_non_parallel 33.88 K 46 | gauge_non_parallel 1.48 K - 22.93x slower +647.21 μs 47 | 48 | ##### With input 2. No Tags ##### 49 | Name ips average deviation median 99th % 50 | fastgauge_non_parallel 27.10 K 36.91 μs ±8.32% 36.30 μs 46.32 μs 51 | gauge_non_parallel 1.41 K 708.80 μs ±1.71% 707.84 μs 748.02 μs 52 | 53 | Comparison: 54 | fastgauge_non_parallel 27.10 K 55 | gauge_non_parallel 1.41 K - 19.21x slower +671.90 μs 56 | 57 | ##### With input 3. Empty Tags ##### 58 | Name ips average deviation median 99th % 59 | fastgauge_non_parallel 24.04 K 41.61 μs ±7.72% 40.91 μs 50.66 μs 60 | gauge_non_parallel 1.40 K 713.36 μs ±1.52% 712.51 μs 747.60 μs 61 | 62 | Comparison: 63 | fastgauge_non_parallel 24.04 K 64 | gauge_non_parallel 1.40 K - 17.15x slower +671.76 μs 65 | 66 | ##### With input 4. One Tag ##### 67 | Name ips average deviation median 99th % 68 | fastgauge_non_parallel 21.43 K 46.66 μs ±9.42% 46.04 μs 54.06 μs 69 | gauge_non_parallel 1.37 K 732.22 μs ±4.73% 728.12 μs 818.03 μs 70 | 71 | Comparison: 72 | fastgauge_non_parallel 21.43 K 73 | gauge_non_parallel 1.37 K - 15.69x slower +685.56 μs 74 | 75 | ##### With input 5. Five Tags ##### 76 | Name ips average deviation median 99th % 77 | fastgauge_non_parallel 12.97 K 77.08 μs ±5.79% 76.29 μs 86.76 μs 78 | gauge_non_parallel 1.33 K 754.57 μs ±1.92% 752.99 μs 819.40 μs 79 | 80 | Comparison: 81 | fastgauge_non_parallel 12.97 K 82 | gauge_non_parallel 1.33 K - 9.79x slower +677.49 μs 83 | 84 | ##### With input 6. Ten Tags ##### 85 | Name ips average deviation median 99th % 86 | fastgauge_non_parallel 8.18 K 122.22 μs ±4.45% 120.78 μs 132.53 μs 87 | gauge_non_parallel 1.26 K 791.22 μs ±1.52% 789.99 μs 831.49 μs 88 | 89 | Comparison: 90 | fastgauge_non_parallel 8.18 K 91 | gauge_non_parallel 1.26 K - 6.47x slower +669.00 μs 92 | Suite saved in external term format at bench/results/fast_gauge/non_parallel.benchee 93 | Operating System: Linux 94 | CPU Information: Intel(R) Xeon(R) CPU @ 3.10GHz 95 | Number of Available Cores: 16 96 | Available memory: 62.79 GB 97 | Elixir 1.15.5 98 | Erlang 25.3.2.7 99 | JIT enabled: true 100 | 101 | Benchmark suite executing with the following configuration: 102 | warmup: 2 s 103 | time: 5 s 104 | memory time: 0 ns 105 | reduction time: 0 ns 106 | parallel: 8 107 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 108 | Estimated total run time: 1 min 24 s 109 | 110 | Benchmarking fastgauge_parallel_8 with input 1. No Options ... 111 | Benchmarking fastgauge_parallel_8 with input 2. No Tags ... 112 | Benchmarking fastgauge_parallel_8 with input 3. Empty Tags ... 113 | Benchmarking fastgauge_parallel_8 with input 4. One Tag ... 114 | Benchmarking fastgauge_parallel_8 with input 5. Five Tags ... 115 | Benchmarking fastgauge_parallel_8 with input 6. Ten Tags ... 116 | Benchmarking gauge_parallel_8 with input 1. No Options ... 117 | Benchmarking gauge_parallel_8 with input 2. No Tags ... 118 | Benchmarking gauge_parallel_8 with input 3. Empty Tags ... 119 | Benchmarking gauge_parallel_8 with input 4. One Tag ... 120 | Benchmarking gauge_parallel_8 with input 5. Five Tags ... 121 | Benchmarking gauge_parallel_8 with input 6. Ten Tags ... 122 | Calculating statistics... 123 | Formatting results... 124 | 125 | ##### With input 1. No Options ##### 126 | Name ips average deviation median 99th % 127 | fastgauge_parallel_8 33.00 K 0.0303 ms ±29.88% 0.0297 ms 0.0393 ms 128 | gauge_parallel_8 0.23 K 4.31 ms ±2.94% 4.30 ms 4.70 ms 129 | 130 | Comparison: 131 | fastgauge_parallel_8 33.00 K 132 | gauge_parallel_8 0.23 K - 142.14x slower +4.28 ms 133 | 134 | ##### With input 2. No Tags ##### 135 | Name ips average deviation median 99th % 136 | fastgauge_parallel_8 26.24 K 0.0381 ms ±22.09% 0.0374 ms 0.0504 ms 137 | gauge_parallel_8 0.23 K 4.28 ms ±2.28% 4.28 ms 4.52 ms 138 | 139 | Comparison: 140 | fastgauge_parallel_8 26.24 K 141 | gauge_parallel_8 0.23 K - 112.41x slower +4.25 ms 142 | 143 | ##### With input 3. Empty Tags ##### 144 | Name ips average deviation median 99th % 145 | fastgauge_parallel_8 23.47 K 0.0426 ms ±20.23% 0.0419 ms 0.0543 ms 146 | gauge_parallel_8 0.23 K 4.33 ms ±2.41% 4.31 ms 4.94 ms 147 | 148 | Comparison: 149 | fastgauge_parallel_8 23.47 K 150 | gauge_parallel_8 0.23 K - 101.54x slower +4.28 ms 151 | 152 | ##### With input 4. One Tag ##### 153 | Name ips average deviation median 99th % 154 | fastgauge_parallel_8 21.37 K 0.0468 ms ±29.72% 0.0457 ms 0.0600 ms 155 | gauge_parallel_8 0.23 K 4.31 ms ±1.42% 4.30 ms 4.51 ms 156 | 157 | Comparison: 158 | fastgauge_parallel_8 21.37 K 159 | gauge_parallel_8 0.23 K - 92.08x slower +4.26 ms 160 | 161 | ##### With input 5. Five Tags ##### 162 | Name ips average deviation median 99th % 163 | fastgauge_parallel_8 12.57 K 0.0796 ms ±11.61% 0.0770 ms 0.118 ms 164 | gauge_parallel_8 0.23 K 4.32 ms ±1.28% 4.31 ms 4.48 ms 165 | 166 | Comparison: 167 | fastgauge_parallel_8 12.57 K 168 | gauge_parallel_8 0.23 K - 54.34x slower +4.24 ms 169 | 170 | ##### With input 6. Ten Tags ##### 171 | Name ips average deviation median 99th % 172 | fastgauge_parallel_8 7.56 K 0.132 ms ±17.93% 0.126 ms 0.25 ms 173 | gauge_parallel_8 0.23 K 4.39 ms ±2.68% 4.36 ms 4.85 ms 174 | 175 | Comparison: 176 | fastgauge_parallel_8 7.56 K 177 | gauge_parallel_8 0.23 K - 33.17x slower +4.25 ms 178 | Suite saved in external term format at bench/results/fast_gauge/parallel_8.benchee 179 | 180 | 181 | Benchee output comparing one ETS table vs one ETS table per scheduler: 182 | ========================================================================================================= 183 | Operating System: Linux 184 | CPU Information: Intel(R) Xeon(R) CPU @ 3.10GHz 185 | Number of Available Cores: 16 186 | Available memory: 62.79 GB 187 | Elixir 1.15.5 188 | Erlang 25.3.2.7 189 | JIT enabled: true 190 | 191 | Benchmark suite executing with the following configuration: 192 | warmup: 2 s 193 | time: 5 s 194 | memory time: 0 ns 195 | reduction time: 0 ns 196 | parallel: 8 197 | inputs: 1. No Options, 2. No Tags, 3. Empty Tags, 4. One Tag, 5. Five Tags, 6. Ten Tags 198 | Estimated total run time: 2 min 6 s 199 | 200 | Benchmarking fastgauge_multitable_parallel_8 with input 1. No Options ... 201 | Benchmarking fastgauge_multitable_parallel_8 with input 2. No Tags ... 202 | Benchmarking fastgauge_multitable_parallel_8 with input 3. Empty Tags ... 203 | Benchmarking fastgauge_multitable_parallel_8 with input 4. One Tag ... 204 | Benchmarking fastgauge_multitable_parallel_8 with input 5. Five Tags ... 205 | Benchmarking fastgauge_multitable_parallel_8 with input 6. Ten Tags ... 206 | Benchmarking fastgauge_parallel_8 with input 1. No Options ... 207 | Benchmarking fastgauge_parallel_8 with input 2. No Tags ... 208 | Benchmarking fastgauge_parallel_8 with input 3. Empty Tags ... 209 | Benchmarking fastgauge_parallel_8 with input 4. One Tag ... 210 | Benchmarking fastgauge_parallel_8 with input 5. Five Tags ... 211 | Benchmarking fastgauge_parallel_8 with input 6. Ten Tags ... 212 | Benchmarking gauge_parallel_8 with input 1. No Options ... 213 | Benchmarking gauge_parallel_8 with input 2. No Tags ... 214 | Benchmarking gauge_parallel_8 with input 3. Empty Tags ... 215 | Benchmarking gauge_parallel_8 with input 4. One Tag ... 216 | Benchmarking gauge_parallel_8 with input 5. Five Tags ... 217 | Benchmarking gauge_parallel_8 with input 6. Ten Tags ... 218 | Calculating statistics... 219 | Formatting results... 220 | 221 | ##### With input 1. No Options ##### 222 | Name ips average deviation median 99th % 223 | fastgauge_multitable_parallel_8 33.45 K 29.90 μs ±32.16% 29.20 μs 39.18 μs 224 | fastgauge_parallel_8 1.52 K 658.56 μs ±7.48% 656.00 μs 772.47 μs 225 | gauge_parallel_8 0.23 K 4380.56 μs ±5.49% 4311.94 μs 5607.94 μs 226 | 227 | Comparison: 228 | fastgauge_multitable_parallel_8 33.45 K 229 | fastgauge_parallel_8 1.52 K - 22.03x slower +628.66 μs 230 | gauge_parallel_8 0.23 K - 146.52x slower +4350.66 μs 231 | 232 | ##### With input 2. No Tags ##### 233 | Name ips average deviation median 99th % 234 | fastgauge_multitable_parallel_8 26.01 K 38.44 μs ±17.78% 37.43 μs 49.91 μs 235 | fastgauge_parallel_8 1.41 K 707.61 μs ±11.25% 702.61 μs 826.31 μs 236 | gauge_parallel_8 0.23 K 4341.18 μs ±2.04% 4292.52 μs 4537.77 μs 237 | 238 | Comparison: 239 | fastgauge_multitable_parallel_8 26.01 K 240 | fastgauge_parallel_8 1.41 K - 18.41x slower +669.17 μs 241 | gauge_parallel_8 0.23 K - 112.93x slower +4302.74 μs 242 | 243 | ##### With input 3. Empty Tags ##### 244 | Name ips average deviation median 99th % 245 | fastgauge_multitable_parallel_8 23.86 K 41.92 μs ±15.20% 41.41 μs 52.07 μs 246 | fastgauge_parallel_8 1.36 K 737.82 μs ±26.06% 704.00 μs 1713.14 μs 247 | gauge_parallel_8 0.23 K 4400.16 μs ±2.25% 4437.52 μs 4609.30 μs 248 | 249 | Comparison: 250 | fastgauge_multitable_parallel_8 23.86 K 251 | fastgauge_parallel_8 1.36 K - 17.60x slower +695.90 μs 252 | gauge_parallel_8 0.23 K - 104.97x slower +4358.24 μs 253 | 254 | ##### With input 4. One Tag ##### 255 | Name ips average deviation median 99th % 256 | fastgauge_multitable_parallel_8 21.60 K 46.29 μs ±16.51% 45.78 μs 56.20 μs 257 | fastgauge_parallel_8 1.32 K 756.86 μs ±8.59% 751.72 μs 887.25 μs 258 | gauge_parallel_8 0.23 K 4380.75 μs ±2.10% 4338.55 μs 4613.73 μs 259 | 260 | Comparison: 261 | fastgauge_multitable_parallel_8 21.60 K 262 | fastgauge_parallel_8 1.32 K - 16.35x slower +710.57 μs 263 | gauge_parallel_8 0.23 K - 94.63x slower +4334.45 μs 264 | 265 | ##### With input 5. Five Tags ##### 266 | Name ips average deviation median 99th % 267 | fastgauge_multitable_parallel_8 12.63 K 79.17 μs ±14.96% 76.39 μs 136.93 μs 268 | fastgauge_parallel_8 1.01 K 993.15 μs ±30.14% 961.91 μs 1721.61 μs 269 | gauge_parallel_8 0.23 K 4385.09 μs ±2.42% 4334.29 μs 4610.62 μs 270 | 271 | Comparison: 272 | fastgauge_multitable_parallel_8 12.63 K 273 | fastgauge_parallel_8 1.01 K - 12.54x slower +913.98 μs 274 | gauge_parallel_8 0.23 K - 55.39x slower +4305.91 μs 275 | 276 | ##### With input 6. Ten Tags ##### 277 | Name ips average deviation median 99th % 278 | fastgauge_multitable_parallel_8 7930.74 0.126 ms ±11.33% 0.123 ms 0.198 ms 279 | fastgauge_parallel_8 806.44 1.24 ms ±48.83% 1.11 ms 5.55 ms 280 | gauge_parallel_8 226.29 4.42 ms ±2.60% 4.37 ms 4.79 ms 281 | 282 | Comparison: 283 | fastgauge_multitable_parallel_8 7930.74 284 | fastgauge_parallel_8 806.44 - 9.83x slower +1.11 ms 285 | gauge_parallel_8 226.29 - 35.05x slower +4.29 ms 286 | Suite saved in external term format at bench/results/fast_gauge/parallel_8.benchee 287 | --------------------------------------------------------------------------------