use std::sync::atomic::Ordering;
use std::time::Instant;
use tracing::{debug, warn};
use super::{Cell, Fabric, MAX_HOPS, Outcome, Pathway};
use crate::batching::engine;
use crate::nerve::latency::LatencyEstimate;
use crate::nerve::neuron::{Cx, Firing, NeuronKind, Quiet};
use crate::nerve::signal::{NeuronId, PathwayId, Signal};
impl Fabric {
pub fn stimulate(&self, receptor: NeuronId, signal: Signal) -> Outcome {
self.stimulate_announced(receptor, signal, None)
}
pub(crate) fn announce(&self, pathway: PathwayId) -> Option<Announced<'_>> {
let queue = self.0.pathways.get(pathway.index())?.queue.as_ref()?;
Some(Announced {
pathway,
arrival: queue.arrival(),
})
}
pub(crate) fn stimulate_announced(
&self,
receptor: NeuronId,
mut signal: Signal,
announced: Option<Announced<'_>>,
) -> Outcome {
signal.origin = receptor;
self.0.received.fetch_add(1, Ordering::Relaxed);
let mut announced = announced;
self.drive(receptor, signal, 0, &mut announced, Instant::now())
}
pub fn stimulate_named(&self, receptor: &str, signal: Signal) -> Option<Outcome> {
Some(self.stimulate(self.neuron(receptor)?, signal))
}
fn drive(
&self,
id: NeuronId,
mut signal: Signal,
depth: usize,
announced: &mut Option<Announced<'_>>,
now: Instant,
) -> Outcome {
let cell = &self.0.cells[id.index()];
let nanos = self.nanos(now);
cell.counters.received.fetch_add(1, Ordering::Relaxed);
if let Err(reason) = cell.gate.admit(&mut signal, nanos) {
cell.counters.quiet(reason);
return Outcome {
quiet: Some((id, reason)),
hops: depth,
..Outcome::default()
};
}
signal.trace.push(id);
let mut acted = 0;
match cell.kind {
NeuronKind::Brain => {
signal.trace.mark_brain();
self.0.reached_brain.fetch_add(1, Ordering::Relaxed);
}
NeuronKind::Effector => {
acted = 1;
if !signal.trace.was_queued()
&& let Some(arc) = cell.arcs.get(signal.origin.index())
{
arc.get_or_init(LatencyEstimate::new)
.observe(now.saturating_duration_since(signal.at));
}
}
NeuronKind::Receptor | NeuronKind::Reflex => {}
}
let cx = Cx {
now,
id,
fabric: self,
};
let firing = cell.neuron.stimulate(signal, &cx);
if matches!(firing, Firing::Quiet) {
cell.gate.stayed_quiet(nanos);
}
let mut outcome = self.after_firing(id, firing, depth, announced);
outcome.acted += acted;
outcome
}
pub(super) fn after_firing(
&self,
id: NeuronId,
firing: Firing,
depth: usize,
announced: &mut Option<Announced<'_>>,
) -> Outcome {
let cell = &self.0.cells[id.index()];
let emitted = match firing {
Firing::Quiet => {
cell.counters.quiet(Quiet::NotInterested);
return Outcome {
quiet: Some((id, Quiet::NotInterested)),
hops: depth + 1,
..Outcome::default()
};
}
Firing::Fire(signals) => signals,
Firing::Compute(work) => {
cell.counters.computed.fetch_add(1, Ordering::Relaxed);
work()
}
};
cell.counters.fired.fetch_add(1, Ordering::Relaxed);
let mut outcome = Outcome {
hops: depth + 1,
..Outcome::default()
};
for signal in emitted {
outcome.merge(self.route(id, signal, depth + 1, announced));
}
outcome
}
fn route(
&self,
from: NeuronId,
mut signal: Signal,
depth: usize,
announced: &mut Option<Announced<'_>>,
) -> Outcome {
if depth >= MAX_HOPS || signal.trace.len() >= MAX_HOPS {
self.0.loops.fetch_add(1, Ordering::Relaxed);
warn!(
"nerve: signal {} dropped after {MAX_HOPS} hops (a loop?)",
signal.id.0
);
return Outcome::default();
}
let now = Instant::now();
let cell = &self.0.cells[from.index()];
let selected = match signal.payload.as_command() {
Some(command) => match self.0.by_name.get(&command.effector) {
Some(&target) if self.0.cells[target.index()].kind == NeuronKind::Effector => {
match cell
.out
.iter()
.map(|&p| &self.0.pathways[p.index()])
.find(|p| p.to == target)
{
Some(pathway) => Some(pathway),
None => {
self.0.commands_direct.fetch_add(1, Ordering::Relaxed);
return self.drive(target, signal, depth, announced, now);
}
}
}
_ => {
self.0.unrouted.fetch_add(1, Ordering::Relaxed);
debug!(
"nerve: no effector named '{}' for a command from {}",
command.effector,
cell.neuron.name()
);
return Outcome {
unrouted: 1,
..Outcome::default()
};
}
},
None => self.select(cell, &signal, now),
};
let Some(pathway) = selected else {
if cell.out.is_empty() {
return Outcome::default();
}
self.0.no_eligible.fetch_add(1, Ordering::Relaxed);
return Outcome {
no_pathway: 1,
..Outcome::default()
};
};
pathway.signals.fetch_add(1, Ordering::Relaxed);
match &pathway.queue {
None => {
let outcome = self.drive(pathway.to, signal, depth, announced, now);
pathway.latency.observe(now.elapsed());
outcome
}
Some(queue) => {
self.prepare_for(pathway, &signal, now);
signal.trace.mark_queued();
let arrival = match announced.take() {
Some(a) if a.pathway == pathway.id => Some(a.arrival),
other => {
*announced = other;
None
}
};
match queue.submit(arrival, signal) {
Ok(()) => Outcome {
queued: 1,
..Outcome::default()
},
Err(_) => {
pathway.overflow.fetch_add(1, Ordering::Relaxed);
Outcome {
dropped: 1,
..Outcome::default()
}
}
}
}
}
}
fn select(&self, cell: &Cell, signal: &Signal, now: Instant) -> Option<&Pathway> {
let remaining = signal.urgency.remaining(now);
let mut best: Option<&Pathway> = None;
for &id in &cell.out {
let pathway = &self.0.pathways[id.index()];
if !pathway.fits(remaining) {
pathway.ineligible.fetch_add(1, Ordering::Relaxed);
continue;
}
if best.is_none_or(|b| pathway.weight > b.weight) {
best = Some(pathway);
}
}
best
}
fn prepare_for(&self, pathway: &Pathway, signal: &Signal, now: Instant) {
let far = match (signal.urgency.remaining(now), pathway.latency.p99()) {
(None, _) | (_, None) => true,
(Some(remaining), Some(p99)) => remaining > 2 * p99,
};
if !far {
return;
}
let nanos = self.nanos(now);
let to = &self.0.cells[pathway.to.index()];
let downstream = to
.out
.iter()
.map(|&p| self.0.pathways[p.index()].to)
.chain(std::iter::once(pathway.to));
for id in downstream {
let cell = &self.0.cells[id.index()];
let last = cell.last_prepared.load(Ordering::Relaxed);
if nanos.saturating_sub(last) < 1_000_000_000 && last != 0 {
continue;
}
cell.last_prepared.store(nanos.max(1), Ordering::Relaxed);
let cx = Cx {
now,
id,
fabric: self,
};
cell.neuron.prepare(&cx);
}
}
}
pub(crate) struct Announced<'a> {
pathway: PathwayId,
arrival: engine::Arrival<'a, Signal>,
}