#pragma once #if !defined(MIJIN_LOGGING_BUFFER_SINK_HPP_INCLUDED) #define MIJIN_LOGGING_BUFFER_SINK_HPP_INCLUDED 1 #include #include #include #include #include #include "./logger.hpp" namespace mijin { inline constexpr std::size_t BUFFER_SINK_DEFAULT_SIZE = 10 * 1024 * 1024; // default 10 MiB buffer template> class BaseBufferSink; namespace impl { static constexpr std::uint32_t INVALID_BUFFER_INDEX = std::numeric_limits::max(); template struct BufferedMessageHeader { const BaseLogChannel* channel; const BaseLogLevel* level; std::source_location sourceLocation; std::uint32_t nextIdx = INVALID_BUFFER_INDEX; std::uint32_t prevIdx = INVALID_BUFFER_INDEX; std::uint32_t numChars = 0; }; template struct MessageBuffer { using char_t = TChar; using traits_t = TTraits; using message_t = BaseLogMessage; using header_t = BufferedMessageHeader; static constexpr std::size_t BUFFER_SIZE = VBufferSize; std::array bytes; mutable std::mutex mutex; [[nodiscard]] header_t& messageAt(std::uint32_t idx) MIJIN_NOEXCEPT { return *reinterpret_cast(bytes.data() + idx); } [[nodiscard]] std::span messageText(header_t& header) MIJIN_NOEXCEPT { return {reinterpret_cast(reinterpret_cast(&header)) + sizeof(header_t), header.numChars}; } [[nodiscard]] const header_t& messageAt(std::uint32_t idx) const MIJIN_NOEXCEPT { return *reinterpret_cast(bytes.data() + idx); } [[nodiscard]] std::span messageText(const header_t& header) const MIJIN_NOEXCEPT { return {reinterpret_cast(reinterpret_cast(&header)) + sizeof(header_t), header.numChars}; } static std::uint32_t messageBytes(std::uint32_t numChars) MIJIN_NOEXCEPT { return static_cast(sizeof(header_t)) + (numChars * static_cast(sizeof(char_t))); } }; template class BufferSinkRange; template class LockedMessageBuffer { public: using char_t = TChar; using traits_t = TTraits; static constexpr std::size_t BUFFER_SIZE = VBufferSize; using range_t = impl::BufferSinkRange; private: using buffer_t = MessageBuffer; const buffer_t* buffer_ = nullptr; std::uint32_t firstIdx_ = INVALID_BUFFER_INDEX; std::uint32_t lastIdx_ = INVALID_BUFFER_INDEX; LockedMessageBuffer(const buffer_t& buffer, std::uint32_t firstIdx, std::uint32_t lastIdx) MIJIN_NOEXCEPT : buffer_(&buffer), firstIdx_(firstIdx), lastIdx_(lastIdx) { buffer_->mutex.lock(); } public: LockedMessageBuffer() noexcept = default; LockedMessageBuffer(const LockedMessageBuffer&) = delete; LockedMessageBuffer(LockedMessageBuffer&& other) noexcept : buffer_(std::exchange(other.buffer_, nullptr)), firstIdx_(other.firstIdx_), lastIdx_(other.lastIdx_) {} ~LockedMessageBuffer() noexcept { reset(); } LockedMessageBuffer& operator=(const LockedMessageBuffer&) = delete; LockedMessageBuffer& operator=(LockedMessageBuffer&& other) noexcept { if (this != &other) { reset(); buffer_ = std::exchange(other.buffer_, nullptr); firstIdx_ = other.firstIdx_; lastIdx_ = other.lastIdx_; } return *this; } [[nodiscard]] range_t getMessages() const MIJIN_NOEXCEPT; private: void reset() { if (buffer_ != nullptr) { buffer_->mutex.unlock(); buffer_ = nullptr; } } template friend class mijin::BaseBufferSink; }; template class BufferSinkIterator { public: using char_t = TChar; using traits_t = TTraits; using message_t = BaseLogMessage; static constexpr std::size_t BUFFER_SIZE = VBufferSize; using difference_type = std::ptrdiff_t; using value_type = const message_t; using pointer = void; using reference = value_type; using iterator_category = std::bidirectional_iterator_tag; private: using buffer_t = MessageBuffer; using header_t = BufferedMessageHeader; using string_view_t = message_t::string_view_t; const buffer_t* buffer_ = nullptr; std::uint32_t idx_ = INVALID_BUFFER_INDEX; std::uint32_t prevIdx_ = INVALID_BUFFER_INDEX; BufferSinkIterator(const buffer_t& buffer, std::uint32_t idx, std::uint32_t prevIdx) MIJIN_NOEXCEPT : buffer_(&buffer), idx_(idx), prevIdx_(prevIdx) {} public: BufferSinkIterator() MIJIN_NOEXCEPT = default; BufferSinkIterator(const BufferSinkIterator&) MIJIN_NOEXCEPT = default; BufferSinkIterator& operator=(const BufferSinkIterator&) MIJIN_NOEXCEPT = default; auto operator<=>(const BufferSinkIterator&) const noexcept = default; reference operator*() const MIJIN_NOEXCEPT { MIJIN_ASSERT(idx_ != INVALID_BUFFER_INDEX, "Attempting to dereference an invalid iterator."); const header_t& header = buffer_->messageAt(idx_); const string_view_t text(buffer_->messageText(header)); return { .text = text, .channel = header.channel, .level = header.level, .sourceLocation = header.sourceLocation }; } BufferSinkIterator& operator++() MIJIN_NOEXCEPT { MIJIN_ASSERT(idx_ != INVALID_BUFFER_INDEX, "Attempting to increment an invalid iterator."); prevIdx_ = idx_; idx_ = buffer_->messageAt(idx_).nextIdx; return *this; } BufferSinkIterator operator++(int) MIJIN_NOEXCEPT { BufferSinkIterator copy(*this); operator++(); return copy; } BufferSinkIterator& operator--() MIJIN_NOEXCEPT { MIJIN_ASSERT(prevIdx_ != INVALID_BUFFER_INDEX, "Attempting to decrement an invalid iterator."); idx_ = prevIdx_; prevIdx_ = buffer_->messageAt(idx_).prevIdx; return *this; } BufferSinkIterator operator--(int) MIJIN_NOEXCEPT { BufferSinkIterator copy(*this); operator--(); return copy; } template friend class BufferSinkRange; }; static_assert(std::forward_iterator, BUFFER_SINK_DEFAULT_SIZE>>); template class BufferSinkRange { public: using char_t = TChar; using traits_t = TTraits; static constexpr std::size_t BUFFER_SIZE = VBufferSize; using iterator = BufferSinkIterator; using const_iterator = iterator; private: using buffer_t = MessageBuffer; const buffer_t* buffer_ = nullptr; std::uint32_t firstIdx_ = INVALID_BUFFER_INDEX; std::uint32_t lastIdx_ = INVALID_BUFFER_INDEX; BufferSinkRange(const buffer_t& buffer, std::uint32_t firstIdx, std::uint32_t lastIdx) MIJIN_NOEXCEPT : buffer_(&buffer), firstIdx_(firstIdx), lastIdx_(lastIdx) { } public: BufferSinkRange() noexcept = default; BufferSinkRange(const BufferSinkRange&) MIJIN_NOEXCEPT = default; BufferSinkRange& operator=(const BufferSinkRange&) MIJIN_NOEXCEPT = default; [[nodiscard]] iterator begin() const MIJIN_NOEXCEPT { return {*buffer_, firstIdx_, INVALID_BUFFER_INDEX}; } [[nodiscard]] iterator end() const MIJIN_NOEXCEPT { return {*buffer_, INVALID_BUFFER_INDEX, lastIdx_}; } [[nodiscard]] const_iterator cbegin() const MIJIN_NOEXCEPT { return {*buffer_, firstIdx_, INVALID_BUFFER_INDEX}; } [[nodiscard]] const_iterator cend() const MIJIN_NOEXCEPT { return {*buffer_, INVALID_BUFFER_INDEX, lastIdx_}; } friend class LockedMessageBuffer; }; static_assert(std::bidirectional_iterator, 100>>); static_assert(std::ranges::bidirectional_range, 100>>); template auto LockedMessageBuffer::getMessages() const MIJIN_NOEXCEPT -> range_t { return range_t(*buffer_, firstIdx_, lastIdx_); } } using impl::LockedMessageBuffer; template class BaseBufferSink : public BaseLogSink { public: using base_t = BaseLogSink; using typename base_t::char_t; using typename base_t::traits_t; using typename base_t::message_t; static constexpr std::size_t BUFFER_SIZE = VBufferSize; using locked_buffer_t = LockedMessageBuffer; private: using buffer_t = impl::MessageBuffer; using header_t = impl::BufferedMessageHeader; buffer_t buffer_; std::uint32_t firstIdx_ = impl::INVALID_BUFFER_INDEX; std::uint32_t lastIdx_ = impl::INVALID_BUFFER_INDEX; public: void handleMessage(const message_t& message) MIJIN_NOEXCEPT override { const std::uint32_t numChars = static_cast(message.text.size()); const std::uint32_t totalBytes = buffer_t::messageBytes(numChars); std::scoped_lock _(buffer_.mutex); // TODO: use a message queue and try_lock here if (lastIdx_ == impl::INVALID_BUFFER_INDEX) { // no message yet insertMessageAt(0, message); firstIdx_ = 0; return; } header_t& lastHeader = buffer_.messageAt(lastIdx_); const std::uint32_t newIdx = lastIdx_ + buffer_t::messageBytes(lastHeader.numChars); if (newIdx + totalBytes < BUFFER_SIZE) { // enough space in the buffer, can append insertMessageAt(newIdx, message); } else { // not enough space, put at front insertMessageAt(0, message); } } [[nodiscard]] locked_buffer_t lockBuffer() const MIJIN_NOEXCEPT { return locked_buffer_t(buffer_, firstIdx_, lastIdx_); } private: void insertMessageAt(std::uint32_t idx, const message_t& message) { const std::uint32_t numChars = static_cast(message.text.size()); freeSpace(idx, idx + buffer_t::messageBytes(numChars)); if (lastIdx_ != impl::INVALID_BUFFER_INDEX) { buffer_.messageAt(lastIdx_).nextIdx = idx; } header_t& newHeader = buffer_.messageAt(idx); ::new (&newHeader) header_t({ .channel = message.channel, .level = message.level, .sourceLocation = message.sourceLocation, .nextIdx = impl::INVALID_BUFFER_INDEX, .prevIdx = lastIdx_, .numChars = numChars }); lastIdx_ = idx; std::ranges::copy(message.text, buffer_.messageText(newHeader).begin()); } void freeSpace(std::uint32_t startIdx, std::uint32_t endIdx) { while (firstIdx_ != impl::INVALID_BUFFER_INDEX && firstIdx_ >= startIdx && firstIdx_ < endIdx) { header_t& message = buffer_.messageAt(firstIdx_); firstIdx_ = message.nextIdx; message.~header_t(); } // cleared everything? if (firstIdx_ == impl::INVALID_BUFFER_INDEX) { lastIdx_ = impl::INVALID_BUFFER_INDEX; } else { buffer_.messageAt(firstIdx_).prevIdx = impl::INVALID_BUFFER_INDEX; } } }; #define SINK_COMMON_ARGS(chr_type) std::size_t BUFFER_SIZE #define SINK_SET_ARGS(chr_type) BUFFER_SIZE, chr_type MIJIN_DEFINE_CHAR_VERSIONS_TMPL(BufferSink, SINK_COMMON_ARGS, SINK_SET_ARGS) #undef SINK_COMMON_ARGS #undef SINK_SET_ARGS } // namespace mijin #endif // !defined(MIJIN_LOGGING_STDIO_SINK_HPP_INCLUDED)