mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Add matching session caching and native codegen improvements
- Introduced a new matching session caching mechanism in the Lua matching module to enhance performance with native code generation. - Rewrote the Lua matching algorithm to utilize session-based caching, reducing redundant computations and improving efficiency. - Updated Rust implementations to include session caching, ensuring consistency across both Lua and Rust. - Enhanced the `hydrate` function to support session management during instance hydration. - Added comprehensive tests to validate the new caching mechanism and its integration with existing functionality. These changes significantly improve the performance and maintainability of the matching process in the application.
This commit is contained in:
parent
307ac81aec
commit
b39c01e5e9
14 changed files with 1674 additions and 461 deletions
|
|
@ -136,19 +136,39 @@ fn run_live_syncback(project_path: &Path, payload: SyncbackPayload) -> anyhow::R
|
|||
}
|
||||
|
||||
fn build_dom_from_chunks(payload: SyncbackPayload) -> anyhow::Result<WeakDom> {
|
||||
use crate::syncback::VISIBLE_SERVICES;
|
||||
|
||||
let mut dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
let root_ref = dom.root_ref();
|
||||
|
||||
let mut global_ref_map: HashMap<Ref, Ref> = HashMap::new();
|
||||
let mut created_services: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
for chunk in &payload.services {
|
||||
let chunk_dom = rbx_binary::from_reader(Cursor::new(&chunk.data))
|
||||
.with_context(|| format!("Failed to parse rbxm for service {}", chunk.class_name))?;
|
||||
|
||||
let service_ref = dom.insert(root_ref, InstanceBuilder::new(&chunk.class_name));
|
||||
created_services.insert(chunk.class_name.clone());
|
||||
|
||||
for &child_ref in chunk_dom.root().children() {
|
||||
deep_clone_into(&chunk_dom, &mut dom, child_ref, service_ref, &mut global_ref_map);
|
||||
if !chunk.data.is_empty() {
|
||||
let chunk_dom = rbx_binary::from_reader(Cursor::new(&chunk.data))
|
||||
.with_context(|| {
|
||||
format!("Failed to parse rbxm for service {}", chunk.class_name)
|
||||
})?;
|
||||
|
||||
for &child_ref in chunk_dom.root().children() {
|
||||
deep_clone_into(
|
||||
&chunk_dom,
|
||||
&mut dom,
|
||||
child_ref,
|
||||
service_ref,
|
||||
&mut global_ref_map,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for &service_name in VISIBLE_SERVICES {
|
||||
if !created_services.contains(service_name) {
|
||||
dom.insert(root_ref, InstanceBuilder::new(service_name));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
//! The change count = how many things the reconciler would need to touch
|
||||
//! to turn instance A into instance B, including the entire subtree.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rbx_dom_weak::types::Ref;
|
||||
|
|
@ -25,6 +26,21 @@ const UNMATCHED_PENALTY: u32 = 10_000;
|
|||
/// O(n^k) explosion on deeply nested trees with many same-named instances.
|
||||
const MAX_SCORING_DEPTH: u32 = 3;
|
||||
|
||||
/// Session-scoped cache for the matching algorithm. Caches
|
||||
/// `compute_change_count` results so that recursive scoring work is
|
||||
/// reused across calls at different tree levels.
|
||||
pub struct MatchingSession {
|
||||
cost_cache: RefCell<HashMap<(Ref, Ref), u32>>,
|
||||
}
|
||||
|
||||
impl MatchingSession {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cost_cache: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of the forward sync matching algorithm.
|
||||
pub struct ForwardMatchResult {
|
||||
/// Pairs of (snapshot_child, tree_ref) that were matched.
|
||||
|
|
@ -33,6 +49,9 @@ pub struct ForwardMatchResult {
|
|||
pub unmatched_snapshot: Vec<InstanceSnapshot>,
|
||||
/// Tree children with no match in the snapshot (to be removed).
|
||||
pub unmatched_tree: Vec<Ref>,
|
||||
/// Total cost of the matching (sum of all matched pair costs +
|
||||
/// unmatched penalties). Zero means no changes across anything.
|
||||
pub total_cost: u32,
|
||||
}
|
||||
|
||||
/// Match snapshot children to tree children, minimizing total changes.
|
||||
|
|
@ -40,12 +59,14 @@ pub fn match_forward(
|
|||
snapshot_children: Vec<InstanceSnapshot>,
|
||||
tree_children: &[Ref],
|
||||
tree: &RojoTree,
|
||||
session: &MatchingSession,
|
||||
) -> ForwardMatchResult {
|
||||
if snapshot_children.is_empty() && tree_children.is_empty() {
|
||||
return ForwardMatchResult {
|
||||
matched: Vec::new(),
|
||||
unmatched_snapshot: Vec::new(),
|
||||
unmatched_tree: Vec::new(),
|
||||
total_cost: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +116,7 @@ pub fn match_forward(
|
|||
let snap_remaining = snap_matched.iter().filter(|&&m| !m).count();
|
||||
let tree_remaining = tree_available.iter().filter(|&&a| a).count();
|
||||
if snap_remaining == 0 || tree_remaining == 0 {
|
||||
return build_result(snap_available, tree_children, &tree_available, matched);
|
||||
return build_result(snap_available, tree_children, &tree_available, matched, tree, session);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
|
@ -142,7 +163,8 @@ pub fn match_forward(
|
|||
None => continue,
|
||||
};
|
||||
for &ti in &avail_tree {
|
||||
let cost = compute_change_count(snap, tree_children[ti], tree, best_so_far, 0);
|
||||
let cost =
|
||||
compute_change_count(snap, tree_children[ti], tree, best_so_far, 0, session);
|
||||
pairs.push((cost, si, ti));
|
||||
if cost < best_so_far {
|
||||
best_so_far = cost;
|
||||
|
|
@ -164,19 +186,31 @@ pub fn match_forward(
|
|||
}
|
||||
}
|
||||
|
||||
build_result(snap_available, tree_children, &tree_available, matched)
|
||||
build_result(snap_available, tree_children, &tree_available, matched, tree, session)
|
||||
}
|
||||
|
||||
/// Build the final result, consuming the snapshot children.
|
||||
/// Computes `total_cost` for all matched pairs (using session cache).
|
||||
fn build_result(
|
||||
mut snap_available: Vec<Option<InstanceSnapshot>>,
|
||||
tree_children: &[Ref],
|
||||
tree_available: &[bool],
|
||||
matched_indices: Vec<(usize, usize)>,
|
||||
tree: &RojoTree,
|
||||
session: &MatchingSession,
|
||||
) -> ForwardMatchResult {
|
||||
let mut matched = Vec::with_capacity(matched_indices.len());
|
||||
let mut total_cost: u32 = 0;
|
||||
for (si, ti) in matched_indices {
|
||||
if let Some(snap) = snap_available[si].take() {
|
||||
total_cost = total_cost.saturating_add(compute_change_count(
|
||||
&snap,
|
||||
tree_children[ti],
|
||||
tree,
|
||||
u32::MAX,
|
||||
0,
|
||||
session,
|
||||
));
|
||||
matched.push((snap, tree_children[ti]));
|
||||
}
|
||||
}
|
||||
|
|
@ -190,10 +224,15 @@ fn build_result(
|
|||
.map(|(_, r)| *r)
|
||||
.collect();
|
||||
|
||||
total_cost = total_cost.saturating_add(
|
||||
(unmatched_snapshot.len() + unmatched_tree.len()) as u32 * UNMATCHED_PENALTY,
|
||||
);
|
||||
|
||||
ForwardMatchResult {
|
||||
matched,
|
||||
unmatched_snapshot,
|
||||
unmatched_tree,
|
||||
total_cost,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,6 +253,7 @@ fn match_children_for_scoring(
|
|||
tree_children: &[Ref],
|
||||
tree: &RojoTree,
|
||||
depth: u32,
|
||||
session: &MatchingSession,
|
||||
) -> ScoringMatchResult {
|
||||
let mut snap_matched = vec![false; snap_children.len()];
|
||||
let mut tree_matched = vec![false; tree_children.len()];
|
||||
|
|
@ -298,6 +338,7 @@ fn match_children_for_scoring(
|
|||
tree,
|
||||
best_so_far,
|
||||
depth,
|
||||
session,
|
||||
);
|
||||
pairs.push((cost, si, ti));
|
||||
if cost < best_so_far {
|
||||
|
|
@ -341,7 +382,16 @@ fn compute_change_count(
|
|||
tree: &RojoTree,
|
||||
best_so_far: u32,
|
||||
depth: u32,
|
||||
session: &MatchingSession,
|
||||
) -> u32 {
|
||||
let cacheable = snap.snapshot_id.is_some() && tree_ref.is_some();
|
||||
if cacheable {
|
||||
let cache_key = (snap.snapshot_id, tree_ref);
|
||||
if let Some(&cached) = session.cost_cache.borrow().get(&cache_key) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
let inst = match tree.get_instance(tree_ref) {
|
||||
Some(i) => i,
|
||||
None => return UNMATCHED_PENALTY,
|
||||
|
|
@ -356,10 +406,17 @@ fn compute_change_count(
|
|||
let tree_children = inst.children();
|
||||
|
||||
if snap_children.is_empty() && tree_children.is_empty() {
|
||||
if cacheable && cost < best_so_far {
|
||||
session
|
||||
.cost_cache
|
||||
.borrow_mut()
|
||||
.insert((snap.snapshot_id, tree_ref), cost);
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
let scoring = match_children_for_scoring(snap_children, tree_children, tree, depth + 1);
|
||||
let scoring =
|
||||
match_children_for_scoring(snap_children, tree_children, tree, depth + 1, session);
|
||||
|
||||
for &(si, ti) in &scoring.matched {
|
||||
let remaining = best_so_far.saturating_sub(cost);
|
||||
|
|
@ -369,6 +426,7 @@ fn compute_change_count(
|
|||
tree,
|
||||
remaining,
|
||||
depth + 1,
|
||||
session,
|
||||
);
|
||||
if cost >= best_so_far {
|
||||
return cost;
|
||||
|
|
@ -379,6 +437,13 @@ fn compute_change_count(
|
|||
(scoring.unmatched_snap + scoring.unmatched_tree) as u32 * UNMATCHED_PENALTY,
|
||||
);
|
||||
|
||||
if cacheable && cost < best_so_far {
|
||||
session
|
||||
.cost_cache
|
||||
.borrow_mut()
|
||||
.insert((snap.snapshot_id, tree_ref), cost);
|
||||
}
|
||||
|
||||
cost
|
||||
}
|
||||
|
||||
|
|
@ -482,7 +547,7 @@ mod tests {
|
|||
];
|
||||
let (tree, children) = make_tree_with_children(&[("Alpha", "Folder"), ("Beta", "Script")]);
|
||||
|
||||
let result = match_forward(snaps, &children, &tree);
|
||||
let result = match_forward(snaps, &children, &tree, &MatchingSession::new());
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
assert!(result.unmatched_tree.is_empty());
|
||||
|
|
@ -497,7 +562,7 @@ mod tests {
|
|||
let (tree, children) =
|
||||
make_tree_with_children(&[("Handler", "ModuleScript"), ("Handler", "Script")]);
|
||||
|
||||
let result = match_forward(snaps, &children, &tree);
|
||||
let result = match_forward(snaps, &children, &tree, &MatchingSession::new());
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
|
||||
for (snap, tree_ref) in &result.matched {
|
||||
|
|
@ -514,7 +579,7 @@ mod tests {
|
|||
];
|
||||
let (tree, children) = make_tree_with_children(&[("A", "Folder"), ("OldOnly", "Folder")]);
|
||||
|
||||
let result = match_forward(snaps, &children, &tree);
|
||||
let result = match_forward(snaps, &children, &tree, &MatchingSession::new());
|
||||
assert_eq!(result.matched.len(), 1);
|
||||
assert_eq!(result.unmatched_snapshot.len(), 1);
|
||||
assert_eq!(result.unmatched_tree.len(), 1);
|
||||
|
|
@ -529,7 +594,7 @@ mod tests {
|
|||
];
|
||||
let (tree, children) = make_tree_with_children(&[("Data", "Folder"), ("Data", "Folder")]);
|
||||
|
||||
let result = match_forward(snaps, &children, &tree);
|
||||
let result = match_forward(snaps, &children, &tree, &MatchingSession::new());
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
assert!(result.unmatched_tree.is_empty());
|
||||
|
|
@ -540,7 +605,7 @@ mod tests {
|
|||
let snaps: Vec<InstanceSnapshot> = vec![];
|
||||
let (tree, children) = make_tree_with_children(&[]);
|
||||
|
||||
let result = match_forward(snaps, &children, &tree);
|
||||
let result = match_forward(snaps, &children, &tree, &MatchingSession::new());
|
||||
assert!(result.matched.is_empty());
|
||||
}
|
||||
|
||||
|
|
@ -605,7 +670,7 @@ mod tests {
|
|||
let root_id = tree.get_root_id();
|
||||
let tree_children: Vec<Ref> = tree.get_instance(root_id).unwrap().children().to_vec();
|
||||
|
||||
let result = match_forward(snaps, &tree_children, &tree);
|
||||
let result = match_forward(snaps, &tree_children, &tree, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), count);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
|
|
@ -635,7 +700,7 @@ mod tests {
|
|||
let (tree, children) =
|
||||
make_tree_with_children(&(0..count).map(|_| ("Line", "Part")).collect::<Vec<_>>());
|
||||
|
||||
let result = match_forward(snaps, &children, &tree);
|
||||
let result = match_forward(snaps, &children, &tree, &MatchingSession::new());
|
||||
assert_eq!(result.matched.len(), count);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
assert!(result.unmatched_tree.is_empty());
|
||||
|
|
@ -714,7 +779,7 @@ mod tests {
|
|||
let root_id = tree.get_root_id();
|
||||
let tree_children: Vec<Ref> = tree.get_instance(root_id).unwrap().children().to_vec();
|
||||
|
||||
let result = match_forward(snaps, &tree_children, &tree);
|
||||
let result = match_forward(snaps, &tree_children, &tree, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
|
|
@ -852,7 +917,7 @@ mod tests {
|
|||
}));
|
||||
|
||||
let (tree, tree_refs) = make_tree_from_snapshots(tree_children);
|
||||
let result = match_forward(snaps, &tree_refs, &tree);
|
||||
let result = match_forward(snaps, &tree_refs, &tree, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 6);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
|
|
@ -945,7 +1010,7 @@ mod tests {
|
|||
tree_children.reverse();
|
||||
|
||||
let (tree, tree_refs) = make_tree_from_snapshots(tree_children);
|
||||
let result = match_forward(snaps, &tree_refs, &tree);
|
||||
let result = match_forward(snaps, &tree_refs, &tree, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 12);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
|
|
@ -1017,7 +1082,7 @@ mod tests {
|
|||
tree_children.reverse();
|
||||
|
||||
let (tree, tree_refs) = make_tree_from_snapshots(tree_children);
|
||||
let result = match_forward(snaps, &tree_refs, &tree);
|
||||
let result = match_forward(snaps, &tree_refs, &tree, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 4);
|
||||
|
||||
|
|
@ -1074,7 +1139,7 @@ mod tests {
|
|||
make_snapshot_with_props("Wall", "Part", tree_props_b),
|
||||
make_snapshot_with_props("Wall", "Part", tree_props_a),
|
||||
]);
|
||||
let result = match_forward(vec![snap_a, snap_b], &tree_refs, &tree);
|
||||
let result = match_forward(vec![snap_a, snap_b], &tree_refs, &tree, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 2, "Both should match");
|
||||
|
||||
|
|
@ -1139,7 +1204,7 @@ mod tests {
|
|||
tree_children.reverse();
|
||||
|
||||
let (tree, tree_refs) = make_tree_from_snapshots(tree_children);
|
||||
let result = match_forward(snaps, &tree_refs, &tree);
|
||||
let result = match_forward(snaps, &tree_refs, &tree, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 10);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
|
|
@ -1208,7 +1273,7 @@ mod tests {
|
|||
let tree_children: Vec<Ref> = tree.get_instance(root_id).unwrap().children().to_vec();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let result = match_forward(snaps, &tree_children, &tree);
|
||||
let result = match_forward(snaps, &tree_children, &tree, &MatchingSession::new());
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
|
|
|
|||
|
|
@ -19,12 +19,15 @@ use super::{
|
|||
|
||||
#[profiling::function]
|
||||
pub fn compute_patch_set(snapshot: Option<InstanceSnapshot>, tree: &RojoTree, id: Ref) -> PatchSet {
|
||||
use super::matching::MatchingSession;
|
||||
|
||||
let mut patch_set = PatchSet::new();
|
||||
|
||||
if let Some(snapshot) = snapshot {
|
||||
let mut context = ComputePatchContext::default();
|
||||
let session = MatchingSession::new();
|
||||
|
||||
compute_patch_set_internal(&mut context, snapshot, tree, id, &mut patch_set);
|
||||
compute_patch_set_internal(&mut context, snapshot, tree, id, &mut patch_set, &session);
|
||||
|
||||
// Rewrite Ref properties to refer to instance IDs instead of snapshot IDs
|
||||
// for all of the IDs that we know about so far.
|
||||
|
|
@ -80,6 +83,7 @@ fn compute_patch_set_internal(
|
|||
tree: &RojoTree,
|
||||
id: Ref,
|
||||
patch_set: &mut PatchSet,
|
||||
session: &super::matching::MatchingSession,
|
||||
) {
|
||||
if snapshot.snapshot_id.is_some() {
|
||||
context
|
||||
|
|
@ -92,7 +96,7 @@ fn compute_patch_set_internal(
|
|||
.expect("Instance did not exist in tree");
|
||||
|
||||
compute_property_patches(&mut snapshot, &instance, patch_set, tree);
|
||||
compute_children_patches(context, &mut snapshot, tree, id, patch_set);
|
||||
compute_children_patches(context, &mut snapshot, tree, id, patch_set, session);
|
||||
}
|
||||
|
||||
fn compute_property_patches(
|
||||
|
|
@ -216,6 +220,7 @@ fn compute_children_patches(
|
|||
tree: &RojoTree,
|
||||
id: Ref,
|
||||
patch_set: &mut PatchSet,
|
||||
session: &super::matching::MatchingSession,
|
||||
) {
|
||||
use super::matching::match_forward;
|
||||
|
||||
|
|
@ -226,12 +231,11 @@ fn compute_children_patches(
|
|||
let instance_children = instance.children().to_vec();
|
||||
let snapshot_children = take(&mut snapshot.children);
|
||||
|
||||
// Use the recursive change-count scoring + greedy assignment matching algorithm.
|
||||
let match_result = match_forward(snapshot_children, &instance_children, tree);
|
||||
let match_result = match_forward(snapshot_children, &instance_children, tree, session);
|
||||
|
||||
// Matched pairs: recursively compute patches.
|
||||
for (snapshot_child, tree_child_id) in match_result.matched {
|
||||
compute_patch_set_internal(context, snapshot_child, tree, tree_child_id, patch_set);
|
||||
compute_patch_set_internal(context, snapshot_child, tree, tree_child_id, patch_set, session);
|
||||
}
|
||||
|
||||
// Unmatched snapshots: new instances to be added.
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||
.collect();
|
||||
|
||||
// Run the 3-pass matching algorithm to pair new ↔ old children.
|
||||
let matching_session = crate::syncback::matching::MatchingSession::new();
|
||||
let match_result = match_children(
|
||||
&eligible_new,
|
||||
&eligible_old,
|
||||
|
|
@ -215,6 +216,7 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||
snapshot.old_tree(),
|
||||
None, // TODO: pass precomputed hashes for better similarity
|
||||
None,
|
||||
&matching_session,
|
||||
);
|
||||
|
||||
// Pre-seed taken_names from ALL matched old children's filesystem
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
//! The change count = how many things the reconciler would need to touch
|
||||
//! to turn instance A into instance B.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use blake3::Hash;
|
||||
|
|
@ -24,12 +25,26 @@ const UNMATCHED_PENALTY: u32 = 10_000;
|
|||
/// only flat property comparison is used (no subtree recursion).
|
||||
const MAX_SCORING_DEPTH: u32 = 3;
|
||||
|
||||
/// Session-scoped cache for the syncback matching algorithm.
|
||||
pub struct MatchingSession {
|
||||
cost_cache: RefCell<HashMap<(Ref, Ref), u32>>,
|
||||
}
|
||||
|
||||
impl MatchingSession {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cost_cache: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of the matching algorithm.
|
||||
#[derive(Debug)]
|
||||
pub struct MatchResult {
|
||||
pub matched: Vec<(Ref, Ref)>,
|
||||
pub unmatched_new: Vec<Ref>,
|
||||
pub unmatched_old: Vec<Ref>,
|
||||
pub total_cost: u32,
|
||||
}
|
||||
|
||||
/// Match new children to old children, minimizing total changes.
|
||||
|
|
@ -40,12 +55,14 @@ pub fn match_children(
|
|||
old_dom: &WeakDom,
|
||||
new_hashes: Option<&HashMap<Ref, Hash>>,
|
||||
old_hashes: Option<&HashMap<Ref, Hash>>,
|
||||
session: &MatchingSession,
|
||||
) -> MatchResult {
|
||||
if new_children.is_empty() && old_children.is_empty() {
|
||||
return MatchResult {
|
||||
matched: Vec::new(),
|
||||
unmatched_new: Vec::new(),
|
||||
unmatched_old: Vec::new(),
|
||||
total_cost: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -102,10 +119,20 @@ pub fn match_children(
|
|||
remaining_old.retain(|r| !matched_old.contains(r));
|
||||
|
||||
if remaining_new.is_empty() || remaining_old.is_empty() {
|
||||
let mut total_cost: u32 = 0;
|
||||
for &(new_ref, old_ref) in &matched {
|
||||
total_cost = total_cost.saturating_add(compute_change_count(
|
||||
new_ref, old_ref, new_dom, old_dom, new_hashes, old_hashes, u32::MAX, 0, session,
|
||||
));
|
||||
}
|
||||
total_cost = total_cost.saturating_add(
|
||||
(remaining_new.len() + remaining_old.len()) as u32 * UNMATCHED_PENALTY,
|
||||
);
|
||||
return MatchResult {
|
||||
matched,
|
||||
unmatched_new: remaining_new,
|
||||
unmatched_old: remaining_old,
|
||||
total_cost,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +178,8 @@ pub fn match_children(
|
|||
for &new_ref in &avail_new {
|
||||
for &old_ref in &avail_old {
|
||||
let cost = compute_change_count(
|
||||
new_ref,
|
||||
old_ref,
|
||||
new_dom,
|
||||
old_dom,
|
||||
new_hashes,
|
||||
old_hashes,
|
||||
best_so_far,
|
||||
0,
|
||||
new_ref, old_ref, new_dom, old_dom, new_hashes, old_hashes, best_so_far, 0,
|
||||
session,
|
||||
);
|
||||
pairs.push((cost, new_ref, old_ref));
|
||||
if cost < best_so_far {
|
||||
|
|
@ -184,10 +205,21 @@ pub fn match_children(
|
|||
remaining_new.retain(|r| !matched_new.contains(r));
|
||||
remaining_old.retain(|r| !matched_old.contains(r));
|
||||
|
||||
let mut total_cost: u32 = 0;
|
||||
for &(new_ref, old_ref) in &matched {
|
||||
total_cost = total_cost.saturating_add(compute_change_count(
|
||||
new_ref, old_ref, new_dom, old_dom, new_hashes, old_hashes, u32::MAX, 0, session,
|
||||
));
|
||||
}
|
||||
total_cost = total_cost.saturating_add(
|
||||
(remaining_new.len() + remaining_old.len()) as u32 * UNMATCHED_PENALTY,
|
||||
);
|
||||
|
||||
MatchResult {
|
||||
matched,
|
||||
unmatched_new: remaining_new,
|
||||
unmatched_old: remaining_old,
|
||||
total_cost,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -209,6 +241,7 @@ fn match_children_for_scoring(
|
|||
new_hashes: Option<&HashMap<Ref, Hash>>,
|
||||
old_hashes: Option<&HashMap<Ref, Hash>>,
|
||||
depth: u32,
|
||||
session: &MatchingSession,
|
||||
) -> ScoringMatchResult {
|
||||
let mut new_matched = vec![false; new_children.len()];
|
||||
let mut old_matched = vec![false; old_children.len()];
|
||||
|
|
@ -298,6 +331,7 @@ fn match_children_for_scoring(
|
|||
old_hashes,
|
||||
best_so_far,
|
||||
depth,
|
||||
session,
|
||||
);
|
||||
pairs.push((cost, ni, oi));
|
||||
if cost < best_so_far {
|
||||
|
|
@ -342,11 +376,25 @@ fn compute_change_count(
|
|||
old_hashes: Option<&HashMap<Ref, Hash>>,
|
||||
best_so_far: u32,
|
||||
depth: u32,
|
||||
session: &MatchingSession,
|
||||
) -> u32 {
|
||||
let cacheable = new_ref.is_some() && old_ref.is_some();
|
||||
if cacheable {
|
||||
if let Some(&cached) = session.cost_cache.borrow().get(&(new_ref, old_ref)) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash fast-path: identical subtree = 0 cost
|
||||
if let (Some(nh), Some(oh)) = (new_hashes, old_hashes) {
|
||||
if let (Some(new_hash), Some(old_hash)) = (nh.get(&new_ref), oh.get(&old_ref)) {
|
||||
if new_hash == old_hash {
|
||||
if cacheable {
|
||||
session
|
||||
.cost_cache
|
||||
.borrow_mut()
|
||||
.insert((new_ref, old_ref), 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -370,6 +418,12 @@ fn compute_change_count(
|
|||
let old_children = old_inst.children();
|
||||
|
||||
if new_children.is_empty() && old_children.is_empty() {
|
||||
if cacheable && cost < best_so_far {
|
||||
session
|
||||
.cost_cache
|
||||
.borrow_mut()
|
||||
.insert((new_ref, old_ref), cost);
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
|
|
@ -381,6 +435,7 @@ fn compute_change_count(
|
|||
new_hashes,
|
||||
old_hashes,
|
||||
depth + 1,
|
||||
session,
|
||||
);
|
||||
|
||||
for &(ni, oi) in &scoring.matched {
|
||||
|
|
@ -394,6 +449,7 @@ fn compute_change_count(
|
|||
old_hashes,
|
||||
remaining,
|
||||
depth + 1,
|
||||
session,
|
||||
);
|
||||
if cost >= best_so_far {
|
||||
return cost;
|
||||
|
|
@ -403,6 +459,13 @@ fn compute_change_count(
|
|||
cost = cost
|
||||
.saturating_add((scoring.unmatched_new + scoring.unmatched_old) as u32 * UNMATCHED_PENALTY);
|
||||
|
||||
if cacheable && cost < best_so_far {
|
||||
session
|
||||
.cost_cache
|
||||
.borrow_mut()
|
||||
.insert((new_ref, old_ref), cost);
|
||||
}
|
||||
|
||||
cost
|
||||
}
|
||||
|
||||
|
|
@ -482,7 +545,7 @@ mod tests {
|
|||
let new_children: Vec<Ref> = new_dom.get_by_ref(new_root).unwrap().children().to_vec();
|
||||
let old_children: Vec<Ref> = old_dom.get_by_ref(old_root).unwrap().children().to_vec();
|
||||
|
||||
let result = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None);
|
||||
let result = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
assert!(result.unmatched_new.is_empty());
|
||||
|
|
@ -522,7 +585,7 @@ mod tests {
|
|||
let new_children: Vec<Ref> = new_dom.get_by_ref(new_root).unwrap().children().to_vec();
|
||||
let old_children: Vec<Ref> = old_dom.get_by_ref(old_root).unwrap().children().to_vec();
|
||||
|
||||
let result = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None);
|
||||
let result = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
for (new_ref, old_ref) in &result.matched {
|
||||
|
|
@ -553,7 +616,7 @@ mod tests {
|
|||
let new_children: Vec<Ref> = new_dom.get_by_ref(new_root).unwrap().children().to_vec();
|
||||
let old_children: Vec<Ref> = old_dom.get_by_ref(old_root).unwrap().children().to_vec();
|
||||
|
||||
let result = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None);
|
||||
let result = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None, &MatchingSession::new());
|
||||
|
||||
assert_eq!(result.matched.len(), 1);
|
||||
assert_eq!(result.unmatched_new.len(), 1);
|
||||
|
|
@ -575,7 +638,7 @@ mod tests {
|
|||
let new_children: Vec<Ref> = new_dom.get_by_ref(new_root).unwrap().children().to_vec();
|
||||
let old_children: Vec<Ref> = old_dom.get_by_ref(old_root).unwrap().children().to_vec();
|
||||
|
||||
let result = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None);
|
||||
let result = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None, &MatchingSession::new());
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
assert!(result.unmatched_new.is_empty());
|
||||
assert!(result.unmatched_old.is_empty());
|
||||
|
|
@ -585,7 +648,7 @@ mod tests {
|
|||
fn empty_children() {
|
||||
let new_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
let old_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
let result = match_children(&[], &[], &new_dom, &old_dom, None, None);
|
||||
let result = match_children(&[], &[], &new_dom, &old_dom, None, None, &MatchingSession::new());
|
||||
assert!(result.matched.is_empty());
|
||||
}
|
||||
|
||||
|
|
@ -604,8 +667,8 @@ mod tests {
|
|||
let new_children: Vec<Ref> = new_dom.get_by_ref(new_root).unwrap().children().to_vec();
|
||||
let old_children: Vec<Ref> = old_dom.get_by_ref(old_root).unwrap().children().to_vec();
|
||||
|
||||
let r1 = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None);
|
||||
let r2 = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None);
|
||||
let r1 = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None, &MatchingSession::new());
|
||||
let r2 = match_children(&new_children, &old_children, &new_dom, &old_dom, None, None, &MatchingSession::new());
|
||||
|
||||
assert_eq!(r1.matched.len(), r2.matched.len());
|
||||
for (a, b) in r1.matched.iter().zip(r2.matched.iter()) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue