dial9 0.5.0-rc2

Low-overhead async runtime telemetry: event recording, Tokio integration, CPU/memory profiling, and a trace viewer CLI
Documentation
#![cfg(feature = "memory-profiling")]
#![cfg(target_os = "linux")]
//! Test that MemoryProfileOverflowEvent is emitted when ring buffers overflow.

mod common;

use common::{CAPTURE_BUFFER_SIZE, capture_processor, decode_all};
use dial9::memory::{Dial9Allocator, MemoryProfiler, MemoryProfilingConfig};
use dial9::{Dial9HandleTokioExt, MemoryBuffer, RecorderPipelineExt, TokioAttachOptions, recorder};
use serde::Deserialize;
use std::time::Duration;

#[global_allocator]
static ALLOC: Dial9Allocator = Dial9Allocator::system();

#[derive(Debug, Deserialize)]
#[serde(tag = "event")]
enum OverflowEvent {
    MemoryProfileOverflowEvent {
        #[allow(dead_code)]
        timestamp_ns: u64,
        dropped_allocs: u64,
        dropped_frees: u64,
    },
    #[serde(other)]
    Other,
}

#[test]
fn overflow_event_emitted_when_ring_overflows() {
    let (capture, batches) = capture_processor();

    let recorder = recorder(MemoryBuffer::new(CAPTURE_BUFFER_SIZE).unwrap())
        .with_custom_pipeline(|p| p.pipe(capture))
        .build();
    let mut builder = tokio::runtime::Builder::new_multi_thread();
    builder.enable_all().worker_threads(1);
    let rt = recorder
        .handle()
        .attach_tokio_runtime(builder, TokioAttachOptions::default())
        .expect("attach tokio");

    let handle = recorder.handle().clone();
    // Use a tiny ring (capacity 4) so it overflows easily under allocation pressure.
    let _mem_guard = MemoryProfiler::from_config(
        MemoryProfilingConfig::builder()
            .sample_rate_bytes(1) // sample every allocation
            .ring_capacity(4)
            .rng_seed(42)
            .build(),
    )
    .install(handle)
    .expect("install should succeed");

    // Generate enough allocations to overflow the tiny ring.
    rt.block_on(async {
        for _ in 0..1000 {
            let v: Vec<u8> = vec![0u8; 64];
            std::hint::black_box(&v);
            drop(v);
        }
        // Wait for at least one flush cycle to pick up the overflow.
        tokio::time::sleep(Duration::from_millis(100)).await;
    });

    drop(rt);
    recorder.graceful_shutdown(Duration::from_secs(1));

    let batches = batches.lock().unwrap();
    let events: Vec<OverflowEvent> = decode_all(&batches);

    let overflows: Vec<_> = events
        .iter()
        .filter_map(|e| match e {
            OverflowEvent::MemoryProfileOverflowEvent {
                dropped_allocs,
                dropped_frees,
                ..
            } => Some((*dropped_allocs, *dropped_frees)),
            _ => None,
        })
        .collect();

    assert!(
        !overflows.is_empty(),
        "expected at least one MemoryProfileOverflowEvent"
    );

    let total_dropped_allocs: u64 = overflows.iter().map(|(a, _)| a).sum();
    let total_dropped_frees: u64 = overflows.iter().map(|(_, f)| f).sum();

    // With ring capacity 4 and 1000 allocations at sample_rate=1, we should
    // have many dropped samples.
    assert!(
        total_dropped_allocs > 0 || total_dropped_frees > 0,
        "expected non-zero drops, got allocs={total_dropped_allocs} frees={total_dropped_frees}"
    );
}