Reuse retained layout subtrees across confirmed document updates

This commit is contained in:
Kinneyzhang 2026-09-05 13:39:28 +08:00
parent 220f27803e
commit 14c1698c41
5 changed files with 2018 additions and 139 deletions

551
native/src/evaluation.rs Normal file
View File

@ -0,0 +1,551 @@
//! One immutable tree of current evaluation uses, scoped to an exact document pair.
use std::cell::{Cell, RefCell};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use super::source_topology::SourceAddress;
use super::{
BoxOverride, LayoutContext, LayoutNode, LayoutTape, LocalStep, Rendered, RetainedDocument,
SourceChanges,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct EvalWork {
pub(crate) lookups: u64,
pub(crate) hits: u64,
pub(crate) body_runs: u64,
pub(crate) records_created: u64,
pub(crate) caller_edges_written: u64,
pub(crate) occurrence_slots_written: u64,
pub(crate) child_slots_visited: u64,
pub(crate) source_path_steps_copied: u64,
pub(crate) use_path_steps_copied: u64,
pub(crate) dirty_seeds: u64,
pub(crate) dirty_prefixes_visited: u64,
pub(crate) dirty_addresses_inserted: u64,
pub(crate) topology_lookups: u64,
pub(crate) incoming_uses_visited: u64,
pub(crate) height_queries: u64,
}
thread_local! {
static WORK: Cell<EvalWork> = Cell::new(EvalWork::default());
}
impl EvalWork {
pub(crate) fn accumulate(&mut self, other: Self) {
macro_rules! add {
($($field:ident),+ $(,)?) => { $(self.$field = self.$field.checked_add(other.$field)
.expect("native evaluation work counter overflow");)+ };
}
add!(
lookups,
hits,
body_runs,
records_created,
caller_edges_written,
occurrence_slots_written,
child_slots_visited,
source_path_steps_copied,
use_path_steps_copied,
dirty_seeds,
dirty_prefixes_visited,
dirty_addresses_inserted,
topology_lookups,
incoming_uses_visited,
height_queries
);
}
}
pub(crate) fn reset_work() {
WORK.set(EvalWork::default());
}
pub(crate) fn work() -> EvalWork {
WORK.get()
}
fn record(delta: EvalWork) {
let mut work = WORK.get();
work.accumulate(delta);
WORK.set(work);
}
pub(super) fn record_height_query() {
record(EvalWork {
height_queries: 1,
..EvalWork::default()
});
}
#[derive(Clone, Debug)]
pub(super) struct SourceView<'a> {
pub(super) node: &'a LayoutNode,
pub(super) address: SourceAddress,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Phase {
FlexMeasure,
FlexMinWidth,
FlexAutoMinContent,
FlexMaxWidth,
FlexBasisContent,
FlexBasisWidth,
ContentIntrinsic,
FlexCrossProbe,
FlexFinal,
WindowFull,
WindowFallback,
WindowChildFull,
WindowChildPartial,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum UseStep {
Child(LocalStep),
Phase(Phase),
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct UseSlot {
path: Vec<UseStep>,
occurrence: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct EvalRequest {
context: LayoutContext,
intrinsic: bool,
size_override: Option<BoxOverride>,
}
#[derive(Debug)]
struct EvalRecord {
source: SourceAddress,
request: EvalRequest,
rendered: Rendered,
children: BTreeMap<UseSlot, Arc<EvalRecord>>,
}
/// Owns the current document and current evaluation tree, never a previous frame.
#[derive(Debug)]
pub(crate) struct RetainedFrame {
document: Arc<RetainedDocument>,
root: Arc<EvalRecord>,
}
impl RetainedFrame {
pub(crate) fn materialize_tape(&self) -> LayoutTape {
self.root
.rendered
.clone()
.into_tape(self.document.style_count)
}
}
#[derive(Debug)]
struct BuildingRecord {
slot: UseSlot,
source: SourceAddress,
request: EvalRequest,
previous: Option<Arc<EvalRecord>>,
children: BTreeMap<UseSlot, Arc<EvalRecord>>,
occurrences: BTreeMap<Vec<UseStep>, usize>,
}
#[derive(Debug)]
struct RenderTxn {
previous: Option<Arc<EvalRecord>>,
dirty: BTreeSet<SourceAddress>,
stack: Vec<BuildingRecord>,
root: Option<Arc<EvalRecord>>,
}
impl RenderTxn {
fn begin(
&mut self,
source: &SourceAddress,
path: &[UseStep],
request: EvalRequest,
) -> Option<Rendered> {
record(EvalWork {
lookups: 1,
..EvalWork::default()
});
let (slot, previous) = if let Some(parent) = self.stack.last_mut() {
let occurrence = parent.occurrences.entry(path.to_vec()).or_default();
let slot = UseSlot {
path: path.to_vec(),
occurrence: *occurrence,
};
*occurrence += 1;
record(EvalWork {
occurrence_slots_written: 1,
use_path_steps_copied: (path.len() * 2) as u64,
..EvalWork::default()
});
let previous = parent
.previous
.as_ref()
.and_then(|old| old.children.get(&slot))
.cloned();
(slot, previous)
} else {
(
UseSlot {
path: Vec::new(),
occurrence: 0,
},
self.previous.clone(),
)
};
if let Some(old) = previous.as_ref().filter(|old| {
old.source == *source && old.request == request && !self.dirty.contains(source)
}) {
record(EvalWork {
hits: 1,
..EvalWork::default()
});
let rendered = old.rendered.clone();
self.attach(slot, Arc::clone(old));
return Some(rendered);
}
record(EvalWork {
body_runs: 1,
..EvalWork::default()
});
self.stack.push(BuildingRecord {
slot,
source: source.clone(),
request,
previous,
children: BTreeMap::new(),
occurrences: BTreeMap::new(),
});
None
}
fn finish(&mut self, rendered: &Result<Rendered, String>) {
let current = self.stack.pop().expect("native evaluation builder stack");
if let Ok(rendered) = rendered {
record(EvalWork {
records_created: 1,
..EvalWork::default()
});
self.attach(
current.slot,
Arc::new(EvalRecord {
source: current.source,
request: current.request,
rendered: rendered.clone(),
children: current.children,
}),
);
}
}
fn attach(&mut self, slot: UseSlot, record: Arc<EvalRecord>) {
if let Some(parent) = self.stack.last_mut() {
record_work_edge();
assert!(
parent.children.insert(slot, record).is_none(),
"unique caller use slot"
);
} else {
self.root = Some(record);
}
}
}
fn record_work_edge() {
record(EvalWork {
caller_edges_written: 1,
..EvalWork::default()
});
}
/// Explicit source/callsite context. Interior mutability is transaction-local;
/// no borrow of the builder stack spans a recursive renderer call.
#[derive(Debug)]
pub(super) struct RenderScope<'a> {
pub(super) view: SourceView<'a>,
pub(super) resolver: Option<&'a RetainedDocument>,
transaction: Option<&'a RefCell<RenderTxn>>,
path: Vec<UseStep>,
}
impl Clone for RenderScope<'_> {
fn clone(&self) -> Self {
if self.transaction.is_some() {
record(EvalWork {
use_path_steps_copied: self.path.len() as u64,
..EvalWork::default()
});
}
Self {
view: self.view.clone(),
resolver: self.resolver,
transaction: self.transaction,
path: self.path.clone(),
}
}
}
impl<'a> RenderScope<'a> {
pub(super) fn uncached(node: &'a LayoutNode, resolver: Option<&'a RetainedDocument>) -> Self {
Self {
view: SourceView {
node,
address: SourceAddress {
owner_id: node.node_id().unwrap_or(0),
path: Arc::from([]),
},
},
resolver,
transaction: None,
path: Vec::new(),
}
}
pub(super) fn resolve(&self) -> Result<Self, String> {
let mut resolved = self.clone();
if let LayoutNode::NodeRef { node_id } = self.view.node {
if let Some(resolver) = self.resolver {
resolved.view = SourceView {
node: resolver.resolve(self.view.node)?,
address: SourceAddress {
owner_id: *node_id,
path: Arc::from([]),
},
};
}
}
Ok(resolved)
}
pub(super) fn child(&self, step: LocalStep, node: &'a LayoutNode) -> Self {
if self.transaction.is_none() {
return Self::uncached(node, self.resolver);
}
let mut address = self.view.address.path.to_vec();
address.push(step);
let mut path = self.path.clone();
path.push(UseStep::Child(step));
if self.transaction.is_some() {
record(EvalWork {
child_slots_visited: 1,
source_path_steps_copied: address.len() as u64,
use_path_steps_copied: path.len() as u64,
..EvalWork::default()
});
}
Self {
view: SourceView {
node,
address: SourceAddress {
owner_id: self.view.address.owner_id,
path: address.into(),
},
},
resolver: self.resolver,
transaction: self.transaction,
path,
}
}
pub(super) fn phase(&self, phase: Phase) -> Self {
if self.transaction.is_none() {
return self.clone();
}
let mut scoped = self.clone();
scoped.path.push(UseStep::Phase(phase));
if self.transaction.is_some() {
record(EvalWork {
use_path_steps_copied: 1,
..EvalWork::default()
});
}
scoped
}
pub(super) fn render(
&self,
context: LayoutContext,
intrinsic: bool,
size_override: Option<BoxOverride>,
) -> Result<Rendered, String> {
let source = self.resolve()?;
if let Some(transaction) = source.transaction {
let request = EvalRequest {
context,
intrinsic,
size_override,
};
if let Some(rendered) =
transaction
.borrow_mut()
.begin(&source.view.address, &source.path, request)
{
return Ok(rendered);
}
}
// Descendant slots are local to this caller, even on a parent request miss.
let body_scope = Self {
path: Vec::new(),
..source
};
let rendered = super::render_node_body(&body_scope, context, intrinsic, size_override);
if let Some(transaction) = body_scope.transaction {
transaction.borrow_mut().finish(&rendered);
}
rendered
}
}
fn dirty_sources(document: &RetainedDocument, changes: &SourceChanges) -> BTreeSet<SourceAddress> {
fn mark(address: SourceAddress, dirty: &mut BTreeSet<SourceAddress>, owners: &mut Vec<u64>) {
for length in (0..=address.path.len()).rev() {
record(EvalWork {
dirty_prefixes_visited: 1,
source_path_steps_copied: length as u64,
..EvalWork::default()
});
let prefix = SourceAddress {
owner_id: address.owner_id,
path: Arc::from(&address.path[..length]),
};
if dirty.insert(prefix) {
record(EvalWork {
dirty_addresses_inserted: 1,
..EvalWork::default()
});
if length == 0 {
owners.push(address.owner_id);
}
}
}
}
let mut dirty = BTreeSet::new();
let mut owners = Vec::new();
changes.visit_changed_slots(|owner_id, _, _, slot, _| {
record(EvalWork {
dirty_seeds: 1,
..EvalWork::default()
});
let path: Arc<[LocalStep]> = match slot {
0 => Arc::from([]),
1 => Arc::from([LocalStep::BoxChild]),
_ => unreachable!("validated native source slot"),
};
mark(SourceAddress { owner_id, path }, &mut dirty, &mut owners);
});
while let Some(owner) = owners.pop() {
record(EvalWork {
topology_lookups: 1,
..EvalWork::default()
});
for incoming in document.topology.incoming_uses(owner) {
record(EvalWork {
incoming_uses_visited: 1,
..EvalWork::default()
});
let mut path = incoming.parent.path.to_vec();
path.push(incoming.child_slot);
record(EvalWork {
source_path_steps_copied: path.len() as u64,
..EvalWork::default()
});
mark(
SourceAddress {
owner_id: incoming.parent.owner_id,
path: path.into(),
},
&mut dirty,
&mut owners,
);
}
}
dirty
}
impl RetainedDocument {
pub(crate) fn render_frame(
self: &Arc<Self>,
previous: Option<&RetainedFrame>,
changes: Option<&SourceChanges>,
context: LayoutContext,
root_width_override: Option<i64>,
) -> Result<Arc<RetainedFrame>, String> {
let reusable = match (previous, changes) {
(Some(previous), Some(changes)) => {
if !changes.applies_to(&previous.document, self) {
return Err(
"Native retained frame source changes do not match the document pair"
.to_owned(),
);
}
Some(previous)
}
(Some(previous), None) if Arc::ptr_eq(&previous.document, self) => Some(previous),
(None, Some(changes)) if !Arc::ptr_eq(&changes.target, self) => {
return Err(
"Native retained frame source changes do not match the target document"
.to_owned(),
);
}
_ => None,
};
let dirty = match (reusable, changes) {
(Some(_), Some(changes)) => dirty_sources(self, changes),
_ => BTreeSet::new(),
};
let transaction = RefCell::new(RenderTxn {
previous: reusable.map(|previous| Arc::clone(&previous.root)),
dirty,
stack: Vec::new(),
root: None,
});
let root = self
.effective_node(self.root_id)
.ok_or_else(|| "Native retained document lost its root".to_owned())?;
super::RESOLVER_LOOKUP_COUNT.with(|count| count.set(count.get().saturating_add(1)));
if root_width_override.is_some() && !matches!(root, LayoutNode::Box { .. }) {
return Err("Native root width override requires a box root".to_owned());
}
let scope = RenderScope {
view: SourceView {
node: root,
address: SourceAddress {
owner_id: self.root_id,
path: Arc::from([]),
},
},
resolver: Some(self),
transaction: Some(&transaction),
path: Vec::new(),
};
scope.render(
context,
false,
root_width_override.map(|declared_width| BoxOverride {
declared_width: Some(declared_width),
..BoxOverride::default()
}),
)?;
let root = transaction
.into_inner()
.root
.expect("native retained root evaluation");
Ok(Arc::new(RetainedFrame {
document: Arc::clone(self),
root,
}))
}
}
#[cfg(test)]
#[path = "evaluation_tests.rs"]
mod tests;

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,18 @@ pub(crate) fn line_plan_work() -> LinePlanWork {
line_plan::work()
}
#[path = "evaluation.rs"]
mod evaluation;
pub(crate) use evaluation::{EvalWork, RetainedFrame};
use evaluation::{Phase, RenderScope};
pub(crate) fn reset_eval_work() {
evaluation::reset_work();
}
pub(crate) fn eval_work() -> EvalWork {
evaluation::work()
}
fn deserialize_arc_vec<'de, D, T>(deserializer: D) -> Result<Arc<Vec<T>>, D::Error>
where
D: Deserializer<'de>,
@ -5500,6 +5512,7 @@ enum FlexMode {
#[derive(Debug, Clone)]
struct FlexRuntimeItem<'a> {
source: &'a LayoutNode,
scope: RenderScope<'a>,
grow: f64,
shrink: f64,
align_self: FlexAlign,
@ -5610,7 +5623,7 @@ fn box_vertical_side(node: &LayoutNode) -> Option<i64> {
fn box_content_intrinsics(
node: &LayoutNode,
resolver: Option<&RetainedDocument>,
scope: &RenderScope<'_>,
context: LayoutContext,
) -> Result<Option<(i64, i64)>, String> {
let LayoutNode::Box {
@ -5633,7 +5646,10 @@ fn box_content_intrinsics(
// current inline viewport. Treating that width as unknown makes
// responsive descendants collapse to their narrow intrinsic form and
// produces a different automatic minimum from the visible renderer.
let rendered = render_node(child, resolver, context, true)?;
let rendered = scope
.child(LocalStep::BoxChild, child)
.phase(Phase::ContentIntrinsic)
.render(context, true, None)?;
Ok(Some((
content_min_width.unwrap_or_else(|| rendered.min_content_width(*wrap_mode)),
rendered.max_width(),
@ -5647,7 +5663,7 @@ fn flex_box_resolve_width(
node: &LayoutNode,
size: &Size,
fallback: Option<i64>,
resolver: Option<&RetainedDocument>,
scope: &RenderScope<'_>,
context: LayoutContext,
) -> Result<Option<i64>, String> {
let LayoutNode::Box {
@ -5662,7 +5678,7 @@ fn flex_box_resolve_width(
return Ok(fallback);
};
let (min_content, max_content) =
box_content_intrinsics(node, resolver, context)?.unwrap_or((0, 0));
box_content_intrinsics(node, scope, context)?.unwrap_or((0, 0));
let side = box_horizontal_side(node).unwrap_or(0);
let stretch = context
.viewport_width_known
@ -5711,7 +5727,7 @@ fn flex_min_main(
source: &LayoutNode,
rendered: &Rendered,
axis: FlexAxis,
resolver: Option<&RetainedDocument>,
scope: &RenderScope<'_>,
context: LayoutContext,
) -> Result<i64, String> {
let LayoutNode::Box {
@ -5730,17 +5746,27 @@ fn flex_min_main(
Ok(match axis {
FlexAxis::Row => {
let side = box_horizontal_side(source).unwrap_or(0);
let declared =
flex_box_resolve_width(source, min_width, Some(0), resolver, context)?.unwrap_or(0);
let declared = flex_box_resolve_width(
source,
min_width,
Some(0),
&scope.phase(Phase::FlexMinWidth),
context,
)?
.unwrap_or(0);
if *wrap_mode == WrapMode::None {
rendered.max_width().max(side + declared)
} else {
let content_min = match content_min_width {
Some(content_min_width) => *content_min_width,
None => {
box_content_intrinsics(source, resolver, context)?
.unwrap_or((0, 0))
.0
box_content_intrinsics(
source,
&scope.phase(Phase::FlexAutoMinContent),
context,
)?
.unwrap_or((0, 0))
.0
}
};
side + declared.max(content_min)
@ -5758,7 +5784,7 @@ fn flex_min_main(
fn flex_max_main(
source: &LayoutNode,
axis: FlexAxis,
resolver: Option<&RetainedDocument>,
scope: &RenderScope<'_>,
context: LayoutContext,
) -> Result<Option<i64>, String> {
let LayoutNode::Box {
@ -5770,8 +5796,14 @@ fn flex_max_main(
return Ok(None);
};
Ok(match axis {
FlexAxis::Row => flex_box_resolve_width(source, max_width, None, resolver, context)?
.map(|value| value + box_horizontal_side(source).unwrap_or(0)),
FlexAxis::Row => flex_box_resolve_width(
source,
max_width,
None,
&scope.phase(Phase::FlexMaxWidth),
context,
)?
.map(|value| value + box_horizontal_side(source).unwrap_or(0)),
FlexAxis::Column => flex_box_resolve_height(source, max_height, None, context)
.map(|value| value + box_vertical_side(source).unwrap_or(0)),
})
@ -5798,7 +5830,7 @@ fn flex_basis_main(
rendered: &Rendered,
axis: FlexAxis,
basis: &Size,
resolver: Option<&RetainedDocument>,
scope: &RenderScope<'_>,
context: LayoutContext,
) -> Result<i64, String> {
let rendered_main = match axis {
@ -5811,15 +5843,24 @@ fn flex_basis_main(
if matches!(basis, Size::Content) {
if axis == FlexAxis::Row && matches!(source, LayoutNode::Box { .. }) {
let (_, content_max) =
box_content_intrinsics(source, resolver, context)?.unwrap_or((0, 0));
box_content_intrinsics(source, &scope.phase(Phase::FlexBasisContent), context)?
.unwrap_or((0, 0));
return Ok(box_horizontal_side(source).unwrap_or(0) + content_max);
}
return Ok(rendered_main);
}
if axis == FlexAxis::Row && matches!(source, LayoutNode::Box { .. }) {
let (_, content_max) = box_content_intrinsics(source, resolver, context)?.unwrap_or((0, 0));
let content = flex_box_resolve_width(source, basis, Some(content_max), resolver, context)?
.unwrap_or(content_max);
let (_, content_max) =
box_content_intrinsics(source, &scope.phase(Phase::FlexBasisContent), context)?
.unwrap_or((0, 0));
let content = flex_box_resolve_width(
source,
basis,
Some(content_max),
&scope.phase(Phase::FlexBasisWidth),
context,
)?
.unwrap_or(content_max);
return Ok(box_horizontal_side(source).unwrap_or(0) + content);
}
Ok(match axis {
@ -5852,13 +5893,11 @@ fn measure_flex_item<'a>(
item: &'a FlexItem,
axis: FlexAxis,
inline_viewport: Option<i64>,
resolver: Option<&'a RetainedDocument>,
scope: RenderScope<'a>,
context: LayoutContext,
) -> Result<FlexRuntimeItem<'a>, String> {
let source = match resolver {
Some(resolver) => resolver.resolve(&item.node)?,
None => &item.node,
};
let scope = scope.resolve()?;
let source = scope.view.node;
let uses_inline_viewport = flex_item_uses_inline_viewport(source);
let measurement_context = LayoutContext {
viewport_width: if uses_inline_viewport {
@ -5870,13 +5909,17 @@ fn measure_flex_item<'a>(
viewport_height: context.viewport_height,
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic,
};
let rendered = render_node(source, resolver, measurement_context, uses_inline_viewport)?;
let min_main = flex_min_main(source, &rendered, axis, resolver, context)?;
let max_main = flex_max_main(source, axis, resolver, context)?;
let base = flex_basis_main(source, &rendered, axis, &item.basis, resolver, context)?.max(0);
let rendered =
scope
.phase(Phase::FlexMeasure)
.render(measurement_context, uses_inline_viewport, None)?;
let min_main = flex_min_main(source, &rendered, axis, &scope, context)?;
let max_main = flex_max_main(source, axis, &scope, context)?;
let base = flex_basis_main(source, &rendered, axis, &item.basis, &scope, context)?.max(0);
let hypothetical = flex_clamp_main(base, min_main, max_main);
Ok(FlexRuntimeItem {
source,
scope,
grow: item.grow,
shrink: item.shrink,
align_self: item.align_self,
@ -6221,7 +6264,7 @@ fn render_flex_sized_entry(
main: i64,
cross: Option<i64>,
container_align: FlexAlign,
resolver: Option<&RetainedDocument>,
phase: Phase,
context: LayoutContext,
intrinsic: bool,
) -> Result<FlexSizedEntry, String> {
@ -6244,13 +6287,10 @@ fn render_flex_sized_entry(
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic,
};
let override_size = box_override_for_flex(item.source, axis, main, cross, stretch);
let mut rendered = render_node_with_override(
item.source,
resolver,
render_context,
intrinsic,
override_size,
)?;
let mut rendered = item
.scope
.phase(phase)
.render(render_context, intrinsic, override_size)?;
match axis {
FlexAxis::Row => {
rendered = pad_rendered_width(rendered, main, FlexAlign::FlexStart);
@ -6341,6 +6381,7 @@ fn exact_rendered_height(
resolver: Option<&RetainedDocument>,
context: LayoutContext,
) -> Option<i64> {
evaluation::record_height_query();
let node = resolver
.and_then(|value| value.resolve(node).ok())
.unwrap_or(node);
@ -6391,19 +6432,25 @@ fn exact_rendered_height(
}
fn render_node_window(
node: &LayoutNode,
resolver: Option<&RetainedDocument>,
scope: &RenderScope<'_>,
context: LayoutContext,
intrinsic: bool,
start: i64,
height: i64,
) -> Option<Result<Rendered, String>> {
let node = resolver
.and_then(|value| value.resolve(node).ok())
.unwrap_or(node);
let scope = match scope.resolve() {
Ok(scope) => scope,
Err(error) => return Some(Err(error)),
};
let node = scope.view.node;
let resolver = scope.resolver;
let total_height = exact_rendered_height(node, resolver, context)?;
if start <= 0 && height >= total_height {
return Some(render_node(node, resolver, context, intrinsic));
return Some(
scope
.phase(Phase::WindowFull)
.render(context, intrinsic, None),
);
}
match node {
LayoutNode::Column { children, .. }
@ -6412,19 +6459,21 @@ fn render_node_window(
&& context.viewport_width_known =>
{
Some(render_column_window(
children, resolver, context, start, height,
children, &scope, context, start, height,
))
}
_ => Some(
render_node(node, resolver, context, intrinsic)
scope
.phase(Phase::WindowFallback)
.render(context, intrinsic, None)
.map(|rendered| slice_rendered(rendered, start, height)),
),
}
}
fn render_column_window(
children: &[LayoutNode],
resolver: Option<&RetainedDocument>,
fn render_column_window<'a>(
children: &'a [LayoutNode],
scope: &RenderScope<'a>,
context: LayoutContext,
start: i64,
height: i64,
@ -6434,8 +6483,8 @@ fn render_column_window(
let mut offset = 0_i64;
let mut parts = Vec::new();
for child in column_leaves(children) {
let child_height = exact_rendered_height(child, resolver, context)
for child in column_leaves(scope, children) {
let child_height = exact_rendered_height(child.view.node, child.resolver, context)
.ok_or_else(|| "Native layout column window has an unbounded child".to_owned())?;
let child_end = offset.saturating_add(child_height);
if child_end <= start {
@ -6449,18 +6498,21 @@ fn render_column_window(
let child_start = start.saturating_sub(offset);
let child_window_height = (child_end.min(end) - (offset + child_start)).max(0);
let mut rendered = if child_start == 0 && child_window_height >= child_height {
render_node(child, resolver, context, false)?
child
.phase(Phase::WindowChildFull)
.render(context, false, None)?
} else {
render_node_window(
child,
resolver,
&child.phase(Phase::WindowChildPartial),
context,
false,
child_start,
child_window_height,
)
.unwrap_or_else(|| {
render_node(child, resolver, context, false)
child
.phase(Phase::WindowFallback)
.render(context, false, None)
.map(|rendered| slice_rendered(rendered, child_start, child_window_height))
})?
};
@ -6488,7 +6540,6 @@ fn flex_line_cross(
line: &[FlexRuntimeItem<'_>],
axis: FlexAxis,
container_align: FlexAlign,
resolver: Option<&RetainedDocument>,
context: LayoutContext,
intrinsic: bool,
) -> Result<i64, String> {
@ -6501,7 +6552,7 @@ fn flex_line_cross(
item.target,
None,
container_align,
resolver,
Phase::FlexCrossProbe,
context,
intrinsic,
)?
@ -6558,7 +6609,6 @@ fn render_flex_row_line(
main_gap: i64,
justify: FlexAlign,
align: FlexAlign,
resolver: Option<&RetainedDocument>,
context: LayoutContext,
intrinsic: bool,
) -> Result<Rendered, String> {
@ -6573,7 +6623,7 @@ fn render_flex_row_line(
item.target,
Some(line_cross),
align,
resolver,
Phase::FlexFinal,
context,
intrinsic,
)?;
@ -6596,7 +6646,6 @@ fn render_flex_column_line(
main_gap: i64,
justify: FlexAlign,
align: FlexAlign,
resolver: Option<&RetainedDocument>,
context: LayoutContext,
intrinsic: bool,
) -> Result<Rendered, String> {
@ -6618,7 +6667,7 @@ fn render_flex_column_line(
item.target,
Some(line_cross),
align,
resolver,
Phase::FlexFinal,
context,
intrinsic,
)?
@ -6645,7 +6694,6 @@ fn render_flex_row(
align: FlexAlign,
align_content: FlexAlign,
single_line: bool,
resolver: Option<&RetainedDocument>,
context: LayoutContext,
intrinsic: bool,
) -> Result<Rendered, String> {
@ -6664,7 +6712,7 @@ fn render_flex_row(
} else {
lines
.iter()
.map(|line| flex_line_cross(line, FlexAxis::Row, align, resolver, context, intrinsic))
.map(|line| flex_line_cross(line, FlexAxis::Row, align, context, intrinsic))
.collect::<Result<Vec<_>, _>>()?
};
let cross_layout =
@ -6690,7 +6738,6 @@ fn render_flex_row(
main_gap,
justify,
align,
resolver,
context,
intrinsic,
)?);
@ -6721,7 +6768,6 @@ fn render_flex_column(
align: FlexAlign,
align_content: FlexAlign,
single_line: bool,
resolver: Option<&RetainedDocument>,
context: LayoutContext,
intrinsic: bool,
) -> Result<Rendered, String> {
@ -6733,9 +6779,7 @@ fn render_flex_column(
} else {
lines
.iter()
.map(|line| {
flex_line_cross(line, FlexAxis::Column, align, resolver, context, intrinsic)
})
.map(|line| flex_line_cross(line, FlexAxis::Column, align, context, intrinsic))
.collect::<Result<Vec<_>, _>>()?
};
let cross_layout =
@ -6751,7 +6795,7 @@ fn render_flex_column(
.enumerate()
{
let rendered = render_flex_column_line(
line, line_cross, main_size, main_gap, justify, align, resolver, context, intrinsic,
line, line_cross, main_size, main_gap, justify, align, context, intrinsic,
)?;
parts.push((rendered, line_cross));
if index + 1 < line_count {
@ -6769,7 +6813,7 @@ fn render_flex_column(
}
#[allow(clippy::too_many_arguments)]
fn render_flex(
fn render_flex<'a>(
direction: FlexDirection,
wrap: FlexWrap,
justify: FlexAlign,
@ -6779,8 +6823,8 @@ fn render_flex(
height: &Size,
row_gap: i64,
column_gap: i64,
source_items: &[FlexItem],
resolver: Option<&RetainedDocument>,
source_items: &'a [FlexItem],
scope: &RenderScope<'a>,
context: LayoutContext,
intrinsic: bool,
) -> Result<Rendered, String> {
@ -6801,7 +6845,7 @@ fn render_flex(
&source_items[index],
axis,
inline_viewport,
resolver,
scope.child(LocalStep::FlexItem(index), &source_items[index].node),
context,
)
})
@ -6829,7 +6873,6 @@ fn render_flex(
align_items,
align_content,
single_line,
resolver,
context,
intrinsic,
),
@ -6843,20 +6886,20 @@ fn render_flex(
align_items,
align_content,
single_line,
resolver,
context,
intrinsic,
),
}
}
#[derive(Debug, Clone, Copy, Default)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct BoxOverride {
content_width: Option<i64>,
content_height: Option<i64>,
declared_width: Option<i64>,
}
#[cfg(test)]
fn render_node(
node: &LayoutNode,
resolver: Option<&RetainedDocument>,
@ -6873,10 +6916,17 @@ fn render_node_with_override(
intrinsic: bool,
size_override: Option<BoxOverride>,
) -> Result<Rendered, String> {
let node = match resolver {
Some(resolver) => resolver.resolve(node)?,
None => node,
};
RenderScope::uncached(node, resolver).render(context, intrinsic, size_override)
}
fn render_node_body(
scope: &RenderScope<'_>,
context: LayoutContext,
intrinsic: bool,
size_override: Option<BoxOverride>,
) -> Result<Rendered, String> {
let node = scope.view.node;
let resolver = scope.resolver;
#[cfg(test)]
TEST_RENDER_NODE_COUNT.with(|count| {
if let Some(value) = count.get() {
@ -7026,6 +7076,7 @@ fn render_node_with_override(
let mut windowed_child_start = None;
let mut windowed_child_height = None;
let mut child_rendered = if let Some(child) = child {
let child_scope = scope.child(LocalStep::BoxChild, child);
let child_context = LayoutContext {
viewport_width: if intrinsic_child {
0
@ -7049,8 +7100,7 @@ fn render_node_with_override(
windowed_child_height = Some(total_height);
Some(
render_node_window(
child,
resolver,
&child_scope,
child_context,
intrinsic || intrinsic_child,
start,
@ -7059,20 +7109,14 @@ fn render_node_with_override(
.expect("exact height checked above")?,
)
} else {
Some(render_node(
child,
resolver,
Some(child_scope.render(
child_context,
intrinsic || intrinsic_child,
None,
)?)
}
} else {
Some(render_node(
child,
resolver,
child_context,
intrinsic || intrinsic_child,
)?)
Some(child_scope.render(child_context, intrinsic || intrinsic_child, None)?)
}
} else {
None
@ -7425,7 +7469,14 @@ fn render_node_with_override(
};
let rendered = children
.iter()
.map(|child| render_node(child, resolver, child_context, intrinsic))
.enumerate()
.map(|(index, child)| {
scope.child(LocalStep::RowChild(index), child).render(
child_context,
intrinsic,
None,
)
})
.collect::<Result<Vec<_>, _>>()?;
let parts = rendered
.into_iter()
@ -7437,8 +7488,8 @@ fn render_node_with_override(
Ok(concat_horizontal_sized(parts, 0))
}
LayoutNode::Column { children, .. } => {
let rendered = column_leaves(children)
.map(|child| render_node(child, resolver, context, intrinsic))
let rendered = column_leaves(scope, children)
.map(|child| child.render(context, intrinsic, None))
.collect::<Result<Vec<_>, _>>()?;
let maximum = rendered
.iter()
@ -7492,7 +7543,7 @@ fn render_node_with_override(
*row_gap,
*column_gap,
items,
resolver,
scope,
context,
intrinsic,
),
@ -7501,12 +7552,19 @@ fn render_node_with_override(
// Flatten only literal Columns. In particular, a retained NodeRef remains a
// child evaluation boundary even when it resolves to an identified Column.
fn column_leaves(children: &[LayoutNode]) -> impl Iterator<Item = &LayoutNode> {
let mut stack = vec![children.iter()];
fn column_leaves<'a>(
scope: &RenderScope<'a>,
children: &'a [LayoutNode],
) -> impl Iterator<Item = RenderScope<'a>> {
let mut stack = vec![(scope.clone(), children.iter().enumerate())];
std::iter::from_fn(move || loop {
match stack.last_mut()?.next() {
Some(LayoutNode::Column { children, .. }) => stack.push(children.iter()),
Some(child) => return Some(child),
let (scope, children) = stack.last_mut()?;
match children.next() {
Some((index, child @ LayoutNode::Column { children, .. })) => {
let child_scope = scope.child(LocalStep::ColumnChild(index), child);
stack.push((child_scope, children.iter().enumerate()));
}
Some((index, child)) => return Some(scope.child(LocalStep::ColumnChild(index), child)),
None => {
stack.pop();
}
@ -7633,7 +7691,11 @@ mod tests {
serde_json::from_str(json).unwrap()
}
fn cluster(text: &str, width: i64, source_template_id: Option<u32>) -> MeasuredCluster {
pub(super) fn cluster(
text: &str,
width: i64,
source_template_id: Option<u32>,
) -> MeasuredCluster {
MeasuredCluster {
text: text.to_owned(),
width,
@ -7644,7 +7706,7 @@ mod tests {
}
}
fn measured_text(lines: Vec<Vec<MeasuredCluster>>) -> MeasuredText {
pub(super) fn measured_text(lines: Vec<Vec<MeasuredCluster>>) -> MeasuredText {
MeasuredText {
lines: lines
.into_iter()
@ -7716,7 +7778,7 @@ mod tests {
node
}
fn child_box(
pub(super) fn child_box(
region_id: i64,
child: LayoutNode,
surface_template_id: Option<u32>,
@ -7766,7 +7828,7 @@ mod tests {
}
}
fn identified(mut node: LayoutNode, node_id: u64, revision: u64) -> LayoutNode {
pub(super) fn identified(mut node: LayoutNode, node_id: u64, revision: u64) -> LayoutNode {
match &mut node {
LayoutNode::Box {
node_id: id,
@ -7801,7 +7863,7 @@ mod tests {
node
}
fn retained_document(root: LayoutNode) -> LayoutDocument {
pub(super) fn retained_document(root: LayoutNode) -> LayoutDocument {
LayoutDocument {
version: LAYOUT_VERSION,
space_width: 1,
@ -7863,7 +7925,7 @@ mod tests {
}
}
fn test_context() -> LayoutContext {
pub(super) fn test_context() -> LayoutContext {
LayoutContext {
viewport_width: 80,
viewport_width_known: true,
@ -8607,7 +8669,7 @@ mod tests {
assert_eq!(joined.lines[1].break_after, Some(AtomProperties::default()));
}
fn nonuniform_text(region_id: i64, widths: &[i64]) -> LayoutNode {
pub(super) fn nonuniform_text(region_id: i64, widths: &[i64]) -> LayoutNode {
LayoutNode::Text {
node_id: None,
node_revision: None,
@ -9624,7 +9686,7 @@ mod tests {
}
}
fn context_test_delta(entries: Vec<JsonValue>) -> DocumentDelta {
pub(super) fn context_test_delta(entries: Vec<JsonValue>) -> DocumentDelta {
serde_json::from_value(serde_json::json!({
"style-base-count": 0,
"styles-append": [],

View File

@ -3,7 +3,8 @@ pub mod sequence;
use layout::{
encode_error_tape, DocumentDelta, LayoutContext, LayoutDocument, LayoutTape, RetainedDocument,
TapeIdentity, TapeOutputOptions, MAX_LAYOUT_DIMENSION, MIN_TAPE_BYTES,
RetainedFrame, SourceChanges, TapeIdentity, TapeOutputOptions, MAX_LAYOUT_DIMENSION,
MIN_TAPE_BYTES,
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
@ -379,6 +380,7 @@ enum JobPayload {
Echo(Vec<u8>),
Layout {
document: LayoutSource,
source_changes: Option<SourceChanges>,
context: LayoutContext,
root_width: i64,
root_width_override: bool,
@ -426,6 +428,27 @@ impl LayoutSource {
Self::Retained(document) => document.styles(),
}
}
fn render_target(
&self,
context: LayoutContext,
root_width: Option<i64>,
previous: Option<&RetainedFrame>,
changes: Option<&SourceChanges>,
) -> Result<(LayoutTape, Option<Arc<RetainedFrame>>), String> {
match self {
Self::Full(document) => {
if changes.is_some() {
return Err("Native source changes require a retained document".to_owned());
}
Ok((document.layout_tape(context, root_width)?, None))
}
Self::Retained(document) => {
let frame = document.render_frame(previous, changes, context, root_width)?;
Ok((frame.materialize_tape(), Some(frame)))
}
}
}
}
#[derive(Clone, Debug)]
@ -444,6 +467,7 @@ struct ConfirmedBaseline {
document: LayoutSource,
document_revision: u64,
tape: LayoutTape,
retained_frame: Option<Arc<RetainedFrame>>,
styles: Vec<layout::StyleTemplate>,
}
@ -453,6 +477,7 @@ struct PendingBaseline {
document: LayoutSource,
document_revision: u64,
tape: LayoutTape,
retained_frame: Option<Arc<RetainedFrame>>,
styles: Vec<layout::StyleTemplate>,
}
@ -471,6 +496,7 @@ struct RenderedJob {
resolver_lookups: u64,
atom_plan_work: layout::AtomPlanWork,
line_plan_work: layout::LinePlanWork,
eval_work: layout::EvalWork,
}
#[derive(Default)]
@ -527,6 +553,7 @@ struct RuntimeState {
source_change_work: layout::SourceChangeWork,
atom_plan_work: layout::AtomPlanWork,
line_plan_work: layout::LinePlanWork,
eval_work: layout::EvalWork,
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
}
@ -607,6 +634,7 @@ struct SessionStats {
source_change_work: layout::SourceChangeWork,
atom_plan_work: layout::AtomPlanWork,
line_plan_work: layout::LinePlanWork,
eval_work: layout::EvalWork,
pending_baselines: usize,
confirmed_baseline: bool,
confirmed_baseline_bytes: usize,
@ -682,6 +710,7 @@ impl Session {
source_change_work: layout::SourceChangeWork::default(),
atom_plan_work: layout::AtomPlanWork::default(),
line_plan_work: layout::LinePlanWork::default(),
eval_work: layout::EvalWork::default(),
confirmed_baseline,
}),
readiness_channel: Mutex::new(None),
@ -807,6 +836,7 @@ impl Session {
frame,
document_base_revision,
document_target_revision,
None,
)?
} else {
let payload = frame
@ -955,6 +985,7 @@ impl Session {
.unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline
.clone();
let mut source_changes = None;
let (document, input_stats) = match (batch.document, batch.document_delta) {
(Some(_), Some(_)) => {
return Err(
@ -1029,9 +1060,9 @@ impl Session {
"Native retained source changes document identity mismatch".to_owned()
);
}
// Consume exact source facts for input accounting. The renderer
// still evaluates the target normally; no history-bearing change
// object is stored in a job, document or confirmed baseline.
// Account for exact input work, then carry these facts only for
// this render job. Documents and baselines never store the
// descriptor's references to both versions.
let mut source_change_work = applied.stats.source_work;
let mut previous_owner = None;
applied
@ -1047,6 +1078,7 @@ impl Session {
let (styles, property_templates) = applied.changes.registry_ranges();
source_change_work.styles_appended = styles.len() as u64;
source_change_work.property_templates_added = property_templates.len() as u64;
source_changes = Some(applied.changes);
(
LayoutSource::Retained(applied.document),
DocumentInputStats {
@ -1104,6 +1136,7 @@ impl Session {
frame,
document_base_revision,
document_target_revision,
source_changes,
)?;
let output = render_layout_payload(
prepared.payload,
@ -1132,6 +1165,7 @@ impl Session {
state.document_resolver_lookups += output.resolver_lookups;
state.atom_plan_work.accumulate(output.atom_plan_work);
state.line_plan_work.accumulate(output.line_plan_work);
state.eval_work.accumulate(output.eval_work);
state.document_parses += input_stats.parses;
state.document_validations += input_stats.validations;
state.document_reuses += input_stats.reuses;
@ -1176,6 +1210,7 @@ impl Session {
document: pending.document,
document_revision: pending.document_revision,
tape: pending.tape,
retained_frame: pending.retained_frame,
styles: pending.styles,
}));
Ok(true)
@ -1253,6 +1288,8 @@ impl Session {
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let pending_baselines = state.pending_baselines.len();
// Compatibility field: the shallow tape container only, not retained
// plan allocations, shared heap memory or a cache memory bound.
let confirmed_baseline_bytes = state
.confirmed_baseline
.as_ref()
@ -1286,6 +1323,7 @@ impl Session {
source_change_work: state.source_change_work,
atom_plan_work: state.atom_plan_work,
line_plan_work: state.line_plan_work,
eval_work: state.eval_work,
pending_baselines,
confirmed_baseline: state.confirmed_baseline.is_some(),
confirmed_baseline_bytes,
@ -1389,6 +1427,7 @@ fn prepare_layout_job(
frame: ControlFrame,
document_base_revision: u64,
document_target_revision: u64,
source_changes: Option<SourceChanges>,
) -> Result<PreparedJob, String> {
layout::reset_resolver_lookups();
if frame.payload.is_some() {
@ -1462,6 +1501,7 @@ fn prepare_layout_job(
delay_ms: frame.delay_ms,
payload: JobPayload::Layout {
document: document.clone(),
source_changes,
context,
root_width,
root_width_override: frame.root_width_override,
@ -1498,9 +1538,11 @@ fn render_layout_payload(
resolver_lookups: 0,
atom_plan_work: layout::AtomPlanWork::default(),
line_plan_work: layout::LinePlanWork::default(),
eval_work: layout::EvalWork::default(),
},
JobPayload::Layout {
document,
source_changes,
context,
root_width,
root_width_override,
@ -1548,6 +1590,7 @@ fn render_layout_payload(
Vec<u8>,
LayoutTape,
Vec<layout::StyleTemplate>,
Option<Arc<RetainedFrame>>,
bool,
u64,
u64,
@ -1557,8 +1600,19 @@ fn render_layout_payload(
layout::reset_resolver_lookups();
layout::reset_atom_plan_work();
layout::reset_line_plan_work();
layout::reset_eval_work();
let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
let target_styles = document.styles()?;
let render_target = || {
document.render_target(
context,
root_width_override.then_some(root_width),
confirmed_baseline
.as_ref()
.and_then(|baseline| baseline.retained_frame.as_deref()),
source_changes.as_ref(),
)
};
if let Some(base_context) = base_context {
let base_identity = BaselineIdentity {
context: base_context,
@ -1582,8 +1636,7 @@ fn render_layout_payload(
})
.cloned();
if require_confirmed_patch_base && base_hit.is_none() {
let target = document
.layout_tape(context, root_width_override.then_some(root_width))?;
let (target, retained_frame) = render_target()?;
let bytes = layout::encode_layout_tape(
target.clone(),
&target_styles,
@ -1591,7 +1644,7 @@ fn render_layout_payload(
output.root_metadata,
output.max_bytes,
)?;
return Ok((bytes, target, target_styles, false, 0, 1));
return Ok((bytes, target, target_styles, retained_frame, false, 0, 1));
}
let (old, baseline_hit, base_renders) = if let Some(baseline) = base_hit {
(baseline.tape.clone(), true, 0)
@ -1605,8 +1658,7 @@ fn render_layout_payload(
1,
)
};
let target =
document.layout_tape(context, root_width_override.then_some(root_width))?;
let (target, retained_frame) = render_target()?;
let bytes = layout::encode_layout_patch_tape(
old,
target.clone(),
@ -1615,10 +1667,17 @@ fn render_layout_payload(
output.root_metadata,
output.max_bytes,
)?;
Ok((bytes, target, target_styles, baseline_hit, base_renders, 1))
Ok((
bytes,
target,
target_styles,
retained_frame,
baseline_hit,
base_renders,
1,
))
} else {
let target =
document.layout_tape(context, root_width_override.then_some(root_width))?;
let (target, retained_frame) = render_target()?;
let bytes = layout::encode_layout_tape(
target.clone(),
&target_styles,
@ -1626,32 +1685,41 @@ fn render_layout_payload(
output.root_metadata,
output.max_bytes,
)?;
Ok((bytes, target, target_styles, false, 0, 1))
Ok((bytes, target, target_styles, retained_frame, false, 0, 1))
}
}));
let resolver_lookups =
validation_resolver_lookups.saturating_add(layout::resolver_lookups());
let atom_plan_work = layout::atom_plan_work();
let line_plan_work = layout::line_plan_work();
let eval_work = layout::eval_work();
match result {
Ok(Ok((bytes, tape, styles, baseline_hit, base_renders, target_renders))) => {
RenderedJob {
bytes,
pending: Some(PendingBaseline {
confirmed_identity: pending_identity,
document: document.clone(),
document_revision: document_target_revision,
tape,
styles,
}),
baseline_hit,
base_renders,
target_renders,
resolver_lookups,
atom_plan_work,
line_plan_work,
}
}
Ok(Ok((
bytes,
tape,
styles,
retained_frame,
baseline_hit,
base_renders,
target_renders,
))) => RenderedJob {
bytes,
pending: Some(PendingBaseline {
confirmed_identity: pending_identity,
document: document.clone(),
document_revision: document_target_revision,
tape,
retained_frame,
styles,
}),
baseline_hit,
base_renders,
target_renders,
resolver_lookups,
atom_plan_work,
line_plan_work,
eval_work,
},
Ok(Err(error)) => RenderedJob {
bytes: encode_error_tape(identity, &error, max_result_bytes),
pending: None,
@ -1661,6 +1729,7 @@ fn render_layout_payload(
resolver_lookups,
atom_plan_work,
line_plan_work,
eval_work,
},
Err(_) => RenderedJob {
bytes: encode_error_tape(identity, "native layout panicked", max_result_bytes),
@ -1671,6 +1740,7 @@ fn render_layout_payload(
resolver_lookups,
atom_plan_work,
line_plan_work,
eval_work,
},
}
}
@ -1706,7 +1776,7 @@ fn render_proof(payload: &[u8]) -> Result<Vec<u8>, String> {
return Err("Native proof render cannot contain delay-ms".to_owned());
}
let document = LayoutSource::Full(Arc::new(document));
let prepared = prepare_layout_job(&document, frame, 0, 1)?;
let prepared = prepare_layout_job(&document, frame, 0, 1, None)?;
let output = render_layout_payload(
prepared.payload,
SYNC_RENDER_SESSION_ID,
@ -1822,6 +1892,7 @@ fn worker_loop(shared: Arc<Shared>) {
state.document_resolver_lookups += output.resolver_lookups;
state.atom_plan_work.accumulate(output.atom_plan_work);
state.line_plan_work.accumulate(output.line_plan_work);
state.eval_work.accumulate(output.eval_work);
state.results.insert(
(job.generation, job.key),
ResultEntry {
@ -2569,6 +2640,149 @@ mod tests {
session.stop(true);
}
#[test]
fn retained_layout_reuse_does_not_bypass_wire_base_proof() {
let session = Session::new(1, 4, 4, 64 * 1024).unwrap();
let first = identified_proof_layout_payload(
r#"[{"key":1,"viewport-width":80,"viewport-height":10,"root-width":80,"runtime-revision":0,"context-hash":77}]"#,
);
session.render_sync(1, &first).unwrap();
assert!(session.confirm(1, 1, 1).unwrap());
let before = session.stats();
// The source and layout request still match, but receiver context
// identity does not: a layout hit must still publish a full tape.
let second = retained_layout_payload(
1,
r#"[{"key":2,"viewport-width":80,"viewport-height":10,"root-width":80,"patch":true,"base-viewport-width":80,"base-viewport-height":10,"base-root-width":80,"runtime-revision":1,"context-hash":78}]"#,
);
let tape = session.render_sync(2, &second).unwrap();
let after = session.stats();
assert_ne!(u16::from_le_bytes(tape[6..8].try_into().unwrap()) & 1, 0);
assert!(!tape_patch_p(&tape));
assert_eq!(after.baseline_hits, before.baseline_hits);
assert_eq!(after.base_renders, before.base_renders);
assert_eq!(after.eval_work.hits - before.eval_work.hits, 1);
assert_eq!(after.eval_work.body_runs, before.eval_work.body_runs);
assert!(session.confirm(2, 2, 2).unwrap());
session.stop(true);
}
#[test]
fn retained_frame_failures_preserve_the_confirmed_fork_and_allow_retry() {
// Failed encoding or confirmation must not promote source/output or
// retain the rejected candidate's evaluation tree.
let parent = Session::new(1, 4, 4, 2048).unwrap();
let first = identified_proof_layout_payload(
r#"[{"key":1,"viewport-width":80,"viewport-height":10,"root-width":80,"runtime-revision":0,"context-hash":77}]"#,
);
let first_tape = parent.render_sync(1, &first).unwrap();
assert_ne!(
u16::from_le_bytes(first_tape[6..8].try_into().unwrap()) & 1,
0
);
assert!(parent.confirm(1, 1, 1).unwrap());
let child = parent.fork_confirmed().unwrap();
let original = parent
.shared
.state
.lock()
.unwrap()
.confirmed_baseline
.clone()
.unwrap();
let original_frame = original.retained_frame.as_ref().unwrap();
let delta = |key, text: &str| {
serde_json::to_vec(&serde_json::json!({
"version": 1, "document-base-revision": 1, "document-target-revision": 2,
"document-delta": {
"style-base-count": 0, "styles-append": [],
"property-template-base-count": 0, "property-template-target-count": 0,
"entries": [{"node-id": 1, "expected-revision": 7, "target-revision": 8,
"slot-patches": [{"slot": 0, "local": {
"content": {"lines": [{"clusters": [{"text": text, "width": 8,
"cjk": false, "space": false}]}]}
}}]}]
},
"frames": [{"key": key, "viewport-width": 80, "viewport-height": 10,
"root-width": 80, "patch": true, "base-viewport-width": 80,
"base-viewport-height": 10, "base-root-width": 80,
"runtime-revision": 1, "context-hash": 77}]
}))
.unwrap()
};
let oversized = child.render_sync(1, &delta(2, &"x".repeat(4096))).unwrap();
assert_eq!(
u16::from_le_bytes(oversized[6..8].try_into().unwrap()) & 1,
0
);
assert_eq!(child.stats().pending_baselines, 0);
assert!(!child.confirm(1, 2, 2).unwrap());
let before = child
.shared
.state
.lock()
.unwrap()
.confirmed_baseline
.clone()
.unwrap();
assert!(Arc::ptr_eq(&original, &before));
let retry = child.render_sync(2, &delta(3, "y")).unwrap();
assert_ne!(u16::from_le_bytes(retry[6..8].try_into().unwrap()) & 1, 0);
assert_eq!(child.stats().pending_baselines, 1);
let rejected_frame = {
let state = child.shared.state.lock().unwrap();
let candidate = state.pending_baselines.get(&(2, 3)).unwrap();
let frame = candidate.retained_frame.as_ref().unwrap();
assert!(!Arc::ptr_eq(original_frame, frame));
Arc::downgrade(frame)
};
assert!(child.confirm(2, 3, 99).is_err());
assert_eq!(child.stats().pending_baselines, 0);
assert!(rejected_frame.upgrade().is_none());
let after = child
.shared
.state
.lock()
.unwrap()
.confirmed_baseline
.clone()
.unwrap();
assert!(Arc::ptr_eq(&original, &after));
let retry = child.render_sync(3, &delta(4, "z")).unwrap();
assert_ne!(u16::from_le_bytes(retry[6..8].try_into().unwrap()) & 1, 0);
assert!(child.confirm(3, 4, 2).unwrap());
let confirmed = child
.shared
.state
.lock()
.unwrap()
.confirmed_baseline
.clone()
.unwrap();
assert!(!Arc::ptr_eq(&original, &confirmed));
assert!(!Arc::ptr_eq(
original_frame,
confirmed.retained_frame.as_ref().unwrap()
));
assert_eq!(confirmed.document_revision, 2);
assert_eq!(original.document_revision, 1);
assert!(Arc::ptr_eq(
&original,
parent
.shared
.state
.lock()
.unwrap()
.confirmed_baseline
.as_ref()
.unwrap()
));
parent.stop(true);
child.stop(true);
}
#[test]
fn confirmed_fork_shares_only_immutable_baseline() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
@ -2857,6 +3071,30 @@ mod tests {
retry.stop(true);
}
#[test]
fn cancellation_releases_retained_candidate_frame() {
let session = Session::new(1, 4, 4, 64 * 1024).unwrap();
let payload = identified_proof_layout_payload(
r#"[{"key":1,"viewport-width":80,"viewport-height":10,"runtime-revision":0}]"#,
);
session.render_sync(1, &payload).unwrap();
let cancelled_frame = {
let state = session.shared.state.lock().unwrap();
Arc::downgrade(
state.pending_baselines[&(1, 1)]
.retained_frame
.as_ref()
.unwrap(),
)
};
session.cancel(1);
assert!(cancelled_frame.upgrade().is_none());
assert_eq!(session.stats().pending_baselines, 0);
assert!(!session.stats().confirmed_baseline);
assert!(!session.confirm(1, 1, 1).unwrap());
session.stop(true);
}
#[test]
fn async_omit_uses_the_latest_synchronously_confirmed_document() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();

View File

@ -3,7 +3,7 @@
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum LocalStep {
BoxChild,
RowChild(usize),
@ -12,7 +12,7 @@ pub(super) enum LocalStep {
}
/// A local structural address stops before crossing an identified node reference.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct SourceAddress {
pub(super) owner_id: u64,
pub(super) path: Arc<[LocalStep]>,