mecha10_behavior_runtime/
execution.rs1use crate::{BoxedBehavior, NodeStatus};
6use mecha10_core::Context;
7use std::time::{Duration, Instant};
8use tracing::{debug, info, warn};
9
10#[derive(Clone)]
11pub struct ExecutionContext {
12 ctx: Context,
13
14 tick_rate_hz: f32,
15}
16
17impl ExecutionContext {
18 pub fn new(ctx: Context, tick_rate_hz: f32) -> Self {
20 Self { ctx, tick_rate_hz }
21 }
22
23 pub fn context(&self) -> &Context {
25 &self.ctx
26 }
27
28 pub fn tick_rate_hz(&self) -> f32 {
30 self.tick_rate_hz
31 }
32}
33
34#[derive(Debug, Clone)]
35pub struct ExecutionStats {
36 pub tick_count: usize,
37
38 pub total_duration: Duration,
39
40 pub avg_tick_duration: Duration,
41
42 pub min_tick_duration: Duration,
43
44 pub max_tick_duration: Duration,
45
46 pub final_status: Option<NodeStatus>,
47}
48
49impl ExecutionStats {
50 fn new() -> Self {
51 Self {
52 tick_count: 0,
53 total_duration: Duration::ZERO,
54 avg_tick_duration: Duration::ZERO,
55 min_tick_duration: Duration::MAX,
56 max_tick_duration: Duration::ZERO,
57 final_status: None,
58 }
59 }
60
61 fn update(&mut self, tick_duration: Duration) {
62 self.tick_count += 1;
63 self.total_duration += tick_duration;
64
65 if tick_duration < self.min_tick_duration {
66 self.min_tick_duration = tick_duration;
67 }
68 if tick_duration > self.max_tick_duration {
69 self.max_tick_duration = tick_duration;
70 }
71
72 self.avg_tick_duration = self.total_duration / self.tick_count as u32;
73 }
74
75 fn finalize(&mut self, status: NodeStatus) {
76 self.final_status = Some(status);
77 }
78}
79
80pub struct BehaviorExecutor {
81 behavior: BoxedBehavior,
82 tick_rate_hz: f32,
83 max_ticks: Option<usize>,
84}
85
86macro_rules! lifecycle_step {
87 ($(#[$meta:meta])* $fn_name:ident, $verb:literal, $behavior_method:ident) => {
88 $(#[$meta])*
89 pub async fn $fn_name(&mut self, ctx: &Context) -> anyhow::Result<()> {
90 info!(concat!($verb, " behavior: {}"), self.behavior.name());
91 self.behavior.$behavior_method(ctx).await?;
92 Ok(())
93 }
94 };
95}
96
97impl BehaviorExecutor {
98 mecha10_core::new_with_defaults!(
99 pub fn new(behavior: BoxedBehavior, tick_rate_hz: f32) -> Self {
106 max_ticks: None
107 }
108 );
109
110 mecha10_core::builder_setter!(into_some, with_max_ticks, max_ticks, usize);
111
112 lifecycle_step!(init, "Initializing", on_init);
113
114 pub async fn run_until_complete(&mut self, ctx: &Context) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
123 let mut stats = ExecutionStats::new();
124 let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
125
126 info!(
127 "Starting behavior execution: {} at {} Hz",
128 self.behavior.name(),
129 self.tick_rate_hz
130 );
131
132 loop {
133 interval.tick().await;
134
135 if let Some(max_ticks) = self.max_ticks {
136 if stats.tick_count >= max_ticks {
137 warn!("Behavior exceeded maximum ticks ({})", max_ticks);
138 return Err(anyhow::anyhow!("Exceeded maximum ticks"));
139 }
140 }
141
142 let tick_start = Instant::now();
143 let status = self.behavior.tick(ctx).await?;
144 let tick_duration = tick_start.elapsed();
145
146 stats.update(tick_duration);
147
148 debug!(
149 "Tick #{}: status={}, duration={:?}",
150 stats.tick_count, status, tick_duration
151 );
152
153 if status.is_complete() {
154 info!(
155 "Behavior completed with status: {} after {} ticks ({:?})",
156 status, stats.tick_count, stats.total_duration
157 );
158 stats.finalize(status);
159 return Ok((status, stats));
160 }
161 }
162 }
163
164 pub async fn run_for_duration(
169 &mut self,
170 ctx: &Context,
171 duration: Duration,
172 ) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
173 let mut stats = ExecutionStats::new();
174 let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
175 let start_time = Instant::now();
176
177 info!(
178 "Starting behavior execution: {} for {:?} at {} Hz",
179 self.behavior.name(),
180 duration,
181 self.tick_rate_hz
182 );
183
184 let mut last_status = NodeStatus::Running;
185
186 while start_time.elapsed() < duration {
187 interval.tick().await;
188
189 let tick_start = Instant::now();
190 let status = self.behavior.tick(ctx).await?;
191 let tick_duration = tick_start.elapsed();
192
193 stats.update(tick_duration);
194 last_status = status;
195
196 debug!(
197 "Tick #{}: status={}, duration={:?}",
198 stats.tick_count, status, tick_duration
199 );
200
201 if status.is_complete() {
202 info!("Behavior completed early with status: {}", status);
203 break;
204 }
205 }
206
207 info!(
208 "Behavior execution ended after {} ticks ({:?})",
209 stats.tick_count, stats.total_duration
210 );
211 stats.finalize(last_status);
212 Ok((last_status, stats))
213 }
214
215 lifecycle_step!(terminate, "Terminating", on_terminate);
216
217 pub async fn reset(&mut self) -> anyhow::Result<()> {
219 info!("Resetting behavior: {}", self.behavior.name());
220 self.behavior.reset().await?;
221 Ok(())
222 }
223}