Skip to main content

bacnet_bip_core/node/
engine.rs

1//! [`Node`]: the mutable half of a B/IP node, which owns its management
2//! exchanges, a foreign device's registration and the broadcasts held for
3//! it, and the timers behind them.
4
5use core::net::SocketAddrV4;
6
7use bacnet_bip_codec::{BvlcResultCode, Bvll, BvllError};
8use bacnet_time::{Duration, Instant, earliest};
9
10use crate::LocalSubnet;
11
12use super::actions::{Action, Actions};
13use super::config::{ConfigError, Foreign, Link, Mode, Retry};
14use super::counters::{NodeCounters, RegisterAttempt, add, bump};
15use super::exchange::{
16    Early, ExchangeId, ExchangeRefusal, Exchanges, Key, LocalFailure, Operation, Phase,
17};
18use super::receive::{Datagram, Delivery, Discard, ForBbmd, Policy, Received, Response};
19use super::registration::{
20    Admission, AttemptOutcome, BroadcastDecision, HeldVerdict, Lifecycle, RegistrationEvent,
21    RegistrationSnapshot,
22};
23use super::send::{Outgoing, SendRefusal};
24
25/// A received datagram's fate, with responses matched to this node's own
26/// exchanges.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Inbound<'d> {
29    /// Hand an NPDU up ([`Received::Deliver`]).
30    Deliver(Delivery<'d>),
31    /// Send a NAK ([`Received::Nak`]).
32    Nak {
33        /// The request's source.
34        to: SocketAddrV4,
35        /// The request's own NAK.
36        code: BvlcResultCode,
37        /// The request was malformed.
38        malformed: bool,
39    },
40    /// The BBMD's share ([`Received::Bbmd`]).
41    Bbmd(ForBbmd<'d>),
42    /// Dropped ([`Received::Discarded`]).
43    Discarded(Discard),
44    /// The response answers `exchange`. A malformed table ACK still
45    /// completes the exchange it fits, with the codec's error, and so does
46    /// one [`Policy::accept_table`] refused, as received.
47    Matched {
48        /// The exchange.
49        exchange: ExchangeId,
50        /// The response.
51        message: Result<Bvll<'d>, BvllError>,
52        /// Whether the codec decoded it and [`Policy::accept_table`] took
53        /// it ([`Response::accepted`]). Only an accepted response counts as
54        /// an answer; the caller fails the exchange otherwise.
55        accepted: bool,
56        /// Whether the exchange ended now. When it beat
57        /// [`Node::exchange_sent`], the caller keeps the response until
58        /// that returns `true`.
59        settled: bool,
60    },
61    /// A second response for an exchange already holding one that beat its
62    /// send. The first is kept; drop this one.
63    Duplicate {
64        /// The exchange.
65        exchange: ExchangeId,
66    },
67    /// The response answers no live exchange. Well-formed ones count as
68    /// unmatched, malformed ones as malformed ([`NodeCounters`]).
69    Unmatched {
70        /// Who sent it.
71        peer: SocketAddrV4,
72        /// A well-formed BVLC-Result's code.
73        code: Option<BvlcResultCode>,
74        /// It came within one response timeout of an exchange it fits
75        /// ending: probably that exchange's answer, too late.
76        late: bool,
77    },
78}
79
80/// The mutable part of a B/IP node: up to `X` management exchanges at once
81/// (at least 1, at most 256), a foreign device's registration, the
82/// broadcasts held for it, and their timers.
83///
84/// Sans-IO: each call takes the current time, nothing reads a clock or a
85/// socket, and what the caller must do comes out of
86/// [`poll_action`](Node::poll_action), which it polls until
87/// [`Action::Idle`] after every call. Sleep until
88/// [`next_deadline`](Node::next_deadline) and call
89/// [`on_deadline`](Node::on_deadline) with the time actually woken; every
90/// other call first catches up on deadlines passed, so the order the caller
91/// makes them in at one instant doesn't change the outcome. Time never
92/// runs backwards: an earlier `now` than the last is taken as the last.
93#[derive(Debug, Clone)]
94pub struct Node<const X: usize = 4> {
95    link: Link,
96    exchanges: Exchanges<X>,
97    lifecycle: Option<Lifecycle>,
98    actions: Actions<X>,
99    counters: NodeCounters,
100    /// The newest Register-Foreign-Device attempt and its exchange.
101    latest_register: Option<(ExchangeId, RegisterAttempt)>,
102    now: Instant,
103}
104
105impl<const X: usize> Node<X> {
106    /// A node on `link`, starting at `now`. A foreign device's first
107    /// registration attempt is due at once.
108    pub fn new(link: Link, now: Instant) -> Result<Self, ConfigError> {
109        const { assert!(X >= 1 && X <= 256, "a node needs 1 to 256 exchange slots") };
110        link.validate()?;
111        let lifecycle = match &link.mode {
112            Mode::Foreign(foreign) => Some(Lifecycle::new(foreign, now)),
113            Mode::Normal | Mode::Bbmd => None,
114        };
115        let mut node = Self {
116            link,
117            exchanges: Exchanges::new(),
118            lifecycle,
119            actions: Actions::new(),
120            counters: NodeCounters::default(),
121            latest_register: None,
122            now,
123        };
124        node.advance(now);
125        Ok(node)
126    }
127
128    /// The node's link.
129    #[must_use]
130    pub const fn link(&self) -> &Link {
131        &self.link
132    }
133
134    /// [`Link::receive`], then [`resolve`](Node::resolve) a response.
135    pub fn receive<'d>(
136        &mut self,
137        datagram: &Datagram<'d>,
138        now: Instant,
139        policy: &mut impl Policy,
140    ) -> Inbound<'d> {
141        let now = self.catch_up(now, false);
142        let inbound = match self.link.receive(datagram, now, policy) {
143            Received::Deliver(delivery) => Inbound::Deliver(delivery),
144            Received::Nak {
145                to,
146                code,
147                malformed,
148            } => Inbound::Nak {
149                to,
150                code,
151                malformed,
152            },
153            Received::Bbmd(share) => Inbound::Bbmd(share),
154            Received::Discarded(reason) => Inbound::Discarded(reason),
155            Received::Response(response) => self.match_response(response, now),
156        };
157        self.advance(now);
158        inbound
159    }
160
161    /// Match a response to the live exchange it answers.
162    ///
163    /// A table ACK answers its table read; a NAK, the operation it names
164    /// ([`BvlcFunction::nak`](bacnet_bip_codec::BvlcFunction::nak)); a
165    /// success, the peer's one live operation a success answers. A
166    /// malformed BVLC-Result never completes anything. A
167    /// Distribute-Broadcast-To-Network NAK from a foreign device's own BBMD
168    /// that matches nothing says the BBMD has no registration for it (Annex
169    /// J.4.5), and with [`Retry::Backoff`] pulls the next attempt forward;
170    /// [`Retry::AtRenewal`] keeps its schedule.
171    pub fn resolve<'d>(&mut self, response: Response<'d>, now: Instant) -> Inbound<'d> {
172        // An attempt that falls due now goes after the match: it isn't sent
173        // yet, so the response can't be its answer.
174        let now = self.catch_up(now, false);
175        let inbound = self.match_response(response, now);
176        self.advance(now);
177        inbound
178    }
179
180    fn match_response<'d>(&mut self, response: Response<'d>, now: Instant) -> Inbound<'d> {
181        let Response {
182            peer,
183            function,
184            message,
185            accepted,
186        } = response;
187        let code = match message {
188            Ok(Bvll::BvlcResult(code)) => Some(code),
189            _ => None,
190        };
191        let valid = message.is_ok() && accepted;
192        let key = if valid {
193            Key::of(function, code)
194        } else {
195            bump(&mut self.counters.malformed_responses);
196            if let Some(index) = self.exchanges.first_with(peer, function) {
197                let operation = self.exchanges.slot(index).operation;
198                bump(&mut self.counters.exchange_mut(operation).malformed_responses);
199            }
200            // Only a table ACK completes an exchange malformed.
201            Operation::of_ack(function).map(Key::Operation)
202        };
203        let Some(index) = key.and_then(|key| self.exchanges.find(peer, key)) else {
204            if !valid {
205                return Inbound::Unmatched {
206                    peer,
207                    code: None,
208                    late: false,
209                };
210            }
211            return self.unmatched(peer, code, key, now);
212        };
213        let exchange = self.exchanges.id(index);
214        let slot = self.exchanges.slot_mut(index);
215        if slot.phase == Phase::Admitted {
216            if slot.early.is_some() {
217                return Inbound::Duplicate { exchange };
218            }
219            slot.early = Some(Early { code, valid });
220            return Inbound::Matched {
221                exchange,
222                message,
223                accepted: valid,
224                settled: false,
225            };
226        }
227        self.settle(index, code, valid, now);
228        Inbound::Matched {
229            exchange,
230            message,
231            accepted: valid,
232            settled: true,
233        }
234    }
235
236    /// A well-formed response that answers no live exchange.
237    fn unmatched<'d>(
238        &mut self,
239        peer: SocketAddrV4,
240        code: Option<BvlcResultCode>,
241        key: Option<Key>,
242        now: Instant,
243    ) -> Inbound<'d> {
244        let late = key.is_some_and(|key| self.exchanges.is_late(peer, key, now));
245        bump(&mut self.counters.unmatched_responses);
246        if late {
247            bump(&mut self.counters.late_responses);
248        }
249        if code.is_some() {
250            self.counters.last_unmatched_result = code;
251        }
252        if code == Some(BvlcResultCode::DISTRIBUTE_BROADCAST_TO_NETWORK_NAK)
253            && let (Mode::Foreign(foreign), Some(lifecycle)) =
254                (&self.link.mode, &mut self.lifecycle)
255            && foreign.bbmd == peer
256            && foreign.retry == Retry::Backoff
257        {
258            bump(&mut self.counters.renewal_hints);
259            lifecycle.hint(now);
260        }
261        Inbound::Unmatched { peer, code, late }
262    }
263
264    /// End the exchange in `index` with a response: `code` for a
265    /// BVLC-Result, `None` for a table ACK; `valid` when it was accepted.
266    fn settle(&mut self, index: usize, code: Option<BvlcResultCode>, valid: bool, now: Instant) {
267        let exchange = self.exchanges.id(index);
268        let slot = self.exchanges.retire(index, now);
269        if !valid {
270            return;
271        }
272        let counters = self.counters.exchange_mut(slot.operation);
273        match code {
274            Some(code) => {
275                bump(&mut counters.results);
276                counters.last_result = Some(code);
277            }
278            None => bump(&mut counters.acknowledgements),
279        }
280        let Some(code) = code else { return };
281        if slot.operation != Operation::RegisterForeignDevice {
282            return;
283        }
284        let accepted = code == BvlcResultCode::SUCCESSFUL_COMPLETION;
285        if accepted {
286            bump(&mut self.counters.registrations_accepted);
287        } else {
288            bump(&mut self.counters.registrations_refused);
289        }
290        let outcome = if accepted {
291            AttemptOutcome::Accepted
292        } else {
293            AttemptOutcome::Refused
294        };
295        self.record_register(exchange, outcome, Some(code));
296        if !slot.automatic {
297            return;
298        }
299        let Some(lifecycle) = &mut self.lifecycle else {
300            return;
301        };
302        let (event, verdict) = if accepted {
303            lifecycle.accepted(now)
304        } else {
305            let verdict = lifecycle.refused(now);
306            (Some(RegistrationEvent::Refused { code }), verdict)
307        };
308        if let Some(event) = event {
309            self.actions.event(event);
310        }
311        self.held_verdict(verdict);
312    }
313
314    /// Whether a foreign device sends, holds or refuses a broadcast at
315    /// `now`. Other modes always send.
316    ///
317    /// Until the first registration attempt is answered or times out, up to
318    /// [`Foreign::hold`](super::Foreign::hold) broadcasts are held, and one
319    /// more is refused with [`SendRefusal::HoldFull`]. Then they are all
320    /// sent; the ones sent while no lease is live are counted.
321    pub fn admit_broadcast(&mut self, now: Instant) -> Result<Admission, SendRefusal> {
322        let now = self.advance(now);
323        let Some(lifecycle) = &mut self.lifecycle else {
324            return Ok(Admission::Send);
325        };
326        match lifecycle.admit_broadcast(now) {
327            BroadcastDecision::Hold => {
328                bump(&mut self.counters.broadcasts_held);
329                Ok(Admission::Hold)
330            }
331            BroadcastDecision::Send { lapsed } => {
332                if lapsed {
333                    bump(&mut self.counters.broadcasts_unregistered);
334                }
335                Ok(Admission::Send)
336            }
337            BroadcastDecision::Full => {
338                bump(&mut self.counters.broadcasts_hold_full);
339                Err(SendRefusal::HoldFull)
340            }
341        }
342    }
343
344    /// Admit a manual management exchange: `request` to `peer`, answered
345    /// within `timeout` of its send. Encode and send the returned message,
346    /// then report it with [`exchange_sent`](Node::exchange_sent) or
347    /// [`exchange_failed`](Node::exchange_failed).
348    ///
349    /// A foreign device refuses a request a success answers to its own
350    /// BBMD ([`ExchangeRefusal::ManagedRegistration`]): the node owns that
351    /// registration. A normal node or a BBMD may send any.
352    pub fn begin_exchange<'r>(
353        &mut self,
354        peer: SocketAddrV4,
355        request: Bvll<'r>,
356        timeout: Duration,
357        now: Instant,
358    ) -> Result<(ExchangeId, Outgoing<'r>), ExchangeRefusal> {
359        self.advance(now);
360        let operation = Operation::of(request.function()).ok_or(ExchangeRefusal::NotARequest)?;
361        if let Mode::Foreign(foreign) = &self.link.mode
362            && foreign.bbmd == peer
363            && operation.answered_by_success()
364        {
365            return Err(ExchangeRefusal::ManagedRegistration);
366        }
367        let ttl = match request {
368            Bvll::RegisterForeignDevice { ttl } => ttl,
369            _ => 0,
370        };
371        let exchange = self.admit(peer, operation, timeout, false, ttl)?;
372        Ok((
373            exchange,
374            Outgoing {
375                to: peer,
376                message: request,
377            },
378        ))
379    }
380
381    fn admit(
382        &mut self,
383        peer: SocketAddrV4,
384        operation: Operation,
385        timeout: Duration,
386        automatic: bool,
387        ttl: u16,
388    ) -> Result<ExchangeId, ExchangeRefusal> {
389        let exchange = self
390            .exchanges
391            .admit(peer, operation, timeout, automatic, ttl)
392            .inspect_err(|_| bump(&mut self.counters.exchange_mut(operation).busy))?;
393        if operation == Operation::RegisterForeignDevice {
394            bump(&mut self.counters.registration_attempts);
395            let attempt = RegisterAttempt {
396                bbmd: peer,
397                ttl,
398                outcome: AttemptOutcome::Pending,
399                result: None,
400            };
401            self.latest_register = Some((exchange, attempt));
402            self.counters.last_registration_attempt = Some(attempt);
403        }
404        Ok(exchange)
405    }
406
407    /// The request of `exchange` was sent at `now`; its response timeout
408    /// starts. `true` when a response that beat the send settles it now.
409    /// A stale or already reported exchange changes nothing.
410    pub fn exchange_sent(&mut self, exchange: ExchangeId, now: Instant) -> bool {
411        let now = self.advance(now);
412        let Some(index) = self.exchanges.live(exchange) else {
413            return false;
414        };
415        let slot = self.exchanges.slot_mut(index);
416        if slot.phase != Phase::Admitted {
417            return false;
418        }
419        slot.phase = Phase::Sent {
420            deadline: now + slot.timeout,
421        };
422        let (operation, automatic, early) = (slot.operation, slot.automatic, slot.early.take());
423        bump(&mut self.counters.exchange_mut(operation).sent);
424        if automatic && let Some(lifecycle) = &mut self.lifecycle {
425            lifecycle.sent();
426        }
427        let settled = early.is_some();
428        if let Some(early) = early {
429            self.settle(index, early.code, early.valid, now);
430        }
431        self.advance(now);
432        settled
433    }
434
435    /// The caller ended `exchange` itself: its send failed, or it gave up.
436    /// A stale exchange changes nothing.
437    pub fn exchange_failed(&mut self, exchange: ExchangeId, why: LocalFailure, now: Instant) {
438        let now = self.advance(now);
439        let Some(index) = self.exchanges.live(exchange) else {
440            return;
441        };
442        let slot = self.exchanges.retire(index, now);
443        let outcome = match why {
444            LocalFailure::SendFailed => {
445                bump(&mut self.counters.exchange_mut(slot.operation).local_errors);
446                AttemptOutcome::LocalError
447            }
448            LocalFailure::Cancelled => AttemptOutcome::Cancelled,
449        };
450        self.ended_unanswered(exchange, slot.operation, slot.automatic, outcome, now);
451        self.advance(now);
452    }
453
454    /// An exchange ended without an answer.
455    fn ended_unanswered(
456        &mut self,
457        exchange: ExchangeId,
458        operation: Operation,
459        automatic: bool,
460        outcome: AttemptOutcome,
461        now: Instant,
462    ) {
463        if operation == Operation::RegisterForeignDevice {
464            self.record_register(exchange, outcome, None);
465        }
466        if !automatic {
467            return;
468        }
469        if let Some(lifecycle) = &mut self.lifecycle {
470            let verdict = lifecycle.failed(now, Some(outcome));
471            self.actions.event(RegistrationEvent::Unanswered);
472            self.held_verdict(verdict);
473        }
474    }
475
476    /// Update the newest Register-Foreign-Device attempt, if `exchange` is
477    /// it.
478    fn record_register(
479        &mut self,
480        exchange: ExchangeId,
481        outcome: AttemptOutcome,
482        result: Option<BvlcResultCode>,
483    ) {
484        if let Some((latest, attempt)) = &mut self.latest_register
485            && *latest == exchange
486        {
487            attempt.outcome = outcome;
488            attempt.result = result;
489            self.counters.last_registration_attempt = Some(*attempt);
490        }
491    }
492
493    fn held_verdict(&mut self, verdict: Option<HeldVerdict>) {
494        let Some(HeldVerdict { release, count }) = verdict else {
495            return;
496        };
497        let counter = if release {
498            &mut self.counters.broadcasts_released
499        } else {
500            &mut self.counters.broadcasts_discarded
501        };
502        add(counter, count);
503        self.actions.held(release);
504    }
505
506    /// The earliest time the node has something to do without being asked:
507    /// an exchange's response timeout, a registration attempt, or a lease's
508    /// end.
509    #[must_use]
510    pub fn next_deadline(&self) -> Option<Instant> {
511        let exchange = self.exchanges.next_deadline().map(|(_, deadline)| deadline);
512        let lifecycle = self.lifecycle.as_ref();
513        earliest([
514            exchange,
515            lifecycle.and_then(Lifecycle::lapse_at),
516            lifecycle.and_then(Lifecycle::due_at),
517        ])
518    }
519
520    /// Do what is due at `now`, the time the caller actually woke.
521    pub fn on_deadline(&mut self, now: Instant) {
522        self.advance(now);
523    }
524
525    /// Catch up to `now`: handle every deadline at or before it, earliest
526    /// first; at one instant, timeouts, then the lease's end, then a due
527    /// attempt. Returns the time used, never earlier than the last.
528    fn advance(&mut self, now: Instant) -> Instant {
529        self.catch_up(now, true)
530    }
531
532    /// [`advance`](Self::advance), leaving a due attempt unstarted unless
533    /// `start_due`.
534    fn catch_up(&mut self, now: Instant, start_due: bool) -> Instant {
535        let now = now.max(self.now);
536        self.now = now;
537        loop {
538            let timeout = self.exchanges.next_deadline();
539            let lifecycle = self.lifecycle.as_ref();
540            let lapse = lifecycle.and_then(Lifecycle::lapse_at);
541            let due = lifecycle.and_then(Lifecycle::due_at).filter(|_| start_due);
542            let first = earliest([timeout.map(|(_, at)| at), lapse, due]);
543            match first {
544                Some(at) if at <= now => {}
545                _ => return now,
546            }
547            if let Some((index, _)) = timeout.filter(|(_, at)| Some(*at) == first) {
548                self.time_out(index, now);
549            } else if lapse == first {
550                if let Some(lifecycle) = &mut self.lifecycle {
551                    lifecycle.lapse();
552                }
553                bump(&mut self.counters.lapses);
554                self.actions.event(RegistrationEvent::Lapsed);
555            } else {
556                self.start_attempt(now);
557            }
558        }
559    }
560
561    fn time_out(&mut self, index: usize, now: Instant) {
562        let exchange = self.exchanges.id(index);
563        let slot = self.exchanges.retire(index, now);
564        bump(&mut self.counters.exchange_mut(slot.operation).timeouts);
565        if !slot.automatic {
566            self.actions.ended(exchange);
567        }
568        self.ended_unanswered(
569            exchange,
570            slot.operation,
571            slot.automatic,
572            AttemptOutcome::TimedOut,
573            now,
574        );
575    }
576
577    /// Admit the automatic registration attempt that is due.
578    fn start_attempt(&mut self, now: Instant) {
579        let Mode::Foreign(foreign) = self.link.mode else {
580            return;
581        };
582        let timeout = foreign.effective_response_timeout();
583        let operation = Operation::RegisterForeignDevice;
584        match self.admit(foreign.bbmd, operation, timeout, true, foreign.ttl) {
585            Ok(exchange) => {
586                if let Some(lifecycle) = &mut self.lifecycle {
587                    lifecycle.admitted(now);
588                }
589                self.actions.register(exchange, foreign.bbmd, foreign.ttl);
590            }
591            Err(_) => {
592                if let Some(lifecycle) = &mut self.lifecycle {
593                    let verdict = lifecycle.failed(now, None);
594                    self.held_verdict(verdict);
595                }
596            }
597        }
598    }
599
600    /// Register a foreign device with `foreign` from `now` on: another
601    /// BBMD, time-to-live or timing. The lifecycle restarts with an attempt
602    /// to the new BBMD due at once, which the next
603    /// [`poll_action`](Node::poll_action) hands out. The automatic attempt
604    /// in flight ends as cancelled, so a late answer to it counts as
605    /// unmatched. A lease proven by the same BBMD stands, since its entry
606    /// does; one from another BBMD is dropped, and broadcasts are held
607    /// again, as at start, until that BBMD's first answer
608    /// ([`admit_broadcast`](Node::admit_broadcast)). Manual exchanges carry
609    /// on.
610    ///
611    /// Refused, changing nothing, outside foreign mode
612    /// ([`ConfigError::NotForeign`]) or with a registration
613    /// [`Link::validate`] refuses.
614    pub fn retarget(&mut self, foreign: Foreign, now: Instant) -> Result<(), ConfigError> {
615        let Mode::Foreign(old) = self.link.mode else {
616            return Err(ConfigError::NotForeign);
617        };
618        let mut link = self.link;
619        link.mode = Mode::Foreign(foreign);
620        link.validate()?;
621        // Timeouts and a lapse due by now happen on the old registration; an
622        // attempt due by now is not started for it.
623        let now = self.catch_up(now, false);
624        if let Some(index) = self.exchanges.automatic() {
625            let exchange = self.exchanges.id(index);
626            self.exchanges.retire(index, now);
627            self.actions.forget_register(exchange);
628            self.record_register(exchange, AttemptOutcome::Cancelled, None);
629        }
630        if let Some(lifecycle) = &mut self.lifecycle {
631            lifecycle.retarget(&foreign, now, old.bbmd == foreign.bbmd);
632        }
633        self.link = link;
634        self.advance(now);
635        Ok(())
636    }
637
638    /// Move the node to another [`LocalSubnet`]: where its broadcasts go
639    /// and which destination counts as one. A B/IP-M node that joins,
640    /// changes or leaves its multicast group (Annex J.8) does this, and so
641    /// must every copy of the link the host decides on. Nothing else
642    /// changes: exchanges and a foreign device's registration carry on.
643    ///
644    /// Refused, changing nothing, with a link [`Link::validate`] refuses.
645    pub fn set_subnet(&mut self, subnet: LocalSubnet) -> Result<(), ConfigError> {
646        let mut link = self.link;
647        link.subnet = subnet;
648        link.validate()?;
649        self.link = link;
650        Ok(())
651    }
652
653    /// The next thing to do, or [`Action::Idle`].
654    pub fn poll_action(&mut self) -> Action {
655        self.actions.poll()
656    }
657
658    /// A foreign device's registration as of `now`; `None` in the other
659    /// modes.
660    #[must_use]
661    pub fn registration(&self, now: Instant) -> Option<RegistrationSnapshot> {
662        self.lifecycle
663            .as_ref()
664            .map(|lifecycle| lifecycle.snapshot(now.max(self.now)))
665    }
666
667    /// The counts since the counters were last taken, which then start
668    /// again from zero: add them to the host's own totals, as wide as it
669    /// needs. Each count is a `u32` that saturates between takes.
670    #[must_use = "the counts are gone once taken; add them to your totals"]
671    pub fn take_counters(&mut self) -> NodeCounters {
672        core::mem::take(&mut self.counters)
673    }
674
675    /// The counts since the counters were last taken, as
676    /// [`take_counters`](Self::take_counters) would return them, without
677    /// taking them: for a look between takes. A node whose counters are
678    /// never taken counts from its start.
679    #[must_use]
680    pub const fn counters(&self) -> NodeCounters {
681        self.counters
682    }
683}