Allocation and copy accounting

#include <openpfc/kernel/diagnostics/memory_traffic.hpp> provides optional, caller-owned accounting of successful allocations and explicit host/device copies in namespace pfc::diagnostics. It answers questions such as “did a warmed step allocate scratch?” or “did this retry upload anything again?”. It does not measure cache, memory-bus or HBM traffic.

Observing a region

namespace dg = pfc::diagnostics;
dg::Traffic traffic;
{
  dg::Scope observing(&traffic); // current thread only
  stepper.step(t, u);
}
auto device = traffic.allocations(dg::Space::Device);
auto h2d = static_cast<std::size_t>(dg::Direction::HostToDevice);
std::uint64_t uploaded = traffic.copies.bytes[h2d];

Scope makes an observation dg::current for the calling thread and restores the previous one on exit, so scopes nest. Scope(nullptr) and PauseObservation disable observation, including an enclosing one, until they end. The observation must outlive every scope that names it.

Allocations

allocations(Space::Host) and allocations(Space::Device) return an AllocationTotals snapshot: successful request/free counts, requested/freed bytes, initial live bytes, current live bytes and the peak of simultaneously live requested payload bytes. A failed request is not an allocation. A runtime free failure (AllocationToken::failed_release()) leaves its payload live and sets complete=false, which stays set across interval resets.

A successful allocation returns a move-only AllocationToken that shares the ledger, so it may outlive the Traffic object. Release it only after the matching free succeeded. reset_interval() clears request, free and copy counters at a quiescent point and keeps all live allocations as the new initial and peak baseline; it never erases ownership.

GPU DataBuffer (CUDA and HIP) reports every device allocation, resize and free. Resize records the new allocation before freeing the old one, so their overlap appears in the peak. Buffers created outside an observation do not acquire tokens retroactively.

Host allocations are counted only when the consumer installs its own global new/delete provider that calls dg::successful_allocation(Space::Host, bytes) after each successful allocation, stores the token with it and calls release() after the actual free. The library never replaces the global allocation functions. The unit-test provider shows this ABI; link such a provider only into an isolated executable. It counts requested object bytes, not allocator headers, padding, malloc, runtime pools or RSS.

Copies

copies.calls[direction] and copies.bytes[direction], indexed by Direction (HostToHost, HostToDevice, DeviceToHost, DeviceToDevice), count successful explicit transfers recorded with dg::copy(direction, bytes). GPU DataBuffer::copy_from_host, copy_to_host and to_host record their uploads and downloads. Zero-length copies are not counted.

Users of the counters include the RK stage allocation test (tests/unit/kernel/simulation/steppers/test_rk_stage_allocations.cpp) and the resident GPU RK integration check (tests/integration/rk_coupled/).