refactor(std,composites): interned cost[k].* port names — the leaks drop (#152)

One process-global intern pool (intern_port/cost_port, the COL_PORTS
LazyLock pattern) in the cost module is now the single source of the
cost[k].<port>/cost[k].<field> names; CostSum::builder and cost_graph
consume it and the per-build .leak()s in cost_graph are gone — the
prerequisite before cost is rebuilt per member across a sweep. Two
unit pins (same pointer for the same pair, dedup for bare names).

Verified: aura-std 166/0, cost_graph 4/4, zero .leak() in the two
sites, full workspace suite green, clippy -D warnings clean.

closes #152
refs #234
This commit is contained in:
2026-07-11 06:56:21 +02:00
parent dd23ea34a5
commit 249aafdb0f
4 changed files with 72 additions and 16 deletions
+54
View File
@@ -15,6 +15,8 @@ use aura_core::{
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
ScalarKind,
};
use std::collections::HashSet;
use std::sync::{LazyLock, Mutex};
/// The 3-field cost-in-R record every cost node emits, in slot order — one source
/// of truth for the producer schema and the `CostSum` aggregator.
@@ -27,6 +29,35 @@ pub const COST_FIELD_NAMES: [&str; COST_WIDTH] =
/// these, beginning at slot `GEOMETRY_WIDTH`.
pub const GEOMETRY_WIDTH: usize = 4;
/// Process-global port-name intern pool (#152): ONE `&'static str` allocation
/// per distinct name for the process lifetime — the `COL_PORTS` `LazyLock`
/// pattern generalized. Rebuilding a cost graph per member across a sweep
/// reuses these strings instead of `.leak()`ing fresh ones per build.
static PORT_NAMES: LazyLock<Mutex<HashSet<&'static str>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
/// Intern an arbitrary port name: the same input yields the same
/// `&'static str` allocation on every call. The one leak per DISTINCT name is
/// the pool's deliberate, bounded cost; per-build callers never leak.
pub fn intern_port(name: &str) -> &'static str {
let mut pool = PORT_NAMES.lock().expect("port-name intern pool lock");
match pool.get(name) {
Some(interned) => interned,
None => {
let interned: &'static str = Box::leak(name.to_string().into_boxed_str());
pool.insert(interned);
interned
}
}
}
/// The interned `cost[{k}].{name}` port/agg name — the single source of the
/// cost-vocabulary name shape, consumed by `CostSum::builder` and `cost_graph`
/// (aura-composites) (#152).
pub fn cost_port(k: usize, name: &str) -> &'static str {
intern_port(&format!("cost[{k}].{name}"))
}
fn geometry_input_ports() -> Vec<PortSpec> {
vec![
PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "closed".into() },
@@ -474,6 +505,29 @@ mod tests {
assert_eq!(schema.params, params);
}
#[test]
fn cost_port_interns_one_static_name_per_distinct_pair() {
// Same (k, name) -> the SAME allocation (pointer equality), process-wide;
// a distinct pair -> a distinct interned name. The no-per-build-leak
// property #152 asks for: rebuilding a cost graph per member reuses these.
let a = cost_port(0, "volatility");
let b = cost_port(0, "volatility");
assert!(std::ptr::eq(a, b), "same pair must return the same interned str");
assert_eq!(a, "cost[0].volatility");
let c = cost_port(1, "volatility");
assert_eq!(c, "cost[1].volatility");
let d = cost_port(0, "cost_in_r");
assert_eq!(d, "cost[0].cost_in_r");
}
#[test]
fn intern_port_dedups_bare_names() {
let a = intern_port("volatility");
let b = intern_port("volatility");
assert!(std::ptr::eq(a, b), "the pool must dedup bare names too");
assert_eq!(a, "volatility");
}
#[test]
fn producer_and_aggregator_share_the_triple() {
// The structural lockstep: producer output and aggregator output/inputs all
+5 -2
View File
@@ -15,7 +15,7 @@ use aura_core::{
/// both the producer side (`cost_node_builder`) and this aggregator. The input-name
/// loop, the output schema, the lookback vector, and the eval accumulator all read
/// it, so the producer↔aggregator field match is structural, not by-convention.
use crate::cost::{COST_FIELD_NAMES, COST_WIDTH};
use crate::cost::{cost_port, COST_FIELD_NAMES, COST_WIDTH};
/// Per-field sum of `n_costs` cost-in-R records. Inputs are
/// `cost[k].{cost_in_r,cum_cost_in_r,open_cost_in_r}` for `k in 0..n_costs`, in
@@ -42,7 +42,10 @@ impl CostSum {
inputs.push(PortSpec {
kind: ScalarKind::F64,
firing: Firing::Any,
name: format!("cost[{k}].{field}"),
// Owned String (PortSpec.name is String); the NAME comes from
// the shared cost_port helper so the aggregator's input names
// and cost_graph's agg-side names share ONE shape definition.
name: cost_port(k, field).to_string(),
});
}
}
+2 -2
View File
@@ -62,8 +62,8 @@ pub use carry_cost::CarryCost;
pub use const_node::Const;
pub use constant_cost::ConstantCost;
pub use cost::{
cost_node_builder, ChargeMode, CostNode, CostRunner, COST_FIELD_NAMES, COST_WIDTH,
GEOMETRY_WIDTH,
cost_node_builder, cost_port, intern_port, ChargeMode, CostNode, CostRunner,
COST_FIELD_NAMES, COST_WIDTH, GEOMETRY_WIDTH,
};
pub use cost_sum::CostSum;
pub use delay::Delay;