Skip to content

Thread-safe data extraction via shared memory and io_uring #1

Description

@fior512

Motivation

The current Snapshot() method is the only way to extract raw samples out of Latte's ring buffers. It has two fundamental problems:

  • Slow: it does a full linear copy under a lock, blocking the measurement thread for the entire duration of the copy.
  • Limiting: it returns a single std::vector<uint64_t> with no thread identity, no ID label, and no timestamp; callers have no way to correlate samples back to their source thread or measurement ID.

A proper extraction path should be lock-free, non-blocking on the hot path, and expose enough metadata to reconstruct a complete picture of per-thread, per-ID latency distributions.

Technical Approach

Shared memory ring buffer

Each per-thread, per-ID buffer is already a power-of-two ring. Exposing the full N×M buffer space through a shm_open / mmap region lets an out-of-process consumer (a sidecar, a dashboard agent) read samples without ever touching a measurement thread.

The region is laid out as a fixed directory header followed by a flat array of independent slots; one slot per (thread_index, id_index) pair. Each slot is strictly SPSC: exactly one capture thread writes it, any number of consumers read it.

struct alignas(64) LatteShmSlotHeader {
    std::atomic<uint64_t> write_idx;  // updated after every Stop()
    uint32_t              thread_idx;
    uint32_t              id_token;   // intern token, not a pointer
    uint64_t              pad[5];     // fill cache line
};

struct LatteShmSlot {
    LatteShmSlotHeader header;
    uint64_t           samples[MAX_SAMPLES];  // power-of-two, wait-free reads
};

struct LatteShmRegion {
    // directory (first page, read-only after InitSharedRegion)
    uint32_t n_threads;
    uint32_t n_ids;
    uint64_t pad[7];
    char     id_table[MAX_IDS][32];   // token -> null-terminated name

    // slots [thread_idx][id_idx]
   LatteShmSlot slots[MAX_THREADS][MAX_IDS];
};

The producer advances header.write_idx with store(, memory_order_release) after writing the sample. The consumer reads it with load(memory_order_acquire) and indexes into slot.samples[]; no mutex, no CAS loop, no stall on the hot path. The 64-byte alignment on LatteShmSlotHeader ensures write_idx never shares a cache line with samples[0], eliminating false sharing between the producer and consumer.

The total region size is fixed at sizeof(LatteShmRegion) and passed to ftruncate inside InitSharedRegion. This is consistent with Latte's existing policy of preallocating for MAX_SAMPLES per buffer at startup.

const char* ID problem

String IDs are passed as raw pointers. Sharing them across process boundaries or caching them in the shm region is unsafe; the pointer is meaningless in another address space. Solution: intern IDs into a fixed-size string table at registration time:

// Intern on first use, O(1) lookup thereafter
uint32_t Latte::InternID(const char* id);   // returns stable 32-bit token
// Shm header carries a compact table: token -> null-terminated name[32]

io_uring for async flushing

When a consumer wants to persist samples to disk (e.g. for post-run analysis) without blocking, io_uring provides true async writes with no thread overhead:

// Optional, only compiled when LATTE_URING is defined
void Latte::FlushAsync(int fd, Parameter unit = Parameter::Time);
// Submits a chain of io_uring write SQEs; returns immediately.
// Completion is checked via Latte::PollFlush() or a CQE wait.

This avoids the write() syscall on the measurement thread entirely and is compatible with the shared-memory layout (the shm region is the source buffer for the SQE).

Acceptance Criteria

  • InitSharedRegion(shm_name) creates and maps a POSIX shm object sized for all registered IDs
  • write_idx updated with memory_order_release; consumer reads with memory_order_acquire
  • ID interning table stored in the shm header; max 256 IDs, 32 chars each
  • const char* IDs never dereferenced outside the registering process
  • FlushAsync(fd) implemented behind #ifdef LATTE_URING using liburing; no-op otherwise
  • LATTE_DISABLE stubs all new entry points to no-ops
  • No regression in speedtest.cpp hot-path latency at default config
  • README section documenting the shm consumer API and the io_uring opt-in

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions