Better buffer management
Currently most classes that are known to only perform one operation at a time either use a static bin_data
, or a locally created bin_data
. Also, buffers are often passed around as bin_data const &
, and at times as range<bin_data::iterator>
.
Locally created bin_data
I expect cause quite a bit of churning on a tiny ESP32.
I propose a solution along the following lines:
- Have a class
buffer_pool
which lendsbuffer
s to users (e.g.nfc
,tag
); suchbuffer
is only moveable and upon destruction, returns itself to thebuffer_pool
, so that the memory can be reused. -
buffer
is either a wrapper aroundbin_data
or an "updated"bin_data
, so it has fundamentally the same functionality, but it never releases its memory. - All classes that have to borrow from
buffer_pool
, take ashared_ptr<buffer_pool>
and if that is not provided, they create their own. In this way, if a shared buffer manager is given, that's managed by the caller, and otherwise all buffers are destroyed with the class. - Change signatures as follows:
- All methods that copy from an entire
bin_data
will takerange<buffer::const_iterator>
. - All methods that fill a pre-sized buffer, will take
range<buffer::iterator>
. - All methods that now resize and populate a
bin_data &
, will either return abuffer
, or take one, preferably the first. -
bin_stream
can be constructed from arange<buffer::const_iterator>
.
Most methods that operate on buffer will proceed as follow:
buffer b = _pool.borrow(42 /* expected memory consumption */);
b << fill_with_data;
return send(command, b.view(), 10ms);
Essentially, a buffers
's ownership is unique, and whenever size is predetermined, only a non-owning range to its data is passed (down).
One possible way to implement this could be as follows:
#include <memory>
struct buffer;
struct buffer_pool : std::enable_shared_from_this<buffer_pool> {
std::vector<std::vector<uint8_t>> pool;
buffer borrow(std::size_t exp_size = 0);
};
struct buffer {
std::weak_ptr<buffer_pool> owner;
std::vector<uint8_t> memory_or_bin_data;
buffer() = default;
buffer(buffer const &) = delete;
buffer &operator=(buffer const &) = delete;
buffer(buffer &&) noexcept = default;
buffer &operator=(buffer &&) noexcept = default;
explicit buffer(std::weak_ptr<buffer_pool> owner_, std::vector<uint8_t> m_ = {})
: owner{std::move(owner_)}, memory_or_bin_data{std::move(m_)} {}
~buffer() {
if (not owner.expired()) {
owner.lock()->pool.emplace_back(std::move(memory_or_bin_data));
}
}
};
buffer buffer_pool::borrow(std::size_t exp_size) {
if (pool.empty()) {
std::vector<uint8_t> m;
m.reserve(exp_size);
return buffer{weak_from_this(), std::move(m)};
} else {
buffer retval{weak_from_this(), pool.back()};
pool.pop_back();
return retval;
}
}
Fun fact, I was just looking for a way to get a weak_ptr
to this
to solve the problem of: how does a buffer
know where to return, and I discovered std::enable_shared_from_this
.