]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | /* |
2 | * This file is open source software, licensed to you under the terms | |
3 | * of the Apache License, Version 2.0 (the "License"). See the NOTICE file | |
4 | * distributed with this work for additional information regarding copyright | |
5 | * ownership. You may not use this file except in compliance with the License. | |
6 | * | |
7 | * You may obtain a copy of the License at | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
11 | * Unless required by applicable law or agreed to in writing, | |
12 | * software distributed under the License is distributed on an | |
13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
14 | * KIND, either express or implied. See the License for the | |
15 | * specific language governing permissions and limitations | |
16 | * under the License. | |
17 | */ | |
18 | /* | |
19 | * Copyright (C) 2018 ScyllaDB Ltd. | |
20 | */ | |
21 | ||
22 | #pragma once | |
23 | ||
24 | #include <atomic> | |
25 | #include <memory> | |
26 | ||
27 | #include <fmt/format.h> | |
28 | ||
29 | #include <seastar/core/future.hh> | |
f67539c2 | 30 | #include <seastar/core/loop.hh> |
11fdf7f2 TL |
31 | |
32 | using namespace seastar; | |
33 | ||
34 | namespace perf_tests { | |
35 | namespace internal { | |
36 | ||
37 | struct config; | |
38 | ||
39 | using clock_type = std::chrono::steady_clock; | |
40 | ||
41 | class performance_test { | |
42 | std::string _test_case; | |
43 | std::string _test_group; | |
44 | ||
45 | uint64_t _single_run_iterations = 0; | |
46 | std::atomic<uint64_t> _max_single_run_iterations; | |
47 | private: | |
48 | void do_run(const config&); | |
49 | protected: | |
50 | [[gnu::always_inline]] [[gnu::hot]] | |
51 | bool stop_iteration() const { | |
52 | return _single_run_iterations >= _max_single_run_iterations.load(std::memory_order_relaxed); | |
53 | } | |
54 | ||
55 | [[gnu::always_inline]] [[gnu::hot]] | |
9f95a23c TL |
56 | void next_iteration(size_t n) { |
57 | _single_run_iterations += n; | |
11fdf7f2 TL |
58 | } |
59 | ||
60 | virtual void set_up() = 0; | |
61 | virtual void tear_down() noexcept = 0; | |
62 | virtual future<clock_type::duration> do_single_run() = 0; | |
63 | public: | |
64 | performance_test(const std::string& test_case, const std::string& test_group) | |
65 | : _test_case(test_case) | |
66 | , _test_group(test_group) | |
67 | { } | |
68 | ||
69 | virtual ~performance_test() = default; | |
70 | ||
71 | const std::string& test_case() const { return _test_case; } | |
72 | const std::string& test_group() const { return _test_group; } | |
73 | std::string name() const { return fmt::format("{}.{}", test_group(), test_case()); } | |
74 | ||
75 | void run(const config&); | |
76 | public: | |
77 | static void register_test(std::unique_ptr<performance_test>); | |
78 | }; | |
79 | ||
80 | // Helper for measuring time. | |
81 | // Each microbenchmark can either use the default behaviour which measures | |
82 | // only the start and stop time of the whole run or manually invoke | |
83 | // start_measuring_time() and stop_measuring_time() in order to measure | |
84 | // only parts of each iteration. | |
85 | class time_measurement { | |
86 | clock_type::time_point _run_start_time; | |
87 | clock_type::time_point _start_time; | |
88 | clock_type::duration _total_time; | |
89 | public: | |
90 | [[gnu::always_inline]] [[gnu::hot]] | |
91 | void start_run() { | |
92 | _total_time = { }; | |
93 | auto t = clock_type::now(); | |
94 | _run_start_time = t; | |
95 | _start_time = t; | |
96 | } | |
97 | ||
98 | [[gnu::always_inline]] [[gnu::hot]] | |
99 | clock_type::duration stop_run() { | |
100 | auto t = clock_type::now(); | |
101 | if (_start_time == _run_start_time) { | |
102 | return t - _start_time; | |
103 | } | |
104 | return _total_time; | |
105 | } | |
106 | ||
107 | [[gnu::always_inline]] [[gnu::hot]] | |
108 | void start_iteration() { | |
109 | _start_time = clock_type::now(); | |
110 | } | |
9f95a23c | 111 | |
11fdf7f2 TL |
112 | [[gnu::always_inline]] [[gnu::hot]] |
113 | void stop_iteration() { | |
114 | auto t = clock_type::now(); | |
115 | _total_time += t - _start_time; | |
116 | } | |
117 | }; | |
118 | ||
119 | extern time_measurement measure_time; | |
120 | ||
121 | namespace { | |
122 | ||
123 | template<bool Condition, typename TrueFn, typename FalseFn> | |
124 | struct do_if_constexpr_ : FalseFn { | |
125 | do_if_constexpr_(TrueFn, FalseFn false_fn) : FalseFn(std::move(false_fn)) { } | |
126 | decltype(auto) operator()() const { | |
127 | // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=64095 | |
128 | return FalseFn::operator()(0); | |
129 | } | |
130 | }; | |
131 | template<typename TrueFn, typename FalseFn> | |
132 | struct do_if_constexpr_<true, TrueFn, FalseFn> : TrueFn { | |
133 | do_if_constexpr_(TrueFn true_fn, FalseFn) : TrueFn(std::move(true_fn)) { } | |
134 | decltype(auto) operator()() const { return TrueFn::operator()(0); } | |
135 | }; | |
136 | ||
137 | template<bool Condition, typename TrueFn, typename FalseFn> | |
138 | do_if_constexpr_<Condition, TrueFn, FalseFn> if_constexpr_(TrueFn&& true_fn, FalseFn&& false_fn) | |
139 | { | |
140 | return do_if_constexpr_<Condition, TrueFn, FalseFn>(std::forward<TrueFn>(true_fn), | |
141 | std::forward<FalseFn>(false_fn)); | |
142 | } | |
143 | ||
144 | } | |
145 | ||
146 | template<typename Test> | |
147 | class concrete_performance_test final : public performance_test { | |
f67539c2 | 148 | std::optional<Test> _test; |
9f95a23c TL |
149 | private: |
150 | template<typename... Args> | |
151 | auto run_test(Args&&...) { | |
152 | return _test->run(); | |
153 | } | |
154 | ||
11fdf7f2 TL |
155 | protected: |
156 | virtual void set_up() override { | |
157 | _test.emplace(); | |
158 | } | |
159 | ||
160 | virtual void tear_down() noexcept override { | |
f67539c2 | 161 | _test = std::nullopt; |
11fdf7f2 TL |
162 | } |
163 | ||
164 | [[gnu::hot]] | |
165 | virtual future<clock_type::duration> do_single_run() override { | |
166 | // Redundant 'this->'s courtesy of https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61636 | |
167 | return if_constexpr_<is_future<decltype(_test->run())>::value>([&] (auto&&...) { | |
168 | measure_time.start_run(); | |
169 | return do_until([this] { return this->stop_iteration(); }, [this] { | |
9f95a23c TL |
170 | return if_constexpr_<std::is_same<decltype(_test->run()), future<>>::value>([&] (auto&&...) { |
171 | this->next_iteration(1); | |
172 | return _test->run(); | |
173 | }, [&] (auto&&... dependency) { | |
174 | // We need `dependency` to make sure the compiler won't be able to instantiate anything | |
175 | // (and notice that the code does not compile) if this part of if_constexpr_ is not active. | |
176 | return run_test(dependency...).then([&] (size_t n) { | |
177 | this->next_iteration(n); | |
178 | }); | |
179 | })(); | |
11fdf7f2 TL |
180 | }).then([] { |
181 | return measure_time.stop_run(); | |
182 | }); | |
183 | }, [&] (auto&&...) { | |
184 | measure_time.start_run(); | |
185 | while (!stop_iteration()) { | |
9f95a23c TL |
186 | if_constexpr_<std::is_void<decltype(_test->run())>::value>([&] (auto&&...) { |
187 | (void)_test->run(); | |
188 | this->next_iteration(1); | |
189 | }, [&] (auto&&... dependency) { | |
190 | // We need `dependency` to make sure the compiler won't be able to instantiate anything | |
191 | // (and notice that the code does not compile) if this part of if_constexpr_ is not active. | |
192 | this->next_iteration(run_test(dependency...)); | |
193 | })(); | |
11fdf7f2 TL |
194 | } |
195 | return make_ready_future<clock_type::duration>(measure_time.stop_run()); | |
196 | })(); | |
197 | } | |
198 | public: | |
199 | using performance_test::performance_test; | |
200 | }; | |
201 | ||
202 | void register_test(std::unique_ptr<performance_test>); | |
203 | ||
204 | template<typename Test> | |
205 | struct test_registrar { | |
206 | test_registrar(const std::string& test_group, const std::string& test_case) { | |
207 | auto test = std::make_unique<concrete_performance_test<Test>>(test_case, test_group); | |
208 | performance_test::register_test(std::move(test)); | |
209 | } | |
210 | }; | |
211 | ||
212 | } | |
213 | ||
214 | [[gnu::always_inline]] | |
215 | inline void start_measuring_time() | |
216 | { | |
217 | internal::measure_time.start_iteration(); | |
218 | } | |
219 | ||
220 | [[gnu::always_inline]] | |
221 | inline void stop_measuring_time() | |
222 | { | |
223 | internal::measure_time.stop_iteration(); | |
224 | } | |
225 | ||
226 | ||
227 | template<typename T> | |
228 | void do_not_optimize(const T& v) | |
229 | { | |
230 | asm volatile("" : : "r,m" (v)); | |
231 | } | |
232 | ||
233 | } | |
234 | ||
9f95a23c TL |
235 | // PERF_TEST and PERF_TEST_F support both synchronous and asynchronous functions. |
236 | // The former should return `void`, the latter `future<>`. | |
237 | // | |
238 | // Test cases may perform multiple operations in a single run, this may be desirable | |
239 | // if the cost of an individual operation is very small. This allows measuring either | |
240 | // the latency of throughput depending on how the test in written. In such cases, | |
241 | // the test function shall return either size_t or future<size_t> for synchronous and | |
242 | // asynchronous cases respectively. The returned value shall be the number of iterations | |
243 | // done in a single test run. | |
244 | ||
11fdf7f2 TL |
245 | #define PERF_TEST_F(test_group, test_case) \ |
246 | struct test_##test_group##_##test_case : test_group { \ | |
247 | [[gnu::always_inline]] inline auto run(); \ | |
248 | }; \ | |
249 | static ::perf_tests::internal::test_registrar<test_##test_group##_##test_case> \ | |
250 | test_##test_group##_##test_case##_registrar(#test_group, #test_case); \ | |
251 | [[gnu::always_inline]] auto test_##test_group##_##test_case::run() | |
252 | ||
253 | #define PERF_TEST(test_group, test_case) \ | |
254 | struct test_##test_group##_##test_case { \ | |
255 | [[gnu::always_inline]] inline auto run(); \ | |
256 | }; \ | |
257 | static ::perf_tests::internal::test_registrar<test_##test_group##_##test_case> \ | |
258 | test_##test_group##_##test_case##_registrar(#test_group, #test_case); \ | |
259 | [[gnu::always_inline]] auto test_##test_group##_##test_case::run() |