lib/histogrex.ex

defmodule Histogrex do
  @moduledoc """
  High Dynamic Range (HDR) Histogram allows the recording of values across
  a configurable range at a configurable precision.

  Storage requirement is fixed (and depends on how the histogram is configured).
  All functions are fully executed within the calling process (not serialized
  through a single process) as the data is stored within write-optimized ETS
  table. Each recording consists of as single call to `:ets.update_counter`.

  Read operations consiste of a single `:ets.lookup` and are subsequently
  processed on that copy of the data (again, within the calling process).

  The fist step involves creating a registry:

    defmodule MyApp.Stats do
      use Histogrex

      histogrex :load_user, min: 1, max: 10_000_000, precision: 3
      histogrex :db_save_settings, min: 1, max: 10_000, precision: 2
      ...
    end

  And then adding this module as a `worker` to your application's supervisor
  tree:

      worker(MyApp.Stats, [])

  You can then record values and make queries:

      alias MyApp.Stats

      Stats.record!(:load_user, 233)
      Stats.record!(:db_save_settings, 84)

      Stats.mean(:load_user)
      Stats.max(:db_save_settings)
      Stats.total_count(:db_save_settings)
      Stats.value_at_quantile(:load_user, 99.9)
  """
  import Bitwise

  @total_count_index 2

  defstruct [
    :name, :registrar, :bucket_count, :counts_length, :unit_magnitude,
    :sub_bucket_mask, :sub_bucket_count, :sub_bucket_half_count,
    :sub_bucket_half_count_magnitude, :template
  ]

  @type t :: %Histogrex{
    name: atom | binary, registrar: module,
    bucket_count: pos_integer, counts_length: pos_integer,
    unit_magnitude: non_neg_integer, sub_bucket_mask: non_neg_integer,
    sub_bucket_count: non_neg_integer, sub_bucket_half_count: non_neg_integer,
    sub_bucket_half_count_magnitude: non_neg_integer, template: nil | tuple,
  }

  defmodule Iterator do
    @moduledoc false
    defstruct [
      :h, :total_count, :counts,
      bucket_index: 0, sub_bucket_index: -1, count_at_index: 0,
      count_to_index: 0, value_from_index: 0, highest_equivalent_value: 0,
    ]

    @type t :: %Iterator{
      h: struct, total_count: non_neg_integer,
      bucket_index: non_neg_integer, sub_bucket_index: integer,
      count_at_index: non_neg_integer, count_to_index: non_neg_integer,
      value_from_index: non_neg_integer, highest_equivalent_value: non_neg_integer,
    }

    @doc """
    Resets the iterator so that it can be reused. There shouldn't be a need to
    execute this directly.
    """
    @spec reset(t) :: t
    def reset(it) do
      %{it |
        bucket_index: 0, sub_bucket_index: -1, count_at_index: 0,
        count_to_index: 0, value_from_index: 0, highest_equivalent_value: 0
      }
    end

    @doc false
    @spec empty() :: t
    def empty() do
      h = %Histogrex{
        bucket_count: 0, counts_length: 1, sub_bucket_mask: 0, unit_magnitude: 0,
        sub_bucket_half_count: 0, sub_bucket_half_count_magnitude: 0, sub_bucket_count: 0,
      }
      %Iterator{total_count: 0, counts: 0, sub_bucket_index: 0, h: h}
    end
  end

  @doc false
  defmacro __using__(_opts) do
    quote location: :keep do
      use GenServer
      import Histogrex, only: [histogrex: 2, template: 2]
      alias Histogrex.Iterator, as: It

      @before_compile Histogrex

      Module.register_attribute(__MODULE__, :histogrex_names, accumulate: true)
      Module.register_attribute(__MODULE__, :histogrex_registry, accumulate: true)

      @doc false
      def start_link(init_arg \\ :ok) do
        {:ok, pid} = GenServer.start_link(__MODULE__, init_arg)
        GenServer.call(pid, :register_tables)
        {:ok, pid}
      end

      def init(args) do
        {:ok, args}
      end

      @doc false
      # we can't inline register_tables() in here as we need the @histogrex_registry
      # module to get loaded with values. register_tables is created via the
      # @before_compile hook
      def handle_call(:register_tables, _from, state) do
        register_tables()
        {:reply, :ok, state}
      end

      @doc false
      @spec record!(atom, non_neg_integer) :: :ok | no_return
      def record!(metric, value) when is_number(value), do: record_n!(metric, value, 1)

      @doc false
      @spec record_n!(atom, non_neg_integer, non_neg_integer) :: :ok | no_return
      def record_n!(metric, value, n) when is_number(value) do
        Histogrex.record!(get_histogrex(metric), value, n)
      end

      @doc false
      @spec record(atom, non_neg_integer) :: :ok | {:error, binary}
      def record(metric, value) when is_number(value), do: record_n(metric, value, 1)

      @doc false
      @spec record_n(atom, non_neg_integer, non_neg_integer) :: :ok | {:error, binary}
      def record_n(metric, value, n) when is_number(value) do
        Histogrex.record(get_histogrex(metric), value, n)
      end

      @doc false
      @spec record!(atom, atom | binary, non_neg_integer) :: :ok | no_return
      def record!(template, metric, value), do: record_n!(template, metric, value, 1)

      @doc false
      @spec record_n!(atom, atom | binary, non_neg_integer, non_neg_integer) :: :ok | no_return
      def record_n!(template, metric, value, n) do
        Histogrex.record!(get_histogrex(template), metric, value, n)
      end

      @doc false
      @spec record(atom, atom | binary, non_neg_integer) :: :ok | {:error, binary}
      def record(template, metric, value), do: record_n(template, metric, value, 1)

      @doc false
      @spec record_n(atom, atom | binary, non_neg_integer, non_neg_integer) :: :ok | {:error, binary}
      def record_n(template, metric, value, n) do
        Histogrex.record(get_histogrex(template), metric, value, n)
      end

      @doc false
      @spec value_at_quantile(Iterator.t | atom, number) :: non_neg_integer
      def value_at_quantile(%It{} = it, q) do
        Histogrex.value_at_quantile(it, q)
      end

      @doc false
      def value_at_quantile(metric, q) do
        Histogrex.value_at_quantile(get_histogrex(metric), q)
      end

      @doc false
      @spec value_at_quantile(atom, atom | binary, number) :: non_neg_integer
      def value_at_quantile(template, metric, q) do
        Histogrex.value_at_quantile(get_histogrex(template), metric, q)
      end

      @doc false
      @spec total_count(Iterator.t | atom) :: non_neg_integer
      def total_count(%It{} = it), do: Histogrex.total_count(it)

      @doc false
      def total_count(metric), do: Histogrex.total_count(get_histogrex(metric))

      @doc false
      @spec total_count(atom, atom | binary) :: non_neg_integer
      def total_count(template, metric), do: Histogrex.total_count(get_histogrex(template), metric)

      @doc false
      @spec mean(atom) :: non_neg_integer
      def mean(%It{} = it), do: Histogrex.mean(it)

      @doc false
      def mean(metric), do: Histogrex.mean(get_histogrex(metric))

      @doc false
      @spec mean(atom, atom | binary) :: non_neg_integer
      def mean(template, metric), do: Histogrex.mean(get_histogrex(template), metric)

      @doc false
      @spec max(Iterator.t | atom) :: non_neg_integer
      def max(%It{} = it), do: Histogrex.max(it)

      @doc false
      def max(metric), do: Histogrex.max(get_histogrex(metric))

      @doc false
      @spec max(atom, atom | binary) :: non_neg_integer
      def max(template, metric), do: Histogrex.max(get_histogrex(template), metric)

      @doc false
      @spec min(Iterator.t | atom) :: non_neg_integer
      def min(%It{} = it), do: Histogrex.min(it)

      @doc false
      def min(metric), do: Histogrex.min(get_histogrex(metric))

      @doc false
      @spec min(atom, atom | binary) :: non_neg_integer
      def min(template, metric), do: Histogrex.min(get_histogrex(template), metric)

      @doc false
      @spec reset(Iterator.t | atom) :: :ok
      def reset(%It{} = it), do: Histogrex.reset(it)

      @doc false
      def reset(metric), do: Histogrex.reset(get_histogrex(metric))

      @doc false
      @spec reset(atom, atom | binary) :: :ok
      def reset(template, metric), do: Histogrex.reset(get_histogrex(template), metric)

      @doc false
      @spec iterator(atom) :: Iterator.t
      def iterator(metric) do
        Histogrex.iterator(get_histogrex(metric))
      end

      @doc false
      @spec iterator(atom, atom | binary) :: Iterator.t
      def iterator(template, metric) do
        Histogrex.iterator(get_histogrex(template), metric)
      end

      @doc false
      @spec delete(atom) :: :ok
      def delete(metric), do: Histogrex.delete(get_histogrex(metric))

      @doc false
      @spec delete(atom, atom | binary) :: :ok
      def delete(template, metric), do: Histogrex.delete(get_histogrex(template), metric)

      @doc false
      @spec reduce(any, ((Iterator.t, any) -> any)) :: any
      def reduce(acc, fun), do: Histogrex.reduce(__MODULE__, acc, fun)
    end
  end

  @doc false
  defmacro __before_compile__(_env) do
    quote do
      @spec get_names() :: [atom]
      def get_names(), do: @histogrex_names

      defp register_tables() do
        :ets.new(__MODULE__, [:set, :public, :named_table, write_concurrency: true])
        for h <- @histogrex_registry do
          Histogrex.reset(h)
        end
      end

      def get_histogrex(_), do: {:error, :undefined}
    end
  end

  @doc """
  registers the histogram

  A `min`, `max` and `precision` must be supplied. `min` and `max` represent
  the minimal and maximal expected values. For example, if you're looking to
  measure how long a database call took, you could specify: `min: 1, max: 10_000`
  and provide time in millisecond, thus allowing you to capture values from 1ms
  to 10sec.

  `min` must be greater than 0.
  `max` must be greater than `min`.
  `precision` must be between 1 and 5 (inclusive).

  ## Examples

      histogrex :user_list, min: 1, max: 10000000, precision: 3

  """
  defmacro histogrex(name, opts) do
    quote location: :keep do
      Module.register_attribute(__MODULE__, unquote(name), [])
      Module.put_attribute(__MODULE__, unquote(name), Histogrex.new(unquote(name), __MODULE__, unquote(opts)[:min], unquote(opts)[:max], unquote(opts)[:precision] || 3, false))
      Module.register_attribute(__MODULE__, :histrogrex_names, [])
      Module.put_attribute(__MODULE__, :histogrex_names, unquote(name))
      Module.register_attribute(__MODULE__, :histogrex_registry, [])
      Module.put_attribute(__MODULE__, :histogrex_registry,  Histogrex.new(unquote(name), __MODULE__, unquote(opts)[:min], unquote(opts)[:max], unquote(opts)[:precision] || 3, false))
      def get_histogrex(unquote(name)), do: Histogrex.new(unquote(name), __MODULE__, unquote(opts)[:min], unquote(opts)[:max], unquote(opts)[:precision] || 3, false)
    end
  end

  defmacro template(name, opts) do
    quote location: :keep do
      Module.register_attribute(__MODULE__, unquote(name), [])
      Module.put_attribute(__MODULE__, unquote(name), Histogrex.new(unquote(name), __MODULE__, unquote(opts)[:min], unquote(opts)[:max], unquote(opts)[:precision] || 3, true))
      def get_histogrex(unquote(name)), do: Histogrex.new(unquote(name), __MODULE__, unquote(opts)[:min], unquote(opts)[:max], unquote(opts)[:precision] || 3, true)
    end
  end

  @doc """
  Creates a new histogrex object. Note that this only creates the configuration
  structure. It does not create the underlying ets table/entries. There should
  be no need to call this direction. Use the `histogrex` macro instead.
  """
  @spec new(binary | atom, module, pos_integer, pos_integer, 1..5, boolean) :: t
  def new(name, registrar, min, max, precision \\ 3, template \\ false) when min > 0 and max > min and precision in (1..5) do
    largest_value_with_single_unit_resolution = 2 * :math.pow(10, precision)
    sub_bucket_count_magnitude = round(Float.ceil(:math.log2(largest_value_with_single_unit_resolution)))

    sub_bucket_half_count_magnitude = case sub_bucket_count_magnitude < 1 do
      true -> 1
      false -> sub_bucket_count_magnitude - 1
    end

    unit_magnitude = case round(Float.floor(:math.log2(min))) do
      n when n < 0 -> 0
      n -> n
    end

    sub_bucket_count = round(:math.pow(2, sub_bucket_half_count_magnitude + 1))
    sub_bucket_half_count = round(sub_bucket_count / 2)
    sub_bucket_mask = bsl(sub_bucket_count - 1, unit_magnitude)

    bucket_count = calculate_bucket_count(bsl(sub_bucket_count, unit_magnitude), max, 1)
    counts_length = round((bucket_count + 1) * (sub_bucket_count / 2))

    template = case template do
      false -> nil
      true -> create_row(name, name, counts_length)
    end

    %__MODULE__{
      name: name,
      template: template,
      registrar: registrar,
      bucket_count: bucket_count,
      counts_length: counts_length,
      unit_magnitude: unit_magnitude,
      sub_bucket_mask: sub_bucket_mask,
      sub_bucket_count: sub_bucket_count,
      sub_bucket_half_count: sub_bucket_half_count,
      sub_bucket_half_count_magnitude: sub_bucket_half_count_magnitude,
    }
  end

  @doc """
  Same as `record/3` but raises on error
  """
  @spec record!(t, pos_integer, pos_integer) :: :ok | no_return
    def record!(h, value, n) do
    case record(h, value, n) do
      :ok -> :ok
      {:error, message} -> raise message
    end
  end

  @doc """
  Records the `value` `n` times where `n` defaults to 1. Return `:ok` on success
  or `{:error, message}` on failure. A larger than the 'max' specified
  when the histogram was created will cause an error. This is usually not called
  directly, but rather through the `record/3` of your custom registry
  """
  @spec record(t | {:error, any}, pos_integer, pos_integer) :: :ok | {:error, any}
  def record({:error, _} = err, _value, _n), do: err

  def record(h, value, n) do
    index = get_value_index(h, value)
    case index < 0 or h.counts_length <= index do
      true -> {:error, "value it outside of range"}
      false ->
        :ets.update_counter(h.registrar, h.name, [{3, n}, {index+4, n}])
        :ok
    end
  end

  @doc """
  Same as `record_template/4` but raises on error
  """
  @spec record!(t, atom | binary, pos_integer, pos_integer) :: :ok | no_return
  def record!(template, metric, value, n) do
    case record(template, metric, value, n) do
      :ok -> :ok
      {:error, message} -> raise message
    end
  end

  @doc """
  Records the `value` `n` times where `n` defaults to 1. Uses the template
  to create the histogram if it doesn't already exist. Return `:ok` on success
  or `{:error, message}` on failure. A larger than the 'max' specified
  when the histogram was created will cause an error. This is usually not called
  directly, but rather through the `record/3` of your custom registry
  """
  @spec record(t | {:error, any}, atom | binary, pos_integer, pos_integer) :: :ok | {:error, any}
  def record({:error, _} = err, _metric, _value, _n), do: err

  def record(h, metric, value, n) do
    index = get_value_index(h, value)
    case index < 0 or h.counts_length <= index do
      true -> {:error, "value it outside of range"}
      false ->
        :ets.update_counter(h.registrar, metric, [{3, n}, {index+4, n}], h.template)
        :ok
    end
  end

  @doc """
  Gets the value at the requested quantile. The quantile must be greater than 0
  and less than or equal to 100. It can be a float.

  Gets the value at the requested quantile using the given iterator. When doing
  multiple calculations, it is slightly more efficent to first recreate and then
  re-use an iterator (plus the values will consistently be calculated based on
  the same data). Iterators are automatically reset before each call.

  Gets the value at the requested quantile for the templated histogram
  """
  @spec value_at_quantile(t | Iterator.t, float) :: float
  def value_at_quantile(%Histogrex{} = h, q) when q > 0 and q <= 100 do
    do_value_at_quantile(iterator(h), q)
  end

  def value_at_quantile(%Iterator{} = it, q) when q > 0 and q <= 100 do
    do_value_at_quantile(Iterator.reset(it), q)
  end

  @spec value_at_quantile(t, atom, float) :: float
  def value_at_quantile(%Histogrex{} = h, metric, q) when q > 0 and q <= 100 do
    do_value_at_quantile(iterator(h, metric), q)
  end

  defp do_value_at_quantile(it, q) do
    count_at_percetile = round(Float.floor((q / 100 * it.total_count) + 0.5))

    Enum.reduce_while(it, 0, fn it, total ->
      total = total + it.count_at_index
      case total >= count_at_percetile do
        true -> {:halt, highest_equivalent_value(it.h, it.value_from_index)}
        false -> {:cont, total}
      end
    end)
  end

  @doc """
  Returns the mean value
  """
  @spec mean(t | Iterator.t) :: float
  def mean(%Histogrex{} = h), do: do_mean(iterator(h))

  def mean(%Iterator{} = it), do: do_mean(Iterator.reset(it))

  def mean({:error, _}), do: 0

  def mean(%Histogrex{} = h, metric), do: do_mean(iterator(h, metric))

  def mean({:error, _}, _metric), do: 0

  defp do_mean(it) do
    case it.total_count == 0 do
      true -> 0
      false ->
        total = Enum.reduce(it, 0, fn it, total ->
          case it.count_at_index do
            0 -> total
            n -> total + n * median_equivalent_value(it.h, it.value_from_index)
          end
        end)
        total / it.total_count
    end
  end

  @doc """
  Resets the histogram to 0 values. Note that the histogram is a fixed-size, so
  calling this won't free any memory. It is useful for testing.
  """
  @spec reset(t | Iterator.t) :: :ok
  def reset(%Histogrex{} = h) do
    :ets.insert(h.registrar, create_row(h.name, nil, h.counts_length))
    :ok
  end

  def reset(%Iterator{} = it) do
    h = it.h
    # cannot use it.h.name as this could be a dynamic metric and we don't want
    # the template name
    name = elem(it.counts, 0)
    :ets.insert(h.registrar, create_row(name, nil, h.counts_length))
    :ok
  end

  @spec reset(t, atom | binary) :: :ok
  def reset(%Histogrex{} = h, metric) do
    :ets.insert(h.registrar, create_row(metric, h.name, h.counts_length))
    :ok
  end

  @doc """
  Deletes the histogram. The histogram can no longer be used.
  """
  @spec delete(t) :: :ok
  def delete(%Histogrex{} = h), do: delete(h, h.name)

  @doc """
  Deletes the histogram. Since this histogram was dynamically created through
  a template, you can safely continue using it.
  """
  @spec delete(t, atom | binary) :: :ok
  def delete(%Histogrex{} = h, metric) do
    :ets.delete(h.registrar, metric)
    :ok
  end

  defp create_row(name, template, count) do
    # +1 for the total_count that we'll store at the start
    List.to_tuple([name |  [template | List.duplicate(0, count + 1)]])
  end

  @doc """
  Get the total number of recorded values. This is O(1)
  """
  @spec total_count(t | Iterator.t) :: non_neg_integer
  def total_count(%Histogrex{} = h) do
    elem(get_counts(h), @total_count_index)
  end

  def total_count(%Iterator{} = it) do
    it.total_count
  end

  def total_count({:error, _}), do: 0

  @spec total_count(t, atom | binary) :: non_neg_integer
  def total_count(%Histogrex{} = h, metric) do
    case get_counts(h, metric) do
      nil -> 0
      counts -> elem(counts, @total_count_index)
    end
  end

  def total_count({:error, _}, _metric), do: 0

  @doc """
  Gets the approximate maximum value recorded. Works both with Iterator and Histogram
  """
  @spec max(t | Iterator.t) :: non_neg_integer
  def max(%Histogrex{} = h), do: do_max(iterator(h))

  def max(%Iterator{} = it), do: do_max(Iterator.reset(it))

  def max({:error, _}), do: 0

  def max(%Histogrex{} = h, metric), do: do_max(iterator(h, metric))

  def max({:error, _}, _metric), do: 0

  defp do_max(it) do
    max = Enum.reduce(it, 0, fn it, max ->
      case it.count_at_index == 0 do
        true -> max
        false -> it.highest_equivalent_value
      end
    end)
    highest_equivalent_value(it.h, max)
  end

  @doc """
  Gets the approximate minimum value recorded. Works both with Iterator and Histogram
  """
  @spec min(t | Iterator.t) :: non_neg_integer
  def min(%Histogrex{} = h), do: do_min(iterator(h))

  def min(%Iterator{} = it), do: do_min(Iterator.reset(it))

  def min({:error, _}), do: 0

  @doc """
  Returns the approximate minimum value from a templated histogram
  """
  def min(%Histogrex{} = h, metric), do: do_min(iterator(h, metric))

  @doc false
  def min({:error, _}, _metric), do: 0

  defp do_min(it) do
    min = Enum.reduce_while(it, 0, fn it, min ->
      case it.count_at_index != 0 && min == 0 do
        true -> {:halt, it.highest_equivalent_value}
        false -> {:cont, min}
      end
    end)
    lowest_equivalent_value(it.h, min)
  end

  @doc false
  @spec iterator(t | {:error, any}) :: Iterator.t
  def iterator({:error, _}), do: Iterator.empty()

  def iterator(h) do
    counts = get_counts(h)
    %Iterator{h: h, counts: counts, total_count: elem(counts, @total_count_index)}
  end

  @doc false
  @spec iterator(t | {:error, any}, atom | binary) :: Iterator.t
  def iterator({:error, _}, _metric), do: Iterator.empty()

  def iterator(h, metric) do
    case get_counts(h, metric) do
      nil ->  Iterator.empty()
      counts -> %Iterator{h: h, counts: counts, total_count: elem(counts, @total_count_index)}
    end
  end

  @doc """
  Reduce all of a registry's histograms
  """
  @spec reduce(module, any, ((Iterator.t, any)-> any)) :: any
  def reduce(module, acc, fun) do
    f = fn counts, acc ->
      name = elem(counts, 0)
      h = case elem(counts, 1) do
        nil -> module.get_histogrex(name)
        template -> module.get_histogrex(template)
      end
      it = %Iterator{h: h, counts: counts, total_count: elem(counts, @total_count_index)}
      fun.({name, it}, acc)
    end
    :ets.foldl(f, acc, module)
  end

  defp get_counts(h), do: get_counts(h, h.name)

  defp get_counts(h, metric) do
    case :ets.lookup(h.registrar, metric) do
      [] -> nil
      [counts] -> counts
    end
  end

  defp calculate_bucket_count(smallest_untrackable_value, max, bucket_count) do
    case smallest_untrackable_value < max do
      false -> bucket_count
      true -> calculate_bucket_count(bsl(smallest_untrackable_value, 1), max, bucket_count + 1)
    end
  end

  defp get_value_index(h, value) do
    {bucket, sub} = get_bucket_indexes(h, value)
    get_count_index(h, bucket, sub)
  end

  @doc false
  def get_count_index(h, bucket_index, sub_bucket_index) do
    bucket_base_index = bsl(bucket_index + 1, h.sub_bucket_half_count_magnitude)
    offset_in_bucket = sub_bucket_index - h.sub_bucket_half_count
    bucket_base_index + offset_in_bucket
  end

  @doc false
  def value_from_index(h, bucket_index, sub_bucket_index) do
    bsl(sub_bucket_index, bucket_index + h.unit_magnitude)
  end

  @doc false
  def highest_equivalent_value(h, value) do
    next_non_equivalent_value(h, value) - 1
  end

  def lowest_equivalent_value(h, value) do
    {bucket_index, sub_bucket_index} = get_bucket_indexes(h, value)
    lowest_equivalent_value(h, bucket_index, sub_bucket_index)
  end

  def lowest_equivalent_value(h, bucket_index, sub_bucket_index) do
    value_from_index(h, bucket_index, sub_bucket_index)
  end

  defp next_non_equivalent_value(h, value) do
    {bucket_index, sub_bucket_index} = get_bucket_indexes(h, value)
    lowest_equivalent_value(h, bucket_index, sub_bucket_index) + size_of_equivalent_value_range(h, bucket_index, sub_bucket_index)
  end

  defp median_equivalent_value(h, value) do
    {bucket_index, sub_bucket_index} = get_bucket_indexes(h, value)
    lowest_equivalent_value(h, bucket_index, sub_bucket_index) + bsr(size_of_equivalent_value_range(h, bucket_index, sub_bucket_index), 1)
  end

  defp size_of_equivalent_value_range(h, bucket_index, sub_bucket_index) do
    adjusted_bucket_index = case sub_bucket_index >= h.sub_bucket_count do
      true -> bucket_index + 1
      false -> bucket_index
    end
    bsl(1, h.unit_magnitude + adjusted_bucket_index)
  end

  @doc false
  defp get_bucket_indexes(h, value) do
    ceiling = bit_length(bor(value, h.sub_bucket_mask), 0)
    bucket_index = ceiling - h.unit_magnitude - (h.sub_bucket_half_count_magnitude + 1)

    sub_bucket_index = bsr(value, bucket_index + h.unit_magnitude)
    {bucket_index, sub_bucket_index}
  end

  defp bit_length(value, n) when value >= 32768 do
    bit_length(bsr(value, 16), n + 16)
  end

  defp bit_length(value, n) do
    {value, n} = case value >= 128 do
      true -> {bsr(value, 8), n + 8}
      false -> {value, n}
    end

    {value, n} = case value >= 8 do
      true -> {bsr(value, 4), n + 4}
      false -> {value, n}
    end

    {value, n} = case value >= 2 do
      true -> {bsr(value, 2), n + 2}
      false -> {value, n}
    end

    case value == 1 do
      true -> n + 1
      false -> n
    end
  end
end

defimpl Enumerable, for: Histogrex.Iterator do
  @doc """
  Gets the total count of recorded samples. This is an 0(1) operation
  """
  def count(it), do: it.total_count

  @doc false
  def member?(_it, _value), do: {:error, __MODULE__}

  @doc false
  def slice(_it), do: {:error, __MODULE__}

  @doc false
  def reduce(_it, {:halt, acc}, _f), do: {:halted, acc}

  @doc false
  def reduce(it, {:suspend, acc}, f), do: {:suspended, acc, &reduce(it, &1, f)}

  @doc false
  def reduce(it, {:cont, acc}, f) do
    case it.count_to_index >= it.total_count do
      true -> {:done, acc}
      false -> do_reduce(it, acc ,f)
    end
  end

  @doc false
  defp do_reduce(it, acc, f) do
    h = it.h
    sub_bucket_index = it.sub_bucket_index + 1
    {it, bucket_index, sub_bucket_index} = case sub_bucket_index >= h.sub_bucket_count do
      true ->
        bucket_index = it.bucket_index + 1
        sub_bucket_index = h.sub_bucket_half_count
        it = %{it | bucket_index: bucket_index, sub_bucket_index: sub_bucket_index}
        {it, bucket_index, sub_bucket_index}
      false ->
        it = %{it | sub_bucket_index: sub_bucket_index}
        {it, it.bucket_index, sub_bucket_index}
    end

    case bucket_index >= h.bucket_count do
      true -> {:done, acc}
      false ->
        count_at_index = count_at_index(it, bucket_index, sub_bucket_index)
        value_from_index = Histogrex.value_from_index(h, bucket_index, sub_bucket_index)
        it = %{it |
          count_at_index: count_at_index,
          value_from_index: value_from_index,
          count_to_index: it.count_to_index + count_at_index,
          highest_equivalent_value: Histogrex.highest_equivalent_value(h, value_from_index),
        }
        reduce(it, f.(it, acc), f)
    end
  end

  defp count_at_index(it, bucket_index, sub_bucket_index) do
    index = Histogrex.get_count_index(it.h, bucket_index, sub_bucket_index)
    # 0 is the name
    # 1 is the template (or nil for static metrics)
    # 2 is the total_count
    # the real count buckets start at 3
    elem(it.counts, index + 3)
  end
end