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()
|
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>
|
fn deserialize_arc_vec<'de, D, T>(deserializer: D) -> Result<Arc<Vec<T>>, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
@ -5500,6 +5512,7 @@ enum FlexMode {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct FlexRuntimeItem<'a> {
|
struct FlexRuntimeItem<'a> {
|
||||||
source: &'a LayoutNode,
|
source: &'a LayoutNode,
|
||||||
|
scope: RenderScope<'a>,
|
||||||
grow: f64,
|
grow: f64,
|
||||||
shrink: f64,
|
shrink: f64,
|
||||||
align_self: FlexAlign,
|
align_self: FlexAlign,
|
||||||
@ -5610,7 +5623,7 @@ fn box_vertical_side(node: &LayoutNode) -> Option<i64> {
|
|||||||
|
|
||||||
fn box_content_intrinsics(
|
fn box_content_intrinsics(
|
||||||
node: &LayoutNode,
|
node: &LayoutNode,
|
||||||
resolver: Option<&RetainedDocument>,
|
scope: &RenderScope<'_>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
) -> Result<Option<(i64, i64)>, String> {
|
) -> Result<Option<(i64, i64)>, String> {
|
||||||
let LayoutNode::Box {
|
let LayoutNode::Box {
|
||||||
@ -5633,7 +5646,10 @@ fn box_content_intrinsics(
|
|||||||
// current inline viewport. Treating that width as unknown makes
|
// current inline viewport. Treating that width as unknown makes
|
||||||
// responsive descendants collapse to their narrow intrinsic form and
|
// responsive descendants collapse to their narrow intrinsic form and
|
||||||
// produces a different automatic minimum from the visible renderer.
|
// 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((
|
Ok(Some((
|
||||||
content_min_width.unwrap_or_else(|| rendered.min_content_width(*wrap_mode)),
|
content_min_width.unwrap_or_else(|| rendered.min_content_width(*wrap_mode)),
|
||||||
rendered.max_width(),
|
rendered.max_width(),
|
||||||
@ -5647,7 +5663,7 @@ fn flex_box_resolve_width(
|
|||||||
node: &LayoutNode,
|
node: &LayoutNode,
|
||||||
size: &Size,
|
size: &Size,
|
||||||
fallback: Option<i64>,
|
fallback: Option<i64>,
|
||||||
resolver: Option<&RetainedDocument>,
|
scope: &RenderScope<'_>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
) -> Result<Option<i64>, String> {
|
) -> Result<Option<i64>, String> {
|
||||||
let LayoutNode::Box {
|
let LayoutNode::Box {
|
||||||
@ -5662,7 +5678,7 @@ fn flex_box_resolve_width(
|
|||||||
return Ok(fallback);
|
return Ok(fallback);
|
||||||
};
|
};
|
||||||
let (min_content, max_content) =
|
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 side = box_horizontal_side(node).unwrap_or(0);
|
||||||
let stretch = context
|
let stretch = context
|
||||||
.viewport_width_known
|
.viewport_width_known
|
||||||
@ -5711,7 +5727,7 @@ fn flex_min_main(
|
|||||||
source: &LayoutNode,
|
source: &LayoutNode,
|
||||||
rendered: &Rendered,
|
rendered: &Rendered,
|
||||||
axis: FlexAxis,
|
axis: FlexAxis,
|
||||||
resolver: Option<&RetainedDocument>,
|
scope: &RenderScope<'_>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
) -> Result<i64, String> {
|
) -> Result<i64, String> {
|
||||||
let LayoutNode::Box {
|
let LayoutNode::Box {
|
||||||
@ -5730,17 +5746,27 @@ fn flex_min_main(
|
|||||||
Ok(match axis {
|
Ok(match axis {
|
||||||
FlexAxis::Row => {
|
FlexAxis::Row => {
|
||||||
let side = box_horizontal_side(source).unwrap_or(0);
|
let side = box_horizontal_side(source).unwrap_or(0);
|
||||||
let declared =
|
let declared = flex_box_resolve_width(
|
||||||
flex_box_resolve_width(source, min_width, Some(0), resolver, context)?.unwrap_or(0);
|
source,
|
||||||
|
min_width,
|
||||||
|
Some(0),
|
||||||
|
&scope.phase(Phase::FlexMinWidth),
|
||||||
|
context,
|
||||||
|
)?
|
||||||
|
.unwrap_or(0);
|
||||||
if *wrap_mode == WrapMode::None {
|
if *wrap_mode == WrapMode::None {
|
||||||
rendered.max_width().max(side + declared)
|
rendered.max_width().max(side + declared)
|
||||||
} else {
|
} else {
|
||||||
let content_min = match content_min_width {
|
let content_min = match content_min_width {
|
||||||
Some(content_min_width) => *content_min_width,
|
Some(content_min_width) => *content_min_width,
|
||||||
None => {
|
None => {
|
||||||
box_content_intrinsics(source, resolver, context)?
|
box_content_intrinsics(
|
||||||
.unwrap_or((0, 0))
|
source,
|
||||||
.0
|
&scope.phase(Phase::FlexAutoMinContent),
|
||||||
|
context,
|
||||||
|
)?
|
||||||
|
.unwrap_or((0, 0))
|
||||||
|
.0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
side + declared.max(content_min)
|
side + declared.max(content_min)
|
||||||
@ -5758,7 +5784,7 @@ fn flex_min_main(
|
|||||||
fn flex_max_main(
|
fn flex_max_main(
|
||||||
source: &LayoutNode,
|
source: &LayoutNode,
|
||||||
axis: FlexAxis,
|
axis: FlexAxis,
|
||||||
resolver: Option<&RetainedDocument>,
|
scope: &RenderScope<'_>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
) -> Result<Option<i64>, String> {
|
) -> Result<Option<i64>, String> {
|
||||||
let LayoutNode::Box {
|
let LayoutNode::Box {
|
||||||
@ -5770,8 +5796,14 @@ fn flex_max_main(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
Ok(match axis {
|
Ok(match axis {
|
||||||
FlexAxis::Row => flex_box_resolve_width(source, max_width, None, resolver, context)?
|
FlexAxis::Row => flex_box_resolve_width(
|
||||||
.map(|value| value + box_horizontal_side(source).unwrap_or(0)),
|
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)
|
FlexAxis::Column => flex_box_resolve_height(source, max_height, None, context)
|
||||||
.map(|value| value + box_vertical_side(source).unwrap_or(0)),
|
.map(|value| value + box_vertical_side(source).unwrap_or(0)),
|
||||||
})
|
})
|
||||||
@ -5798,7 +5830,7 @@ fn flex_basis_main(
|
|||||||
rendered: &Rendered,
|
rendered: &Rendered,
|
||||||
axis: FlexAxis,
|
axis: FlexAxis,
|
||||||
basis: &Size,
|
basis: &Size,
|
||||||
resolver: Option<&RetainedDocument>,
|
scope: &RenderScope<'_>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
) -> Result<i64, String> {
|
) -> Result<i64, String> {
|
||||||
let rendered_main = match axis {
|
let rendered_main = match axis {
|
||||||
@ -5811,15 +5843,24 @@ fn flex_basis_main(
|
|||||||
if matches!(basis, Size::Content) {
|
if matches!(basis, Size::Content) {
|
||||||
if axis == FlexAxis::Row && matches!(source, LayoutNode::Box { .. }) {
|
if axis == FlexAxis::Row && matches!(source, LayoutNode::Box { .. }) {
|
||||||
let (_, content_max) =
|
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(box_horizontal_side(source).unwrap_or(0) + content_max);
|
||||||
}
|
}
|
||||||
return Ok(rendered_main);
|
return Ok(rendered_main);
|
||||||
}
|
}
|
||||||
if axis == FlexAxis::Row && matches!(source, LayoutNode::Box { .. }) {
|
if axis == FlexAxis::Row && matches!(source, LayoutNode::Box { .. }) {
|
||||||
let (_, content_max) = box_content_intrinsics(source, resolver, context)?.unwrap_or((0, 0));
|
let (_, content_max) =
|
||||||
let content = flex_box_resolve_width(source, basis, Some(content_max), resolver, context)?
|
box_content_intrinsics(source, &scope.phase(Phase::FlexBasisContent), context)?
|
||||||
.unwrap_or(content_max);
|
.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);
|
return Ok(box_horizontal_side(source).unwrap_or(0) + content);
|
||||||
}
|
}
|
||||||
Ok(match axis {
|
Ok(match axis {
|
||||||
@ -5852,13 +5893,11 @@ fn measure_flex_item<'a>(
|
|||||||
item: &'a FlexItem,
|
item: &'a FlexItem,
|
||||||
axis: FlexAxis,
|
axis: FlexAxis,
|
||||||
inline_viewport: Option<i64>,
|
inline_viewport: Option<i64>,
|
||||||
resolver: Option<&'a RetainedDocument>,
|
scope: RenderScope<'a>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
) -> Result<FlexRuntimeItem<'a>, String> {
|
) -> Result<FlexRuntimeItem<'a>, String> {
|
||||||
let source = match resolver {
|
let scope = scope.resolve()?;
|
||||||
Some(resolver) => resolver.resolve(&item.node)?,
|
let source = scope.view.node;
|
||||||
None => &item.node,
|
|
||||||
};
|
|
||||||
let uses_inline_viewport = flex_item_uses_inline_viewport(source);
|
let uses_inline_viewport = flex_item_uses_inline_viewport(source);
|
||||||
let measurement_context = LayoutContext {
|
let measurement_context = LayoutContext {
|
||||||
viewport_width: if uses_inline_viewport {
|
viewport_width: if uses_inline_viewport {
|
||||||
@ -5870,13 +5909,17 @@ fn measure_flex_item<'a>(
|
|||||||
viewport_height: context.viewport_height,
|
viewport_height: context.viewport_height,
|
||||||
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic,
|
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic,
|
||||||
};
|
};
|
||||||
let rendered = render_node(source, resolver, measurement_context, uses_inline_viewport)?;
|
let rendered =
|
||||||
let min_main = flex_min_main(source, &rendered, axis, resolver, context)?;
|
scope
|
||||||
let max_main = flex_max_main(source, axis, resolver, context)?;
|
.phase(Phase::FlexMeasure)
|
||||||
let base = flex_basis_main(source, &rendered, axis, &item.basis, resolver, context)?.max(0);
|
.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);
|
let hypothetical = flex_clamp_main(base, min_main, max_main);
|
||||||
Ok(FlexRuntimeItem {
|
Ok(FlexRuntimeItem {
|
||||||
source,
|
source,
|
||||||
|
scope,
|
||||||
grow: item.grow,
|
grow: item.grow,
|
||||||
shrink: item.shrink,
|
shrink: item.shrink,
|
||||||
align_self: item.align_self,
|
align_self: item.align_self,
|
||||||
@ -6221,7 +6264,7 @@ fn render_flex_sized_entry(
|
|||||||
main: i64,
|
main: i64,
|
||||||
cross: Option<i64>,
|
cross: Option<i64>,
|
||||||
container_align: FlexAlign,
|
container_align: FlexAlign,
|
||||||
resolver: Option<&RetainedDocument>,
|
phase: Phase,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
) -> Result<FlexSizedEntry, String> {
|
) -> Result<FlexSizedEntry, String> {
|
||||||
@ -6244,13 +6287,10 @@ fn render_flex_sized_entry(
|
|||||||
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic,
|
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic,
|
||||||
};
|
};
|
||||||
let override_size = box_override_for_flex(item.source, axis, main, cross, stretch);
|
let override_size = box_override_for_flex(item.source, axis, main, cross, stretch);
|
||||||
let mut rendered = render_node_with_override(
|
let mut rendered = item
|
||||||
item.source,
|
.scope
|
||||||
resolver,
|
.phase(phase)
|
||||||
render_context,
|
.render(render_context, intrinsic, override_size)?;
|
||||||
intrinsic,
|
|
||||||
override_size,
|
|
||||||
)?;
|
|
||||||
match axis {
|
match axis {
|
||||||
FlexAxis::Row => {
|
FlexAxis::Row => {
|
||||||
rendered = pad_rendered_width(rendered, main, FlexAlign::FlexStart);
|
rendered = pad_rendered_width(rendered, main, FlexAlign::FlexStart);
|
||||||
@ -6341,6 +6381,7 @@ fn exact_rendered_height(
|
|||||||
resolver: Option<&RetainedDocument>,
|
resolver: Option<&RetainedDocument>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
) -> Option<i64> {
|
) -> Option<i64> {
|
||||||
|
evaluation::record_height_query();
|
||||||
let node = resolver
|
let node = resolver
|
||||||
.and_then(|value| value.resolve(node).ok())
|
.and_then(|value| value.resolve(node).ok())
|
||||||
.unwrap_or(node);
|
.unwrap_or(node);
|
||||||
@ -6391,19 +6432,25 @@ fn exact_rendered_height(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_node_window(
|
fn render_node_window(
|
||||||
node: &LayoutNode,
|
scope: &RenderScope<'_>,
|
||||||
resolver: Option<&RetainedDocument>,
|
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
start: i64,
|
start: i64,
|
||||||
height: i64,
|
height: i64,
|
||||||
) -> Option<Result<Rendered, String>> {
|
) -> Option<Result<Rendered, String>> {
|
||||||
let node = resolver
|
let scope = match scope.resolve() {
|
||||||
.and_then(|value| value.resolve(node).ok())
|
Ok(scope) => scope,
|
||||||
.unwrap_or(node);
|
Err(error) => return Some(Err(error)),
|
||||||
|
};
|
||||||
|
let node = scope.view.node;
|
||||||
|
let resolver = scope.resolver;
|
||||||
let total_height = exact_rendered_height(node, resolver, context)?;
|
let total_height = exact_rendered_height(node, resolver, context)?;
|
||||||
if start <= 0 && height >= total_height {
|
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 {
|
match node {
|
||||||
LayoutNode::Column { children, .. }
|
LayoutNode::Column { children, .. }
|
||||||
@ -6412,19 +6459,21 @@ fn render_node_window(
|
|||||||
&& context.viewport_width_known =>
|
&& context.viewport_width_known =>
|
||||||
{
|
{
|
||||||
Some(render_column_window(
|
Some(render_column_window(
|
||||||
children, resolver, context, start, height,
|
children, &scope, context, start, height,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
_ => Some(
|
_ => Some(
|
||||||
render_node(node, resolver, context, intrinsic)
|
scope
|
||||||
|
.phase(Phase::WindowFallback)
|
||||||
|
.render(context, intrinsic, None)
|
||||||
.map(|rendered| slice_rendered(rendered, start, height)),
|
.map(|rendered| slice_rendered(rendered, start, height)),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_column_window(
|
fn render_column_window<'a>(
|
||||||
children: &[LayoutNode],
|
children: &'a [LayoutNode],
|
||||||
resolver: Option<&RetainedDocument>,
|
scope: &RenderScope<'a>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
start: i64,
|
start: i64,
|
||||||
height: i64,
|
height: i64,
|
||||||
@ -6434,8 +6483,8 @@ fn render_column_window(
|
|||||||
let mut offset = 0_i64;
|
let mut offset = 0_i64;
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
for child in column_leaves(children) {
|
for child in column_leaves(scope, children) {
|
||||||
let child_height = exact_rendered_height(child, resolver, context)
|
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())?;
|
.ok_or_else(|| "Native layout column window has an unbounded child".to_owned())?;
|
||||||
let child_end = offset.saturating_add(child_height);
|
let child_end = offset.saturating_add(child_height);
|
||||||
if child_end <= start {
|
if child_end <= start {
|
||||||
@ -6449,18 +6498,21 @@ fn render_column_window(
|
|||||||
let child_start = start.saturating_sub(offset);
|
let child_start = start.saturating_sub(offset);
|
||||||
let child_window_height = (child_end.min(end) - (offset + child_start)).max(0);
|
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 {
|
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 {
|
} else {
|
||||||
render_node_window(
|
render_node_window(
|
||||||
child,
|
&child.phase(Phase::WindowChildPartial),
|
||||||
resolver,
|
|
||||||
context,
|
context,
|
||||||
false,
|
false,
|
||||||
child_start,
|
child_start,
|
||||||
child_window_height,
|
child_window_height,
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|| {
|
.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))
|
.map(|rendered| slice_rendered(rendered, child_start, child_window_height))
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
@ -6488,7 +6540,6 @@ fn flex_line_cross(
|
|||||||
line: &[FlexRuntimeItem<'_>],
|
line: &[FlexRuntimeItem<'_>],
|
||||||
axis: FlexAxis,
|
axis: FlexAxis,
|
||||||
container_align: FlexAlign,
|
container_align: FlexAlign,
|
||||||
resolver: Option<&RetainedDocument>,
|
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
) -> Result<i64, String> {
|
) -> Result<i64, String> {
|
||||||
@ -6501,7 +6552,7 @@ fn flex_line_cross(
|
|||||||
item.target,
|
item.target,
|
||||||
None,
|
None,
|
||||||
container_align,
|
container_align,
|
||||||
resolver,
|
Phase::FlexCrossProbe,
|
||||||
context,
|
context,
|
||||||
intrinsic,
|
intrinsic,
|
||||||
)?
|
)?
|
||||||
@ -6558,7 +6609,6 @@ fn render_flex_row_line(
|
|||||||
main_gap: i64,
|
main_gap: i64,
|
||||||
justify: FlexAlign,
|
justify: FlexAlign,
|
||||||
align: FlexAlign,
|
align: FlexAlign,
|
||||||
resolver: Option<&RetainedDocument>,
|
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
) -> Result<Rendered, String> {
|
) -> Result<Rendered, String> {
|
||||||
@ -6573,7 +6623,7 @@ fn render_flex_row_line(
|
|||||||
item.target,
|
item.target,
|
||||||
Some(line_cross),
|
Some(line_cross),
|
||||||
align,
|
align,
|
||||||
resolver,
|
Phase::FlexFinal,
|
||||||
context,
|
context,
|
||||||
intrinsic,
|
intrinsic,
|
||||||
)?;
|
)?;
|
||||||
@ -6596,7 +6646,6 @@ fn render_flex_column_line(
|
|||||||
main_gap: i64,
|
main_gap: i64,
|
||||||
justify: FlexAlign,
|
justify: FlexAlign,
|
||||||
align: FlexAlign,
|
align: FlexAlign,
|
||||||
resolver: Option<&RetainedDocument>,
|
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
) -> Result<Rendered, String> {
|
) -> Result<Rendered, String> {
|
||||||
@ -6618,7 +6667,7 @@ fn render_flex_column_line(
|
|||||||
item.target,
|
item.target,
|
||||||
Some(line_cross),
|
Some(line_cross),
|
||||||
align,
|
align,
|
||||||
resolver,
|
Phase::FlexFinal,
|
||||||
context,
|
context,
|
||||||
intrinsic,
|
intrinsic,
|
||||||
)?
|
)?
|
||||||
@ -6645,7 +6694,6 @@ fn render_flex_row(
|
|||||||
align: FlexAlign,
|
align: FlexAlign,
|
||||||
align_content: FlexAlign,
|
align_content: FlexAlign,
|
||||||
single_line: bool,
|
single_line: bool,
|
||||||
resolver: Option<&RetainedDocument>,
|
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
) -> Result<Rendered, String> {
|
) -> Result<Rendered, String> {
|
||||||
@ -6664,7 +6712,7 @@ fn render_flex_row(
|
|||||||
} else {
|
} else {
|
||||||
lines
|
lines
|
||||||
.iter()
|
.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<_>, _>>()?
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
};
|
};
|
||||||
let cross_layout =
|
let cross_layout =
|
||||||
@ -6690,7 +6738,6 @@ fn render_flex_row(
|
|||||||
main_gap,
|
main_gap,
|
||||||
justify,
|
justify,
|
||||||
align,
|
align,
|
||||||
resolver,
|
|
||||||
context,
|
context,
|
||||||
intrinsic,
|
intrinsic,
|
||||||
)?);
|
)?);
|
||||||
@ -6721,7 +6768,6 @@ fn render_flex_column(
|
|||||||
align: FlexAlign,
|
align: FlexAlign,
|
||||||
align_content: FlexAlign,
|
align_content: FlexAlign,
|
||||||
single_line: bool,
|
single_line: bool,
|
||||||
resolver: Option<&RetainedDocument>,
|
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
) -> Result<Rendered, String> {
|
) -> Result<Rendered, String> {
|
||||||
@ -6733,9 +6779,7 @@ fn render_flex_column(
|
|||||||
} else {
|
} else {
|
||||||
lines
|
lines
|
||||||
.iter()
|
.iter()
|
||||||
.map(|line| {
|
.map(|line| flex_line_cross(line, FlexAxis::Column, align, context, intrinsic))
|
||||||
flex_line_cross(line, FlexAxis::Column, align, resolver, context, intrinsic)
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
};
|
};
|
||||||
let cross_layout =
|
let cross_layout =
|
||||||
@ -6751,7 +6795,7 @@ fn render_flex_column(
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
let rendered = render_flex_column_line(
|
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));
|
parts.push((rendered, line_cross));
|
||||||
if index + 1 < line_count {
|
if index + 1 < line_count {
|
||||||
@ -6769,7 +6813,7 @@ fn render_flex_column(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_flex(
|
fn render_flex<'a>(
|
||||||
direction: FlexDirection,
|
direction: FlexDirection,
|
||||||
wrap: FlexWrap,
|
wrap: FlexWrap,
|
||||||
justify: FlexAlign,
|
justify: FlexAlign,
|
||||||
@ -6779,8 +6823,8 @@ fn render_flex(
|
|||||||
height: &Size,
|
height: &Size,
|
||||||
row_gap: i64,
|
row_gap: i64,
|
||||||
column_gap: i64,
|
column_gap: i64,
|
||||||
source_items: &[FlexItem],
|
source_items: &'a [FlexItem],
|
||||||
resolver: Option<&RetainedDocument>,
|
scope: &RenderScope<'a>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
) -> Result<Rendered, String> {
|
) -> Result<Rendered, String> {
|
||||||
@ -6801,7 +6845,7 @@ fn render_flex(
|
|||||||
&source_items[index],
|
&source_items[index],
|
||||||
axis,
|
axis,
|
||||||
inline_viewport,
|
inline_viewport,
|
||||||
resolver,
|
scope.child(LocalStep::FlexItem(index), &source_items[index].node),
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -6829,7 +6873,6 @@ fn render_flex(
|
|||||||
align_items,
|
align_items,
|
||||||
align_content,
|
align_content,
|
||||||
single_line,
|
single_line,
|
||||||
resolver,
|
|
||||||
context,
|
context,
|
||||||
intrinsic,
|
intrinsic,
|
||||||
),
|
),
|
||||||
@ -6843,20 +6886,20 @@ fn render_flex(
|
|||||||
align_items,
|
align_items,
|
||||||
align_content,
|
align_content,
|
||||||
single_line,
|
single_line,
|
||||||
resolver,
|
|
||||||
context,
|
context,
|
||||||
intrinsic,
|
intrinsic,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
struct BoxOverride {
|
struct BoxOverride {
|
||||||
content_width: Option<i64>,
|
content_width: Option<i64>,
|
||||||
content_height: Option<i64>,
|
content_height: Option<i64>,
|
||||||
declared_width: Option<i64>,
|
declared_width: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn render_node(
|
fn render_node(
|
||||||
node: &LayoutNode,
|
node: &LayoutNode,
|
||||||
resolver: Option<&RetainedDocument>,
|
resolver: Option<&RetainedDocument>,
|
||||||
@ -6873,10 +6916,17 @@ fn render_node_with_override(
|
|||||||
intrinsic: bool,
|
intrinsic: bool,
|
||||||
size_override: Option<BoxOverride>,
|
size_override: Option<BoxOverride>,
|
||||||
) -> Result<Rendered, String> {
|
) -> Result<Rendered, String> {
|
||||||
let node = match resolver {
|
RenderScope::uncached(node, resolver).render(context, intrinsic, size_override)
|
||||||
Some(resolver) => resolver.resolve(node)?,
|
}
|
||||||
None => node,
|
|
||||||
};
|
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)]
|
#[cfg(test)]
|
||||||
TEST_RENDER_NODE_COUNT.with(|count| {
|
TEST_RENDER_NODE_COUNT.with(|count| {
|
||||||
if let Some(value) = count.get() {
|
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_start = None;
|
||||||
let mut windowed_child_height = None;
|
let mut windowed_child_height = None;
|
||||||
let mut child_rendered = if let Some(child) = child {
|
let mut child_rendered = if let Some(child) = child {
|
||||||
|
let child_scope = scope.child(LocalStep::BoxChild, child);
|
||||||
let child_context = LayoutContext {
|
let child_context = LayoutContext {
|
||||||
viewport_width: if intrinsic_child {
|
viewport_width: if intrinsic_child {
|
||||||
0
|
0
|
||||||
@ -7049,8 +7100,7 @@ fn render_node_with_override(
|
|||||||
windowed_child_height = Some(total_height);
|
windowed_child_height = Some(total_height);
|
||||||
Some(
|
Some(
|
||||||
render_node_window(
|
render_node_window(
|
||||||
child,
|
&child_scope,
|
||||||
resolver,
|
|
||||||
child_context,
|
child_context,
|
||||||
intrinsic || intrinsic_child,
|
intrinsic || intrinsic_child,
|
||||||
start,
|
start,
|
||||||
@ -7059,20 +7109,14 @@ fn render_node_with_override(
|
|||||||
.expect("exact height checked above")?,
|
.expect("exact height checked above")?,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Some(render_node(
|
Some(child_scope.render(
|
||||||
child,
|
|
||||||
resolver,
|
|
||||||
child_context,
|
child_context,
|
||||||
intrinsic || intrinsic_child,
|
intrinsic || intrinsic_child,
|
||||||
|
None,
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Some(render_node(
|
Some(child_scope.render(child_context, intrinsic || intrinsic_child, None)?)
|
||||||
child,
|
|
||||||
resolver,
|
|
||||||
child_context,
|
|
||||||
intrinsic || intrinsic_child,
|
|
||||||
)?)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -7425,7 +7469,14 @@ fn render_node_with_override(
|
|||||||
};
|
};
|
||||||
let rendered = children
|
let rendered = children
|
||||||
.iter()
|
.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<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
let parts = rendered
|
let parts = rendered
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -7437,8 +7488,8 @@ fn render_node_with_override(
|
|||||||
Ok(concat_horizontal_sized(parts, 0))
|
Ok(concat_horizontal_sized(parts, 0))
|
||||||
}
|
}
|
||||||
LayoutNode::Column { children, .. } => {
|
LayoutNode::Column { children, .. } => {
|
||||||
let rendered = column_leaves(children)
|
let rendered = column_leaves(scope, children)
|
||||||
.map(|child| render_node(child, resolver, context, intrinsic))
|
.map(|child| child.render(context, intrinsic, None))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
let maximum = rendered
|
let maximum = rendered
|
||||||
.iter()
|
.iter()
|
||||||
@ -7492,7 +7543,7 @@ fn render_node_with_override(
|
|||||||
*row_gap,
|
*row_gap,
|
||||||
*column_gap,
|
*column_gap,
|
||||||
items,
|
items,
|
||||||
resolver,
|
scope,
|
||||||
context,
|
context,
|
||||||
intrinsic,
|
intrinsic,
|
||||||
),
|
),
|
||||||
@ -7501,12 +7552,19 @@ fn render_node_with_override(
|
|||||||
|
|
||||||
// Flatten only literal Columns. In particular, a retained NodeRef remains a
|
// Flatten only literal Columns. In particular, a retained NodeRef remains a
|
||||||
// child evaluation boundary even when it resolves to an identified Column.
|
// child evaluation boundary even when it resolves to an identified Column.
|
||||||
fn column_leaves(children: &[LayoutNode]) -> impl Iterator<Item = &LayoutNode> {
|
fn column_leaves<'a>(
|
||||||
let mut stack = vec![children.iter()];
|
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 {
|
std::iter::from_fn(move || loop {
|
||||||
match stack.last_mut()?.next() {
|
let (scope, children) = stack.last_mut()?;
|
||||||
Some(LayoutNode::Column { children, .. }) => stack.push(children.iter()),
|
match children.next() {
|
||||||
Some(child) => return Some(child),
|
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 => {
|
None => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
@ -7633,7 +7691,11 @@ mod tests {
|
|||||||
serde_json::from_str(json).unwrap()
|
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 {
|
MeasuredCluster {
|
||||||
text: text.to_owned(),
|
text: text.to_owned(),
|
||||||
width,
|
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 {
|
MeasuredText {
|
||||||
lines: lines
|
lines: lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -7716,7 +7778,7 @@ mod tests {
|
|||||||
node
|
node
|
||||||
}
|
}
|
||||||
|
|
||||||
fn child_box(
|
pub(super) fn child_box(
|
||||||
region_id: i64,
|
region_id: i64,
|
||||||
child: LayoutNode,
|
child: LayoutNode,
|
||||||
surface_template_id: Option<u32>,
|
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 {
|
match &mut node {
|
||||||
LayoutNode::Box {
|
LayoutNode::Box {
|
||||||
node_id: id,
|
node_id: id,
|
||||||
@ -7801,7 +7863,7 @@ mod tests {
|
|||||||
node
|
node
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retained_document(root: LayoutNode) -> LayoutDocument {
|
pub(super) fn retained_document(root: LayoutNode) -> LayoutDocument {
|
||||||
LayoutDocument {
|
LayoutDocument {
|
||||||
version: LAYOUT_VERSION,
|
version: LAYOUT_VERSION,
|
||||||
space_width: 1,
|
space_width: 1,
|
||||||
@ -7863,7 +7925,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_context() -> LayoutContext {
|
pub(super) fn test_context() -> LayoutContext {
|
||||||
LayoutContext {
|
LayoutContext {
|
||||||
viewport_width: 80,
|
viewport_width: 80,
|
||||||
viewport_width_known: true,
|
viewport_width_known: true,
|
||||||
@ -8607,7 +8669,7 @@ mod tests {
|
|||||||
assert_eq!(joined.lines[1].break_after, Some(AtomProperties::default()));
|
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 {
|
LayoutNode::Text {
|
||||||
node_id: None,
|
node_id: None,
|
||||||
node_revision: 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!({
|
serde_json::from_value(serde_json::json!({
|
||||||
"style-base-count": 0,
|
"style-base-count": 0,
|
||||||
"styles-append": [],
|
"styles-append": [],
|
||||||
|
|||||||
@ -3,7 +3,8 @@ pub mod sequence;
|
|||||||
|
|
||||||
use layout::{
|
use layout::{
|
||||||
encode_error_tape, DocumentDelta, LayoutContext, LayoutDocument, LayoutTape, RetainedDocument,
|
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 serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
@ -379,6 +380,7 @@ enum JobPayload {
|
|||||||
Echo(Vec<u8>),
|
Echo(Vec<u8>),
|
||||||
Layout {
|
Layout {
|
||||||
document: LayoutSource,
|
document: LayoutSource,
|
||||||
|
source_changes: Option<SourceChanges>,
|
||||||
context: LayoutContext,
|
context: LayoutContext,
|
||||||
root_width: i64,
|
root_width: i64,
|
||||||
root_width_override: bool,
|
root_width_override: bool,
|
||||||
@ -426,6 +428,27 @@ impl LayoutSource {
|
|||||||
Self::Retained(document) => document.styles(),
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
@ -444,6 +467,7 @@ struct ConfirmedBaseline {
|
|||||||
document: LayoutSource,
|
document: LayoutSource,
|
||||||
document_revision: u64,
|
document_revision: u64,
|
||||||
tape: LayoutTape,
|
tape: LayoutTape,
|
||||||
|
retained_frame: Option<Arc<RetainedFrame>>,
|
||||||
styles: Vec<layout::StyleTemplate>,
|
styles: Vec<layout::StyleTemplate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,6 +477,7 @@ struct PendingBaseline {
|
|||||||
document: LayoutSource,
|
document: LayoutSource,
|
||||||
document_revision: u64,
|
document_revision: u64,
|
||||||
tape: LayoutTape,
|
tape: LayoutTape,
|
||||||
|
retained_frame: Option<Arc<RetainedFrame>>,
|
||||||
styles: Vec<layout::StyleTemplate>,
|
styles: Vec<layout::StyleTemplate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,6 +496,7 @@ struct RenderedJob {
|
|||||||
resolver_lookups: u64,
|
resolver_lookups: u64,
|
||||||
atom_plan_work: layout::AtomPlanWork,
|
atom_plan_work: layout::AtomPlanWork,
|
||||||
line_plan_work: layout::LinePlanWork,
|
line_plan_work: layout::LinePlanWork,
|
||||||
|
eval_work: layout::EvalWork,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -527,6 +553,7 @@ struct RuntimeState {
|
|||||||
source_change_work: layout::SourceChangeWork,
|
source_change_work: layout::SourceChangeWork,
|
||||||
atom_plan_work: layout::AtomPlanWork,
|
atom_plan_work: layout::AtomPlanWork,
|
||||||
line_plan_work: layout::LinePlanWork,
|
line_plan_work: layout::LinePlanWork,
|
||||||
|
eval_work: layout::EvalWork,
|
||||||
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
|
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,6 +634,7 @@ struct SessionStats {
|
|||||||
source_change_work: layout::SourceChangeWork,
|
source_change_work: layout::SourceChangeWork,
|
||||||
atom_plan_work: layout::AtomPlanWork,
|
atom_plan_work: layout::AtomPlanWork,
|
||||||
line_plan_work: layout::LinePlanWork,
|
line_plan_work: layout::LinePlanWork,
|
||||||
|
eval_work: layout::EvalWork,
|
||||||
pending_baselines: usize,
|
pending_baselines: usize,
|
||||||
confirmed_baseline: bool,
|
confirmed_baseline: bool,
|
||||||
confirmed_baseline_bytes: usize,
|
confirmed_baseline_bytes: usize,
|
||||||
@ -682,6 +710,7 @@ impl Session {
|
|||||||
source_change_work: layout::SourceChangeWork::default(),
|
source_change_work: layout::SourceChangeWork::default(),
|
||||||
atom_plan_work: layout::AtomPlanWork::default(),
|
atom_plan_work: layout::AtomPlanWork::default(),
|
||||||
line_plan_work: layout::LinePlanWork::default(),
|
line_plan_work: layout::LinePlanWork::default(),
|
||||||
|
eval_work: layout::EvalWork::default(),
|
||||||
confirmed_baseline,
|
confirmed_baseline,
|
||||||
}),
|
}),
|
||||||
readiness_channel: Mutex::new(None),
|
readiness_channel: Mutex::new(None),
|
||||||
@ -807,6 +836,7 @@ impl Session {
|
|||||||
frame,
|
frame,
|
||||||
document_base_revision,
|
document_base_revision,
|
||||||
document_target_revision,
|
document_target_revision,
|
||||||
|
None,
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
let payload = frame
|
let payload = frame
|
||||||
@ -955,6 +985,7 @@ impl Session {
|
|||||||
.unwrap_or_else(|poison| poison.into_inner())
|
.unwrap_or_else(|poison| poison.into_inner())
|
||||||
.confirmed_baseline
|
.confirmed_baseline
|
||||||
.clone();
|
.clone();
|
||||||
|
let mut source_changes = None;
|
||||||
let (document, input_stats) = match (batch.document, batch.document_delta) {
|
let (document, input_stats) = match (batch.document, batch.document_delta) {
|
||||||
(Some(_), Some(_)) => {
|
(Some(_), Some(_)) => {
|
||||||
return Err(
|
return Err(
|
||||||
@ -1029,9 +1060,9 @@ impl Session {
|
|||||||
"Native retained source changes document identity mismatch".to_owned()
|
"Native retained source changes document identity mismatch".to_owned()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Consume exact source facts for input accounting. The renderer
|
// Account for exact input work, then carry these facts only for
|
||||||
// still evaluates the target normally; no history-bearing change
|
// this render job. Documents and baselines never store the
|
||||||
// object is stored in a job, document or confirmed baseline.
|
// descriptor's references to both versions.
|
||||||
let mut source_change_work = applied.stats.source_work;
|
let mut source_change_work = applied.stats.source_work;
|
||||||
let mut previous_owner = None;
|
let mut previous_owner = None;
|
||||||
applied
|
applied
|
||||||
@ -1047,6 +1078,7 @@ impl Session {
|
|||||||
let (styles, property_templates) = applied.changes.registry_ranges();
|
let (styles, property_templates) = applied.changes.registry_ranges();
|
||||||
source_change_work.styles_appended = styles.len() as u64;
|
source_change_work.styles_appended = styles.len() as u64;
|
||||||
source_change_work.property_templates_added = property_templates.len() as u64;
|
source_change_work.property_templates_added = property_templates.len() as u64;
|
||||||
|
source_changes = Some(applied.changes);
|
||||||
(
|
(
|
||||||
LayoutSource::Retained(applied.document),
|
LayoutSource::Retained(applied.document),
|
||||||
DocumentInputStats {
|
DocumentInputStats {
|
||||||
@ -1104,6 +1136,7 @@ impl Session {
|
|||||||
frame,
|
frame,
|
||||||
document_base_revision,
|
document_base_revision,
|
||||||
document_target_revision,
|
document_target_revision,
|
||||||
|
source_changes,
|
||||||
)?;
|
)?;
|
||||||
let output = render_layout_payload(
|
let output = render_layout_payload(
|
||||||
prepared.payload,
|
prepared.payload,
|
||||||
@ -1132,6 +1165,7 @@ impl Session {
|
|||||||
state.document_resolver_lookups += output.resolver_lookups;
|
state.document_resolver_lookups += output.resolver_lookups;
|
||||||
state.atom_plan_work.accumulate(output.atom_plan_work);
|
state.atom_plan_work.accumulate(output.atom_plan_work);
|
||||||
state.line_plan_work.accumulate(output.line_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_parses += input_stats.parses;
|
||||||
state.document_validations += input_stats.validations;
|
state.document_validations += input_stats.validations;
|
||||||
state.document_reuses += input_stats.reuses;
|
state.document_reuses += input_stats.reuses;
|
||||||
@ -1176,6 +1210,7 @@ impl Session {
|
|||||||
document: pending.document,
|
document: pending.document,
|
||||||
document_revision: pending.document_revision,
|
document_revision: pending.document_revision,
|
||||||
tape: pending.tape,
|
tape: pending.tape,
|
||||||
|
retained_frame: pending.retained_frame,
|
||||||
styles: pending.styles,
|
styles: pending.styles,
|
||||||
}));
|
}));
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@ -1253,6 +1288,8 @@ impl Session {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(|poison| poison.into_inner());
|
.unwrap_or_else(|poison| poison.into_inner());
|
||||||
let pending_baselines = state.pending_baselines.len();
|
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
|
let confirmed_baseline_bytes = state
|
||||||
.confirmed_baseline
|
.confirmed_baseline
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -1286,6 +1323,7 @@ impl Session {
|
|||||||
source_change_work: state.source_change_work,
|
source_change_work: state.source_change_work,
|
||||||
atom_plan_work: state.atom_plan_work,
|
atom_plan_work: state.atom_plan_work,
|
||||||
line_plan_work: state.line_plan_work,
|
line_plan_work: state.line_plan_work,
|
||||||
|
eval_work: state.eval_work,
|
||||||
pending_baselines,
|
pending_baselines,
|
||||||
confirmed_baseline: state.confirmed_baseline.is_some(),
|
confirmed_baseline: state.confirmed_baseline.is_some(),
|
||||||
confirmed_baseline_bytes,
|
confirmed_baseline_bytes,
|
||||||
@ -1389,6 +1427,7 @@ fn prepare_layout_job(
|
|||||||
frame: ControlFrame,
|
frame: ControlFrame,
|
||||||
document_base_revision: u64,
|
document_base_revision: u64,
|
||||||
document_target_revision: u64,
|
document_target_revision: u64,
|
||||||
|
source_changes: Option<SourceChanges>,
|
||||||
) -> Result<PreparedJob, String> {
|
) -> Result<PreparedJob, String> {
|
||||||
layout::reset_resolver_lookups();
|
layout::reset_resolver_lookups();
|
||||||
if frame.payload.is_some() {
|
if frame.payload.is_some() {
|
||||||
@ -1462,6 +1501,7 @@ fn prepare_layout_job(
|
|||||||
delay_ms: frame.delay_ms,
|
delay_ms: frame.delay_ms,
|
||||||
payload: JobPayload::Layout {
|
payload: JobPayload::Layout {
|
||||||
document: document.clone(),
|
document: document.clone(),
|
||||||
|
source_changes,
|
||||||
context,
|
context,
|
||||||
root_width,
|
root_width,
|
||||||
root_width_override: frame.root_width_override,
|
root_width_override: frame.root_width_override,
|
||||||
@ -1498,9 +1538,11 @@ fn render_layout_payload(
|
|||||||
resolver_lookups: 0,
|
resolver_lookups: 0,
|
||||||
atom_plan_work: layout::AtomPlanWork::default(),
|
atom_plan_work: layout::AtomPlanWork::default(),
|
||||||
line_plan_work: layout::LinePlanWork::default(),
|
line_plan_work: layout::LinePlanWork::default(),
|
||||||
|
eval_work: layout::EvalWork::default(),
|
||||||
},
|
},
|
||||||
JobPayload::Layout {
|
JobPayload::Layout {
|
||||||
document,
|
document,
|
||||||
|
source_changes,
|
||||||
context,
|
context,
|
||||||
root_width,
|
root_width,
|
||||||
root_width_override,
|
root_width_override,
|
||||||
@ -1548,6 +1590,7 @@ fn render_layout_payload(
|
|||||||
Vec<u8>,
|
Vec<u8>,
|
||||||
LayoutTape,
|
LayoutTape,
|
||||||
Vec<layout::StyleTemplate>,
|
Vec<layout::StyleTemplate>,
|
||||||
|
Option<Arc<RetainedFrame>>,
|
||||||
bool,
|
bool,
|
||||||
u64,
|
u64,
|
||||||
u64,
|
u64,
|
||||||
@ -1557,8 +1600,19 @@ fn render_layout_payload(
|
|||||||
layout::reset_resolver_lookups();
|
layout::reset_resolver_lookups();
|
||||||
layout::reset_atom_plan_work();
|
layout::reset_atom_plan_work();
|
||||||
layout::reset_line_plan_work();
|
layout::reset_line_plan_work();
|
||||||
|
layout::reset_eval_work();
|
||||||
let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
|
let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
|
||||||
let target_styles = document.styles()?;
|
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 {
|
if let Some(base_context) = base_context {
|
||||||
let base_identity = BaselineIdentity {
|
let base_identity = BaselineIdentity {
|
||||||
context: base_context,
|
context: base_context,
|
||||||
@ -1582,8 +1636,7 @@ fn render_layout_payload(
|
|||||||
})
|
})
|
||||||
.cloned();
|
.cloned();
|
||||||
if require_confirmed_patch_base && base_hit.is_none() {
|
if require_confirmed_patch_base && base_hit.is_none() {
|
||||||
let target = document
|
let (target, retained_frame) = render_target()?;
|
||||||
.layout_tape(context, root_width_override.then_some(root_width))?;
|
|
||||||
let bytes = layout::encode_layout_tape(
|
let bytes = layout::encode_layout_tape(
|
||||||
target.clone(),
|
target.clone(),
|
||||||
&target_styles,
|
&target_styles,
|
||||||
@ -1591,7 +1644,7 @@ fn render_layout_payload(
|
|||||||
output.root_metadata,
|
output.root_metadata,
|
||||||
output.max_bytes,
|
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 {
|
let (old, baseline_hit, base_renders) = if let Some(baseline) = base_hit {
|
||||||
(baseline.tape.clone(), true, 0)
|
(baseline.tape.clone(), true, 0)
|
||||||
@ -1605,8 +1658,7 @@ fn render_layout_payload(
|
|||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let target =
|
let (target, retained_frame) = render_target()?;
|
||||||
document.layout_tape(context, root_width_override.then_some(root_width))?;
|
|
||||||
let bytes = layout::encode_layout_patch_tape(
|
let bytes = layout::encode_layout_patch_tape(
|
||||||
old,
|
old,
|
||||||
target.clone(),
|
target.clone(),
|
||||||
@ -1615,10 +1667,17 @@ fn render_layout_payload(
|
|||||||
output.root_metadata,
|
output.root_metadata,
|
||||||
output.max_bytes,
|
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 {
|
} else {
|
||||||
let target =
|
let (target, retained_frame) = render_target()?;
|
||||||
document.layout_tape(context, root_width_override.then_some(root_width))?;
|
|
||||||
let bytes = layout::encode_layout_tape(
|
let bytes = layout::encode_layout_tape(
|
||||||
target.clone(),
|
target.clone(),
|
||||||
&target_styles,
|
&target_styles,
|
||||||
@ -1626,32 +1685,41 @@ fn render_layout_payload(
|
|||||||
output.root_metadata,
|
output.root_metadata,
|
||||||
output.max_bytes,
|
output.max_bytes,
|
||||||
)?;
|
)?;
|
||||||
Ok((bytes, target, target_styles, false, 0, 1))
|
Ok((bytes, target, target_styles, retained_frame, false, 0, 1))
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
let resolver_lookups =
|
let resolver_lookups =
|
||||||
validation_resolver_lookups.saturating_add(layout::resolver_lookups());
|
validation_resolver_lookups.saturating_add(layout::resolver_lookups());
|
||||||
let atom_plan_work = layout::atom_plan_work();
|
let atom_plan_work = layout::atom_plan_work();
|
||||||
let line_plan_work = layout::line_plan_work();
|
let line_plan_work = layout::line_plan_work();
|
||||||
|
let eval_work = layout::eval_work();
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok((bytes, tape, styles, baseline_hit, base_renders, target_renders))) => {
|
Ok(Ok((
|
||||||
RenderedJob {
|
bytes,
|
||||||
bytes,
|
tape,
|
||||||
pending: Some(PendingBaseline {
|
styles,
|
||||||
confirmed_identity: pending_identity,
|
retained_frame,
|
||||||
document: document.clone(),
|
baseline_hit,
|
||||||
document_revision: document_target_revision,
|
base_renders,
|
||||||
tape,
|
target_renders,
|
||||||
styles,
|
))) => RenderedJob {
|
||||||
}),
|
bytes,
|
||||||
baseline_hit,
|
pending: Some(PendingBaseline {
|
||||||
base_renders,
|
confirmed_identity: pending_identity,
|
||||||
target_renders,
|
document: document.clone(),
|
||||||
resolver_lookups,
|
document_revision: document_target_revision,
|
||||||
atom_plan_work,
|
tape,
|
||||||
line_plan_work,
|
retained_frame,
|
||||||
}
|
styles,
|
||||||
}
|
}),
|
||||||
|
baseline_hit,
|
||||||
|
base_renders,
|
||||||
|
target_renders,
|
||||||
|
resolver_lookups,
|
||||||
|
atom_plan_work,
|
||||||
|
line_plan_work,
|
||||||
|
eval_work,
|
||||||
|
},
|
||||||
Ok(Err(error)) => RenderedJob {
|
Ok(Err(error)) => RenderedJob {
|
||||||
bytes: encode_error_tape(identity, &error, max_result_bytes),
|
bytes: encode_error_tape(identity, &error, max_result_bytes),
|
||||||
pending: None,
|
pending: None,
|
||||||
@ -1661,6 +1729,7 @@ fn render_layout_payload(
|
|||||||
resolver_lookups,
|
resolver_lookups,
|
||||||
atom_plan_work,
|
atom_plan_work,
|
||||||
line_plan_work,
|
line_plan_work,
|
||||||
|
eval_work,
|
||||||
},
|
},
|
||||||
Err(_) => RenderedJob {
|
Err(_) => RenderedJob {
|
||||||
bytes: encode_error_tape(identity, "native layout panicked", max_result_bytes),
|
bytes: encode_error_tape(identity, "native layout panicked", max_result_bytes),
|
||||||
@ -1671,6 +1740,7 @@ fn render_layout_payload(
|
|||||||
resolver_lookups,
|
resolver_lookups,
|
||||||
atom_plan_work,
|
atom_plan_work,
|
||||||
line_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());
|
return Err("Native proof render cannot contain delay-ms".to_owned());
|
||||||
}
|
}
|
||||||
let document = LayoutSource::Full(Arc::new(document));
|
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(
|
let output = render_layout_payload(
|
||||||
prepared.payload,
|
prepared.payload,
|
||||||
SYNC_RENDER_SESSION_ID,
|
SYNC_RENDER_SESSION_ID,
|
||||||
@ -1822,6 +1892,7 @@ fn worker_loop(shared: Arc<Shared>) {
|
|||||||
state.document_resolver_lookups += output.resolver_lookups;
|
state.document_resolver_lookups += output.resolver_lookups;
|
||||||
state.atom_plan_work.accumulate(output.atom_plan_work);
|
state.atom_plan_work.accumulate(output.atom_plan_work);
|
||||||
state.line_plan_work.accumulate(output.line_plan_work);
|
state.line_plan_work.accumulate(output.line_plan_work);
|
||||||
|
state.eval_work.accumulate(output.eval_work);
|
||||||
state.results.insert(
|
state.results.insert(
|
||||||
(job.generation, job.key),
|
(job.generation, job.key),
|
||||||
ResultEntry {
|
ResultEntry {
|
||||||
@ -2569,6 +2640,149 @@ mod tests {
|
|||||||
session.stop(true);
|
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]
|
#[test]
|
||||||
fn confirmed_fork_shares_only_immutable_baseline() {
|
fn confirmed_fork_shares_only_immutable_baseline() {
|
||||||
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
||||||
@ -2857,6 +3071,30 @@ mod tests {
|
|||||||
retry.stop(true);
|
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]
|
#[test]
|
||||||
fn async_omit_uses_the_latest_synchronously_confirmed_document() {
|
fn async_omit_uses_the_latest_synchronously_confirmed_document() {
|
||||||
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub(super) enum LocalStep {
|
pub(super) enum LocalStep {
|
||||||
BoxChild,
|
BoxChild,
|
||||||
RowChild(usize),
|
RowChild(usize),
|
||||||
@ -12,7 +12,7 @@ pub(super) enum LocalStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A local structural address stops before crossing an identified node reference.
|
/// 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) struct SourceAddress {
|
||||||
pub(super) owner_id: u64,
|
pub(super) owner_id: u64,
|
||||||
pub(super) path: Arc<[LocalStep]>,
|
pub(super) path: Arc<[LocalStep]>,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user