diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md new file mode 100644 index 0000000..19aea1c --- /dev/null +++ b/IMPLEMENTATION.md @@ -0,0 +1,22 @@ +# Implementation Notes + +## Label Width Handling + +For the purpose of discussing this benchmark code, a _label_ is a a short text string describing a benchmark observation (which is implemented as an instance of class `Benchmark::Tms`). This label string is stored in the `Tms` instance. + +The label _width_ can be specified to ensure that there is enough horizontal space to accommodate all the labels in the output. If it is not specified in a call to `bm`, then the output may be skewed: + +``` + user system total real +An operation 0.000004 0.000001 0.000005 ( 0.000001) +Another operation 0.000001 0.000000 0.000001 ( 0.000001) +``` + +However, `bmbm` manages to calculate maximum label width before printing any data, so the lines are all aligned correctly: + +``` + user system total real +An operation 0.000002 0.000000 0.000002 ( 0.000001) +Another operation 0.000001 0.000000 0.000001 ( 0.000001) +``` + diff --git a/lib/benchmark.rb b/lib/benchmark.rb index 4cfc0d8..3fac631 100644 --- a/lib/benchmark.rb +++ b/lib/benchmark.rb @@ -128,6 +128,9 @@ module Benchmark # benchmark tests. Reserves +label_width+ leading spaces for # labels on each line. Prints +caption+ at the top of the # report, and uses +format+ to format each line. + # (Note: +caption+ must contain a terminating newline character, + # see the default Benchmark::Tms::CAPTION for an example.) + # # Returns an array of Benchmark::Tms objects. # # If the block returns an array of diff --git a/test/benchmark/test_benchmark.rb b/test/benchmark/test_benchmark.rb index 3030bc5..52039c4 100644 --- a/test/benchmark/test_benchmark.rb +++ b/test/benchmark/test_benchmark.rb @@ -3,6 +3,9 @@ require 'benchmark' class TestBenchmark < Test::Unit::TestCase + + # Call `report` 3 times with labels, then return an array of 2 statistics, total and average, + # for inclusion in the output. BENCH_FOR_TIMES_UPTO = lambda do |x| n = 1000 tf = x.report("for:") { for _ in 1..n; '1'; end } @@ -11,6 +14,9 @@ class TestBenchmark < Test::Unit::TestCase [tf+tt+tu, (tf+tt+tu)/3] end + # Call `report` 3 times without labels and return last Benchmark::Tms returned by `report`. + # The fact that the value returned is a Tms and not an array of Tms instances will be interpreted + # by the `bm` method to indicate that this is *not* something to be added to the report items. BENCH_FOR_TIMES_UPTO_NO_LABEL = lambda do |x| n = 1000 x.report { for _ in 1..n; '1'; end } @@ -18,10 +24,15 @@ class TestBenchmark < Test::Unit::TestCase x.report { 1.upto(n) do ; '1'; end } end + # Sample labels for the benchmarking output (will appear as, e.g. "first --time-- --time-- --time-- ( --time--)") def labels %w[first second third] end + # Provides a benchmark method that can be called with or without a block. + # The `type` parameter will be used by `send` to call the appropriate method (e.g. `bm`) + # If called without a block, then `report` will be called once for each label in the array of labels + # returned by the `labels` method. def bench(type = :bm, *args, &block) if block Benchmark.send(type, *args, &block) @@ -34,10 +45,15 @@ def bench(type = :bm, *args, &block) end end + # Captures stdout of the benchmark report from stdout into a string, and replaces the measurements + # with a generic string so that comparisons will not fail due to different timings. + # + # If block is nil then the labels in the array returned by the `labels` method will be used for empty tests. def capture_bench_output(type, *args, &block) capture_output { bench(type, *args, &block) }.first.gsub(/[ \-]\d\.\d{6}/, ' --time--') end + # Tests that `to_s` and `format` output nicely in the expected formats. def test_tms_outputs_nicely assert_equal(" 0.000000 0.000000 0.000000 ( 0.000000)\n", Benchmark::Tms.new.to_s) assert_equal(" 1.000000 2.000000 10.000000 ( 5.000000)\n", Benchmark::Tms.new(1,2,3,4,5).to_s) @@ -48,12 +64,15 @@ def test_tms_outputs_nicely Benchmark::Tms.new(100, 150, 0, 0, 200).to_s) end + # Test that Tms#format will not modify the format string parameter passed into it def test_tms_wont_modify_the_format_String_given format = "format %u" Benchmark::Tms.new.format(format) assert_equal("format %u", format) end + # Expected output when an array of 2 numbers (total and avg) is returned from the block being measured, + # and the labels '>total:' and '>avg:' are specified: BENCHMARK_OUTPUT_WITH_TOTAL_AVG = <avg: --time-- --time-- --time-- ( --time--) BENCH + # Verifies that there is no vertical space output where captions (headings) would normally be, + # if no caption has been specified. def test_benchmark_does_not_print_any_space_if_the_given_caption_is_empty assert_equal(<<-BENCH, capture_bench_output(:benchmark)) first --time-- --time-- --time-- ( --time--) @@ -71,7 +92,9 @@ def test_benchmark_does_not_print_any_space_if_the_given_caption_is_empty BENCH end - def test_benchmark_makes_extra_calcultations_with_an_Array_at_the_end_of_the_benchmark_and_show_the_result + # Tests the `benchmark` method's ability to take an array of values returned by the measured block + # and display them, using the labels passed as parameters at the end of `benchmark`'s parameter list.' + def test_benchmark_makes_extra_calculations_with_an_Array_at_the_end_of_the_benchmark_and_show_the_result assert_equal(BENCHMARK_OUTPUT_WITH_TOTAL_AVG, capture_bench_output(:benchmark, Benchmark::CAPTION, 7, @@ -79,6 +102,12 @@ def test_benchmark_makes_extra_calcultations_with_an_Array_at_the_end_of_the_ben &BENCH_FOR_TIMES_UPTO)) end + # Tests `bm` and `bmbm` methods to verify that: + # + # 1) the returned object is an array whose size is equal to the size of the specified array of labels + # 2) each element of that array is an instance of Benchmark::Tms + # 3) the label property of the Tms instance is equal to the original label passed + # (which came from the array returned by the `labels` method) def test_bm_returns_an_Array_of_the_times_with_the_labels [:bm, :bmbm].each do |meth| capture_output do @@ -93,6 +122,7 @@ def test_bm_returns_an_Array_of_the_times_with_the_labels end end + # Verifies that overriding the label width results in correct horizontal spacing for caption and row values def test_bm_correctly_output_when_the_label_width_is_given assert_equal(<<-BENCH, capture_bench_output(:bm, 6)) user system total real @@ -102,6 +132,7 @@ def test_bm_correctly_output_when_the_label_width_is_given BENCH end + # Verifies that the absence of a label results in correct horizontal spacing def test_bm_correctly_output_when_no_label_is_given assert_equal(<<-BENCH, capture_bench_output(:bm, &BENCH_FOR_TIMES_UPTO_NO_LABEL)) user system total real @@ -111,12 +142,16 @@ def test_bm_correctly_output_when_no_label_is_given BENCH end + # Verify that bm can add line items after the benchmark report lines, as long as the + # labels are provided to the `bm` call, and the values are returned by the measured block + # in the form of an array. def test_bm_can_make_extra_calcultations_with_an_array_at_the_end_of_the_benchmark assert_equal(BENCHMARK_OUTPUT_WITH_TOTAL_AVG, capture_bench_output(:bm, 7, ">total:", ">avg:", &BENCH_FOR_TIMES_UPTO)) end + # Expected output of `bmbm` when no block is provided and the array of labels returned by the `labels` method is used. BMBM_OUTPUT = <