diff --git a/include/boost/mysql/impl/internal/next_power_of_two.hpp b/include/boost/mysql/impl/internal/next_power_of_two.hpp new file mode 100644 index 000000000..6b19d5c18 --- /dev/null +++ b/include/boost/mysql/impl/internal/next_power_of_two.hpp @@ -0,0 +1,68 @@ +// +// Copyright (c) 2026 Vladislav Soulgard (vsoulgard at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_IMPL_INTERNAL_NEXT_POWER_OF_TWO_HPP +#define BOOST_MYSQL_IMPL_INTERNAL_NEXT_POWER_OF_TWO_HPP + +#include + +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +template +struct recursive_shift_or; + +// Apply shift and continue to the next power of 2 +template +struct recursive_shift_or +{ + static void apply(UnsignedInt& n) + { + n |= n >> Shift; + recursive_shift_or::apply(n); + } +}; + +// Stop recursion when shift exceeds type width +template +struct recursive_shift_or +{ + static void apply(UnsignedInt&) {} +}; + +// Returns the smallest power of two greater than or equal to n. +// Precondition: n must not exceed the largest power of two that fits +// in UnsignedInt. For example: +// - uint8_t: n <= 128 (2^7) +// - uint16_t: n <= 32768 (2^15) +// - uint32_t: n <= 2147483648 (2^31) +// - uint64_t: n <= 9223372036854775808 (2^63) +// +// Passing a larger value results in undefined behavior (overflow). +// In debug builds, this is caught by BOOST_ASSERT. +template +UnsignedInt next_power_of_two(UnsignedInt n) noexcept +{ + static_assert(std::is_unsigned::value, ""); + // Assert overflow (if value is bigger than maximum power) + BOOST_ASSERT(!(n > (std::numeric_limits::max() >> 1) + 1)); + if (n == 0) return 1; + n--; + // Fill all lower bits + recursive_shift_or<1, UnsignedInt, (1 < sizeof(UnsignedInt) * 8)>::apply(n); + return n + 1; +} + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/impl/internal/sansio/message_reader.hpp b/include/boost/mysql/impl/internal/sansio/message_reader.hpp index cbe46a758..5fde41d1d 100644 --- a/include/boost/mysql/impl/internal/sansio/message_reader.hpp +++ b/include/boost/mysql/impl/internal/sansio/message_reader.hpp @@ -93,7 +93,17 @@ class message_reader BOOST_ATTRIBUTE_NODISCARD error_code prepare_buffer() { - buffer_.remove_reserved(); + constexpr std::size_t small_move_threshold = 1024; + const std::size_t occupied_space = buffer_.pending_size() + buffer_.current_message_size(); + // Compact the buffer (remove reserved area) if one of the following holds: + // 1. The cost of memmove is low: active data (current_message + pending) + // is small enough to make the copy cheap. + // 2. Compaction could prevent a reallocation. + if (occupied_space <= small_move_threshold || + (state_.required_size > buffer_.free_size())) + { + buffer_.remove_reserved(); + } auto ec = buffer_.grow_to_fit(state_.required_size); if (ec) return ec; diff --git a/include/boost/mysql/impl/internal/sansio/read_buffer.hpp b/include/boost/mysql/impl/internal/sansio/read_buffer.hpp index ee17a608a..9197bbafe 100644 --- a/include/boost/mysql/impl/internal/sansio/read_buffer.hpp +++ b/include/boost/mysql/impl/internal/sansio/read_buffer.hpp @@ -11,6 +11,8 @@ #include #include +#include + #include #include #include @@ -138,14 +140,20 @@ class read_buffer } // Makes sure the free size is at least n bytes long; resizes the buffer if required + // Buffer grows to power of two, unless limited by max_size BOOST_ATTRIBUTE_NODISCARD error_code grow_to_fit(std::size_t n) { if (free_size() < n) { - std::size_t new_size = buffer_.size() + n - free_size(); + std::size_t required_size = buffer_.size() + n - free_size(); + std::size_t new_size = next_power_of_two(required_size); if (new_size > max_size_) - return client_errc::max_buffer_size_exceeded; + { + new_size = required_size; + if (new_size > max_size_) + return client_errc::max_buffer_size_exceeded; + } buffer_.resize(new_size); } return error_code(); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 85bcf8d6a..1f5a0a5d0 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -78,6 +78,7 @@ add_executable( test/impl/dt_to_string.cpp test/impl/ssl_context_with_default.cpp + test/impl/next_power_of_two.cpp test/impl/variant_stream.cpp test/spotchecks/connection_use_after_move.cpp diff --git a/test/unit/Jamfile b/test/unit/Jamfile index cc989d47f..2554c8572 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -87,6 +87,7 @@ run test/impl/dt_to_string.cpp test/impl/ssl_context_with_default.cpp + test/impl/next_power_of_two.cpp test/impl/variant_stream.cpp test/spotchecks/connection_use_after_move.cpp diff --git a/test/unit/test/impl/next_power_of_two.cpp b/test/unit/test/impl/next_power_of_two.cpp new file mode 100644 index 000000000..06cea8b90 --- /dev/null +++ b/test/unit/test/impl/next_power_of_two.cpp @@ -0,0 +1,90 @@ +// +// Copyright (c) 2026 Vladislav Soulgard (vsoulgard at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include + +#include + +#include + +using namespace boost::mysql::detail; + +BOOST_AUTO_TEST_SUITE(test_next_power_of_two) + +BOOST_AUTO_TEST_CASE(basic) +{ + // n = 0 (special case) + BOOST_TEST(next_power_of_two(0u) == 1u); + + // n is already power of two + BOOST_TEST(next_power_of_two(1u) == 1u); + BOOST_TEST(next_power_of_two(2u) == 2u); + BOOST_TEST(next_power_of_two(4u) == 4u); + BOOST_TEST(next_power_of_two(8u) == 8u); + BOOST_TEST(next_power_of_two(16u) == 16u); + BOOST_TEST(next_power_of_two(32u) == 32u); + BOOST_TEST(next_power_of_two(64u) == 64u); + BOOST_TEST(next_power_of_two(128u) == 128u); + + // n just below power of two + BOOST_TEST(next_power_of_two(3u) == 4u); + BOOST_TEST(next_power_of_two(7u) == 8u); + BOOST_TEST(next_power_of_two(15u) == 16u); + BOOST_TEST(next_power_of_two(31u) == 32u); + BOOST_TEST(next_power_of_two(63u) == 64u); + BOOST_TEST(next_power_of_two(127u) == 128u); + + // n just above power of two + BOOST_TEST(next_power_of_two(5u) == 8u); + BOOST_TEST(next_power_of_two(9u) == 16u); + BOOST_TEST(next_power_of_two(17u) == 32u); + BOOST_TEST(next_power_of_two(33u) == 64u); + BOOST_TEST(next_power_of_two(65u) == 128u); + BOOST_TEST(next_power_of_two(129u) == 256u); + + // n is random value + BOOST_TEST(next_power_of_two(6u) == 8u); + BOOST_TEST(next_power_of_two(13u) == 16u); + BOOST_TEST(next_power_of_two(21u) == 32u); + BOOST_TEST(next_power_of_two(45u) == 64u); + BOOST_TEST(next_power_of_two(89u) == 128u); + BOOST_TEST(next_power_of_two(200u) == 256u); + BOOST_TEST(next_power_of_two(300u) == 512u); + BOOST_TEST(next_power_of_two(400u) == 512u); + BOOST_TEST(next_power_of_two(505u) == 512u); + BOOST_TEST(next_power_of_two(888u) == 1024u); +} + +BOOST_AUTO_TEST_CASE(different_types) +{ + // uint8_t + BOOST_TEST(next_power_of_two(0u) == 1u); + BOOST_TEST(next_power_of_two(62u) == 64u); + BOOST_TEST(next_power_of_two(100u) == 128u); + BOOST_TEST(next_power_of_two(128u) == 128u); + + // uint16_t + BOOST_TEST(next_power_of_two(0u) == 1u); + BOOST_TEST(next_power_of_two(1000u) == 1024u); + BOOST_TEST(next_power_of_two(16383u) == 16384u); + BOOST_TEST(next_power_of_two(32768u) == 32768u); + + // uint32_t + BOOST_TEST(next_power_of_two(0u) == 1u); + BOOST_TEST(next_power_of_two(100000u) == 131072u); + BOOST_TEST(next_power_of_two(1u << 30) == 1u << 30); + BOOST_TEST(next_power_of_two((1u << 30) + 1) == 1u << 31); + + // uint64_t + BOOST_TEST(next_power_of_two(0u) == 1u); + BOOST_TEST(next_power_of_two(1ull << 40) == 1ull << 40); + BOOST_TEST(next_power_of_two((1ull << 40) + 1) == 1ull << 41); + BOOST_TEST(next_power_of_two(1ull << 62) == 1ull << 62); + BOOST_TEST(next_power_of_two((1ull << 62) + 1) == 1ull << 63); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/message_reader.cpp b/test/unit/test/sansio/message_reader.cpp index 411e12225..2658f0a7c 100644 --- a/test/unit/test/sansio/message_reader.cpp +++ b/test/unit/test/sansio/message_reader.cpp @@ -43,9 +43,10 @@ class reader_fixture reader_fixture( std::vector contents, std::size_t buffsize = 512, - std::size_t max_size = static_cast(-1) + std::size_t max_size = static_cast(-1), + std::size_t max_frame_size = 64 // max frame size is 64 ) - : reader(buffsize, max_size, 64), // max frame size is 64 + : reader(buffsize, max_size, max_frame_size), contents_(std::move(contents)), buffer_first_(reader.internal_buffer().first()) { @@ -384,7 +385,7 @@ BOOST_AUTO_TEST_CASE(buffer_resizing_not_enough_space) ec = fix.reader.prepare_buffer(); BOOST_TEST(ec == error_code()); fix.record_buffer_first(); - BOOST_TEST(fix.buffsize() == 50u); + BOOST_TEST(fix.buffsize() == 64u); // Finish reading fix.read_bytes(50); @@ -406,7 +407,7 @@ BOOST_AUTO_TEST_CASE(buffer_resizing_old_messages_removed) fix.check_message(u8vec(60, 0x04)); // Record size, as this should not increase - BOOST_TEST(fix.buffsize() == 60u); + BOOST_TEST(fix.buffsize() == 64u); // Parse new messages for (std::uint8_t i = 0u; i < 100u; ++i) @@ -429,7 +430,7 @@ BOOST_AUTO_TEST_CASE(buffer_resizing_old_messages_removed) } // Buffer size should be the same - BOOST_TEST(fix.buffsize() == 60u); + BOOST_TEST(fix.buffsize() == 64u); } BOOST_AUTO_TEST_CASE(buffer_resizing_size_eq_max_size) @@ -501,6 +502,52 @@ BOOST_AUTO_TEST_CASE(buffer_resizing_max_size_exceeded_subsequent_frames) BOOST_TEST(ec == client_errc::max_buffer_size_exceeded); } +BOOST_AUTO_TEST_CASE(buffer_resizing_size_power_of_two) +{ + // Setup + reader_fixture fix(create_frame(42, u8vec(4, 0x04)), 0, 1024, 1024); + fix.reader.prepare_read(fix.seqnum); + fix.read_until_completion(); + BOOST_TEST(fix.buffsize() == 4u); + + std::size_t test_sizes[] = { + 5, 7, 8, + 9, 15, 16, + 17, 31, 32, + 33, 63, 64, + 65, 127, 128, + 129, 255, 256, + 257, 511, 512, 513 + }; + + // Test that buffer capacity grows to powers of two for various payload sizes + for (auto new_size : test_sizes) + { + BOOST_TEST_CONTEXT(new_size) + { + // Setup + u8vec msg_body(new_size, 0x04); + fix.seqnum = static_cast(new_size); + fix.set_contents(create_frame(fix.seqnum, msg_body)); + std::size_t next_power_of_two = 1; + while (next_power_of_two < new_size) + next_power_of_two *= 2; + + // Prepare read + fix.reader.prepare_read(fix.seqnum); + + // Read the message into the buffer and trigger the op until completion. + // This will call prepare_buffer() internally + fix.read_until_completion(); + + // Check results + BOOST_TEST_REQUIRE(fix.reader.error() == error_code()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(fix.reader.message(), msg_body); + BOOST_TEST(fix.buffsize() == next_power_of_two); + } + } +} + // Keep parsing state BOOST_AUTO_TEST_CASE(keep_state_continuation) { @@ -630,4 +677,82 @@ BOOST_AUTO_TEST_CASE(reset_keep_state_true) BOOST_TEST(fix.seqnum == 21u); } +BOOST_AUTO_TEST_CASE(memmove_avoided) +{ + // Test that std::memmove() is avoided for large messages (>1024 bytes) + // when the buffer has sufficient free space. + + // Setup + reader_fixture fix(create_frame(0, u8vec(0, 0)), 4096, 4096, 4096); + + // Buffer is free + std::size_t free_bytes = fix.reader.buffer().size(); + BOOST_TEST(free_bytes == 4096u); + + // Small messages (<=1024 bytes): memmove is expected + { + constexpr std::size_t small_msg_size = 200; + // First frame and second frame header expected to + // be memmoved, when second payload will be parsed + std::size_t expected = 4096 - small_msg_size; + auto first_frame = create_frame(42, u8vec(small_msg_size, 0x0a)); + auto second_frame = create_frame(43, u8vec(small_msg_size, 0x0a)); + fix.seqnum = 42; + fix.set_contents(concat(first_frame, second_frame)); + fix.reader.prepare_read(fix.seqnum); + auto ec = fix.reader.prepare_buffer(); + BOOST_TEST(ec == error_code()); + fix.read_bytes(2 * (frame_header_size + small_msg_size)); + fix.reader.prepare_read(fix.seqnum); + ec = fix.reader.prepare_buffer(); + BOOST_TEST(ec == error_code()); + free_bytes = fix.reader.buffer().size(); + BOOST_TEST(free_bytes == expected); + } + + // Large messages (>1024 bytes): memmove is avoided if free space is big enough + { + constexpr std::size_t large_msg_size = 1025; + // First frame expected not to be memmoved, + // when we parse second message + constexpr std::size_t expected = 4096 - 2 * (frame_header_size + large_msg_size); + auto first_frame = create_frame(42, u8vec(large_msg_size, 0x0a)); + auto second_frame = create_frame(43, u8vec(large_msg_size, 0x0a)); + fix.seqnum = 42; + fix.set_contents(concat(first_frame, second_frame)); + fix.reader.prepare_read(fix.seqnum); + auto ec = fix.reader.prepare_buffer(); + BOOST_TEST(ec == error_code()); + fix.read_bytes(2 * (frame_header_size + large_msg_size)); + fix.reader.prepare_read(fix.seqnum); + ec = fix.reader.prepare_buffer(); + BOOST_TEST(ec == error_code()); + free_bytes = fix.reader.buffer().size(); + BOOST_TEST(free_bytes == expected); + } + + // Buffer cannot fit second message: memmove is expected + { + constexpr std::size_t very_large_msg_size = 2048; + // First frame and second frame header expected to + // be memmoved, when second payload will be parsed + constexpr std::size_t expected = 4096 - very_large_msg_size; + auto first_frame = create_frame(42, u8vec(very_large_msg_size, 0x0a)); + auto second_frame = create_frame(43, u8vec(very_large_msg_size, 0x0a)); + fix.seqnum = 42; + fix.set_contents(concat(first_frame, second_frame)); + fix.reader.prepare_read(fix.seqnum); + auto ec = fix.reader.prepare_buffer(); + BOOST_TEST(ec == error_code()); + fix.read_bytes(4092); + fix.reader.prepare_read(fix.seqnum); + ec = fix.reader.prepare_buffer(); + // Read what is left + fix.read_bytes(12); + BOOST_TEST(ec == error_code()); + free_bytes = fix.reader.buffer().size(); + BOOST_TEST(free_bytes == expected); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/read_buffer.cpp b/test/unit/test/sansio/read_buffer.cpp index 93ee07d00..00ca1097a 100644 --- a/test/unit/test/sansio/read_buffer.cpp +++ b/test/unit/test/sansio/read_buffer.cpp @@ -448,7 +448,7 @@ BOOST_AUTO_TEST_CASE(not_enough_space) auto ec = buff.grow_to_fit(100); BOOST_TEST(ec == error_code()); - check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08}, 100); + check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08}, 120); checker.check_reallocation(); } @@ -463,7 +463,7 @@ BOOST_AUTO_TEST_CASE(one_missing_byte) auto ec = buff.grow_to_fit(9); BOOST_TEST(ec == error_code()); - check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08}, 9); + check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08}, 24); checker.check_reallocation(); } @@ -518,7 +518,7 @@ BOOST_AUTO_TEST_CASE(lt_max_size) auto ec = buff.grow_to_fit(7); BOOST_TEST(ec == error_code()); - check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08}, 7); + check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08}, 8); checker.check_reallocation(); } @@ -563,25 +563,55 @@ BOOST_AUTO_TEST_CASE(several_grows) // Grow with reallocation auto ec = buff.grow_to_fit(4); BOOST_TEST(ec == error_code()); - check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08}, 4); + check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08}, 8); // Place some more bytes in the buffer copy_to_free_area(buff, {0x09, 0x0a}); buff.move_to_pending(2); - check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08, 0x09, 0x0a}, 2); + check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08, 0x09, 0x0a}, 6); // Grow without reallocation ec = buff.grow_to_fit(2); BOOST_TEST(ec == error_code()); - check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08, 0x09, 0x0a}, 2); + check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08, 0x09, 0x0a}, 6); copy_to_free_area(buff, {0x0b, 0x0c}); buff.move_to_pending(2); - check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, 0); + check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, 4); // Fail when attempting to grow past max size ec = buff.grow_to_fit(5); BOOST_TEST(ec == client_errc::max_buffer_size_exceeded); - check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, 0); + check_buffer(buff, {}, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, {0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, 4); +} + +BOOST_AUTO_TEST_CASE(is_power_of_two) +{ + read_buffer buff(8); + + std::size_t test_sizes[] = { + 5, 7, 8, + 9, 15, 16, + 17, 31, 32, + 33, 63, 64, + 65, 127, 128, + 129, 255, 256, + 257, 511, 512, 513 + }; + + // Test that buffer capacity grows to powers of two + for (auto new_size : test_sizes) + { + BOOST_TEST_CONTEXT(new_size) + { + std::size_t next_power_of_two = 1; + while (next_power_of_two < new_size) + next_power_of_two *= 2; + auto ec = buff.grow_to_fit(new_size); + BOOST_TEST(ec == error_code()); + BOOST_TEST(buff.size() == next_power_of_two); + } + } + BOOST_TEST(buff.size() == 1024u); } BOOST_AUTO_TEST_SUITE_END()