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
Motivation
The current
Snapshot()method is the only way to extract raw samples out of Latte's ring buffers. It has two fundamental problems: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/mmapregion 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.
The producer advances
header.write_idxwithstore(, memory_order_release)after writing the sample. The consumer reads it withload(memory_order_acquire)and indexes intoslot.samples[]; no mutex, no CAS loop, no stall on the hot path. The 64-byte alignment onLatteShmSlotHeaderensureswrite_idxnever shares a cache line withsamples[0], eliminating false sharing between the producer and consumer.The total region size is fixed at
sizeof(LatteShmRegion)and passed toftruncateinsideInitSharedRegion. This is consistent with Latte's existing policy of preallocating forMAX_SAMPLESper buffer at startup.const char*ID problemString 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:
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:
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 IDswrite_idxupdated withmemory_order_release; consumer reads withmemory_order_acquireconst char*IDs never dereferenced outside the registering processFlushAsync(fd)implemented behind#ifdef LATTE_URINGusingliburing; no-op otherwiseLATTE_DISABLEstubs all new entry points to no-opsspeedtest.cpphot-path latency at default config