use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread::ThreadId;
static FRAME_NUMBER: AtomicU64 = AtomicU64::new(0);
static IS_BEVY_CONTEXT: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone)]
pub struct DiagContext {
pub is_bevy: bool,
pub frame_active: bool,
pub frame_number: u64,
pub thread_id: ThreadId,
pub thread_name: Option<String>,
pub is_main_thread: bool,
}
impl DiagContext {
pub fn capture() -> Self {
let thread = std::thread::current();
let thread_name = thread.name().map(String::from);
Self {
is_bevy: IS_BEVY_CONTEXT.load(Ordering::Relaxed),
frame_active: crate::core::tls::with_tls(|tls| tls.is_frame_active()),
frame_number: FRAME_NUMBER.load(Ordering::Relaxed),
thread_id: thread.id(),
thread_name,
is_main_thread: is_main_thread(),
}
}
pub fn minimal() -> Self {
Self {
is_bevy: IS_BEVY_CONTEXT.load(Ordering::Relaxed),
frame_active: false,
frame_number: FRAME_NUMBER.load(Ordering::Relaxed),
thread_id: std::thread::current().id(),
thread_name: None,
is_main_thread: false,
}
}
pub fn format(&self) -> String {
let mut parts = Vec::new();
if self.is_bevy {
parts.push("bevy=true".to_string());
}
parts.push(format!("frame={}", self.frame_number));
if self.frame_active {
parts.push("frame_active=true".to_string());
}
if let Some(ref name) = self.thread_name {
parts.push(format!("thread=\"{}\"", name));
} else {
parts.push(format!("thread={:?}", self.thread_id));
}
if self.is_main_thread {
parts.push("main_thread=true".to_string());
}
parts.join(", ")
}
}
impl std::fmt::Display for DiagContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.format())
}
}
pub fn set_bevy_context(active: bool) {
IS_BEVY_CONTEXT.store(active, Ordering::Relaxed);
}
pub fn is_bevy_context() -> bool {
IS_BEVY_CONTEXT.load(Ordering::Relaxed)
}
pub fn increment_frame() {
FRAME_NUMBER.fetch_add(1, Ordering::Relaxed);
}
pub fn frame_number() -> u64 {
FRAME_NUMBER.load(Ordering::Relaxed)
}
pub fn reset_frame_counter() {
FRAME_NUMBER.store(0, Ordering::Relaxed);
}
static MAIN_THREAD_ID: std::sync::OnceLock<ThreadId> = std::sync::OnceLock::new();
pub fn init_main_thread() {
let _ = MAIN_THREAD_ID.set(std::thread::current().id());
}
pub fn is_main_thread() -> bool {
MAIN_THREAD_ID
.get()
.map(|id| *id == std::thread::current().id())
.unwrap_or(false)
}
pub fn check_frame_context() {
let ctx = DiagContext::capture();
if !ctx.frame_active {
if ctx.is_bevy {
super::emit::emit_with_context(
&super::kind::FA101,
&ctx.format(),
);
} else {
super::emit::emit_with_context(
&super::kind::FA001,
&ctx.format(),
);
}
}
}
pub fn check_bevy_plugin() {
#[cfg(feature = "bevy")]
{
if !is_bevy_context() {
super::emit::emit(&super::kind::FA101);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_capture() {
let ctx = DiagContext::minimal();
assert!(!ctx.is_bevy);
assert!(!ctx.frame_active);
}
#[test]
fn test_frame_counter() {
reset_frame_counter();
assert_eq!(frame_number(), 0);
increment_frame();
assert_eq!(frame_number(), 1);
increment_frame();
assert_eq!(frame_number(), 2);
reset_frame_counter();
}
#[test]
fn test_bevy_context() {
set_bevy_context(false);
assert!(!is_bevy_context());
set_bevy_context(true);
assert!(is_bevy_context());
set_bevy_context(false);
}
#[test]
fn test_context_format() {
reset_frame_counter();
set_bevy_context(false);
let ctx = DiagContext::minimal();
let formatted = ctx.format();
assert!(formatted.contains("frame=0"));
}
}