perf: retain precise name observations in property binding caches

This commit is contained in:
2026-10-24 02:18:25 +08:00
parent ea81150390
commit e2802efe0a
4 changed files with 60 additions and 22 deletions
+15 -14
View File
@@ -1,7 +1,7 @@
//! Target-owned managed bindings. Dependency versions are borrowed identities;
//! the host's weak Block registry never roots an otherwise unreachable graph.
use std::{
collections::{BTreeMap, HashSet},
collections::HashSet,
sync::{Arc, Mutex},
};
@@ -14,7 +14,7 @@ use crate::{block, event_dispatch::Subscription, ui_bridge::attach_desktop_type}
/// It never owns a reference or dereferences a possibly reclaimed cached object.
#[derive(Debug, Clone)]
struct Cached {
reads: BTreeMap<ObjectRef, u64>,
reads: Vec<endor_core::ReadDependency>,
output: Value,
}
@@ -114,7 +114,11 @@ pub fn connect(
Ok(token)
}
pub fn refresh(vm: &mut Vm, nodes: &[WeakObjectRef]) -> Result<()> {
pub fn refresh(
vm: &mut Vm,
nodes: &[WeakObjectRef],
host_desktops: &HashSet<ObjectRef>,
) -> Result<usize> {
let mut work = 0;
loop {
let mut evaluated = false;
@@ -140,8 +144,8 @@ pub fn refresh(vm: &mut Vm, nodes: &[WeakObjectRef]) -> Result<()> {
.map_err(|_| anyhow::anyhow!("binding cache poisoned"))?
.clone();
if cached.as_ref().is_some_and(|cached| {
cached.reads.iter().all(|(object, version)| {
vm.object_store().change_version(*object).ok() == Some(*version)
cached.reads.iter().all(|read| {
vm.object_store().read_dependency_valid(read)
}) && vm
.get_attr(target, &binding.field)
.is_ok_and(|value| value == cached.output)
@@ -154,7 +158,7 @@ pub fn refresh(vm: &mut Vm, nodes: &[WeakObjectRef]) -> Result<()> {
}
evaluated = true;
let (value, reads) = vm
.call_read_only_with_limits(
.call_read_only_precise_with_limits(
compute,
&[],
endor_core::ExecutionLimits {
@@ -164,12 +168,9 @@ pub fn refresh(vm: &mut Vm, nodes: &[WeakObjectRef]) -> Result<()> {
)
.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
let applied: Result<()> = (|| {
let mut versions = BTreeMap::new();
for object in reads {
if let Ok(version) = vm.object_store().change_version(object) {
versions.insert(object, version);
}
}
let reads = reads.dependencies.into_iter()
.filter(|read| !host_desktops.contains(&read.object()))
.collect();
let (_, data) = block::resolve(vm.object_store(), target)?;
block::apply_visual_property(&mut data.clone(), &binding.field, &value)?;
if vm.get_attr(target, &binding.field)? != value {
@@ -180,7 +181,7 @@ pub fn refresh(vm: &mut Vm, nodes: &[WeakObjectRef]) -> Result<()> {
.lock()
.map_err(|_| anyhow::anyhow!("binding cache poisoned"))? =
Some(Cached {
reads: versions,
reads,
output: value.clone(),
});
Ok(())
@@ -194,7 +195,7 @@ pub fn refresh(vm: &mut Vm, nodes: &[WeakObjectRef]) -> Result<()> {
result?;
}
if !evaluated {
return Ok(());
return Ok(work);
}
}
}
+31
View File
@@ -6175,6 +6175,37 @@ returned = button.dispatch(dispatched)"#,
assert!(!session.is_faulted());
}
#[test]
fn property_bindings_use_precise_names_and_repair_direct_output_changes() {
let (mut session, _) = ReplSession::load(
r#"
size = 80
function readWidth(): Number { return size }
rect = RectBlock(1,20)
hidden = RectBlock(1,20)
rect.bind("width",readWidth)
hidden.bind("width",readWidth)
page = Page()
page.render(rect)
"#, "app.main", 903, 1,
).unwrap();
let count = |session: &mut ReplSession| {
session.execute("ui.stats().bindingEvaluations", "app.main").unwrap().value
.parse::<usize>().unwrap()
};
let baseline = count(&mut session);
assert_eq!(baseline, 2);
assert!(session.execute("42", "app.main").unwrap().document.is_none());
assert_eq!(count(&mut session), baseline);
session.execute("size = 120", "app.main").unwrap();
assert_eq!(count(&mut session), baseline + 2);
assert_eq!(session.execute("hidden.width", "app.main").unwrap().value, "120");
session.execute("rect.width = 200", "app.main").unwrap();
assert_eq!(count(&mut session), baseline + 3);
assert_eq!(session.execute("rect.width", "app.main").unwrap().value, "120");
assert_eq!(count(&mut session), baseline + 3);
}
#[test]
fn source_render_caches_do_not_root_a_page_block_waiting_frame_cycle() {
let (mut session, _) = ReplSession::load(
+13 -7
View File
@@ -233,6 +233,7 @@ struct Session {
layout_cancels: usize,
paint_cancels: usize,
paint_slices: usize,
binding_evaluations: usize,
exiting: bool,
}
@@ -1176,6 +1177,7 @@ impl NativeCallable for Function {
("layoutCancels", session.layout_cancels as i64),
("paintCancels", session.paint_cancels as i64),
("paintSlices", session.paint_slices as i64),
("bindingEvaluations", session.binding_evaluations as i64),
(
"dependencyInvalidations",
session.dependency_invalidations as i64,
@@ -2471,13 +2473,16 @@ impl UiBridge {
Ok(())
}
pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> {
let nodes = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.nodes
.clone();
crate::binding::refresh(vm, &nodes)
let (nodes, desktops) = {
let state = self.session.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
(state.nodes.clone(), state.host_desktops())
};
let evaluated = crate::binding::refresh(vm, &nodes, &desktops)?;
let mut state = self.session.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
state.binding_evaluations = state.binding_evaluations.saturating_add(evaluated);
Ok(())
}
pub fn prepare_frame(&self, vm: &mut Vm) -> Result<Option<Option<Arc<UiNode>>>> {
let changes = vm.take_changes();
@@ -3018,6 +3023,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
layout_cancels: 0,
paint_cancels: 0,
paint_slices: 0,
binding_evaluations: 0,
exiting: false,
}));
let mut public_functions = Vec::new();
+1 -1