ops: decay only persists nodes whose weight actually changed
Previously decay() wrote all nodes to the append log on every run, even if their weight was unchanged (factor of 1.0 or negligible delta). Now only nodes with meaningful weight change get version bumped and persisted. Also simplified: near-prune clamping now happens inline instead of in a separate pass.
This commit is contained in:
parent
9eaf5e6690
commit
d068d60eab
1 changed files with 75 additions and 14 deletions
|
|
@ -61,6 +61,67 @@ impl Store {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rename a node: change its key, update debug strings on all edges.
|
||||||
|
///
|
||||||
|
/// Graph edges (source/target UUIDs) are unaffected — they're already
|
||||||
|
/// UUID-based. We update the human-readable source_key/target_key strings
|
||||||
|
/// on relations, and created_at is preserved untouched.
|
||||||
|
///
|
||||||
|
/// Appends: (new_key, v+1) + (old_key, deleted, v+1) + updated relations.
|
||||||
|
pub fn rename_node(&mut self, old_key: &str, new_key: &str) -> Result<(), String> {
|
||||||
|
if old_key == new_key {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.nodes.contains_key(new_key) {
|
||||||
|
return Err(format!("Key '{}' already exists", new_key));
|
||||||
|
}
|
||||||
|
let node = self.nodes.get(old_key)
|
||||||
|
.ok_or_else(|| format!("No node '{}'", old_key))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// New version under the new key
|
||||||
|
let mut renamed = node.clone();
|
||||||
|
renamed.key = new_key.to_string();
|
||||||
|
renamed.version += 1;
|
||||||
|
|
||||||
|
// Deletion record for the old key (same UUID, independent version counter)
|
||||||
|
let mut tombstone = node.clone();
|
||||||
|
tombstone.deleted = true;
|
||||||
|
tombstone.version += 1;
|
||||||
|
|
||||||
|
// Collect affected relations and update their debug key strings
|
||||||
|
let updated_rels: Vec<_> = self.relations.iter()
|
||||||
|
.filter(|r| r.source_key == old_key || r.target_key == old_key)
|
||||||
|
.map(|r| {
|
||||||
|
let mut r = r.clone();
|
||||||
|
r.version += 1;
|
||||||
|
if r.source_key == old_key { r.source_key = new_key.to_string(); }
|
||||||
|
if r.target_key == old_key { r.target_key = new_key.to_string(); }
|
||||||
|
r
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Persist (each append acquires its own file lock)
|
||||||
|
self.append_nodes(&[renamed.clone(), tombstone])?;
|
||||||
|
if !updated_rels.is_empty() {
|
||||||
|
self.append_relations(&updated_rels)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update in-memory cache
|
||||||
|
self.nodes.remove(old_key);
|
||||||
|
self.uuid_to_key.insert(renamed.uuid, new_key.to_string());
|
||||||
|
self.nodes.insert(new_key.to_string(), renamed);
|
||||||
|
for updated in &updated_rels {
|
||||||
|
if let Some(r) = self.relations.iter_mut().find(|r| r.uuid == updated.uuid) {
|
||||||
|
r.source_key = updated.source_key.clone();
|
||||||
|
r.target_key = updated.target_key.clone();
|
||||||
|
r.version = updated.version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Modify a node in-place, bump version, and persist to capnp log.
|
/// Modify a node in-place, bump version, and persist to capnp log.
|
||||||
fn modify_node(&mut self, key: &str, f: impl FnOnce(&mut Node)) -> Result<(), String> {
|
fn modify_node(&mut self, key: &str, f: impl FnOnce(&mut Node)) -> Result<(), String> {
|
||||||
let node = self.nodes.get_mut(key)
|
let node = self.nodes.get_mut(key)
|
||||||
|
|
@ -111,30 +172,30 @@ impl Store {
|
||||||
let threshold = self.params.prune_threshold as f32;
|
let threshold = self.params.prune_threshold as f32;
|
||||||
let mut decayed = 0;
|
let mut decayed = 0;
|
||||||
let mut pruned = 0;
|
let mut pruned = 0;
|
||||||
let mut to_remove = Vec::new();
|
let mut updated = Vec::new();
|
||||||
|
|
||||||
for (key, node) in &mut self.nodes {
|
for (_key, node) in &mut self.nodes {
|
||||||
let factor = node.category.decay_factor(base) as f32;
|
let factor = node.category.decay_factor(base) as f32;
|
||||||
|
let old_weight = node.weight;
|
||||||
node.weight *= factor;
|
node.weight *= factor;
|
||||||
node.version += 1;
|
|
||||||
decayed += 1;
|
// Clamp near-prune nodes instead of removing
|
||||||
if node.weight < threshold {
|
if node.weight < threshold {
|
||||||
to_remove.push(key.clone());
|
node.weight = node.weight.max(0.01);
|
||||||
pruned += 1;
|
pruned += 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Don't actually remove — just mark very low weight
|
// Only persist nodes whose weight actually changed
|
||||||
// Actual pruning happens during GC
|
if (node.weight - old_weight).abs() > 1e-6 {
|
||||||
for key in &to_remove {
|
node.version += 1;
|
||||||
if let Some(node) = self.nodes.get_mut(key) {
|
updated.push(node.clone());
|
||||||
node.weight = node.weight.max(0.01);
|
decayed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist all decayed weights to capnp log
|
if !updated.is_empty() {
|
||||||
let updated: Vec<Node> = self.nodes.values().cloned().collect();
|
let _ = self.append_nodes(&updated);
|
||||||
let _ = self.append_nodes(&updated);
|
}
|
||||||
|
|
||||||
(decayed, pruned)
|
(decayed, pruned)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue