Packages

HTML formatter with pretty graphs for the (micro) benchmarking library benchee. Also allows export as PNG image!

Current section

Files

Jump to
benchee_html lib benchee formatters html.ex
Raw

lib/benchee/formatters/html.ex

defmodule Benchee.Formatters.HTML do
require EEx
alias Benchee.Conversion.{Format, Duration, Count, DeviationPercent}
alias Benchee.Utility.FileCreation
alias Benchee.Formatters.JSON
# Major pages
EEx.function_from_file :defp, :comparison,
"priv/templates/comparison.html.eex",
[:input_name, :suite, :suite_json]
EEx.function_from_file :defp, :job_detail,
"priv/templates/job_detail.html.eex",
[:input_name, :job_name, :measurements, :system,
:job_json]
EEx.function_from_file :defp, :index,
"priv/templates/index.html.eex",
[:names_to_paths, :system]
# Partials
EEx.function_from_file :defp, :head,
"priv/templates/partials/head.html.eex",
[]
EEx.function_from_file :defp, :header,
"priv/templates/partials/header.html.eex",
[:input_name, :system]
EEx.function_from_file :defp, :js_includes,
"priv/templates/partials/js_includes.html.eex",
[]
EEx.function_from_file :defp, :version_note,
"priv/templates/partials/version_note.html.eex",
[:system]
EEx.function_from_file :defp, :input_label,
"priv/templates/partials/input_label.html.eex",
[:input_name]
EEx.function_from_file :defp, :data_table,
"priv/templates/partials/data_table.html.eex",
[:statistics, :options]
# Small wrapper to have default arguments
defp render_data_table(statistics, options \\ []) do
data_table statistics, options
end
@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.HTML.output/1,
&Benchee.Formatters.Console.output/1
],
formatter_options: [html: [file: "samples_output/flat_map.html"]],
)
"""
@doc """
Uses `Benchee.Formatters.HTML.format/1` to transform the statistics output to
HTML with JS, but also already writes it to files defined in the initial
configuration under `formatter_options: [html: [file:
"benchmark_out/my.html"]]`.
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 output(Benchee.Suite.t) :: Benchee.Suite.t
def output(map)
def output(suite = %{configuration:
%{formatter_options:
%{html: %{file: filename}}}}) do
base_directory = create_base_directory(filename)
copy_asset_files(base_directory)
suite
|> format
|> FileCreation.each(filename)
suite
end
def output(_suite) do
raise "You need to specify a file to write the HTML to in the configuration as formatter_options: [html: [file: \"my.html\"]]"
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
@doc """
Transforms the statistical results from benchmarking to html to be written
somewhere, such as a file through `IO.write/2`.
Returns a map from file name/path to file content.
"""
@spec format(Benchee.Suite.t) :: %{Benchee.Suite.key => String.t}
def format(%{statistics: statistics, run_times: run_times, system: system,
configuration: %{
formatter_options: %{html: %{file: filename}}}}) do
statistics
|> input_job_reports(run_times, system, filename)
|> add_index(filename, system)
|> List.flatten
|> Map.new
end
defp input_job_reports(statistics, run_times, system, filename) do
Enum.map statistics, fn({input, input_stats}) ->
input_run_times = Map.fetch! run_times, input
reports_for_input(input, input_stats, input_run_times, system, filename)
end
end
defp reports_for_input(input, input_stats, input_run_times, system, filename) do
comparison = comparison_report(input, input_stats, input_run_times, system, filename)
job_reports = job_reports(input, input_stats, input_run_times, system)
[comparison | job_reports]
end
defp comparison_report(input, input_stats, input_run_times, system, filename) do
sorted_stats = Benchee.Statistics.sort input_stats
input_json = JSON.format_measurements(input_stats, input_run_times)
input_suite = %{
statistics: sorted_stats,
run_times: input_run_times,
system: system,
job_count: length(sorted_stats),
filename: filename
}
{[input, "comparison"], comparison(input, input_suite, input_json)}
end
defp job_reports(input, input_stats, input_run_times, system) do
merged_stats = merge_job_measurements(input_stats, input_run_times)
# extract some of me to benchee_json pretty please?
Enum.map(merged_stats, fn({job_name, measurements}) ->
job_json = JSON.encode!(measurements)
{[input, job_name],
job_detail(input, job_name, measurements, system, job_json)}
end)
end
defp add_index(grouped_main_contents, filename, system) do
index_structure = inputs_to_paths(grouped_main_contents, filename)
index_entry = {[], index(index_structure, system)}
[index_entry | grouped_main_contents]
end
defp inputs_to_paths(grouped_main_contents, filename) do
grouped_main_contents
|> Enum.map(fn(reports) -> input_to_paths(reports, filename) end)
|> Map.new
end
defp input_to_paths(input_reports, filename) do
[{[input_name | _], _} | _] = input_reports
paths = Enum.map input_reports, fn({tags, _content}) ->
relative_file_path(filename, tags)
end
{input_name, paths}
end
defp relative_file_path(filename, tags) do
filename
|> Path.basename
|> FileCreation.interleave(tags)
end
@doc """
Given statistics and run times for an input get all the data of a job
together.
## Exmaples
iex> statistics = %{
...> "Job" => %{average: 500.0},
...> "Other" => %{average: 200.0}
...> }
iex> run_times = %{"Job" => [400, 600], "Other" => [150, 250]}
iex> Benchee.Formatters.HTML.merge_job_measurements(statistics, run_times)
%{
"Job" => %{
statistics: %{average: 500.0},
run_times: [400, 600]
},
"Other" => %{
statistics: %{average: 200.0},
run_times: [150, 250]
}
}
"""
def merge_job_measurements(statistics, run_times) do
Map.merge(statistics, run_times, fn(_key, stats, times) ->
%{
statistics: stats,
run_times: times
}
end)
end
defp format_duration(duration) do
Format.format({:erlang.float(duration), :microsecond}, Duration)
end
defp format_count(count) do
Format.format({count, :one}, Count)
end
defp format_percent(deviation_percent) do
DeviationPercent.format deviation_percent
end
defp inputs_supplied?(input_name) do
input_name != Benchee.Benchmark.no_input
end
defp input_headline(input_name) do
if inputs_supplied?(input_name) do
" (#{input_name})"
else
""
end
end
@job_count_class "job-count-"
# there seems to be no way to set a maximum bar width other than through chart
# allowed width... or I can't find it.
defp max_width_class(job_count) when job_count < 7 do
"#{@job_count_class}#{job_count}"
end
defp max_width_class(_job_count), do: ""
end