use crate::{
cpu,
event::Event,
record::{ID_OFFSET, TS_OFFSET},
ring::Ring,
wire::DumpWriter,
};
use alloc::{boxed::Box, vec, vec::Vec};
use core::{
cell::Cell,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
};
fn mix64(mut x: u64) -> u64 {
x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
x ^ (x >> 31)
}
fn gen_instance_id() -> u64 {
use std::hash::{BuildHasher, Hasher};
let make = || {
std::collections::hash_map::RandomState::new()
.build_hasher()
.finish()
};
mix64(make() ^ mix64(make()))
}
fn gen_thread_seed() -> u16 {
static SRC: AtomicU64 = AtomicU64::new(0);
let n = SRC.fetch_add(1, Ordering::Relaxed);
(mix64(n.wrapping_add(0x9e37_79b9_7f4a_7c15)) & 0xFFFF) as u16
}
std::thread_local! {
static SPAN_SEED: u16 = gen_thread_seed();
static SPAN_COUNTER: Cell<u64> = const { Cell::new(0) };
}
pub trait Clock: Send + Sync + 'static {
fn now_nanos(&self) -> u64;
}
impl<C: Clock> Clock for alloc::sync::Arc<C> {
#[inline]
fn now_nanos(&self) -> u64 {
(**self).now_nanos()
}
}
#[cfg(feature = "std")]
pub struct SystemClock {
base_nanos: u64,
start: std::time::Instant,
}
#[cfg(feature = "std")]
impl SystemClock {
pub fn new() -> Self {
let base_nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
Self {
base_nanos,
start: std::time::Instant::now(),
}
}
}
#[cfg(feature = "std")]
impl Default for SystemClock {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "std")]
impl Clock for SystemClock {
#[inline]
fn now_nanos(&self) -> u64 {
#[cfg(feature = "bach")]
if let Some(now) = bach::time::Instant::try_now() {
return now.elapsed_since_start().as_nanos() as u64;
}
self.base_nanos
.wrapping_add(self.start.elapsed().as_nanos() as u64)
}
}
pub struct ManualClock(AtomicU64);
impl ManualClock {
pub fn new(start: u64) -> Self {
Self(AtomicU64::new(start))
}
pub fn advance(&self, delta: u64) -> u64 {
self.0.fetch_add(delta, Ordering::Relaxed) + delta
}
}
impl Clock for ManualClock {
#[inline]
fn now_nanos(&self) -> u64 {
self.0.load(Ordering::Relaxed)
}
}
pub struct Recorder<C: Clock = SystemClock> {
enabled: AtomicBool,
shards: Box<[Ring]>,
clock: C,
instance_id: u64,
}
impl Recorder {
pub fn new(num_shards: usize, bytes_per_shard: usize) -> Self {
assert!(num_shards > 0, "num_shards must be non-zero");
let shards = (0..num_shards)
.map(|_| Ring::new(bytes_per_shard))
.collect::<Vec<_>>()
.into_boxed_slice();
let clock = SystemClock::new();
Self {
enabled: AtomicBool::new(false),
shards,
clock,
instance_id: gen_instance_id(),
}
}
}
impl<C: Clock> Recorder<C> {
pub fn with_clock<NewClock: Clock>(self, clock: NewClock) -> Recorder<NewClock> {
Recorder {
enabled: self.enabled,
shards: self.shards,
clock,
instance_id: self.instance_id,
}
}
pub fn set_enabled(&self, on: bool) {
self.enabled.store(on, Ordering::Relaxed);
}
#[inline(always)]
pub fn is_enabled(&self) -> bool {
#[cfg(feature = "capture")]
{
self.enabled.load(Ordering::Relaxed)
}
#[cfg(not(feature = "capture"))]
{
let _ = &self.enabled;
false
}
}
pub fn num_shards(&self) -> usize {
self.shards.len()
}
#[inline]
pub fn record<E: Event>(&self, event: &E) {
if !self.is_enabled() {
return;
}
let shard = cpu::current_hint(self.shards.len());
let ts = self.clock.now_nanos().to_le_bytes();
let id = E::ID.get().to_le_bytes();
debug_assert_eq!(TS_OFFSET, 0);
debug_assert_eq!(ID_OFFSET, ts.len());
self.shards[shard].push_parts(&[&ts, &id, event.as_bytes()]);
}
pub fn instance_id(&self) -> u64 {
self.instance_id
}
pub fn new_span_id(&self) -> u64 {
let seed = SPAN_SEED.with(|s| *s) as u64;
let counter = SPAN_COUNTER.with(|c| {
let next = c.get().wrapping_add(1);
c.set(next);
next
});
(counter << 16) | seed
}
pub fn enter<En, Ex, MkEx>(
&self,
make_enter: impl FnOnce(u64) -> En,
make_exit: MkEx,
) -> SpanGuard<'_, C, Ex, MkEx>
where
En: Event,
Ex: Event,
MkEx: FnOnce(u64) -> Ex,
{
let id = self.new_span_id();
if self.is_enabled() {
self.record(&make_enter(id));
}
SpanGuard {
recorder: self,
id,
make_exit: Some(make_exit),
}
}
pub fn dump<'a>(
&self,
schemas: impl IntoIterator<Item = &'a crate::schema::EventSchema>,
intern: impl IntoIterator<Item = (u32, &'a [u8])>,
views: impl IntoIterator<Item = &'a str>,
host: &str,
) -> Vec<u8> {
let mut w = DumpWriter::new();
w.schema_registry(schemas);
w.intern_table(self.instance_id, intern);
for sql in views {
w.views(sql);
}
let mut region = vec![0u8; self.shards.first().map_or(0, |r| r.capacity())];
for (i, ring) in self.shards.iter().enumerate() {
if region.len() != ring.capacity() {
region = vec![0u8; ring.capacity()];
}
let head = ring.snapshot_into(&mut region);
w.shard(self.instance_id, i as u32, head as u64, ®ion);
}
w.meta(self.instance_id, host);
w.finish()
}
}
#[must_use = "the span ends when the guard is dropped; bind it to a variable"]
pub struct SpanGuard<'a, C: Clock, Ex: Event, MkEx: FnOnce(u64) -> Ex> {
recorder: &'a Recorder<C>,
id: u64,
make_exit: Option<MkEx>,
}
impl<C: Clock, Ex: Event, MkEx: FnOnce(u64) -> Ex> SpanGuard<'_, C, Ex, MkEx> {
pub fn id(&self) -> u64 {
self.id
}
}
impl<C: Clock, Ex: Event, MkEx: FnOnce(u64) -> Ex> Drop for SpanGuard<'_, C, Ex, MkEx> {
fn drop(&mut self) {
if let Some(make) = self.make_exit.take() {
self.recorder.record(&make(self.id));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::DumpReader;
#[test]
fn disabled_records_nothing() {
let rec = Recorder::new(1, 4096).with_clock(ManualClock::new(0));
assert!(!rec.is_enabled());
let bytes = rec.dump(
core::iter::empty(),
core::iter::empty(),
core::iter::empty(),
"",
);
let r = DumpReader::new(bytes).unwrap();
let shards = r.shards().unwrap();
assert_eq!(shards.len(), 1);
assert_eq!(shards[0].head, 0);
}
#[test]
fn span_id_is_unique_and_monotonic_per_thread() {
let rec = Recorder::new(1, 4096).with_clock(ManualClock::new(0));
let a = rec.new_span_id();
let b = rec.new_span_id();
assert_eq!(a & 0xFFFF, b & 0xFFFF);
assert!(b >> 16 > a >> 16);
}
#[test]
fn dump_carries_instance_id() {
let rec = Recorder::new(1, 4096).with_clock(ManualClock::new(0));
let bytes = rec.dump(
core::iter::empty(),
core::iter::empty(),
core::iter::empty(),
"host-x",
);
let metas = DumpReader::new(bytes).unwrap().metas().unwrap();
assert_eq!(metas.len(), 1);
assert_eq!(metas[0].instance_id, rec.instance_id());
assert_eq!(metas[0].host, "host-x");
}
#[test]
fn capture_feature_gates_enable() {
let rec = Recorder::new(1, 4096).with_clock(ManualClock::new(0));
rec.set_enabled(true);
assert_eq!(rec.is_enabled(), cfg!(feature = "capture"));
}
}