ElixirScope.Capture.RingBuffer (elixir_scope v0.0.1)

High-performance lock-free ring buffer for event ingestion.

Uses :atomics for lock-free operations and :persistent_term for metadata storage. Designed for >100k events/sec throughput with bounded memory usage.

Key features:

  • Lock-free writes using atomic compare-and-swap
  • Bounded memory with configurable overflow behavior
  • Multiple reader support with position tracking
  • Minimal allocation overhead
  • Graceful degradation under extreme load

Summary

Functions

Clears all events from the buffer and resets counters.

Destroys the buffer and cleans up resources.

Creates a new ring buffer with the specified configuration.

Reads the next available event from the buffer.

Reads multiple events in batch for better throughput.

Returns the buffer size.

Gets current buffer statistics.

Writes an event to the ring buffer.

Types

buffer_id()

@type buffer_id() :: atom()

overflow_strategy()

@type overflow_strategy() :: :drop_oldest | :drop_newest | :block

position()

@type position() :: non_neg_integer()

t()

@type t() :: %ElixirScope.Capture.RingBuffer{
  atomics_ref: :atomics.atomics_ref(),
  buffer_table: :ets.tab(),
  id: buffer_id(),
  mask: non_neg_integer(),
  overflow_strategy: overflow_strategy(),
  size: pos_integer()
}

Functions

clear(buffer)

@spec clear(t()) :: :ok

Clears all events from the buffer and resets counters.

destroy(buffer)

@spec destroy(t()) :: :ok

Destroys the buffer and cleans up resources.

new(opts \\ [])

@spec new(keyword()) :: {:ok, t()} | {:error, term()}

Creates a new ring buffer with the specified configuration.

Options

  • :size - Buffer size (must be power of 2, default: 1024)
  • :overflow_strategy - What to do when buffer is full (default: drop_oldest)
  • :name - Optional name for the buffer (default: generates unique name)

Examples

iex> {:ok, buffer} = RingBuffer.new(size: 1024)
iex> RingBuffer.size(buffer)
1024

read(buffer, read_position \\ 0)

@spec read(t(), position()) :: {:ok, ElixirScope.Events.event(), position()} | :empty

Reads the next available event from the buffer.

Returns {:ok, event, new_position} or :empty if no events available.

read_batch(buffer, start_position, count)

@spec read_batch(t(), position(), pos_integer()) ::
  {[ElixirScope.Events.event()], position()}

Reads multiple events in batch for better throughput.

Returns {events, new_position} where events is a list of up to count events.

size(ring_buffer)

@spec size(t()) :: pos_integer()

Returns the buffer size.

stats(buffer)

@spec stats(t()) :: %{
  size: pos_integer(),
  write_position: position(),
  read_position: position(),
  available_events: non_neg_integer(),
  total_writes: non_neg_integer(),
  total_reads: non_neg_integer(),
  dropped_events: non_neg_integer(),
  utilization: float()
}

Gets current buffer statistics.

write(buffer, event)

@spec write(t(), ElixirScope.Events.event()) :: :ok | {:error, :buffer_full}

Writes an event to the ring buffer.

This is the critical hot path - optimized for minimal latency. Target: <1µs per write operation.

Examples

iex> {:ok, buffer} = RingBuffer.new()
iex> event = %Events.FunctionExecution{function: :test}
iex> :ok = RingBuffer.write(buffer, event)