Recreate introspection data from the monitor's context snapshot

This commit is contained in:
Dennis Kobert
2026-08-27 13:53:25 +00:00
parent 79f3571b02
commit 50911a6a25
10 changed files with 302 additions and 253 deletions

View File

@@ -1,6 +1,6 @@
use crate::node_registry;
use core_types::arena::Arena;
use core_types::context::{ContextImpl, DynSlot, EvalScope, VarArg, VarArgLink, VarArgSlots};
use core_types::context::{ContextImpl, DynSlot, EvalScope, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, VarArg, VarArgLink, VarArgSlots};
use core_types::gpoll::GPoll;
use core_types::node::Node;
use core_types::registry::{EdgeHandle, ErasedNode};
@@ -144,21 +144,76 @@ impl DynamicExecutor {
Ok(ResolvedDocumentNodeTypesDelta { add, remove })
}
/// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path.
/// A monitor's record capture materializes here against the arena, inside
/// the introspection window, so consumers downcast the legacy value type
/// directly: a rank-0 capture yields its element and a level capture its
/// legacy list. The captured input context stays on the serialized io
/// record for consumers that need it.
/// Calls the `Node::serialize` for that specific node. A monitor serializes
/// its stored context snapshot, and this entry recreates the monitored
/// value from it as the legacy value the editor's downcasts expect: a
/// rank-0 wire yields its element and a leveled wire its legacy list.
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
let result = self.tree.introspect(node_path)?;
if let Some(io) = result.downcast_ref::<core_types::memo::IORecord<core_types::context::CtxSnapshot, core_types::record::RecordCapture>>() {
let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner);
return graphic_types::boundary::capture_to_legacy(&io.output, &arena).map(Arc::from).ok_or(IntrospectError::NoData);
if result.downcast_ref::<core_types::context::CtxSnapshot>().is_some() {
return self
.introspect_with(node_path, |layout, batch, arena| graphic_types::boundary::batch_to_legacy(layout, batch, arena))
.map(Arc::from);
}
Ok(result)
}
/// Re-evaluates the monitored edge at `node_path` with its stored context
/// snapshot and hands the resulting resident batch to `read`, inside the
/// introspection window. The monitor stores only the context; the value is
/// recreated against current source data, so a read right after an
/// execution serves out of the warm memo entries.
pub fn introspect_with<R>(
&self,
node_path: &[NodeId],
read: impl FnOnce(&core_types::record::Layout, core_types::node::RecordBatch<'_>, &Arena) -> Option<R>,
) -> Result<R, IntrospectError> {
let serialized = self.tree.introspect(node_path)?;
let Some(snapshot) = serialized.downcast_ref::<core_types::context::CtxSnapshot>() else {
return Err(IntrospectError::NoData);
};
let edge = self
.tree
.get_by_path(node_path)
.and_then(EdgeHandle::record_edge)
.ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?;
let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner);
core_types::record::stack::reserve(self.tree.stack_need());
let generations = self.runtime.snapshot();
let scope = EvalScope::new(snapshot.try_real_time(), snapshot.try_animation_time(), snapshot.try_pointer_position(), &generations, &arena);
let Some(ctx) = snapshot.rehydrate(&scope) else {
return Err(IntrospectError::NoData);
};
let layout = core_types::node::Node::<ContextImpl>::layout(&edge);
let mark = core_types::record::stack::sp();
let result = if layout.depth > 0 {
match core_types::record::materialize_level(&edge, &ctx, &arena) {
core_types::record::LevelStatus::Batch(batch, _) => read(layout, batch, &arena),
_ => None,
}
} else {
match core_types::node::Node::eval(&edge, &ctx) {
GPoll::Final(value) | GPoll::Partial(value) => {
let rec = layout.rec(&value);
// SAFETY: the eval produced one live record of the edge's layout.
let batch = unsafe { core_types::node::RecordBatch::new(rec.ptr(), 1, layout) };
read(layout, batch, &arena)
}
GPoll::Fallback(boxed) => {
let (value, _) = *boxed;
let rec = layout.rec(&value);
// SAFETY: as for the final arm.
let batch = unsafe { core_types::node::RecordBatch::new(rec.ptr(), 1, layout) };
read(layout, batch, &arena)
}
_ => None,
}
};
// SAFETY: the read finished, so no record above the mark is live.
unsafe { core_types::record::stack::rewind(mark) };
result.ok_or(IntrospectError::NoData)
}
pub fn input_type(&self) -> Option<Type> {
self.typing_context.type_of(self.output).map(|node_io| node_io.call_argument.clone())
}
@@ -325,6 +380,12 @@ impl BorrowTree {
self.nodes.get(&id).map(|(node, _)| node.duplicate())
}
/// The edge handle for the node at a document path.
pub fn get_by_path(&self, node_path: &[NodeId]) -> Option<EdgeHandle> {
let (id, _) = self.source_map.get(node_path)?;
self.get(*id)
}
/// Evaluate a node of the [`BorrowTree`], downcasting its edge to the expected output type.
pub fn eval<I, T: 'static>(&self, id: NodeId, input: &I) -> Option<GPoll<T>>
where

View File

@@ -1004,6 +1004,54 @@ impl CtxSnapshot {
pub fn generations(&self) -> &[(SourceId, u64)] {
&self.generations
}
/// Rebuilds the captured context against `scope`, allocating the borrowed
/// chains in the scope's arena; `None` reports arena exhaustion. The
/// scope carries the serving generation, so the recreated evaluation reads
/// current source data under the captured addressing.
pub fn rehydrate<'e>(&'e self, scope: &'e EvalScope<'e>) -> Option<ContextImpl<'e>> {
let arena = scope.arena();
let mut ctx = ContextImpl::root(scope);
if let Some(footprint) = self.footprint {
let (footprint, _) = arena.alloc(footprint)?;
ctx = ctx.with_footprint(footprint);
}
if let Some(levels) = &self.index {
let mut outer = None;
for &index in levels.iter().skip(1).rev() {
let (link, _) = arena.alloc(IndexLink { index: index as u64, outer })?;
outer = Some(link);
}
ctx.index = IndexLink {
index: levels.first().copied().unwrap_or_default() as u64,
outer,
};
}
if let Some(positions) = &self.positions {
let mut head = None;
for &position in positions.iter().rev() {
let (link, _) = arena.alloc(PositionLink { position, outer: head })?;
head = Some(link);
}
if let Some(head) = head {
ctx = ctx.with_position(head);
}
}
let mut varargs = None;
for args in self.varargs.iter().rev() {
let slots: Vec<DynSlot<'e>> = args.iter().map(|slot| &**slot as DynSlot<'e>).collect();
let slots = arena.alloc_slice_copy(&slots)?;
let (link, _) = arena.alloc(VarArgLink {
args: VarArgSlots::Slice(slots),
outer: varargs,
})?;
varargs = Some(link);
}
if let Some(varargs) = varargs {
ctx = ctx.with_varargs(varargs);
}
Some(ctx)
}
}
impl ExtractFootprint for CtxSnapshot {

View File

@@ -1554,96 +1554,6 @@ pub unsafe fn interrupt_frame(entry: usize, layout: &Layout) {
claim_frame(layout);
}
/// A captured record: the layout plus a generation-checked handle to the
/// arena copy, materialized by the introspection holder, which owns the
/// arena. A dead generation materializes to `None`, never to a stale read.
#[derive(Clone)]
pub struct RecordCapture {
layout: Layout,
lanes: usize,
bytes: crate::arena::ArenaWeak<Box<[u8]>>,
}
impl std::fmt::Debug for RecordCapture {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("RecordCapture(..)")
}
}
impl RecordCapture {
/// # Safety
/// `rec` must be a live record of `layout`.
pub unsafe fn capture(layout: &Layout, rec: Rec, arena: &crate::arena::Arena) -> Option<RecordCapture> {
let bytes = unsafe { copy_record_bytes(layout, rec) };
arena.alloc(bytes).map(|(_, weak)| RecordCapture {
layout: layout.clone(),
lanes: 1,
bytes: weak,
})
}
/// Captures every lane of a leveled wire's batch.
///
/// # Safety
/// `batch` must hold live records of `layout`.
pub unsafe fn capture_level(layout: &Layout, batch: crate::node::RecordBatch<'_>, arena: &crate::arena::Arena) -> Option<RecordCapture> {
let stride = layout.lane_stride();
let mut bytes = vec![0u8; batch.len() * stride].into_boxed_slice();
for lane in 0..batch.len() {
// SAFETY: both sides hold `len` lanes at the shared layout's stride.
unsafe { std::ptr::copy_nonoverlapping(batch.get(lane).rec().ptr(), bytes.as_mut_ptr().add(lane * stride), stride) };
}
arena.alloc(bytes).map(|(_, weak)| RecordCapture {
layout: layout.clone(),
lanes: batch.len(),
bytes: weak,
})
}
pub fn layout(&self) -> &Layout {
&self.layout
}
/// The captured lane count: one for a rank-0 capture, the whole extent
/// for a level capture.
pub fn lanes(&self) -> usize {
self.lanes
}
/// A batch view over the captured records, alive while the arena holds
/// the capture's generation.
pub fn batch<'a>(&'a self, arena: &'a crate::arena::Arena) -> Option<crate::node::RecordBatch<'a>> {
let bytes = self.bytes.upgrade(arena)?;
// SAFETY: the constructors store `lanes` records of `layout`.
Some(unsafe { crate::node::RecordBatch::new(bytes.as_ptr(), self.lanes, &self.layout) })
}
/// The captured element of the first lane, cloned out through the
/// layout's erased glue.
pub fn materialize_element(&self, arena: &crate::arena::Arena) -> Option<Box<dyn std::any::Any + Send + Sync>> {
let bytes = self.bytes.upgrade(arena)?;
match self.lanes {
0 => None,
_ => Some(unsafe { (self.layout.element.clone_out)(bytes.as_ptr()) }),
}
}
/// The first lane's attributes, read out through the layout's erased glue.
pub fn materialize(&self, arena: &crate::arena::Arena) -> Option<Vec<(&'static str, Box<dyn crate::list::AnyAttributeValue>)>> {
let bytes = self.bytes.upgrade(arena)?;
if self.lanes == 0 {
return Some(Vec::new());
}
Some(
self.layout
.fields
.iter()
.map(|field| (field.name, unsafe { (field.read_erased)(bytes.as_ptr().add(field.offset)) }))
.collect(),
)
}
}
/// A record deep-copied out of its evaluation: the packed bytes plus owned
/// clones of every parked payload, replayable into a later evaluation's
/// storage through the layout's erased glue. The layout stays with the
@@ -2088,6 +1998,13 @@ impl<'a, T: 'static> crate::lane::LaneSource for RunView<'a, T> {
}
}
impl<T: crate::render_complexity::RenderComplexity + 'static> crate::render_complexity::RenderComplexity for RunView<'_, T> {
fn render_complexity(&self) -> usize {
use crate::lane::LaneSource;
(0..self.lane_count()).filter_map(|lane| self.element(lane)).map(crate::render_complexity::RenderComplexity::render_complexity).sum()
}
}
impl<T: crate::bounds::BoundingBox + 'static> crate::bounds::BoundingBox for RunView<'_, T> {
fn bounding_box(&self, transform: glam::DAffine2, include_stroke: bool) -> crate::bounds::RenderBoundingBox {
crate::bounds::lane_bounding_box(self, transform, include_stroke)

View File

@@ -303,6 +303,12 @@ impl EdgeHandle {
self.downcast_erased(record_edge_type::<T>())
}
/// The erased record edge, for callers that dispatch on the layout rather
/// than a static element type. `None` for a plain (non-record) edge.
pub fn record_edge(self) -> Option<SharedEdge<ErasedRecordNode>> {
self.node.downcast::<SharedEdge<ErasedRecordNode>>().ok().map(|edge| *edge)
}
pub fn downcast_erased<N: ?Sized + 'static>(self, expected: Type) -> Result<SharedEdge<N>, ConstructionError> {
let found = self.ty;
self.node.downcast::<SharedEdge<N>>().map(|edge| *edge).map_err(|_| ConstructionError::Type {

View File

@@ -10,7 +10,7 @@ use core_types::arena::Arena;
use core_types::context::InjectIndex;
use core_types::gpoll::{Finality, GraphError};
use core_types::node::Node;
use core_types::record::{Group, GroupContent, GroupItem, LevelStatus, RecordCapture, RecordValue, materialize_level};
use core_types::record::{Group, GroupContent, GroupItem, LevelStatus, RecordValue, materialize_level};
use core_types::uuid::NodeId;
use glam::{DAffine2, DVec2};
use vector_types::GradientStops;
@@ -46,36 +46,35 @@ where
}
}
/// The captured wire as the legacy value the editor's downcasts expect: a
/// rank-0 capture is its element, a leveled `Graphic` capture becomes its
/// legacy list through the group bridge, and another element type becomes a
/// legacy list of that element. `None` for an element type outside the
/// legacy vocabulary or a capture whose arena generation has passed.
pub fn capture_to_legacy(capture: &RecordCapture, arena: &Arena) -> Option<Box<dyn std::any::Any + Send + Sync>> {
if capture.layout().depth == 0 {
// The group-carrying types legacy-convert while the capture is still
/// The resident batch as the legacy value the editor's downcasts expect: a
/// rank-0 wire is its element, a leveled `Graphic` wire becomes its legacy
/// list through the group bridge, and another element type becomes a legacy
/// list of that element. `None` for an element type outside the legacy
/// vocabulary or an empty rank-0 batch.
pub fn batch_to_legacy(layout: &core_types::record::Layout, batch: core_types::node::RecordBatch<'_>, _arena: &Arena) -> Option<Box<dyn std::any::Any + Send + Sync>> {
if layout.depth == 0 {
// The group-carrying types legacy-convert while the batch is
// resident; the deep clone-out would hand back the unreadable owned
// form.
if capture.lanes() > 0 {
let element = &capture.layout().element;
if element.type_id == std::any::TypeId::of::<Graphic>() {
let batch = capture.batch(arena)?;
// SAFETY: the layout records the element type, and a parked
// element stores its reference at offset 0.
let graphic = unsafe { core_types::record::borrow_element::<Graphic>(batch.get(0).rec()) };
return Some(Box::new(crate::graphic::map_groups_to_legacy(graphic)));
}
if element.type_id == std::any::TypeId::of::<Artboard>() {
let batch = capture.batch(arena)?;
// SAFETY: as for the graphic arm.
let artboard = unsafe { core_types::record::borrow_element::<Artboard>(batch.get(0).rec()) };
return Some(Box::new(artboard.with_legacy_groups()));
}
if batch.is_empty() {
return None;
}
return capture.materialize_element(arena);
let element = &layout.element;
if element.type_id == std::any::TypeId::of::<Graphic>() {
// SAFETY: the layout records the element type, and a parked
// element stores its reference at offset 0.
let graphic = unsafe { core_types::record::borrow_element::<Graphic>(batch.get(0).rec()) };
return Some(Box::new(crate::graphic::map_groups_to_legacy(graphic)));
}
if element.type_id == std::any::TypeId::of::<Artboard>() {
// SAFETY: as for the graphic arm.
let artboard = unsafe { core_types::record::borrow_element::<Artboard>(batch.get(0).rec()) };
return Some(Box::new(artboard.with_legacy_groups()));
}
// SAFETY: lane 0 is a live record of `layout`.
return Some(unsafe { (element.clone_out)(batch.get(0).rec().ptr()) });
}
let batch = capture.batch(arena)?;
// SAFETY: the captured bytes live in the arena for the capture's generation.
// SAFETY: the caller's batch is resident for the read.
let item = unsafe { GroupItem::from_resident(batch) };
fn typed<T: Clone + Send + Sync + 'static>(item: &GroupItem) -> Option<Box<dyn std::any::Any + Send + Sync>> {
run_to_legacy_list::<T>(item).map(|list| Box::new(list) as Box<dyn std::any::Any + Send + Sync>)

View File

@@ -2966,6 +2966,46 @@ impl Render for List<String> {
}
}
impl Render for RunView<'_, Artboard> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_artboard_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_artboard_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
collect_artboard_metadata(self, metadata, footprint)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_artboard_upstream_click_targets(self, click_targets)
}
fn contains_artboard(&self) -> bool {
self.lane_count() > 0
}
}
impl Render for RunView<'_, String> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_text_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_text_vello(self, scene, transform, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_text_metadata(self, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_text_upstream_click_targets(self, click_targets)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SvgSegment {
Slice(&'static str),

View File

@@ -3,8 +3,7 @@ use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll, ModifyIndex};
use core_types::frame_table::{FrameTable, Lookup};
use core_types::gpoll::{Finality, GPoll};
use core_types::graphene_hash::CacheHash;
use core_types::memo::IORecord;
use core_types::record::{LevelStatus, OwnedRecord, RecordCapture, RecordValue, claim_frame, copy_record_bytes, serve_frame};
use core_types::record::{LevelStatus, OwnedRecord, RecordValue, claim_frame, copy_record_bytes, serve_frame};
use core_types::registry::cache_key;
use std::sync::Arc;
use std::sync::Mutex;
@@ -183,70 +182,27 @@ fn frame_memo<'e>(
}
}
type MonitorValue = Arc<Mutex<Option<IORecord<CtxSnapshot, RecordCapture>>>>;
type MonitorValue = Arc<Mutex<Option<CtxSnapshot>>>;
/// The Monitor node is used by the editor to access the data flowing through it.
/// The Monitor node is used by the editor to access the data flowing through
/// it. It stores only the evaluation context: the output is pure over
/// (context, source generations), so introspection recreates it by
/// re-evaluating this edge with the rehydrated snapshot.
#[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"))]
fn monitor<'e>(
ctx: impl Ctx + DeriveCtx + ExtractAll + ExtractArena<'e> + ModifyIndex + Copy,
#[data] io: MonitorValue,
content: impl Node<Context<'_>, Output = RecordValue<'e>>,
) -> GPoll<RecordValue<'e>> {
let entry_sp = core_types::record::stack::sp();
let publish = |captured: Option<RecordCapture>| {
*io.lock().unwrap() = captured.map(|output| IORecord {
input: CtxSnapshot::capture(ctx),
output,
});
};
// A leveled capture covers the whole extent, which the materialization below
// computes lane by lane. Serving THIS lane out of that batch rather than
// evaluating the content separately is what keeps the cost linear: the extra
// eval would double the work under every enclosing monitor.
if content.layout().depth > 0 && ctx.index() == 0 {
return match content.materialize_level(ctx, ctx.arena()) {
LevelStatus::Batch(batch, finality) => {
// SAFETY: the batch came from this edge, so it carries the edge's layout.
publish(unsafe { RecordCapture::capture_level(content.layout(), batch, ctx.arena()) });
let Some(lane) = (!batch.is_empty()).then(|| batch.get(0)) else {
// An empty level ends here; the past-end signal serves drains.
claim_frame(content.layout());
return GPoll::Error(Box::new(core_types::gpoll::GraphError::past_end()));
};
// SAFETY: the lane is a live record of this edge's layout.
let value = unsafe { serve_frame(content.layout(), lane.rec().ptr()) };
match finality {
Finality::AllFinal => GPoll::Final(value),
Finality::Partial => GPoll::Partial(value),
}
}
// A valueless materialization leaves frames no one reads, so the
// entry mark, not the current top, is what this node's frame sits on.
LevelStatus::Pending => {
// SAFETY: nothing borrows the frames above the entry mark.
unsafe { core_types::record::interrupt_frame(entry_sp, content.layout()) };
GPoll::Pending
}
LevelStatus::Error(error) => {
// SAFETY: nothing borrows the frames above the entry mark.
unsafe { core_types::record::interrupt_frame(entry_sp, content.layout()) };
GPoll::Error(Box::new(error))
}
};
if ctx.index() == 0 {
*io.lock().unwrap() = Some(CtxSnapshot::capture(ctx));
}
let result = content.eval(&ctx);
if ctx.index() == 0
&& let GPoll::Final(value) | GPoll::Partial(value) = &result
{
// SAFETY: the value came from this edge, so it carries the edge's layout.
publish(unsafe { RecordCapture::capture(content.layout(), content.layout().rec(value), ctx.arena()) });
}
result
content.eval(&ctx)
}
fn serialize_monitor(io: &MonitorValue) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
let io = io.lock().unwrap();
io.as_ref().map(|io| Arc::new(io.clone()) as Arc<dyn std::any::Any + Send + Sync>)
io.as_ref().map(|snapshot| Arc::new(snapshot.clone()) as Arc<dyn std::any::Any + Send + Sync>)
}
#[cfg(test)]
@@ -299,7 +255,7 @@ mod tests {
}
#[test]
fn monitor_serialize_exposes_the_capture_through_the_edge() {
fn monitor_serialize_recreates_the_value_from_its_snapshot() {
let arena = Arena::new(1024).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -308,25 +264,25 @@ mod tests {
let layout = element_layout::<u32>();
let monitor = MonitorNode::new(core_types::record::RecordLift::<u32, _>::new(ValueNode(11u32)), &layout);
let handle = EdgeHandle::new_record::<u32>(Arc::new(monitor) as Arc<ErasedRecordNode>);
assert!(handle.serialize().is_none(), "no capture before the first eval");
assert!(handle.serialize().is_none(), "no snapshot before the first eval");
let edge = handle.duplicate().downcast_record::<u32>().unwrap();
let GPoll::Final(_) = edge.eval(&ctx) else {
panic!("expected a final record");
};
let io = handle.serialize().expect("the eval landed a capture");
let io = io.downcast_ref::<IORecord<CtxSnapshot, RecordCapture>>().expect("the capture is the monitor io");
assert!(
core_types::context::ExtractFootprint::try_footprint(&io.input).is_none(),
"the root context has no footprint to capture"
);
let element = io.output.materialize_element(&arena).expect("the capture materializes inside the window");
assert_eq!(*element.downcast_ref::<u32>().unwrap(), 11);
let io = handle.serialize().expect("the eval landed a snapshot");
let snapshot = io.downcast_ref::<CtxSnapshot>().expect("the monitor serializes its context snapshot");
let ctx = snapshot.rehydrate(&scope).expect("the arena holds the chains");
let GPoll::Final(value) = edge.eval(&ctx) else {
panic!("expected a final record");
};
// SAFETY: the eval produced a live record of the edge's layout.
assert_eq!(unsafe { layout.rec(&value).element::<u32>() }, 11);
}
#[test]
fn a_leveled_monitor_captures_the_whole_extent() {
fn a_leveled_monitor_recreates_the_whole_extent() {
let arena = Arena::new(1 << 12).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -342,10 +298,13 @@ mod tests {
panic!("expected a final record");
};
let io = handle.serialize().expect("the eval landed a capture");
let io = io.downcast_ref::<IORecord<CtxSnapshot, RecordCapture>>().expect("the capture is the monitor io");
assert_eq!(io.output.lanes(), 3, "the capture holds the whole extent, not the addressed lane");
let batch = io.output.batch(&arena).expect("the capture lives in this generation");
let io = handle.serialize().expect("the eval landed a snapshot");
let snapshot = io.downcast_ref::<CtxSnapshot>().expect("the monitor serializes its context snapshot");
let ctx = snapshot.rehydrate(&scope).expect("the arena holds the chains");
let LevelStatus::Batch(batch, _) = core_types::record::materialize_level(&edge, &ctx, &arena) else {
panic!("expected a materialized level");
};
assert_eq!(batch.len(), 3, "the recreation holds the whole extent, not the addressed lane");
let lanes = unsafe { core_types::node::List::<u32>::new(batch) };
let values: Vec<u32> = (0..lanes.len()).map(|lane| *lanes.element_ref(lane)).collect();
assert_eq!(values, vec![10, 20, 30]);

View File

@@ -2576,16 +2576,16 @@ mod tests {
}
#[test]
fn record_monitor_forwards_and_captures_for_the_introspection_window() {
let mut arena = Arena::new(1024).unwrap();
fn record_monitor_forwards_and_recreates_from_its_snapshot() {
let arena = Arena::new(1024).unwrap();
let generations = [];
let layout = f64_layout(&["opacity"]);
reserve_for(&[&layout, &layout]);
let monitor = crate::memo::MonitorNode::new(f64_record_source(&layout, 4., vec![(layout.offset_of("opacity", 0).unwrap(), 0.25)]), &layout);
let scope = scope_fixture(&generations, &arena);
{
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let GPoll::Final(value) = monitor.eval(&ctx) else {
panic!("expected a final record");
@@ -2594,16 +2594,14 @@ mod tests {
}
let io = Node::<ContextImpl>::serialize(&monitor).unwrap();
let io = io
.downcast_ref::<core_types::memo::IORecord<core_types::context::CtxSnapshot, core_types::record::RecordCapture>>()
.unwrap();
let fields = io.output.materialize(&arena).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].0, "opacity");
assert_eq!(*fields[0].1.as_any().downcast_ref::<f64>().unwrap(), 0.25);
arena.reset();
assert!(io.output.materialize(&arena).is_none(), "a dead generation materializes to nothing");
let snapshot = io.downcast_ref::<core_types::context::CtxSnapshot>().expect("the monitor serializes its context snapshot");
let ctx = snapshot.rehydrate(&scope).expect("the arena holds the chains");
let GPoll::Final(value) = monitor.eval(&ctx) else {
panic!("expected a final record");
};
let rec = layout.rec(&value);
assert_eq!(unsafe { rec.element::<f64>() }, 4.);
assert_eq!(unsafe { rec.read::<f64>(layout.offset_of("opacity", 0).unwrap()) }, 0.25);
}
#[test]

View File

@@ -684,7 +684,7 @@ mod tests {
}
#[test]
fn a_level_capture_converts_to_its_legacy_list() {
fn a_level_batch_converts_to_its_legacy_list() {
let arena = Arena::new(1 << 16).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
@@ -695,8 +695,7 @@ mod tests {
let record::LevelStatus::Batch(batch, _) = record::materialize_level(&source, &ctx, &arena) else {
panic!("expected a batch");
};
let capture = unsafe { record::RecordCapture::capture_level(&layout, batch, &arena) }.expect("the capture parks in the arena");
let legacy = graphic_types::boundary::capture_to_legacy(&capture, &arena).expect("f64 is in the legacy vocabulary");
let legacy = graphic_types::boundary::batch_to_legacy(&layout, batch, &arena).expect("f64 is in the legacy vocabulary");
let list = legacy.downcast_ref::<List<f64>>().unwrap();
assert_eq!(list.len(), 2);
assert_eq!(list.element(0).copied(), Some(1.5));