Reuse retained layout subtrees across confirmed document updates
This commit is contained in:
parent
220f27803e
commit
14c1698c41
551
native/src/evaluation.rs
Normal file
551
native/src/evaluation.rs
Normal 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;
|
||||
1028
native/src/evaluation_tests.rs
Normal file
1028
native/src/evaluation_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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": [],
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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]>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user