Skip to content

Commit 6e88e3e

Browse files
jamiscomandeo-mongo
authored andcommitted
RUBY-3313 Record benchmark percentiles (mongodb#2772)
* RUBY-3314 Implement variable iterations for benchmarks * report percentiles along with the median * rename Benchmarking::Micro to Benchmarking::BSON * refactoring to appease rubocop
1 parent b36d358 commit 6e88e3e

File tree

5 files changed

+139
-56
lines changed

5 files changed

+139
-56
lines changed

Diff for: .rubocop.yml

+3
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ Style/Documentation:
8686
Exclude:
8787
- 'spec/**/*'
8888

89+
Style/FormatStringToken:
90+
Enabled: false
91+
8992
Style/ModuleFunction:
9093
EnforcedStyle: extend_self
9194

Diff for: Rakefile

+24-16
Original file line numberDiff line numberDiff line change
@@ -135,24 +135,32 @@ require_relative "profile/benchmarking"
135135

136136
# Some require data files, available from the drivers team. See the comments above each task for details."
137137
namespace :benchmark do
138-
desc "Run the driver benchmark tests."
139-
140-
namespace :micro do
141-
desc "Run the common driver micro benchmarking tests"
138+
desc "Run the bson benchmarking tests"
139+
task :bson do
140+
puts "BSON BENCHMARK"
141+
Mongo::Benchmarking.report({
142+
bson: Mongo::Benchmarking::BSON.run_all({
143+
flat: %i[ encode decode ],
144+
deep: %i[ encode decode ],
145+
full: %i[ encode decode ],
146+
})
147+
})
148+
end
142149

150+
namespace :bson do
143151
namespace :flat do
144152
desc "Benchmarking for flat bson documents."
145153

146154
# Requirement: A file in Mongo::Benchmarking::DATA_PATH, called flat_bson.json.
147155
task :encode do
148-
puts "MICRO BENCHMARK:: FLAT:: ENCODE"
149-
Mongo::Benchmarking::Micro.run(:flat, :encode)
156+
puts "BSON BENCHMARK :: FLAT :: ENCODE"
157+
Mongo::Benchmarking.report({ bson: { flat: { encode: Mongo::Benchmarking::BSON.run(:flat, :encode) } } })
150158
end
151159

152160
# Requirement: A file in Mongo::Benchmarking::DATA_PATH, called flat_bson.json.
153161
task :decode do
154-
puts "MICRO BENCHMARK:: FLAT:: DECODE"
155-
Mongo::Benchmarking::Micro.run(:flat, :decode)
162+
puts "BSON BENCHMARK :: FLAT :: DECODE"
163+
Mongo::Benchmarking.report({ bson: { flat: { decode: Mongo::Benchmarking::BSON.run(:flat, :decode) } } })
156164
end
157165
end
158166

@@ -161,14 +169,14 @@ namespace :benchmark do
161169

162170
# Requirement: A file in Mongo::Benchmarking::DATA_PATH, called deep_bson.json.
163171
task :encode do
164-
puts "MICRO BENCHMARK:: DEEP:: ENCODE"
165-
Mongo::Benchmarking::Micro.run(:deep, :encode)
172+
puts "BSON BENCHMARK :: DEEP :: ENCODE"
173+
Mongo::Benchmarking.report({ bson: { deep: { encode: Mongo::Benchmarking::BSON.run(:deep, :encode) } } })
166174
end
167175

168176
# Requirement: A file in Mongo::Benchmarking::DATA_PATH, called deep_bson.json.
169177
task :decode do
170-
puts "MICRO BENCHMARK:: DEEP:: DECODE"
171-
Mongo::Benchmarking::Micro.run(:deep, :decode)
178+
puts "BSON BENCHMARK :: DEEP :: DECODE"
179+
Mongo::Benchmarking.report({ bson: { deep: { decode: Mongo::Benchmarking::BSON.run(:deep, :decode) } } })
172180
end
173181
end
174182

@@ -177,14 +185,14 @@ namespace :benchmark do
177185

178186
# Requirement: A file in Mongo::Benchmarking::DATA_PATH, called full_bson.json.
179187
task :encode do
180-
puts "MICRO BENCHMARK:: FULL:: ENCODE"
181-
Mongo::Benchmarking::Micro.run(:full, :encode)
188+
puts "BSON BENCHMARK :: FULL :: ENCODE"
189+
Mongo::Benchmarking.report({ bson: { full: { encode: Mongo::Benchmarking::BSON.run(:full, :encode) } } })
182190
end
183191

184192
# Requirement: A file in Mongo::Benchmarking::DATA_PATH, called full_bson.json.
185193
task :decode do
186-
puts "MICRO BENCHMARK:: FULL:: DECODE"
187-
Mongo::Benchmarking::Micro.run(:full, :decode)
194+
puts "BSON BENCHMARK :: FULL :: DECODE"
195+
Mongo::Benchmarking.report({ bson: { full: { decode: Mongo::Benchmarking::BSON.run(:full, :decode) } } })
188196
end
189197
end
190198
end

Diff for: profile/benchmarking.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
require 'benchmark'
1818
require_relative 'benchmarking/helper'
19-
require_relative 'benchmarking/micro'
19+
require_relative 'benchmarking/bson'
2020
require_relative 'benchmarking/single_doc'
2121
require_relative 'benchmarking/multi_doc'
2222
require_relative 'benchmarking/parallel'

Diff for: profile/benchmarking/micro.rb renamed to profile/benchmarking/bson.rb

+43-35
Original file line numberDiff line numberDiff line change
@@ -18,74 +18,82 @@ module Mongo
1818
module Benchmarking
1919
# These tests focus on BSON encoding and decoding; they are client-side only and
2020
# do not involve any transmission of data to or from the server.
21-
#
22-
# @since 2.2.3
23-
module Micro
21+
module BSON
2422
extend self
2523

26-
# Run a micro benchmark test.
24+
# Runs all of the benchmarks specified by the given mapping.
25+
#
26+
# @example Run a collection of benchmarks.
27+
# Benchmarking::BSON.run_all(
28+
# flat: %i[ encode decode ],
29+
# deep: %i[ encode decode ],
30+
# full: %i[ encode decode ]
31+
# )
32+
#
33+
# @return [ Hash ] a hash of the results for each benchmark
34+
def run_all(map)
35+
{}.tap do |results|
36+
map.each do |type, actions|
37+
results[type] = {}
38+
39+
actions.each do |action|
40+
results[type][action] = run(type, action)
41+
end
42+
end
43+
end
44+
end
45+
46+
# Run a BSON benchmark test.
2747
#
2848
# @example Run a test.
29-
# Benchmarking::Micro.run(:flat)
49+
# Benchmarking::BSON.run(:flat)
3050
#
3151
# @param [ Symbol ] type The type of test to run.
32-
# @param [ Integer ] repetitions The number of test repetitions.
33-
#
34-
# @return [ Numeric ] The test results.
52+
# @param [ :encode | :decode ] action The action to perform.
3553
#
36-
# @since 2.2.3
37-
def run(type, action, repetitions = Benchmarking::TEST_REPETITIONS)
38-
file_name = type.to_s << '_bson.json'
39-
GC.disable
40-
file_path = [ Benchmarking::DATA_PATH, file_name ].join('/')
41-
puts "#{action} : #{send(action, file_path, repetitions)}"
42-
GC.enable
54+
# @return [ Array<Number> ] The test results for each iteration
55+
def run(type, action)
56+
file_path = File.join(Benchmarking::DATA_PATH, "#{type}_bson.json")
57+
Benchmarking.without_gc { send(action, file_path) }
4358
end
4459

45-
# Run an encoding micro benchmark test.
60+
# Run an encoding BSON benchmark test.
4661
#
4762
# @example Run an encoding test.
48-
# Benchmarking::Micro.encode(file_name)
63+
# Benchmarking::BSON.encode(file_name)
4964
#
5065
# @param [ String ] file_name The name of the file with data for the test.
5166
# @param [ Integer ] repetitions The number of test repetitions.
5267
#
53-
# @return [ Numeric ] The median of the results.
54-
#
55-
# @since 2.2.3
56-
def encode(file_name, repetitions)
68+
# @return [ Array<Numeric> ] The list of the results for each iteration
69+
def encode(file_name)
5770
data = Benchmarking.load_file(file_name)
58-
document = BSON::Document.new(data.first)
71+
document = ::BSON::Document.new(data.first)
5972

60-
results = Benchmarking.benchmark(max_iterations: repetitions) do
73+
Benchmarking.benchmark do
6174
10_000.times { document.to_bson }
6275
end
63-
Benchmarking.median(results)
6476
end
6577

66-
# Run a decoding micro benchmark test.
78+
# Run a decoding BSON benchmark test.
6779
#
6880
# @example Run an decoding test.
69-
# Benchmarking::Micro.decode(file_name)
81+
# Benchmarking::BSON.decode(file_name)
7082
#
7183
# @param [ String ] file_name The name of the file with data for the test.
7284
# @param [ Integer ] repetitions The number of test repetitions.
7385
#
74-
# @return [ Numeric ] The median of the results.
75-
#
76-
# @since 2.2.3
77-
def decode(file_name, repetitions)
86+
# @return [ Array<Numeric> ] The list of the results for each iteration
87+
def decode(file_name)
7888
data = Benchmarking.load_file(file_name)
79-
buffer = BSON::Document.new(data.first).to_bson
89+
buffer = ::BSON::Document.new(data.first).to_bson
8090

81-
results = Benchmarking.benchmark(max_iterations: repetitions) do
91+
Benchmarking.benchmark do
8292
10_000.times do
83-
BSON::Document.from_bson(buffer)
93+
::BSON::Document.from_bson(buffer)
8494
buffer.rewind!
8595
end
8696
end
87-
88-
Benchmarking.median(results)
8997
end
9098
end
9199
end

Diff for: profile/benchmarking/helper.rb

+68-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def load_file(file_name)
3636
# @since 2.2.3
3737
def parse_json(document)
3838
JSON.parse(document).tap do |doc|
39-
doc['_id'] = BSON::ObjectId.from_string(doc['_id']['$oid']) if doc['_id'] && doc['_id']['$oid']
39+
doc['_id'] = ::BSON::ObjectId.from_string(doc['_id']['$oid']) if doc['_id'] && doc['_id']['$oid']
4040
end
4141
end
4242

@@ -81,19 +81,83 @@ def benchmark(max_iterations: Benchmarking::TEST_REPETITIONS, min_time: 60, max_
8181
end
8282
end
8383

84+
# Formats and displays a report of the given results.
85+
#
86+
# @param [ Hash ] results the results of a benchmarking run.
87+
# @param [ Integer ] indent how much the report should be indented.
88+
# @param [ Array<Numeric> ] percentiles the percentile values to report
89+
def report(results, indent: 0, percentiles: [ 10, 25, 50, 75, 90, 95, 98, 99 ])
90+
results.each do |key, value|
91+
puts format('%*s%s:', indent, '', key)
92+
if value.is_a?(Hash)
93+
report(value, indent: indent + 2, percentiles: percentiles)
94+
else
95+
report_result(value, indent, percentiles)
96+
end
97+
end
98+
end
99+
100+
# A utility class for returning the list item at a given percentile
101+
# value.
102+
class Percentiles
103+
# @return [ Array<Number> ] the sorted list of numbers to consider
104+
attr_reader :list
105+
106+
# Create a new Percentiles object that encapsulates the given list of
107+
# numbers.
108+
#
109+
# @param [ Array<Number> ] list the list of numbers to considier
110+
def initialize(list)
111+
@list = list.sort
112+
end
113+
114+
# Finds and returns the element in the list that represents the given
115+
# percentile value.
116+
#
117+
# @param [ Number ] percentile a number in the range [1,100]
118+
#
119+
# @return [ Number ] the element of the list for the given percentile.
120+
def [](percentile)
121+
i = (list.size * percentile / 100.0).ceil - 1
122+
list[i]
123+
end
124+
end
125+
84126
# Get the median of values in a list.
85127
#
86128
# @example Get the median.
87129
# Benchmarking.median(values)
88130
#
89-
# @param [ Array ] The values to get the median of.
131+
# @param [ Array ] values The values to get the median of.
90132
#
91133
# @return [ Numeric ] The median of the list.
92-
#
93-
# @since 2.2.3
94134
def median(values)
95135
i = (values.size / 2) - 1
96136
values.sort[i]
97137
end
138+
139+
# Runs a given block with GC disabled.
140+
def without_gc
141+
GC.disable
142+
yield
143+
ensure
144+
GC.enable
145+
end
146+
147+
private
148+
149+
# Formats and displays the results of a single benchmark run.
150+
#
151+
# @param [ Array<Numeric> ] results the results to report
152+
# @param [ Integer ] indent how much the report should be indented
153+
# @param [ Array<Numeric> ] percentiles the percentiles to report
154+
def report_result(results, indent, percentiles)
155+
ps = Percentiles.new(results)
156+
puts format('%*smedian: %g', indent + 2, '', ps[50])
157+
puts format('%*spercentiles:', indent + 2, '')
158+
percentiles.each do |pct|
159+
puts format('%*s%g: %g', indent + 4, '', pct, ps[pct])
160+
end
161+
end
98162
end
99163
end

0 commit comments

Comments
 (0)