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:
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user