use core_types::arena::{Arena, ArenaCell}; use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll, ExtractArena}; use core_types::frame_table::{FrameTable, Lookup}; use core_types::gpoll::{Extent, Finality, GPoll, Interrupt}; use core_types::graphene_hash::CacheHash; use core_types::memo::*; use core_types::node::Node; use core_types::registry::cache_key; use std::sync::Arc; use std::sync::Mutex; /// Helps speed up repeated renders in a computationally-heavy part of the node graph. /// /// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed. #[node_macro::node(category("General"), path(graphene_core::memo), skip_impl, extent(memoize_extent))] fn memoize(input: I, #[data] cache: Arc>>, content: impl Node) -> GPoll { let key = cache_key(&input); if let Some((hash, value, finality)) = cache.lock().unwrap().as_ref() && *hash == key { return match finality { Finality::AllFinal => GPoll::Final(value.clone()), Finality::Partial => GPoll::Partial(value.clone()), }; } let result = content.eval(input); match &result { GPoll::Final(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)), GPoll::Partial(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)), GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {} } result } fn memoize_extent(node: &MemoizeNode, ctx: &C) -> GPoll where T: Clone, NodeContent: Node, { node.content.extent(ctx) } #[node_macro::node(category(""), path(graphene_core::memo), skip_impl, extent(frame_memo_extent))] fn frame_memo<'e, T: Clone + 'static + Send + Sync>( ctx: impl Ctx + CacheHash + ExtractArena<'e>, #[data] cell: ArenaCell>, content: impl Node, Output = T>, ) -> GPoll<&'e T> { let arena = ctx.arena(); let table = match cell.load(arena) { Some(table) => table, None => match arena.alloc(FrameTable::new()) { Some((table, weak)) => { cell.store(weak); table } None => return park(arena, content.eval(ctx)), }, }; match table.lookup(cache_key(ctx)) { Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value), Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value), Lookup::Vacant(slot) => match content.eval(ctx) { GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)), GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)), unpublishable => { slot.release(); park(arena, unpublishable) } }, Lookup::Full => park(arena, content.eval(ctx)), } } fn frame_memo_extent(node: &FrameMemoNode, ctx: &C) -> GPoll where T: Clone + 'static + Send + Sync, NodeContent: Node, { node.content.extent(ctx) } pub fn park(arena: &Arena, result: GPoll) -> GPoll<&T> { match result { GPoll::Final(value) => match arena.alloc(value) { Some((parked, _)) => GPoll::Final(parked), None => GPoll::arena_exhausted(), }, GPoll::Partial(value) => match arena.alloc(value) { Some((parked, _)) => GPoll::Partial(parked), None => GPoll::arena_exhausted(), }, GPoll::Fallback(boxed) => { let (value, error) = *boxed; match arena.alloc(value) { Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))), None => GPoll::arena_exhausted(), } } GPoll::Pending => GPoll::Pending, GPoll::Error(error) => GPoll::Error(error), } } /// Adapts an owned edge to a lending one by parking each result in the eval arena. #[node_macro::node(category(""), path(graphene_core::memo), skip_impl)] fn lend<'e, T: Send + Sync>(ctx: impl Ctx + ExtractArena<'e>, value: T) -> GPoll<&'e T> { park(ctx.arena(), GPoll::Final(value)) } type MonitorValue = Arc>>>>; /// The Monitor node is used by the editor to access the data flowing through it. #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"), skip_impl)] fn monitor( ctx: impl Ctx + DeriveCtx + ExtractAll, #[allow(clippy::type_complexity)] #[data] io: MonitorValue, content: impl Node, Output = T>, ) -> Result { let output = content.eval(&ctx.derived())?; *io.lock().unwrap() = Some(Arc::new(IORecord { input: CtxSnapshot::capture(ctx), output: output.clone(), })); Ok(output) } fn serialize_monitor(io: &MonitorValue) -> Option> { let io = io.lock().unwrap(); io.as_ref().map(|output| output.clone() as Arc) } #[cfg(test)] mod tests { use super::*; use core_types::SourceId; use core_types::context::{ContextImpl, EvalScope}; use core_types::registry::{EdgeHandle, ErasedLendNode, ErasedNode}; use std::sync::atomic::{AtomicU32, Ordering}; struct CountingNode(AtomicU32); impl Node for CountingNode { type Output = u32; fn eval(&self, _input: &Input) -> GPoll { GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1) } } struct PartialCountingNode(AtomicU32); impl Node for PartialCountingNode { type Output = u32; fn eval(&self, _input: &Input) -> GPoll { GPoll::Partial(self.0.fetch_add(1, Ordering::Relaxed) + 1) } } struct ValueNode(T); impl Node for ValueNode { type Output = T; fn eval(&self, _input: &Input) -> GPoll { GPoll::Final(self.0.clone()) } } fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { EvalScope::new(Some(0.5), None, None, generations, arena) } #[test] fn monitor_serialize_exposes_the_io_record_through_the_edge() { let arena = Arena::new(1024).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let handle = EdgeHandle::new(Arc::new(MonitorNode::new(ValueNode(11u32))) as Arc>); assert!(handle.serialize().is_none(), "no record before the first eval"); let edge = handle.duplicate().downcast::().unwrap(); assert_eq!(edge.eval(&ctx), GPoll::Final(11)); let record = handle.serialize().expect("the eval landed a record"); let record = record.downcast_ref::>().expect("the record is the monitor io"); assert_eq!(record.output, 11); } #[test] fn memoize_caches_across_evals() { let arena = Arena::new(1024).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0))); assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); } #[test] fn memo_invalidates_on_generation_bump() { let arena = Arena::new(1024).unwrap(); let source: SourceId = 7; let before = [(source, 1)]; let after = [(source, 2)]; let scope_before = scope_fixture(&before, &arena); let scope_after = scope_fixture(&after, &arena); let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0))); assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); assert_eq!(memoized.eval(&ContextImpl::root(&scope_after)), GPoll::Final(2)); } #[test] fn memo_replays_partiality_on_hit() { let arena = Arena::new(1024).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let memoized = MemoizeNode::new(PartialCountingNode(AtomicU32::new(0))); assert_eq!(memoized.eval(&ctx), GPoll::Partial(1)); assert_eq!(memoized.eval(&ctx), GPoll::Partial(1)); } #[test] fn memoized_edges_stack_and_rewire() { let arena = Arena::new(1024).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let edge = EdgeHandle::new(Arc::new(CountingNode(AtomicU32::new(0))) as Arc>); let memoized = EdgeHandle::new(Arc::new(MemoizeNode::new(edge.downcast::().unwrap())) as Arc>); let stacked = MemoizeNode::new(memoized.downcast::().unwrap()); assert_eq!(stacked.eval(&ctx), GPoll::Final(1)); assert_eq!(stacked.eval(&ctx), GPoll::Final(1)); } #[test] fn frame_memo_turns_an_owned_edge_into_a_lending_edge() { let arena = Arena::new(4096).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let edge = EdgeHandle::new(Arc::new(ValueNode("lent out".to_string())) as Arc>); let lending = EdgeHandle::new_ref(Arc::new(FrameMemoNode::new(edge.downcast::().unwrap())) as Arc>); assert_eq!(*lending.ty(), core_types::registry::lend_edge_type::()); let node = lending.downcast_lend::().unwrap(); let GPoll::Final(first) = node.eval(&ctx) else { panic!("lend must fill the frame table and lend"); }; let GPoll::Final(second) = node.eval(&ctx) else { panic!("second eval must lend the published value"); }; assert_eq!(first, "lent out"); assert!(std::ptr::eq(first, second)); } }