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}