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
+11 -12
View File
@@ -18,8 +18,9 @@
use aura_core::{PrimitiveBuilder, Scalar}; use aura_core::{PrimitiveBuilder, Scalar};
use aura_engine::{Composite, GraphBuilder, NodeHandle}; use aura_engine::{Composite, GraphBuilder, NodeHandle};
use aura_std::{ use aura_std::{
CostSum, Delay, Ema, FixedStop, LinComb, Mul, PositionManagement, Sizer, Sqrt, Sub, cost_port, intern_port, CostSum, Delay, Ema, FixedStop, LinComb, Mul,
COST_FIELD_NAMES, GEOMETRY_WIDTH, PM_FIELD_NAMES, PositionManagement, Sizer, Sqrt, Sub, COST_FIELD_NAMES, GEOMETRY_WIDTH,
PM_FIELD_NAMES,
}; };
/// The volatility stop as a composition of primitives: /// The volatility stop as a composition of primitives:
@@ -149,11 +150,11 @@ pub fn risk_executor_vol_open(risk_budget: f64) -> Composite {
/// schema is geometry-prefix-first then the factor's extras (slot `GEOMETRY_WIDTH` /// schema is geometry-prefix-first then the factor's extras (slot `GEOMETRY_WIDTH`
/// onward); `cost_graph` reads `schema().inputs[GEOMETRY_WIDTH..]` to discover the /// onward); `cost_graph` reads `schema().inputs[GEOMETRY_WIDTH..]` to discover the
/// extras and names them `cost[k].<port>` — mirroring `CostSum`'s own `cost[k].*` /// extras and names them `cost[k].<port>` — mirroring `CostSum`'s own `cost[k].*`
/// input vocabulary. Runtime-computed port names are `.leak()`ed to `&'static str` /// input vocabulary. Runtime-computed port names are interned (`cost_port` /
/// to satisfy the `NodeHandle::input`/`output` bound — fine while cost is run-path /// `intern_port`, the `COL_PORTS` production pattern — #152): one allocation
/// only (the graph is built once, a one-shot leak), but to be interned (the /// per DISTINCT name for the process lifetime, so rebuilding the graph per
/// `COL_PORTS` production pattern) before cost reaches the per-member sweep path, /// member across a sweep reuses the same strings instead of leaking fresh
/// where a per-build leak would accumulate. See #152. /// ones per build.
pub fn cost_graph(cost_nodes: Vec<PrimitiveBuilder>) -> Composite { pub fn cost_graph(cost_nodes: Vec<PrimitiveBuilder>) -> Composite {
assert!(!cost_nodes.is_empty(), "cost_graph needs at least one cost node"); assert!(!cost_nodes.is_empty(), "cost_graph needs at least one cost node");
let n = cost_nodes.len(); let n = cost_nodes.len();
@@ -183,14 +184,12 @@ pub fn cost_graph(cost_nodes: Vec<PrimitiveBuilder>) -> Composite {
stop_t.push(h.input("stop_price")); stop_t.push(h.input("stop_price"));
// Each extra input becomes a `cost[k].<port>` composite role. // Each extra input becomes a `cost[k].<port>` composite role.
for name in &extra_names { for name in &extra_names {
let role = g.input_role(&format!("cost[{k}].{name}")); let role = g.input_role(cost_port(k, name));
let port: &'static str = name.clone().leak(); g.feed(role, [h.input(intern_port(name))]);
g.feed(role, [h.input(port)]);
} }
// The node's 3 cost fields -> CostSum's `cost[k].<field>` inputs. // The node's 3 cost fields -> CostSum's `cost[k].<field>` inputs.
for field in COST_FIELD_NAMES { for field in COST_FIELD_NAMES {
let agg_in: &'static str = format!("cost[{k}].{field}").leak(); g.connect(h.output(field), agg.input(cost_port(k, field)));
g.connect(h.output(field), agg.input(agg_in));
} }
} }
g.feed(closed, closed_t); g.feed(closed, closed_t);
+54
View File
@@ -15,6 +15,8 @@ use aura_core::{
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
ScalarKind, 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 /// 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. /// 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`. /// these, beginning at slot `GEOMETRY_WIDTH`.
pub const GEOMETRY_WIDTH: usize = 4; 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> { fn geometry_input_ports() -> Vec<PortSpec> {
vec![ vec![
PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "closed".into() }, PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "closed".into() },
@@ -474,6 +505,29 @@ mod tests {
assert_eq!(schema.params, params); 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] #[test]
fn producer_and_aggregator_share_the_triple() { fn producer_and_aggregator_share_the_triple() {
// The structural lockstep: producer output and aggregator output/inputs all // 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 /// 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 /// 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. /// 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 /// 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 /// `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 { inputs.push(PortSpec {
kind: ScalarKind::F64, kind: ScalarKind::F64,
firing: Firing::Any, 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 const_node::Const;
pub use constant_cost::ConstantCost; pub use constant_cost::ConstantCost;
pub use cost::{ pub use cost::{
cost_node_builder, ChargeMode, CostNode, CostRunner, COST_FIELD_NAMES, COST_WIDTH, cost_node_builder, cost_port, intern_port, ChargeMode, CostNode, CostRunner,
GEOMETRY_WIDTH, COST_FIELD_NAMES, COST_WIDTH, GEOMETRY_WIDTH,
}; };
pub use cost_sum::CostSum; pub use cost_sum::CostSum;
pub use delay::Delay; pub use delay::Delay;