defmodule Benchee.Suite do
@moduledoc """
Main Benchee data structure that aggregates the results from every step.
Different layers of the benchmarking rely on different data being present
here. For instance for `Benchee.Statistics.statistics/1` to work the
`run_time_data` key of each scenario needs to be filled with the samples
collected by `Benchee.Benchmark.collect/1`.
Formatters can then use the data to display all of the results and the
configuration.
"""
defstruct [
:system,
configuration: %Benchee.Configuration{},
scenarios: []
]
@typedoc """
Valid key for either input or benchmarking job names.
"""
@type key :: String.t() | atom
@typedoc """
The main suite consisting of the configuration data, information about the system and most
importantly a list of `t:Benchee.Scenario.t/0`.
"""
@type t :: %__MODULE__{
configuration: Benchee.Configuration.t() | nil,
system: Benchee.System.t() | nil,
scenarios: [] | [Benchee.Scenario.t()]
}
end
defimpl DeepMerge.Resolver, for: Benchee.Suite do
def resolve(original, override = %Benchee.Suite{}, resolver) do
cleaned_override =
override
|> Map.from_struct()
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> Map.new()
Map.merge(original, cleaned_override, resolver)
end
def resolve(original, override, resolver) when is_map(override) do
Map.merge(original, override, resolver)
end
end
if Code.ensure_loaded?(Table.Reader) do
defimpl Table.Reader, for: Benchee.Suite do
alias Benchee.CollectionData
alias Benchee.Scenario
def init(suite) do
measurements_processed = map_measurements_processed(suite)
columns = get_columns_from_suite(suite, measurements_processed)
{rows, count} = extract_rows_from_suite(suite, measurements_processed)
{:rows, %{columns: columns, count: count}, rows}
end
defp map_measurements_processed(suite) do
Enum.filter(Scenario.measurement_types(), fn type ->
Enum.any?(suite.scenarios, fn scenario -> Scenario.data_processed?(scenario, type) end)
end)
end
@run_time_fields [
"samples",
"ips",
"average",
"std_dev",
"median",
"minimum",
"maximum",
"mode",
"sample_size"
]
@non_run_time_fields List.delete(@run_time_fields, "ips")
defp get_columns_from_suite(suite, measurements_processed) do
config_percentiles = suite.configuration.percentiles
percentile_labels =
Enum.map(config_percentiles, fn percentile ->
"p_#{percentile}"
end)
measurement_headers =
Enum.flat_map(measurements_processed, fn measurement_type ->
fields = fields_for(measurement_type) ++ percentile_labels
Enum.map(fields, fn field -> "#{measurement_type}_#{field}" end)
end)
["job_name" | measurement_headers]
end
defp fields_for(:run_time), do: @run_time_fields
defp fields_for(_), do: @non_run_time_fields
defp extract_rows_from_suite(suite, measurements_processed) do
config_percentiles = suite.configuration.percentiles
Enum.map_reduce(suite.scenarios, 0, fn %Scenario{} = scenario, count ->
secenario_data =
Enum.flat_map(measurements_processed, fn measurement_type ->
scenario
|> Scenario.measurement_data(measurement_type)
|> get_stats_from_collection_data(measurement_type, config_percentiles)
end)
row = [scenario.job_name | secenario_data]
{row, count + 1}
end)
end
defp get_stats_from_collection_data(
%CollectionData{statistics: statistics, samples: samples},
measurement_type,
percentiles
) do
percentile_data =
Enum.map(percentiles, fn percentile -> statistics.percentiles[percentile] end)
Enum.concat([
[samples],
maybe_ips(statistics, measurement_type),
[
statistics.average,
statistics.std_dev,
statistics.median,
statistics.minimum,
statistics.maximum,
statistics.mode,
statistics.sample_size
],
percentile_data
])
end
defp maybe_ips(statistics, :run_time), do: [statistics.ips]
defp maybe_ips(_, _not_run_time), do: []
end
end