perf: validate precise source name dependencies before relayout and repaint

This commit is contained in:
2026-10-24 01:40:56 +08:00
parent 0c63cd3102
commit 6a8a643371
5 changed files with 115 additions and 50 deletions
+42
View File
@@ -21,6 +21,8 @@ pub(crate) struct Presentation {
pub mounts: crate::mount::MountTable,
pub published_version: u64,
pub force_frame: bool,
pub paint_names: Vec<endor_core::ReadDependency>,
pub layout_names: Vec<endor_core::ReadDependency>,
pub paint_reads: std::collections::BTreeMap<ObjectRef, u64>,
pub dependency_invalidations: usize,
pub last_invalid_dependency: String,
@@ -54,6 +56,25 @@ impl Presentation {
vm.change_version(*object).ok()
);
}
if let Some((phase, dependency)) = self
.layout_names
.iter()
.map(|dependency| ("layout", dependency))
.chain(
self.paint_names
.iter()
.map(|dependency| ("paint", dependency)),
)
.find(|(_, dependency)| !vm.object_store().read_dependency_valid(dependency))
{
self.force_frame = true;
self.dependency_invalidations = self.dependency_invalidations.saturating_add(1);
let name = match dependency {
endor_core::ReadDependency::Name { name, .. } => name.as_str(),
_ => "<dictionary kind>",
};
self.last_invalid_dependency = format!("{phase} name {}:{name}", dependency.object());
}
self.render_cache.invalidate(ChangeBatch {
changes: changes.changes.clone(),
rescan_required: changes.rescan_required,
@@ -106,6 +127,8 @@ impl Presentation {
mounts: crate::mount::MountTable::default(),
published_version: 0,
force_frame: false,
paint_names: Vec::new(),
layout_names: Vec::new(),
paint_reads: std::collections::BTreeMap::new(),
dependency_invalidations: 0,
last_invalid_dependency: String::new(),
@@ -174,3 +197,22 @@ mod tests {
assert!(vm.upgrade(weak).unwrap().is_none());
}
}
pub(crate) fn split_reads(
reads: Vec<endor_core::ReadDependency>,
) -> (
std::collections::BTreeMap<ObjectRef, u64>,
Vec<endor_core::ReadDependency>,
) {
let mut objects = std::collections::BTreeMap::new();
let mut names = Vec::new();
for dependency in reads {
match dependency {
endor_core::ReadDependency::Object { object, version } => {
objects.insert(object, version);
}
dependency => names.push(dependency),
}
}
(objects, names)
}
+48 -29
View File
@@ -14,7 +14,7 @@ use endor_core::{
VmError, WeakObjectRef,
};
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
collections::{BTreeSet, HashMap, HashSet},
sync::{Arc, Mutex},
};
@@ -38,7 +38,7 @@ struct Measurement {
children: Vec<Size>,
constraints: [f32; 4],
size: Size,
reads: BTreeMap<ObjectRef, u64>,
reads: Vec<endor_core::ReadDependency>,
}
/// Borrowed identities and versions only: this cache adds no managed roots.
#[derive(Debug, Default, Clone)]
@@ -167,20 +167,20 @@ impl NativeCallable for Step {
if matches!(self.phase, Phase::Measure) {
let key = cx.store().downgrade(self.mount)?;
let constraints = plan.constraints[self.index];
let cached = plan
.cache
.entries
.get(&key)
.filter(|entry| {
entry.owner == mount.block
&& entry.method == method
&& entry.children == children
&& entry.constraints == constraints
&& entry.reads.iter().all(|(object, version)| {
cx.store().change_version(*object).ok() == Some(*version)
})
})
.map(|entry| entry.size);
let cached =
plan.cache
.entries
.get(&key)
.filter(|entry| {
entry.owner == mount.block
&& entry.method == method
&& entry.children == children
&& entry.constraints == constraints
&& entry.reads.iter().all(|dependency| {
cx.store().read_dependency_valid(dependency)
})
})
.map(|entry| entry.size);
if let Some(size) = cached {
plan.cache.hits += 1;
Some(size)
@@ -359,15 +359,14 @@ impl NativeCallable for Collect {
let tracked = pending
.map(|pending| {
cx.store()
.end_read_tracking_details(pending.token)
.end_read_tracking_precise(pending.token)
.map(|reads| (pending, reads))
})
.transpose()?;
if let Some((pending, reads)) = tracked {
let mut versions = BTreeMap::new();
let mut volatile = false;
let registry = desktop_type_registry(cx, self.desktop)?;
for read in reads {
for read in &reads.accesses {
let object = read.object;
if object == self.desktop || object == registry {
continue;
@@ -379,7 +378,6 @@ impl NativeCallable for Collect {
volatile |= read.content_read;
continue;
}
versions.insert(object, cx.store().change_version(object)?);
}
if volatile {
plan.cache.entries.remove(&pending.key);
@@ -392,7 +390,18 @@ impl NativeCallable for Collect {
children: pending.children,
constraints: pending.constraints,
size,
reads: versions,
reads: reads
.dependencies
.into_iter()
.filter(|dependency| {
let object = dependency.object();
object != self.desktop
&& object != registry
&& cx.store().object(object).is_ok_and(|value| {
!value.payload().as_any().is::<Mount>()
})
})
.collect(),
},
);
}
@@ -719,7 +728,7 @@ pub struct Draft {
pub frame: Frame,
pub runtime: LayoutRuntime,
pub cache: Cache,
pub reads: BTreeSet<ObjectRef>,
pub reads: Vec<endor_core::ReadDependency>,
}
#[must_use = "pending layout work must be resumed or cancelled with its owning VM"]
@@ -760,9 +769,13 @@ impl Work {
) -> Result<Progress> {
match progress {
Ok(endor_core::ExecutionProgress::Pending { .. }) => Ok(Progress::Pending(self)),
Ok(endor_core::ExecutionProgress::Complete { value, reads, .. }) => {
Ok(endor_core::ExecutionProgress::Complete {
value,
dependencies,
..
}) => {
vm.release(self.prepare)?;
finish(vm, self, value, reads)
finish(vm, self, value, dependencies)
}
Err(error) => {
// An invalid quantum can fail without discarding the VM's
@@ -911,7 +924,12 @@ pub fn start(
work.accept(vm, progress)
}
fn finish(vm: &mut Vm, mut work: Work, value: Value, reads: Vec<ObjectRef>) -> Result<Progress> {
fn finish(
vm: &mut Vm,
mut work: Work,
value: Value,
reads: Vec<endor_core::ReadDependency>,
) -> Result<Progress> {
vm.release_value(value)?;
let mut plan = work
.plan
@@ -946,11 +964,12 @@ fn finish(vm: &mut Vm, mut work: Work, value: Value, reads: Vec<ObjectRef>) -> R
};
work.state.reads = reads
.into_iter()
.filter(|object| {
*object != work.desktop
&& *object != registry
.filter(|dependency| {
let object = dependency.object();
object != work.desktop
&& object != registry
&& vm
.object(*object)
.object(object)
.is_ok_and(|object| !object.payload().as_any().is::<Mount>())
})
.collect();
+15 -10
View File
@@ -13,7 +13,7 @@ use endor_core::{
Value, Vm, VmError, WeakObjectRef,
};
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
collections::{BTreeMap, HashMap},
sync::{Arc, Mutex},
};
@@ -289,7 +289,7 @@ impl NativeCallable for Advance {
pub struct Draft {
pub frame: Frame,
pub reads: BTreeSet<ObjectRef>,
pub reads: Vec<endor_core::ReadDependency>,
pub slices: usize,
}
@@ -333,10 +333,14 @@ impl Work {
Ok(endor_core::ExecutionProgress::Pending { .. }) => {
Ok(Progress::Pending(Box::new(self)))
}
Ok(endor_core::ExecutionProgress::Complete { value, reads, .. }) => {
Ok(endor_core::ExecutionProgress::Complete {
value,
dependencies,
..
}) => {
vm.release(self.prepare)?;
vm.release_value(value)?;
finish(vm, self, reads)
finish(vm, self, dependencies)
}
Err(error) => {
let diagnostic = crate::endor_host::runtime_error(vm, error);
@@ -390,7 +394,7 @@ pub fn start(
work.accept(vm, result)
}
fn finish(vm: &mut Vm, mut work: Work, reads: Vec<ObjectRef>) -> Result<Progress> {
fn finish(vm: &mut Vm, mut work: Work, reads: Vec<endor_core::ReadDependency>) -> Result<Progress> {
let output = work
.output
.lock()
@@ -398,7 +402,7 @@ fn finish(vm: &mut Vm, mut work: Work, reads: Vec<ObjectRef>) -> Result<Progress
if output.commands.is_empty() {
return Ok(Progress::Complete(Box::new(Draft {
frame: work.frame,
reads: BTreeSet::new(),
reads: Vec::new(),
slices: work.slices,
})));
}
@@ -443,11 +447,12 @@ fn finish(vm: &mut Vm, mut work: Work, reads: Vec<ObjectRef>) -> Result<Progress
let registry = object(vm.get_attr(desktop, "types")?)?;
let reads = reads
.into_iter()
.filter(|object| {
*object != desktop
&& *object != registry
.filter(|dependency| {
let object = dependency.object();
object != desktop
&& object != registry
&& vm
.object(*object)
.object(object)
.is_ok_and(|object| !object.payload().as_any().is::<Mount>())
})
.collect();
+9 -10
View File
@@ -2596,17 +2596,13 @@ impl UiBridge {
progress: crate::source_paint::Progress,
) -> Result<crate::source_paint::Progress> {
if let crate::source_paint::Progress::Complete(draft) = &progress {
let reads = draft
.reads
.iter()
.copied()
.map(|object| vm.change_version(object).map(|version| (object, version)))
.collect::<Result<std::collections::BTreeMap<_, _>, _>>()?;
let (reads, names) = crate::presentation::split_reads(draft.reads.clone());
let mut state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
state.paint_reads = reads;
state.paint_names = names;
state.paint_slices = draft.slices;
}
Ok(progress)
@@ -2650,6 +2646,11 @@ impl UiBridge {
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.layout_reads
.clear();
self.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.layout_names
.clear();
let frame = {
let mut state = self
.session
@@ -2893,10 +2894,7 @@ impl UiBridge {
frame.tabs = tabs;
frame.window = window;
// Capture versions before lifecycle hooks can change a dependency.
let reads = reads
.into_iter()
.map(|object| vm.change_version(object).map(|version| (object, version)))
.collect::<Result<std::collections::BTreeMap<_, _>, _>>()?;
let (reads, names) = crate::presentation::split_reads(reads);
frame.focus = match vm.get_attr(desktop, "focus") {
Ok(Value::Number(id)) => id.as_i64().and_then(|n| u64::try_from(n).ok()),
_ => None,
@@ -2913,6 +2911,7 @@ impl UiBridge {
state.layout = layout;
state.measure_cache = cache;
state.layout_reads = reads;
state.layout_names = names;
}
self.commit_frame(vm, Some(root), &frame)?;
if let Some(hooks) = hooks {
+1 -1