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 {
101 max_ticks: None
102 }
103 );
104
105 mecha10_core::builder_setter!(into_some, with_max_ticks, max_ticks, usize);
106
107 lifecycle_step!(init, "Initializing", on_init);
108
109 pub async fn run_until_complete(&mut self, ctx: &Context) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
113 let mut stats = ExecutionStats::new();
114 let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
115
116 info!(
117 "Starting behavior execution: {} at {} Hz",
118 self.behavior.name(),
119 self.tick_rate_hz
120 );
121
122 loop {
123 interval.tick().await;
124
125 if let Some(max_ticks) = self.max_ticks {
126 if stats.tick_count >= max_ticks {
127 warn!("Behavior exceeded maximum ticks ({})", max_ticks);
128 return Err(anyhow::anyhow!("Exceeded maximum ticks"));
129 }
130 }
131
132 let tick_start = Instant::now();
133 let status = self.behavior.tick(ctx).await?;
134 let tick_duration = tick_start.elapsed();
135
136 stats.update(tick_duration);
137
138 debug!(
139 "Tick #{}: status={}, duration={:?}",
140 stats.tick_count, status, tick_duration
141 );
142
143 if status.is_complete() {
144 info!(
145 "Behavior completed with status: {} after {} ticks ({:?})",
146 status, stats.tick_count, stats.total_duration
147 );
148 stats.finalize(status);
149 return Ok((status, stats));
150 }
151 }
152 }
153
154 pub async fn run_for_duration(
157 &mut self,
158 ctx: &Context,
159 duration: Duration,
160 ) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
161 let mut stats = ExecutionStats::new();
162 let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
163 let start_time = Instant::now();
164
165 info!(
166 "Starting behavior execution: {} for {:?} at {} Hz",
167 self.behavior.name(),
168 duration,
169 self.tick_rate_hz
170 );
171
172 let mut last_status = NodeStatus::Running;
173
174 while start_time.elapsed() < duration {
175 interval.tick().await;
176
177 let tick_start = Instant::now();
178 let status = self.behavior.tick(ctx).await?;
179 let tick_duration = tick_start.elapsed();
180
181 stats.update(tick_duration);
182 last_status = status;
183
184 debug!(
185 "Tick #{}: status={}, duration={:?}",
186 stats.tick_count, status, tick_duration
187 );
188
189 if status.is_complete() {
190 info!("Behavior completed early with status: {}", status);
191 break;
192 }
193 }
194
195 info!(
196 "Behavior execution ended after {} ticks ({:?})",
197 stats.tick_count, stats.total_duration
198 );
199 stats.finalize(last_status);
200 Ok((last_status, stats))
201 }
202
203 lifecycle_step!(terminate, "Terminating", on_terminate);
204
205 pub async fn reset(&mut self) -> anyhow::Result<()> {
207 info!("Resetting behavior: {}", self.behavior.name());
208 self.behavior.reset().await?;
209 Ok(())
210 }
211}