Problem Description / Motivation
In docs/decisions.md and docs/architecture.md, the rationale for telemetry snapshot publication states:
"Reader lifetime is the hard part of replacing a mutex-protected snapshot. Shared ownership supplies a clear reclamation rule. The actual atomic implementation can use a lock internally. Diagnostics expose this; benchmarks decide whether a more specialized scheme is worth its maintenance cost."
"A custom reclamation scheme would need a measured reason and a separate correctness argument."
With the formal inclusion of Hazard Pointers into the ISO C++26 Standard Library (<hazard_pointer>, P2530) and the battle-tested production evidence from Meta's Folly Hazard Pointers (folly::hazptr), both the measured reason and the formal correctness argument are now established for web_htop:
- The Measured Reason:
std::atomic<std::shared_ptr<T>> in libstdc++ relies on an internal table of 16 spinlocks (_Sp_locker), causing hidden lock contention, priority inversion vulnerability, and reporting is_lock_free() == false.
- Releasing the last
shared_ptr on the Reactor thread forces the epoll event loop to synchronously destroy large snapshot object graphs (vector<ProcessInfo>, multiple maps, three pre-encoded JSON strings), causing millisecond tail-latency spikes on active TCP connections.
- Atomic reference counting incurs frequent
lock xadd cache-line bouncing between reader and writer CPU cores.
- The Correctness Argument:
- Hazard Pointers provide wait-free read-side protection (
hp.protect()) without modifying the protected object's memory or cache lines.
- Safe Memory Reclamation (SMR) ensures that retired snapshot objects are never freed while any reader holds a reference, yet the actual deallocation is deferred and executed exclusively on the writer thread (the sampling worker), completely shielding the
Reactor event loop from memory destruction latency.
Technical Specification: C++26 <hazard_pointer> / Folly Hazptr Pattern
In modern C++26 concurrency (ISO/IEC 14882:2026 <hazard_pointer>), the Safe Memory Reclamation pattern is structured around three primitives:
std::hazard_pointer_obj_base<T>: Base class for protectable heap objects providing void retire().
std::hazard_pointer: RAII holder owned by a reader thread.
template<class T> T* hazard_pointer::protect(const std::atomic<T*>& src) noexcept: Atomically observes the pointer and advertises it in the thread's hazard slot, preventing reclamation during access.
Target Architecture for web_htop:
[ Metrics Collector Worker ]
│
1. Allocate & Encode
│
2. Publish (Atomic Exchange)
│
old = active_.exchange(next, std::memory_order_acq_rel)
│
3. old->retire()
(Worker reclaims expired generations;
Reactor NEVER calls free!)
│
▼
┌──────────────────────────────┐
│ std::atomic<Snapshot const*> │
└──────────────┬───────────────┘
│
│ 4. hp.protect(active_)
│ (Wait-free read, 0 atomic refcounts,
│ 0 cache-line invalidations)
▼
[ Epoll Reactor Thread ]
Snapshot Definition:
#if defined(__cpp_lib_hazard_pointer) && __cpp_lib_hazard_pointer >= 202306L
#include <hazard_pointer>
namespace smr = std;
#else
#include "common/concurrency/hazard_pointer.hpp"
namespace smr = web_htop::concurrency;
#endif
namespace web_htop::server
{
struct PublishedSnapshot : smr::hazard_pointer_obj_base<PublishedSnapshot>
{
models::SystemSnapshot snapshot;
std::shared_ptr<std::string const> json, frame, processes_json;
std::chrono::steady_clock::time_point published_at;
std::uint64_t encode_us{};
};
}
Writer Publication (Publish):
void SharedState::Publish(models::SystemSnapshot snapshot)
{
auto next = std::make_unique<PublishedSnapshot>();
// ... encode json, frame, processes_json ...
auto* old = active_.exchange(next.release(), std::memory_order_acq_rel);
if (old)
{
old->retire(); // Deallocation happens safely on the worker thread!
}
}
Reader Observation (Load):
class SnapshotGuard
{
public:
SnapshotGuard(smr::hazard_pointer&& hp, PublishedSnapshot const* ptr) noexcept
: hp_(std::move(hp)), ptr_(ptr) {}
PublishedSnapshot const* operator->() const noexcept { return ptr_; }
PublishedSnapshot const& operator*() const noexcept { return *ptr_; }
explicit operator bool() const noexcept { return ptr_ != nullptr; }
private:
smr::hazard_pointer hp_;
PublishedSnapshot const* ptr_;
};
SnapshotGuard SharedState::Load() const noexcept
{
auto hp = smr::make_hazard_pointer();
PublishedSnapshot const* ptr = hp.protect(active_);
return SnapshotGuard(std::move(hp), ptr);
}
Step-by-Step Implementation Strategy
- Lightweight, Zero-Dependency C++20 Hazard Pointer Header:
- Provide
common/include/common/concurrency/hazard_pointer.hpp implementing the standard C++26 <hazard_pointer> interface (~120 lines of clean C++20):
- Thread-local hazard pointer registry (fixed small array or lock-free linked list).
- Worker-side retire list with batch reclamation.
- Automatic forwarding to
std::hazard_pointer when compiled with C++26 toolchains (__cpp_lib_hazard_pointer >= 202306L).
- Encapsulated
SharedState Adaptation:
- Inherit
PublishedSnapshot from smr::hazard_pointer_obj_base<PublishedSnapshot>.
- Update
SharedState::Publish() to exchange() and retire().
- Update
SharedState::Load() to return SnapshotGuard (compatible with operator->).
- Set
IsLockFree() to return active_.is_lock_free() (guaranteed true on 64-bit architectures).
- Verification:
- ThreadSanitizer verification under heavy concurrent read/write stress.
- Verify zero allocations and zero deallocations on the
Reactor thread during publication cycles.
Key Benefits
- Zero Reactor Deallocation Latency: The epoll reactor thread is 100% freed from object destruction overhead; all garbage collection / retirement runs on the worker thread.
- True Lock-Free Telemetry Publication: Eliminates the 16 global spinlocks of
std::atomic<std::shared_ptr>; snapshot_store_lock_free becomes true.
- Elimination of Atomic Refcount Bouncing: Reader acquires snapshot via relaxed store to thread-local slot + atomic acquire load, leaving the shared cache line in Shared (S) state instead of Modified (M).
- C++26 Standards Compliance: Adopts the exact idioms of ISO C++26 P2530 and Meta's Folly Hazptr with 100% backward compatibility with C++20.
Acceptance Criteria
Problem Description / Motivation
In
docs/decisions.mdanddocs/architecture.md, the rationale for telemetry snapshot publication states:With the formal inclusion of Hazard Pointers into the ISO C++26 Standard Library (
<hazard_pointer>, P2530) and the battle-tested production evidence from Meta's Folly Hazard Pointers (folly::hazptr), both the measured reason and the formal correctness argument are now established forweb_htop:std::atomic<std::shared_ptr<T>>inlibstdc++relies on an internal table of 16 spinlocks (_Sp_locker), causing hidden lock contention, priority inversion vulnerability, and reportingis_lock_free() == false.shared_ptron theReactorthread forces the epoll event loop to synchronously destroy large snapshot object graphs (vector<ProcessInfo>, multiple maps, three pre-encoded JSON strings), causing millisecond tail-latency spikes on active TCP connections.lock xaddcache-line bouncing between reader and writer CPU cores.hp.protect()) without modifying the protected object's memory or cache lines.Reactorevent loop from memory destruction latency.Technical Specification: C++26
<hazard_pointer>/ Folly Hazptr PatternIn modern C++26 concurrency (ISO/IEC 14882:2026
<hazard_pointer>), the Safe Memory Reclamation pattern is structured around three primitives:std::hazard_pointer_obj_base<T>: Base class for protectable heap objects providingvoid retire().std::hazard_pointer: RAII holder owned by a reader thread.template<class T> T* hazard_pointer::protect(const std::atomic<T*>& src) noexcept: Atomically observes the pointer and advertises it in the thread's hazard slot, preventing reclamation during access.Target Architecture for
web_htop:Snapshot Definition:
Writer Publication (
Publish):Reader Observation (
Load):Step-by-Step Implementation Strategy
common/include/common/concurrency/hazard_pointer.hppimplementing the standard C++26<hazard_pointer>interface (~120 lines of clean C++20):std::hazard_pointerwhen compiled with C++26 toolchains (__cpp_lib_hazard_pointer >= 202306L).SharedStateAdaptation:PublishedSnapshotfromsmr::hazard_pointer_obj_base<PublishedSnapshot>.SharedState::Publish()toexchange()andretire().SharedState::Load()to returnSnapshotGuard(compatible withoperator->).IsLockFree()to returnactive_.is_lock_free()(guaranteedtrueon 64-bit architectures).Reactorthread during publication cycles.Key Benefits
std::atomic<std::shared_ptr>;snapshot_store_lock_freebecomestrue.Acceptance Criteria
smr::hazard_pointercompatible with C++26<hazard_pointer>API.SharedState::IsLockFree()returnstrueon x86_64 and arm64.free()ordeletein theReactorthread during snapshot publication under high load.-fsanitize=thread).SharedStateand the new concurrency header, preserving modular atomicity.