/* * Copyright (C) 2018-present ScyllaDB */ /* * SPDX-License-Identifier: AGPL-3.0-or-later */ #pragma once #include "utils/fragmented_temporary_buffer.hh" #include #include #include namespace utils { // Users of reusable_buffer don't need the templated parts, // so the non-templated implementation parts are extracted // to a separate class. // This should prevent some unnecessary template instantiations. class reusable_buffer_impl { protected: friend class reusable_buffer_guard; // Max size observed since the last decay(), rounded up. // Currently we round to the smallest power of 2 greater or equal to // to the observed size. size_t _high_watermark = 0; std::unique_ptr _buf; // The underlying contiguous buffer. size_t _buf_size = 0; size_t _refcount = 0; // Used purely for anti-misuse checks. size_t _reallocs = 0; // Number of size changes. public: size_t size() const noexcept { return _buf_size; } size_t reallocs() const noexcept { return _reallocs; } protected: // The guard keeps a reference to the buffer, so it must have a stable address. reusable_buffer_impl(const reusable_buffer_impl&) = delete; reusable_buffer_impl& operator=(const reusable_buffer_impl&) = delete; reusable_buffer_impl() = default; ~reusable_buffer_impl() { assert(_refcount == 0); } void resize(size_t new_size) & { // Reusable buffers are expected to be used when large contiguous // allocations are unavoidable. There is no point in warning // about them since there isn't much that can be done. seastar::memory::scoped_large_allocation_warning_disable g; // Clear before shrinking so that the old buffer // is freed before allocating the new buffer. // This lesses the pressure on the allocator and should guarantee // a success if the new size is smaller than the old size. _buf = nullptr; _buf_size = 0; _reallocs += 1; _buf.reset(new bytes::value_type[new_size]); _buf_size = new_size; } // The below methods can be considered "public", but they are only accessible // through their guard wrappers, to make misuse harder. bytes_mutable_view get_temporary_buffer(size_t size) & { _high_watermark = std::max(_high_watermark, std::bit_ceil(size)); if (_high_watermark > _buf_size) { resize(_high_watermark); } return {_buf.get(), size}; } // The helpers below only interact with the rest of the class through // get_temporary_buffer(). // They could as well be free functions. // Returns a linearized view onto the provided data. // If the input view is already linearized, it is returned as-is. // Otherwise the contents are copied to the reusable buffer // and a view into the buffer is returned. bytes_view get_linearized_view(fragmented_temporary_buffer::view ftb) & { if (ftb.current_fragment().size() == ftb.size_bytes()) { return {ftb.current_fragment().data(), ftb.size_bytes()}; } const auto out = get_temporary_buffer(ftb.size_bytes()).data(); auto dst = out; for (bytes_view fragment : fragmented_temporary_buffer::view(ftb)) { dst = std::copy(fragment.begin(), fragment.end(), dst); } return {out, ftb.size_bytes()}; } // Returns a linearized view onto the provided data. // If the input view is already linearized, it is returned as-is. // Otherwise the contents are copied to the reusable buffer // and a view into the buffer is returned. bytes_view get_linearized_view(bytes_ostream& bo) & { if (bo.is_linearized()) { return bo.view(); } const auto out = get_temporary_buffer(bo.size()).data(); auto dst = out; for (bytes_view fragment : bo) { dst = std::copy(fragment.begin(), fragment.end(), dst); } return {out, bo.size()}; } // Provides a contiguous buffer of size `maximum_length` to `fn`. // `fn` writes to the buffer and returns the number of bytes written. // A fragmented buffer containing the data written by `fn` is returned. // // If the data fits into a single fragment, `fn` is ran directly on // the only fragment of a newly allocated fragmented_buffer. // Otherwise the contiguous buffer for `fn` is allocated from the reusable // buffer, and its contents are copied to a new fragmented buffer after `fn` // returns. // This way a copy is avoided in the small case. template requires std::is_invocable_r_v bytes_ostream make_bytes_ostream(size_t maximum_length, Function&& fn) & { bytes_ostream output; if (maximum_length && maximum_length <= bytes_ostream::max_chunk_size()) { auto ptr = output.write_place_holder(maximum_length); size_t actual_length = fn(bytes_mutable_view(ptr, maximum_length)); output.remove_suffix(output.size() - actual_length); } else { auto view = get_temporary_buffer(maximum_length); size_t actual_length = fn(view); output.write(bytes_view(view.data(), actual_length)); } return output; } // Provides a contiguous buffer of size `maximum_length` to `fn`. // `fn` writes to the buffer and returns the number of bytes written. // A fragmented buffer containing the data written by `fn` is returned. // // If the data fits into a single fragment, `fn` is ran directly on // the only fragment of a newly allocated fragmented_buffer. // Otherwise the contiguous buffer for `fn` is allocated from the reusable // buffer, and its contents are copied to a new fragmented buffer after `fn` // returns. // This way a copy is avoided in the small case. template requires std::is_invocable_r_v fragmented_temporary_buffer make_fragmented_temporary_buffer(size_t maximum_length, Function&& fn) & { if (maximum_length <= fragmented_temporary_buffer::default_fragment_size) { seastar::temporary_buffer buf(maximum_length); auto view = bytes_mutable_view(reinterpret_cast(buf.get_write()), buf.size()); size_t actual_length = fn(view); buf.trim(actual_length); std::vector> chunks; chunks.push_back(std::move(buf)); return fragmented_temporary_buffer(std::move(chunks), actual_length); } else { auto view = get_temporary_buffer(maximum_length); size_t actual_length = fn(view); return fragmented_temporary_buffer(reinterpret_cast(view.data()), actual_length); } } }; /* Sometimes (e.g. for compression), we need a big contiguous buffer * of a given size. * * Big buffers are a problem for the allocator, so if such a buffer * is needed regularly, we want to reuse it. In the optimum, we * only want to use one buffer per concurrent task (if all uses * of the buffer are synchronous, one buffer per shard is enough), * and only resize the buffer when it's too small for the current * request. * * At the same time we might not want to keep the buffer around forever. * A pathological request might cause the buffer to grow to some * unreasonable size. After the source of the pathology (e.g. bad compression * chunk size) is fixed, we want the buffer to free the capacity that will * most likely not be used again. * * This class provides a buffer which balances both tasks by periodically * shrinking its size to the largest recently seen size. * The buffer always has a power-of-2 size, to minimize the number of * allocations for small size changes. * * Even though the views returned by the buffer's methods have exactly the same * requested size, the entire underlying capacity (of size size()) is safe to use. * The returned views are always a prefix of the underlying capacity. * * Under a stable workload, the buffer's size will be stable. * If a pathological request comes, the buffer will grow to accommodate it. * 1-2 periods after the pathology ceases, the buffer will shrink back * to its steady-state size. * * Be very careful with this buffer. * Its content might disappear during a preemption point. This can be * checked by comparing reallocs() before and after the preemption point. * The content might be also easily invalidated by an unrelated future * sharing the same buffer, if you are not careful. * * To make this buffer slightly harder to misuse, it's only accessible * by reusable_buffer_guard. * There can only exist one guard at a time. * The guard can be used to obtain a view at most once. */ template class reusable_buffer : public reusable_buffer_impl { public: using period_type = typename Clock::duration; private: seastar::timer _decay_timer; period_type _decay_period; void decay() & { assert(_refcount == 0); if (_high_watermark <= _buf_size / 16) { // We shrink when the size falls at least by four power-of-2 // notches, instead of just one notch. This adds hysteresis: // it prevents small oscillations from inducing shrink/grow cycles. // With the factor of 16 above, only outliers at least 8x bigger than // the otherwise stable size might cause reallocations. // // In other words, we "define" a pathological request as one that is // at least 8x bigger than the stable size, where the stable size // is the max of all sizes seen within a decay period. resize(_high_watermark); } _high_watermark = 0; } public: reusable_buffer(period_type period) : _decay_period(period) { _decay_timer.set_callback([this] {decay();}); _decay_timer.arm_periodic(_decay_period); } }; /* Exists only to assert that there exists at most one reference to the * reusable_buffer, to hopefully make it less of a footgun. * * The reference/use counts exist only for assert purposes. * They don't influence the program otherwise. * * Never keep the guard across preemption points. * * Don't let views obtained through the guard outlive the guard, * that defeats its purpose and is begging for trouble. * * The guard only accesses _refcount and its "public" methods. * It doesn't mess with its internals. */ class reusable_buffer_guard { private: reusable_buffer_impl& _buf; bool used = false; private: void mark_used() { assert(!used); used = true; } public: reusable_buffer_guard(const reusable_buffer_guard&) = delete; reusable_buffer_guard& operator=(const reusable_buffer_guard&) = delete; reusable_buffer_guard(reusable_buffer_impl& _buf) : _buf(_buf) { assert(_buf._refcount == 0); _buf._refcount += 1; } ~reusable_buffer_guard() { _buf._refcount -= 1; } // The result mustn't outlive `this`. // No method of `this` may be called again. bytes_mutable_view get_temporary_buffer(size_t size) & { mark_used(); return _buf.get_temporary_buffer(size); } // The result mustn't outlive `this`. // No method of `this` may be called again. bytes_view get_linearized_view(fragmented_temporary_buffer::view ftb) & { mark_used(); return _buf.get_linearized_view(std::move(ftb)); } // The result mustn't outlive `this`. // No method of `this` may be called again. bytes_view get_linearized_view(bytes_ostream& bo) & { mark_used(); return _buf.get_linearized_view(bo); } // The result mustn't outlive `this`. // No method of `this` may be called again. template requires std::is_invocable_r_v bytes_ostream make_bytes_ostream(size_t maximum_length, Function&& fn) & { mark_used(); return _buf.make_bytes_ostream(maximum_length, std::forward(fn)); } // The result mustn't outlive `this`. // No method of `this` may be called again. template requires std::is_invocable_r_v fragmented_temporary_buffer make_fragmented_temporary_buffer(size_t maximum_length, Function&& fn) & { mark_used(); return _buf.make_fragmented_temporary_buffer(maximum_length, std::forward(fn)); } }; } // namespace utils