Skip to main content

nros_node/
config.rs

1//! Build-time configurable constants.
2//!
3//! Values are set via environment variables at build time.
4//! See build.rs for env var names and defaults.
5
6include!(concat!(env!("OUT_DIR"), "/nros_node_config.rs"));
7
8/// Issue 1970 -- one executor tier's own sizing, as `build.rs` derived it from
9/// the sizing descriptor's `[[tier]]` row: the callback slots, node slots and
10/// arena its members need, and the parameters they declare (`None`: the tier
11/// stated no count, so it carves the image's store). Read through
12/// [`TIER_SIZINGS`]; `crate::executor::tier_sizing` turns a row into the
13/// `ExecutorSizing` its executor is carved at.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct TierSizing {
16    /// The tier's name, as the entry's tier table spells it.
17    pub name: &'static str,
18    pub cbs: usize,
19    pub arena: usize,
20    pub nodes: usize,
21    pub params: Option<usize>,
22}
23
24/// phase-271 — default arena bytes for a per-entry executor holding `cbs`
25/// callback slots. Scales the build-time [`ARENA_SIZE`] (which is sized for the
26/// default [`MAX_CBS`]) linearly by `cbs`, so a per-entry [`ExecutorSizing`] with
27/// a caller-chosen `cbs` gets the same per-slot arena budget the global default
28/// used — a fat entry (more callbacks) gets a larger arena, a lean one a smaller
29/// one — without a workspace-global `NROS_EXECUTOR_ARENA_SIZE`. Floored at the
30/// full default so no per-entry executor is ever smaller than a single-slot
31/// build would have been (the arena is a worst-case upper bound; over-provision
32/// is safe, under-provision fails entity creation).
33///
34/// # The ratio is for an UNDECLARED image only (issue 1290)
35///
36/// The scaling above reads `ARENA_SIZE` as a budget for `MAX_CBS` slots, so
37/// `ARENA_SIZE / MAX_CBS` is one slot's worth and `cbs` of them is `cbs` slots'
38/// worth. That is exactly what `ARENA_SIZE` is while nothing declared: the
39/// derivation budgets `max_cbs` worst-case entries plus a base overhead.
40///
41/// It stopped being that in phase-403 step 3 for an image that DECLARES its
42/// entities. There `nros-node/build.rs` sums the model per KIND — subscriptions
43/// at their own depth and type, timers at 64 bytes, services, action clients —
44/// and `MAX_CBS` is a separate knob that takes no part. Dividing by it then
45/// divides by a number with no relationship to the sum, and multiplying by
46/// `cbs` scales a total that was never per-slot. Measured on
47/// `packages/testing/nros-bench/large-msg-baremetal` with one declared
48/// subscription: the model is 14,424 bytes and `arena_size_for(2)` returned
49/// 7,212. Half.
50///
51/// So when the image declares, this asks the model: `ARENA_SIZE` IS the model
52/// (unfloored since issue 1971, and `executor::arena_oracle` refuses at
53/// compile time to let a stated knob sit below it), and every per-entry executor gets all of it.
54///
55/// **That over-provisions a TIERED boot**, where one tier holds a subset of the
56/// image and now carries the whole model. Deliberate, and the direction is the
57/// safe one: the alternative needs the ENTRY's own per-kind counts where the
58/// macro emits the sizing, and those do not reach it today (issue 1290 records
59/// the choice). An entry that wants less states its own
60/// [`ExecutorSizing`] and holds it to what it registers with
61/// [`ExecutorSizing::assert_covers_model`], which is a compile-time check the
62/// ratio never had.
63///
64/// [`ExecutorSizing`]: crate::executor::ExecutorSizing
65/// [`ExecutorSizing::assert_covers_model`]: crate::executor::ExecutorSizing::assert_covers_model
66pub const fn arena_size_for(cbs: usize) -> usize {
67    // `!= 0` and not `> 0`: on a build that declares nothing the generated
68    // const IS the literal `0`, and clippy's `absurd_extreme_comparisons`
69    // const-propagates it and refuses `> 0` as always-false under
70    // `-D warnings`. Same predicate, and it reads the same way -- `0` means
71    // "no model", never "a model of nothing".
72    if arena_model::REQUIRED != 0 {
73        return ARENA_SIZE;
74    }
75    let scaled = (cbs * ARENA_SIZE).div_ceil(if MAX_CBS == 0 { 1 } else { MAX_CBS });
76    // Never below one default slot's worth so a tiny `cbs` still has headroom for
77    // the base overhead the derivation folds into `ARENA_SIZE`.
78    let floor = ARENA_SIZE.div_ceil(if MAX_CBS == 0 { 1 } else { MAX_CBS });
79    if scaled < floor { floor } else { scaled }
80}
81
82#[cfg(test)]
83mod arena_size_for_tests {
84    use super::{ARENA_SIZE, MAX_CBS, arena_model, arena_size_for};
85
86    /// Every number in this module goes through here, and that is not style.
87    ///
88    /// The build script emits `arena_model::REQUIRED` as a LITERAL, and on a
89    /// build that declares nothing the literal is `0` — the minimum of
90    /// `usize`. clippy then const-folds every comparison against it and
91    /// `absurd_extreme_comparisons` / `assertions_on_constants` refuse the
92    /// whole module under `-D warnings`, which is what `check-workspace-all`
93    /// runs. `black_box` hides only the CONSTANT-ness; the value, and every
94    /// assertion's meaning, is unchanged. Deleting it does not weaken a test,
95    /// it stops the crate compiling.
96    fn opaque(v: usize) -> usize {
97        core::hint::black_box(v)
98    }
99
100    /// issue 1290 — a per-entry arena must cover the model the image declares.
101    ///
102    /// This is the property `arena_size_for` violated: it scaled `ARENA_SIZE`
103    /// by `cbs / MAX_CBS` on an image where `ARENA_SIZE` is a per-KIND sum and
104    /// `MAX_CBS` takes no part in it, so the image's only executor got a
105    /// fraction of its own requirement.
106    ///
107    /// It is only a claim about a build that DECLARED — `REQUIRED` is 0
108    /// otherwise, and then the ratio is the right arithmetic over the right
109    /// number. The `node-std-tests` lane runs this test a second time with the
110    /// reference island's declared shape in the environment, which is what
111    /// makes the first branch reachable at all; the `assert!` on `REQUIRED`
112    /// there is what stops that run passing vacuously.
113    #[test]
114    fn a_per_entry_arena_covers_the_declared_model() {
115        if opaque(arena_model::REQUIRED) == 0 {
116            // Nothing declared: `ARENA_SIZE` is a budget over `MAX_CBS` slots,
117            // so the ratio is meaningful and a full-width entry gets all of it.
118            assert_eq!(opaque(arena_size_for(MAX_CBS)), opaque(ARENA_SIZE));
119            return;
120        }
121        for cbs in 0..=MAX_CBS.max(1) * 2 {
122            assert!(
123                opaque(arena_size_for(cbs)) >= opaque(arena_model::REQUIRED),
124                "arena_size_for({cbs}) = {} is below the {} bytes this image's \
125                 declared entities are modelled at — every registration past it \
126                 fails with BufferTooSmall (issue 1290)",
127                arena_size_for(cbs),
128                arena_model::REQUIRED,
129            );
130        }
131    }
132
133    /// The POSITIVE CONTROL, and the reason the test above is not free.
134    ///
135    /// `#[ignore]`d because it can only say anything in a build that DECLARED,
136    /// and a `cargo test` of this crate declares nothing. `node-std-tests` runs
137    /// it with the reference island's shape in the environment; the first
138    /// assertion is what stops that run passing vacuously if the declaration
139    /// ever stops arriving.
140    ///
141    /// The arithmetic this replaced is RESTATED here — the one place in the
142    /// tree it may be, because what is being asserted is precisely that it was
143    /// WRONG. Without it a reader cannot tell whether the property above holds
144    /// because the fix works or because the numbers never collided.
145    #[test]
146    #[ignore = "needs NROS_ENTITY_COUNT_* in the build environment; run by `just check node-std-tests`"]
147    fn the_scaling_this_replaced_was_short_of_the_declared_model() {
148        assert!(
149            opaque(arena_model::REQUIRED) > opaque(arena_model::FLOOR),
150            "this build declared no entities (REQUIRED = {}, FLOOR = {}), so \
151             nothing below is a claim about anything. Set NROS_ENTITY_COUNT_* \
152             for the build, as `just check node-std-tests` does (issue 1290).",
153            arena_model::REQUIRED,
154            arena_model::FLOOR,
155        );
156        // Above the floor and with no knob stated, the derived arena IS the
157        // model — which is what makes the comparison below arithmetic rather
158        // than luck.
159        assert_eq!(opaque(ARENA_SIZE), opaque(arena_model::REQUIRED));
160        assert!(
161            opaque(MAX_CBS) >= 2,
162            "the ratio is only a ratio for MAX_CBS >= 2"
163        );
164
165        let divisor = MAX_CBS;
166        let scaled = |cbs: usize| {
167            let s = (cbs * ARENA_SIZE).div_ceil(divisor);
168            let floor = ARENA_SIZE.div_ceil(divisor);
169            if s < floor { floor } else { s }
170        };
171        // One slot short of the full width is where it bit: the bench sizes its
172        // ONE executor with `arena_size_for(2)` against a `MAX_CBS` of 4.
173        let cbs = MAX_CBS - 1;
174        assert!(
175            opaque(scaled(cbs)) < opaque(arena_model::REQUIRED),
176            "the cbs/MAX_CBS scaling gave {} for cbs={cbs} against a declared \
177             model of {} — it was not short here, so this test is asserting \
178             nothing",
179            scaled(cbs),
180            arena_model::REQUIRED,
181        );
182        assert!(
183            opaque(arena_size_for(cbs)) >= opaque(arena_model::REQUIRED),
184            "arena_size_for({cbs}) = {} still does not cover the declared model \
185             of {} (issue 1290)",
186            arena_size_for(cbs),
187            arena_model::REQUIRED,
188        );
189    }
190}