use std::cell::RefCell;
use std::time::Instant;
const SPAN_RING_SIZE: usize = 256;
const PLOT_SLOTS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpanRecord {
pub name: &'static str,
pub duration_ns: u64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct PlotEntry {
name: Option<&'static str>,
value: f64,
}
impl PlotEntry {
const fn empty() -> Self {
Self {
name: None,
value: 0.0,
}
}
}
#[derive(Debug)]
struct ProfileBuffer {
spans: [SpanRecord; SPAN_RING_SIZE],
span_index: usize,
span_count: usize,
plots: [PlotEntry; PLOT_SLOTS],
frame_count: u64,
}
impl ProfileBuffer {
fn new() -> Self {
Self {
spans: [SpanRecord {
name: "",
duration_ns: 0,
}; SPAN_RING_SIZE],
span_index: 0,
span_count: 0,
plots: [PlotEntry::empty(); PLOT_SLOTS],
frame_count: 0,
}
}
#[inline]
fn record_span(&mut self, name: &'static str, duration_ns: u64) {
self.spans[self.span_index] = SpanRecord { name, duration_ns };
self.span_index = (self.span_index + 1) % SPAN_RING_SIZE;
if self.span_count < SPAN_RING_SIZE {
self.span_count += 1;
}
}
#[inline]
fn record_plot(&mut self, name: &'static str, value: f64) {
for entry in self.plots.iter_mut() {
if entry.name == Some(name) {
entry.value = value;
return;
}
}
for entry in self.plots.iter_mut() {
if entry.name.is_none() {
entry.name = Some(name);
entry.value = value;
return;
}
}
self.plots[0].name = Some(name);
self.plots[0].value = value;
}
#[inline]
fn mark_frame(&mut self) {
self.frame_count += 1;
}
fn last_span_duration(&self, name: &'static str) -> Option<u64> {
if self.span_count == 0 {
return None;
}
for i in (0..self.span_count).rev() {
let idx = (self.span_index + SPAN_RING_SIZE - 1 - i) % SPAN_RING_SIZE;
if self.spans[idx].name == name {
return Some(self.spans[idx].duration_ns);
}
}
None
}
fn plot_value(&self, name: &'static str) -> Option<f64> {
self.plots
.iter()
.find(|e| e.name == Some(name))
.map(|e| e.value)
}
fn span_record_count(&self) -> usize {
self.span_count
}
fn frame_count(&self) -> u64 {
self.frame_count
}
}
thread_local! {
static PROFILE: RefCell<ProfileBuffer> = RefCell::new(ProfileBuffer::new());
}
pub struct TracySpan {
name: &'static str,
start: std::cell::Cell<Option<Instant>>,
}
impl TracySpan {
#[inline]
pub fn begin(name: &'static str) -> Self {
Self {
name,
start: std::cell::Cell::new(Some(Instant::now())),
}
}
#[inline]
pub fn end(&self) {
if let Some(start) = self.start.take() {
let duration_ns = start.elapsed().as_nanos() as u64;
PROFILE.with(|p| p.borrow_mut().record_span(self.name, duration_ns));
}
}
}
pub struct TracySpanGuard {
span: TracySpan,
}
impl Drop for TracySpanGuard {
#[inline]
fn drop(&mut self) {
self.span.end();
}
}
#[inline]
pub fn span(name: &'static str) -> TracySpanGuard {
TracySpanGuard {
span: TracySpan::begin(name),
}
}
#[inline]
pub fn frame_mark() {
PROFILE.with(|p| p.borrow_mut().mark_frame());
}
#[inline]
pub fn plot(name: &'static str, value: f64) {
PROFILE.with(|p| p.borrow_mut().record_plot(name, value));
}
pub fn last_span_duration(name: &'static str) -> Option<u64> {
PROFILE.with(|p| p.borrow().last_span_duration(name))
}
pub fn plot_value(name: &'static str) -> Option<f64> {
PROFILE.with(|p| p.borrow().plot_value(name))
}
pub fn span_record_count() -> usize {
PROFILE.with(|p| p.borrow().span_record_count())
}
pub fn frame_count() -> u64 {
PROFILE.with(|p| p.borrow().frame_count())
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn span_records_duration() {
let name = "span_records_duration";
{
let _g = span(name);
std::thread::sleep(Duration::from_micros(100));
}
let dur = last_span_duration(name);
assert!(dur.is_some(), "span should be recorded");
assert!(
dur.unwrap() >= 50_000,
"duration should be >= ~50us, got {}",
dur.unwrap()
);
}
#[test]
fn manual_span_end_records() {
let name = "manual_span_end_records";
let s = TracySpan::begin(name);
std::thread::sleep(Duration::from_micros(50));
s.end();
let dur = last_span_duration(name);
assert!(dur.is_some());
assert!(dur.unwrap() >= 20_000);
}
#[test]
fn double_end_is_noop() {
use std::time::Duration;
let name = "double_end_is_noop";
let s = TracySpan::begin(name);
s.end();
let first = last_span_duration(name).unwrap();
let s2 = TracySpan::begin(name);
std::thread::sleep(Duration::from_nanos(1_000));
s2.end();
let second = last_span_duration(name).unwrap();
assert!(second > 0);
let _ = first;
}
#[test]
fn frame_mark_increments_counter() {
let before = frame_count();
frame_mark();
frame_mark();
assert_eq!(frame_count(), before + 2);
}
#[test]
fn plot_records_and_updates() {
plot("plot_test_a", 1.0);
assert_eq!(plot_value("plot_test_a"), Some(1.0));
plot("plot_test_a", 2.5);
assert_eq!(plot_value("plot_test_a"), Some(2.5));
}
#[test]
fn plot_missing_returns_none() {
assert!(plot_value("definitely_not_a_plot_xyz").is_none());
}
#[test]
fn missing_span_returns_none() {
assert!(last_span_duration("definitely_not_a_span_xyz").is_none());
}
#[test]
fn ring_buffer_overwrites_oldest() {
for i in 0..(SPAN_RING_SIZE + 10) {
let _g = span("ring_span_a");
let _g2 = span("ring_span_b");
let _ = i;
}
assert!(last_span_duration("ring_span_b").is_some());
assert!(span_record_count() <= SPAN_RING_SIZE);
}
#[test]
fn plot_slots_evict_when_full() {
for i in 0..(PLOT_SLOTS + 5) {
plot("plot_evict", i as f64);
}
assert_eq!(plot_value("plot_evict"), Some((PLOT_SLOTS + 4) as f64));
}
#[test]
fn tracy_overhead_under_100us_per_frame() {
{
let _g = span("warmup");
}
frame_mark();
plot("warmup_plot", 0.0);
const FRAMES: u32 = 60;
let start = Instant::now();
for _ in 0..FRAMES {
let _g = span("overhead_frame");
frame_mark();
plot("overhead_plot", 1.0);
}
let elapsed = start.elapsed();
let per_frame_ns = elapsed.as_nanos() / FRAMES as u128;
assert!(
per_frame_ns < 100_000,
"Tracy overhead {per_frame_ns}ns/frame exceeds the 100us (100_000ns) gate"
);
}
}