defmodule Benchee.Formatters.HTML do
@moduledoc """
Functionality for converting Benchee benchmarking results to an HTML page
with plotly.js generated graphs and friends.
## Examples
list = Enum.to_list(1..10_000)
map_fun = fn(i) -> [i, i * i] end
Benchee.run(
%{
"flat_map" => fn -> Enum.flat_map(list, map_fun) end,
"map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
},
formatters: [
Benchee.Formatters.Console,
{Benchee.Formatters.HTML, file: "samples_output/flat_map.html"}
]
)
"""
@behaviour Benchee.Formatter
alias Benchee.{
Configuration,
Conversion,
Formatters.HTML.Render,
Formatters.JSON,
Suite,
Utility.FileCreation
}
@doc """
Transforms the statistical results from benchmarking to html reports.
Returns a map from file name/path to file content. This list is ready to be put into
`Benchee.Utility.FileCreation.each/3`. It's a list of names to be "interleaved" with
a main file name that points to the contents of the file. For example:
%{["big list", "flat_map"] => "...file content..."}
"""
@spec format(Suite.t(), map) :: [{list(String.t()), String.t()}]
def format(
%Suite{
scenarios: scenarios,
system: system,
configuration: %Configuration{unit_scaling: unit_scaling}
},
opts
) do
ensure_applications_loaded()
%{file: filename, inline_assets: inline_assets} = merge_default_configuration(opts)
scenario_data =
scenarios
# at best we'd keep the input order here, so no grouping but its easiest for now
|> Enum.group_by(fn scenario -> scenario.input_name end)
|> Map.new(fn input_to_scenarios = {input_name, _scenarios} ->
{input_name,
reports_for_input(input_to_scenarios, system, filename, unit_scaling, inline_assets)}
end)
index_data =
Enum.map(scenario_data, fn {input_name, scenarios} ->
{input_name, Enum.map(scenarios, fn {descriptors, _content} -> descriptors end)}
end)
scenario_pages = scenario_data |> Map.values() |> List.flatten()
index_page = build_index(index_data, filename, system, inline_assets)
[index_page | scenario_pages]
end
@doc """
Writes the output of `Benchee.Formatters.HTML.format/2` to disk.
Generates the following files:
* index file (exactly like `file` is named)
* a comparison of all the benchmarked jobs (one per benchmarked input)
* for each job a detail page with more detailed run time graphs for that
particular job (one per benchmark input)
"""
@spec write(%{Suite.key() => String.t()}, map) :: :ok
def write(data, opts) do
%{
file: filename,
auto_open: auto_open?,
inline_assets: inline_assets?
} = merge_default_configuration(opts)
prepare_folder_structure(filename, inline_assets?)
FileCreation.each(data, filename)
if auto_open?, do: open_report(filename)
:ok
end
defp ensure_applications_loaded do
_ = Application.load(:benchee)
_ = Application.load(:benchee_html)
end
@default_configuration %{
file: "benchmarks/output/results.html",
auto_open: true,
inline_assets: false
}
defp merge_default_configuration(opts) do
Map.merge(@default_configuration, opts)
end
defp prepare_folder_structure(filename, inline_assets?) do
base_directory = create_base_directory(filename)
unless inline_assets?, do: copy_asset_files(base_directory)
base_directory
end
defp create_base_directory(filename) do
base_directory = Path.dirname(filename)
File.mkdir_p!(base_directory)
base_directory
end
@asset_directory "assets"
defp copy_asset_files(base_directory) do
asset_target_directory = Path.join(base_directory, @asset_directory)
asset_source_directory = Application.app_dir(:benchee_html, "priv/assets/")
File.cp_r!(asset_source_directory, asset_target_directory)
end
defp reports_for_input({input_name, scenarios}, system, filename, unit_scaling, inline_assets) do
units = Conversion.units(scenarios, unit_scaling)
scenario_reports =
Enum.map(scenarios, fn scenario ->
scenario_report(scenario, system, units, inline_assets)
end)
comparison = comparison_report(input_name, scenarios, system, filename, units, inline_assets)
[comparison | scenario_reports]
end
defp scenario_report(scenario, system, units, inline_assets) do
scenario_json = JSON.encode!(scenario)
{
[scenario.input_name, scenario.name],
Render.scenario_detail(
scenario,
scenario_json,
system,
units,
inline_assets
)
}
end
defp comparison_report(input_name, scenarios, system, filename, units, inline_assets) do
scenarios_json = JSON.encode!(scenarios)
input_suite = %{
system: system,
job_count: length(scenarios),
filename: filename,
scenarios: scenarios
}
{[input_name, "comparison"],
Render.comparison(input_name, input_suite, units, scenarios_json, inline_assets)}
end
defp build_index(input_to_descriptors, filename, system, inline_assets?) do
full_index_data = build_index_data(input_to_descriptors, filename)
{"", Render.index(full_index_data, system, inline_assets?)}
end
defp build_index_data(input_to_descriptors, filename) do
Enum.map(input_to_descriptors, fn {input_name, descriptors_list} ->
{input_name,
Enum.map(descriptors_list, fn descriptors ->
Render.relative_file_path(filename, descriptors)
end)}
end)
end
defp open_report(filename) do
browser = get_browser()
{_, exit_code} = System.cmd(browser, [filename])
unless exit_code > 0, do: IO.puts("Opened report using #{browser}")
end
defp get_browser do
case :os.type() do
{:unix, :darwin} -> "open"
{:unix, _} -> "xdg-open"
{:win32, _} -> "explorer"
end
end
end