diff --git a/CMakeLists.txt b/CMakeLists.txt index 57d0684..a68114f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,9 +18,12 @@ find_package(ament_cmake_export_dependencies REQUIRED) find_package(ament_cmake_export_targets REQUIRED) find_package(ament_cmake_test REQUIRED) find_package(benchmark REQUIRED) -find_package(osrf_testing_tools_cpp REQUIRED) +find_package(mimick_vendor REQUIRED) -add_library(${PROJECT_NAME} +# TODO(cottsay): It would be great if this didn't need to be STATIC, but +# libmimick is specifically compiled static and without PIC. +add_library(${PROJECT_NAME} STATIC + src/mimick_memory_manager.cpp src/${PROJECT_NAME}.cpp) target_include_directories(${PROJECT_NAME} PUBLIC @@ -29,12 +32,26 @@ target_include_directories(${PROJECT_NAME} PUBLIC target_link_libraries(${PROJECT_NAME} PUBLIC benchmark::benchmark - osrf_testing_tools_cpp::memory_tools) + mimick) target_compile_definitions(${PROJECT_NAME} PRIVATE "PERFORMANCE_TEST_FIXTURE_BUILDING_DLL") -install(TARGETS ${PROJECT_NAME} +add_library(memory_aware_benchmark_main + src/memory_aware_benchmark_main.cpp + src/memory_aware_console_reporter.cpp) + +target_include_directories(memory_aware_benchmark_main PRIVATE + $ + $) + +target_link_libraries(memory_aware_benchmark_main PUBLIC + benchmark::benchmark) + +target_compile_definitions(memory_aware_benchmark_main PRIVATE + "PERFORMANCE_TEST_FIXTURE_BUILDING_DLL") + +install(TARGETS ${PROJECT_NAME} memory_aware_benchmark_main EXPORT ${PROJECT_NAME} ARCHIVE DESTINATION lib LIBRARY DESTINATION lib @@ -50,7 +67,9 @@ install( ament_export_targets(${PROJECT_NAME}) ament_export_dependencies( - ament_cmake_google_benchmark benchmark osrf_testing_tools_cpp) + ament_cmake_google_benchmark + benchmark + mimick_vendor) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -58,29 +77,13 @@ if(BUILD_TESTING) set(performance_test_fixture_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake") add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + add_library(${PROJECT_NAME}::memory_aware_benchmark_main ALIAS memory_aware_benchmark_main) include(performance_test_fixture-extras.cmake) add_performance_test( benchmark_malloc_realloc test/benchmark/benchmark_malloc_realloc.cpp - TIMEOUT 90) - - # Bypassing performance_test_fixture to avoid LD_PRELOAD of - # osrf_testing_tools_cpp - set(_no_mem_tools_skip_arg) - if(TARGET benchmark_malloc_realloc) - get_target_property(ARG_SKIP_RETURN_CODE benchmark_malloc_realloc SKIP_RETURN_CODE) - if(ARG_SKIP_RETURN_CODE) - set(_no_mem_tools_skip_arg SKIP_TEST) - endif() - else() - set(_no_mem_tools_skip_arg SKIP_TEST) - endif() - ament_add_google_benchmark( - benchmark_malloc_realloc_no_memory_tools - test/benchmark/benchmark_malloc_realloc_no_memory_tools.cpp - ${_no_mem_tools_skip_arg} - TIMEOUT 60) + TIMEOUT 120) add_performance_test( benchmark_pause_resume diff --git a/cmake/add_performance_test.cmake b/cmake/add_performance_test.cmake index affa6da..642e92c 100644 --- a/cmake/add_performance_test.cmake +++ b/cmake/add_performance_test.cmake @@ -14,7 +14,7 @@ # # Add a google benchmark test which utilizes performance_test_fixture to -# leverage osrf_testing_tools_cpp. +# leverage Mimick for memory recording. # # Call add_executable(target ARGN), link it against the google benchmark # libraries and register the executable as a test. @@ -22,8 +22,6 @@ # If google benchmark is not available the specified target is not being created # and therefore the target existence should be checked before being used. # -# If osrf_testing_tools_cpp memory_tools is not available the test is skipped. -# # :param target: the target name which will also be used as the test name # :type target: string # :param ARGN: the list of source files @@ -65,27 +63,16 @@ macro(add_performance_test target) endif() # add executable - set(_argn_executable ${_ARG_UNPARSED_ARGUMENTS}) - if(_ARG_SKIP_LINKING_MAIN_LIBRARIES) - list(APPEND _argn_executable "SKIP_LINKING_MAIN_LIBRARIES") - endif() + set(_argn_executable ${_ARG_UNPARSED_ARGUMENTS} "SKIP_LINKING_MAIN_LIBRARIES") ament_add_google_benchmark_executable("${target}" ${_argn_executable}) if(TARGET ${target}) - _performance_test_fixture_find_memory_tools() - target_link_libraries(${target} - osrf_testing_tools_cpp::memory_tools performance_test_fixture::performance_test_fixture) - if(_PERFORMANCE_TEST_FIXTURE_MEMORY_TOOLS_AVAILABLE) - if(NOT _ARG_ENV) - set(_ARG_ENV "") - endif() - list(APPEND _ARG_ENV - ${_PERFORMANCE_TEST_FIXTURE_MEMORY_TOOLS_ENV}) - else() - set(_ARG_SKIP_TEST TRUE) + if(NOT _ARG_SKIP_LINKING_MAIN_LIBRARIES) + target_link_libraries(${target} + performance_test_fixture::memory_aware_benchmark_main) endif() endif() diff --git a/include/performance_test_fixture/mimick_memory_manager.hpp b/include/performance_test_fixture/mimick_memory_manager.hpp new file mode 100644 index 0000000..3d077a3 --- /dev/null +++ b/include/performance_test_fixture/mimick_memory_manager.hpp @@ -0,0 +1,80 @@ +// Copyright 2020 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef PERFORMANCE_TEST_FIXTURE__MIMICK_MEMORY_MANAGER_HPP_ +#define PERFORMANCE_TEST_FIXTURE__MIMICK_MEMORY_MANAGER_HPP_ + +#include + +#include +#include +#include +#include +#include + +// Defined by mimick.h +struct mmk_stub; + +// Defined by mimick_utils.hpp +template +struct mmk_allocator; + +namespace performance_test_fixture +{ + +class MimickMemoryManager : public benchmark::MemoryManager +{ +public: + MimickMemoryManager(); + + ~MimickMemoryManager(); + + virtual void Pause(); + + virtual void Reset(); + + virtual void Resume(); + + void Start() override; + + void Stop(benchmark::MemoryManager::Result * result) override; + +private: + void Stop() noexcept; + + void * on_calloc(size_t nitems, size_t size); + void on_free(void * ptr); + void * on_malloc(size_t size); + void * on_realloc(void * ptr, size_t size); + + std::mutex stat_lock; + bool recording_enabled; + + decltype(benchmark::MemoryManager::Result::max_bytes_used) cur_bytes_used; + decltype(benchmark::MemoryManager::Result::max_bytes_used) max_bytes_used; + decltype(benchmark::MemoryManager::Result::num_allocs) num_allocs; + + template + using mmk_unordered_set = std::unordered_set, std::equal_to, mmk_allocator>; + std::unique_ptr> ptr_set; + + struct mmk_stub * calloc_stub; + struct mmk_stub * free_stub; + struct mmk_stub * malloc_stub; + struct mmk_stub * realloc_stub; +}; + +} // namespace performance_test_fixture + +#endif // PERFORMANCE_TEST_FIXTURE__MIMICK_MEMORY_MANAGER_HPP_ diff --git a/include/performance_test_fixture/performance_test_fixture.hpp b/include/performance_test_fixture/performance_test_fixture.hpp index 415b1d1..d144b69 100644 --- a/include/performance_test_fixture/performance_test_fixture.hpp +++ b/include/performance_test_fixture/performance_test_fixture.hpp @@ -16,41 +16,29 @@ #define PERFORMANCE_TEST_FIXTURE__PERFORMANCE_TEST_FIXTURE_HPP_ #include -#include -#include +#include -#include "performance_test_fixture/visibility_control.hpp" +#include "performance_test_fixture/mimick_memory_manager.hpp" namespace performance_test_fixture { -class PerformanceTest : public ::benchmark::Fixture +class PerformanceTest : public benchmark::Fixture { public: - PERFORMANCE_TEST_FIXTURE_PUBLIC PerformanceTest(); - PERFORMANCE_TEST_FIXTURE_PUBLIC - void SetUp(::benchmark::State & state); + void SetUp(benchmark::State & state) override; - PERFORMANCE_TEST_FIXTURE_PUBLIC - void TearDown(::benchmark::State & state); + void TearDown(benchmark::State & state) override; protected: - PERFORMANCE_TEST_FIXTURE_PUBLIC - void on_alloc(osrf_testing_tools_cpp::memory_tools::MemoryToolsService & service); - - PERFORMANCE_TEST_FIXTURE_PUBLIC void reset_heap_counters(); - PERFORMANCE_TEST_FIXTURE_PUBLIC void set_are_allocation_measurements_active(bool value); -private: - std::atomic_size_t allocation_count; - bool suppress_memory_tools_logging; - bool are_allocation_measurements_active; + std::unique_ptr memory_manager; }; } // namespace performance_test_fixture diff --git a/package.xml b/package.xml index 5d9f265..efec6ee 100644 --- a/package.xml +++ b/package.xml @@ -4,7 +4,7 @@ performance_test_fixture 0.0.6 - Test fixture and CMake macro for using osrf_testing_tools_cpp with Google Benchmark + Test fixture and CMake macro for using Mimick with Google Benchmark Alejandro Hernandez Cordero Apache License 2.0 @@ -19,7 +19,7 @@ ament_cmake_google_benchmark google_benchmark_vendor - osrf_testing_tools_cpp + mimick_vendor ament_cmake_google_benchmark ament_lint_auto diff --git a/performance_test_fixture-extras.cmake b/performance_test_fixture-extras.cmake index 6f57e01..bd99326 100644 --- a/performance_test_fixture-extras.cmake +++ b/performance_test_fixture-extras.cmake @@ -12,30 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -macro(_performance_test_fixture_find_memory_tools) - if(NOT DEFINED _PERFORMANCE_TEST_FIXTURE_FIND_MEMORY_TOOLS) - set(_PERFORMANCE_TEST_FIXTURE_FIND_MEMORY_TOOLS TRUE) - find_package(osrf_testing_tools_cpp REQUIRED) - - get_target_property( - _PERFORMANCE_TEST_FIXTURE_MEMORY_TOOLS_AVAILABLE - osrf_testing_tools_cpp::memory_tools - LIBRARY_PRELOAD_ENVIRONMENT_IS_AVAILABLE) - - if(NOT _PERFORMANCE_TEST_FIXTURE_MEMORY_TOOLS_AVAILABLE) - if(AMENT_RUN_PERFORMANCE_TESTS) - message(WARNING - "'osrf_testing_tools_cpp' memory tools are not available, C++ tests " - "using 'performance_test_fixture' can not be run and will be " - "skipped.") - endif() - else() - get_target_property( - _PERFORMANCE_TEST_FIXTURE_MEMORY_TOOLS_ENV - osrf_testing_tools_cpp::memory_tools - LIBRARY_PRELOAD_ENVIRONMENT_VARIABLE) - endif() - endif() -endmacro() - include("${performance_test_fixture_DIR}/add_performance_test.cmake") diff --git a/src/memory_aware_benchmark_main.cpp b/src/memory_aware_benchmark_main.cpp new file mode 100644 index 0000000..347c164 --- /dev/null +++ b/src/memory_aware_benchmark_main.cpp @@ -0,0 +1,36 @@ +// Copyright 2020 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "performance_test_fixture/visibility_control.hpp" +#include "./memory_aware_console_reporter.hpp" + +constexpr benchmark::ConsoleReporter::OutputOptions output_options = + benchmark::ConsoleReporter::OO_None; + +PERFORMANCE_TEST_FIXTURE_PUBLIC +int main(int argc, char * argv[]) +{ + benchmark::Initialize(&argc, argv); + + if (benchmark::ReportUnrecognizedArguments(argc, argv)) { + return 1; + } + + performance_test_fixture::MemoryAwareConsoleReporter display_reporter(output_options); + benchmark::RunSpecifiedBenchmarks(&display_reporter); + + return 0; +} diff --git a/src/memory_aware_console_reporter.cpp b/src/memory_aware_console_reporter.cpp new file mode 100644 index 0000000..b1ed45a --- /dev/null +++ b/src/memory_aware_console_reporter.cpp @@ -0,0 +1,49 @@ +// Copyright 2020 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "./memory_aware_console_reporter.hpp" + +namespace performance_test_fixture +{ + +MemoryAwareConsoleReporter::MemoryAwareConsoleReporter( + benchmark::ConsoleReporter::OutputOptions opts_) +: benchmark::ConsoleReporter(opts_) +{ +} + +void MemoryAwareConsoleReporter::ReportRuns( + const std::vector & reports) +{ + std::vector augmented_reports(reports.begin(), reports.end()); + + for (std::vector::iterator it = augmented_reports.begin(); + it != augmented_reports.end(); + it++) + { + if (it->has_memory_result) { + *it = benchmark::BenchmarkReporter::Run(*it); + it->counters["allocs_per_iter"] = + benchmark::Counter(static_cast(it->allocs_per_iter)); + it->counters["max_bytes_used"] = + benchmark::Counter(static_cast(it->max_bytes_used)); + } + } + + benchmark::ConsoleReporter::ReportRuns(augmented_reports); +} + +} // namespace performance_test_fixture diff --git a/src/memory_aware_console_reporter.hpp b/src/memory_aware_console_reporter.hpp new file mode 100644 index 0000000..d3e122d --- /dev/null +++ b/src/memory_aware_console_reporter.hpp @@ -0,0 +1,35 @@ +// Copyright 2020 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MEMORY_AWARE_CONSOLE_REPORTER_HPP_ +#define MEMORY_AWARE_CONSOLE_REPORTER_HPP_ + +#include + +#include + +namespace performance_test_fixture +{ + +class MemoryAwareConsoleReporter : public benchmark::ConsoleReporter +{ +public: + explicit MemoryAwareConsoleReporter( + benchmark::ConsoleReporter::OutputOptions opts_ = benchmark::ConsoleReporter::OO_Defaults); + void ReportRuns(const std::vector & reports) override; +}; + +} // namespace performance_test_fixture + +#endif // MEMORY_AWARE_CONSOLE_REPORTER_HPP_ diff --git a/src/mimick_memory_manager.cpp b/src/mimick_memory_manager.cpp new file mode 100644 index 0000000..91a25a8 --- /dev/null +++ b/src/mimick_memory_manager.cpp @@ -0,0 +1,255 @@ +// Copyright 2020 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "performance_test_fixture/mimick_memory_manager.hpp" + +#include +#include +#include +#include +#include + +#include "mimick/mimick.h" +#include "./mimick_utils.hpp" + +namespace performance_test_fixture +{ + +MimickMemoryManager::MimickMemoryManager() +: recording_enabled(true), + cur_bytes_used(0), + max_bytes_used(0), + num_allocs(0), + ptr_set(new mmk_unordered_set()), + calloc_stub(MMK_STUB_INVALID), + free_stub(MMK_STUB_INVALID), + malloc_stub(MMK_STUB_INVALID), + realloc_stub(MMK_STUB_INVALID) +{ +} + +MimickMemoryManager::~MimickMemoryManager() +{ + Stop(); +} + +void MimickMemoryManager::Pause() +{ + stat_lock.lock(); + + recording_enabled = false; + + stat_lock.unlock(); +} + +void MimickMemoryManager::Reset() +{ + stat_lock.lock(); + + cur_bytes_used = 0; + max_bytes_used = 0; + num_allocs = 0; + ptr_set->clear(); + + stat_lock.unlock(); +} + +void MimickMemoryManager::Resume() +{ + stat_lock.lock(); + + recording_enabled = true; + + stat_lock.unlock(); +} + +void MimickMemoryManager::Start() +{ + stat_lock.lock(); + + Stop(); + + recording_enabled = true; + + cur_bytes_used = 0; + max_bytes_used = 0; + num_allocs = 0; + ptr_set->clear(); + + calloc_stub = mmk_stub_create_wrapped("calloc", on_calloc, this, size_t, size_t); + if (MMK_STUB_INVALID == calloc_stub) { + perror("Failed to create 'calloc' stub"); + } + + free_stub = mmk_stub_create_wrapped("free", on_free, this, void *); + if (MMK_STUB_INVALID == free_stub) { + perror("Failed to create 'free' stub"); + } + + malloc_stub = mmk_stub_create_wrapped("malloc", on_malloc, this, size_t); + if (MMK_STUB_INVALID == malloc_stub) { + perror("Failed to create 'malloc' stub"); + } + + realloc_stub = mmk_stub_create_wrapped("realloc", on_realloc, this, void *, size_t); + if (MMK_STUB_INVALID == realloc_stub) { + perror("Failed to create 'realloc' stub"); + } + + stat_lock.unlock(); +} + +void MimickMemoryManager::Stop(benchmark::MemoryManager::Result * result) +{ + stat_lock.lock(); + + Stop(); + + if (nullptr != result) { + result->max_bytes_used = max_bytes_used; + result->num_allocs = num_allocs; + } + + stat_lock.unlock(); +} + +void MimickMemoryManager::Stop() noexcept +{ + if (MMK_STUB_INVALID != calloc_stub) { + mmk_stub_destroy(calloc_stub); + calloc_stub = MMK_STUB_INVALID; + } + + if (MMK_STUB_INVALID != free_stub) { + mmk_stub_destroy(free_stub); + free_stub = MMK_STUB_INVALID; + } + + if (MMK_STUB_INVALID != malloc_stub) { + mmk_stub_destroy(malloc_stub); + malloc_stub = MMK_STUB_INVALID; + } + + if (MMK_STUB_INVALID != realloc_stub) { + mmk_stub_destroy(realloc_stub); + realloc_stub = MMK_STUB_INVALID; + } +} + +void * MimickMemoryManager::on_calloc(size_t nitems, size_t size) +{ + stat_lock.lock(); + void * new_ptr = mmk_malloc(sizeof(size_t) + (nitems * size)); + + if (nullptr != new_ptr) { + std::memset(new_ptr, 0x0, nitems * size); + } + + if (recording_enabled) { + num_allocs++; + if (nullptr != new_ptr) { + cur_bytes_used += nitems * size; + *static_cast(new_ptr) = nitems * size; + new_ptr = static_cast(new_ptr) + 1; + ptr_set->emplace(new_ptr); + if (cur_bytes_used > max_bytes_used) { + max_bytes_used = cur_bytes_used; + } + } + } + + stat_lock.unlock(); + + return new_ptr; +} + +void MimickMemoryManager::on_free(void * ptr) +{ + stat_lock.lock(); + + auto ph = ptr_set->find(ptr); + if (ph != ptr_set->end()) { + ptr = static_cast(ptr) - 1; + cur_bytes_used -= *static_cast(ptr); + ptr_set->erase(ph); + } + + mmk_free(ptr); + + stat_lock.unlock(); +} + +void * MimickMemoryManager::on_malloc(size_t size) +{ + stat_lock.lock(); + void * new_ptr = mmk_malloc(sizeof(size_t) + size); + + if (recording_enabled) { + num_allocs++; + if (nullptr != new_ptr) { + cur_bytes_used += size; + *static_cast(new_ptr) = size; + new_ptr = static_cast(new_ptr) + 1; + ptr_set->emplace(new_ptr); + if (cur_bytes_used > max_bytes_used) { + max_bytes_used = cur_bytes_used; + } + } + } + + stat_lock.unlock(); + + return new_ptr; +} + +void * MimickMemoryManager::on_realloc(void * ptr, size_t size) +{ + stat_lock.lock(); + + auto ph = ptr_set->find(ptr); + if (ph != ptr_set->end()) { + // Assume that the realloc will succeed + ptr = static_cast(ptr) - 1; + cur_bytes_used -= *static_cast(ptr); + ptr_set->erase(ph); + } + + void * new_ptr = mmk_realloc(ptr, size); + + if ((nullptr == new_ptr && 0 != size)) { + // Realloc failed - undo the removal + cur_bytes_used += *static_cast(ptr); + ptr = static_cast(ptr) + 1; + ptr_set->emplace(ptr); + } + + if (recording_enabled) { + num_allocs++; + if (nullptr != new_ptr) { + cur_bytes_used += size; + *static_cast(new_ptr) = size; + new_ptr = static_cast(new_ptr) + 1; + ptr_set->emplace(new_ptr); + if (cur_bytes_used > max_bytes_used) { + max_bytes_used = cur_bytes_used; + } + } + } + + stat_lock.unlock(); + + return new_ptr; +} + +} // namespace performance_test_fixture diff --git a/src/mimick_utils.hpp b/src/mimick_utils.hpp new file mode 100644 index 0000000..7b82ad3 --- /dev/null +++ b/src/mimick_utils.hpp @@ -0,0 +1,88 @@ +// Copyright 2020 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MIMICK_UTILS_HPP_ +#define MIMICK_UTILS_HPP_ + +#include +#include + +#include "mimick/mimick.h" + +template +typename std::result_of::type +mmk_wrapper(A... a) +{ + struct mmk_stub * stub = mmk_ctx(); + assert(nullptr != stub); + C * instance = static_cast(mmk_stub_context(stub)); + assert(nullptr != instance); + return (instance->*fn)(a ...); +} + +#define mmk_stub_create_wrapped(name, func, inst, ...) \ + mmk_stub_create( \ + name, \ + (mmk_fn) & mmk_wrapper< \ + decltype(&std::remove_reference::type::func), \ + &std::remove_reference::type::func, \ + std::remove_reference::type, \ + __VA_ARGS__>, \ + inst); + +template +struct mmk_allocator +{ + typedef T value_type; + + mmk_allocator() noexcept + { + // TODO(cottsay): This is just a hack to get Mimick to initialize the "vital" functions. + mmk_stub * stub = mmk_stub_create("free", (mmk_fn)dummy_free, nullptr); + if (MMK_STUB_INVALID != stub) { + // We don't actually care if this succeeds - calling the function will initialize Mimick + // regardless. + mmk_stub_destroy(stub); + } + } + + template + constexpr mmk_allocator(const mmk_allocator &) noexcept {} + + T * allocate(std::size_t n) + { + if (n > std::numeric_limits::max() / sizeof(T)) { + throw std::bad_alloc(); + } + + if (T * p = static_cast(mmk_malloc(n * sizeof(T)))) { + return p; + } + + throw std::bad_alloc(); + } + + void deallocate(T * p, std::size_t) noexcept + { + mmk_free(p); + } + +private: + static void dummy_free(void * unused) noexcept + { + (void)unused; + } +}; + +#endif // MIMICK_UTILS_HPP_ diff --git a/src/performance_test_fixture.cpp b/src/performance_test_fixture.cpp index bdee084..49221c7 100644 --- a/src/performance_test_fixture.cpp +++ b/src/performance_test_fixture.cpp @@ -14,98 +14,64 @@ #include "performance_test_fixture/performance_test_fixture.hpp" -#include -#include -#include - -#ifdef _WIN32 -#pragma warning(disable : 4996) -#endif - namespace performance_test_fixture { -PerformanceTest::PerformanceTest() -: suppress_memory_tools_logging(true), are_allocation_measurements_active(false) +/* + * This is a lightweight MemoryManager wrapper that unregisters itself when it + * is asked to stop recording. This is necessary so that when a benchmark + * executable contains multiple tests and not all of them use this fixture, + * the memory manager (which is stored in Google Benchmark as a global) isn't + * registered despite the absence of this fixture. + * + * Since that behavior is specific to our use of a fixture to take care of + * the MemoryManager registration, it isn't intrinsic to the MemoryManager + * concept in general, which is why this class is implemented here. + */ +template +class AutoStopMemoryManager : public U { - const char * performance_test_fixture_enable_trace = getenv( - "PERFORMANCE_TEST_FIXTURE_ENABLE_TRACE"); - if (nullptr != performance_test_fixture_enable_trace && - strcmp("1", performance_test_fixture_enable_trace) == 0) +public: + void Stop(benchmark::MemoryManager::Result * result) override { - suppress_memory_tools_logging = false; + U::Stop(result); + benchmark::RegisterMemoryManager(nullptr); } +}; - ComputeStatistics( - "max", - [](const std::vector & v) -> double { - return *(std::max_element(std::begin(v), std::end(v))); - }); - ComputeStatistics( - "min", - [](const std::vector & v) -> double { - return *(std::min_element(std::begin(v), std::end(v))); - }); -} - -void PerformanceTest::SetUp(benchmark::State &) +PerformanceTest::PerformanceTest() +: memory_manager(new AutoStopMemoryManager()) { - reset_heap_counters(); - - osrf_testing_tools_cpp::memory_tools::initialize(); - osrf_testing_tools_cpp::memory_tools::on_unexpected_calloc( - std::function( - std::bind(&PerformanceTest::on_alloc, this, std::placeholders::_1))); - osrf_testing_tools_cpp::memory_tools::on_unexpected_malloc( - std::function( - std::bind(&PerformanceTest::on_alloc, this, std::placeholders::_1))); - osrf_testing_tools_cpp::memory_tools::on_unexpected_realloc( - std::function( - std::bind(&PerformanceTest::on_alloc, this, std::placeholders::_1))); - osrf_testing_tools_cpp::memory_tools::enable_monitoring(); - osrf_testing_tools_cpp::memory_tools::expect_no_calloc_begin(); - osrf_testing_tools_cpp::memory_tools::expect_no_malloc_begin(); - osrf_testing_tools_cpp::memory_tools::expect_no_realloc_begin(); } -void PerformanceTest::TearDown(benchmark::State & state) +void PerformanceTest::SetUp(benchmark::State &) { - osrf_testing_tools_cpp::memory_tools::expect_no_calloc_end(); - osrf_testing_tools_cpp::memory_tools::expect_no_malloc_end(); - osrf_testing_tools_cpp::memory_tools::expect_no_realloc_end(); - - if (osrf_testing_tools_cpp::memory_tools::is_working()) { - state.counters["heap_allocations"] = benchmark::Counter( - static_cast(allocation_count), - benchmark::Counter::kAvgIterations); - } - - osrf_testing_tools_cpp::memory_tools::disable_monitoring(); - osrf_testing_tools_cpp::memory_tools::uninitialize(); + // Registering the MemoryManager doesn't mean that it will be used in the + // current run. Benchmark does the timing measurements first, so this will + // cause the MemoryManager to be registered during those runs, but it won't + // be actually started until the registration is checked after the timing + // runs are finished. + benchmark::RegisterMemoryManager(memory_manager.get()); + memory_manager->Reset(); } -void PerformanceTest::on_alloc( - osrf_testing_tools_cpp::memory_tools::MemoryToolsService & service -) +void PerformanceTest::TearDown(benchmark::State &) { - // Refraining from using an if-branch here in performance-critical code - allocation_count += static_cast(are_allocation_measurements_active); - - if (suppress_memory_tools_logging) { - service.ignore(); - } + memory_manager->Pause(); } void PerformanceTest::reset_heap_counters() { - allocation_count = 0; - are_allocation_measurements_active = true; + memory_manager->Reset(); } void PerformanceTest::set_are_allocation_measurements_active(bool value) { - are_allocation_measurements_active = value; + if (value) { + memory_manager->Resume(); + } else { + memory_manager->Pause(); + } } - } // namespace performance_test_fixture diff --git a/test/benchmark/benchmark_malloc_realloc.cpp b/test/benchmark/benchmark_malloc_realloc.cpp index f1a5d89..1b2373d 100644 --- a/test/benchmark/benchmark_malloc_realloc.cpp +++ b/test/benchmark/benchmark_malloc_realloc.cpp @@ -21,6 +21,7 @@ namespace { +constexpr int kForcePerformanceTracking = 2; constexpr int kEnablePerformanceTracking = 1; constexpr int kDisablePerformanceTracking = 0; @@ -31,15 +32,21 @@ class PerformanceTestFixture : public performance_test_fixture::PerformanceTest public: void SetUp(benchmark::State & state) { - if (kEnablePerformanceTracking != state.range(0) && - kDisablePerformanceTracking != state.range(0)) - { - std::string message = - std::string("Invalid enable/disable value: ") + std::to_string(state.range(0)); - state.SkipWithError(message.c_str()); - } - if (kEnablePerformanceTracking == state.range(0)) { - performance_test_fixture::PerformanceTest::SetUp(state); + switch (state.range(0)) { + case kDisablePerformanceTracking: + break; + case kEnablePerformanceTracking: + performance_test_fixture::PerformanceTest::SetUp(state); + break; + case kForcePerformanceTracking: + performance_test_fixture::PerformanceTest::SetUp(state); + memory_manager->Start(); + break; + default: + std::string message = + std::string("Invalid enable/disable value: ") + std::to_string(state.range(0)); + state.SkipWithError(message.c_str()); + break; } } @@ -47,6 +54,8 @@ class PerformanceTestFixture : public performance_test_fixture::PerformanceTest { if (kEnablePerformanceTracking == state.range(0)) { performance_test_fixture::PerformanceTest::TearDown(state); + } else if (kForcePerformanceTracking == state.range(0)) { + memory_manager->Pause(); } } }; @@ -90,11 +99,34 @@ BENCHMARK_DEFINE_F(PerformanceTestFixture, benchmark_on_calloc)( } } +BENCHMARK_DEFINE_F(PerformanceTestFixture, benchmark_on_malloc_many)( + benchmark::State & state) +{ + const size_t alloc_size = state.range(1); + if (alloc_size < 1) { + state.SkipWithError("Size for allocation is too small for this test"); + } + void * ptrs[4096]; + for (auto _ : state) { + for (size_t i = 0; i < 4096; i++) { + ptrs[i] = std::malloc(alloc_size); + if (PERFORMANCE_TEST_FIXTURE_UNLIKELY(nullptr == ptrs[i])) { + state.SkipWithError("Malloc failed to malloc"); + } + } + for (size_t i = 0; i < 4096; i++) { + std::free(ptrs[i]); + } + benchmark::DoNotOptimize(ptrs); + benchmark::ClobberMemory(); + } +} + BENCHMARK_DEFINE_F(PerformanceTestFixture, benchmark_on_realloc)( benchmark::State & state) { - const int64_t malloc_size = state.range(1); - if (malloc_size < 1) { + const int64_t alloc_size = state.range(1); + if (alloc_size < 1) { state.SkipWithError("Size for allocation is too small for this test"); } const size_t realloc_size = state.range(2); @@ -102,7 +134,7 @@ BENCHMARK_DEFINE_F(PerformanceTestFixture, benchmark_on_realloc)( state.SkipWithError("Size for reallocation is too small for this test"); } for (auto _ : state) { - void * ptr = std::malloc(malloc_size); + void * ptr = std::malloc(alloc_size); if (PERFORMANCE_TEST_FIXTURE_UNLIKELY(nullptr == ptr)) { state.SkipWithError("Malloc failed to malloc"); } @@ -124,6 +156,7 @@ static void alloc_args(benchmark::internal::Benchmark * b) for (int64_t shift_left = 0; shift_left < 32; shift_left += 4) { b->Args({kDisablePerformanceTracking, 1ll << shift_left}); b->Args({kEnablePerformanceTracking, 1ll << shift_left}); + b->Args({kForcePerformanceTracking, 1ll << shift_left}); } } @@ -133,6 +166,18 @@ BENCHMARK_REGISTER_F(PerformanceTestFixture, benchmark_on_malloc) BENCHMARK_REGISTER_F(PerformanceTestFixture, benchmark_on_calloc) ->ArgNames({"Enable Performance Tracking", "Alloc Size"})->Apply(alloc_args); +static void alloc_many_args(benchmark::internal::Benchmark * b) +{ + for (int64_t shift_left = 0; shift_left < 24; shift_left += 4) { + b->Args({kDisablePerformanceTracking, 1ll << shift_left}); + b->Args({kEnablePerformanceTracking, 1ll << shift_left}); + b->Args({kForcePerformanceTracking, 1ll << shift_left}); + } +} + +BENCHMARK_REGISTER_F(PerformanceTestFixture, benchmark_on_malloc_many) +->ArgNames({"Enable Performance Tracking", "Alloc Size"})->Apply(alloc_many_args); + // Three types of realloc tests, one where malloc is smaller than realloc, one where they are // the same, and one where malloc is larger than realloc. Realloc size ranges from 1 to 2^27 // each time multiplying by 32. Each stop is tested with/without performance metrics @@ -146,6 +191,7 @@ static void realloc_args(benchmark::internal::Benchmark * b) } b->Args({kDisablePerformanceTracking, 1ll << malloc_shift, 1ll << realloc_shift}); b->Args({kEnablePerformanceTracking, 1ll << malloc_shift, 1ll << realloc_shift}); + b->Args({kForcePerformanceTracking, 1ll << malloc_shift, 1ll << realloc_shift}); } } } diff --git a/test/benchmark/benchmark_malloc_realloc_no_memory_tools.cpp b/test/benchmark/benchmark_malloc_realloc_no_memory_tools.cpp deleted file mode 100644 index 0410446..0000000 --- a/test/benchmark/benchmark_malloc_realloc_no_memory_tools.cpp +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2020 Open Source Robotics Foundation, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include -#include - -#include "./macros.h" - -// This does not make use of PauseTiming or ResumeTiming because timing is very short for these -// benchmarks. However, they should allow for comparisons to the other benchmark_malloc_realloc -static void benchmark_on_malloc(benchmark::State & state) -{ - const size_t alloc_size = state.range(0); - if (alloc_size < 1) { - state.SkipWithError("Size for allocation is too small for this test"); - } - for (auto _ : state) { - void * ptr = std::malloc(alloc_size); - if (PERFORMANCE_TEST_FIXTURE_UNLIKELY(nullptr == ptr)) { - state.SkipWithError("Malloc failed to malloc"); - } - std::free(ptr); - benchmark::DoNotOptimize(ptr); - benchmark::ClobberMemory(); - } -} - -static void benchmark_on_calloc(benchmark::State & state) -{ - const size_t alloc_size = state.range(0); - if (alloc_size < 1) { - state.SkipWithError("Size for allocation is too small for this test"); - } - for (auto _ : state) { - void * ptr = std::calloc(1, alloc_size); - if (PERFORMANCE_TEST_FIXTURE_UNLIKELY(nullptr == ptr)) { - state.SkipWithError("Calloc failed to calloc"); - } - std::free(ptr); - benchmark::DoNotOptimize(ptr); - benchmark::ClobberMemory(); - } -} - -static void benchmark_on_realloc(benchmark::State & state) -{ - const int64_t malloc_size = state.range(0); - if (malloc_size < 1) { - state.SkipWithError("Size for allocation is too small for this test"); - } - const size_t realloc_size = state.range(1); - if (realloc_size < 1) { - state.SkipWithError("Size for reallocation is too small for this test"); - } - for (auto _ : state) { - void * ptr = std::malloc(malloc_size); - if (PERFORMANCE_TEST_FIXTURE_UNLIKELY(nullptr == ptr)) { - state.SkipWithError("Malloc failed to malloc"); - } - void * realloc_ptr = std::realloc(ptr, realloc_size); - if (PERFORMANCE_TEST_FIXTURE_UNLIKELY(nullptr == realloc_ptr)) { - std::free(ptr); - state.SkipWithError("Malloc failed to realloc"); - } - std::free(realloc_ptr); - benchmark::DoNotOptimize(realloc_ptr); - benchmark::ClobberMemory(); - } -} - -// Malloc sizes Range from 1 to 2^27 each time multiplying by 16 -BENCHMARK(benchmark_on_malloc)->ArgNames({"Alloc Size"})->RangeMultiplier(16)->Range(1, 1 << 28); - -BENCHMARK(benchmark_on_calloc)->ArgNames({"Alloc Size"})->RangeMultiplier(16)->Range(1, 1 << 28); - -// Three types of realloc tests, one where malloc is smaller than realloc, one where they are -// the same, and one where malloc is larger than realloc. Realloc size ranges from 1 to 2^27 -// each time multiplying by 32. -static void realloc_args(benchmark::internal::Benchmark * b) -{ - for (int64_t malloc_adjustment = -1; malloc_adjustment <= 1; ++malloc_adjustment) { - for (int64_t realloc_shift = 0; realloc_shift < 32; realloc_shift += 8) { - const int64_t malloc_shift = realloc_shift + malloc_adjustment; - if (malloc_shift < 0) { - continue; - } - b->Args({1ll << malloc_shift, 1ll << realloc_shift}); - } - } -} -BENCHMARK(benchmark_on_realloc)->ArgNames({"Alloc Size", "Realloc Size"})->Apply(realloc_args);