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