1
2 //
3 // This source file is part of appleseed.
4 // Visit https://appleseedhq.net/ for additional information and resources.
5 //
6 // This software is released under the MIT license.
7 //
8 // Copyright (c) 2010-2013 Francois Beaune, Jupiter Jazz Limited
9 // Copyright (c) 2014-2018 Francois Beaune, The appleseedhq Organization
10 //
11 // Permission is hereby granted, free of charge, to any person obtaining a copy
12 // of this software and associated documentation files (the "Software"), to deal
13 // in the Software without restriction, including without limitation the rights
14 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 // copies of the Software, and to permit persons to whom the Software is
16 // furnished to do so, subject to the following conditions:
17 //
18 // The above copyright notice and this permission notice shall be included in
19 // all copies or substantial portions of the Software.
20 //
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27 // THE SOFTWARE.
28 //
29
30 // Interface header.
31 #include "benchmarksuite.h"
32
33 // appleseed.foundation headers.
34 #include "foundation/math/vector.h"
35 #include "foundation/platform/compiler.h"
36 #include "foundation/platform/thread.h"
37 #include "foundation/platform/timers.h"
38 #include "foundation/platform/types.h"
39 #include "foundation/utility/benchmark/benchmarkresult.h"
40 #include "foundation/utility/benchmark/ibenchmarkcase.h"
41 #include "foundation/utility/benchmark/ibenchmarkcasefactory.h"
42 #include "foundation/utility/benchmark/timingresult.h"
43 #include "foundation/utility/filter.h"
44 #include "foundation/utility/gnuplotfile.h"
45 #include "foundation/utility/stopwatch.h"
46 #include "foundation/utility/string.h"
47
48 // Standard headers.
49 #include <algorithm>
50 #include <cassert>
51 #include <cmath>
52 #include <cstddef>
53 #include <exception>
54 #include <limits>
55 #include <memory>
56 #include <string>
57 #include <vector>
58
59 using namespace std;
60
61 namespace foundation
62 {
63
64 //
65 // BenchmarkSuite class implementation.
66 //
67
68 #define GENERATE_BENCHMARK_PLOTS
69
70 namespace
71 {
72 // An empty benchmark case used for measuring the overhead of calling IBenchmarkCase::run().
73 struct EmptyBenchmarkCase
74 : public IBenchmarkCase
75 {
get_namefoundation::__anonfb4b57550111::EmptyBenchmarkCase76 const char* get_name() const override
77 {
78 return "Empty";
79 }
80
runfoundation::__anonfb4b57550111::EmptyBenchmarkCase81 APPLESEED_NO_INLINE void run() override
82 {
83 }
84 };
85 }
86
87 struct BenchmarkSuite::Impl
88 {
89 #if APPLESEED_X86
90 typedef Stopwatch<X86Timer> StopwatchType;
91 #else
92 typedef Stopwatch<DefaultProcessorTimer> StopwatchType;
93 #endif
94
95 string m_name;
96 vector<IBenchmarkCaseFactory*> m_factories;
97
measure_runtime_secondsfoundation::BenchmarkSuite::Impl98 static double measure_runtime_seconds(
99 IBenchmarkCase* benchmark,
100 StopwatchType& stopwatch)
101 {
102 stopwatch.start();
103 benchmark->run();
104 stopwatch.measure();
105
106 return stopwatch.get_seconds();
107 }
108
measure_runtime_ticksfoundation::BenchmarkSuite::Impl109 static double measure_runtime_ticks(
110 IBenchmarkCase* benchmark,
111 StopwatchType& stopwatch)
112 {
113 stopwatch.start();
114 benchmark->run();
115 stopwatch.measure();
116
117 return static_cast<double>(stopwatch.get_ticks());
118 }
119
120 template <typename MeasurementFunction>
measure_runtimefoundation::BenchmarkSuite::Impl121 static double measure_runtime(
122 IBenchmarkCase* benchmark,
123 StopwatchType& stopwatch,
124 MeasurementFunction& measurement_function,
125 const size_t measurement_count)
126 {
127 double lowest_runtime = numeric_limits<double>::max();
128
129 for (size_t i = 0; i < measurement_count; ++i)
130 {
131 const double runtime = measurement_function(benchmark, stopwatch);
132 lowest_runtime = min(lowest_runtime, runtime);
133 }
134
135 return lowest_runtime;
136 }
137
compute_measurement_countfoundation::BenchmarkSuite::Impl138 static size_t compute_measurement_count(
139 IBenchmarkCase* benchmark,
140 StopwatchType& stopwatch)
141 {
142 // Measure the runtime using a very small number of measurements. Not accurate.
143 const size_t InitialMeasurementCount = 10;
144 const double measurement_time =
145 measure_runtime(
146 benchmark,
147 stopwatch,
148 BenchmarkSuite::Impl::measure_runtime_seconds,
149 InitialMeasurementCount);
150
151 // Compute the number of measurements to get an accurate runtime measure.
152 const size_t MaxMeasurementCount = 1000000;
153 const double MaxTargetTotalTime = 0.1; // seconds
154 return
155 static_cast<size_t>(ceil(
156 min(MaxMeasurementCount * measurement_time, MaxTargetTotalTime) / measurement_time));
157 }
158
159 // Measure and return the overhead (in ticks) of running an empty benchmark case.
measure_call_overhead_ticksfoundation::BenchmarkSuite::Impl160 static double measure_call_overhead_ticks(
161 StopwatchType& stopwatch,
162 const size_t measurement_count)
163 {
164 unique_ptr<IBenchmarkCase> empty_case(new EmptyBenchmarkCase());
165 return
166 measure_runtime(
167 empty_case.get(),
168 stopwatch,
169 BenchmarkSuite::Impl::measure_runtime_ticks,
170 measurement_count);
171 }
172 };
173
BenchmarkSuite(const char * name)174 BenchmarkSuite::BenchmarkSuite(const char* name)
175 : impl(new Impl())
176 {
177 assert(name);
178 impl->m_name = name;
179 }
180
~BenchmarkSuite()181 BenchmarkSuite::~BenchmarkSuite()
182 {
183 delete impl;
184 }
185
get_name() const186 const char* BenchmarkSuite::get_name() const
187 {
188 return impl->m_name.c_str();
189 }
190
register_case(IBenchmarkCaseFactory * factory)191 void BenchmarkSuite::register_case(IBenchmarkCaseFactory* factory)
192 {
193 assert(factory);
194 impl->m_factories.push_back(factory);
195 }
196
run(BenchmarkResult & suite_result) const197 void BenchmarkSuite::run(BenchmarkResult& suite_result) const
198 {
199 PassThroughFilter filter;
200 run(filter, suite_result);
201 }
202
run(const IFilter & filter,BenchmarkResult & suite_result) const203 void BenchmarkSuite::run(
204 const IFilter& filter,
205 BenchmarkResult& suite_result) const
206 {
207 BenchmarkingThreadContext benchmarking_context;
208 bool has_begun_suite = false;
209
210 for (size_t i = 0; i < impl->m_factories.size(); ++i)
211 {
212 IBenchmarkCaseFactory* factory = impl->m_factories[i];
213
214 // Skip benchmark cases that aren't let through by the filter.
215 if (!filter.accepts(factory->get_name()))
216 continue;
217
218 if (!has_begun_suite)
219 {
220 // Tell the listeners that a benchmark suite is about to be executed.
221 suite_result.begin_suite(*this);
222 suite_result.signal_suite_execution();
223 has_begun_suite = true;
224 }
225
226 // Instantiate the benchmark case.
227 unique_ptr<IBenchmarkCase> benchmark(factory->create());
228
229 // Tell the listeners that a benchmark case is about to be executed.
230 suite_result.begin_case(*this, *benchmark.get());
231
232 #ifdef NDEBUG
233 try
234 #endif
235 {
236 suite_result.signal_case_execution();
237
238 // Recreate the stopwatch (and the underlying timer) for every benchmark
239 // case, since the CPU frequency will fluctuate quite a bit depending on
240 // the CPU load. We need an up-to-date frequency estimation in order to
241 // compute accurate call rates.
242 Impl::StopwatchType stopwatch(100000);
243
244 // Estimate benchmarking parameters.
245 const size_t measurement_count =
246 Impl::compute_measurement_count(benchmark.get(), stopwatch);
247
248 // Measure the overhead of calling IBenchmarkCase::run().
249 const double overhead_ticks =
250 Impl::measure_call_overhead_ticks(stopwatch, measurement_count);
251
252 // Run the benchmark case.
253 const double runtime_ticks =
254 Impl::measure_runtime(
255 benchmark.get(),
256 stopwatch,
257 BenchmarkSuite::Impl::measure_runtime_ticks,
258 measurement_count);
259
260 // Gather the timing results.
261 TimingResult timing_result;
262 timing_result.m_iteration_count = 1;
263 timing_result.m_measurement_count = measurement_count;
264 timing_result.m_frequency = static_cast<double>(stopwatch.get_timer().frequency());
265 timing_result.m_ticks = runtime_ticks > overhead_ticks ? runtime_ticks - overhead_ticks : 0.0;
266
267 // Post the timing result.
268 suite_result.write(
269 *this,
270 *benchmark.get(),
271 __FILE__,
272 __LINE__,
273 timing_result);
274
275 #ifdef GENERATE_BENCHMARK_PLOTS
276 vector<Vector2d> points;
277
278 const size_t PointCount = 100;
279 for (size_t j = 0; j < PointCount; ++j)
280 {
281 const double ticks =
282 Impl::measure_runtime(
283 benchmark.get(),
284 stopwatch,
285 BenchmarkSuite::Impl::measure_runtime_ticks,
286 max<size_t>(1, measurement_count / PointCount));
287 points.emplace_back(
288 static_cast<double>(j),
289 ticks > overhead_ticks ? ticks - overhead_ticks : 0.0);
290 }
291
292 const string filepath =
293 format("unit benchmarks/plots/{0}_{1}.gnuplot", get_name(), benchmark->get_name());
294
295 GnuplotFile plotfile;
296 plotfile.new_plot().set_points(points);
297 plotfile.write(filepath);
298 #endif
299 }
300 #ifdef NDEBUG
301 catch (const exception& e)
302 {
303 if (!is_empty_string(e.what()))
304 {
305 suite_result.write(
306 *this,
307 *benchmark.get(),
308 __FILE__,
309 __LINE__,
310 "an unexpected exception was caught: %s",
311 e.what());
312 }
313 else
314 {
315 suite_result.write(
316 *this,
317 *benchmark.get(),
318 __FILE__,
319 __LINE__,
320 "an unexpected exception was caught (no details available).");
321 }
322
323 suite_result.signal_case_failure();
324 }
325 catch (...)
326 {
327 suite_result.write(
328 *this,
329 *benchmark.get(),
330 __FILE__,
331 __LINE__,
332 "an unexpected exception was caught (no details available).");
333
334 suite_result.signal_case_failure();
335 }
336 #endif
337
338 // Tell the listeners that the benchmark case execution has ended.
339 suite_result.end_case(*this, *benchmark.get());
340 }
341
342 if (has_begun_suite)
343 {
344 // Report a benchmark suite failure if one or more benchmark cases failed.
345 if (suite_result.get_case_failure_count() > 0)
346 suite_result.signal_suite_failure();
347
348 // Tell the listeners that the benchmark suite execution has ended.
349 suite_result.end_suite(*this);
350 }
351 }
352
353 } // namespace foundation
354