lib/recode/stop_watch.ex

defmodule Recode.StopWatch do
  @moduledoc """
  A stopwatch to measure times in milliseconds.

  ## Examples

      iex> alias Recode.StopWatch
      iex> StopWatch.init!(:test)
      iex> StopWatch.start!(:test, :foo)
      iex> t1 = StopWatch.time!(:test, :foo)
      iex> Process.sleep(1)
      iex> t2 = StopWatch.time!(:test, :foo)
      iex> Process.sleep(1)
      iex> StopWatch.stop!(:test, :foo)
      iex> t3 = StopWatch.time!(:test, :foo)
      iex> t1 < t2 and t2 < t3
      true
      iex> t3 == StopWatch.time!(:test, :foo)
      true
  """
  @name :stop_watch

  defmacro __using__(_opts) do
    quote do
      require Recode.StopWatch
      alias Recode.StopWatch
    end
  end

  def init!(name \\ @name) do
    :ets.new(name, [:set, :public, :named_table])
  rescue
    ArgumentError ->
      reraise ArgumentError,
              [message: "StopWatch #{inspect(name)} already exisits."],
              __STACKTRACE__
  end

  @doc false
  def _put!(name, key, op, timestamp) do
    with false <- :ets.insert_new(name, {{key, op}, timestamp}) do
      raise ArgumentError,
            "StopWatch has already received the #{inspect(op)} operation for #{inspect(key)}."
    end
  end

  @doc false
  def _time!(name \\ @name, key, timestamp) do
    case :ets.match(name, {{key, :_}, :"$1"}) do
      [[timestamp1], [timestamp2]] -> abs(timestamp1 - timestamp2)
      [[timestamp1]] -> abs(timestamp - timestamp1)
    end
  end

  defmacro start!(name \\ @name, key) do
    quote bind_quoted: [name: name, key: key] do
      Recode.StopWatch._put!(name, key, :start, System.monotonic_time(:millisecond))
    end
  end

  defmacro stop!(name \\ @name, key) do
    quote bind_quoted: [name: name, key: key] do
      Recode.StopWatch._put!(name, key, :stop, System.monotonic_time(:millisecond))
    end
  end

  defmacro time!(name \\ @name, key) do
    quote bind_quoted: [name: name, key: key] do
      Recode.StopWatch._time!(name, key, System.monotonic_time(:millisecond))
    end
  end
end