Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c8b8e8f
Initial impl
anarthal Nov 20, 2025
2ebe82e
Make buffer a member
anarthal Nov 20, 2025
a028853
Factor out rebase_tring
anarthal Nov 20, 2025
be4d0ff
Simplify copy ops
anarthal Nov 20, 2025
97fe56e
Better comment
anarthal Nov 20, 2025
98ea8a8
Separate the buffer
anarthal Nov 20, 2025
bb57e20
encapsulate copy assign
anarthal Nov 20, 2025
61b8cff
Finish encapsulating
anarthal Nov 20, 2025
cf6798a
remove swap
anarthal Nov 20, 2025
3a0c0ba
Use std::copy
anarthal Nov 21, 2025
100ae04
Move flat_tree tests
anarthal Nov 21, 2025
f8e71b1
use lightweight test
anarthal Nov 21, 2025
6646b69
Use container checks
anarthal Nov 21, 2025
20c230c
simplify the constant
anarthal Nov 21, 2025
5ac045e
Default ctor
anarthal Nov 21, 2025
4bf90d4
Test add nodes
anarthal Nov 21, 2025
58f5ed7
add nodes copies
anarthal Nov 21, 2025
c72f42d
Capacity increase
anarthal Nov 21, 2025
e0c3753
Make reallocations powers of 2
anarthal Nov 21, 2025
5d727ae
Reserve tests
anarthal Nov 21, 2025
1a1452f
clear tests
anarthal Nov 21, 2025
9e19635
Copy ctor tests
anarthal Nov 25, 2025
f83a4e3
More copy tests
anarthal Nov 25, 2025
8f7e95d
Move ctor tests
anarthal Nov 25, 2025
5e3ec9f
copy assign
anarthal Nov 25, 2025
38f49eb
copy assign corrections
anarthal Nov 25, 2025
4df05ee
final copy assign tests
anarthal Nov 25, 2025
101f60e
comparison tests
anarthal Nov 25, 2025
82a3f8b
cleanup
anarthal Nov 25, 2025
5e1f4dc
Finished tests
anarthal Nov 27, 2025
1f40390
Reference docs
anarthal Nov 27, 2025
cf60b99
quickref
anarthal Nov 27, 2025
fc9602c
doc fixes
anarthal Nov 27, 2025
a953a23
qualify resp3 names in doc
anarthal Nov 27, 2025
807c8a2
Mention in discussion
anarthal Nov 27, 2025
e7a808c
Make node operator== bool instead of auto and docs
anarthal Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions doc/modules/ROOT/pages/reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ xref:reference:boost/redis/response.adoc[`response`]

xref:reference:boost/redis/generic_response.adoc[`generic_response`]

xref:reference:boost/redis/generic_flat_response.adoc[`generic_flat_response`]

xref:reference:boost/redis/consume_one-08.adoc[`consume_one`]


Expand All @@ -70,25 +72,33 @@ xref:reference:boost/redis/adapter/result.adoc[`adapter::result`]
xref:reference:boost/redis/any_adapter.adoc[`any_adapter`]

|
xref:reference:boost/redis/resp3/basic_node.adoc[`basic_node`]
xref:reference:boost/redis/resp3/basic_node.adoc[`resp3::basic_node`]

xref:reference:boost/redis/resp3/node.adoc[`resp3::node`]

xref:reference:boost/redis/resp3/node_view.adoc[`resp3::node_view`]

xref:reference:boost/redis/resp3/basic_tree.adoc[`resp3::basic_tree`]

xref:reference:boost/redis/resp3/tree.adoc[`resp3::tree`]

xref:reference:boost/redis/resp3/node.adoc[`node`]
xref:reference:boost/redis/resp3/view_tree.adoc[`resp3::view_tree`]

xref:reference:boost/redis/resp3/node_view.adoc[`node_view`]
xref:reference:boost/redis/resp3/flat_tree.adoc[`resp3::flat_tree`]

xref:reference:boost/redis/resp3/boost_redis_to_bulk-08.adoc[`boost_redis_to_bulk`]

xref:reference:boost/redis/resp3/type.adoc[`type`]
xref:reference:boost/redis/resp3/type.adoc[`resp3::type`]

xref:reference:boost/redis/resp3/is_aggregate.adoc[`is_aggregate`]
xref:reference:boost/redis/resp3/is_aggregate.adoc[`resp3::is_aggregate`]


|

xref:reference:boost/redis/adapter/adapt2.adoc[`adapter::adapt2`]

xref:reference:boost/redis/resp3/parser.adoc[`parser`]
xref:reference:boost/redis/resp3/parser.adoc[`resp3::parser`]

xref:reference:boost/redis/resp3/parse.adoc[`parse`]
xref:reference:boost/redis/resp3/parse.adoc[`resp3::parse`]

|===
5 changes: 3 additions & 2 deletions doc/modules/ROOT/pages/requests_responses.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ struct basic_node {
----

Any response to a Redis command can be parsed into a
xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response].
xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response]
and its counterpart xref:reference:boost/redis/generic_flat_response.adoc[boost::redis::generic_flat_response].
The vector can be seen as a pre-order view of the response tree.
Using it is not different than using other types:

Expand All @@ -292,7 +293,7 @@ co_await conn->async_exec(req, resp);
For example, suppose we want to retrieve a hash data structure
from Redis with `HGETALL`, some of the options are

* `boost::redis::generic_response`: always works.
* `boost::redis::generic_response` and `boost::redis::generic_flat_response`: always works.
* `std::vector<std::string>`: efficient and flat, all elements as string.
* `std::map<std::string, std::string>`: efficient if you need the data as a `std::map`.
* `std::map<U, V>`: efficient if you are storing serialized data. Avoids temporaries and requires `boost_redis_from_bulk` for `U` and `V`.
Expand Down
210 changes: 137 additions & 73 deletions include/boost/redis/impl/flat_tree.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -7,119 +7,183 @@
//

#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/tree.hpp>

#include <boost/assert.hpp>

#include <algorithm>
#include <cstddef>
#include <cstring>
#include <string_view>

namespace boost::redis::resp3 {

flat_tree::flat_tree(flat_tree const& other)
: data_{other.data_}
, view_tree_{other.view_tree_}
, ranges_{other.ranges_}
, pos_{0u}
, reallocs_{0u}
, total_msgs_{other.total_msgs_}
namespace detail {

// Updates string views by performing pointer arithmetic
inline void rebase_strings(view_tree& nodes, const char* old_base, const char* new_base)
{
view_tree_.resize(ranges_.size());
set_views();
for (auto& nd : nodes) {
if (!nd.value.empty()) {
const auto offset = nd.value.data() - old_base;
BOOST_ASSERT(offset >= 0);
nd.value = {new_base + offset, nd.value.size()};
}
}
}

flat_tree&
flat_tree::operator=(flat_tree other)
// --- Operations in flat_buffer ---

// Compute the new capacity upon reallocation. We always use powers of 2,
// starting in 512, to prevent many small allocations
inline std::size_t compute_capacity(std::size_t current, std::size_t requested)
{
swap(*this, other);
return *this;
std::size_t res = (std::max)(current, static_cast<std::size_t>(512u));
while (res < requested)
res *= 2u;
return res;
}

void flat_tree::reserve(std::size_t bytes, std::size_t nodes)
// Copy construction
inline flat_buffer copy_construct(const flat_buffer& other)
{
data_.reserve(bytes);
view_tree_.reserve(nodes);
ranges_.reserve(nodes);
flat_buffer res{{}, other.size, 0u, 0u};

if (other.size > 0u) {
const std::size_t capacity = compute_capacity(0u, other.size);
res.data.reset(new char[capacity]);
res.capacity = capacity;
res.reallocs = 1u;
std::copy(other.data.get(), other.data.get() + other.size, res.data.get());
}

return res;
}

void flat_tree::clear()
// Copy assignment
inline void copy_assign(flat_buffer& buff, const flat_buffer& other)
{
pos_ = 0u;
total_msgs_ = 0u;
reallocs_ = 0u;
data_.clear();
view_tree_.clear();
ranges_.clear();
// Make space if required
if (buff.capacity < other.size) {
const std::size_t capacity = compute_capacity(buff.capacity, other.size);
buff.data.reset(new char[capacity]);
buff.capacity = capacity;
++buff.reallocs;
}

// Copy the contents
std::copy(other.data.get(), other.data.get() + other.size, buff.data.get());
buff.size = other.size;
}

void flat_tree::set_views()
// Grows the buffer until reaching a target size.
// Might rebase the strings in nodes
inline void grow(flat_buffer& buff, std::size_t new_capacity, view_tree& nodes)
{
BOOST_ASSERT_MSG(pos_ < view_tree_.size(), "notify_done called but no nodes added.");
BOOST_ASSERT_MSG(view_tree_.size() == ranges_.size(), "Incompatible sizes.");
if (new_capacity <= buff.capacity)
return;

for (; pos_ < view_tree_.size(); ++pos_) {
auto const& r = ranges_.at(pos_);
view_tree_.at(pos_).value = std::string_view{data_.data() + r.offset, r.size};
}
// Compute the actual capacity that we will be using
new_capacity = compute_capacity(buff.capacity, new_capacity);

// Allocate space
std::unique_ptr<char[]> new_buffer{new char[new_capacity]};

// Copy any data into the newly allocated space
const char* data_before = buff.data.get();
char* data_after = new_buffer.get();
std::copy(data_before, data_before + buff.size, data_after);

// Update the string views so they don't dangle
rebase_strings(nodes, data_before, data_after);

// Replace the buffer. Note that size hasn't changed here
buff.data = std::move(new_buffer);
buff.capacity = new_capacity;
++buff.reallocs;
}

void flat_tree::notify_done()
// Appends a string to the buffer.
// Might rebase the string in nodes, but doesn't append any new node.
inline std::string_view append(flat_buffer& buff, std::string_view value, view_tree& nodes)
{
total_msgs_ += 1;
set_views();
// If there is nothing to copy, do nothing
if (value.empty())
return value;

// Make space for the new string
const std::size_t new_size = buff.size + value.size();
grow(buff, new_size, nodes);

// Copy the new value
const std::size_t offset = buff.size;
std::copy(value.data(), value.data() + value.size(), buff.data.get() + offset);
buff.size = new_size;
return {buff.data.get() + offset, value.size()};
}

void flat_tree::push(node_view const& node)
{
auto data_before = data_.data();
add_node_impl(node);
auto data_after = data_.data();
} // namespace detail

if (data_after != data_before) {
pos_ = 0;
reallocs_ += 1;
}
flat_tree::flat_tree(flat_tree const& other)
: data_{detail::copy_construct(other.data_)}
, view_tree_{other.view_tree_}
, total_msgs_{other.total_msgs_}
{
detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get());
}

void flat_tree::add_node_impl(node_view const& node)
flat_tree& flat_tree::operator=(const flat_tree& other)
{
ranges_.push_back({data_.size(), node.value.size()});
if (this != &other) {
// Copy the data
detail::copy_assign(data_, other.data_);

// This must come after setting the offset above.
data_.insert(data_.end(), node.value.begin(), node.value.end());
// Copy the nodes
view_tree_ = other.view_tree_;
detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get());

view_tree_.push_back(node);
// Copy the other fields
total_msgs_ = other.total_msgs_;
}

return *this;
}

void swap(flat_tree& a, flat_tree& b)
void flat_tree::reserve(std::size_t bytes, std::size_t nodes)
{
using std::swap;

swap(a.data_, b.data_);
swap(a.view_tree_, b.view_tree_);
swap(a.ranges_, b.ranges_);
swap(a.pos_, b.pos_);
swap(a.reallocs_, b.reallocs_);
swap(a.total_msgs_, b.total_msgs_);
// Space for the strings
detail::grow(data_, bytes, view_tree_);

// Space for the nodes
view_tree_.reserve(nodes);
}

bool
operator==(
flat_tree::range const& a,
flat_tree::range const& b)
void flat_tree::clear() noexcept
{
return a.offset == b.offset && a.size == b.size;
data_.size = 0u;
view_tree_.clear();
total_msgs_ = 0u;
}

bool operator==(flat_tree const& a, flat_tree const& b)
void flat_tree::push(node_view const& nd)
{
return
a.data_ == b.data_ &&
a.view_tree_ == b.view_tree_ &&
a.ranges_ == b.ranges_ &&
a.pos_ == b.pos_ &&
//a.reallocs_ == b.reallocs_ &&
a.total_msgs_ == b.total_msgs_;
// Add the string
const std::string_view str = detail::append(data_, nd.value, view_tree_);

// Add the node
view_tree_.push_back({
nd.data_type,
nd.aggregate_size,
nd.depth,
str,
});
}

bool operator!=(flat_tree const& a, flat_tree const& b)
bool operator==(flat_tree const& a, flat_tree const& b)
{
return !(a == b);
// data is already taken into account by comparing the nodes.
return a.view_tree_ == b.view_tree_ && a.total_msgs_ == b.total_msgs_;
}

} // namespace boost::redis
} // namespace boost::redis::resp3
Loading