mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Improve Debug-formatting of Id in debug-builds (#8190)
The Debug-formatting of `Id` in debug-builds now contain the full
lineage:
```rs
#[test]
fn with_chain() {
let id = Id::new("a").with("b").with("c").with(7_i32);
assert_eq!(
format!("{id:?}"),
r#"Id::new("a").with("b").with("c").with(7)"#
);
}
```
## Related
* https://github.com/emilk/egui/pull/5851
* https://github.com/emilk/egui/pull/7988
This commit is contained in:
@@ -65,7 +65,12 @@ impl Id {
|
||||
|
||||
/// Generate a new root [`Id`] by hashing some source (e.g. a string or integer).
|
||||
pub fn new(source: impl AsId) -> Self {
|
||||
Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
|
||||
let id = Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(&source));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id_source::insert_root(id, &source);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Generate a child [`Id`] by salting the parent [`Id`] with the given argument.
|
||||
@@ -73,8 +78,13 @@ impl Id {
|
||||
use std::hash::{BuildHasher as _, Hasher as _};
|
||||
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
|
||||
hasher.write_u64(self.value());
|
||||
hasher.write_u64(IdSalt::new(salt).value());
|
||||
Self::from_hash(hasher.finish())
|
||||
hasher.write_u64(IdSalt::new(&salt).value());
|
||||
let id = Self::from_hash(hasher.finish());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id_source::insert_child(id, self, &salt);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Short and readable summary
|
||||
@@ -116,10 +126,19 @@ impl Id {
|
||||
|
||||
impl std::fmt::Debug for Id {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:04X}", self.value() as u16)
|
||||
if *self == Self::NULL {
|
||||
return write!(f, "Id::NULL");
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(source) = id_source::get(*self) {
|
||||
return f.write_str(&source);
|
||||
}
|
||||
write!(f, "id_{:04X}", self.value() as u16)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Convenience
|
||||
impl From<&'static str> for Id {
|
||||
#[inline]
|
||||
@@ -135,12 +154,6 @@ impl From<String> for Id {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_size() {
|
||||
assert_eq!(std::mem::size_of::<Id>(), 8);
|
||||
assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
@@ -148,3 +161,108 @@ pub type IdSet = nohash_hasher::IntSet<Id>;
|
||||
|
||||
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
pub type IdMap<V> = nohash_hasher::IntMap<Id, V>;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// In debug builds, remember the `Debug`-formatted call chain that produced each [`Id`].
|
||||
///
|
||||
/// Used by [`Id`]'s `Debug` impl so that `Id::new("foo")` prints as `Id::new("foo")`,
|
||||
/// and `Id::new("foo").with("bar")` prints as `Id::new("foo").with("bar")`, etc.
|
||||
#[cfg(debug_assertions)]
|
||||
mod id_source {
|
||||
use super::{AsId, AsIdSalt, Id, IdMap};
|
||||
use epaint::mutex::RwLock;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static SOURCE_MAP: LazyLock<RwLock<IdMap<String>>> = LazyLock::new(RwLock::default);
|
||||
|
||||
pub(super) fn insert_root(id: Id, source: &impl AsId) {
|
||||
if SOURCE_MAP.read().contains_key(&id) {
|
||||
return;
|
||||
}
|
||||
// Format outside the lock since `{source:?}` may itself recurse into [`Id`]'s `Debug` impl.
|
||||
let formatted = format!("Id::new({source:?})");
|
||||
SOURCE_MAP.write().insert(id, formatted);
|
||||
}
|
||||
|
||||
pub(super) fn insert_child(id: Id, parent: Id, salt: &impl AsIdSalt) {
|
||||
if SOURCE_MAP.read().contains_key(&id) {
|
||||
return;
|
||||
}
|
||||
// Look up parent's repr and drop the read guard before formatting,
|
||||
// since `{parent:?}` and `{salt:?}` may themselves recurse into [`Id`]'s `Debug` impl.
|
||||
let cached_parent_repr = SOURCE_MAP.read().get(&parent).cloned();
|
||||
let parent_repr = cached_parent_repr.unwrap_or_else(|| format!("{parent:?}"));
|
||||
let formatted = format!("{parent_repr}.with({salt:?})");
|
||||
SOURCE_MAP.write().insert(id, formatted);
|
||||
}
|
||||
|
||||
pub(super) fn get(id: Id) -> Option<String> {
|
||||
SOURCE_MAP.read().get(&id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_size() {
|
||||
assert_eq!(std::mem::size_of::<Id>(), 8);
|
||||
assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(debug_assertions)]
|
||||
mod debug_format_tests {
|
||||
use crate::IdSalt;
|
||||
|
||||
use super::Id;
|
||||
|
||||
#[test]
|
||||
fn root_string() {
|
||||
let id = Id::new("foo");
|
||||
assert_eq!(format!("{id:?}"), r#"Id::new("foo")"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_integer() {
|
||||
let id = Id::new(42_i32);
|
||||
assert_eq!(format!("{id:?}"), "Id::new(42)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_id_salt() {
|
||||
let id = Id::new(IdSalt::new("foo"));
|
||||
assert_eq!(format!("{id:?}"), r#"Id::new(IdSalt::new("foo"))"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_one_child() {
|
||||
let id = Id::new("parent").with("child");
|
||||
assert_eq!(format!("{id:?}"), r#"Id::new("parent").with("child")"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_chain() {
|
||||
let id = Id::new("a").with("b").with("c").with(7_i32);
|
||||
assert_eq!(
|
||||
format!("{id:?}"),
|
||||
r#"Id::new("a").with("b").with("c").with(7)"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_id_as_source() {
|
||||
let inner = Id::new("foo");
|
||||
let outer = Id::new(inner);
|
||||
assert_eq!(format!("{outer:?}"), r#"Id::new(Id::new("foo"))"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_prints_as_null() {
|
||||
assert_eq!(format!("{:?}", Id::NULL), "Id::NULL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_as_parent() {
|
||||
let id = Id::NULL.with("foo");
|
||||
assert_eq!(format!("{id:?}"), r#"Id::NULL.with("foo")"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,12 @@ impl nohash_hasher::IsEnabled for IdSalt {}
|
||||
impl IdSalt {
|
||||
/// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer).
|
||||
pub fn new(source: impl AsIdSalt) -> Self {
|
||||
Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
|
||||
let id_salt = Self::from_hash(ahash::RandomState::with_seeds(5, 6, 7, 8).hash_one(&source));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id_salt_source::maybe_insert(id_salt, &source);
|
||||
|
||||
id_salt
|
||||
}
|
||||
|
||||
/// Create a new root [`IdSalt`] from a high-entropy hash.
|
||||
@@ -54,6 +59,78 @@ impl IdSalt {
|
||||
|
||||
impl std::fmt::Debug for IdSalt {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(source) = id_salt_source::get(*self) {
|
||||
return write!(f, "IdSalt::new({source})");
|
||||
}
|
||||
write!(f, "salt_{:04X}", self.value() as u16)
|
||||
}
|
||||
}
|
||||
|
||||
/// In debug builds, remember the `Debug`-formatted source that produced each [`IdSalt`].
|
||||
///
|
||||
/// Used by [`IdSalt`]'s `Debug` impl so that `IdSalt::new("foo")` prints as
|
||||
/// `IdSalt::new("foo")`, and `IdSalt::new(IdSalt::new("foo"))` prints as
|
||||
/// `IdSalt::new(IdSalt::new("foo"))`, etc.
|
||||
#[cfg(debug_assertions)]
|
||||
mod id_salt_source {
|
||||
use super::{AsIdSalt, IdSalt};
|
||||
use epaint::mutex::RwLock;
|
||||
use nohash_hasher::IntMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static SOURCE_MAP: LazyLock<RwLock<IntMap<IdSalt, String>>> = LazyLock::new(RwLock::default);
|
||||
|
||||
pub(super) fn maybe_insert(id_salt: IdSalt, source: &impl AsIdSalt) {
|
||||
if !SOURCE_MAP.read().contains_key(&id_salt) {
|
||||
let formatted = format!("{source:?}");
|
||||
SOURCE_MAP.write().insert(id_salt, formatted);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get(id_salt: IdSalt) -> Option<String> {
|
||||
SOURCE_MAP.read().get(&id_salt).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(debug_assertions)]
|
||||
mod tests {
|
||||
use super::IdSalt;
|
||||
|
||||
#[test]
|
||||
fn debug_format_string_source() {
|
||||
let salt = IdSalt::new("foo");
|
||||
assert_eq!(format!("{salt:?}"), r#"IdSalt::new("foo")"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format_integer_source() {
|
||||
let salt = IdSalt::new(42_i32);
|
||||
assert_eq!(format!("{salt:?}"), "IdSalt::new(42)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format_nested_salt() {
|
||||
let inner = IdSalt::new("foo");
|
||||
let outer = IdSalt::new(inner);
|
||||
assert_eq!(format!("{outer:?}"), r#"IdSalt::new(IdSalt::new("foo"))"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format_triple_nested_salt() {
|
||||
let a = IdSalt::new("foo");
|
||||
let b = IdSalt::new(a);
|
||||
let c = IdSalt::new(b);
|
||||
assert_eq!(
|
||||
format!("{c:?}"),
|
||||
r#"IdSalt::new(IdSalt::new(IdSalt::new("foo")))"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format_tuple_source() {
|
||||
let salt = IdSalt::new(("foo", 7_i32));
|
||||
assert_eq!(format!("{salt:?}"), r#"IdSalt::new(("foo", 7))"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c9b497aa9b2b92843937c84a6ff8901f248501d5d4a89c2fb1237008a44fcad1
|
||||
size 114038
|
||||
oid sha256:bbd7fa4db7dd580968949b9d76c1521d5627cc1ee8cd19bb3dea752d3d47607b
|
||||
size 114174
|
||||
|
||||
Reference in New Issue
Block a user