Restore serialize-based introspection through the typed edges

This commit is contained in:
Dennis Kobert
2026-07-30 10:15:51 +00:00
parent bb736002cf
commit b70b28bbe3
8 changed files with 66 additions and 16 deletions
@@ -14,7 +14,7 @@ use graphene_std::memo::IORecord;
use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType}; use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType};
use graphene_std::{Artboard, Color, Context, Graphic}; use graphene_std::{Artboard, Color, CtxSnapshot, Graphic};
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
@@ -167,7 +167,7 @@ macro_rules! generate_layout_downcast {
($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => { ($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => {
if false { None } if false { None }
$( $(
else if let Some(io) = $introspected_data.downcast_ref::<IORecord<Context, $ty>>() { else if let Some(io) = $introspected_data.downcast_ref::<IORecord<CtxSnapshot, $ty>>() {
Some(io.output.layout_with_breadcrumb($data)) Some(io.output.layout_with_breadcrumb($data))
} }
)* )*
@@ -178,7 +178,7 @@ macro_rules! generate_layout_downcast {
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> { fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
// `List<NodeId>` is interpreted as a path (e.g. the value produced by `path_of_subgraph`), shown as a // `List<NodeId>` is interpreted as a path (e.g. the value produced by `path_of_subgraph`), shown as a
// `List` where each item's NodeId resolves against the prefix made up of the items above it. // `List` where each item's NodeId resolves against the prefix made up of the items above it.
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<NodeId>>>() { if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<NodeId>>>() {
return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data)); return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data));
} }
generate_layout_downcast!(introspected_data, data, [ generate_layout_downcast!(introspected_data, data, [
+4 -4
View File
@@ -15,7 +15,7 @@ use graphene_std::raster::{CPU, Raster};
use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box}; use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box};
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::{Vector, graphic_types}; use graphene_std::vector::{Vector, graphic_types};
use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration}; use graphene_std::{ATTR_TRANSFORM, CtxSnapshot, Graphic, NodeInputDecleration};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
@@ -892,7 +892,7 @@ fn introspected_output<T: Clone + Send + Sync + 'static>(data: &Arc<dyn Any + Se
if let Some(io) = data.downcast_ref::<IORecord<Footprint, T>>() { if let Some(io) = data.downcast_ref::<IORecord<Footprint, T>>() {
return Some(io.output.clone()); return Some(io.output.clone());
} }
if let Some(io) = data.downcast_ref::<IORecord<Context, T>>() { if let Some(io) = data.downcast_ref::<IORecord<CtxSnapshot, T>>() {
return Some(io.output.clone()); return Some(io.output.clone());
} }
None None
@@ -911,7 +911,7 @@ mod test {
use crate::test_utils::test_prelude::{self, NodeGraphLayer}; use crate::test_utils::test_prelude::{self, NodeGraphLayer};
use graph_craft::ProtoNodeIdentifier; use graph_craft::ProtoNodeIdentifier;
use graph_craft::document::NodeNetwork; use graph_craft::document::NodeNetwork;
use graphene_std::Context; use graphene_std::CtxSnapshot;
use graphene_std::NodeInputDecleration; use graphene_std::NodeInputDecleration;
use graphene_std::memo::IORecord; use graphene_std::memo::IORecord;
use test_prelude::LayerNodeIdentifier; use test_prelude::LayerNodeIdentifier;
@@ -979,7 +979,7 @@ mod test {
Some(x.output.clone()) Some(x.output.clone())
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Input::Result>>() { } else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Input::Result>>() {
Some(x.output.clone()) Some(x.output.clone())
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Context, Input::Result>>() { } else if let Some(x) = dynamic.downcast_ref::<IORecord<CtxSnapshot, Input::Result>>() {
Some(x.output.clone()) Some(x.output.clone())
} else { } else {
warn!("cannot downcast type for introspection"); warn!("cannot downcast type for introspection");
+5 -5
View File
@@ -19,7 +19,7 @@ use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRend
use graphene_std::transform::RenderQuality; use graphene_std::transform::RenderQuality;
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::vector::style::RenderMode; use graphene_std::vector::style::RenderMode;
use graphene_std::{Artboard, Context, Graphic}; use graphene_std::{Artboard, CtxSnapshot, Graphic};
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::util::wrap_network_in_scope; use interpreted_executor::util::wrap_network_in_scope;
use spin::Mutex; use spin::Mutex;
@@ -413,7 +413,7 @@ impl NodeRuntime {
}; };
// Graphic list: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content) // Graphic list: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content)
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>() { if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<Graphic>>>() {
if update_thumbnails { if update_thumbnails {
let bounds = graphene_std::renderer::graphic_list_bounding_box(&io.output, DAffine2::IDENTITY); let bounds = graphene_std::renderer::graphic_list_bounding_box(&io.output, DAffine2::IDENTITY);
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
@@ -421,19 +421,19 @@ impl NodeRuntime {
} }
// Artboard thumbnail bounds come from the clipping rectangles, not the content union, since the renderer // Artboard thumbnail bounds come from the clipping rectangles, not the content union, since the renderer
// clips content to those rectangles so anything outside isn't visible // clips content to those rectangles so anything outside isn't visible
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Artboard>>>() { else if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<Artboard>>>() {
if update_thumbnails { if update_thumbnails {
let bounds = artboard_clip_bounds(&io.output); let bounds = artboard_clip_bounds(&io.output);
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
} }
} }
// Vector list: vector modifications // Vector list: vector modifications
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Vector>>>() { else if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<Vector>>>() {
// Insert the vector modify // Insert the vector modify
self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default()); self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default());
} }
// String list: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`) // String list: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`)
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<String>>>() { else if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<String>>>() {
if update_thumbnails { if update_thumbnails {
let bounds = graphene_std::renderer::text_list_bounding_box(&io.output, DAffine2::IDENTITY); let bounds = graphene_std::renderer::text_list_bounding_box(&io.output, DAffine2::IDENTITY);
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
@@ -282,11 +282,11 @@ impl BorrowTree {
self.nodes.insert(id, (node, path)); self.nodes.insert(id, (node, path));
} }
/// Returns the introspection record for that specific node, for example the cached value for a monitor node. The node path must match the document node path. /// Calls the `GNode::serialize` for that specific node, returning for example the captured io record for a monitor node. The node path must match the document node path.
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> { pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?;
let (_node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?; let (node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?;
Err(IntrospectError::NoData) node.serialize().ok_or(IntrospectError::NoData)
} }
pub fn get(&self, id: NodeId) -> Option<EdgeHandle> { pub fn get(&self, id: NodeId) -> Option<EdgeHandle> {
@@ -31,6 +31,11 @@ pub trait GNode<Input> {
GPoll::Final(Extent::Free) GPoll::Final(Extent::Free)
} }
/// Introspection access to node-resident records, for example the monitor's captured io; `None` for ordinary nodes.
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
None
}
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output> fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
where where
Input: InjectIndex + Copy, Input: InjectIndex + Copy,
@@ -148,6 +148,11 @@ where
unsafe { self.ptr.as_ref() }.extent(input) unsafe { self.ptr.as_ref() }.extent(input)
} }
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
// SAFETY: as in eval.
unsafe { self.ptr.as_ref() }.serialize()
}
fn eval_batch<'a>( fn eval_batch<'a>(
&self, &self,
input: &'a Input, input: &'a Input,
@@ -165,6 +170,7 @@ where
pub struct EdgeHandle { pub struct EdgeHandle {
node: Box<DynEdge>, node: Box<DynEdge>,
share: fn(&DynEdge) -> Box<DynEdge>, share: fn(&DynEdge) -> Box<DynEdge>,
serialize: fn(&DynEdge) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
ty: Type, ty: Type,
} }
@@ -185,11 +191,13 @@ impl EdgeHandle {
pub fn new_erased<N: ?Sized + 'static>(node: std::sync::Arc<N>, ty: Type) -> Self pub fn new_erased<N: ?Sized + 'static>(node: std::sync::Arc<N>, ty: Type) -> Self
where where
N: for<'c> GNode<ContextImpl<'c>>,
SharedEdge<N>: WasmNotSend + WasmNotSync, SharedEdge<N>: WasmNotSend + WasmNotSync,
{ {
Self { Self {
node: Box::new(SharedEdge::new(node)), node: Box::new(SharedEdge::new(node)),
share: |edge| Box::new(edge.downcast_ref::<SharedEdge<N>>().expect("share hook matches the stored edge type").share()), share: |edge| Box::new(edge.downcast_ref::<SharedEdge<N>>().expect("share hook matches the stored edge type").share()),
serialize: |edge| GNode::<ContextImpl>::serialize(edge.downcast_ref::<SharedEdge<N>>().expect("serialize hook matches the stored edge type")),
ty, ty,
} }
} }
@@ -202,10 +210,15 @@ impl EdgeHandle {
Self { Self {
node: (self.share)(&*self.node), node: (self.share)(&*self.node),
share: self.share, share: self.share,
serialize: self.serialize,
ty: self.ty.clone(), ty: self.ty.clone(),
} }
} }
pub fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
(self.serialize)(&*self.node)
}
pub fn downcast<T: 'static>(self) -> Result<SharedEdge<ErasedGNode<T>>, ConstructionError> { pub fn downcast<T: 'static>(self) -> Result<SharedEdge<ErasedGNode<T>>, ConstructionError> {
self.downcast_erased(edge_type::<T>()) self.downcast_erased(edge_type::<T>())
} }
+14
View File
@@ -234,6 +234,18 @@ pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeF
} }
}; };
let serialize_impl = match &parsed.attributes.serialize {
Some(path) => {
let data_refs = data_names.iter().map(|name| quote!(&self.#name));
quote! {
fn serialize(&self) -> Option<::std::sync::Arc<dyn ::std::any::Any + Send + Sync>> {
#path(#(#data_refs),*)
}
}
}
None => quote!(),
};
let batch_impl = match &parsed.attributes.batch { let batch_impl = match &parsed.attributes.batch {
Some(path) => quote! { Some(path) => quote! {
fn eval_batch<'__batch>( fn eval_batch<'__batch>(
@@ -432,6 +444,8 @@ pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeF
#extent_impl #extent_impl
#serialize_impl
#batch_impl #batch_impl
} }
}; };
+19 -1
View File
@@ -170,6 +170,24 @@ mod tests {
EvalScope::new(Some(0.5), None, None, generations, arena) 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);
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<ErasedGNode<u32>>);
assert!(handle.serialize().is_none(), "no record before the first eval");
let edge = handle.duplicate().downcast::<u32>().unwrap();
assert_eq!(edge.eval(&ctx), GPoll::Final(11));
let record = handle.serialize().expect("the eval landed a record");
let record = record.downcast_ref::<IORecord<CtxSnapshot, u32>>().expect("the record is the monitor io");
assert_eq!(record.output, 11);
}
#[test] #[test]
fn memoize_caches_across_evals() { fn memoize_caches_across_evals() {
let arena = Arena::new(1024); let arena = Arena::new(1024);
@@ -236,7 +254,7 @@ mod tests {
let edge = EdgeHandle::new(Arc::new(ValueNode("lent out".to_string())) as Arc<ErasedGNode<String>>); let edge = EdgeHandle::new(Arc::new(ValueNode("lent out".to_string())) as Arc<ErasedGNode<String>>);
let lending = EdgeHandle::new_ref(Arc::new(FrameMemoNode::new(edge.downcast::<String>().unwrap())) as Arc<ErasedLendGNode<String>>); let lending = EdgeHandle::new_ref(Arc::new(FrameMemoNode::new(edge.downcast::<String>().unwrap())) as Arc<ErasedLendGNode<String>>);
assert_eq!(*lending.ty(), Type::Ref(Box::new(concrete!(String)))); assert_eq!(*lending.ty(), core_types::registry::lend_edge_type::<String>());
let node = lending.downcast_lend::<String>().unwrap(); let node = lending.downcast_lend::<String>().unwrap();
let GPoll::Final(first) = node.eval(&ctx) else { let GPoll::Final(first) = node.eval(&ctx) else {