1
0
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:
Emil Ernerfeldt
2026-06-10 09:39:33 +02:00
committed by GitHub
parent 71c4ff3c33
commit 858c8fd99f
3 changed files with 208 additions and 13 deletions

View File

@@ -65,7 +65,12 @@ impl Id {
/// Generate a new root [`Id`] by hashing some source (e.g. a string or integer). /// Generate a new root [`Id`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl AsId) -> Self { 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. /// 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 _}; use std::hash::{BuildHasher as _, Hasher as _};
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.value()); hasher.write_u64(self.value());
hasher.write_u64(IdSalt::new(salt).value()); hasher.write_u64(IdSalt::new(&salt).value());
Self::from_hash(hasher.finish()) let id = Self::from_hash(hasher.finish());
#[cfg(debug_assertions)]
id_source::insert_child(id, self, &salt);
id
} }
/// Short and readable summary /// Short and readable summary
@@ -116,10 +126,19 @@ impl Id {
impl std::fmt::Debug for Id { impl std::fmt::Debug for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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 /// Convenience
impl From<&'static str> for Id { impl From<&'static str> for Id {
#[inline] #[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. /// `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. /// `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>; 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")"#);
}
}

View File

@@ -30,7 +30,12 @@ impl nohash_hasher::IsEnabled for IdSalt {}
impl IdSalt { impl IdSalt {
/// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer). /// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl AsIdSalt) -> Self { 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. /// Create a new root [`IdSalt`] from a high-entropy hash.
@@ -54,6 +59,78 @@ impl IdSalt {
impl std::fmt::Debug for IdSalt { impl std::fmt::Debug for IdSalt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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) 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))"#);
}
}

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c9b497aa9b2b92843937c84a6ff8901f248501d5d4a89c2fb1237008a44fcad1 oid sha256:bbd7fa4db7dd580968949b9d76c1521d5627cc1ee8cd19bb3dea752d3d47607b
size 114038 size 114174