use serde::Serialize;
pub type NodeId = u32;
#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
pub struct Warning {
pub node: Option<NodeId>,
#[ts(type = "string")]
pub kind: &'static str,
pub value: String,
pub message: String,
}
#[cfg(all(feature = "devtools", debug_assertions))]
mod imp {
use super::NodeId;
use super::Warning;
use std::cell::{Cell, RefCell};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
const DECODE_CAP: usize = 64;
const RUNTIME_CAP: usize = 256;
thread_local! {
static DECODE: RefCell<Vec<Warning>> = const { RefCell::new(Vec::new()) };
static CURRENT_NODE: Cell<Option<NodeId>> = const { Cell::new(None) };
}
static ARMED: AtomicBool = AtomicBool::new(false);
static RUNTIME: Mutex<Vec<Warning>> = Mutex::new(Vec::new());
pub fn decode_batch_start() {
DECODE.with(|d| d.borrow_mut().clear());
}
pub fn decode_watermark() -> usize {
DECODE.with(|d| d.borrow().len())
}
pub fn decode_report(kind: &'static str, value: &str, message: &str) {
super::log_warn(kind, value, message);
crate::console_log::push(
crate::console_log::Source::Rust,
crate::console_log::Level::Warn,
&format!("[{kind}] {message}"),
);
DECODE.with(|d| {
let mut d = d.borrow_mut();
if d.len() < DECODE_CAP {
d.push(Warning {
node: None,
kind,
value: value.to_owned(),
message: message.to_owned(),
});
}
});
}
pub fn decode_attribute_since(mark: usize, node: Option<NodeId>) {
DECODE.with(|d| {
for w in d.borrow_mut().iter_mut().skip(mark) {
w.node = node;
}
});
}
pub fn take_decode_warnings() -> Vec<Warning> {
DECODE.with(|d| std::mem::take(&mut *d.borrow_mut()))
}
pub fn arm_runtime() {
ARMED.store(true, Ordering::Relaxed);
}
pub struct NodeScope(Option<NodeId>);
impl Drop for NodeScope {
fn drop(&mut self) {
CURRENT_NODE.with(|c| c.set(self.0));
}
}
pub fn node_scope(id: NodeId) -> NodeScope {
CURRENT_NODE.with(|c| NodeScope(c.replace(Some(id))))
}
pub fn report(kind: &'static str, value: &str, message: &str) {
super::log_warn(kind, value, message);
if !ARMED.load(Ordering::Relaxed) {
return;
}
let node = CURRENT_NODE.with(|c| c.get());
crate::console_log::push(
crate::console_log::Source::Rust,
crate::console_log::Level::Warn,
&match node {
Some(n) => format!("[{kind}] {message} (node {n})"),
None => format!("[{kind}] {message}"),
},
);
let mut sink = RUNTIME.lock().unwrap_or_else(|e| e.into_inner());
if sink.len() < RUNTIME_CAP {
sink.push(Warning {
node,
kind,
value: value.to_owned(),
message: message.to_owned(),
});
}
}
pub fn take_runtime_warnings() -> Vec<Warning> {
std::mem::take(&mut *RUNTIME.lock().unwrap_or_else(|e| e.into_inner()))
}
}
#[cfg_attr(not(feature = "devtools"), allow(dead_code))]
#[cfg(not(all(feature = "devtools", debug_assertions)))]
mod imp {
use super::NodeId;
use super::Warning;
#[inline(always)]
pub fn decode_batch_start() {}
#[inline(always)]
pub fn decode_watermark() -> usize {
0
}
#[inline(always)]
pub fn decode_report(kind: &'static str, value: &str, message: &str) {
super::log_warn(kind, value, message);
}
#[inline(always)]
pub fn decode_attribute_since(_mark: usize, _node: Option<NodeId>) {}
#[inline(always)]
pub fn take_decode_warnings() -> Vec<Warning> {
Vec::new()
}
#[inline(always)]
pub fn arm_runtime() {}
pub struct NodeScope;
#[inline(always)]
pub fn node_scope(_id: NodeId) -> NodeScope {
NodeScope
}
#[inline(always)]
pub fn report(kind: &'static str, value: &str, message: &str) {
super::log_warn(kind, value, message);
}
#[inline(always)]
pub fn take_runtime_warnings() -> Vec<Warning> {
Vec::new()
}
}
pub use imp::*;
pub fn report_if(validate: bool, kind: &'static str, make: &dyn Fn() -> (String, String)) {
if validate {
let (value, message) = make();
report(kind, &value, &message);
}
}
pub(crate) fn log_warn(kind: &str, value: &str, message: &str) -> bool {
use std::collections::HashSet;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::{LazyLock, Mutex};
const SEEN_CAP: usize = 4096;
static SEEN: LazyLock<Mutex<HashSet<u64>>> = LazyLock::new(Default::default);
let mut hasher = DefaultHasher::new();
(kind, value, message).hash(&mut hasher);
let key = hasher.finish();
let mut seen = SEEN.lock().unwrap_or_else(|e| e.into_inner());
if seen.len() >= SEEN_CAP {
seen.clear();
}
if !seen.insert(key) {
return false;
}
drop(seen);
tracing::warn!(target: "bevy_react", "{message}");
true
}
#[cfg(any(test, feature = "test_util"))]
pub fn test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(all(test, feature = "devtools", debug_assertions))]
mod tests {
use super::*;
#[test]
fn decode_sink_stamps_and_drains() {
decode_batch_start();
decode_report("length", "aa16", "invalid length \"aa16\"");
let mark = decode_watermark();
decode_report("display", "flexx", "unrecognized display \"flexx\"");
decode_attribute_since(mark, Some(9));
let warns = take_decode_warnings();
assert_eq!(warns.len(), 2);
assert_eq!(warns[0].node, None);
assert_eq!(warns[1].node, Some(9));
assert!(take_decode_warnings().is_empty(), "drain empties the sink");
}
#[test]
fn node_scope_nests_and_restores() {
let _lock = test_lock();
arm_runtime();
let _ = take_runtime_warnings();
{
let _outer = node_scope(1);
report("color", "redd", "unrecognized color");
{
let _inner = node_scope(2);
report("color", "bluu", "unrecognized color");
}
report("color", "grean", "unrecognized color");
}
report("color", "unscoped", "unrecognized color");
let nodes: Vec<_> = take_runtime_warnings()
.into_iter()
.map(|w| w.node)
.collect();
assert_eq!(nodes, vec![Some(1), Some(2), Some(1), None]);
}
}
#[cfg(test)]
mod log_warn_tests {
#[test]
fn log_warn_dedups_per_distinct_warning() {
assert!(super::log_warn("color", "lw-t1-redd", "unrecognized color"));
assert!(!super::log_warn(
"color",
"lw-t1-redd",
"unrecognized color"
));
assert!(!super::log_warn(
"color",
"lw-t1-redd",
"unrecognized color"
));
assert!(super::log_warn(
"color",
"lw-t1-bluee",
"unrecognized color"
));
assert!(super::log_warn(
"length",
"lw-t1-redd",
"unrecognized color"
));
assert!(super::log_warn("color", "lw-t1-redd", "other message"));
}
}