lib/benchee/formatters/console.ex

defmodule Benchee.Formatters.Console do
  @moduledoc """
  Formatter to print out the results of benchmarking suite to the console.

  Example:

      Name                  ips        average  deviation         median         99th %
      flat_map           2.40 K      417.00 μs     ±9.40%      411.45 μs      715.21 μs
      map.flatten        1.24 K      806.89 μs    ±16.62%      768.02 μs     1170.67 μs

      Comparison:
      flat_map           2.40 K
      map.flatten        1.24 K - 1.93x slower

      Memory usage statistics:

      Name           Memory usage
      flat_map          624.97 KB
      map.flatten       781.25 KB - 1.25x memory usage

      **All measurements for memory usage were the same**

      Reduction count statistics:

      Name              average  deviation      median      99th %
      flat_map           417.00      ±9.40      411.45      715.21
      map.flatten        806.89     ±16.62      768.02     1170.67

      Comparison:
      flat_map           417.00
      map.flatten        806.89 - 1.93x more reductions
  """

  @behaviour Benchee.Formatter

  alias Benchee.Suite
  alias Benchee.Formatters.Console.{Memory, Reductions, RunTime}

  @doc """
  Formats the benchmark statistics to a report suitable for output on the CLI.

  Returns a list of lists, where each list element is a group belonging to one
  specific input. So if there only was one (or no) input given through `:inputs`
  then there's just one list inside.

  ## Examples

  ```
  iex> scenarios = [
  ...>   %Benchee.Scenario{
  ...>     name: "My Job", input_name: "My input", run_time_data: %Benchee.CollectionData{
  ...>       statistics: %Benchee.Statistics{
  ...>         average: 200.0,
  ...>         ips: 5000.0,
  ...>         std_dev_ratio: 0.1,
  ...>         median: 190.0,
  ...>         percentiles: %{99 => 300.1},
  ...>         sample_size: 200
  ...>       }
  ...>     },
  ...>     memory_usage_data: %Benchee.CollectionData{statistics: %Benchee.Statistics{}}
  ...>   },
  ...>   %Benchee.Scenario{
  ...>     name: "Job 2", input_name: "My input", run_time_data: %Benchee.CollectionData{
  ...>       statistics: %Benchee.Statistics{
  ...>         average: 400.0,
  ...>         ips: 2500.0,
  ...>         std_dev_ratio: 0.2,
  ...>         median: 390.0,
  ...>         percentiles: %{99 => 500.1},
  ...>         sample_size: 200
  ...>       }
  ...>     },
  ...>     memory_usage_data: %Benchee.CollectionData{statistics: %Benchee.Statistics{}}
  ...>   }
  ...> ]
  iex> suite = %Benchee.Suite{
  ...>   scenarios: scenarios,
  ...>   configuration: %Benchee.Configuration{
  ...>     unit_scaling: :best,
  ...>   }
  ...> }
  iex> format(suite, %{comparison: false, extended_statistics: false})
  [["\n##### With input My input #####", "\nName             ips        average  deviation         median         99th %\n",
  "My Job           5 K         200 ns    ±10.00%         190 ns      300.10 ns\n",
  "Job 2         2.50 K         400 ns    ±20.00%         390 ns      500.10 ns\n"]]

  ```

  """
  @impl true
  @spec format(Suite.t(), map) :: [any]
  def format(%Suite{scenarios: scenarios, configuration: config}, options \\ %{}) do
    config =
      config
      |> Map.take([:unit_scaling, :title])
      |> Map.merge(options)

    scenarios
    |> Enum.reduce([], &update_grouped_list/2)
    |> Enum.map(fn {input, scenarios} ->
      generate_output(scenarios, config, input)
    end)
  end

  # Normally one would prepend to lists and not append. In this case this lead to 2
  # `Enum.reverse` scattered around. As these lists are usually very small (mostly less
  # than 10 elements) I opted for `++` here.
  defp update_grouped_list(scenario, grouped_scenarios) do
    case List.keyfind(grouped_scenarios, scenario.input_name, 0) do
      {_, group} ->
        new_tuple = {scenario.input_name, group ++ [scenario]}
        List.keyreplace(grouped_scenarios, scenario.input_name, 0, new_tuple)

      _ ->
        grouped_scenarios ++ [{scenario.input_name, [scenario]}]
    end
  end

  @doc """
  Takes the output of `format/1` and writes that to the console.
  """
  @impl true
  @spec write(any, map) :: :ok | {:error, String.t()}
  def write(output, _options \\ %{}) do
    IO.write(output)
  rescue
    _ -> {:error, "Unknown Error"}
  end

  defp generate_output(scenarios, config, input) do
    [
      suite_header(input, config)
      | RunTime.format_scenarios(scenarios, config) ++
          Memory.format_scenarios(scenarios, config) ++
          Reductions.format_scenarios(scenarios, config)
    ]
  end

  defp suite_header(input, config) do
    "#{title_header(config)}#{input_header(input)}"
  end

  defp title_header(%{title: nil}), do: ""
  defp title_header(%{title: title}), do: "\n*** #{title} ***\n"

  @no_input_marker Benchee.Benchmark.no_input()
  defp input_header(input) when input == @no_input_marker, do: ""
  defp input_header(input), do: "\n##### With input #{input} #####"
end