├── .github └── PULL_REQUEST_TEMPLATE.md ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── include └── amz │ ├── algorithm │ ├── copy_while.hpp │ ├── remove_and_copy_if.hpp │ └── remove_range_if.hpp │ ├── bounded_channel.hpp │ ├── call.hpp │ ├── deferred_reclamation_allocator.hpp │ └── small_spin_mutex.hpp └── test ├── CMakeLists.txt ├── algorithm ├── copy_while.cpp ├── remove_and_copy_if.cpp └── remove_range_if.cpp ├── bounded_channel ├── custom_container.cpp ├── iterator.comparison.cpp ├── iterator.cpp ├── pop.cpp ├── pop_into_optional.cpp ├── push.cpp ├── stress.cpp ├── try_pop.cpp ├── try_pop_for.cpp ├── try_pop_until.cpp ├── try_push.cpp ├── try_push_for.cpp └── try_push_until.cpp ├── call.cpp ├── deferred_reclamation_allocator ├── bounded_allocator.hpp ├── compare.cpp ├── ctor.copy.cpp ├── ctor.move.cpp ├── deallocate.bad_alloc.cpp ├── deallocate.delay.cpp ├── dtor.cpp ├── dtor.delay.cpp ├── fancy_pointer.cpp ├── integration.oom_then_purge.cpp ├── interprocess │ ├── 0_setup.cpp │ ├── 1_allocate.cpp │ ├── 2_cleanup.cpp │ ├── README.md │ └── common.hpp ├── oom_allocator.hpp ├── purge.exhaustive.cpp ├── purge.noexcept.cpp └── purge.opportunistic.cpp ├── small_spin_mutex.cpp └── valgrind.supp /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # This CMake file provides the following high-level targets: 3 | # 4 | # : 5 | # If no target is specified, the AmazonTemplateLibrary library is built 6 | # and installed. 7 | # 8 | # release: 9 | # Build and install the library, and run the unit tests. 10 | # 11 | # build-tests: 12 | # Build the unit tests, but don't run them. 13 | # 14 | # test: 15 | # Build and then run the unit tests. 16 | # 17 | # test-valgrind: 18 | # Build and then run the unit tests under Valgrind. Only available when 19 | # Valgrind is available. 20 | # 21 | # install: 22 | # Build and install the library. 23 | ############################################################################## 24 | cmake_minimum_required(VERSION 3.8.0) 25 | 26 | project(AmazonTemplateLibrary VERSION 1.0.0 27 | LANGUAGES CXX) 28 | 29 | find_package(Boost 1.63 REQUIRED) # Boost.Intrusive < 1.63 has a bug that breaks us 30 | 31 | # Setup the `atl` library target and export it 32 | add_library(atl INTERFACE) 33 | target_include_directories(atl INTERFACE "$") 34 | target_compile_features(atl INTERFACE cxx_std_14) 35 | target_link_libraries(atl INTERFACE Boost::boost) 36 | install(TARGETS atl 37 | EXPORT ${PROJECT_NAME}Config 38 | INCLUDES DESTINATION include) 39 | install(EXPORT ${PROJECT_NAME}Config 40 | NAMESPACE ${PROJECT_NAME}:: 41 | DESTINATION lib/cmake/${PROJECT_NAME}) 42 | install(DIRECTORY include/amz 43 | DESTINATION include 44 | FILES_MATCHING PATTERN "*.hpp") 45 | 46 | # Setup unit tests 47 | enable_testing() 48 | add_custom_target(build-tests COMMENT "Build all the unit tests.") 49 | add_test(NAME build-tests 50 | COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" --target build-tests) 51 | add_subdirectory(test) 52 | 53 | # Setup a default target that runs when no target is specified. 54 | add_custom_target(default ALL 55 | COMMENT "Build and install the library." 56 | COMMAND "${CMAKE_COMMAND}" -P "${CMAKE_BINARY_DIR}/cmake_install.cmake" 57 | USES_TERMINAL) 58 | 59 | # Setup the 'release' target (which is run on Package Builder). 60 | add_custom_target(release 61 | COMMENT "Build and install the library, and run the unit tests." 62 | COMMAND "${CMAKE_COMMAND}" -E chdir "${CMAKE_BINARY_DIR}" "${CMAKE_CTEST_COMMAND}" --output-on-failure 63 | COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" --target install 64 | DEPENDS build-tests 65 | USES_TERMINAL) 66 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/amzn/amazon-template-library/issues), or [recently closed](https://github.com/amzn/amazon-template-library/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/amzn/amazon-template-library/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/amzn/amazon-template-library/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Amazon Template Library 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## The Amazon Template Library 2 | 3 | A collection of general purpose C++ utilities that play well with the Standard 4 | Library and Boost. 5 | 6 | ### Dependencies 7 | 8 | This library depends on [Boost][] and on [Catch2][] for testing. 9 | 10 | ### How to use 11 | 12 | Using the AmazonTemplateLibrary from CMake is very easy. Simply make sure the 13 | library can be found as part of your `CMAKE_PREFIX_PATH`, and then do the 14 | following from your `CMakeLists.txt`: 15 | 16 | ```cmake 17 | find_package(AmazonTemplateLibrary) 18 | target_link_libraries(your-target PRIVATE AmazonTemplateLibrary::atl) 19 | ``` 20 | 21 | ### License 22 | 23 | This library is licensed under the Apache 2.0 License. 24 | 25 | 26 | 27 | [Boost]: https://www.boost.org 28 | [Catch2]: https://github.com/catchorg/Catch2 29 | -------------------------------------------------------------------------------- /include/amz/algorithm/copy_while.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_ALGORITHM_COPY_WHILE_HPP 15 | #define AMZ_ALGORITHM_COPY_WHILE_HPP 16 | 17 | #include 18 | #include 19 | 20 | 21 | namespace amz { 22 | 23 | // Given a range of elements delimited by two InputIterators `[first, last)`, 24 | // `copy_while` copies the prefix of that range that satisfies the given 25 | // predicate into an OutputIterator. In other words, it copies elements of 26 | // the range as long as the predicate is satisfied. 27 | // 28 | // The algorithm returns a pair containing: 29 | // (1) an iterator to the first element of the range that was NOT copied 30 | // (or last if all the elements were copied) 31 | // (2) an OutputIterator to one-past-the-last element that was copied in the 32 | // output range 33 | // 34 | // This algorithm assumes: 35 | // (1) `[first, last)` is a valid range 36 | // (2) `pred(*it)` is valid for all `it` in the range `[first, last)` 37 | // 38 | // IMPORTANT PERFORMANCE GUARANTEES: 39 | // Given a range whose prefix satisfying the predicate has a length of `n`, 40 | // this algorithm does at most 41 | // (1) `n+1` increments and `n+1` dereferences of the `first` iterator 42 | // (2) `n+1` applications of the predicate 43 | // 44 | // This is of utmost importance in cases where the increment and dereferencing 45 | // of an iterator is costly. These guarantees should be considered part of the 46 | // interface in the case of a future refactoring. In particular, this can't be 47 | // replaced by `boost::algorithm::copy_while` in recent Boost versions, because 48 | // these guarantees are not met (iterator dereferences are not cached). 49 | // 50 | // Author: Louis Dionne 51 | template 52 | std::pair 53 | copy_while(InputIterator first, InputIterator last, OutputIterator result, Predicate const& pred) { 54 | using value_type = typename std::iterator_traits::value_type; 55 | for (; first != last; ++first) { 56 | // Cache *first to meet the requirements on the number of dereferences 57 | value_type const& v = *first; 58 | if (!pred(v)) break; 59 | *result++ = v; 60 | } 61 | return std::make_pair(first, result); 62 | } 63 | 64 | } // end namespace amz 65 | 66 | #endif // include guard 67 | -------------------------------------------------------------------------------- /include/amz/algorithm/remove_and_copy_if.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_ALGORITHM_REMOVE_AND_COPY_IF_HPP 15 | #define AMZ_ALGORITHM_REMOVE_AND_COPY_IF_HPP 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | 22 | namespace amz { 23 | 24 | // Given a range of elements delimited by two ForwardIterators `[first, last)` 25 | // and a predicate `pred`, `remove_and_copy_if` copies the elements for which 26 | // `pred` is satisfied to the specified output range and removes them from the 27 | // input range. 28 | // 29 | // This is very similar to `std::remove_if`, except the elements that are 30 | // removed are also copied to a specified output range. This is also similar 31 | // to `std::remove_copy_if`, except the input range is filtered in place. 32 | // 33 | // Like for `std::remove_if`, removing is done by shifting (by means of 34 | // assignment) the elements in the input range in such a way that the 35 | // elements that are not removed all appear contiguously as the subrange 36 | // `[first, ret)`, where `ret` is the new end of the input range. Relative 37 | // order of the elements that remain is preserved. Iterators in the range 38 | // `[ret, last)` are still dereferenceable, but the elements they point to 39 | // have valid but unspecified state. 40 | // 41 | // Note that the physical size of the container is unchanged. As such, a call 42 | // to `remove_and_copy_if` is typically followed by a call to a container's 43 | // `erase` method, which reduces the physical size of the container to match 44 | // its new logical size. 45 | // 46 | // This algorithm returns a pair containing: 47 | // (1) the iterator `ret` defined above, as would be returned by an equivalent 48 | // call to `std::remove_if` 49 | // (2) an OutputIterator to one-past-the-last element that was copied in 50 | // the output range, as would be returned by an equivalent call to 51 | // `std::remove_copy_if` 52 | // 53 | // This algorithm assumes: 54 | // (1) `[first, last)` is a valid range 55 | // (2) The input and output ranges do not overlap 56 | // (3) The input range's `reference` type is MoveAssignable 57 | // (4) `pred(*it)` is valid for all `it` in the range `[first, last)` 58 | // (5) The output range has at least `std::count_if(first, last, pred)` elements 59 | // 60 | // Performance guarantees: 61 | // Given a range of length `n`, this algorithm does exactly `n` applications 62 | // of the predicate and at most `n` copies. 63 | // 64 | // TODO: Consider using move assignment to move elements around instead of 65 | // copying them. 66 | // 67 | // Author: Louis Dionne 68 | template 69 | std::pair 70 | remove_and_copy_if(ForwardIt first, ForwardIt last, OutputIt result, Predicate const& pred) { 71 | using value_type = typename std::iterator_traits::value_type; 72 | ForwardIt compress = std::find_if(first, last, pred); 73 | for (first = compress; first != last; ++first) { 74 | value_type const& v = *first; 75 | if (pred(v)) 76 | *result++ = v; 77 | else 78 | *compress++ = v; 79 | } 80 | return std::make_pair(compress, result); 81 | } 82 | 83 | } // end namespace amz 84 | 85 | #endif // include guard 86 | -------------------------------------------------------------------------------- /include/amz/algorithm/remove_range_if.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_ALGORITHM_REMOVE_RANGE_IF_HPP 15 | #define AMZ_ALGORITHM_REMOVE_RANGE_IF_HPP 16 | 17 | #include 18 | #include 19 | 20 | namespace amz { 21 | 22 | // Given a range of elements delimited by two ForwardIterators `[first, last)` 23 | // and predicates `equivalent` and `pred`, divides the range into the largest 24 | // sub-ranges of equivalent elements (as determined by `equivalent`), removes 25 | // those sub-ranges for which `pred` returns `true` and returns the a 26 | // past-the-end iterator for the new end of the range. 27 | // 28 | // This is very similar to `std::remove_if`, except the elements are first 29 | // grouped using `equivalent` and are removed as sub-ranges -- rather than as 30 | // individual elements. 31 | // 32 | // Like for `std::remove_if`, removing is done by shifting (by means of 33 | // move assignment) the elements in the input range in such a way that the 34 | // elements that are not removed all appear contiguously as the subrange 35 | // `[first, ret)`, where `ret` is the new end of the input range. Relative 36 | // order of the elements that remain is preserved. Iterators in the range 37 | // `[ret, last)` are still dereferenceable, but the elements they point to 38 | // have valid but unspecified state. 39 | // 40 | // Note that the physical size of the container is unchanged. As such, a call 41 | // to `remove_range_if` is typically followed by a call to a container's 42 | // `erase` method, which reduces the physical size of the container to match 43 | // its new logical size. 44 | // 45 | // This algorithm assumes: 46 | // 1) `[first, last)` is a valid range; 47 | // 2) the input range's `reference` type is MoveAssignable; 48 | // 3) `pred(sub_first, sub_last)` is valid for all sub-ranges, 49 | // `[sub_first, sub_last)` of `[first, last)` that contains equivalent 50 | // elements; 51 | // 4) `equivalent(*it1, *it2)` is valid for all ForwardIterators `it1` and 52 | // `it2` in the range `[first, last)` 53 | // 5) and `equivalent` maintains an equivalence relation over the input 54 | // sequence. 55 | // 56 | // Performance guarantees: 57 | // * Exactly `std::distance(first, last)-1` applications of `equivalent` 58 | // * No more than `std::distance(first, last)-1` applications of `std::move` 59 | // * Exactly `N` applications of `pred` where `N` is the number of sub-ranges 60 | // 61 | // Author: John McFarlane 62 | template 63 | ForwardIterator remove_range_if(ForwardIterator first, ForwardIterator last, EquivalenceRelation equivalent, RangePredicate pred) { 64 | auto write_pos = first; 65 | while (first != last) { 66 | // Establish sub-range of equivalent elements, `[first, sub_last)`. 67 | auto sub_last = std::find_if(std::next(first), last, [equivalent, first](auto const& element) { 68 | return !equivalent(*first, element); 69 | }); 70 | 71 | // If the sub-range is *not* to be removed, 72 | if (!pred(first, sub_last)) { 73 | // if it needs to be shifted toward the start of the sequence, 74 | if (write_pos != first) { 75 | // move it. 76 | std::move(first, sub_last, write_pos); 77 | } 78 | 79 | std::advance(write_pos, std::distance(first, sub_last)); 80 | } 81 | 82 | first = sub_last; 83 | } 84 | return write_pos; 85 | } 86 | 87 | } 88 | 89 | #endif // AMZ_ALGORITHM_REMOVE_RANGE_IF_HPP 90 | -------------------------------------------------------------------------------- /include/amz/bounded_channel.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_BOUNDED_CHANNEL_HPP 15 | #define AMZ_BOUNDED_CHANNEL_HPP 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include 31 | 32 | 33 | namespace amz { 34 | 35 | //! Status code returned by many operations on channels and indicating the 36 | //! state of the channel and the result of the operation. 37 | enum class channel_op_status { 38 | //! Denotes that the operation was successful. 39 | success, 40 | 41 | //! Denotes that the operation failed because the channel is empty. 42 | empty, 43 | 44 | //! Denotes that the operation failed because the channel is full. 45 | full, 46 | 47 | //! Denotes that the operation failed because the channel has been closed. 48 | closed, 49 | 50 | //! Denotes that the operation failed because it could not finish within 51 | //! the allocated timeout. 52 | timeout 53 | }; 54 | 55 | //! Multi-producer multi-consumer thread-safe channel. 56 | //! 57 | //! This class represents a queue that can be concurrently pushed to and popped 58 | //! from from different threads, without explicit synchronization. The channel 59 | //! is bounded, which means that trying to push a new element to the channel 60 | //! when the channel is full will result in either blocking from the calling 61 | //! thread (for blocking operations) or soft failure (for non blocking 62 | //! operations). When one wishes to stop populating the channel, the channel 63 | //! can be closed, which makes it impossible for anything new to be pushed to 64 | //! it, but still allows for consumers to pop from it until it becomes empty 65 | //! (known as draining the channel). 66 | //! 67 | //! The underlying container used by the channel can be customized with a 68 | //! template argument. The only requirement is that the container can be 69 | //! used as the underlying container for a `std::queue`. 70 | //! 71 | //! The design of this channel is heavily based on Boost.Fiber's channels. 72 | //! 73 | //! Note on performance and usability 74 | //! ================================= 75 | //! This channel implementation uses locks for synchronization under the hood. 76 | //! There exist open source implementations of lock-free MPMC queues with much 77 | //! better performance characteristics than this channel. However, we could not 78 | //! find any that was as simple to reason about and as ergonomic as this channel. 79 | //! If your use case is such that only a reasonable number of elements are sent 80 | //! through the channel (which typically means that work is distributed in a 81 | //! coarse grained manner between producers and consumers), this channel is 82 | //! probably vastly sufficient. Otherwise, you should probably use a different 83 | //! implementation. As always, benchmarking is key. 84 | //! 85 | //! 86 | //! Note on lifetime 87 | //! ================ 88 | //! As usual in C++, a `bounded_channel` must outlive any reference to it. 89 | //! However, it is customary for threads using a channel to require it being 90 | //! closed before they can be joined. When that is the case, closing the 91 | //! channel before joining the threads and destructing the channel is required. 92 | //! In particular, closing the channel will not block until there are no more 93 | //! uses of the channel. 94 | //! 95 | //! If tracking the last usage of the channel is very difficult to do statically, 96 | //! a `std::shared_ptr` can be used to hold the channel. In this case, closing 97 | //! the channel will be done automatically in the destructor of the channel, 98 | //! which is going to be executed after the last usage of the channel is done. 99 | //! Be aware that this can lead to subtle situations where a thread requires 100 | //! the destructor to run in order for it to complete, but the destructor can 101 | //! only be run if when the thread completes. 102 | //! 103 | //! For example, any thread using a `std::shared_ptr` to a channel that would 104 | //! unconditionally `pop()` from the channel is bound to deadlock. Indeed, it 105 | //! would require the channel to be closed in order for `pop()` to complete, 106 | //! but it would require the thread to finish (and hence `pop()` to complete) 107 | //! in order for the last `std::shared_ptr` to go out of scope and trigger the 108 | //! closing of the channel. Hence, when in doubt, prefer closing the channel 109 | //! explicitly and giving it a statically verifiable lifetime. 110 | template > 111 | class bounded_channel { 112 | public: 113 | static_assert(std::is_same{}, 114 | "The value_type of the underlying container used to implement the channel " 115 | "must match the value_type provided to the channel itself."); 116 | 117 | using value_type = T; 118 | 119 | bounded_channel() = delete; 120 | 121 | //! Creates a `bounded_channel` with the given capacity. 122 | explicit bounded_channel(std::size_t capacity); 123 | 124 | bounded_channel(bounded_channel const&) = delete; 125 | bounded_channel(bounded_channel&&) = delete; 126 | bounded_channel& operator=(bounded_channel const&) = delete; 127 | bounded_channel& operator=(bounded_channel&&) = delete; 128 | 129 | //! Deactivates the channel, preventing new elements from being pushed to it. 130 | //! 131 | //! After the channel has been closed, no new values can be pushed into the 132 | //! channel. Threads blocked in any pushing or popping operation will be 133 | //! notified that the channel has been closed. Closing the channel is 134 | //! effectively a way to tell producers that they can't add anything more 135 | //! to the channel, while still allowing for consumers to consume elements 136 | //! that were put into the channel before it was closed. 137 | //! 138 | //! Note that closing the channel will not actually block the current thread, 139 | //! nor will it ensure that threads using the channel are joined. This must 140 | //! be ensured manually before the channel is destroyed. 141 | void close(); 142 | 143 | //! Closes the channel. 144 | //! 145 | //! Since `close()` does not block or wait for all usages of the channel to 146 | //! go away, one must make sure that no threads are waiting on the channel 147 | //! (e.g. in a `pop()` or `push()` operation). In particular, this means 148 | //! that the notification of all waiting threads performed in `close()` 149 | //! should in fact notify no threads at all; otherwise the behavior is 150 | //! undefined. 151 | ~bounded_channel() { close(); } 152 | 153 | //! Pushes a new value into the channel and returns whether the operation 154 | //! succeeded, possibly blocking if the channel is full. 155 | //! 156 | //! - If the channel has been closed, returns `closed`. 157 | //! - If the channel is open and not full, enqueues the new value, notifies 158 | //! at least one thread waiting on a popping operation and returns `success`. 159 | //! - If the channel is open but full, waits until either the channel is 160 | //! closed (returns `closed`) or it is not full anymore (enqueues the new 161 | //! value, notifies waiting threads and returns `success`). 162 | channel_op_status push(value_type const& va) { return this->push_impl(va); } 163 | channel_op_status push(value_type&& va) { return this->push_impl(std::move(va)); } 164 | 165 | //! Tries pushing a new value into the channel and returns whether the 166 | //! operation succeeded, without blocking if the channel is full. 167 | //! 168 | //! - If the channel has been closed, returns `closed`. 169 | //! - If the channel is open and not full, enqueues the new value, notifies 170 | //! at least one thread waiting on a popping operation and returns `success`. 171 | //! - If the channel is open but full, returns `full`. 172 | channel_op_status try_push(value_type const& va) { return this->try_push_impl(va); } 173 | channel_op_status try_push(value_type&& va) { return this->try_push_impl(std::move(va)); } 174 | 175 | //! Tries pushing a new value into the channel for a given amount of time 176 | //! and returns whether the operation succeeded within the allocated time. 177 | //! 178 | //! - If the channel is closed, returns `closed`. 179 | //! - If the channel is open and not full, enqueues the new value, notifies 180 | //! at least one thread waiting on a popping operation and returns `success`. 181 | //! - If the channel is open but full, waits until either the channel 182 | //! becomes non-full (enqueues the new value, notifies waiting threads 183 | //! and returns `success`), is closed (returns `closed`), or the timeout 184 | //! expires (returns `timeout`). 185 | //! 186 | //! Note 187 | //! ==== 188 | //! This function may block for longer than the given timeout due to 189 | //! scheduling or resource contention delays. 190 | template 191 | channel_op_status try_push_for(std::chrono::duration timeout_duration, value_type const& va) { 192 | return this->try_push_until(std::chrono::steady_clock::now() + timeout_duration, va); 193 | } 194 | template 195 | channel_op_status try_push_for(std::chrono::duration timeout_duration, value_type&& va) { 196 | return this->try_push_until(std::chrono::steady_clock::now() + timeout_duration, std::move(va)); 197 | } 198 | 199 | //! Equivalent to `try_push_for`, but tries pushing until a specific point in 200 | //! time is reached instead of using a relative duration. 201 | template 202 | channel_op_status try_push_until(std::chrono::time_point timeout_time, value_type const& va) { 203 | return this->try_push_until_impl(timeout_time, va); 204 | } 205 | template 206 | channel_op_status try_push_until(std::chrono::time_point timeout_time, value_type&& va) { 207 | return this->try_push_until_impl(timeout_time, std::move(va)); 208 | } 209 | 210 | //! Dequeues an element from the channel and returns whether the operation 211 | //! succeeded, possibly blocking if the channel is empty. 212 | //! 213 | //! - If the channel is not empty, immediately dequeues a value from the 214 | //! channel into the output parameter `va`, notifies at least one thread 215 | //! waiting on a pushing operation, and returns `success`. 216 | //! - If the channel is empty, waits until either one new item is pushed to 217 | //! the channel (extracts value into `va`, notify waiting threads and 218 | //! returns `success`), or the channel is closed (returns `closed`). 219 | //! 220 | //! Note 221 | //! ==== 222 | //! This method can pop into any variable that can be assigned from an 223 | //! element in the channel. This allows popping into an optional value, 224 | //! for example. 225 | template ::value> 227 | > 228 | channel_op_status pop(Value& va); 229 | 230 | //! Tries dequeuing an element from the channel and returns whether the 231 | //! operation succeeded, without blocking if the channel is empty. 232 | //! 233 | //! - If the channel is not empty, immediately dequeues a value from the 234 | //! channel into the output parameter `va`, notifies at least one thread 235 | //! waiting on a pushing operation, and returns `success`. 236 | //! - If the channel is empty and has been closed, returns `closed`. 237 | //! - If the channel is empty and has not been closed, returns `empty`. 238 | //! 239 | //! Note 240 | //! ==== 241 | //! This method can pop into any variable that can be assigned from an 242 | //! element in the channel. This allows popping into an optional value, 243 | //! for example. 244 | template ::value> 246 | > 247 | channel_op_status try_pop(Value& va); 248 | 249 | //! Tries dequeuing an element from the channel for a given amount of time 250 | //! and returns whether the operation succeeded within the allocated time. 251 | //! 252 | //! - If the channel is not empty, immediately dequeues a value from the 253 | //! channel into the output parameter `va`, notifies at least one thread 254 | //! waiting on a pushing operation, and returns `success`. 255 | //! - If the channel is empty and has been closed, returns `closed`. 256 | //! - If the channel is empty and has not been closed, waits until either 257 | //! the channel becomes non-empty (dequeues a value, notifies waiting 258 | //! threads and returns `success`), is closed (returns `closed`), or 259 | //! the timeout expires (returns `timeout`). 260 | //! 261 | //! Notes 262 | //! ===== 263 | //! - This function may block for longer than the given timeout due to 264 | //! scheduling or resource contention delays. 265 | //! - This method can pop into any variable that can be assigned from an 266 | //! element in the channel. This allows popping into an optional value, 267 | //! for example. 268 | template ::value> 270 | > 271 | channel_op_status try_pop_for(std::chrono::duration timeout_duration, Value& va) { 272 | return this->try_pop_until(std::chrono::steady_clock::now() + timeout_duration, va); 273 | } 274 | 275 | //! Equivalent to `try_pop_for`, but tries popping until a specific point in 276 | //! time is reached instead of using a relative duration. 277 | template ::value> 279 | > 280 | channel_op_status try_pop_until(std::chrono::time_point timeout_time, Value& va); 281 | 282 | 283 | //! InputIterator associated to a channel. 284 | //! 285 | //! This InputIterator can be used to consume values from a channel. A valid 286 | //! iterator will compare equal to the past-the-end iterator when the channel 287 | //! associated to it has been closed and is empty. 288 | //! 289 | //! Notes 290 | //! ===== 291 | //! - This iterator works in terms of `pop()`, which means that it will 292 | //! produce values until the channel is empty, even if it has been closed. 293 | //! In other words, a channel that is closed while being iterated upon will 294 | //! get fully drained by the iterator. 295 | //! - This is an InputIterator, which means that it can't be used to make 296 | //! more than a single pass over the channel. In other words, do not 297 | //! assume that making a copy of an iterator will allow retrieving the 298 | //! contents of the channel a second time -- once an iterator has been 299 | //! used to extract a value from the channel, this value can never be 300 | //! retrieved again using any other iterator over the same channel. 301 | class iterator; 302 | 303 | //! Returns an iterator to the beginning of the channel. 304 | iterator begin() { return iterator{*this}; } 305 | 306 | //! Returns a past-the-end iterator over the channel. 307 | iterator end() { return iterator{}; } 308 | 309 | private: 310 | std::size_t const capacity_; 311 | std::queue queue_; 312 | // Note: timed_mutex is necessary because we use try_lock_for, and 313 | // condition_variable_any is necessary because we use timed_mutex. 314 | using mutex_type = std::timed_mutex; 315 | mutex_type mutex_; 316 | std::condition_variable_any consumers_; // notified when we push something new; waited on by popping (consumer) threads 317 | std::condition_variable_any producers_; // notified when we pop something; waited on by pushing (producer) threads 318 | bool closed_; 319 | 320 | template 321 | channel_op_status push_impl(Value&& va); 322 | template 323 | channel_op_status try_push_impl(Value&& va); 324 | template 325 | channel_op_status try_push_until_impl(TimePoint timeout_time, Value&& va); 326 | 327 | // WARNING -- not thread safe 328 | bool is_full() const { return queue_.size() >= capacity_; } 329 | 330 | // WARNING -- not thread safe 331 | bool is_closed() const { return closed_; } 332 | 333 | // WARNING -- not thread safe 334 | bool is_empty() const { return queue_.empty(); } 335 | }; 336 | 337 | ////////////////////////////////////////////////////////////////////////////// 338 | // Channel implementation 339 | ////////////////////////////////////////////////////////////////////////////// 340 | template 341 | bounded_channel::bounded_channel(std::size_t capacity) 342 | : capacity_{capacity} 343 | , queue_{} 344 | , mutex_{} 345 | , consumers_{} 346 | , producers_{} 347 | , closed_{false} 348 | { } 349 | 350 | template 351 | void bounded_channel::close() { 352 | { 353 | std::unique_lock lock{mutex_}; 354 | closed_ = true; 355 | } 356 | producers_.notify_all(); 357 | consumers_.notify_all(); 358 | } 359 | 360 | // 361 | // push(), try_push(), try_push_until() 362 | // 363 | template 364 | template 365 | channel_op_status bounded_channel::push_impl(Value&& va) { 366 | std::unique_lock lock{mutex_}; 367 | producers_.wait(lock, [this] { return this->is_closed() || !this->is_full(); }); 368 | if (is_closed()) { 369 | return channel_op_status::closed; 370 | } else { 371 | assert(!is_full()); 372 | queue_.push(std::forward(va)); 373 | lock.unlock(); 374 | consumers_.notify_one(); 375 | return channel_op_status::success; 376 | } 377 | } 378 | 379 | template 380 | template 381 | channel_op_status bounded_channel::try_push_impl(Value&& va) { 382 | std::unique_lock lock{mutex_}; 383 | if (is_closed()) { 384 | return channel_op_status::closed; 385 | } else if (!is_full()) { 386 | queue_.push(std::forward(va)); 387 | lock.unlock(); 388 | consumers_.notify_one(); 389 | return channel_op_status::success; 390 | } else { 391 | assert(is_full()); 392 | return channel_op_status::full; 393 | } 394 | } 395 | 396 | template 397 | template 398 | channel_op_status bounded_channel::try_push_until_impl(TimePoint timeout_time, Value&& va) { 399 | std::unique_lock lock{mutex_, timeout_time}; // try to lock, but not past the timeout time 400 | if (!lock.owns_lock()) { 401 | return channel_op_status::timeout; 402 | } 403 | 404 | bool const timed_out = !producers_.wait_until(lock, timeout_time, [this] { 405 | return this->is_closed() || !this->is_full(); 406 | }); 407 | if (timed_out) { 408 | return channel_op_status::timeout; 409 | } else if (is_closed()) { 410 | return channel_op_status::closed; 411 | } else { 412 | assert(!is_full() && "we have not timed out and the channel is not closed; the channel should not be full"); 413 | queue_.push(std::forward(va)); 414 | lock.unlock(); 415 | consumers_.notify_one(); 416 | return channel_op_status::success; 417 | } 418 | } 419 | 420 | // 421 | // pop(), try_pop(), try_pop_until() 422 | // 423 | template 424 | template 425 | channel_op_status bounded_channel::pop(Value& va) { 426 | std::unique_lock lock{mutex_}; 427 | consumers_.wait(lock, [this] { return !this->is_empty() || this->is_closed(); }); 428 | if (!is_empty()) { 429 | va = std::move(queue_.front()); 430 | queue_.pop(); 431 | lock.unlock(); 432 | producers_.notify_one(); 433 | return channel_op_status::success; 434 | } else { 435 | assert(is_closed()); 436 | return channel_op_status::closed; 437 | } 438 | } 439 | 440 | template 441 | template 442 | channel_op_status bounded_channel::try_pop(Value& va) { 443 | std::unique_lock lock{mutex_}; 444 | if (!is_empty()) { 445 | va = std::move(queue_.front()); 446 | queue_.pop(); 447 | lock.unlock(); 448 | producers_.notify_one(); 449 | return channel_op_status::success; 450 | } else if (is_closed()) { 451 | return channel_op_status::closed; 452 | } else { 453 | assert(is_empty()); 454 | return channel_op_status::empty; 455 | } 456 | } 457 | 458 | template 459 | template 460 | channel_op_status bounded_channel::try_pop_until(std::chrono::time_point timeout_time, Value& va) { 461 | std::unique_lock lock{mutex_, timeout_time}; // try to lock for no longer than the timeout 462 | if (!lock.owns_lock()) { 463 | return channel_op_status::timeout; 464 | } 465 | 466 | bool const timed_out = !consumers_.wait_until(lock, timeout_time, [this] { 467 | return !this->is_empty() || this->is_closed(); 468 | }); 469 | if (timed_out) { 470 | return channel_op_status::timeout; 471 | } else if (!is_empty()) { 472 | va = std::move(queue_.front()); 473 | queue_.pop(); 474 | lock.unlock(); 475 | producers_.notify_one(); 476 | return channel_op_status::success; 477 | } else { 478 | assert(is_closed()); 479 | return channel_op_status::closed; 480 | } 481 | } 482 | 483 | ////////////////////////////////////////////////////////////////////////////// 484 | // Iterator implementation 485 | ////////////////////////////////////////////////////////////////////////////// 486 | template 487 | class bounded_channel::iterator { 488 | private: 489 | bounded_channel* channel_; // nullptr if and only if the iterator is past-the-end 490 | boost::optional value_; 491 | 492 | public: 493 | using iterator_category = std::input_iterator_tag; 494 | using difference_type = std::ptrdiff_t; 495 | using value_type = typename bounded_channel::value_type; 496 | using pointer = value_type*; 497 | using reference = value_type&; 498 | 499 | iterator() noexcept 500 | : channel_{nullptr} 501 | , value_{boost::none} 502 | { } 503 | 504 | explicit iterator(bounded_channel& channel) noexcept 505 | : channel_{&channel} 506 | , value_{boost::none} 507 | { 508 | ++*this; 509 | } 510 | 511 | iterator(iterator const& other) 512 | : channel_{other.channel_} 513 | , value_{other.value_} 514 | { } 515 | 516 | iterator& operator=(iterator const& other) { 517 | channel_ = other.channel_; 518 | value_ = other.value_; 519 | return *this; 520 | } 521 | 522 | friend bool operator==(iterator const& a, iterator const& b) { 523 | // Note that this is obviously a too wide definition of equality, however 524 | // we can't really do better since InputIterators do not play very well 525 | // with copying and equality comparison. 526 | return a.channel_ == nullptr && b.channel_ == nullptr; 527 | } 528 | 529 | friend bool operator!=(iterator const& a, iterator const& b) { 530 | return !(a == b); 531 | } 532 | 533 | iterator& operator++() { 534 | assert(channel_ != nullptr && "incrementing a past-the-end channel iterator"); 535 | switch (channel_->pop(value_)) { 536 | case channel_op_status::success: { 537 | break; 538 | } 539 | case channel_op_status::closed: { 540 | channel_ = nullptr; 541 | value_ = boost::none; 542 | break; 543 | } 544 | default: { 545 | assert(false && "pop() should always return either success or closed"); 546 | break; 547 | } 548 | }; 549 | return *this; 550 | } 551 | 552 | iterator operator++(int) = delete; // This should not be provided for InputIterators 553 | 554 | reference operator*() noexcept { 555 | assert(value_ != boost::none); 556 | return *value_; 557 | } 558 | 559 | pointer operator->() noexcept { 560 | assert(value_ != boost::none); 561 | return &*value_; 562 | } 563 | }; 564 | 565 | } // end namespace amz 566 | 567 | #endif // include guard 568 | -------------------------------------------------------------------------------- /include/amz/call.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_CALL_HPP 15 | #define AMZ_CALL_HPP 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | 26 | namespace amz { 27 | 28 | //! Executes the Callable object `f` if the LimitingFlag `flag` is active, 29 | //! otherwise do nothing. 30 | //! 31 | //! A `LimitingFlag` is an object with a single method, `bool active()`, which 32 | //! returns whether some action should be taken. They can be used in conjunction 33 | //! with `amz::call` to limit how many times or how often a function should 34 | //! be called (i.e. an action should be taken). This is similar to how 35 | //! `std::once_flag` is combined with `std::call_once` to ensure that 36 | //! a function is not called more than once (even across threads), but 37 | //! `LimitingFlag`s are more general (and are not required to be thread safe). 38 | //! 39 | //! @param flag 40 | //! A LimitingFlag to determine whether `f` should be executed. `f` 41 | //! is executed if `flag.active()` is true, otherwise nothing is done. 42 | //! 43 | //! @param f 44 | //! Callable to be executed. 45 | //! 46 | //! @param args... 47 | //! Arguments forwarded to the Callable `f` if it is executed. 48 | //! 49 | //! @returns 50 | //! An optional containing the result of calling `f` is a call was 51 | //! made, and an empty optional otherwise. If `f` returns `void`, 52 | //! `amz::call` returns an optional containing a `boost::blank`. 53 | //! 54 | //! Example usage 55 | //! ------------- 56 | //! Let's say you have a loop that processes some data, and which may be 57 | //! waiting for data at times. You'd like to notify the user that the 58 | //! program is alive and doing something, but you only want to do that 59 | //! every so often. One way to do this is to use the `amz::at_most_every` 60 | //! flag to limit the rate at which a message is printed: 61 | //! ``` 62 | //! int main() { 63 | //! auto at_most_every_second = amz::at_most_every{std::chrono::seconds{1}}; 64 | //! while (is_waiting_for_data()) { 65 | //! if (process_data()) { 66 | //! break; 67 | //! } 68 | //! 69 | //! // Called no more than once per second. 70 | //! amz::call(at_most_every_second, []{ 71 | //! std::cout << "I'm still alive..." << std::endl; 72 | //! }); 73 | //! } 74 | //! } 75 | //! ``` 76 | //! 77 | //! However, notice that if any part of the loop blocks for longer than a 78 | //! second, the message will not be printed during that second. In other 79 | //! words, these call flags allow _limiting_ how often a call is performed, 80 | //! but it does not allow ensuring that the call is performed at _least_ 81 | //! some number of times. 82 | template ().active()) /* only enable when the flag is a LimitingFlag */, 84 | typename Result = decltype(std::declval()(std::declval()...)), 85 | typename = std::enable_if_t::value>> 86 | boost::optional 87 | call(LimitingFlag& flag, Callable&& f, Args&& ...args) { 88 | if (flag.active()) { 89 | return std::forward(f)(std::forward(args)...); 90 | } else { 91 | return boost::none; 92 | } 93 | } 94 | 95 | template ().active()) /* only enable when the flag is a LimitingFlag */, 97 | typename Result = decltype(std::declval()(std::declval()...)), 98 | typename = std::enable_if_t::value>> 99 | boost::optional 100 | call(LimitingFlag& flag, Callable&& f, Args&& ...args) { 101 | if (flag.active()) { 102 | std::forward(f)(std::forward(args)...); 103 | return boost::blank{}; 104 | } else { 105 | return boost::none; 106 | } 107 | } 108 | 109 | //! LimitingFlag that is active at most once per period of a given duration. 110 | //! 111 | //! This LimitingFlag allows ensuring that an action is not taken more often 112 | //! than once every so often. 113 | struct at_most_every { 114 | explicit at_most_every(std::chrono::steady_clock::duration interval) 115 | : interval_{interval} 116 | { 117 | auto one_tick = std::chrono::steady_clock::duration::zero(); 118 | ++one_tick; 119 | last_active_ = std::chrono::steady_clock::now() - (interval + one_tick); 120 | } 121 | 122 | bool active() { 123 | auto const now = std::chrono::steady_clock::now(); 124 | if (now > last_active_ + interval_) { 125 | last_active_ = now; 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | private: 132 | std::chrono::steady_clock::time_point last_active_; 133 | std::chrono::steady_clock::duration const interval_; 134 | }; 135 | 136 | //! LimitingFlag that is active at most a given number of times, and then 137 | //! becomes inactive forever. 138 | //! 139 | //! This LimitingFlag allows ensuring that an action is taken at most a 140 | //! certain number of times, and no more. 141 | struct at_most { 142 | explicit at_most(std::size_t times) 143 | : max_activations_{times} 144 | , n_activations_{0} 145 | { } 146 | 147 | bool active() { 148 | if (n_activations_ < max_activations_) { 149 | ++n_activations_; 150 | return true; 151 | } 152 | 153 | return false; 154 | } 155 | 156 | private: 157 | std::size_t const max_activations_; 158 | std::size_t n_activations_; 159 | }; 160 | 161 | } // end namespace amz 162 | 163 | #endif // include guard 164 | -------------------------------------------------------------------------------- /include/amz/deferred_reclamation_allocator.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_DEFERRED_RECLAMATION_ALLOCATOR_HPP 15 | #define AMZ_DEFERRED_RECLAMATION_ALLOCATOR_HPP 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | 32 | namespace amz { 33 | 34 | namespace detail { 35 | // Equivalent to `std::destroy_at` in C++17. 36 | template 37 | void destroy_at(T* ptr) { 38 | ptr->~T(); 39 | } 40 | 41 | // Equivalent to `std::destroy_n`, in C++17. 42 | template 43 | ForwardIterator destroy_n(ForwardIterator first, Size n) { 44 | for (; n > 0; (void)++first, --n) { 45 | detail::destroy_at(std::addressof(*first)); 46 | } 47 | return first; 48 | } 49 | 50 | struct opportunistic_t { }; 51 | struct exhaustive_t { }; 52 | } // end namespace detail 53 | 54 | struct purge_mode { 55 | //! Tag used to specify that `purge()` should stop as soon as an entry in 56 | //! the _delay list_ is not ready to be purged. 57 | static constexpr detail::opportunistic_t opportunistic{}; 58 | 59 | //! Tag used to specify that `purge()` should purge all the elements in the 60 | //! _delay list_, waiting as needed to purge elements that are not ready yet. 61 | static constexpr detail::exhaustive_t exhaustive{}; 62 | }; 63 | 64 | //! Allocator adaptor that defers object destruction and memory reclamation 65 | //! until a fixed time period has elapsed. 66 | //! 67 | //! When modifying shared data that is concurrently being accessed by other 68 | //! threads, it is sometimes necessary to delay destructive operations (like 69 | //! object destruction and memory reclamation) to a time where no other threads 70 | //! may be using that data. This general pattern is known as read-copy-update 71 | //! (RCU) [1], with many possible implementations. In a nutshell, the pattern 72 | //! is typically as follows: 73 | //! 1. Make the shared data unavailable to readers, typically (but not 74 | //! necessarily) by atomically replacing a pointer by another one pointing 75 | //! to a newer version of the data. After this, no new readers may gain a 76 | //! reference to the "old" data. 77 | //! 2. Wait for all the previous readers to be done with their reference to 78 | //! the "old" data. "Previous" readers are readers that existed before 79 | //! step 1, and which may have obtained a reference to the "old" data 80 | //! before it was made unavailable. 81 | //! 3. At this point, there cannot be any readers holding references to the 82 | //! "old" data, and so the destructive operation (typically destruction 83 | //! and memory reclamation) can be carried. 84 | //! 85 | //! Knowing exactly when all previous readers are done with their reference to 86 | //! the "old" data can be challenging. However, in cases where readers are 87 | //! known to never hold on to shared data for more than a fixed time period, 88 | //! RCU can be substantially simplified by simply making sure that we do not 89 | //! perform the destructive operation until after that fixed time period has 90 | //! elapsed after making the data unavailable to new readers. This has the 91 | //! advantage of being extremely simple, but the disadvantage that memory 92 | //! will never be reclaimed sooner than after the fixed time period has 93 | //! elapsed, even if no readers have references to the data. 94 | //! 95 | //! The purpose of this allocator adaptor is to do precisely that, i.e. to 96 | //! defer the destruction of objects and the deallocation of the associated 97 | //! memory until after some fixed time period has elapsed. The fixed time 98 | //! period is called the _timeout time_, and it must be provided by the user 99 | //! when constructing the allocator. This allocator proceeds as follows: 100 | //! 1. When destroying data, this allocator does not do anything. 101 | //! Destruction is deferred until deallocation is performed. 102 | //! 2. When deallocating data, this allocator puts the data in a buffer of a 103 | //! fixed size (the _delay buffer_). When the _delay buffer_ is full, it 104 | //! puts it on a list (the _delay list_) along with a time stamp of the 105 | //! current time. The elements in the _delay list_ are kept in order of 106 | //! their time stamp, such that older elements are at the beginning of the 107 | //! list. A larger size for the _delay buffer_ means a coarser granularity 108 | //! of the _timeout time_, but less frequent allocations to add entries to 109 | //! the _delay list_. 110 | //! 3. On each deallocation, the allocator tries to destroy and deallocate as 111 | //! many elements from the _delay list_ as it can. An element can be 112 | //! destroyed and freed when more than the _timeout time_ has passed since 113 | //! it was put on the _delay list_ (in step 2 above). This process of 114 | //! removing elements that are older than the _timeout time_ from the 115 | //! beginning of the _delay list_ is known as _purging_ (see the `purge()` 116 | //! function). 117 | //! 118 | //! This functionality is implemented as an allocator adaptor, meaning that 119 | //! `deferred_reclamation_allocator` is meant to wrap another, underlying, 120 | //! allocator. All memory allocation and eventual deallocation is done through 121 | //! the underlying allocator, which allows a great deal of flexibility in 122 | //! creating custom allocators through composition of existing allocators. 123 | //! 124 | //! Notes on copy-constructibility 125 | //! ------------------------------ 126 | //! This allocator meets the requirement of copy-constructibility by copying 127 | //! the underlying allocator and the provided timeout-related settings. 128 | //! However, the _delay buffer_ and the _delay list_ are not copied. This, 129 | //! along with the definition of equality comparison based on that of the 130 | //! underlying allocator and the _timeout time_, ensures that we have proper 131 | //! copy semantics. Indeed, the requirements of the Allocator concept say two 132 | //! allocators must compare equal if the memory allocated with one can be 133 | //! deallocated with the other. Memory allocated with one `deferred_memory_allocator` 134 | //! can always be deallocated by any other `deferred_memory_allocator` (assuming 135 | //! the underlying allocators compare equal), so long as a to-be-destroyed 136 | //! element is never put in more than one _delay buffer_ or _delay list_. 137 | //! Since we never copy those, the only way for this to happen would be to 138 | //! deallocate the same object twice, which is already an error. 139 | //! 140 | //! [1]: https://en.wikipedia.org/wiki/Read-copy-update 141 | //! 142 | //! @todo 143 | //! - We're missing the following: 144 | //! + propagate_on_container_copy_assignment 145 | //! + propagate_on_container_move_assignment 146 | //! + propagate_on_container_swap 147 | template 148 | class deferred_reclamation_allocator { 149 | using AllocatorTraits = std::allocator_traits; 150 | struct force_copy_tag { }; 151 | template 152 | using alloc_rebind_t = typename AllocatorTraits::template rebind_alloc; 153 | template 154 | using alloc_pointer_t = typename alloc_rebind_t::pointer; 155 | 156 | public: 157 | using pointer = typename AllocatorTraits::pointer; 158 | using const_pointer = typename AllocatorTraits::const_pointer; 159 | using void_pointer = typename AllocatorTraits::void_pointer; 160 | using const_void_pointer = typename AllocatorTraits::const_void_pointer; 161 | using size_type = typename AllocatorTraits::size_type; 162 | using difference_type = typename AllocatorTraits::difference_type; 163 | using value_type = typename AllocatorTraits::value_type; 164 | 165 | template 166 | struct rebind { 167 | using other = deferred_reclamation_allocator>; 168 | }; 169 | 170 | deferred_reclamation_allocator() = delete; 171 | 172 | //! Create a deferred allocator with the given underlying allocator, and 173 | //! other settings. 174 | //! 175 | //! @param allocator 176 | //! The underlying allocator to use for allocations and deallocations. 177 | //! @param timeout 178 | //! The time period for which deallocated data must be kept around 179 | //! before actual destruction and deallocation occurs. 180 | //! @param delay_buffer_size 181 | //! The size of the _delay buffer_, which controls how often we flush 182 | //! the buffer to the _delay list_ and try to purge the _delay list_. 183 | //! This must be an integer greater than 0. 184 | template 185 | deferred_reclamation_allocator(Allocator allocator, 186 | std::chrono::duration timeout, 187 | std::size_t delay_buffer_size = 100) 188 | : allocator_{allocator} 189 | , timeout_{std::chrono::duration_cast(timeout)} 190 | , now_{TimeoutClock::now()} 191 | , buffer_allocator_{allocator} 192 | , buffer_capacity_{delay_buffer_size} 193 | , current_buffer_size_{0} 194 | , delay_list_{} 195 | { 196 | assert(delay_buffer_size >= 1); 197 | current_buffer_ = buffer_new(); 198 | } 199 | 200 | //! Create a deferred allocator with a default-constructed instance of the 201 | //! underlying allocator and the given settings. 202 | //! 203 | //! @param timeout 204 | //! The time period for which deallocated data must be kept around 205 | //! before actual destruction and deallocation occurs. 206 | //! @param delay_buffer_size 207 | //! The size of the _delay buffer_, which controls how often we flush 208 | //! the buffer to the _delay list_ and try to purge the _delay list_. 209 | //! This must be an integer greater than 0. 210 | template 211 | deferred_reclamation_allocator(std::chrono::duration timeout, 212 | std::size_t delay_buffer_size = 100) 213 | : deferred_reclamation_allocator{Allocator{}, timeout, delay_buffer_size} 214 | { } 215 | 216 | //! Converting copy-constructor from another `deferred_reclamation_allocator`. 217 | //! 218 | //! This allows constructing a `deferred_reclamation_allocator` from another 219 | //! such allocator with a different underlying allocator, so long as the 220 | //! underlying allocators are compatible (i.e. one can be constructed from 221 | //! the other). For details on how this behaves, see notes on 222 | //! copy-constructibility in the class-wide documentation. 223 | template ::value 225 | >> 226 | deferred_reclamation_allocator(deferred_reclamation_allocator const& other, 227 | force_copy_tag /* ignore this */ = {}) 228 | : allocator_{other.allocator_} 229 | , timeout_{other.timeout_} 230 | , now_{TimeoutClock::now()} 231 | , buffer_allocator_{other.buffer_allocator_} 232 | , buffer_capacity_{other.buffer_capacity_} 233 | , current_buffer_size_{0} 234 | , delay_list_{} 235 | { 236 | current_buffer_ = buffer_new(); 237 | } 238 | 239 | //! Copy constructs an allocator from another `deferred_reclamation_allocator`. 240 | //! 241 | //! For details, see notes on copy-constructibility in the class-wide 242 | //! documentation. 243 | deferred_reclamation_allocator(deferred_reclamation_allocator const& other) 244 | : deferred_reclamation_allocator{other, force_copy_tag{}} 245 | { } 246 | 247 | //! Move constructs a `deferred_reclamation_allocator` from another one. 248 | //! 249 | //! The underlying allocator is move-constructed, the various allocator 250 | //! settings are copied, and both the _delay buffer_ and the _delay list_ 251 | //! are moved to the newly constructed allocator. 252 | //! 253 | //! A moved-from `deferred_reclamation_allocator` may only be destroyed; 254 | //! calling any other method is undefined behavior. 255 | deferred_reclamation_allocator(deferred_reclamation_allocator&& other) 256 | : allocator_{std::move(other.allocator_)} 257 | , timeout_{other.timeout_} 258 | , now_{TimeoutClock::now()} 259 | , buffer_allocator_{std::move(other.buffer_allocator_)} 260 | , buffer_capacity_{other.buffer_capacity_} 261 | , current_buffer_size_{other.current_buffer_size_} 262 | , current_buffer_{std::exchange(other.current_buffer_, nullptr)} 263 | , delay_list_{std::move(other.delay_list_)} 264 | { } 265 | 266 | deferred_reclamation_allocator& operator=(deferred_reclamation_allocator const&) = delete; 267 | deferred_reclamation_allocator& operator=(deferred_reclamation_allocator&&) = delete; 268 | 269 | //! Forwards the allocation to the underlying allocator. 270 | //! 271 | //! @warning 272 | //! Since this allocator performs destruction and deallocation in the same 273 | //! step, one should never deallocate something that has not been constructed. 274 | //! Doing otherwise would result in this allocator trying to destroy an 275 | //! object that was never constructed, which is undefined behavior. To 276 | //! avoid this, make sure a call to `allocate` is always matched by a call 277 | //! to `construct`. For illustration, the following is wrong: 278 | //! ```c++ 279 | //! deferred_reclamation_allocator<...> allocator{...}; 280 | //! 281 | //! // Allocate some memory, but don't construct in it. 282 | //! auto* ptr = allocator.allocate(1); 283 | //! 284 | //! ... do stuff ... 285 | //! 286 | //! // Perhaps I realized I wouldn't need the memory I allocated above, 287 | //! // so I will just free it. 288 | //! allocator.deallocate(ptr, 1); // <= WRONG! `*ptr` was never constructed 289 | //! ``` 290 | //! 291 | //! @param n 292 | //! The number of objects to allocate storage for. 293 | //! @returns 294 | //! The result of calling `allocate(n)` on the underlying allocator. 295 | pointer allocate(std::size_t n) { 296 | assert(!was_moved_from()); 297 | return allocator_.allocate(n); 298 | } 299 | 300 | //! Constructs an object using the underlying allocator. 301 | //! 302 | //! All the arguments are simply forwarded to the underlying allocator's 303 | //! `construct()` method. 304 | template 305 | void construct(pointer p, Args&& ...args) { 306 | assert(!was_moved_from()); 307 | allocator_.construct(std::move(p), std::forward(args)...); 308 | } 309 | 310 | //! Does not do anything, since destruction is delayed until deallocation. 311 | //! 312 | //! As explained in the class-wide documentation, destruction is deferred 313 | //! until some fixed time period has elapsed after a request to deallocate 314 | //! memory is issued. Destruction and deallocation through the underlying 315 | //! allocator are both done at that point, not before. 316 | //! 317 | //! @warning 318 | //! Since this allocator does not actually destruct the object when `destroy` 319 | //! is called, one should never reuse memory obtained through this allocator 320 | //! after calling `destroy` on it. For example, the following is wrong: 321 | //! ```c++ 322 | //! deferred_reclamation_allocator<...> allocator{...}; 323 | //! 324 | //! // Create an object and use it 325 | //! auto* ptr = allocator.allocate(1); 326 | //! allocator.construct(ptr, args1...); 327 | //! use(*ptr); 328 | //! 329 | //! // Destroy the object, and construct another one in the same storage. 330 | //! allocator.destroy(ptr); // <= This line doesn't do anything. 331 | //! allocator.construct(ptr, args2...); // <= WRONG: Previous object still there! 332 | //! use(*ptr); 333 | //! ``` 334 | void destroy(pointer) noexcept { 335 | assert(!was_moved_from()); 336 | } 337 | 338 | //! Mark the given pointer for delayed destruction and deletion by putting 339 | //! it on the _delay list_. 340 | //! 341 | //! The pointer is first put on the _delay buffer_ for it to eventually be 342 | //! added to the _delay list_ and then purged. When it is finally purged, 343 | //! both destruction and deallocation will go through the underlying 344 | //! allocator's `destroy()` and `deallocate()` functions. This also means 345 | //! that this method is only `noexcept` when the allocator's `value_type` 346 | //! is nothrow destructible. 347 | //! 348 | //! Whenever the _delay buffer_ is full, it will be appended to the _delay 349 | //! list_ and a new _delay buffer_ will be created (or reused, see below). 350 | //! Whenever the _delay buffer_ is flushed to the _delay list_, the allocator 351 | //! will update its version of the current time and it will try to purge 352 | //! elements on the _delay list_ whose _timeout time_ has elapsed. 353 | //! 354 | //! Note on memory allocation during deallocation 355 | //! --------------------------------------------- 356 | //! When memory is deallocated through this allocator and the _delay buffer_ 357 | //! is full, it must be offloaded to the _delay list_. To do so, the allocator 358 | //! first tries to purge the _delay list_ and reuse a buffer that's not needed 359 | //! anymore. However, in case no such buffer can be reused (i.e. the _delay 360 | //! list_ is empty or no elements on the _delay list_ are ready to be purged), 361 | //! a new buffer will be allocated with the underlying allocator. This can 362 | //! cause memory usage to go up when `deallocate()` is called. This situation 363 | //! will typically happen when the allocator starts up and the _delay list_ 364 | //! is empty, or when many deallocations are requested within a time window 365 | //! smaller than the _timeout time_. 366 | //! 367 | //! If an exception is thrown when the allocator tries to allocate a new 368 | //! buffer in the above procedure, the allocator will wait until it can 369 | //! purge an entry from the _delay list_. It will then reuse the buffer 370 | //! that was thus made available, and use this as a new _delay buffer_. 371 | //! If the _delay list_ is already empty, however, the allocator can not 372 | //! hope to reuse an existing buffer on the _delay list_. In this case, the 373 | //! allocator will simply wait for the _timeout time_ to elapse before 374 | //! purging the current _delay buffer_ and the object(s) being deallocated. 375 | //! 376 | //! This allows the allocator to operate correctly even in low-memory 377 | //! conditions, at the cost of blocking for at most the _timeout time_ 378 | //! when performing a deallocation. 379 | //! 380 | //! @param p 381 | //! A pointer to deallocate when the required amount of time has 382 | //! elapsed. 383 | //! @param n 384 | //! The number of objects passed to the earlier call to `allocate()`. 385 | //! 386 | //! @todo 387 | //! - What is the allocator's exception guarantee when an exception is thrown? 388 | void deallocate(pointer p, std::size_t n) noexcept(std::is_nothrow_destructible{}) { 389 | assert(!was_moved_from()); 390 | assert(!current_buffer_full() && "the buffer should never be full when entering `deallocate()`, " 391 | "since we purge it as soon as it becomes full"); 392 | 393 | current_buffer_push_back({p, n}); // preallocated; does not throw 394 | 395 | // When the buffer is full, we timestamp it and offload it to the delay 396 | // list. We then purge the delay list and try to start a new buffer, 397 | // possibly reusing a buffer that was just made available. 398 | if (current_buffer_full()) { 399 | // 1. Timestamp and offload the current buffer. 400 | now_ = current_buffer_->timestamp = TimeoutClock::now(); 401 | delay_list_.push_back(*std::exchange(current_buffer_, nullptr)); // intrusive list; does not throw 402 | 403 | // 2. Try to reuse an existing buffer by purging the delay list. 404 | current_buffer_ = purge_delay_list_and_reuse_existing_buffer(); 405 | 406 | // 3. If we were not able to reuse an existing buffer because no entry 407 | // in the delay list was ready, we allocate a new one and handle 408 | // error conditions. 409 | if (current_buffer_ == nullptr) { 410 | try { 411 | current_buffer_ = buffer_new(); // strong exception guarantee 412 | } catch (std::bad_alloc const&) { 413 | // Wait until we can free at least one entry in the delay list, purge 414 | // it and reuse the buffer. In the worst case, we'll be waiting to 415 | // purge and reuse the `current_buffer_` that we just inserted on the 416 | // delay list. 417 | assert(!delay_list_.empty() && "we just pushed back the latest buffer to the delay " 418 | "list, so there should be at least one element"); 419 | auto const& oldest = delay_list_.front(); 420 | std::this_thread::sleep_until(oldest.timestamp + timeout_); 421 | now_ = TimeoutClock::now(); 422 | current_buffer_ = purge_delay_list_and_reuse_existing_buffer(); 423 | } 424 | } 425 | 426 | assert(current_buffer_ != nullptr); 427 | current_buffer_size_ = 0; // mark the current buffer as being empty 428 | } 429 | } 430 | 431 | //! Purges everything on the _delay list_ and in the current _delay buffer_, 432 | //! waiting for the _timeout times_ of objects to elapse when required. 433 | //! 434 | //! The destructor will reclaim everything that was deallocated through 435 | //! `deallocate()`, i.e. everything that was put on the _delay list_. 436 | //! The destructor __does__ respect the guarantee that nothing will be 437 | //! reclaimed before its _timeout time_ has elapsed, which means that it 438 | //! will have to wait before it can destroy and reclaim some objects. 439 | //! 440 | //! In particular, if the _delay buffer_ is not empty, the allocator will 441 | //! have to wait for the total _timeout time_ before it can reclaim objects 442 | //! in it, since they have not been timestamped yet. 443 | //! 444 | //! This function is noexcept exactly when the `value_type` is nothrow 445 | //! destructible, since it may need to destroy objects on the _delay list_. 446 | //! 447 | //! @note 448 | //! There are two main approaches we could use to do this reclamation: 449 | //! - We could simply wait for the _timeout time_ of the youngest object to 450 | //! elapse, and then reclaim everything. 451 | //! - We could start trying to progressively reclaim the oldest objects first 452 | //! and work our way to the youngest one, waiting whenever we need to let 453 | //! an object's _timeout time_ elapse. 454 | //! The former is simpler and requires less overall work. However, the latter 455 | //! may be faster, as we may be able to reclaim the youngest objects without 456 | //! having to wait by the time we make it to them. Also, we expect such an 457 | //! allocator to be destroyed quite rarely, but with potentially large numbers 458 | //! of objects on the delay list. Hence, it seems like optimizing the 459 | //! destructor latency is preferable to optimizing the total amount of work 460 | //! it does. Consequently, the approach we currently use is the latter. 461 | //! This is an implementation detail that must not be relied upon. 462 | ~deferred_reclamation_allocator() noexcept(std::is_nothrow_destructible{}) { 463 | // If we've been moved-from, we don't have anything special to do. 464 | if (was_moved_from()) { 465 | return; 466 | } 467 | 468 | // 1. Check the current time and timestamp the current buffer. We then 469 | // proceed with the _delay list_; we'll handle the buffer at the end. 470 | auto now = TimeoutClock::now(); 471 | current_buffer_->timestamp = now; 472 | 473 | // 2. Reclaim all the buffers on the _delay list_, waiting as needed. 474 | purge(purge_mode::exhaustive); 475 | assert(delay_list_.empty()); 476 | 477 | // 3. If the current buffer is not empty, wait for the remaining time 478 | // required and reclaim everything in it. The buffer is not full 479 | // (otherwise it would have been on the _delay list_), so it can't 480 | // be handled exactly as the ones above. 481 | if (!current_buffer_empty()) { 482 | auto const ready_to_delete = current_buffer_->timestamp + timeout_; 483 | if (now <= ready_to_delete) { 484 | std::this_thread::sleep_until(ready_to_delete); 485 | } 486 | reclaim_buffer_elements(current_buffer_->elements, 487 | current_buffer_->elements + current_buffer_size_); 488 | } 489 | buffer_delete(current_buffer_); 490 | } 491 | 492 | //! Returns whether an allocator can be used to deallocate storage allocated 493 | //! by another allocator. 494 | //! 495 | //! Two `deferred_reclamation_allocator`s are equal if and only if their 496 | //! underlying allocators are equal and their _timeout times_ are equal. 497 | //! 498 | //! For details, see the comment on copy-constructibility in the class-wide 499 | //! documentation. 500 | friend bool operator==(deferred_reclamation_allocator const& a, 501 | deferred_reclamation_allocator const& b) 502 | { return a.timeout_ == b.timeout_ && a.allocator_ == b.allocator_; } 503 | 504 | //! Equivalent to `!(a == b)`. 505 | friend bool operator!=(deferred_reclamation_allocator const& a, 506 | deferred_reclamation_allocator const& b) 507 | { return !(a == b); } 508 | 509 | //! Purges the _delay list_, destroying and deallocating elements that have 510 | //! been in the _delay list_ for more than the _timeout time_. 511 | //! 512 | //! This method can be used by an application with special knowledge of its 513 | //! own usage pattern to shrink the _delay list_. It has two flavors: 514 | //! opportunistic and exhaustive. 515 | //! - Opportunistic means that only elements of the _delay list_ that are 516 | //! old enough to be reclaimed will be reclaimed, and `purge()` will stop 517 | //! as soon as it encounters an element that is too young to be reclaimed. 518 | //! This is the default mode of operation. 519 | //! 520 | //! - Exhaustive means that all the elements in the delay list will be 521 | //! reclaimed. When an element is too young to be reclaimed, the method 522 | //! will simply wait for it to become reclaimable. 523 | //! 524 | //! To pick the desired flavor, use the following: 525 | //! ```c++ 526 | //! allocator.purge(amz::purge_mode::opportunistic); 527 | //! allocator.purge(amz::purge_mode::exhaustive); 528 | //! allocator.purge(); // same as opportunistic 529 | //! ``` 530 | //! 531 | //! Note that in all cases, the current _delay buffer_ is NOT reclaimed. 532 | //! This is because the _delay buffer_ is not timestamped until it is full, 533 | //! which means we would have no way to ensure that elements in the _delay 534 | //! buffer_ can be reclaimed other than waiting the full _timeout time_. 535 | //! We don't do that. 536 | //! 537 | //! This method is `noexcept` exactly when destroying the `value_type` of 538 | //! this allocator is `noexcept`. 539 | template 540 | void purge(Flavor = {}) noexcept(std::is_nothrow_destructible{}) { 541 | static_assert(std::is_same{} || 542 | std::is_same{}, 543 | "'deferred_reclamation_allocator::purge' has two flavor: opportunistic and exhaustive. pick one."); 544 | assert(!was_moved_from()); 545 | 546 | auto const reclaim_full_buffer = [this](alloc_pointer_t buffer) { 547 | reclaim_buffer_elements(buffer->elements, 548 | buffer->elements + buffer_capacity_); 549 | buffer_delete(buffer); 550 | }; 551 | 552 | now_ = TimeoutClock::now(); 553 | 554 | while (!delay_list_.empty()) { 555 | auto& oldest = delay_list_.front(); 556 | 557 | // If the oldest buffer can be purged, just do it and keep going. 558 | // Note that a buffer must be full in order to make it to the delay list. 559 | // Hence, we know that the buffers we manipulate below are always full, 560 | // which means their size == their capacity. 561 | auto const ready_to_delete = oldest.timestamp + timeout_; 562 | if (now_ > ready_to_delete) { 563 | delay_list_.pop_front_and_dispose(reclaim_full_buffer); 564 | } 565 | 566 | // Otherwise, if the oldest buffer is still too young to be purged, 567 | // there's two options: 568 | // (1) we were being opportunistic: just stop trying to purge 569 | else if (std::is_same{}) { 570 | return; 571 | } 572 | 573 | // (2) we're being exhaustive: wait for enough time to pass and try again 574 | else if (std::is_same{}) { 575 | std::this_thread::sleep_until(ready_to_delete); 576 | delay_list_.pop_front_and_dispose(reclaim_full_buffer); 577 | // We know we slept until at least that time point, so we can use 578 | // this as our `now` to avoid calling `TimeoutClock::now()`. 579 | now_ = ready_to_delete; 580 | } 581 | 582 | else { 583 | assert(false && "unreachable"); 584 | } 585 | } 586 | } 587 | 588 | private: 589 | template 590 | friend class deferred_reclamation_allocator; 591 | Allocator allocator_; 592 | 593 | //////////////////////////////////////////////////////////////////////////// 594 | // Timeout-related members and definitions 595 | //////////////////////////////////////////////////////////////////////////// 596 | using TimeoutClock = std::chrono::steady_clock; 597 | static_assert(noexcept(TimeoutClock::now()), 598 | "We make the assumption that our clock does not throw in various places " 599 | "for providing noexcept guarantees. This must be satisfied."); 600 | 601 | using TimePoint = typename TimeoutClock::time_point; 602 | using Duration = typename TimeoutClock::duration; 603 | 604 | Duration const timeout_; 605 | TimePoint now_; 606 | 607 | //////////////////////////////////////////////////////////////////////////// 608 | // Implementation of the buffered delay list. 609 | // 610 | // Note: 611 | // Most of the following functions operate on the buffer currently owned by 612 | // the allocator. Despite it being inflexible and non-generic, we do things 613 | // this way because it avoids duplicating information like the size of 614 | // a buffer (we only need the size of the current buffer) or the capacity 615 | // of a buffer (they all have the same capacity). 616 | //////////////////////////////////////////////////////////////////////////// 617 | struct DelayBufferElement { 618 | pointer p; 619 | std::size_t n; 620 | }; 621 | 622 | struct DelayBuffer 623 | : boost::intrusive::slist_base_hook< 624 | boost::intrusive::link_mode, 625 | boost::intrusive::void_pointer 626 | > 627 | { 628 | DelayBuffer() = default; 629 | TimePoint timestamp; 630 | DelayBufferElement elements[]; 631 | }; 632 | 633 | using DelayList = boost::intrusive::slist>; 634 | 635 | // Reclaim the `DelayBufferElement`s in the provided range with the 636 | // underlying allocator. This does not make any check related to the 637 | // _timeout time_. 638 | template 639 | void reclaim_buffer_elements(Iterator elem, Iterator last) { 640 | for (; elem != last; ++elem) { 641 | detail::destroy_n(elem->p, elem->n); // may throw if ~value_type can throw 642 | allocator_.deallocate(elem->p, elem->n); // does not throw 643 | } 644 | } 645 | 646 | // Returns whether the current buffer is full. 647 | bool current_buffer_full() const noexcept { 648 | return current_buffer_size_ == buffer_capacity_; 649 | } 650 | 651 | // Returns whether the current buffer is empty. 652 | bool current_buffer_empty() const noexcept { 653 | return current_buffer_size_ == 0; 654 | } 655 | 656 | // Appends an element to the current buffer. The behavior is undefined if 657 | // this function is called when the current buffer is full. This function 658 | // never throws or allocates. 659 | void current_buffer_push_back(DelayBufferElement elem) noexcept { 660 | assert(!current_buffer_full() && "trying to push_back in the current buffer, but it is full"); 661 | current_buffer_->elements[current_buffer_size_] = elem; 662 | ++current_buffer_size_; 663 | } 664 | 665 | // We need to allocate buffers as chunks of individual bytes because their 666 | // size is not fixed at compile-time (the dreaded trailing array member). 667 | using BufferAllocator = alloc_rebind_t; 668 | 669 | // Allocates a new empty buffer of the maximum buffer size with the underlying 670 | // allocator. The timestamp of the buffer is just default constructed (i.e. 671 | // not representing a meaningful value). 672 | alloc_pointer_t buffer_new() { 673 | std::size_t const buffer_element_bytes = buffer_capacity_ * sizeof(DelayBufferElement); 674 | alloc_pointer_t bytes = buffer_allocator_.allocate(sizeof(DelayBuffer) + buffer_element_bytes); 675 | assert(bytes != nullptr); 676 | return new (std::addressof(*bytes)) DelayBuffer{}; 677 | } 678 | 679 | // Deallocates and destroys a buffer. It is undefined behavior to use this 680 | // function on a buffer that is not empty, i.e. that has not been fully 681 | // purged using `reclaim_buffer_elements`. 682 | void buffer_delete(alloc_pointer_t buffer) { 683 | assert(buffer != nullptr); 684 | buffer->~DelayBuffer(); 685 | alloc_pointer_t bytes = reinterpret_cast(std::addressof(*buffer)); 686 | buffer_allocator_.deallocate(bytes, buffer_capacity_); 687 | } 688 | 689 | alloc_pointer_t purge_delay_list_and_reuse_existing_buffer() { 690 | alloc_pointer_t reuse = nullptr; 691 | while (!delay_list_.empty()) { 692 | auto& oldest = delay_list_.front(); 693 | // If the current time is too early, stop trying to purge. 694 | if (now_ <= oldest.timestamp + timeout_) 695 | return reuse; 696 | 697 | // Otherwise, reclaim everything in the buffer and unlink it from the delay list. 698 | reclaim_buffer_elements(oldest.elements, oldest.elements + buffer_capacity_); 699 | delay_list_.pop_front(); // does not throw or invalidate references 700 | 701 | // If we haven't found a buffer to reuse yet, we keep this one for reuse. 702 | // Otherwise, we keep the buffer we've already found and deallocate this 703 | // one. Because of the order in which we visit buffers on the delay list, 704 | // the first one is going to be the oldest one, i.e. the one that we 705 | // allocated first. We presume it's better to free buffers that were 706 | // allocated more recently than the other way around. 707 | if (reuse == nullptr) { 708 | reuse = &oldest; 709 | } else { 710 | buffer_delete(&oldest); 711 | } 712 | } 713 | return reuse; 714 | } 715 | 716 | bool was_moved_from() const noexcept { 717 | return current_buffer_ == nullptr; 718 | } 719 | 720 | BufferAllocator buffer_allocator_; 721 | std::size_t const buffer_capacity_; 722 | std::size_t current_buffer_size_; 723 | alloc_pointer_t current_buffer_; // `nullptr` iff `*this` has been moved-from 724 | DelayList delay_list_; 725 | }; 726 | } // end namespace amz 727 | 728 | #endif // include guard 729 | -------------------------------------------------------------------------------- /include/amz/small_spin_mutex.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_SMALL_SPIN_MUTEX_HPP 15 | #define AMZ_SMALL_SPIN_MUTEX_HPP 16 | 17 | #include 18 | 19 | 20 | namespace amz { 21 | 22 | // Lightweight non-recursive spin mutex with strict size guarantees. 23 | // 24 | // Like for all spin mutexes, this is almost certainly not what you want. 25 | // Indeed, this spin mutex will cause a thread seeking to acquire the mutex 26 | // to busy-wait without doing any useful work, and without sleeping so that 27 | // another thread can make progress. However, in very few situations, a spin 28 | // mutex can be used to make fine-grained access to shared data thread-safe 29 | // whilst minimizing the overhead of locking. 30 | // 31 | // This mutex implementation is not recursive, which means that a thread 32 | // may not acquire the mutex when it already owns it. 33 | // 34 | // THIS CLASS PROVIDES THE FOLLOWING GUARANTEES, WHICH MUST BE WEAKENED UNDER 35 | // NO CIRCUMSTANCES: 36 | // - The size of the class is at most one byte. 37 | // - Only true-atomic operations are used internally, i.e. we never 38 | // fall back to a system-level mechanism for locking. 39 | // - The `lock()` method will busy-wait without yielding. 40 | // - The class is TriviallyDestructible. 41 | // 42 | // These guarantees make this spin mutex implementation suitable for scenarios 43 | // where other locking mechanisms or other spin mutex implementations would be 44 | // untenable. In particular, it can be used when size is a prime concern (e.g. 45 | // when reusing free bytes in an existing data structure), or when yielding 46 | // to the system is unacceptable (e.g. because of latency constraints). 47 | // 48 | // Note that in most cases, the need for such fine-grained locking hints at 49 | // the fact that RCU[1] should be used instead. 50 | // 51 | // [1]: https://en.wikipedia.org/wiki/Read-copy-update 52 | struct small_spin_mutex { 53 | small_spin_mutex() = default; 54 | small_spin_mutex(small_spin_mutex const&) = delete; 55 | small_spin_mutex(small_spin_mutex&&) = delete; 56 | 57 | small_spin_mutex& operator=(small_spin_mutex const&) = delete; 58 | small_spin_mutex& operator=(small_spin_mutex&&) = delete; 59 | 60 | // Try locking the mutex, and return whether it succeeded. 61 | // 62 | // If the mutex is already locked, this method will return immediately 63 | // without blocking. To block the calling thread until the mutex can 64 | // be acquired, use `lock()` instead. 65 | // 66 | // @returns 67 | // True if the mutex has been acquired and is now owned by the calling 68 | // thread, and false otherwise. 69 | // 70 | // TODO: In C++17, apply the [[nodiscard]] attribute to make sure people 71 | // do not misuse this. 72 | bool try_lock() noexcept { 73 | return !flag_.test_and_set(std::memory_order_acquire); 74 | } 75 | 76 | // Blocks until the calling thread acquires the mutex. 77 | // 78 | // This method will busy-wait until it can acquire the mutex. There is 79 | // no back off policy for yielding after a certain number of attempts 80 | // have been made. 81 | // 82 | // When this method returns, the calling thread has acquired the mutex. 83 | // 84 | // The behavior is undefined if this method is called while the calling 85 | // thread already owns the mutex (concretely, you should expect a deadlock). 86 | void lock() noexcept { 87 | while (!try_lock()) 88 | /* spin */; 89 | } 90 | 91 | // Unlocks the mutex. 92 | // 93 | // The behavior is undefined if the mutex was not owned by the calling 94 | // thread. 95 | void unlock() noexcept { 96 | flag_.clear(std::memory_order_release); 97 | } 98 | 99 | private: 100 | std::atomic_flag flag_ = ATOMIC_FLAG_INIT; 101 | }; 102 | 103 | } // end namespace amz 104 | 105 | #endif // include guard 106 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_program(MEMORYCHECK_COMMAND NAMES valgrind-wrapper valgrind) 2 | if (MEMORYCHECK_COMMAND) 3 | message(STATUS "Found Valgrind: ${MEMORYCHECK_COMMAND}") 4 | set(MEMORYCHECK_COMMAND_OPTIONS "--leak-check=full --gen-suppressions=all --error-exitcode=1") 5 | set(MEMORYCHECK_SUPPRESSIONS_FILE "${CMAKE_CURRENT_SOURCE_DIR}/valgrind.supp") 6 | add_custom_target(test-valgrind 7 | COMMENT "Build and then run all the unit tests under a memory checker." 8 | COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure -D ExperimentalMemCheck 9 | WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" 10 | DEPENDS build-tests 11 | USES_TERMINAL) 12 | include(CTest) 13 | else() 14 | message(STATUS "Valgrind not found; the test-valgrind target won't be available") 15 | endif() 16 | 17 | # add_test(...) 18 | # 19 | # Equivalent to a vanilla `add_test`, but adds a dependency on the 20 | # `build-tests` test. This way, running the `test` target will build 21 | # the unit tests before trying to run them, which would otherwise fail. 22 | function(add_test) 23 | cmake_parse_arguments(ARG "" "NAME" "" ${ARGN}) 24 | _add_test(${ARGN}) 25 | set_property(TEST ${ARG_NAME} APPEND 26 | PROPERTY DEPENDS build-tests) 27 | endfunction() 28 | 29 | # get_target_name( ) 30 | # 31 | # Retrieves the name of the unit test target associated to a single source file. 32 | function(get_target_name target file) 33 | string(REPLACE ".cpp" "" _target "${file}") 34 | string(REPLACE "/" "." _target "${_target}") 35 | set(${target} "test.${_target}" PARENT_SCOPE) 36 | endfunction() 37 | 38 | find_package(Catch2 REQUIRED) 39 | find_package(Boost REQUIRED COMPONENTS filesystem) 40 | 41 | # add_test_executable( ) 42 | # 43 | # Creates a test executable for the given source file. 44 | function(add_test_executable target file) 45 | add_executable(${target} EXCLUDE_FROM_ALL "${file}") 46 | target_link_libraries(${target} PRIVATE atl Catch2::Catch Boost::boost Boost::filesystem) 47 | set_target_properties(${target} PROPERTIES CXX_EXTENSIONS NO) 48 | target_compile_options(${target} PRIVATE -Wall) 49 | add_dependencies(build-tests ${target}) 50 | endfunction() 51 | 52 | # Add all the unit tests. 53 | file(GLOB_RECURSE UNIT_TESTS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*.cpp") 54 | foreach(testfile IN LISTS UNIT_TESTS) 55 | get_target_name(target "${testfile}") 56 | if (NOT TARGET ${target}) 57 | add_test_executable(${target} "${testfile}") 58 | add_test(NAME ${target} COMMAND ${target} 59 | WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") 60 | endif() 61 | endforeach() 62 | 63 | # For the special multi-process unit test, make sure tests are run in the 64 | # correct order. 65 | set_tests_properties(test.deferred_reclamation_allocator.interprocess.0_setup PROPERTIES FIXTURES_SETUP deferred_alloc_ipc) 66 | set_tests_properties(test.deferred_reclamation_allocator.interprocess.2_cleanup PROPERTIES FIXTURES_CLEANUP deferred_alloc_ipc) 67 | set_tests_properties(test.deferred_reclamation_allocator.interprocess.1_allocate PROPERTIES FIXTURES_REQUIRED deferred_alloc_ipc) 68 | -------------------------------------------------------------------------------- /test/algorithm/copy_while.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define CATCH_CONFIG_MAIN 22 | #include 23 | 24 | 25 | // An iterator that counts the number of dereferences and increments are done 26 | // on it. Useful in the tests below. 27 | template 28 | struct counting_iterator : std::iterator_traits { 29 | counting_iterator(Iterator it, int& increments, int& dereferences) 30 | : iterator(it), increments(increments), dereferences(dereferences) 31 | { } 32 | 33 | counting_iterator(counting_iterator const& other) = default; 34 | 35 | Iterator iterator; 36 | int& increments; 37 | int& dereferences; 38 | 39 | counting_iterator& operator++() { 40 | ++increments; 41 | ++iterator; 42 | return *this; 43 | } 44 | 45 | counting_iterator operator++(int) { 46 | counting_iterator copy = *this; 47 | ++(*this); 48 | return copy; 49 | } 50 | 51 | typename std::iterator_traits::reference operator*() { 52 | ++dereferences; 53 | return *iterator; 54 | } 55 | 56 | friend bool operator==(counting_iterator const& a, counting_iterator const& b) 57 | { return a.iterator == b.iterator; } 58 | friend bool operator!=(counting_iterator const& a, counting_iterator const& b) 59 | { return !(a == b); } 60 | }; 61 | 62 | template 63 | auto less_than(T const& t) { 64 | return [=](T const& x) { return x < t; }; 65 | } 66 | 67 | TEST_CASE("test with an empty range") { 68 | std::array data; 69 | std::vector actual; 70 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), [](int) { return true; }); 71 | REQUIRE(actual.empty()); 72 | } 73 | 74 | TEST_CASE("test case 0") { 75 | std::array data = {{0, 1, 2, 3, 4, 5}}; 76 | std::vector actual; 77 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), less_than(0)); 78 | 79 | std::vector expected; 80 | REQUIRE(actual == expected); 81 | } 82 | 83 | TEST_CASE("test case 1") { 84 | std::array data = {{0, 1, 2, 3, 4, 5}}; 85 | std::vector actual; 86 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), less_than(1)); 87 | 88 | std::vector expected; 89 | expected.push_back(0); 90 | REQUIRE(actual == expected); 91 | } 92 | 93 | TEST_CASE("test case 2") { 94 | std::array data = {{0, 1, 2, 3, 4, 5}}; 95 | std::vector actual; 96 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), less_than(2)); 97 | 98 | std::vector expected = {0, 1}; 99 | REQUIRE(actual == expected); 100 | } 101 | 102 | TEST_CASE("test case 3") { 103 | std::array data = {{0, 1, 2, 3, 4, 5}}; 104 | std::vector actual; 105 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), less_than(3)); 106 | 107 | std::vector expected = {0, 1, 2}; 108 | REQUIRE(actual == expected); 109 | } 110 | 111 | TEST_CASE("test case 4") { 112 | std::array data = {{0, 1, 2, 3, 4, 5}}; 113 | std::vector actual; 114 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), less_than(4)); 115 | 116 | std::vector expected = {0, 1, 2, 3}; 117 | REQUIRE(actual == expected); 118 | } 119 | 120 | TEST_CASE("test case 5") { 121 | std::array data = {{0, 1, 2, 3, 4, 5}}; 122 | std::vector actual; 123 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), less_than(5)); 124 | 125 | std::vector expected = {0, 1, 2, 3, 4}; 126 | REQUIRE(actual == expected); 127 | } 128 | 129 | TEST_CASE("test case 6") { 130 | std::array data = {{0, 1, 2, 3, 4, 5}}; 131 | std::vector actual; 132 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), less_than(6)); 133 | std::vector expected = {0, 1, 2, 3, 4, 5}; 134 | REQUIRE(actual == expected); 135 | } 136 | 137 | TEST_CASE("test case 7") { 138 | std::array data = {{0, 1, 2, 3, 4, 5}}; 139 | std::vector actual; 140 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), [](int) { return true; }); 141 | std::vector expected = {0, 1, 2, 3, 4, 5}; 142 | REQUIRE(actual == expected); 143 | } 144 | 145 | TEST_CASE("check returned pair") { 146 | std::array data = {{0, 1, 2, 3, 4, 5}}; 147 | std::vector actual; 148 | using BackInserter =std::back_insert_iterator>; 149 | std::pair result = 150 | amz::copy_while(data.begin(), data.end(), std::back_inserter(actual), less_than(3)); 151 | 152 | std::vector expected = {0, 1, 2}; 153 | REQUIRE(actual == expected); 154 | 155 | REQUIRE(result.first == data.begin() + 3); 156 | *result.second = 999; 157 | expected.push_back(999); 158 | REQUIRE(actual == expected); 159 | } 160 | 161 | TEST_CASE("check exact number of increments and dereferences") { 162 | std::array data = {{0, 1, 2, 3, 4, 5}}; 163 | int first_increments = 0, first_dereferences = 0; 164 | counting_iterator first(data.begin(), first_increments, first_dereferences); 165 | 166 | int last_increments = 0, last_dereferences = 0; 167 | counting_iterator last(data.end(), last_increments, last_dereferences); 168 | 169 | std::vector result; 170 | amz::copy_while(first, last, std::back_inserter(result), less_than(3)); 171 | REQUIRE(first_increments == 3); // first does not reach the end 172 | REQUIRE(first_dereferences == 4); // 173 | 174 | REQUIRE(last_increments == 0); // Should obviously be met 175 | REQUIRE(last_dereferences == 0); // 176 | } 177 | 178 | TEST_CASE("increments and dereferences all the range") { 179 | std::array data = {{0, 1, 2, 3, 4, 5}}; 180 | int first_increments = 0, first_dereferences = 0; 181 | counting_iterator first(data.begin(), first_increments, first_dereferences); 182 | 183 | int last_increments = 0, last_dereferences = 0; 184 | counting_iterator last(data.end(), last_increments, last_dereferences); 185 | 186 | std::vector result; 187 | amz::copy_while(first, last, std::back_inserter(result), [](int) { return true; }); 188 | REQUIRE(first_increments == 6); // first reaches the end 189 | REQUIRE(first_dereferences == 6); // 190 | 191 | REQUIRE(last_increments == 0); // Should obviously be met 192 | REQUIRE(last_dereferences == 0); // 193 | } 194 | -------------------------------------------------------------------------------- /test/algorithm/remove_and_copy_if.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define CATCH_CONFIG_MAIN 22 | #include 23 | 24 | 25 | template 26 | auto rmcp_if(In& in, Out& out, Pred const& pred) { 27 | return amz::remove_and_copy_if(in.begin(), in.end(), std::back_inserter(out), pred); 28 | } 29 | 30 | TEST_CASE("remove nothing") { 31 | std::array data = {{0, 1, 2, 3, 4, 5}}; 32 | std::vector actual; 33 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 34 | 35 | std::vector expected = {}; 36 | REQUIRE(actual == expected); 37 | REQUIRE(result.first == data.end()); 38 | } 39 | 40 | TEST_CASE("remove first element") { 41 | std::array data = {{-1, 1, 2, 3, 4, 5}}; 42 | std::vector actual; 43 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 44 | 45 | std::vector expected = {-1}; 46 | REQUIRE(actual == expected); 47 | REQUIRE(result.first == data.begin() + 5); 48 | } 49 | 50 | TEST_CASE("remove 2") { 51 | std::array data = {{-1, 1, -2, 3, 4, 5}}; 52 | std::vector actual; 53 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 54 | 55 | std::vector expected = {-1, -2}; 56 | REQUIRE(actual == expected); 57 | REQUIRE(result.first == data.begin() + 4); 58 | } 59 | 60 | TEST_CASE("remove 3") { 61 | std::array data = {{-1, 1, -2, -3, 4, 5}}; 62 | std::vector actual; 63 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 64 | 65 | std::vector expected = {-1, -2, -3}; 66 | REQUIRE(actual == expected); 67 | REQUIRE(result.first == data.begin() + 3); 68 | } 69 | 70 | TEST_CASE("remove 4") { 71 | std::array data = {{-1, 1, -2, -3, -4, 5}}; 72 | std::vector actual; 73 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 74 | 75 | std::vector expected = {-1, -2, -3, -4}; 76 | REQUIRE(actual == expected); 77 | REQUIRE(result.first == data.begin() + 2); 78 | } 79 | 80 | TEST_CASE("remove 5") { 81 | std::array data = {{-1, -2, -3, -4, -5, -6}}; 82 | std::vector actual; 83 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 84 | 85 | std::vector expected = {-1, -2, -3, -4, -5, -6}; 86 | REQUIRE(actual == expected); 87 | REQUIRE(result.first == data.begin()); 88 | } 89 | 90 | TEST_CASE("remove 6") { 91 | std::array data = {{1, 2, -3, 4, 5, 6}}; 92 | std::vector actual; 93 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 94 | 95 | std::vector expected = {-3}; 96 | REQUIRE(actual == expected); 97 | REQUIRE(result.first == data.begin() + 5); 98 | } 99 | 100 | TEST_CASE("remove 7") { 101 | std::array data = {{1, 2, -3, -4, 5, 6}}; 102 | std::vector actual; 103 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 104 | 105 | std::vector expected = {-3, -4}; 106 | REQUIRE(actual == expected); 107 | REQUIRE(result.first == data.begin() + 4); 108 | } 109 | 110 | TEST_CASE("corner case: empty input range") { 111 | std::array data; 112 | std::vector actual; 113 | auto result = rmcp_if(data, actual, [](int) { return true; }); 114 | 115 | REQUIRE(actual.empty()); 116 | REQUIRE(result.first == data.end()); 117 | } 118 | 119 | TEST_CASE("corner case: singleton input range; don't remove anything") { 120 | std::array data = {{-1}}; 121 | std::vector actual; 122 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 123 | 124 | std::vector expected = {-1}; 125 | REQUIRE(actual == expected); 126 | REQUIRE(result.first == data.begin()); 127 | } 128 | 129 | TEST_CASE("corner case: singleton input range, remove everything") { 130 | std::array data = {{1}}; 131 | std::vector actual; 132 | auto result = rmcp_if(data, actual, [](int x) { return x < 0; }); 133 | 134 | std::vector expected = {}; 135 | REQUIRE(actual == expected); 136 | REQUIRE(result.first == data.end()); 137 | } 138 | -------------------------------------------------------------------------------- /test/algorithm/remove_range_if.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | TEST_CASE("remove_range_if empty") { 29 | auto sequence = std::vector{}; 30 | 31 | auto old_end = std::end(sequence); 32 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [](auto, auto) { 33 | FAIL("function should not be visited for empty sequence"); 34 | return false; 35 | }, [](auto, auto) { 36 | FAIL("function should not be visited for empty sequence"); 37 | return false; 38 | }); 39 | 40 | CHECK(old_end == new_end); 41 | } 42 | 43 | TEST_CASE("remove_range_if one element keep") { 44 | auto sequence = std::array{{'a'}}; 45 | auto const expected_sequence = sequence; 46 | auto p_call_count = 0; 47 | 48 | auto old_end = std::end(sequence); 49 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [](auto, auto) { 50 | FAIL("function should not be visited for empty sequence"); 51 | return false; 52 | }, [&](auto range_first, auto range_last) { 53 | ++ p_call_count; 54 | CHECK(range_first == std::begin(sequence)); 55 | CHECK(range_last == old_end); 56 | return false; 57 | }); 58 | 59 | CHECK(old_end == new_end); 60 | CHECK(std::equal(std::begin(expected_sequence), std::end(expected_sequence), std::begin(sequence))); 61 | CHECK(p_call_count == 1); 62 | } 63 | 64 | TEST_CASE("remove_range_if one element remove") { 65 | auto sequence = std::vector>{}; 66 | sequence.emplace_back(new int(7654321)); 67 | 68 | auto p_call_count = 0; 69 | 70 | auto old_end = std::end(sequence); 71 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [](auto const&, auto const&) { 72 | FAIL("function should not be visited for empty sequence"); 73 | return false; 74 | }, [&](auto range_first, auto range_last) { 75 | ++ p_call_count; 76 | CHECK(range_first == std::begin(sequence)); 77 | CHECK(range_last == old_end); 78 | return true; 79 | }); 80 | 81 | CHECK(std::begin(sequence) == new_end); 82 | CHECK(1 == sequence.size()); 83 | CHECK(7654321 == *sequence[0]); 84 | CHECK(p_call_count == 1); 85 | } 86 | 87 | TEST_CASE("remove_range_if two same elements keep") { 88 | auto sequence = std::list{{123, 123}}; 89 | auto const expected_sequence = sequence; 90 | auto e_call_count = 0; 91 | auto p_call_count = 0; 92 | 93 | auto old_end = std::end(sequence); 94 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [&](auto element1, auto element2) { 95 | ++ e_call_count; 96 | CHECK(123 == element1); 97 | CHECK(123 == element2); 98 | return true; 99 | }, [&](auto range_first, auto range_last) { 100 | ++ p_call_count; 101 | CHECK(range_first == std::begin(sequence)); 102 | CHECK(range_last == std::end(sequence)); 103 | return false; 104 | }); 105 | 106 | CHECK(old_end == new_end); 107 | CHECK(std::equal(std::begin(expected_sequence), std::end(expected_sequence), std::begin(sequence))); 108 | CHECK(e_call_count == sequence.size() - 1); 109 | CHECK(p_call_count == 1); 110 | } 111 | 112 | TEST_CASE("remove_range_if two same elements drop") { 113 | auto sequence = std::list{{123, 123}}; 114 | auto const expected_sequence = std::list{{123, 123}}; 115 | auto e_call_count = 0; 116 | auto p_call_count = 0; 117 | 118 | auto old_end = std::end(sequence); 119 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [&](auto element1, auto element2) { 120 | ++ e_call_count; 121 | CHECK(123 == element1); 122 | CHECK(123 == element2); 123 | return true; 124 | }, [&](auto range_first, auto range_last) { 125 | ++ p_call_count; 126 | CHECK(range_first == std::begin(sequence)); 127 | CHECK(range_last == std::end(sequence)); 128 | return true; 129 | }); 130 | 131 | CHECK(std::begin(sequence) == new_end); 132 | CHECK(std::equal(std::begin(expected_sequence), std::end(expected_sequence), std::begin(sequence))); 133 | CHECK(e_call_count == sequence.size() - 1); 134 | CHECK(p_call_count == 1); 135 | } 136 | 137 | TEST_CASE("remove_range_if two different elements keep") { 138 | auto sequence = std::list{{123, 456}}; 139 | auto const expected_sequence = sequence; 140 | auto e_call_count = 0; 141 | auto p_call_count = 0; 142 | 143 | auto old_end = std::end(sequence); 144 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [&](auto element1, auto element2) { 145 | ++ e_call_count; 146 | CHECK(sequence.front() == std::min(element1, element2)); 147 | CHECK(sequence.back() == std::max(element1, element2)); 148 | return false; 149 | }, [&](auto range_first, auto range_last) { 150 | ++ p_call_count; 151 | switch (p_call_count) { 152 | case 1: 153 | CHECK(range_first == std::begin(sequence)); 154 | CHECK(range_last == std::next(range_first)); 155 | break; 156 | case 2: 157 | CHECK(range_first == std::next(std::begin(sequence))); 158 | CHECK(range_last == std::next(range_first)); 159 | break; 160 | } 161 | return false; 162 | }); 163 | 164 | CHECK(old_end == new_end); 165 | CHECK(std::equal(std::begin(expected_sequence), std::end(expected_sequence), std::begin(sequence))); 166 | CHECK(e_call_count == sequence.size() - 1); 167 | CHECK(p_call_count == 2); 168 | } 169 | 170 | TEST_CASE("remove_range_if two different elements drop first") { 171 | auto sequence = std::list{{123, 456}}; 172 | auto const expected_sequence = std::list{{456}}; 173 | auto e_call_count = 0; 174 | auto p_call_count = 0; 175 | 176 | auto old_end = std::end(sequence); 177 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [&](auto element1, auto element2) { 178 | ++ e_call_count; 179 | CHECK(sequence.front() == std::min(element1, element2)); 180 | CHECK(sequence.back() == std::max(element1, element2)); 181 | return false; 182 | }, [&](auto range_first, auto range_last) { 183 | ++ p_call_count; 184 | switch (p_call_count) { 185 | case 1: 186 | CHECK(range_first == std::begin(sequence)); 187 | CHECK(range_last == std::next(range_first)); 188 | return true; 189 | case 2: 190 | CHECK(range_first == std::next(std::begin(sequence))); 191 | CHECK(range_last == std::next(range_first)); 192 | return false; 193 | } 194 | FAIL("called too many times?"); 195 | return false; 196 | }); 197 | 198 | CHECK(std::prev(old_end) == new_end); 199 | CHECK(std::equal(std::begin(expected_sequence), std::end(expected_sequence), std::begin(sequence))); 200 | CHECK(e_call_count == sequence.size() - 1); 201 | CHECK(p_call_count == 2); 202 | } 203 | 204 | TEST_CASE("remove_range_if two different elements drop second") { 205 | auto sequence = std::list{{123, 456}}; 206 | auto const expected_sequence = std::list{{123}}; 207 | auto e_call_count = 0; 208 | auto p_call_count = 0; 209 | 210 | auto old_end = std::end(sequence); 211 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [&](auto element1, auto element2) { 212 | ++ e_call_count; 213 | CHECK(sequence.front() == std::min(element1, element2)); 214 | CHECK(sequence.back() == std::max(element1, element2)); 215 | return false; 216 | }, [&](auto range_first, auto range_last) { 217 | ++ p_call_count; 218 | switch (p_call_count) { 219 | case 1: 220 | CHECK(range_first == std::begin(sequence)); 221 | CHECK(range_last == std::next(range_first)); 222 | return false; 223 | case 2: 224 | CHECK(range_first == std::next(std::begin(sequence))); 225 | CHECK(range_last == std::next(range_first)); 226 | return true; 227 | } 228 | FAIL("called too many times?"); 229 | return false; 230 | }); 231 | 232 | CHECK(std::prev(old_end) == new_end); 233 | CHECK(std::equal(std::begin(expected_sequence), std::end(expected_sequence), std::begin(sequence))); 234 | CHECK(e_call_count == sequence.size() - 1); 235 | CHECK(p_call_count == 2); 236 | } 237 | 238 | TEST_CASE("remove_range_if large varied string") { 239 | // choose ranges using case-insensitive equality; 240 | // filter ranges that begin with upper case 241 | using namespace std::literals; 242 | auto sequence = "AaAgRRRRrrrjJJJ843kaniu32NFNNFFFFggggg"s; 243 | auto const expected_sequence = "gjJJJ843kaniu32ggggg"s; 244 | auto e_call_count = 0; 245 | auto p_call_count = 0; 246 | 247 | auto old_end = std::end(sequence); 248 | auto new_end = amz::remove_range_if(std::begin(sequence), old_end, [&](auto element1, auto element2) { 249 | ++ e_call_count; 250 | return std::tolower(element1) == std::tolower(element2); 251 | return false; 252 | }, [&](auto range_first, auto) { 253 | ++ p_call_count; 254 | return std::isupper(*range_first); 255 | }); 256 | 257 | CHECK(std::next(std::begin(sequence), 20) == new_end); 258 | CHECK(std::equal(std::begin(expected_sequence), std::end(expected_sequence), std::begin(sequence))); 259 | CHECK(e_call_count == sequence.size() - 1); 260 | CHECK(p_call_count == 19); 261 | } 262 | -------------------------------------------------------------------------------- /test/bounded_channel/custom_container.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | 23 | TEST_CASE("The underlying container of the channel can be customized") { 24 | using Container = std::list; 25 | amz::bounded_channel channel{64}; 26 | channel.push(1); 27 | channel.push(2); 28 | channel.push(3); 29 | channel.push(4); 30 | channel.push(5); 31 | channel.close(); 32 | 33 | std::vector actual; 34 | for (int i : channel) { 35 | actual.push_back(i); 36 | } 37 | 38 | std::vector const expected = {1, 2, 3, 4 ,5}; 39 | REQUIRE(actual == expected); 40 | } 41 | -------------------------------------------------------------------------------- /test/bounded_channel/iterator.comparison.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define CATCH_CONFIG_MAIN 4 | #include 5 | 6 | #include 7 | 8 | 9 | // This test makes sure that the bounded_channel's iterators do not require 10 | // their value_type to be EqualityComparable. 11 | 12 | struct NonComparable { }; 13 | 14 | bool operator==(NonComparable const&, NonComparable const&) = delete; 15 | bool operator!=(NonComparable const&, NonComparable const&) = delete; 16 | 17 | TEST_CASE("Iterators don't require an EqualityComparable value_type") { 18 | amz::bounded_channel channel{64}; 19 | channel.push(NonComparable{}); 20 | channel.close(); 21 | 22 | for (auto it = std::begin(channel); it != std::end(channel); ++it) { 23 | // nothing 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/bounded_channel/iterator.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | 26 | TEST_CASE("Iterators compare against past-the-end iterator properly") { 27 | amz::bounded_channel channel{64}; 28 | 29 | SECTION("With an empty and closed channel") { 30 | channel.close(); 31 | auto it = std::begin(channel); 32 | REQUIRE(it == std::end(channel)); 33 | } 34 | 35 | SECTION("With a non-empty and non-closed channel") { 36 | channel.push(1); 37 | auto it = std::begin(channel); 38 | REQUIRE(it != std::end(channel)); 39 | } 40 | 41 | SECTION("With a non-empty and closed channel") { 42 | channel.push(1); 43 | channel.close(); 44 | auto it = std::begin(channel); 45 | REQUIRE(it != std::end(channel)); 46 | ++it; 47 | REQUIRE(it == std::end(channel)); 48 | } 49 | } 50 | 51 | TEST_CASE("Copy of an iterator points to the same value as the original iterator") { 52 | amz::bounded_channel channel{64}; 53 | channel.push(1); 54 | channel.push(2); 55 | 56 | auto it = std::begin(channel); 57 | auto copy1 = it; 58 | REQUIRE(*it == 1); 59 | REQUIRE(*copy1 == 1); 60 | 61 | ++it; 62 | auto copy2 = it; 63 | REQUIRE(*it == 2); 64 | REQUIRE(*copy2 == 2); 65 | } 66 | 67 | TEST_CASE("Assigned iterator points to the same value as the assigned-from iterator") { 68 | amz::bounded_channel channel{64}; 69 | channel.push(1); 70 | channel.push(2); 71 | 72 | auto it = std::begin(channel); 73 | amz::bounded_channel::iterator other; 74 | other = it; 75 | REQUIRE(*it == 1); 76 | REQUIRE(*other == 1); 77 | 78 | ++it; 79 | other = it; 80 | REQUIRE(*it == 2); 81 | REQUIRE(*other == 2); 82 | } 83 | 84 | TEST_CASE("Non past-the-end iterators do not compare equal") { 85 | amz::bounded_channel channel{64}; 86 | channel.push(1); 87 | channel.push(2); 88 | channel.push(3); 89 | channel.push(4); 90 | 91 | auto it1 = std::begin(channel); 92 | auto it2 = std::begin(channel); 93 | REQUIRE(it1 != it2); 94 | 95 | ++it1; 96 | ++it2; 97 | REQUIRE(it1 != it2); 98 | } 99 | 100 | TEST_CASE("Comparison with past-the-end iterator works as expected") { 101 | amz::bounded_channel channel{64}; 102 | channel.push(1); 103 | channel.push(2); 104 | channel.close(); 105 | 106 | auto it = std::begin(channel); 107 | auto end = std::end(channel); 108 | REQUIRE(it != end); 109 | 110 | ++it; 111 | REQUIRE(it != end); 112 | 113 | ++it; 114 | REQUIRE(it == end); 115 | REQUIRE(it == std::end(channel)); 116 | } 117 | 118 | TEST_CASE("Iterator can be used to iterate over the contents of the channel when the channel is not empty") { 119 | amz::bounded_channel channel{64}; 120 | channel.push(1); 121 | channel.push(2); 122 | channel.push(3); 123 | channel.push(4); 124 | 125 | std::thread iter_thread; 126 | 127 | SECTION("When all the channel is populated before the iterator is created") { 128 | iter_thread = std::thread{[&] { 129 | std::vector actual; 130 | std::copy(std::begin(channel), std::end(channel), std::back_inserter(actual)); 131 | std::vector const expected = {1, 2, 3, 4}; 132 | REQUIRE(actual == expected); 133 | }}; 134 | } 135 | 136 | SECTION("When more elements are added after the iterator has been created") { 137 | iter_thread = std::thread{[&] { 138 | std::vector actual; 139 | std::copy(std::begin(channel), std::end(channel), std::back_inserter(actual)); 140 | std::vector const expected = {1, 2, 3, 4, 5, 6, 7}; 141 | REQUIRE(actual == expected); 142 | }}; 143 | channel.push(5); 144 | channel.push(6); 145 | channel.push(7); 146 | } 147 | 148 | channel.close(); 149 | iter_thread.join(); 150 | } 151 | 152 | TEST_CASE("Iterator blocks when the channel is empty") { 153 | amz::bounded_channel channel{64}; 154 | 155 | std::atomic unblocked{false}; 156 | std::atomic started{false}; 157 | std::thread iter_thread{[&] { 158 | started = true; 159 | auto it = std::begin(channel); 160 | unblocked = true; 161 | REQUIRE(*it == 1); 162 | ++it; 163 | REQUIRE(*it == 2); 164 | }}; 165 | 166 | // Wait until the `iter_thread` has started. 167 | while (!started) std::this_thread::yield(); 168 | 169 | // Make sure the thread is blocked. 170 | REQUIRE(started); 171 | REQUIRE(!unblocked); 172 | 173 | // Push something to unblock it. 174 | channel.push(1); 175 | channel.push(2); 176 | 177 | iter_thread.join(); 178 | } 179 | 180 | TEST_CASE("Iterator drains the channel when the channel is closed") { 181 | amz::bounded_channel channel{64}; 182 | channel.push(1); 183 | channel.push(2); 184 | channel.push(3); 185 | channel.push(4); 186 | 187 | std::vector actual; 188 | 189 | SECTION("When the channel is closed before the iterator has been created") { 190 | channel.close(); 191 | auto it = std::begin(channel); 192 | std::copy(it, std::end(channel), std::back_inserter(actual)); 193 | std::vector const expected = {1, 2, 3, 4}; 194 | REQUIRE(actual == expected); 195 | } 196 | 197 | SECTION("When the channel is closed after the iterator has been created") { 198 | auto it = std::begin(channel); 199 | channel.push(5); 200 | channel.close(); 201 | std::copy(it, std::end(channel), std::back_inserter(actual)); 202 | std::vector const expected = {1, 2, 3, 4, 5}; 203 | REQUIRE(actual == expected); 204 | } 205 | } 206 | 207 | TEST_CASE("More than one iterator can be used to pop from the channel at once") { 208 | amz::bounded_channel channel{64}; 209 | channel.push(1); 210 | channel.push(2); 211 | channel.push(3); 212 | channel.push(4); 213 | channel.push(5); 214 | channel.push(6); 215 | channel.close(); 216 | 217 | auto it1 = std::begin(channel); 218 | auto it2 = std::begin(channel); 219 | REQUIRE(*it1 == 1); 220 | REQUIRE(*it2 == 2); 221 | 222 | ++it1; 223 | REQUIRE(*it1 == 3); 224 | 225 | ++it2; 226 | REQUIRE(*it2 == 4); 227 | 228 | ++it2; 229 | REQUIRE(*it2 == 5); 230 | 231 | ++it1; 232 | REQUIRE(*it1 == 6); 233 | } 234 | -------------------------------------------------------------------------------- /test/bounded_channel/pop.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | 23 | TEST_CASE("pop() succeeds when the channel is non-empty and open") { 24 | amz::bounded_channel channel{64}; 25 | channel.push(1); 26 | channel.push(2); 27 | 28 | int i = 999; 29 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 30 | REQUIRE(i == 1); 31 | 32 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 33 | REQUIRE(i == 2); 34 | } 35 | 36 | TEST_CASE("pop() succeeds when the channel is non-empty and closed") { 37 | amz::bounded_channel channel{64}; 38 | channel.push(1); 39 | channel.push(2); 40 | channel.close(); 41 | 42 | int i = 999; 43 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 44 | REQUIRE(i == 1); 45 | 46 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 47 | REQUIRE(i == 2); 48 | } 49 | 50 | TEST_CASE("pop() returns `closed` when the channel is empty and closed") { 51 | amz::bounded_channel channel{64}; 52 | channel.push(1); 53 | 54 | int i = 999; 55 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 56 | REQUIRE(i == 1); 57 | 58 | channel.close(); 59 | REQUIRE(channel.pop(i) == amz::channel_op_status::closed); 60 | REQUIRE(i == 1); 61 | } 62 | 63 | TEST_CASE("pop() blocks until a value becomes available") { 64 | amz::bounded_channel channel{64}; 65 | 66 | std::atomic started{false}; 67 | std::thread t{[&] { 68 | started = true; 69 | int i = 999; 70 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 71 | REQUIRE(i == 1); 72 | }}; 73 | 74 | // Wait until the thread has started. 75 | while (!started) std::this_thread::yield(); 76 | 77 | // Here, we assume the thread is blocked in `pop()`, and this will unblock it. 78 | channel.push(1); 79 | 80 | t.join(); 81 | } 82 | -------------------------------------------------------------------------------- /test/bounded_channel/pop_into_optional.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | 25 | TEST_CASE("popping a value into something that is only assignable from the value_type of the channel works") { 26 | amz::bounded_channel channel{64}; 27 | boost::optional result = boost::none; 28 | 29 | SECTION("pop()") { 30 | channel.push(9); 31 | channel.pop(result); 32 | REQUIRE(result == 9); 33 | } 34 | 35 | SECTION("try_pop()") { 36 | channel.try_pop(result); 37 | REQUIRE(result == boost::none); 38 | 39 | channel.push(9); 40 | channel.try_pop(result); 41 | REQUIRE(result == 9); 42 | } 43 | 44 | SECTION("try_pop_for()") { 45 | channel.try_pop_for(std::chrono::milliseconds{1}, result); 46 | REQUIRE(result == boost::none); 47 | 48 | channel.push(9); 49 | channel.try_pop_for(std::chrono::milliseconds{1}, result); 50 | REQUIRE(result == 9); 51 | } 52 | 53 | SECTION("try_pop_until()") { 54 | auto const future = std::chrono::steady_clock::now() + std::chrono::milliseconds{1}; 55 | channel.try_pop_until(future, result); 56 | REQUIRE(result == boost::none); 57 | 58 | channel.push(9); 59 | channel.try_pop_until(future, result); 60 | REQUIRE(result == 9); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/bounded_channel/push.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | 23 | TEST_CASE("push() succeeds when the channel is non-full and open") { 24 | amz::bounded_channel channel{64}; 25 | REQUIRE(channel.push(1) == amz::channel_op_status::success); 26 | 27 | int i = 999; 28 | channel.pop(i); 29 | REQUIRE(i == 1); 30 | } 31 | 32 | TEST_CASE("push() returns `closed` when the channel is non-full and closed") { 33 | amz::bounded_channel channel{64}; 34 | channel.close(); 35 | REQUIRE(channel.push(1) == amz::channel_op_status::closed); 36 | } 37 | 38 | TEST_CASE("push() returns `closed` when the channel is full and closed") { 39 | amz::bounded_channel channel{3}; 40 | channel.push(1); 41 | channel.push(2); 42 | channel.push(3); 43 | channel.close(); 44 | REQUIRE(channel.push(4) == amz::channel_op_status::closed); 45 | } 46 | 47 | TEST_CASE("push() blocks until the channel becomes non-full") { 48 | amz::bounded_channel channel{2}; 49 | channel.push(1); 50 | channel.push(2); 51 | 52 | std::atomic started{false}; 53 | std::thread t{[&] { 54 | started = true; 55 | REQUIRE(channel.push(3) == amz::channel_op_status::success); 56 | }}; 57 | 58 | // Wait until the thread has started. 59 | while (!started) std::this_thread::yield(); 60 | 61 | // Here, we assume the thread is blocked in `push()`, and this will unblock it. 62 | int i = 999; 63 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 64 | REQUIRE(i == 1); 65 | 66 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 67 | REQUIRE(i == 2); 68 | 69 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 70 | REQUIRE(i == 3); 71 | 72 | t.join(); 73 | } 74 | -------------------------------------------------------------------------------- /test/bounded_channel/stress.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | 26 | TEST_CASE("Stress test with multiple producer and consumer threads") { 27 | amz::bounded_channel channel{64}; 28 | 29 | constexpr int N_INTEGERS = 10000; 30 | constexpr int N_PRODUCERS = 10; 31 | constexpr int N_CONSUMERS = 10; 32 | 33 | // Producers put integers in an increasing fashion into the channel. 34 | std::vector producers; 35 | for (int i = 0; i != N_PRODUCERS; ++i) { 36 | producers.emplace_back([&channel] { 37 | for (int i = 0; i != N_INTEGERS; ++i) { 38 | channel.push(i); 39 | } 40 | }); 41 | } 42 | 43 | // Consumers read from the channel and populate their own local result 44 | // vector with whatever they extract from the channel. 45 | std::vector>> results; 46 | for (int i = 0; i != N_CONSUMERS; ++i) { 47 | results.push_back(std::async(std::launch::async, [&channel] { 48 | std::vector result; 49 | for (int value : channel) { 50 | result.push_back(value); 51 | } 52 | return result; 53 | })); 54 | } 55 | 56 | // Block until all the producers are done. Blocking for the consumers will 57 | // happen in `future::get()` below. When the producers are all done, we 58 | // close the channel so that the consumers know we're done for good. If we 59 | // don't do that, the consumers will be stuck trying to pop() forever. 60 | for (auto& producer : producers) { 61 | producer.join(); 62 | } 63 | channel.close(); 64 | 65 | // Aggregate all the resulting vectors into the same vector and make sure we 66 | // properly funneled everything through the channel. 67 | std::vector actual; 68 | for (auto& result : results) { 69 | std::vector tmp = result.get(); 70 | actual.insert(std::end(actual), std::begin(tmp), std::end(tmp)); 71 | } 72 | std::sort(std::begin(actual), std::end(actual)); 73 | 74 | std::vector expected; 75 | for (int prod = 0; prod != N_PRODUCERS; ++prod) { 76 | for (int i = 0; i != N_INTEGERS; ++i) { 77 | expected.push_back(i); 78 | } 79 | } 80 | std::sort(std::begin(expected), std::end(expected)); 81 | 82 | REQUIRE(actual == expected); 83 | } 84 | -------------------------------------------------------------------------------- /test/bounded_channel/try_pop.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | 23 | TEST_CASE("try_pop() succeeds when the channel is non-empty and open") { 24 | amz::bounded_channel channel{64}; 25 | channel.push(1); 26 | 27 | int i = 999; 28 | REQUIRE(channel.try_pop(i) == amz::channel_op_status::success); 29 | REQUIRE(i == 1); 30 | } 31 | 32 | TEST_CASE("try_pop() succeeds when the channel is non-empty and closed") { 33 | amz::bounded_channel channel{64}; 34 | channel.push(1); 35 | channel.close(); 36 | 37 | int i = 999; 38 | REQUIRE(channel.try_pop(i) == amz::channel_op_status::success); 39 | REQUIRE(i == 1); 40 | } 41 | 42 | TEST_CASE("try_pop() returns `closed` when the channel is empty and closed") { 43 | amz::bounded_channel channel{64}; 44 | channel.close(); 45 | int i = 999; 46 | REQUIRE(channel.try_pop(i) == amz::channel_op_status::closed); 47 | REQUIRE(i == 999); 48 | } 49 | 50 | TEST_CASE("try_pop() returns `empty` when the channel is empty but not closed") { 51 | amz::bounded_channel channel{64}; 52 | int i = 999; 53 | REQUIRE(channel.try_pop(i) == amz::channel_op_status::empty); 54 | REQUIRE(i == 999); 55 | } 56 | -------------------------------------------------------------------------------- /test/bounded_channel/try_pop_for.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | TEST_CASE("try_pop_for() succeeds when the channel is non-empty and open") { 25 | amz::bounded_channel channel{64}; 26 | channel.push(1); 27 | 28 | int i = 999; 29 | REQUIRE(channel.try_pop_for(std::chrono::seconds{0}, i) == amz::channel_op_status::success); 30 | REQUIRE(i == 1); 31 | } 32 | 33 | TEST_CASE("try_pop_for() succeeds when the channel is non-empty and closed") { 34 | amz::bounded_channel channel{64}; 35 | channel.push(1); 36 | channel.close(); 37 | 38 | int i = 999; 39 | REQUIRE(channel.try_pop_for(std::chrono::seconds{0}, i) == amz::channel_op_status::success); 40 | REQUIRE(i == 1); 41 | } 42 | 43 | TEST_CASE("try_pop_for() returns `closed` when the channel is empty and closed") { 44 | amz::bounded_channel channel{64}; 45 | channel.close(); 46 | int i = 999; 47 | REQUIRE(channel.try_pop_for(std::chrono::seconds{0}, i) == amz::channel_op_status::closed); 48 | REQUIRE(i == 999); 49 | } 50 | 51 | TEST_CASE("try_pop_for() returns `timeout` when the channel stays empty past the timeout") { 52 | amz::bounded_channel channel{64}; 53 | int i = 999; 54 | REQUIRE(channel.try_pop_for(std::chrono::milliseconds{1}, i) == amz::channel_op_status::timeout); 55 | REQUIRE(i == 999); 56 | } 57 | 58 | TEST_CASE("try_pop_for() succeeds when the channel becomes non-empty within the timeout") { 59 | amz::bounded_channel channel{64}; 60 | 61 | std::atomic started{false}; 62 | std::thread t{[&] { 63 | started = true; 64 | // Try popping for so long that we will virtually never run into the 65 | // situation where the other thread does not have the time to push() 66 | // before we time out, which would incorrectly fail this test. 67 | int i = 999; 68 | REQUIRE(channel.try_pop_for(std::chrono::seconds{10}, i) == amz::channel_op_status::success); 69 | REQUIRE(i == 1); 70 | }}; 71 | 72 | // Synchronize with the other thread to give it a chance to run. 73 | while (!started) std::this_thread::yield(); 74 | 75 | // Unblock the other thread. 76 | channel.push(1); 77 | std::this_thread::yield(); 78 | 79 | t.join(); 80 | } 81 | -------------------------------------------------------------------------------- /test/bounded_channel/try_pop_until.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | 21 | 22 | // 23 | // Note: Since `try_pop_until()` and `try_pop_for()` are basically implemented 24 | // the same, we don't bother testing this one too much. We roughly just 25 | // make sure it compiles. 26 | // 27 | 28 | TEST_CASE("try_pop_until() succeeds when the channel is non-empty and open") { 29 | amz::bounded_channel channel{64}; 30 | channel.push(1); 31 | 32 | int i = 999; 33 | REQUIRE(channel.try_pop_until(std::chrono::steady_clock::now(), i) == amz::channel_op_status::success); 34 | REQUIRE(i == 1); 35 | } 36 | -------------------------------------------------------------------------------- /test/bounded_channel/try_push.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | 20 | TEST_CASE("try_push() succeeds when the channel is non-full and open") { 21 | amz::bounded_channel channel{64}; 22 | REQUIRE(channel.try_push(1) == amz::channel_op_status::success); 23 | 24 | int i = 999; 25 | channel.pop(i); 26 | REQUIRE(i == 1); 27 | } 28 | 29 | TEST_CASE("try_push() returns `closed` when the channel is non-full and closed") { 30 | amz::bounded_channel channel{64}; 31 | channel.close(); 32 | REQUIRE(channel.try_push(1) == amz::channel_op_status::closed); 33 | } 34 | 35 | TEST_CASE("try_push() returns `closed` when the channel is full and closed") { 36 | amz::bounded_channel channel{3}; 37 | channel.push(1); 38 | channel.push(2); 39 | channel.push(3); 40 | channel.close(); 41 | REQUIRE(channel.try_push(4) == amz::channel_op_status::closed); 42 | } 43 | 44 | TEST_CASE("try_push() returns `full` when the channel is full and open") { 45 | amz::bounded_channel channel{2}; 46 | channel.push(1); 47 | channel.push(2); 48 | REQUIRE(channel.try_push(3) == amz::channel_op_status::full); 49 | 50 | // also make sure nothing was pushed 51 | int i = 999; 52 | channel.pop(i); 53 | REQUIRE(i == 1); 54 | channel.pop(i); 55 | REQUIRE(i == 2); 56 | 57 | REQUIRE(channel.try_pop(i) == amz::channel_op_status::empty); 58 | } 59 | -------------------------------------------------------------------------------- /test/bounded_channel/try_push_for.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | TEST_CASE("try_push_for() succeeds when the channel is non-full and open") { 25 | amz::bounded_channel channel{64}; 26 | REQUIRE(channel.try_push_for(std::chrono::seconds{0}, 1) == amz::channel_op_status::success); 27 | 28 | int i = 999; 29 | channel.pop(i); 30 | REQUIRE(i == 1); 31 | } 32 | 33 | TEST_CASE("try_push_for() returns `closed` when the channel is non-full and closed") { 34 | amz::bounded_channel channel{64}; 35 | channel.close(); 36 | REQUIRE(channel.try_push_for(std::chrono::seconds{0}, 1) == amz::channel_op_status::closed); 37 | } 38 | 39 | TEST_CASE("try_push_for() returns `closed` when the channel is full and closed") { 40 | amz::bounded_channel channel{3}; 41 | channel.push(1); 42 | channel.push(2); 43 | channel.push(3); 44 | channel.close(); 45 | REQUIRE(channel.try_push_for(std::chrono::seconds{0}, 4) == amz::channel_op_status::closed); 46 | } 47 | 48 | TEST_CASE("try_push_for() returns `timeout` when the channel stays full past the timeout") { 49 | amz::bounded_channel channel{3}; 50 | channel.push(1); 51 | channel.push(2); 52 | channel.push(3); 53 | 54 | REQUIRE(channel.try_push_for(std::chrono::milliseconds{1}, 4) == amz::channel_op_status::timeout); 55 | } 56 | 57 | TEST_CASE("try_push_for() succeeds when the channel becomes non-full within the timeout") { 58 | amz::bounded_channel channel{2}; 59 | channel.push(1); 60 | channel.push(2); 61 | REQUIRE(channel.try_push(888) == amz::channel_op_status::full); // the channel is now full with [1, 2] in it 62 | 63 | std::atomic started{false}; 64 | std::thread t{[&] { 65 | started = true; 66 | // Try pushing for so long that we will virtually never run into the 67 | // situation where the other thread does not have the time to pop() 68 | // before we time out, which would incorrectly fail this test. 69 | REQUIRE(channel.try_push_for(std::chrono::seconds{10}, 3) == amz::channel_op_status::success); 70 | }}; 71 | 72 | // Synchronize with the other thread to give it a chance to run. 73 | while (!started) std::this_thread::yield(); 74 | 75 | // Unblock the other thread by popping something off the channel. 76 | int i = 999; 77 | channel.pop(i); 78 | REQUIRE(i == 1); 79 | std::this_thread::yield(); 80 | 81 | channel.pop(i); 82 | REQUIRE(i == 2); 83 | 84 | channel.pop(i); 85 | REQUIRE(i == 3); 86 | 87 | t.join(); 88 | } 89 | -------------------------------------------------------------------------------- /test/bounded_channel/try_push_until.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | 21 | 22 | // 23 | // Note: Since `try_push_until()` and `try_push_for()` are basically implemented 24 | // the same, we don't bother testing this one too much. We roughly just 25 | // make sure it compiles. 26 | // 27 | 28 | TEST_CASE("try_push_until() succeeds when the channel is non-full and open") { 29 | amz::bounded_channel channel{64}; 30 | auto const future = std::chrono::steady_clock::now() + std::chrono::seconds{10}; 31 | REQUIRE(channel.try_push_until(future, 1) == amz::channel_op_status::success); 32 | 33 | int i = 999; 34 | REQUIRE(channel.pop(i) == amz::channel_op_status::success); 35 | REQUIRE(i == 1); 36 | } 37 | -------------------------------------------------------------------------------- /test/call.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | #include // required by Catch 19 | 20 | #define CATCH_CONFIG_MAIN 21 | #include 22 | 23 | #include 24 | #include 25 | 26 | 27 | TEST_CASE("at_most_every triggers not too often") { 28 | auto at_most_every_ms = amz::at_most_every{std::chrono::milliseconds{1}}; 29 | 30 | std::size_t calls = 0; 31 | auto start = std::chrono::steady_clock::now(); 32 | while (true) { 33 | amz::call(at_most_every_ms, [&]{ ++calls; }); 34 | 35 | // Iterate for roughly one second, and break out afterwards. 36 | if (std::chrono::steady_clock::now() > start + std::chrono::seconds{1}) 37 | break; 38 | } 39 | auto end = std::chrono::steady_clock::now(); 40 | 41 | auto elapsed_ms = std::chrono::duration_cast(end - start); 42 | 43 | // We can't have been called more than the number of milliseconds that elapsed 44 | // when we iterated, because we can't be called more often than once every ms. 45 | REQUIRE(calls <= elapsed_ms.count()); 46 | 47 | // We must have been called at least once (the first time around). 48 | REQUIRE(calls >= 1); 49 | 50 | // But realistically, we should have been called even more than that. 51 | // Given that a millisecond is pretty long compared to the expected time 52 | // it takes to go once around the loop, it's probably safe to assume that 53 | // we've been called during at least a third of the milliseconds that elapsed. 54 | REQUIRE(calls >= elapsed_ms.count() / 3); 55 | } 56 | 57 | TEST_CASE("at_most{n} triggers n times") { 58 | for (std::size_t times = 0; times != 10; ++times) { 59 | auto n_times = amz::at_most{times}; 60 | std::size_t calls = 0; 61 | 62 | for (std::size_t i = 0; i != 1000; ++i) { 63 | amz::call(n_times, [&]{ 64 | ++calls; 65 | }); 66 | } 67 | 68 | REQUIRE(calls == times); 69 | } 70 | } 71 | 72 | struct mock_flag { 73 | bool active() const { return active_; } 74 | bool active_; 75 | }; 76 | 77 | TEST_CASE("amz::call returns the result of the function when active") { 78 | mock_flag active{true}; 79 | 80 | SECTION("with a non-void return type") { 81 | boost::optional result = amz::call(active, [] { return 3; }); 82 | REQUIRE(result != boost::none); 83 | REQUIRE(*result == 3); 84 | } 85 | 86 | SECTION("with a void return type") { 87 | boost::optional result = amz::call(active, [] { }); 88 | REQUIRE(result != boost::none); 89 | } 90 | } 91 | 92 | TEST_CASE("amz::call returns an empty optional when inactive") { 93 | mock_flag inactive{false}; 94 | 95 | SECTION("with a non-void return type") { 96 | boost::optional result = amz::call(inactive, [] { return 3; }); 97 | REQUIRE(result == boost::none); 98 | } 99 | 100 | SECTION("with a void return type") { 101 | boost::optional result = amz::call(inactive, [] { }); 102 | REQUIRE(result == boost::none); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/bounded_allocator.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_TEST_BOUNDED_ALLOCATOR_HPP 15 | #define AMZ_TEST_BOUNDED_ALLOCATOR_HPP 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | namespace utils { 25 | 26 | // An allocator adapter that throws `bad_alloc` whenever the number of live 27 | // allocations reaches a certain limit. The number of live allocations is 28 | // shared by all copies of the allocator, and it is provided by the user of 29 | // this class so that it can be observed from the outside. 30 | template 31 | class bounded_allocator { 32 | Allocator allocator_; 33 | std::size_t const max_live_allocations_; 34 | std::size_t& live_allocations_; 35 | 36 | using AllocatorTraits = std::allocator_traits; 37 | 38 | template 39 | friend class bounded_allocator; 40 | 41 | public: 42 | bounded_allocator(Allocator allocator, std::size_t max_live_allocations, std::size_t& live_allocations) 43 | : allocator_{std::move(allocator)} 44 | , max_live_allocations_{max_live_allocations} 45 | , live_allocations_{live_allocations} 46 | { } 47 | 48 | explicit bounded_allocator(std::size_t max_live_allocations, std::size_t& live_allocations) 49 | : bounded_allocator{Allocator{}, max_live_allocations, live_allocations} 50 | { } 51 | 52 | template ::value 54 | >> 55 | bounded_allocator(bounded_allocator const& other) 56 | : allocator_{other.allocator_} 57 | , max_live_allocations_{other.max_live_allocations_} 58 | , live_allocations_{other.live_allocations_} 59 | { } 60 | 61 | using pointer = typename AllocatorTraits::pointer; 62 | using const_pointer = typename AllocatorTraits::const_pointer; 63 | using void_pointer = typename AllocatorTraits::void_pointer; 64 | using const_void_pointer = typename AllocatorTraits::const_void_pointer; 65 | using size_type = typename AllocatorTraits::size_type; 66 | using difference_type = typename AllocatorTraits::difference_type; 67 | using value_type = typename AllocatorTraits::value_type; 68 | 69 | template 70 | struct rebind { 71 | using other = bounded_allocator>; 72 | }; 73 | 74 | template 75 | void construct(pointer p, Args&& ...args) { 76 | allocator_.construct(p, std::forward(args)...); 77 | } 78 | 79 | void destroy(pointer p) { 80 | allocator_.destroy(p); 81 | } 82 | 83 | pointer allocate(size_type n) { 84 | if (live_allocations_ + 1 > max_live_allocations_) { 85 | throw std::bad_alloc{}; 86 | } else { 87 | auto p = allocator_.allocate(n); 88 | ++live_allocations_; 89 | return p; 90 | } 91 | } 92 | 93 | void deallocate(pointer p, size_type n) { 94 | allocator_.deallocate(p, n); 95 | --live_allocations_; 96 | } 97 | }; 98 | 99 | } // end namespace utils 100 | 101 | #endif // include guard 102 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/compare.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | 23 | // An allocator that refuses to free what's been allocated by anyone but itself. 24 | template 25 | struct SelfCompatibleAllocator : std::allocator { 26 | bool operator==(SelfCompatibleAllocator const& other) const 27 | { return &other == this; } 28 | bool operator!=(SelfCompatibleAllocator const& other) const 29 | { return !(*this == other); } 30 | }; 31 | 32 | // An allocator that is always equal to another allocator of the same type. 33 | template 34 | using AlwaysEqualAllocator = std::allocator; 35 | 36 | TEST_CASE("an allocator should be equal to itself") { 37 | using Allocator = amz::deferred_reclamation_allocator>; 38 | Allocator alloc{std::chrono::microseconds{10}}; 39 | REQUIRE(alloc == alloc); 40 | } 41 | 42 | TEST_CASE("making a copy of an allocator should yield an allocator that compares equal") { 43 | using Allocator = amz::deferred_reclamation_allocator>; 44 | Allocator alloc{std::chrono::microseconds{10}}; 45 | Allocator copy{alloc}; 46 | REQUIRE(alloc == copy); 47 | } 48 | 49 | TEST_CASE("two allocators with equal allocators and timeouts should compare equal") { 50 | using Allocator = amz::deferred_reclamation_allocator>; 51 | Allocator alloc1{std::chrono::microseconds{10}}; 52 | Allocator alloc2{std::chrono::microseconds{10}}; 53 | REQUIRE(alloc1 == alloc2); 54 | } 55 | 56 | TEST_CASE("allocators with different timeouts should not compare equal") { 57 | using Allocator = amz::deferred_reclamation_allocator>; 58 | Allocator alloc1{std::chrono::microseconds{10}}; 59 | Allocator alloc2{std::chrono::microseconds{11}}; 60 | 61 | REQUIRE(alloc1 != alloc2); 62 | } 63 | 64 | TEST_CASE("allocators with different allocators should not compare equal") { 65 | using Allocator = amz::deferred_reclamation_allocator>; 66 | Allocator alloc1{std::chrono::microseconds{10}}; 67 | Allocator alloc2{std::chrono::microseconds{10}}; 68 | 69 | REQUIRE(alloc1 != alloc2); 70 | } 71 | 72 | TEST_CASE("allocators with both different timeouts and allocators should not compare equal") { 73 | using Allocator = amz::deferred_reclamation_allocator>; 74 | Allocator alloc1{std::chrono::microseconds{10}}; 75 | Allocator alloc2{std::chrono::microseconds{11}}; 76 | 77 | REQUIRE(alloc1 != alloc2); 78 | } 79 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/ctor.copy.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | using ValueType = int; 25 | using UnderlyingAllocator = std::allocator; 26 | using Allocator = amz::deferred_reclamation_allocator; 27 | 28 | TEST_CASE("copies yield allocators that can deallocate what was allocated by a compatible allocator") { 29 | auto const with_buffer_size = [](std::size_t buffer_size, std::size_t overflow) { 30 | auto const timeout = std::chrono::microseconds{10}; 31 | Allocator alloc1{timeout}; 32 | Allocator alloc2{alloc1}; 33 | 34 | REQUIRE(alloc1 == alloc2); 35 | 36 | std::size_t const allocations = buffer_size * 10 + overflow; 37 | for (std::size_t i = 0; i != allocations; ++i) { 38 | ValueType* p = alloc1.allocate(1); 39 | alloc1.construct(p); 40 | 41 | alloc2.destroy(p); 42 | alloc2.deallocate(p, 1); 43 | } 44 | }; 45 | 46 | for (std::size_t overflow : {0, 1, 2}) { 47 | with_buffer_size(1, overflow); 48 | with_buffer_size(2, overflow); 49 | with_buffer_size(20, overflow); 50 | with_buffer_size(40, overflow); 51 | with_buffer_size(100, overflow); 52 | with_buffer_size(1000, overflow); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/ctor.move.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | 25 | using ValueType = int; 26 | using UnderlyingAllocator = std::allocator; 27 | using Allocator = amz::deferred_reclamation_allocator; 28 | 29 | TEST_CASE("move constructing passes the resources to the newly-constructed allocator") { 30 | auto const with_buffer_size = [](std::size_t buffer_size, std::size_t overflow) { 31 | std::vector pointers; 32 | 33 | auto const timeout = std::chrono::microseconds{10}; 34 | Allocator alloc1{timeout}; 35 | 36 | std::size_t const allocations = buffer_size * 10 + overflow; 37 | for (std::size_t i = 0; i != allocations; ++i) { 38 | ValueType* p = alloc1.allocate(1); 39 | alloc1.construct(p); 40 | pointers.push_back(p); 41 | } 42 | 43 | Allocator alloc2{std::move(alloc1)}; 44 | for (std::size_t i = 0; i != allocations; ++i) { 45 | alloc2.destroy(pointers[i]); 46 | alloc2.deallocate(pointers[i], 1); 47 | } 48 | }; 49 | 50 | for (std::size_t overflow : {0, 1, 2}) { 51 | with_buffer_size(1, overflow); 52 | with_buffer_size(2, overflow); 53 | with_buffer_size(20, overflow); 54 | with_buffer_size(40, overflow); 55 | with_buffer_size(100, overflow); 56 | with_buffer_size(1000, overflow); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/deallocate.bad_alloc.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #include "oom_allocator.hpp" 17 | 18 | #define CATCH_CONFIG_MAIN 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | 28 | struct OnDestruction { 29 | OnDestruction(std::function f) : callback(f) { } 30 | ~OnDestruction() { callback(); } 31 | std::function callback; 32 | }; 33 | 34 | using ValueType = OnDestruction; 35 | using OutOfMemoryAllocator = utils::oom_allocator>; 36 | using Allocator = amz::deferred_reclamation_allocator; 37 | 38 | TEST_CASE("objects are still freed when we're in low memory conditions") { 39 | auto const test = [](auto timeout, std::size_t delay_buffer_size, std::size_t overflow) { 40 | bool oom_flag = false; 41 | std::size_t const allocations = delay_buffer_size * 10 + overflow; 42 | std::vector pointers; pointers.resize(allocations, nullptr); 43 | std::vector was_destroyed; was_destroyed.resize(allocations, false); 44 | 45 | { 46 | Allocator allocator{OutOfMemoryAllocator{oom_flag}, timeout, delay_buffer_size}; 47 | 48 | // Allocate a bunch of stuff. 49 | for (std::size_t i = 0; i != allocations; ++i) { 50 | ValueType* p = allocator.allocate(1); 51 | allocator.construct(p, [&was_destroyed, i]{ was_destroyed[i] = true; }); 52 | pointers[i] = p; 53 | } 54 | 55 | // Deallocate half of it. This will add some entries to the delay list. 56 | std::size_t const first_half = allocations / 2; 57 | for (std::size_t i = 0; i != first_half; ++i) { 58 | allocator.destroy(pointers[i]); 59 | allocator.deallocate(pointers[i], 1); 60 | } 61 | 62 | // Put the underlying allocator in out-of-memory situation, and deallocate 63 | // the rest. 64 | std::size_t const second_half = allocations - first_half; 65 | oom_flag = true; 66 | for (std::size_t i = first_half; i != allocations; ++i) { 67 | allocator.destroy(pointers[i]); 68 | allocator.deallocate(pointers[i], 1); 69 | } 70 | 71 | // make sure a `bad_alloc` was thrown at least once, otherwise the unit test is moot 72 | REQUIRE(oom_flag == false); 73 | } 74 | 75 | for (bool destroyed : was_destroyed) { 76 | REQUIRE(destroyed); 77 | } 78 | }; 79 | 80 | for (std::size_t delay_buffer_size : {1, 2, 10, 100}) { 81 | for (std::size_t overflow : {0, 1, 2, 10}) { 82 | test(std::chrono::milliseconds{10}, delay_buffer_size, overflow); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/deallocate.delay.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | 26 | struct OnDestruction { 27 | OnDestruction(std::function f) : callback(f) { } 28 | ~OnDestruction() { callback(); } 29 | std::function callback; 30 | }; 31 | 32 | TEST_CASE("deallocated objects live at least for the duration of the timeout") { 33 | using ValueType = OnDestruction; 34 | using UnderlyingAllocator = std::allocator; 35 | using Allocator = amz::deferred_reclamation_allocator; 36 | using TimePoint = std::chrono::steady_clock::time_point; 37 | 38 | auto const test = [](auto timeout, std::size_t delay_buffer_size, std::size_t cycles) { 39 | // Time at which we call the allocator's `deallocate()` method for an object. 40 | std::map deallocation_times; 41 | 42 | // Actual time at which objects are reclaimed. 43 | std::map reclamation_times; 44 | 45 | { 46 | // Important: 47 | // The maps have to outlive the allocator, since the allocator's destructor 48 | // may call the object's destructors, which use the maps. 49 | Allocator allocator{timeout, delay_buffer_size}; 50 | 51 | // Allocate/deallocate a bunch of objects, and mark the time at which 52 | // deallocation is requested for each of them. We do this for a couple 53 | // of timeout cycles. 54 | { 55 | auto const start = std::chrono::steady_clock::now(); 56 | for (std::size_t i = 0; std::chrono::steady_clock::now() <= start + (timeout * cycles); ++i) { 57 | ValueType* p = allocator.allocate(1); 58 | allocator.construct(p, [i, &reclamation_times] { 59 | reclamation_times[i] = std::chrono::steady_clock::now(); 60 | }); 61 | allocator.destroy(p); 62 | deallocation_times[i] = std::chrono::steady_clock::now(); 63 | allocator.deallocate(p, 1); 64 | } 65 | } 66 | 67 | // Make sure that nothing that we requested to deallocate _and_ that was 68 | // actually reclaimed, was reclaimed before its timeout time had elapsed. 69 | for (auto const& reclamation : reclamation_times) { 70 | auto deallocation = deallocation_times.find(reclamation.first); 71 | REQUIRE(deallocation != deallocation_times.end()); 72 | 73 | auto const deallocation_time = deallocation->second; 74 | auto const reclamation_time = reclamation.second; 75 | REQUIRE(reclamation_time > deallocation_time + timeout); 76 | } 77 | } 78 | }; 79 | 80 | test(std::chrono::milliseconds{1}, 100, 10); 81 | test(std::chrono::milliseconds{10}, 100, 10); 82 | test(std::chrono::milliseconds{50}, 100, 10); 83 | } 84 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/dtor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | 31 | template 32 | struct OnDestruction { 33 | OnDestruction(T v, std::function f) 34 | : value(v), callback(f) 35 | { } 36 | 37 | ~OnDestruction() { callback(value); } 38 | 39 | T value; 40 | std::function callback; 41 | }; 42 | 43 | template 44 | static std::string random_string(RNG& rng, std::size_t max_length) { 45 | auto randchar = [&] { 46 | char const charset[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 47 | std::size_t const max_index = (sizeof(charset) - 1); 48 | return charset[rng() % max_index]; 49 | }; 50 | std::size_t const length = rng() % max_length; 51 | std::string str(length, 0); 52 | std::generate_n(str.begin(), length, randchar); 53 | return str; 54 | } 55 | 56 | using ValueType = OnDestruction; 57 | using UnderlyingAllocator = std::allocator; 58 | using Allocator = amz::deferred_reclamation_allocator; 59 | 60 | TEST_CASE("all allocated elements are destroyed when the allocator is destroyed") { 61 | std::mt19937 rng{}; 62 | auto const test = [&](auto timeout, std::size_t delay_buffer_size, std::size_t block_size) { 63 | // Generate a random set of unique strings with a fixed size. Those strings 64 | // will act as tokens for objects being destroyed. 65 | std::set strings; 66 | while (strings.size() != 100 * block_size) { 67 | strings.insert(random_string(rng, 6)); 68 | } 69 | 70 | // Allocate, construct, destroy, deallocate, and make sure the destructor 71 | // of the allocator actually cleans up everything. 72 | std::set destroyed; 73 | { 74 | Allocator allocator{timeout, delay_buffer_size}; 75 | for (auto string = strings.begin(); string != strings.end(); /* see inner loop */) { 76 | // Allocate a block of objects. 77 | ValueType* const block = allocator.allocate(block_size); 78 | 79 | // Construct and destroy each object in the block. 80 | for (ValueType* p = block; p != block + block_size; ++p) { 81 | REQUIRE(string != strings.end()); // otherwise, the test is broken 82 | allocator.construct(p, *string, [&](auto const& v){ destroyed.insert(v); }); 83 | allocator.destroy(p); 84 | ++string; 85 | } 86 | 87 | // Deallocate the block. 88 | allocator.deallocate(block, block_size); 89 | } 90 | } 91 | REQUIRE(destroyed == strings); 92 | }; 93 | 94 | for (std::size_t block_size = 1; block_size != 5; ++block_size) { 95 | test(std::chrono::microseconds{5}, 1, block_size); 96 | test(std::chrono::microseconds{5}, 2, block_size); 97 | test(std::chrono::microseconds{5}, 100, block_size); 98 | 99 | test(std::chrono::milliseconds{5}, 1, block_size); 100 | test(std::chrono::milliseconds{5}, 2, block_size); 101 | test(std::chrono::milliseconds{5}, 100, block_size); 102 | } 103 | } 104 | 105 | TEST_CASE("destroy an empty allocator") { 106 | Allocator allocator{std::chrono::microseconds{10}}; 107 | } 108 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/dtor.delay.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | 26 | struct OnDestruction { 27 | OnDestruction(std::function f) : callback(f) { } 28 | ~OnDestruction() { callback(); } 29 | std::function callback; 30 | }; 31 | 32 | TEST_CASE("deallocated objects live at least for the duration of the timeout") { 33 | using ValueType = OnDestruction; 34 | using UnderlyingAllocator = std::allocator; 35 | using Allocator = amz::deferred_reclamation_allocator; 36 | using TimePoint = std::chrono::steady_clock::time_point; 37 | 38 | auto const test = [](auto timeout, std::size_t delay_buffer_size) { 39 | // Time at which we call the allocator's `deallocate()` method for an object. 40 | std::map deallocation_times; 41 | 42 | // Actual time at which objects are reclaimed. 43 | std::map reclamation_times; 44 | 45 | { 46 | Allocator allocator{timeout, delay_buffer_size}; 47 | 48 | // Allocate/deallocate a bunch of objects until a small fraction of the 49 | // timeout time of the first objects created has elapsed, and then 50 | // destroy the allocator. This will trigger a purge, and we'll make 51 | // sure that nothing was deallocated too soon (i.e. the destructor 52 | // respects the timeout time). 53 | auto const start = std::chrono::steady_clock::now(); 54 | for (std::size_t i = 0; std::chrono::steady_clock::now() <= start + (timeout / 4); ++i) { 55 | ValueType* p = allocator.allocate(1); 56 | allocator.construct(p, [i, &reclamation_times] { 57 | reclamation_times[i] = std::chrono::steady_clock::now(); 58 | }); 59 | allocator.destroy(p); 60 | deallocation_times[i] = std::chrono::steady_clock::now(); 61 | allocator.deallocate(p, 1); 62 | } 63 | } 64 | 65 | // Make sure that nothing that we requested to deallocate was reclaimed 66 | // before its timeout time had elapsed. 67 | for (auto const& reclamation : reclamation_times) { 68 | auto deallocation = deallocation_times.find(reclamation.first); 69 | REQUIRE(deallocation != deallocation_times.end()); 70 | 71 | auto const deallocation_time = deallocation->second; 72 | auto const reclamation_time = reclamation.second; 73 | REQUIRE(reclamation_time > deallocation_time + timeout); 74 | } 75 | }; 76 | 77 | test(std::chrono::milliseconds{10}, 100); 78 | test(std::chrono::milliseconds{50}, 100); 79 | } 80 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/fancy_pointer.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | 32 | struct OnDestruction { 33 | OnDestruction(std::function f) : callback(f) { } 34 | ~OnDestruction() { callback(); } 35 | std::function callback; 36 | }; 37 | 38 | template 39 | void with_ipc_allocator(F f) { 40 | namespace fs = boost::filesystem; 41 | namespace ipc = boost::interprocess; 42 | fs::path filename = fs::temp_directory_path() / fs::unique_path(); 43 | constexpr std::size_t const FILE_SIZE = 100000000; // 100 MB 44 | using Allocator = ipc::allocator; 45 | ipc::file_mapping::remove(filename.c_str()); 46 | ipc::managed_mapped_file mmap(ipc::create_only, filename.c_str(), FILE_SIZE); 47 | Allocator allocator = mmap.get_allocator(); 48 | try { 49 | f(allocator); 50 | } catch (...) { 51 | ipc::file_mapping::remove(filename.c_str()); 52 | throw; 53 | } 54 | } 55 | 56 | TEST_CASE("basic utilization of the allocator with fancy pointers") { 57 | auto const test = [](auto timeout, std::size_t buffer_size) { 58 | with_ipc_allocator([=](auto ipc_allocator) { 59 | using Allocator = amz::deferred_reclamation_allocator; 60 | using Pointer = typename std::allocator_traits::pointer; 61 | 62 | std::size_t const allocations = 50000; 63 | std::set deleted; 64 | 65 | { 66 | Allocator allocator{ipc_allocator, timeout, buffer_size}; 67 | std::vector pointers; 68 | 69 | for (int i = 0; i != allocations; ++i) { 70 | Pointer p = allocator.allocate(1); 71 | allocator.construct(p, [&deleted, i] { deleted.insert(i); }); 72 | pointers.push_back(p); 73 | } 74 | 75 | for (Pointer p : pointers) { 76 | allocator.destroy(p); 77 | allocator.deallocate(p, 1); 78 | } 79 | } 80 | 81 | for (int i = 0; i != allocations; ++i) { 82 | REQUIRE(deleted.count(i)); 83 | } 84 | }); 85 | }; 86 | 87 | for (std::size_t buffer_size : {1, 2, 10, 100, 1000, 10000}) { 88 | test(std::chrono::microseconds{10}, buffer_size); 89 | } 90 | } 91 | 92 | TEST_CASE("opportunistic purge with fancy pointers") { 93 | auto const test = [](auto timeout, std::size_t buffer_size) { 94 | with_ipc_allocator([=](auto ipc_allocator) { 95 | using Allocator = amz::deferred_reclamation_allocator; 96 | using Pointer = typename std::allocator_traits::pointer; 97 | 98 | std::size_t const allocations = 50000; 99 | std::set deleted; 100 | 101 | { 102 | Allocator allocator{ipc_allocator, timeout, buffer_size}; 103 | std::vector pointers; 104 | 105 | for (int i = 0; i != allocations; ++i) { 106 | Pointer p = allocator.allocate(1); 107 | allocator.construct(p, [&deleted, i] { deleted.insert(i); }); 108 | pointers.push_back(p); 109 | } 110 | 111 | for (Pointer p : pointers) { 112 | allocator.destroy(p); 113 | allocator.deallocate(p, 1); 114 | } 115 | 116 | allocator.purge(amz::purge_mode::opportunistic); 117 | std::this_thread::sleep_for(timeout); 118 | allocator.purge(amz::purge_mode::opportunistic); 119 | } 120 | 121 | for (int i = 0; i != allocations; ++i) { 122 | REQUIRE(deleted.count(i)); 123 | } 124 | }); 125 | }; 126 | 127 | for (std::size_t buffer_size : {1, 2, 10, 100, 1000, 100000}) { 128 | test(std::chrono::milliseconds{10}, buffer_size); 129 | } 130 | } 131 | 132 | TEST_CASE("exhaustive purge with fancy pointers") { 133 | auto const test = [](auto timeout, std::size_t buffer_size) { 134 | with_ipc_allocator([=](auto ipc_allocator) { 135 | using Allocator = amz::deferred_reclamation_allocator; 136 | using Pointer = typename std::allocator_traits::pointer; 137 | 138 | std::size_t const allocations = 50000; 139 | std::set deleted; 140 | 141 | { 142 | Allocator allocator{ipc_allocator, timeout, buffer_size}; 143 | std::vector pointers; 144 | 145 | for (int i = 0; i != allocations; ++i) { 146 | Pointer p = allocator.allocate(1); 147 | allocator.construct(p, [&deleted, i] { deleted.insert(i); }); 148 | pointers.push_back(p); 149 | } 150 | 151 | for (Pointer p : pointers) { 152 | allocator.destroy(p); 153 | allocator.deallocate(p, 1); 154 | } 155 | 156 | allocator.purge(amz::purge_mode::exhaustive); 157 | } 158 | 159 | for (int i = 0; i != allocations; ++i) { 160 | REQUIRE(deleted.count(i)); 161 | } 162 | }); 163 | }; 164 | 165 | for (std::size_t buffer_size : {1, 2, 10, 100, 1000, 100000}) { 166 | test(std::chrono::milliseconds{10}, buffer_size); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/integration.oom_then_purge.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | #include "bounded_allocator.hpp" 16 | 17 | #define CATCH_CONFIG_MAIN 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | 28 | using ValueType = int; 29 | using UnderlyingAllocator = utils::bounded_allocator>; 30 | using Allocator = amz::deferred_reclamation_allocator; 31 | 32 | TEST_CASE("purging after bad_alloc allows recovering") { 33 | auto const test = [&](auto timeout, std::size_t delay_buffer_size) { 34 | std::size_t const max_live_allocations = 1000; 35 | std::size_t live_allocations = 0; 36 | UnderlyingAllocator bounded_alloc{max_live_allocations, live_allocations}; 37 | Allocator allocator{bounded_alloc, timeout, delay_buffer_size}; 38 | std::vector pointers; 39 | 40 | // Allocate a bunch of objects, and deallocate half of them. This makes 41 | // sure that we populate the delay list with some stuff. We do that until 42 | // a `bad_alloc` is thrown, and then we request the allocator to purge 43 | // itself. 44 | std::cout << "starting to allocate objects" << std::endl; 45 | try { 46 | while (true) { 47 | auto p1 = allocator.allocate(1); 48 | allocator.construct(p1, 0); 49 | REQUIRE_NOTHROW(pointers.push_back(p1)); 50 | 51 | auto p2 = allocator.allocate(1); 52 | allocator.construct(p2, 0); 53 | allocator.destroy(p2); 54 | allocator.deallocate(p2, 1); 55 | } 56 | } catch (std::bad_alloc const&) { 57 | // Make sure we throw if we try to allocate at this point, then purge. 58 | REQUIRE_THROWS_AS(allocator.allocate(1), std::bad_alloc); 59 | std::cout << "got bad_alloc with " << live_allocations 60 | << " live allocations, purging exhaustively" << std::endl; 61 | allocator.purge(amz::purge_mode::exhaustive); 62 | std::cout << live_allocations << " live allocations left after purging" << std::endl; 63 | } 64 | 65 | // Validate that we can indeed allocate after we've purged. 66 | REQUIRE_NOTHROW([&]{ 67 | auto p = allocator.allocate(1); 68 | allocator.construct(p, 0); 69 | allocator.destroy(p); 70 | allocator.deallocate(p, 1); 71 | }()); 72 | 73 | std::cout << "purging done, deallocating everything left" << std::endl; 74 | 75 | // Deallocate all remaining objects that were allocated just for the test. 76 | for (ValueType* p : pointers) { 77 | allocator.destroy(p); 78 | allocator.deallocate(p, 1); 79 | } 80 | }; 81 | 82 | for (std::size_t delay_buffer_size : {1, 2, 10, 100}) { 83 | test(std::chrono::nanoseconds{1}, delay_buffer_size); 84 | test(std::chrono::microseconds{10}, delay_buffer_size); 85 | test(std::chrono::milliseconds{10}, delay_buffer_size); 86 | test(std::chrono::milliseconds{100}, delay_buffer_size); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/interprocess/0_setup.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include "common.hpp" 15 | 16 | 17 | int main(int argc, char *argv[]) { 18 | std::cout << "Writing allocator to file " << filename.c_str() << std::endl; 19 | ipc::managed_mapped_file mmap(ipc::create_only, filename.c_str(), file_size); 20 | IpcAllocator ipc_allocator = mmap.get_allocator(); 21 | Allocator* allocator = mmap.construct("myalloc")(ipc_allocator, timeout, buffer_size); 22 | 23 | MY_ASSERT(allocator != nullptr); 24 | 25 | for (int i = 0; i != parent_allocations; ++i) { 26 | Pointer p = allocator->allocate(1); 27 | allocator->construct(p, i); 28 | allocator->destroy(p); 29 | allocator->deallocate(p, 1); 30 | } 31 | 32 | MY_ASSERT(mmap.flush()); 33 | } 34 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/interprocess/1_allocate.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include "common.hpp" 15 | 16 | 17 | int main(int argc, char *argv[]) { 18 | std::cout << "Reading allocator from file " << filename.c_str() << std::endl; 19 | ipc::managed_mapped_file mmap(ipc::open_only, filename.c_str()); 20 | Allocator* allocator = mmap.find("myalloc").first; 21 | MY_ASSERT(allocator != nullptr); 22 | 23 | std::vector pointers; 24 | for (int i = 0; i != child_allocations; ++i) { 25 | Pointer p = allocator->allocate(1); 26 | allocator->construct(p, i); 27 | pointers.push_back(p); 28 | } 29 | 30 | for (Pointer p : pointers) { 31 | allocator->destroy(p); 32 | allocator->deallocate(p, 1); 33 | } 34 | 35 | std::this_thread::sleep_for(timeout); 36 | allocator->purge(amz::purge_mode::opportunistic); 37 | } 38 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/interprocess/2_cleanup.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include "common.hpp" 15 | 16 | 17 | int main(int argc, char *argv[]) { 18 | std::cout << "Removing file " << filename.c_str() << std::endl; 19 | ipc::file_mapping::remove(filename.c_str()); 20 | } 21 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/interprocess/README.md: -------------------------------------------------------------------------------- 1 | This test makes sure that we can place a `deferred_reclamation_allocator` 2 | in a memory mapped file and load it from a different process. This allows 3 | data structures using the allocator to be stored in memory mapped files. 4 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/interprocess/common.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include // std::system, EXIT_FAILURE 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | namespace fs = boost::filesystem; 29 | namespace ipc = boost::interprocess; 30 | using namespace std::literals; 31 | 32 | 33 | #define MY_ASSERT(...) \ 34 | do { if (!(__VA_ARGS__)) throw "Assertion failed: " #__VA_ARGS__ ; } while (false) 35 | 36 | using ValueType = int; 37 | using IpcAllocator = ipc::allocator; 38 | using Allocator = amz::deferred_reclamation_allocator; 39 | using Pointer = std::allocator_traits::pointer; 40 | 41 | static constexpr auto timeout = std::chrono::milliseconds{10}; 42 | static constexpr std::size_t buffer_size = 10; 43 | static constexpr std::size_t parent_allocations = 10000; 44 | static constexpr std::size_t child_allocations = 10000; 45 | static constexpr std::size_t const file_size = 10000000; // 10 MB 46 | static fs::path filename = fs::current_path() / ".deferred_reclamation_allocator__interprocess_test"; 47 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/oom_allocator.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #ifndef AMZ_TEST_OOM_ALLOCATOR_HPP 15 | #define AMZ_TEST_OOM_ALLOCATOR_HPP 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | 23 | namespace utils { 24 | 25 | // An allocator adapter that throws `bad_alloc` whenever some boolean flag is 26 | // set, and unsets the flag when it's done. By having the user control when 27 | // that flag is set, it's possible to test allocators in artificial 28 | // out-of-memory conditions. After a `bad_alloc` has been thrown, the 29 | // flag is reset to `false` so that a user can observe that a `bad_alloc` 30 | // was indeed thrown. 31 | template 32 | class oom_allocator { 33 | Allocator allocator_; 34 | bool& oom_flag_; 35 | 36 | using AllocatorTraits = std::allocator_traits; 37 | 38 | template 39 | friend class oom_allocator; 40 | 41 | public: 42 | oom_allocator(Allocator allocator, bool& oom_flag) 43 | : allocator_{std::move(allocator)} 44 | , oom_flag_{oom_flag} 45 | { } 46 | 47 | explicit oom_allocator(bool& oom_flag) 48 | : oom_allocator{Allocator{}, oom_flag} 49 | { } 50 | 51 | template ::value 53 | >> 54 | oom_allocator(oom_allocator const& other) 55 | : allocator_{other.allocator_} 56 | , oom_flag_{other.oom_flag_} 57 | { } 58 | 59 | using pointer = typename AllocatorTraits::pointer; 60 | using const_pointer = typename AllocatorTraits::const_pointer; 61 | using void_pointer = typename AllocatorTraits::void_pointer; 62 | using const_void_pointer = typename AllocatorTraits::const_void_pointer; 63 | using size_type = typename AllocatorTraits::size_type; 64 | using difference_type = typename AllocatorTraits::difference_type; 65 | using value_type = typename AllocatorTraits::value_type; 66 | 67 | template 68 | struct rebind { 69 | using other = oom_allocator>; 70 | }; 71 | 72 | template 73 | void construct(pointer p, Args&& ...args) { 74 | allocator_.construct(p, std::forward(args)...); 75 | } 76 | 77 | void destroy(pointer p) { 78 | allocator_.destroy(p); 79 | } 80 | 81 | pointer allocate(size_type n) { 82 | if (oom_flag_) { 83 | oom_flag_ = false; 84 | throw std::bad_alloc{}; 85 | } else { 86 | return allocator_.allocate(n); 87 | } 88 | } 89 | 90 | void deallocate(pointer p, size_type n) { 91 | allocator_.deallocate(p, n); 92 | } 93 | }; 94 | 95 | } // end namespace utils 96 | 97 | #endif // include guard 98 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/purge.exhaustive.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | 28 | struct OnDestruction { 29 | OnDestruction(std::function f) : callback(f) { } 30 | ~OnDestruction() { callback(); } 31 | std::function callback; 32 | }; 33 | 34 | using ValueType = OnDestruction; 35 | using UnderlyingAllocator = std::allocator; 36 | using Allocator = amz::deferred_reclamation_allocator; 37 | 38 | TEST_CASE("an allocated object IS destroyed when purging if it made it to the delay list") { 39 | auto const timeout = std::chrono::milliseconds{100}; // make the timeout large enough to make sure we call `purge()` before entries are ripe 40 | std::size_t const delay_buffer_size = 1; // make sure the buffer gets flushed on the first deallocation 41 | 42 | bool was_destroyed = false; 43 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 44 | ValueType* p = allocator.allocate(1); 45 | allocator.construct(p, [&] { was_destroyed = true; }); 46 | allocator.destroy(p); 47 | allocator.deallocate(p, 1); 48 | REQUIRE(!was_destroyed); 49 | allocator.purge(amz::purge_mode::exhaustive); 50 | REQUIRE(was_destroyed); 51 | } 52 | 53 | TEST_CASE("an allocated object IS NOT destroyed when purging if it did not make it to the delay list") { 54 | auto const timeout = std::chrono::milliseconds{2}; 55 | std::size_t const delay_buffer_size = 2; // make sure the buffer does not get flushed on the first deallocation 56 | 57 | bool was_destroyed = false; 58 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 59 | ValueType* p = allocator.allocate(1); 60 | allocator.construct(p, [&] { was_destroyed = true; }); 61 | allocator.destroy(p); 62 | allocator.deallocate(p, 1); 63 | REQUIRE(!was_destroyed); 64 | allocator.purge(amz::purge_mode::exhaustive); 65 | REQUIRE(!was_destroyed); 66 | } 67 | 68 | TEST_CASE("deallocate after purging") { 69 | // The following tests are whitebox tests making sure that we properly 70 | // maintain the state of the delay list when we purge. 71 | 72 | auto const timeout = std::chrono::milliseconds{10}; 73 | std::size_t const delay_buffer_size = 1; 74 | 75 | SECTION("after purging nothing") { 76 | std::map was_destroyed; 77 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 78 | 79 | ValueType* p1 = allocator.allocate(1); 80 | ValueType* p2 = allocator.allocate(1); 81 | 82 | allocator.construct(p1, [&] { was_destroyed["p1"] = true; }); 83 | allocator.construct(p2, [&] { was_destroyed["p2"] = true; }); 84 | 85 | allocator.destroy(p1); 86 | allocator.destroy(p2); 87 | 88 | allocator.purge(amz::purge_mode::exhaustive); 89 | REQUIRE(was_destroyed.empty()); 90 | 91 | allocator.deallocate(p1, 1); 92 | allocator.deallocate(p2, 1); 93 | 94 | allocator.purge(amz::purge_mode::exhaustive); 95 | 96 | REQUIRE(was_destroyed.size() == 2); 97 | REQUIRE(was_destroyed["p1"]); 98 | REQUIRE(was_destroyed["p2"]); 99 | } 100 | 101 | SECTION("after purging something") { 102 | std::map was_destroyed; 103 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 104 | 105 | ValueType* p1 = allocator.allocate(1); 106 | ValueType* p2 = allocator.allocate(1); 107 | 108 | allocator.construct(p1, [&] { was_destroyed["p1"] = true; }); 109 | allocator.construct(p2, [&] { was_destroyed["p2"] = true; }); 110 | 111 | // Create something dummy so we have something to purge. 112 | { 113 | ValueType* dummy = allocator.allocate(1); 114 | allocator.construct(dummy, [&] { was_destroyed["dummy"] = true; }); 115 | allocator.destroy(dummy); 116 | allocator.deallocate(dummy, 1); 117 | } 118 | 119 | // Purge and make sure we purged something. 120 | allocator.purge(amz::purge_mode::exhaustive); 121 | REQUIRE(was_destroyed["dummy"]); 122 | 123 | // Deallocate what's left. 124 | allocator.deallocate(p1, 1); 125 | allocator.deallocate(p2, 1); 126 | 127 | // Purge again and make sure we've deallocated correctly. 128 | allocator.purge(amz::purge_mode::exhaustive); 129 | 130 | REQUIRE(was_destroyed.size() == 3); 131 | REQUIRE(was_destroyed["dummy"]); 132 | REQUIRE(was_destroyed["p1"]); 133 | REQUIRE(was_destroyed["p2"]); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/purge.noexcept.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | 20 | // This test makes sure that `purge()` is noexcept if and only if the 21 | // destructor of the value type is nothrow destructible. 22 | 23 | template 24 | using deferred_alloc = amz::deferred_reclamation_allocator< 25 | std::allocator 26 | >; 27 | 28 | struct ThrowDestructible1 { ~ThrowDestructible1() noexcept(false) { } }; 29 | struct ThrowDestructible2 { ThrowDestructible1 member; ~ThrowDestructible2() { } }; 30 | struct NoThrowDestructible1 { ~NoThrowDestructible1() noexcept { } }; 31 | struct NoThrowDestructible2 { ~NoThrowDestructible2() { } }; 32 | 33 | #define PURGE_NOEXCEPT_TEST(...) \ 34 | static_assert(!noexcept(std::declval&>().purge(__VA_ARGS__)), ""); \ 35 | static_assert(!noexcept(std::declval&>().purge(__VA_ARGS__)), ""); \ 36 | static_assert(noexcept(std::declval&>().purge(__VA_ARGS__)), ""); \ 37 | static_assert(noexcept(std::declval&>().purge(__VA_ARGS__)), ""); 38 | 39 | PURGE_NOEXCEPT_TEST() 40 | PURGE_NOEXCEPT_TEST(amz::purge_mode::opportunistic) 41 | PURGE_NOEXCEPT_TEST(amz::purge_mode::exhaustive) 42 | 43 | int main() { } 44 | -------------------------------------------------------------------------------- /test/deferred_reclamation_allocator/purge.opportunistic.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | 28 | struct OnDestruction { 29 | OnDestruction(std::function f) : callback(f) { } 30 | ~OnDestruction() { callback(); } 31 | std::function callback; 32 | }; 33 | 34 | using ValueType = OnDestruction; 35 | using UnderlyingAllocator = std::allocator; 36 | using Allocator = amz::deferred_reclamation_allocator; 37 | 38 | TEST_CASE("an allocated object IS destroyed when purging AFTER the timeout elapses, if it made it to the delay list") { 39 | auto const timeout = std::chrono::milliseconds{2}; 40 | std::size_t const delay_buffer_size = 1; // make sure the buffer gets flushed on the first deallocation 41 | 42 | bool was_destroyed = false; 43 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 44 | ValueType* p = allocator.allocate(1); 45 | allocator.construct(p, [&] { was_destroyed = true; }); 46 | allocator.destroy(p); 47 | allocator.deallocate(p, 1); 48 | REQUIRE(!was_destroyed); 49 | std::this_thread::sleep_for(timeout); 50 | allocator.purge(amz::purge_mode::opportunistic); 51 | REQUIRE(was_destroyed); 52 | } 53 | 54 | TEST_CASE("an allocated object IS NOT destroyed when purging even AFTER the timeout elapses, if it did not make it to the delay list") { 55 | auto const timeout = std::chrono::milliseconds{2}; 56 | std::size_t const delay_buffer_size = 2; // make sure the buffer does not get flushed on the first deallocation 57 | 58 | bool was_destroyed = false; 59 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 60 | ValueType* p = allocator.allocate(1); 61 | allocator.construct(p, [&] { was_destroyed = true; }); 62 | allocator.destroy(p); 63 | allocator.deallocate(p, 1); 64 | REQUIRE(!was_destroyed); 65 | std::this_thread::sleep_for(timeout); 66 | allocator.purge(amz::purge_mode::opportunistic); 67 | REQUIRE(!was_destroyed); 68 | } 69 | 70 | TEST_CASE("an allocated object IS NOT destroyed when purging BEFORE the timeout elapses") { 71 | bool was_destroyed = false; 72 | auto const timeout = std::chrono::milliseconds{200}; 73 | std::size_t const delay_buffer_size = 1; 74 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 75 | ValueType* p = allocator.allocate(1); 76 | allocator.construct(p, [&] { was_destroyed = true; }); 77 | allocator.destroy(p); 78 | allocator.deallocate(p, 1); 79 | REQUIRE(!was_destroyed); 80 | 81 | // Sleep just a bit; it is very unlikely that we'll sleep long enough for 82 | // the timeout to elapse. 83 | std::this_thread::sleep_for(std::chrono::milliseconds{10}); 84 | allocator.purge(amz::purge_mode::opportunistic); 85 | REQUIRE(!was_destroyed); 86 | } 87 | 88 | TEST_CASE("deallocate after purging") { 89 | // The following tests are whitebox tests making sure that we properly 90 | // maintain the state of the delay list when we purge. 91 | 92 | auto const timeout = std::chrono::milliseconds{10}; 93 | std::size_t const delay_buffer_size = 1; 94 | 95 | SECTION("after purging nothing") { 96 | std::map was_destroyed; 97 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 98 | 99 | ValueType* p1 = allocator.allocate(1); 100 | ValueType* p2 = allocator.allocate(1); 101 | 102 | allocator.construct(p1, [&] { was_destroyed["p1"] = true; }); 103 | allocator.construct(p2, [&] { was_destroyed["p2"] = true; }); 104 | 105 | allocator.destroy(p1); 106 | allocator.destroy(p2); 107 | 108 | allocator.purge(amz::purge_mode::opportunistic); 109 | REQUIRE(was_destroyed.empty()); 110 | 111 | allocator.deallocate(p1, 1); 112 | allocator.deallocate(p2, 1); 113 | 114 | std::this_thread::sleep_for(timeout); 115 | allocator.purge(amz::purge_mode::opportunistic); 116 | 117 | REQUIRE(was_destroyed.size() == 2); 118 | REQUIRE(was_destroyed["p1"]); 119 | REQUIRE(was_destroyed["p2"]); 120 | } 121 | 122 | SECTION("after purging something") { 123 | std::map was_destroyed; 124 | Allocator allocator{UnderlyingAllocator{}, timeout, delay_buffer_size}; 125 | 126 | ValueType* p1 = allocator.allocate(1); 127 | ValueType* p2 = allocator.allocate(1); 128 | 129 | allocator.construct(p1, [&] { was_destroyed["p1"] = true; }); 130 | allocator.construct(p2, [&] { was_destroyed["p2"] = true; }); 131 | 132 | // Create something dummy so we have something to purge. 133 | { 134 | ValueType* dummy = allocator.allocate(1); 135 | allocator.construct(dummy, [&] { was_destroyed["dummy"] = true; }); 136 | allocator.destroy(dummy); 137 | allocator.deallocate(dummy, 1); 138 | } 139 | 140 | // Purge and make sure we purged something. 141 | std::this_thread::sleep_for(timeout); 142 | allocator.purge(amz::purge_mode::opportunistic); 143 | REQUIRE(was_destroyed["dummy"]); 144 | 145 | // Deallocate what's left. 146 | allocator.deallocate(p1, 1); 147 | allocator.deallocate(p2, 1); 148 | 149 | // Purge again and make sure we've deallocated correctly. 150 | std::this_thread::sleep_for(timeout); 151 | allocator.purge(amz::purge_mode::opportunistic); 152 | 153 | REQUIRE(was_destroyed.size() == 3); 154 | REQUIRE(was_destroyed["dummy"]); 155 | REQUIRE(was_destroyed["p1"]); 156 | REQUIRE(was_destroyed["p2"]); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/small_spin_mutex.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | #include 15 | 16 | #define CATCH_CONFIG_MAIN 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | 32 | static_assert(sizeof(amz::small_spin_mutex) <= sizeof(char), 33 | "amz::small_spin_mutex is guaranteed to be no more than one byte in size"); 34 | 35 | static_assert(!std::is_copy_constructible::value, 36 | "amz::small_spin_mutex should not be CopyConstructible"); 37 | 38 | static_assert(!std::is_move_constructible::value, 39 | "amz::small_spin_mutex should not be MoveConstructible"); 40 | 41 | static_assert(!std::is_copy_assignable::value, 42 | "amz::small_spin_mutex should not be CopyAssignable"); 43 | 44 | static_assert(!std::is_move_assignable::value, 45 | "amz::small_spin_mutex should not be MoveAssignable"); 46 | 47 | static_assert(std::is_trivially_destructible::value, 48 | "amz::small_spin_mutex should always be TriviallyDestructible"); 49 | 50 | TEST_CASE("check noexcept properties") { 51 | amz::small_spin_mutex a{}; 52 | static_assert(noexcept(amz::small_spin_mutex{}), ""); 53 | static_assert(noexcept(a.lock()), ""); 54 | static_assert(noexcept(a.try_lock()), ""); 55 | static_assert(noexcept(a.unlock()), ""); 56 | } 57 | 58 | TEST_CASE("lock/unlock") { 59 | amz::small_spin_mutex a{}; 60 | amz::small_spin_mutex b{}; 61 | 62 | a.lock(); 63 | b.lock(); 64 | a.unlock(); 65 | b.unlock(); 66 | } 67 | 68 | TEST_CASE("try_lock/unlock") { 69 | amz::small_spin_mutex a{}; 70 | amz::small_spin_mutex b{}; 71 | 72 | REQUIRE(a.try_lock()); 73 | REQUIRE(!a.try_lock()); 74 | REQUIRE(!a.try_lock()); 75 | REQUIRE(!a.try_lock()); 76 | 77 | REQUIRE(b.try_lock()); 78 | REQUIRE(!b.try_lock()); 79 | REQUIRE(!b.try_lock()); 80 | REQUIRE(!b.try_lock()); 81 | 82 | a.unlock(); 83 | REQUIRE(a.try_lock()); 84 | a.unlock(); 85 | 86 | b.unlock(); 87 | REQUIRE(b.try_lock()); 88 | b.unlock(); 89 | } 90 | 91 | TEST_CASE("try_lock when already locked") { 92 | amz::small_spin_mutex a{}; 93 | amz::small_spin_mutex b{}; 94 | 95 | a.lock(); 96 | REQUIRE(!a.try_lock()); 97 | 98 | b.lock(); 99 | REQUIRE(!a.try_lock()); 100 | REQUIRE(!b.try_lock()); 101 | 102 | a.unlock(); 103 | b.unlock(); 104 | } 105 | 106 | template 107 | auto& pick_random(Range const& range, RandomNumberGenerator& rng) { 108 | auto first = std::begin(range); 109 | auto last = std::end(range); 110 | assert(first != last && "can't pick a random element if the range is empty"); 111 | auto const dist = std::distance(first, last); 112 | auto random = std::next(first, rng() % dist); 113 | assert(random != last && "can never happen because we picked within the range"); 114 | return *random; 115 | } 116 | 117 | TEST_CASE("multithreaded access") { 118 | // We create N threads that check for the validity of a variable, and update 119 | // it with a new valid value. Validity of the variable is established by 120 | // the variable being in some fixed set (see below). If the mutex was 121 | // somehow not making its job, the hope is that a thread might catch the 122 | // variable in some in-between state, which would hopefully be invalid. 123 | struct Record { 124 | std::string string; 125 | amz::small_spin_mutex mutex; 126 | }; 127 | 128 | std::set const valid_strings{ 129 | "foo", "bar", "baz", "dinosaur", "battery", "multithreaded", "access", 130 | "I", "hate", "deadlocks", "and", "I'll", "be", "incredibly", "careful", 131 | "when", "using", "this", "class", 132 | 133 | "long string that takes a while to copy and hence has more chances to " 134 | "catch a thread in the middle of a copy xxxxxxxxxxxxxxxxxxxxxxxxxxxxx " 135 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 136 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 137 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 138 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 139 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 140 | }; 141 | 142 | std::random_device rd; 143 | std::mt19937 g(rd()); 144 | Record record{pick_random(valid_strings, g)}; 145 | 146 | std::size_t const THREADS = 4; 147 | std::vector threads; 148 | std::generate_n(std::back_inserter(threads), THREADS, [&record, valid_strings] { 149 | return std::thread{[&record, valid_strings] { 150 | std::random_device rd; 151 | std::mt19937 g(rd()); 152 | 153 | for (int i = 0; i != 1000; ++i) { 154 | auto& s = pick_random(valid_strings, g); 155 | record.mutex.lock(); 156 | REQUIRE(valid_strings.count(record.string)); 157 | record.string = s; 158 | record.mutex.unlock(); 159 | } 160 | }}; 161 | }); 162 | 163 | for (auto& thread : threads) { 164 | thread.join(); 165 | } 166 | } 167 | 168 | TEST_CASE("small_spin_mutex is default constructed to an unlocked state") { 169 | // This test may look crazy, but we had a bug where the atomic_flag was not 170 | // properly initialized inside the mutex, and all hell broke loose. 171 | 172 | // Fill the memory with ones so that if the spin lock is not initialized 173 | // properly, it'll show. 174 | char* memory = static_cast(std::malloc(sizeof(amz::small_spin_mutex))); 175 | std::uninitialized_fill_n(memory, sizeof(amz::small_spin_mutex), 1); 176 | amz::small_spin_mutex* mutex = new (memory) amz::small_spin_mutex; // default construct, do not value-initialize 177 | REQUIRE(mutex->try_lock()); 178 | mutex->unlock(); 179 | 180 | mutex->~small_spin_mutex(); 181 | std::free(memory); 182 | } 183 | -------------------------------------------------------------------------------- /test/valgrind.supp: -------------------------------------------------------------------------------- 1 | { 2 | RHEL5_64 red herring 1 (can be removed when we don't need to run this on RHEL5) 3 | Memcheck:Cond 4 | fun:index 5 | fun:expand_dynamic_string_token 6 | fun:_dl_map_object 7 | fun:map_doit 8 | fun:_dl_catch_error 9 | fun:handle_ld_preload 10 | fun:dl_main 11 | fun:_dl_sysdep_start 12 | fun:_dl_start 13 | obj:/lib64/ld-2.12.so 14 | } 15 | { 16 | RHEL5_64 red herring 2 (can be removed when we don't need to run this on RHEL5) 17 | Memcheck:Cond 18 | fun:_dl_relocate_object 19 | fun:dl_main 20 | fun:_dl_sysdep_start 21 | fun:_dl_start 22 | obj:/lib64/ld-2.12.so 23 | } --------------------------------------------------------------------------------