blob: 4196d9fa70495ce6359daa98f62900db73e25601 [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ZIRCON_SYSTEM_ULIB_TRACE_ENGINE_ROLLING_BUFFER_H_
#define ZIRCON_SYSTEM_ULIB_TRACE_ENGINE_ROLLING_BUFFER_H_
/// This class provides a view into a buffer that implements a lockless multi-writer single reader
/// specialized for trace-engine.
///
/// Several design choices made here were done so to maintain compatibility with the previous
/// algorithm and current apis exposed by trace-engine. As such, this buffer may not be an ideal
/// abstraction for other use cases. Such decisions include:
///
/// - Not providing a means to commit writes: the caller must externally manage writes in flights to
/// correctly read and service the buffers.
/// - Double buffering rather than concurrent reads/writes.
/// - Exposing buffer swap functionality to the caller. Writers are responsible for ensuring that
/// buffers are correctly serviced when notified.
///
/// See the file comments in //zircon/system/ulib/trace-engine/rolling_buffer.cc for details on
/// operation. See //zircon/system/ulib/trace-engine/test/rolling_buffer_test.cc for examples on
/// usage.
#include <atomic>
#include <span>
#include <variant>
enum class BufferNumber : uint8_t {
kZero = 0,
kOne = 1,
};
/// The buffer maintains state using a single uint64_t atomic.
///
/// The state is split using the following bit fields:
///
/// Buffer Status, 2 bits, 0xC000'0000'0000'0000:
/// Indicates if one of the double buffers is full and requires servicing.
/// Buffer 0 is indicated by the MSB.
///
/// Wrapped Counter, 30 bits, 0x3FFF'FFFF'0000'0000:
/// Tracks the number of buffer swaps we have completed this session. Serves as an indicator of
/// which buffer is the active (writing) buffer.
///
/// Offset, 32 bits, 0x0000'0000'FFFF'FFFF:
/// Tracks the offset of the allocation pointer into the current buffer.
///
/// This class serves as an abstraction over reading and writing theses fields.
class RollingBufferState {
public:
RollingBufferState() = default;
explicit RollingBufferState(uint64_t state) : state_(state) {}
RollingBufferState(uint8_t buffer_status, uint32_t offset, uint32_t counter)
: state_(uint64_t{offset} | (static_cast<uint64_t>(counter) << kWrappedCounterShift) |
(static_cast<uint64_t>(buffer_status) << kBufferStatusShift)) {}
bool operator<=>(const RollingBufferState&) const = default;
uint32_t GetBufferOffset() const {
return static_cast<uint32_t>((state_ & kOffsetMask) >> kOffsetShift);
}
bool IsBufferFilled(BufferNumber buffer_number) const {
uint64_t buffer_to_check = buffer_number == BufferNumber::kZero ? 0b01 : 0b10;
return (state_ & (buffer_to_check << kBufferStatusShift)) != 0;
}
[[nodiscard]] // The returns a new state, it doesn't modify the existing one.
RollingBufferState WithBufferMarkedFilled(BufferNumber buffer_number) const {
const uint8_t desired_set_buffer = buffer_number == BufferNumber::kZero ? 0b01 : 0b10;
return RollingBufferState{state_ | (uint64_t{desired_set_buffer} << kBufferStatusShift)};
}
[[nodiscard]] // The returns a new state, it doesn't modify the existing one.
RollingBufferState WithBufferMarkedServiced(BufferNumber buffer_number) const {
const uint8_t desired_unset_buffer = buffer_number == BufferNumber::kZero ? 0b01 : 0b10;
return RollingBufferState{state_ & ~(uint64_t{desired_unset_buffer} << kBufferStatusShift)};
}
[[nodiscard]] // The returns a new state, it doesn't modify the existing one.
RollingBufferState WithAllocatedBytes(uint32_t num_bytes) const {
return RollingBufferState{state_ + num_bytes};
}
[[nodiscard]] // The returns a new state, it doesn't modify the existing one.
RollingBufferState WithOffset(uint32_t new_offset) const {
uint64_t masked_state = state_ & ~kOffsetMask;
return RollingBufferState{masked_state | (new_offset << kOffsetShift)};
}
[[nodiscard]] // The returns a new state, it doesn't modify the existing one.
RollingBufferState WithNextWrappedCount() const {
uint64_t wrapped_increment = uint64_t{1} << kWrappedCounterShift;
return RollingBufferState{state_ + wrapped_increment};
}
uint32_t GetWrappedCount() const {
return static_cast<uint32_t>((state_ & kWrappedCounterMask) >> kWrappedCounterShift);
}
uint8_t GetBufferStatus() const {
return static_cast<uint8_t>((state_ & kBufferStatusMask) >> kBufferStatusShift);
}
static BufferNumber ToBufferNumber(uint32_t wrapped_count) {
return static_cast<BufferNumber>(wrapped_count & 1);
}
BufferNumber GetBufferNumber() const { return ToBufferNumber(GetWrappedCount()); }
private:
// We use two bits to mark that a given rolling buffer is full.
static constexpr size_t kBufferStatusShift = 62;
static constexpr uint64_t kBufferStatusMask = 0xC000'0000'0000'0000;
static constexpr size_t kWrappedCounterShift = 32;
static constexpr uint64_t kWrappedCounterMask = 0x3FFF'FFFF'0000'0000;
static constexpr size_t kOffsetShift = 0;
static constexpr uint64_t kOffsetMask = 0x0000'0000'FFFF'FFFF;
uint64_t state_ = 0;
};
struct BufferFull {};
struct SwitchingAllocation {
// The state the buffer is in when the write failed.
uint64_t* ptr;
RollingBufferState prev_state;
};
struct Allocation {
// A pointer to the allocated memory.
uint64_t* ptr;
};
using AllocationResult = std::variant<Allocation, BufferFull, SwitchingAllocation>;
template <class... Ts>
struct AllocationVisitor : Ts... {
using Ts::operator()...;
};
class RollingBuffer {
public:
void SetOneshotBuffer(std::span<uint8_t> buffer) {
rolling_buffers_[0] = buffer;
rolling_buffers_[1] = std::span<uint8_t>{};
}
void SetDoubleBuffers(std::span<uint8_t> buffer0, std::span<uint8_t> buffer1) {
rolling_buffers_[0] = buffer0;
rolling_buffers_[1] = buffer1;
}
// Allocate data from the configured buffer. Thread safe.
//
// When we allocate data, we may get:
// 1) A successful Allocation. We may write our data into the provided pointer.
// 2) A successful SwitchingAllocation. We may write out data into the provided pointer. However,
// this also contains a notification that the previous buffer was full and requires servicing,
// as well as includes the state of the buffer directly before writing the record. This is
// guaranteed to be returned only once per wrap count.
// SAFETY: This notification means that the buffer has filled, but there may still be active
// writers in the buffer. The receiver is responsible for ensuring that all writes are complete
// before attempting to read or service the buffer.
// 3) A failure with BufferFull. There's no room in either buffer, and we're waiting for someone
// to service the buffers so there's nothing to do.
AllocationResult AllocRecord(size_t num_bytes);
AllocationResult Flush();
// Return the total number of allocated bytes in the current buffer.
size_t BytesAllocated() const;
// Set the allocation pointer back to the beginning of the buffer and consider it to be empty.
void Reset();
// Get the per-buffer size of the rolling buffers
size_t BufferSize(BufferNumber idx) const {
return rolling_buffers_[static_cast<uint8_t>(idx)].size_bytes();
}
// Get the total size of all the rolling buffers
size_t MaxBufferSize() const {
return rolling_buffers_[0].size_bytes() + rolling_buffers_[1].size_bytes();
}
RollingBufferState State() const { return state_.load(std::memory_order_relaxed); }
// The maximum rolling buffer size in bits.
static constexpr size_t kRollingBufferSizeBits = 32;
// Maximum size, in bytes, of a rolling buffer.
static constexpr size_t kMaxRollingBufferSize = 1ull << kRollingBufferSizeBits;
/// Mark that we are done with this buffer, freeing the space for more writes.
/// SAFETY: Caller is responsible for ensuring that there are no remaining active writers in this
/// buffer before calling this.
bool SetBufferServiced(uint32_t wrapped_count);
// Mark the currently set buffer as full.
void SetBufferFull();
private:
// Our rolling double buffers.
std::span<uint8_t> rolling_buffers_[2];
// The underlying atomic state of our wrapped count, buffer service status, and allocation
// pointer.
std::atomic<RollingBufferState> state_;
static_assert(std::atomic<RollingBufferState>::is_always_lock_free,
"RollingBufferState doesn't fit into a supported atomic size");
};
#endif // ZIRCON_SYSTEM_ULIB_TRACE_ENGINE_ROLLING_BUFFER_H_