diff --git a/BUILD.gn b/BUILD.gn index 088dfd3d03..71cbd770ff 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -616,6 +616,9 @@ if (current_toolchain != default_toolchain) { "$dir_pw_rpc:test_rpc_server", "$dir_pw_unit_test:test_rpc_server", ] + + # Build the benchmarks, but don't run them by default. + deps += [ ":pw_benchmarks" ] } } @@ -663,6 +666,11 @@ if (current_toolchain != default_toolchain) { output_metadata = true } + # TODO(b/369853273): Merge benchmarks and perf tests + group("pw_benchmarks") { + deps = [ "$dir_pw_allocator/benchmarks" ] + } + # Fuzzers not based on a fuzzing engine. Engine-based fuzzers should be # included in `pw_module_tests`. pw_test_group("pw_custom_fuzzers") { diff --git a/pw_allocator/BUILD.gn b/pw_allocator/BUILD.gn index a9cfa3a594..2ad85293f9 100644 --- a/pw_allocator/BUILD.gn +++ b/pw_allocator/BUILD.gn @@ -653,7 +653,10 @@ pw_test_group("tests") { ":unique_ptr_test", ":worst_fit_block_allocator_test", ] - group_deps = [ "examples" ] + group_deps = [ + "benchmarks:tests", + "examples", + ] } # Docs diff --git a/pw_allocator/CMakeLists.txt b/pw_allocator/CMakeLists.txt index 3910590cf3..7f69452e6e 100644 --- a/pw_allocator/CMakeLists.txt +++ b/pw_allocator/CMakeLists.txt @@ -727,4 +727,5 @@ pw_add_test(pw_allocator.worst_fit_block_allocator_test pw_allocator ) +add_subdirectory(benchmarks) add_subdirectory(examples) diff --git a/pw_allocator/benchmarks/BUILD.bazel b/pw_allocator/benchmarks/BUILD.bazel new file mode 100644 index 0000000000..7a3e3f5bbc --- /dev/null +++ b/pw_allocator/benchmarks/BUILD.bazel @@ -0,0 +1,151 @@ +# Copyright 2020 The Pigweed Authors +# +# 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 +# +# https://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. + +load( + "//pw_build:pigweed.bzl", + "pw_cc_test", +) + +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +# Libraries + +cc_library( + name = "measurements", + testonly = True, + srcs = [ + "measurements.cc", + ], + hdrs = [ + "public/pw_allocator/benchmarks/measurements.h", + ], + includes = ["public"], + deps = [ + "//pw_chrono:system_clock", + "//pw_containers:intrusive_map", + "//pw_metric:metric", + ], +) + +cc_library( + name = "benchmark", + testonly = True, + srcs = [ + "benchmark.cc", + ], + hdrs = [ + "public/pw_allocator/benchmarks/benchmark.h", + "public/pw_allocator/benchmarks/config.h", + ], + includes = ["public"], + target_compatible_with = select({ + "@platforms//os:linux": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + ":measurements", + "//pw_allocator:block_allocator", + "//pw_allocator:fragmentation", + "//pw_allocator:test_harness", + "//pw_chrono:system_clock", + "//pw_metric:metric", + "//pw_tokenizer", + ], +) + +# Binaries + +cc_binary( + name = "best_fit_benchmark", + testonly = True, + srcs = [ + "best_fit_benchmark.cc", + ], + deps = [ + ":benchmark", + "//pw_allocator:best_fit_block_allocator", + "//pw_random", + ], +) + +cc_binary( + name = "dual_first_fit_benchmark", + testonly = True, + srcs = [ + "dual_first_fit_benchmark.cc", + ], + deps = [ + ":benchmark", + "//pw_allocator:dual_first_fit_block_allocator", + "//pw_random", + ], +) + +cc_binary( + name = "first_fit_benchmark", + testonly = True, + srcs = [ + "first_fit_benchmark.cc", + ], + deps = [ + ":benchmark", + "//pw_allocator:first_fit_block_allocator", + "//pw_random", + ], +) + +cc_binary( + name = "last_fit_benchmark", + testonly = True, + srcs = [ + "last_fit_benchmark.cc", + ], + deps = [ + ":benchmark", + "//pw_allocator:last_fit_block_allocator", + "//pw_random", + ], +) + +cc_binary( + name = "worst_fit_benchmark", + testonly = True, + srcs = [ + "worst_fit_benchmark.cc", + ], + deps = [ + ":benchmark", + "//pw_allocator:worst_fit_block_allocator", + "//pw_random", + ], +) + +# Unit tests + +pw_cc_test( + name = "measurements_test", + srcs = ["measurements_test.cc"], + deps = [":measurements"], +) + +pw_cc_test( + name = "benchmark_test", + srcs = ["benchmark_test.cc"], + deps = [ + ":benchmark", + "//pw_allocator:testing", + ], +) diff --git a/pw_allocator/benchmarks/BUILD.gn b/pw_allocator/benchmarks/BUILD.gn new file mode 100644 index 0000000000..7e806274b4 --- /dev/null +++ b/pw_allocator/benchmarks/BUILD.gn @@ -0,0 +1,136 @@ +# Copyright 2024 The Pigweed Authors +# +# 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 +# +# https://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. + +import("//build_overrides/pigweed.gni") + +import("$dir_pw_build/target_types.gni") +import("$dir_pw_chrono/backend.gni") +import("$dir_pw_unit_test/test.gni") + +group("benchmarks") { + deps = [ + ":best_fit_benchmark", + ":dual_first_fit_benchmark", + ":first_fit_benchmark", + ":last_fit_benchmark", + ":worst_fit_benchmark", + ] +} + +config("public_include_path") { + include_dirs = [ "public" ] + visibility = [ ":*" ] +} + +# Libraries + +pw_source_set("measurements") { + public_configs = [ ":public_include_path" ] + public = [ "public/pw_allocator/benchmarks/measurements.h" ] + public_deps = [ + "$dir_pw_chrono:system_clock", + "$dir_pw_containers:intrusive_map", + dir_pw_metric, + ] + sources = [ "measurements.cc" ] +} + +pw_source_set("benchmark") { + public_configs = [ ":public_include_path" ] + public = [ + "public/pw_allocator/benchmarks/benchmark.h", + "public/pw_allocator/benchmarks/config.h", + ] + public_deps = [ + ":measurements", + "$dir_pw_allocator:block_allocator", + "$dir_pw_allocator:fragmentation", + "$dir_pw_allocator:test_harness", + "$dir_pw_chrono:system_clock", + dir_pw_metric, + dir_pw_tokenizer, + ] + sources = [ "benchmark.cc" ] +} + +# Binaries + +pw_executable("best_fit_benchmark") { + sources = [ "best_fit_benchmark.cc" ] + deps = [ + ":benchmark", + "$dir_pw_allocator:best_fit_block_allocator", + "$dir_pw_random", + ] +} + +pw_executable("dual_first_fit_benchmark") { + sources = [ "dual_first_fit_benchmark.cc" ] + deps = [ + ":benchmark", + "$dir_pw_allocator:dual_first_fit_block_allocator", + "$dir_pw_random", + ] +} + +pw_executable("first_fit_benchmark") { + sources = [ "first_fit_benchmark.cc" ] + deps = [ + ":benchmark", + "$dir_pw_allocator:first_fit_block_allocator", + "$dir_pw_random", + ] +} + +pw_executable("last_fit_benchmark") { + sources = [ "last_fit_benchmark.cc" ] + deps = [ + ":benchmark", + "$dir_pw_allocator:last_fit_block_allocator", + "$dir_pw_random", + ] +} + +pw_executable("worst_fit_benchmark") { + sources = [ "worst_fit_benchmark.cc" ] + deps = [ + ":benchmark", + "$dir_pw_allocator:worst_fit_block_allocator", + "$dir_pw_random", + ] +} + +# Unit tests + +pw_test("measurements_test") { + enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != "" + deps = [ ":measurements" ] + sources = [ "measurements_test.cc" ] +} + +pw_test("benchmark_test") { + enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != "" + deps = [ + ":benchmark", + "$dir_pw_allocator:testing", + ] + sources = [ "benchmark_test.cc" ] +} + +pw_test_group("tests") { + tests = [ + ":benchmark_test", + ":measurements_test", + ] +} diff --git a/pw_allocator/benchmarks/CMakeLists.txt b/pw_allocator/benchmarks/CMakeLists.txt new file mode 100644 index 0000000000..355801c28c --- /dev/null +++ b/pw_allocator/benchmarks/CMakeLists.txt @@ -0,0 +1,71 @@ +# Copyright 2024 The Pigweed Authors +# +# 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 +# +# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake) + +# Libraries + +pw_add_library(pw_allocator.benchmarks.measurements STATIC + HEADERS + public/pw_allocator/benchmarks/measurements.h + PUBLIC_INCLUDES + public + PUBLIC_DEPS + pw_chrono.system_clock + pw_containers.intrusive_map + pw_metric + SOURCES + measurements.cc +) + +pw_add_library(pw_allocator.benchmarks.benchmark STATIC + HEADERS + public/pw_allocator/benchmarks/benchmark.h + public/pw_allocator/benchmarks/config.h + PUBLIC_INCLUDES + public + PUBLIC_DEPS + pw_allocator.benchmarks.measurements + pw_allocator.block_allocator + pw_allocator.fragmentation + pw_allocator.test_harness + pw_chrono.system_clock + pw_metric + pw_tokenizer + SOURCES + benchmark.cc +) + +# Unit tests + +pw_add_test(pw_allocator.benchmarks.measurements_test + SOURCES + measurements_test.cc + PRIVATE_DEPS + pw_allocator.benchmarks.measurements + GROUPS + modules + pw_allocator +) + +pw_add_test(pw_allocator.benchmarks.benchmark_test + SOURCES + benchmark_test.cc + PRIVATE_DEPS + pw_allocator.benchmarks.benchmark + pw_allocator.testing + GROUPS + modules + pw_allocator +) diff --git a/pw_allocator/benchmarks/benchmark.cc b/pw_allocator/benchmarks/benchmark.cc new file mode 100644 index 0000000000..b6cf52a2f3 --- /dev/null +++ b/pw_allocator/benchmarks/benchmark.cc @@ -0,0 +1,85 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/benchmark.h" + +#include "pw_allocator/fragmentation.h" +#include "pw_assert/check.h" + +namespace pw::allocator::internal { + +// GenericBlockAllocatorBenchmark methods + +void GenericBlockAllocatorBenchmark::BeforeAllocate(const Layout& layout) { + size_ = layout.size(); + DoBefore(); +} + +void GenericBlockAllocatorBenchmark::AfterAllocate(const void* ptr) { + DoAfter(); + if (ptr == nullptr) { + data_.failed = true; + } else { + ++num_allocations_; + } + Update(); +} + +void GenericBlockAllocatorBenchmark::BeforeDeallocate(const void* ptr) { + size_ = GetBlockInnerSize(ptr); + DoBefore(); +} + +void GenericBlockAllocatorBenchmark::AfterDeallocate() { + DoAfter(); + data_.failed = false; + PW_CHECK(num_allocations_ != 0); + --num_allocations_; + Update(); +} + +void GenericBlockAllocatorBenchmark::BeforeReallocate(const Layout& layout) { + size_ = layout.size(); + DoBefore(); +} + +void GenericBlockAllocatorBenchmark::AfterReallocate(const void* new_ptr) { + DoAfter(); + data_.failed = new_ptr == nullptr; + Update(); +} + +void GenericBlockAllocatorBenchmark::DoBefore() { + start_ = chrono::SystemClock::now(); +} + +void GenericBlockAllocatorBenchmark::DoAfter() { + auto finish = chrono::SystemClock::now(); + PW_ASSERT(start_.has_value()); + auto elapsed = finish - start_.value(); + data_.nanoseconds = + std::chrono::duration_cast(elapsed).count(); + + IterateOverBlocks(data_); + Fragmentation fragmentation = GetBlockFragmentation(); + data_.fragmentation = CalculateFragmentation(fragmentation); +} + +void GenericBlockAllocatorBenchmark::Update() { + measurements_.GetByCount(num_allocations_).Update(data_); + measurements_.GetByFragmentation(data_.fragmentation).Update(data_); + measurements_.GetBySize(size_).Update(data_); +} + +} // namespace pw::allocator::internal diff --git a/pw_allocator/benchmarks/benchmark_test.cc b/pw_allocator/benchmarks/benchmark_test.cc new file mode 100644 index 0000000000..18f09a1a18 --- /dev/null +++ b/pw_allocator/benchmarks/benchmark_test.cc @@ -0,0 +1,173 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/benchmark.h" + +#include "public/pw_allocator/benchmarks/measurements.h" +#include "pw_allocator/benchmarks/measurements.h" +#include "pw_allocator/fragmentation.h" +#include "pw_allocator/test_harness.h" +#include "pw_allocator/testing.h" +#include "pw_random/xor_shift.h" +#include "pw_unit_test/framework.h" + +namespace { + +constexpr size_t kCapacity = 65536; +constexpr size_t kMaxSize = 64; + +using AllocatorForTest = ::pw::allocator::test::AllocatorForTest; +using Benchmark = + ::pw::allocator::DefaultBlockAllocatorBenchmark; +using ::pw::allocator::CalculateFragmentation; +using ::pw::allocator::Measurement; +using ::pw::allocator::Measurements; +using ::pw::allocator::test::AllocationRequest; +using ::pw::allocator::test::kToken; +using ::pw::allocator::test::Request; + +template +bool IsChanged(Benchmark& benchmark, GetByKey get_by_key) { + return get_by_key(benchmark.measurements()).count() != 0; +} + +bool ByCountChanged(Benchmark& benchmark, size_t count) { + return IsChanged(benchmark, [count](Measurements& m) -> Measurement& { + return m.GetByCount(count); + }); +} + +TEST(BenchmarkTest, ByCount) { + AllocatorForTest allocator; + Benchmark benchmark(kToken, allocator); + benchmark.set_prng_seed(1); + benchmark.set_available(kCapacity); + + EXPECT_FALSE(ByCountChanged(benchmark, 0)); + + benchmark.GenerateRequest(kMaxSize); + EXPECT_TRUE(ByCountChanged(benchmark, 0)); + + while (benchmark.num_allocations() < 9) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_FALSE(ByCountChanged(benchmark, 10)); + + while (benchmark.num_allocations() < 10) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_TRUE(ByCountChanged(benchmark, 10)); + + while (benchmark.num_allocations() < 99) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_FALSE(ByCountChanged(benchmark, 100)); + + while (benchmark.num_allocations() < 100) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_TRUE(ByCountChanged(benchmark, 100)); + + while (benchmark.num_allocations() < 999) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_FALSE(ByCountChanged(benchmark, 1000)); + + while (benchmark.num_allocations() < 1000) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_TRUE(ByCountChanged(benchmark, 1000)); +} + +size_t ByFragmentationChanged(Benchmark& benchmark, float fragmentation) { + return IsChanged(benchmark, + [fragmentation](Measurements& m) -> Measurement& { + return m.GetByFragmentation(fragmentation); + }); +} + +TEST(BenchmarkTest, ByFragmentation) { + AllocatorForTest allocator; + Benchmark benchmark(kToken, allocator); + benchmark.set_prng_seed(1); + benchmark.set_available(kCapacity); + + EXPECT_FALSE(ByFragmentationChanged(benchmark, 0.2f)); + + while (CalculateFragmentation(allocator.MeasureFragmentation()) < 0.2f) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_TRUE(ByFragmentationChanged(benchmark, 0.2f)); + + while (CalculateFragmentation(allocator.MeasureFragmentation()) < 0.4f) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_TRUE(ByFragmentationChanged(benchmark, 0.4f)); + + while (CalculateFragmentation(allocator.MeasureFragmentation()) < 0.6f) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_TRUE(ByFragmentationChanged(benchmark, 0.6f)); + + while (CalculateFragmentation(allocator.MeasureFragmentation()) < 0.8f) { + benchmark.GenerateRequest(kMaxSize); + } + EXPECT_TRUE(ByFragmentationChanged(benchmark, 0.8f)); +} + +bool BySizeChanged(Benchmark& benchmark, size_t size) { + return IsChanged(benchmark, [size](Measurements& m) -> Measurement& { + return m.GetBySize(size); + }); +} + +TEST(BenchmarkTest, BySize) { + AllocatorForTest allocator; + Benchmark benchmark(kToken, allocator); + benchmark.set_prng_seed(1); + benchmark.set_available(kCapacity); + AllocationRequest request; + + EXPECT_FALSE(BySizeChanged(benchmark, 4096)); + request.size = 8192; + EXPECT_TRUE(benchmark.HandleRequest(request)); + EXPECT_TRUE(BySizeChanged(benchmark, 4096)); + EXPECT_FALSE(BySizeChanged(benchmark, 4095)); + + EXPECT_FALSE(BySizeChanged(benchmark, 1024)); + request.size = 4095; + EXPECT_TRUE(benchmark.HandleRequest(request)); + EXPECT_TRUE(BySizeChanged(benchmark, 1024)); + EXPECT_FALSE(BySizeChanged(benchmark, 1023)); + + EXPECT_FALSE(BySizeChanged(benchmark, 256)); + request.size = 256; + EXPECT_TRUE(benchmark.HandleRequest(request)); + EXPECT_TRUE(BySizeChanged(benchmark, 256)); + EXPECT_FALSE(BySizeChanged(benchmark, 255)); + + EXPECT_FALSE(BySizeChanged(benchmark, 64)); + request.size = 96; + EXPECT_TRUE(benchmark.HandleRequest(request)); + EXPECT_TRUE(BySizeChanged(benchmark, 64)); + EXPECT_FALSE(BySizeChanged(benchmark, 63)); + + EXPECT_FALSE(BySizeChanged(benchmark, 16)); + request.size = 63; + EXPECT_TRUE(benchmark.HandleRequest(request)); + EXPECT_TRUE(BySizeChanged(benchmark, 16)); + EXPECT_FALSE(BySizeChanged(benchmark, 15)); +} + +} // namespace diff --git a/pw_allocator/benchmarks/best_fit_benchmark.cc b/pw_allocator/benchmarks/best_fit_benchmark.cc new file mode 100644 index 0000000000..147ca77c94 --- /dev/null +++ b/pw_allocator/benchmarks/best_fit_benchmark.cc @@ -0,0 +1,45 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/benchmark.h" +#include "pw_allocator/benchmarks/config.h" +#include "pw_allocator/best_fit_block_allocator.h" +#include "pw_allocator/deallocator.h" + +namespace pw::allocator { + +constexpr metric::Token kBestFitBenchmark = + PW_TOKENIZE_STRING("best fit benchmark"); + +std::array buffer; + +void DoBestFitBenchmark() { + BestFitBlockAllocator allocator(buffer); + DefaultBlockAllocatorBenchmark benchmark(kBestFitBenchmark, allocator); + benchmark.set_prng_seed(1); + benchmark.set_available(benchmarks::kCapacity); + benchmark.GenerateRequests(benchmarks::kMaxSize, benchmarks::kNumRequests); + benchmark.metrics().Dump(); +} + +} // namespace pw::allocator + +int main() { + pw::allocator::DoBestFitBenchmark(); + return 0; +} diff --git a/pw_allocator/benchmarks/dual_first_fit_benchmark.cc b/pw_allocator/benchmarks/dual_first_fit_benchmark.cc new file mode 100644 index 0000000000..a914eda7ff --- /dev/null +++ b/pw_allocator/benchmarks/dual_first_fit_benchmark.cc @@ -0,0 +1,44 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/benchmark.h" +#include "pw_allocator/benchmarks/config.h" +#include "pw_allocator/dual_first_fit_block_allocator.h" + +namespace pw::allocator { + +constexpr metric::Token kDualFirstFitBenchmark = + PW_TOKENIZE_STRING("dual first fit benchmark"); + +std::array buffer; + +void DoDualFirstFitBenchmark() { + DualFirstFitBlockAllocator allocator(buffer, benchmarks::kMaxSize / 2); + DefaultBlockAllocatorBenchmark benchmark(kDualFirstFitBenchmark, allocator); + benchmark.set_prng_seed(1); + benchmark.set_available(benchmarks::kCapacity); + benchmark.GenerateRequests(benchmarks::kMaxSize, benchmarks::kNumRequests); + benchmark.metrics().Dump(); +} + +} // namespace pw::allocator + +int main() { + pw::allocator::DoDualFirstFitBenchmark(); + return 0; +} diff --git a/pw_allocator/benchmarks/first_fit_benchmark.cc b/pw_allocator/benchmarks/first_fit_benchmark.cc new file mode 100644 index 0000000000..00bdf11308 --- /dev/null +++ b/pw_allocator/benchmarks/first_fit_benchmark.cc @@ -0,0 +1,44 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/benchmark.h" +#include "pw_allocator/benchmarks/config.h" +#include "pw_allocator/first_fit_block_allocator.h" + +namespace pw::allocator { + +constexpr metric::Token kFirstFitBenchmark = + PW_TOKENIZE_STRING("first fit benchmark"); + +std::array buffer; + +void DoFirstFitBenchmark() { + FirstFitBlockAllocator allocator(buffer); + DefaultBlockAllocatorBenchmark benchmark(kFirstFitBenchmark, allocator); + benchmark.set_prng_seed(1); + benchmark.set_available(benchmarks::kCapacity); + benchmark.GenerateRequests(benchmarks::kMaxSize, benchmarks::kNumRequests); + benchmark.metrics().Dump(); +} + +} // namespace pw::allocator + +int main() { + pw::allocator::DoFirstFitBenchmark(); + return 0; +} diff --git a/pw_allocator/benchmarks/last_fit_benchmark.cc b/pw_allocator/benchmarks/last_fit_benchmark.cc new file mode 100644 index 0000000000..aa3116c8ab --- /dev/null +++ b/pw_allocator/benchmarks/last_fit_benchmark.cc @@ -0,0 +1,44 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/benchmark.h" +#include "pw_allocator/benchmarks/config.h" +#include "pw_allocator/last_fit_block_allocator.h" + +namespace pw::allocator { + +constexpr metric::Token kLastFitBenchmark = + PW_TOKENIZE_STRING("last fit benchmark"); + +std::array buffer; + +void DoLastFitBenchmark() { + LastFitBlockAllocator allocator(buffer); + DefaultBlockAllocatorBenchmark benchmark(kLastFitBenchmark, allocator); + benchmark.set_prng_seed(1); + benchmark.set_available(benchmarks::kCapacity); + benchmark.GenerateRequests(benchmarks::kMaxSize, benchmarks::kNumRequests); + benchmark.metrics().Dump(); +} + +} // namespace pw::allocator + +int main() { + pw::allocator::DoLastFitBenchmark(); + return 0; +} diff --git a/pw_allocator/benchmarks/measurements.cc b/pw_allocator/benchmarks/measurements.cc new file mode 100644 index 0000000000..6220ea33e5 --- /dev/null +++ b/pw_allocator/benchmarks/measurements.cc @@ -0,0 +1,122 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/measurements.h" + +namespace pw::allocator { +namespace internal { + +// GenericMeasurement methods + +GenericMeasurement::GenericMeasurement(metric::Token name) : metrics_(name) { + metrics_.Add(nanoseconds_); + metrics_.Add(fragmentation_); + metrics_.Add(largest_); + metrics_.Add(failures_); +} + +void GenericMeasurement::Update(const BenchmarkSample& data) { + count_++; + + float mean = nanoseconds_.value(); + mean += (data.nanoseconds - mean) / count_; + nanoseconds_.Set(mean); + + mean = fragmentation_.value(); + mean += (data.fragmentation - mean) / count_; + fragmentation_.Set(mean); + + mean = largest_.value(); + mean += (data.largest - mean) / count_; + largest_.Set(mean); + + if (data.failed) { + failures_.Increment(); + } +} + +} // namespace internal + +// Measurements methods + +Measurements::Measurements(metric::Token name) : metrics_(name) { + metrics_.Add(metrics_by_count_); + metrics_.Add(metrics_by_fragmentation_); + metrics_.Add(metrics_by_size_); +} + +void Measurements::AddByCount(Measurement& measurement) { + metrics_by_count_.Add(measurement.metrics()); + by_count_.insert(measurement); +} + +void Measurements::AddByFragmentation(Measurement& measurement) { + metrics_by_fragmentation_.Add(measurement.metrics()); + by_fragmentation_.insert(measurement); +} + +void Measurements::AddBySize(Measurement& measurement) { + metrics_by_size_.Add(measurement.metrics()); + by_size_.insert(measurement); +} + +void Measurements::Clear() { + by_count_.clear(); + by_fragmentation_.clear(); + by_size_.clear(); +} + +Measurement& Measurements::GetByCount(size_t count) { + PW_ASSERT(!by_count_.empty()); + auto iter = by_count_.upper_bound(count); + if (iter != by_count_.begin()) { + --iter; + } + return *iter; +} + +Measurement& Measurements::GetByFragmentation(float fragmentation) { + PW_ASSERT(!by_fragmentation_.empty()); + auto iter = by_fragmentation_.upper_bound(fragmentation); + if (iter != by_fragmentation_.begin()) { + --iter; + } + return *iter; +} + +Measurement& Measurements::GetBySize(size_t size) { + PW_ASSERT(!by_size_.empty()); + auto iter = by_size_.upper_bound(size); + if (iter != by_size_.begin()) { + --iter; + } + return *iter; +} + +// DefaultMeasurements methods + +DefaultMeasurements::DefaultMeasurements(metric::Token name) + : Measurements(name) { + for (auto& measurement : by_count_) { + AddByCount(measurement); + } + for (auto& measurement : by_fragmentation_) { + AddByFragmentation(measurement); + } + for (auto& measurement : by_size_) { + AddBySize(measurement); + } +} + +} // namespace pw::allocator diff --git a/pw_allocator/benchmarks/measurements_test.cc b/pw_allocator/benchmarks/measurements_test.cc new file mode 100644 index 0000000000..610615fbff --- /dev/null +++ b/pw_allocator/benchmarks/measurements_test.cc @@ -0,0 +1,180 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/measurements.h" + +#include "pw_metric/metric.h" +#include "pw_unit_test/framework.h" + +namespace { + +using pw::allocator::Measurement; +using pw::allocator::Measurements; +using pw::allocator::internal::BenchmarkSample; + +constexpr pw::metric::Token kName = PW_TOKENIZE_STRING("test"); + +TEST(MeasurementTest, Construct_Default) { + Measurement measurement(kName, 0); + + EXPECT_EQ(measurement.nanoseconds(), 0.f); + EXPECT_FLOAT_EQ(measurement.fragmentation(), 0.f); + EXPECT_FLOAT_EQ(measurement.largest(), 0.f); + EXPECT_EQ(measurement.failures(), 0u); +} + +TEST(MeasurementTest, Update_Once) { + BenchmarkSample data = { + .nanoseconds = 1000, + .fragmentation = 0.1f, + .largest = 4096, + .failed = false, + }; + + Measurement measurement(kName, 0); + measurement.Update(data); + + EXPECT_FLOAT_EQ(measurement.nanoseconds(), 1000.f); + EXPECT_FLOAT_EQ(measurement.fragmentation(), 0.1f); + EXPECT_FLOAT_EQ(measurement.largest(), 4096.f); + EXPECT_EQ(measurement.failures(), 0u); +} + +TEST(MeasurementTest, Update_TwiceSame) { + BenchmarkSample data = { + .nanoseconds = 1000, + .fragmentation = 0.1f, + .largest = 4096, + .failed = true, + }; + + Measurement measurement(kName, 0); + measurement.Update(data); + measurement.Update(data); + + EXPECT_FLOAT_EQ(measurement.nanoseconds(), 1000.f); + EXPECT_FLOAT_EQ(measurement.fragmentation(), 0.1f); + EXPECT_FLOAT_EQ(measurement.largest(), 4096.f); + EXPECT_EQ(measurement.failures(), 2u); +} + +TEST(MeasurementTest, Update_TwiceDifferent) { + BenchmarkSample data = { + .nanoseconds = 1000, + .fragmentation = 0.1f, + .largest = 4096, + .failed = true, + }; + + Measurement measurement(kName, 0); + measurement.Update(data); + + data.nanoseconds = 2000; + data.fragmentation = 0.04f; + data.largest = 2048; + data.failed = false; + measurement.Update(data); + + EXPECT_FLOAT_EQ(measurement.nanoseconds(), 1500.f); + EXPECT_FLOAT_EQ(measurement.fragmentation(), 0.07f); + EXPECT_FLOAT_EQ(measurement.largest(), 3072.f); + EXPECT_EQ(measurement.failures(), 1u); +} + +TEST(MeasurementTest, Update_ManyVarious) { + BenchmarkSample data; + data.largest = 8192; + Measurement measurement(kName, 0); + for (size_t i = 0; i < 10; ++i) { + data.nanoseconds += 100.f; + data.fragmentation += 0.02f; + data.largest -= 512; + data.failed = !data.failed; + measurement.Update(data); + } + + // sum([1..10]) is 55, for averages that are 5.5 times each increment. + EXPECT_FLOAT_EQ(measurement.nanoseconds(), 5.5f * 100); + EXPECT_FLOAT_EQ(measurement.fragmentation(), 5.5f * 0.02f); + EXPECT_FLOAT_EQ(measurement.largest(), 8192 - (5.5f * 512)); + EXPECT_EQ(measurement.failures(), 5u); +} + +class TestMeasurements : public Measurements { + public: + TestMeasurements() : Measurements(kName) {} + ~TestMeasurements() { Measurements::Clear(); } + using Measurements::AddByCount; + using Measurements::AddByFragmentation; + using Measurements::AddBySize; + using Measurements::GetByCount; + using Measurements::GetByFragmentation; + using Measurements::GetBySize; +}; + +TEST(MeasurementsTest, ByCount) { + Measurement at_least_0(kName, 0); + Measurement at_least_10(kName, 10); + Measurement at_least_100(kName, 100); + + TestMeasurements by_count; + by_count.AddByCount(at_least_0); + by_count.AddByCount(at_least_10); + by_count.AddByCount(at_least_100); + + EXPECT_EQ(&(by_count.GetByCount(0)), &at_least_0); + EXPECT_EQ(&(by_count.GetByCount(9)), &at_least_0); + EXPECT_EQ(&(by_count.GetByCount(10)), &at_least_10); + EXPECT_EQ(&(by_count.GetByCount(99)), &at_least_10); + EXPECT_EQ(&(by_count.GetByCount(100)), &at_least_100); + EXPECT_EQ(&(by_count.GetByCount(size_t(-1))), &at_least_100); +} + +TEST(MeasurementsTest, ByFragmentation) { + Measurement bottom_third(kName, 0.0f); + Measurement middle_third(kName, 0.33f); + Measurement top_third(kName, 0.66f); + + TestMeasurements by_fragmentation; + by_fragmentation.AddByFragmentation(bottom_third); + by_fragmentation.AddByFragmentation(middle_third); + by_fragmentation.AddByFragmentation(top_third); + + EXPECT_EQ(&(by_fragmentation.GetByFragmentation(0)), &bottom_third); + EXPECT_EQ(&(by_fragmentation.GetByFragmentation(0.3299)), &bottom_third); + EXPECT_EQ(&(by_fragmentation.GetByFragmentation(0.33f)), &middle_third); + EXPECT_EQ(&(by_fragmentation.GetByFragmentation(0.6599f)), &middle_third); + EXPECT_EQ(&(by_fragmentation.GetByFragmentation(0.66f)), &top_third); + EXPECT_EQ(&(by_fragmentation.GetByFragmentation(1.0f)), &top_third); +} + +TEST(MeasurementsTest, BySize) { + Measurement at_least_0(kName, 0); + Measurement at_least_16(kName, 0x10); + Measurement at_least_256(kName, 0x100); + + TestMeasurements by_size; + by_size.AddBySize(at_least_0); + by_size.AddBySize(at_least_16); + by_size.AddBySize(at_least_256); + + EXPECT_EQ(&(by_size.GetBySize(0)), &at_least_0); + EXPECT_EQ(&(by_size.GetBySize(0xf)), &at_least_0); + EXPECT_EQ(&(by_size.GetBySize(0x10)), &at_least_16); + EXPECT_EQ(&(by_size.GetBySize(0xff)), &at_least_16); + EXPECT_EQ(&(by_size.GetBySize(0x100)), &at_least_256); + EXPECT_EQ(&(by_size.GetBySize(size_t(-1))), &at_least_256); +} + +} // namespace diff --git a/pw_allocator/benchmarks/public/pw_allocator/benchmarks/benchmark.h b/pw_allocator/benchmarks/public/pw_allocator/benchmarks/benchmark.h new file mode 100644 index 0000000000..294fb8e9aa --- /dev/null +++ b/pw_allocator/benchmarks/public/pw_allocator/benchmarks/benchmark.h @@ -0,0 +1,168 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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. +#pragma once + +#include +#include + +#include "pw_allocator/benchmarks/measurements.h" +#include "pw_allocator/fragmentation.h" +#include "pw_allocator/layout.h" +#include "pw_allocator/test_harness.h" +#include "pw_chrono/system_clock.h" +#include "pw_metric/metric.h" +#include "pw_tokenizer/tokenize.h" + +namespace pw::allocator { +namespace internal { + +/// Base class for benchmarking block allocators. +/// +/// This class extends the test harness to sample data relevant to benchmarking +/// the performance of a blockallocator before and after each request. It is not +/// templated and avoids any types related to the specific block allocator. +/// +/// Callers should not use this class directly, and instead using +/// `BlockAllocatorBenchmark`. +class GenericBlockAllocatorBenchmark : public pw::allocator::test::TestHarness { + public: + metric::Group& metrics() { return measurements_.metrics(); } + Measurements& measurements() { return measurements_; } + + protected: + constexpr explicit GenericBlockAllocatorBenchmark(Measurements& measurements) + : measurements_(measurements) {} + + private: + /// @copydoc test::TestHarness::BeforeAllocate + void BeforeAllocate(const Layout& layout) override; + + /// @copydoc test::TestHarness::BeforeAllocate + void AfterAllocate(const void* ptr) override; + + /// @copydoc test::TestHarness::BeforeAllocate + void BeforeDeallocate(const void* ptr) override; + + /// @copydoc test::TestHarness::BeforeAllocate + void AfterDeallocate() override; + + /// @copydoc test::TestHarness::BeforeAllocate + void BeforeReallocate(const Layout& layout) override; + + /// @copydoc test::TestHarness::BeforeAllocate + void AfterReallocate(const void* new_ptr) override; + + /// Preparse to benchmark an allocator request. + void DoBefore(); + + /// Finishes benchmarking an allocator request. + void DoAfter(); + + /// Updates the `measurements` with data from an allocator request. + void Update(); + + /// Returns the inner size of block from is usable space. + virtual size_t GetBlockInnerSize(const void* ptr) const = 0; + + /// Iterates over an allocators blocks and collects benchmark data. + virtual void IterateOverBlocks(BenchmarkSample& data) const = 0; + + /// Measures the current fragmentation of an allocator. + virtual Fragmentation GetBlockFragmentation() const = 0; + + std::optional start_; + size_t num_allocations_ = 0; + size_t size_ = 0; + BenchmarkSample data_; + Measurements& measurements_; +}; + +} // namespace internal + +/// test harness used for benchmarking block allocators. +/// +/// This class records measurements aggregated from benchmarking samples of a +/// sequence of block allocator requests. The `Measurements` objects must +/// outlive the benchmark test harness. +/// +/// @tparam AllocatorType Type of the block allocator being benchmarked. +template +class BlockAllocatorBenchmark + : public internal::GenericBlockAllocatorBenchmark { + public: + BlockAllocatorBenchmark(Measurements& measurements, AllocatorType& allocator) + : internal::GenericBlockAllocatorBenchmark(measurements), + allocator_(allocator) {} + + private: + using BlockType = typename AllocatorType::BlockType; + + /// @copydoc test::TestHarness::Init + Allocator* Init() override { return &allocator_; } + + /// @copydoc GenericBlockAllocatorBenchmark::GetBlockInnerSize + size_t GetBlockInnerSize(const void* ptr) const override; + + /// @copydoc GenericBlockAllocatorBenchmark::IterateOverBlocks + void IterateOverBlocks(internal::BenchmarkSample& data) const override; + + /// @copydoc GenericBlockAllocatorBenchmark::GetBlockFragmentation + Fragmentation GetBlockFragmentation() const override; + + AllocatorType& allocator_; +}; + +/// Block allocator benchmark that use a default set of measurements +/// +/// This class simplifies the set up of a block allocator benchmark by defining +/// a default set of metrics and linking all the relevant metrics together. +template +class DefaultBlockAllocatorBenchmark + : public BlockAllocatorBenchmark { + public: + DefaultBlockAllocatorBenchmark(metric::Token name, AllocatorType& allocator) + : BlockAllocatorBenchmark(measurements_, allocator), + measurements_(name) {} + + private: + DefaultMeasurements measurements_; +}; + +// Template method implementations + +template +size_t BlockAllocatorBenchmark::GetBlockInnerSize( + const void* ptr) const { + const auto* block = BlockType::FromUsableSpace(ptr); + return block->InnerSize(); +} + +template +void BlockAllocatorBenchmark::IterateOverBlocks( + internal::BenchmarkSample& data) const { + data.largest = 0; + for (const auto* block : allocator_.blocks()) { + if (!block->Used()) { + data.largest = std::max(data.largest, block->InnerSize()); + } + } +} + +template +Fragmentation BlockAllocatorBenchmark::GetBlockFragmentation() + const { + return allocator_.MeasureFragmentation(); +} + +} // namespace pw::allocator diff --git a/pw_allocator/benchmarks/public/pw_allocator/benchmarks/config.h b/pw_allocator/benchmarks/public/pw_allocator/benchmarks/config.h new file mode 100644 index 0000000000..b98e6ffe2c --- /dev/null +++ b/pw_allocator/benchmarks/public/pw_allocator/benchmarks/config.h @@ -0,0 +1,24 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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. +#pragma once + +#include + +namespace pw::allocator::benchmarks { + +constexpr size_t kCapacity = 0x4000000; // 64 MiB +constexpr size_t kMaxSize = 0x2000; // 8 KiB +constexpr size_t kNumRequests = 40000; + +} // namespace pw::allocator::benchmarks diff --git a/pw_allocator/benchmarks/public/pw_allocator/benchmarks/measurements.h b/pw_allocator/benchmarks/public/pw_allocator/benchmarks/measurements.h new file mode 100644 index 0000000000..9baf6a6ed6 --- /dev/null +++ b/pw_allocator/benchmarks/public/pw_allocator/benchmarks/measurements.h @@ -0,0 +1,175 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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. +#pragma once + +#include +#include + +#include "pw_containers/intrusive_map.h" +#include "pw_metric/metric.h" + +namespace pw::allocator { +namespace internal { + +// Forward declaration for friending. +class GenericBlockAllocatorBenchmark; + +/// Collection data relating to an allocating request. +struct BenchmarkSample { + /// How many nanoseconds the request took. + uint64_t nanoseconds = 0; + + /// Current fragmentation reported by the block allocaor. + float fragmentation = 0.f; + + /// Current single largest allocation that could succeed. + size_t largest = 0; + + /// Result of the last allocator request. + bool failed = false; +}; + +/// Base class for an accumulation of samples into a single measurement. +/// +/// As samples are collected, they are aggregated into a set of bins described +/// a specific domain and a range in that domain, e.g. the set of all +/// samples for requests of at least 512 bytes but less than 1024. +/// +/// This class describes the common behavior of those bins without referencing +/// specific domain. Callers should not use this class directly, and use +/// `Measurement` instead.measurement +class GenericMeasurement { + public: + GenericMeasurement(metric::Token name); + + metric::Group& metrics() { return metrics_; } + size_t count() const { return count_; } + + float nanoseconds() const { return nanoseconds_.value(); } + float fragmentation() const { return fragmentation_.value(); } + float largest() const { return largest_.value(); } + uint32_t failures() const { return failures_.value(); } + + void Update(const BenchmarkSample& data); + + private: + metric::Group metrics_; + + PW_METRIC(nanoseconds_, "mean response time (ns)", 0.f); + PW_METRIC(fragmentation_, "mean fragmentation metric", 0.f); + PW_METRIC(largest_, "mean max available (bytes)", 0.f); + PW_METRIC(failures_, "number of calls that failed", 0u); + + size_t count_ = 0; +}; + +} // namespace internal + +/// An accumulation of samples into a single measurement. +/// +/// This class extends `GenericMeasurement` with a key that describes what +/// domain is being used to partition samples. It is intrusively mappable using +/// that key, allowing other objects such as `Measurements` to maintain sorted +/// containers of this type. +template +class Measurement : public internal::GenericMeasurement, + public IntrusiveMap>::Item { + public: + Measurement(metric::Token name, Key lower_limit) + : internal::GenericMeasurement(name), lower_limit_(lower_limit) {} + const Key& key() const { return lower_limit_; } + + private: + Key lower_limit_; +}; + +/// A collection of sorted containers of `Measurement`s. +/// +/// This collection includes sorting `Measurement`s by +/// * The number of allocator requests that have been performed. +/// * The level of fragmentation as measured by the block allocator. +/// * The size of the most recent allocator request. +class Measurements { + public: + explicit Measurements(metric::Token name); + + metric::Group& metrics() { return metrics_; } + Measurement& GetByCount(size_t count); + Measurement& GetByFragmentation(float fragmentation); + Measurement& GetBySize(size_t size); + + protected: + void AddByCount(Measurement& measurement); + void AddByFragmentation(Measurement& measurement); + void AddBySize(Measurement& measurement); + + /// Removes measurements from the sorted + void Clear(); + + private: + // Allow the benchmark harness to retrieve measurements. + friend class internal::GenericBlockAllocatorBenchmark; + + metric::Group metrics_; + + PW_METRIC_GROUP(metrics_by_count_, "by allocation count"); + IntrusiveMap> by_count_; + + PW_METRIC_GROUP(metrics_by_fragmentation_, "by fragmentation"); + IntrusiveMap> by_fragmentation_; + + PW_METRIC_GROUP(metrics_by_size_, "by allocation size"); + IntrusiveMap> by_size_; +}; + +/// A default set of measurements for benchmarking allocators. +/// +/// This organizes measurements in to logarithmically increasing ranges of +/// alloations counts and sizes, as well as fragmentation quintiles. +class DefaultMeasurements final : public Measurements { + public: + DefaultMeasurements(metric::Token name); + ~DefaultMeasurements() { Measurements::Clear(); } + + private: + static constexpr size_t kNumByCount = 5; + std::array, kNumByCount> by_count_{{ + {PW_TOKENIZE_STRING_EXPR("allocation count in [0, 10)"), 0}, + {PW_TOKENIZE_STRING_EXPR("allocation count in [10, 100)"), 10}, + {PW_TOKENIZE_STRING_EXPR("allocation count in [100, 1,000)"), 100}, + {PW_TOKENIZE_STRING_EXPR("allocation count in [1,000, 10,000)"), 1000}, + {PW_TOKENIZE_STRING_EXPR("allocation count in [10,000, inf)"), 10000}, + }}; + + static constexpr size_t kNumByFragmentation = 5; + std::array, kNumByFragmentation> by_fragmentation_ = {{ + {PW_TOKENIZE_STRING_EXPR("fragmentation in [0.0, 0.2)"), 0.0f}, + {PW_TOKENIZE_STRING_EXPR("fragmentation in [0.2, 0.4)"), 0.2f}, + {PW_TOKENIZE_STRING_EXPR("fragmentation in [0.4, 0.6)"), 0.4f}, + {PW_TOKENIZE_STRING_EXPR("fragmentation in [0.6, 0.8)"), 0.6f}, + {PW_TOKENIZE_STRING_EXPR("fragmentation in [0.8, 1.0]"), 0.8f}, + }}; + + static constexpr size_t kNumBySize = 6; + std::array, kNumBySize> by_size_ = {{ + {PW_TOKENIZE_STRING_EXPR("usable size in [0, 16)"), 0}, + {PW_TOKENIZE_STRING_EXPR("usable size in [16, 64)"), 16}, + {PW_TOKENIZE_STRING_EXPR("usable size in [64, 256)"), 64}, + {PW_TOKENIZE_STRING_EXPR("usable size in [256, 1024)"), 256}, + {PW_TOKENIZE_STRING_EXPR("usable size in [1024, 4096)"), 1024}, + {PW_TOKENIZE_STRING_EXPR("usable size in [4096, inf)"), 4096}, + }}; +}; + +} // namespace pw::allocator diff --git a/pw_allocator/benchmarks/worst_fit_benchmark.cc b/pw_allocator/benchmarks/worst_fit_benchmark.cc new file mode 100644 index 0000000000..cd6321c452 --- /dev/null +++ b/pw_allocator/benchmarks/worst_fit_benchmark.cc @@ -0,0 +1,44 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +// +// https://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 "pw_allocator/benchmarks/benchmark.h" +#include "pw_allocator/benchmarks/config.h" +#include "pw_allocator/worst_fit_block_allocator.h" + +namespace pw::allocator { + +constexpr metric::Token kWorstFitBenchmark = + PW_TOKENIZE_STRING("worst fit benchmark"); + +std::array buffer; + +void DoWorstFitBenchmark() { + WorstFitBlockAllocator allocator(buffer); + DefaultBlockAllocatorBenchmark benchmark(kWorstFitBenchmark, allocator); + benchmark.set_prng_seed(1); + benchmark.set_available(benchmarks::kCapacity); + benchmark.GenerateRequests(benchmarks::kMaxSize, benchmarks::kNumRequests); + benchmark.metrics().Dump(); +} + +} // namespace pw::allocator + +int main() { + pw::allocator::DoWorstFitBenchmark(); + return 0; +} diff --git a/pw_allocator/public/pw_allocator/testing.h b/pw_allocator/public/pw_allocator/testing.h index b988556a56..c761927b48 100644 --- a/pw_allocator/public/pw_allocator/testing.h +++ b/pw_allocator/public/pw_allocator/testing.h @@ -90,6 +90,11 @@ class AllocatorForTest : public Allocator { } } + /// @copydoc BlockAllocator::MeasureFragmentation + Fragmentation MeasureFragmentation() const { + return allocator_->MeasureFragmentation(); + } + private: /// @copydoc Allocator::Allocate void* DoAllocate(Layout layout) override { diff --git a/pw_allocator/py/BUILD.bazel b/pw_allocator/py/BUILD.bazel new file mode 100644 index 0000000000..86a30ec6a2 --- /dev/null +++ b/pw_allocator/py/BUILD.bazel @@ -0,0 +1,37 @@ +# Copyright 2022 The Pigweed Authors +# +# 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 +# +# https://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. + +load("@rules_python//python:defs.bzl", "py_library") +load("//pw_build:python.bzl", "pw_py_binary") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "pw_allocator", + srcs = [ + "pw_allocator/__init__.py", + "pw_allocator/benchmarks.py", + ], + imports = ["."], + deps = [ + "//pw_cli/py:pw_cli", + ], +) + +pw_py_binary( + name = "benchmarks", + srcs = ["pw_allocator/__main__.py"], + main = "pw_allocator/__main__.py", + deps = [":pw_allocator"], +) diff --git a/pw_allocator/py/BUILD.gn b/pw_allocator/py/BUILD.gn index 9cd48498ab..c76daf5c6b 100644 --- a/pw_allocator/py/BUILD.gn +++ b/pw_allocator/py/BUILD.gn @@ -23,6 +23,8 @@ pw_python_package("py") { ] sources = [ "pw_allocator/__init__.py", + "pw_allocator/__main__.py", + "pw_allocator/benchmarks.py", "pw_allocator/heap_viewer.py", ] python_deps = [ "$dir_pw_cli/py" ] diff --git a/pw_allocator/py/pw_allocator/__main__.py b/pw_allocator/py/pw_allocator/__main__.py new file mode 100644 index 0000000000..b2b6703440 --- /dev/null +++ b/pw_allocator/py/pw_allocator/__main__.py @@ -0,0 +1,18 @@ +# Copyright 2024 The Pigweed Authors +# +# 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 +# +# https://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. +"""Runs the main function in detokenize.py.""" + +from pw_allocator import benchmarks + +benchmarks.main() diff --git a/pw_allocator/py/pw_allocator/benchmarks.py b/pw_allocator/py/pw_allocator/benchmarks.py new file mode 100644 index 0000000000..901a35a877 --- /dev/null +++ b/pw_allocator/py/pw_allocator/benchmarks.py @@ -0,0 +1,227 @@ +# Copyright 2024 The Pigweed Authors +# +# 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 +# +# https://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. +"""Collects benchmarks for supported allocators.""" + +import argparse +import json +import os +import subprocess +import sys + +from pathlib import Path +from typing import Any, IO + + +def _parse_args() -> argparse.Namespace: + """Parse arguments.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + '-o', '--output', type=str, help='Path to write a CSV file to' + ) + parser.add_argument( + '-v', '--verbose', action='store_true', help='Echo benchmarks to stdout' + ) + return parser.parse_args() + + +BAZEL = 'bazelisk' + +ALLOCATORS = [ + 'best_fit', + 'dual_first_fit', + 'first_fit', + 'last_fit', + 'worst_fit', +] + +BY_ALLOC_COUNT = 'by allocation count' +BY_ALLOC_COUNT_1K = 'allocation count in [1,000, 10,000)' +BY_ALLOC_COUNT_BUCKETS = [ + 'allocation count in [0, 10)', + 'allocation count in [10, 100)', + 'allocation count in [100, 1,000)', + BY_ALLOC_COUNT_1K, + 'allocation count in [10,000, inf)', +] + +BY_FRAGMENTATION = 'by fragmentation' +BY_FRAGMENTATION_BUCKETS = [ + 'fragmentation in [0.0, 0.2)', + 'fragmentation in [0.2, 0.4)', + 'fragmentation in [0.4, 0.6)', + 'fragmentation in [0.6, 0.8)', + 'fragmentation in [0.8, 1.0]', +] + +BY_ALLOC_SIZE = 'by allocation size' +BY_ALLOC_SIZE_BUCKETS = [ + 'usable size in [0, 16)', + 'usable size in [16, 64)', + 'usable size in [64, 256)', + 'usable size in [256, 1024)', + 'usable size in [1024, 4096)', + 'usable size in [4096, inf)', +] + +BUCKETS = { + BY_ALLOC_COUNT: BY_ALLOC_COUNT_BUCKETS, + BY_FRAGMENTATION: BY_FRAGMENTATION_BUCKETS, + BY_ALLOC_SIZE: BY_ALLOC_SIZE_BUCKETS, +} + +METRICS = [ + 'mean response time (ns)', + 'mean fragmentation metric', + 'mean max available (bytes)', + 'number of calls that failed', +] + + +def find_pw_root() -> Path: + """Returns the path to the Pigweed repository.""" + pw_root = os.getenv('PW_ROOT') + if pw_root: + return Path(pw_root) + cwd = Path(os.getcwd()) + marker = 'PIGWEED_MODULES' + while True: + f = cwd / marker + if f.is_file(): + return cwd + if cwd.parent == cwd: + raise RuntimeError('Unable to find Pigweed root') + cwd = cwd.parent + + +class Benchmark: + """Collection of benchmarks for a single allocator.""" + + def __init__(self, allocator: str): + self.allocator = allocator + self.metrics: dict[str, dict] = {} + self.data: dict[str, Any] = {} + self.metrics[BY_ALLOC_COUNT] = {} + self.metrics[BY_FRAGMENTATION] = {} + self.metrics[BY_ALLOC_SIZE] = {} + + def collect(self): + """Builds and runs all categories of an allocator benchmark.""" + pw_root = os.getenv('PW_ROOT') + if not pw_root: + raise RuntimeError('PW_ROOT is not set') + benchmark = f'{self.allocator}_benchmark' + label = f'//pw_allocator/benchmarks:{benchmark}' + subprocess.run([BAZEL, 'build', label], cwd=pw_root) + p1 = subprocess.Popen( + [BAZEL, 'run', label], cwd=pw_root, stdout=subprocess.PIPE + ) + p2 = subprocess.Popen( + [ + 'python', + 'pw_tokenizer/py/pw_tokenizer/detokenize.py', + 'base64', + f'bazel-bin/pw_allocator/benchmarks/{benchmark}#.*', + ], + cwd=pw_root, + stdin=p1.stdout, + stdout=subprocess.PIPE, + ) + p1.stdout.close() + output = p2.communicate()[0] + lines = [ + line[18:] for line in output.decode('utf-8').strip().split('\n') + ] + for _, data in json.loads('\n'.join(lines)).items(): + self.data = data + self.collect_category(BY_ALLOC_COUNT) + self.collect_category(BY_FRAGMENTATION) + self.collect_category(BY_ALLOC_SIZE) + + def collect_category(self, category: str): + """Runs one category of an allocator benchmark.""" + category_data = self.data[category] + for name in BUCKETS[category]: + self.metrics[category][name] = category_data[name] + + +class BenchmarkSuite: + """Collection of benchmarks for all supported allocators.""" + + def __init__(self): + self.benchmarks = [Benchmark(allocator) for allocator in ALLOCATORS] + + def collect(self): + """Builds and runs all allocator benchmarks.""" + for benchmark in self.benchmarks: + benchmark.collect() + + def write_benchmarks(self, output: IO): + """Reorganizes benchmark metrics and writes them to the given output.""" + for metric in METRICS: + self.write_category(output, metric, BY_ALLOC_COUNT) + self.write_category(output, metric, BY_FRAGMENTATION) + self.write_category(output, metric, BY_ALLOC_SIZE) + + def write_category(self, output: IO, metric: str, category: str): + """Writes a single category of benchmarks to the given output.""" + output.write(f'{metric} {category}\t') + for benchmark in self.benchmarks: + output.write(f'{benchmark.allocator}\t') + output.write('\n') + + for bucket in BUCKETS[category]: + output.write(f'{bucket}\t') + for benchmark in self.benchmarks: + output.write(f'{benchmark.metrics[category][bucket][metric]}\t') + output.write('\n') + output.write('\n') + + def print_summary(self): + """Writes selected metrics to stdout.""" + print('\n' + '#' * 80) + print(f'Results for {BY_ALLOC_COUNT_1K}:') + sys.stdout.write(' ' * 32) + for benchmark in self.benchmarks: + sys.stdout.write(benchmark.allocator.ljust(16)) + sys.stdout.write('\n') + + for metric in METRICS: + sys.stdout.write(metric.ljust(32)) + for benchmark in self.benchmarks: + metrics = benchmark.metrics[BY_ALLOC_COUNT][BY_ALLOC_COUNT_1K] + sys.stdout.write(f'{metrics[metric]}'.ljust(16)) + sys.stdout.write('\n') + print('#' * 80) + + +def main() -> int: + """Builds and runs allocator benchmarks.""" + args = _parse_args() + suite = BenchmarkSuite() + suite.collect() + if args.output: + with open(Path(args.output), 'w+') as output: + suite.write_benchmarks(output) + print(f'\nWrote to {Path(args.output).resolve()}') + + if args.verbose: + suite.write_benchmarks(sys.stdout) + + suite.print_summary() + + return 0 + + +if __name__ == '__main__': + main() diff --git a/pw_allocator/test_harness.cc b/pw_allocator/test_harness.cc index 037bbe01be..10424eabb2 100644 --- a/pw_allocator/test_harness.cc +++ b/pw_allocator/test_harness.cc @@ -128,8 +128,7 @@ bool TestHarness::HandleRequest(const Request& request) { using T = std::decay_t; if constexpr (std::is_same_v) { - size_t size = std::max(r.size, sizeof(Allocation)); - Layout layout(size, r.alignment); + Layout layout(r.size, r.alignment); BeforeAllocate(layout); void* ptr = allocator_->Allocate(layout); AfterAllocate(ptr); @@ -139,7 +138,6 @@ bool TestHarness::HandleRequest(const Request& request) { } else { max_size_ = std::max(layout.size() / 2, size_t(1)); } - } else if constexpr (std::is_same_v) { Allocation* old = RemoveAllocation(r.index); if (old == nullptr) { @@ -154,8 +152,7 @@ bool TestHarness::HandleRequest(const Request& request) { if (old == nullptr) { return false; } - size_t new_size = std::max(r.new_size, sizeof(Allocation)); - Layout new_layout = Layout(new_size, old->layout.alignment()); + Layout new_layout(r.new_size, old->layout.alignment()); BeforeReallocate(new_layout); void* new_ptr = allocator_->Reallocate(old, new_layout); AfterReallocate(new_ptr); @@ -182,7 +179,18 @@ void TestHarness::Reset() { } void TestHarness::AddAllocation(void* ptr, Layout layout) { - PW_ASSERT(layout.size() >= sizeof(Allocation)); + constexpr Layout min_layout = Layout::Of(); + if (layout.size() < min_layout.size() || + layout.alignment() < min_layout.alignment()) { + // The harness should want to test small sizes and alignments, but it + // needs the layout to be at least `Layout::Of` in order + // to persist details about it. If either the size or alignment is too + // small, deallocate immediately. + BeforeDeallocate(ptr); + allocator_->Deallocate(ptr); + AfterDeallocate(); + return; + } auto* bytes = static_cast(ptr); auto* allocation = ::new (bytes) Allocation(layout); allocations_.push_back(*allocation);