Migrate usage of the Hash trait for cache invalidation to the dedicated CacheHash trait (#4051)

* WIP start migrating usages of hash for cache invalidadion to dedicated trait

* Finish migrating usages

* Code review

* Add comments clearifying the reasoning for using random ids in the VectorModification cach hash impl

* Fix some remaining hash violations

* Finish migration and fix compilation

* Fix import ordering

* Cleanup

* Fix code review stuff

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2026-04-27 07:18:47 +02:00
committed by GitHub
parent 7bb01c9651
commit 3d84e63ef9
64 changed files with 828 additions and 448 deletions

View File

@@ -1,7 +1,7 @@
use core_types::table::Table;
use core_types::transform::Footprint;
use core_types::uuid::NodeId;
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector};
@@ -9,7 +9,7 @@ use raster_types::{CPU, GPU, Raster};
const DAY: f64 = 1000. * 3600. * 24.;
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, CacheHash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
pub enum RealTimeMode {
#[label("UTC")]
Utc,

View File

@@ -51,17 +51,17 @@ async fn context_modification<T>(
#[cfg(test)]
mod tests {
use super::*;
use core_types::graphene_hash::CacheHash;
use core_types::transform::Footprint;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::hash::Hasher;
/// Test that the hash of a nullified context remains stable even when nullified inputs change
/// Verifies that nullified context fields don't affect the cache hash — only the kept features matter.
#[test]
fn test_nullified_context_hash_stability() {
use core_types::Context;
use std::sync::Arc;
// Create original contexts using the Context type (Option<Arc<OwnedContextImpl>>)
let original_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default())
@@ -71,53 +71,48 @@ mod tests {
.with_animation_time(20.25),
));
// Test nullifying different features - hash should remain stable for each nullification
let features_to_keep = ContextFeatures::empty(); // Nullify everything
// Create nullified context - this should only keep features specified in features_to_keep
let nullified_ctx = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep);
// Calculate hash of nullified context
let mut hasher1 = DefaultHasher::new();
nullified_ctx.hash(&mut hasher1);
let hash1 = hasher1.finish();
// Create a different original context with changed values
// A second context with different values for the nullified fields
let changed_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default()) // Same footprint
.with_footprint(Footprint::default())
.with_index(2)
.with_real_time(999.9) // Different real time
.with_real_time(999.9)
.with_vararg(Box::new("test"))
.with_animation_time(888.8), // Different animation time
.with_animation_time(888.8),
));
// Create nullified context from the changed original - should have same hash since everything is nullified
let nullified_changed_ctx = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep);
// Nullify everything — both should hash the same regardless of their field values
let features_to_keep = ContextFeatures::empty();
let nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep);
let nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep);
let mut hasher1 = DefaultHasher::new();
nullified1.cache_hash(&mut hasher1);
let mut hasher2 = DefaultHasher::new();
nullified_changed_ctx.hash(&mut hasher2);
let hash2 = hasher2.finish();
nullified2.cache_hash(&mut hasher2);
// Hash should be the same because all features were nullified
assert_eq!(hash1, hash2, "Hash of nullified context should remain stable regardless of input changes when features are nullified");
assert_eq!(
hasher1.finish(),
hasher2.finish(),
"Hash of nullified context should remain stable regardless of input changes when features are nullified"
);
// Test partial nullification - keep only footprint
// Keep only footprint and varargs — both have the same footprint and vararg, so hash should still match
let partial_features = ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS;
let partial_nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features);
let partial_nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features);
let partial1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features);
let partial2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features);
let mut hasher3 = DefaultHasher::new();
partial_nullified1.hash(&mut hasher3);
let hash3 = hasher3.finish();
partial1.cache_hash(&mut hasher3);
let mut hasher4 = DefaultHasher::new();
partial_nullified2.hash(&mut hasher4);
let hash4 = hasher4.finish();
partial2.cache_hash(&mut hasher4);
// These should be the same because both have the same footprint (Footprint::default()) and varargs
// and other features are nullified
assert_eq!(hash3, hash4, "Hash should be stable when keeping only footprint and footprint values are the same");
assert_eq!(
hasher3.finish(),
hasher4.finish(),
"Hash should be stable when keeping only footprint and varargs and their values are the same"
);
}
}

View File

@@ -1,4 +1,4 @@
use core_types::Ctx;
use core_types::{CacheHash, Ctx};
use dyn_any::DynAny;
use glam::{DVec2, IVec2, UVec2};
@@ -15,7 +15,7 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
/// The X or Y component of a vec2.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, CacheHash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
#[widget(Radio)]
pub enum XY {
#[default]

View File

@@ -1,7 +1,8 @@
use core_types::WasmNotSend;
use core_types::graphene_hash::CacheHash;
use core_types::memo::*;
use std::hash::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::hash::Hasher;
use std::sync::Arc;
use std::sync::Mutex;
@@ -13,9 +14,9 @@ use std::sync::Mutex;
///
/// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache.
#[node_macro::node(category(""), path(graphene_core::memo), skip_impl)]
async fn memo<I: Hash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, node: impl Node<I, Output = T>) -> T {
async fn memo<I: CacheHash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, node: impl Node<I, Output = T>) -> T {
let mut hasher = DefaultHasher::new();
input.hash(&mut hasher);
input.cache_hash(&mut hasher);
let hash = hasher.finish();
if let Some(data) = cache.lock().as_ref().unwrap().as_ref().and_then(|data| (data.0 == hash).then_some(data.1.clone())) {