byteor_pipeline_spec/
dot.rs1#[cfg(feature = "alloc")]
4use alloc::string::{String, ToString};
5
6#[cfg(feature = "alloc")]
7use alloc::vec::Vec;
8
9#[cfg(feature = "alloc")]
10use crate::{
11 EndpointKindV1, LaneGraphV1, LaneKindV1, MergePolicyV1, PipelineSpecV1, RoleCfgV1, SpecError,
12 StageOpV1,
13};
14
15#[cfg(feature = "alloc")]
16fn dot_escape(value: &str) -> String {
17 let mut out = String::with_capacity(value.len());
18 for ch in value.chars() {
19 match ch {
20 '\\' => out.push_str("\\\\"),
21 '"' => out.push_str("\\\""),
22 '\n' => out.push_str("\\n"),
23 _ => out.push(ch),
24 }
25 }
26 out
27}
28
29#[cfg(feature = "alloc")]
30fn node_id(prefix: &str, name: &str) -> String {
31 let mut out = String::from(prefix);
32 out.push('_');
33 for ch in name.chars() {
34 if ch.is_ascii_alphanumeric() || ch == '_' {
35 out.push(ch);
36 } else {
37 out.push('_');
38 }
39 }
40 out
41}
42
43#[cfg(feature = "alloc")]
44fn lane_kind_label(kind: LaneKindV1) -> String {
45 match kind {
46 LaneKindV1::Events => "Events".to_string(),
47 LaneKindV1::Journal => "Journal".to_string(),
48 LaneKindV1::SequencedSlots { capacity, gating } => {
49 let mut s = String::from("SequencedSlots");
50 s.push_str("\\ncapacity=");
51 s.push_str(&capacity.to_string());
52 s.push_str(" gating=");
53 s.push_str(&gating.to_string());
54 s
55 }
56 LaneKindV1::FanoutBroadcast { consumers } => {
57 let mut s = String::from("FanoutBroadcast");
58 s.push_str("\\nconsumers=");
59 s.push_str(&consumers.to_string());
60 s
61 }
62 }
63}
64
65#[cfg(feature = "alloc")]
66fn stage_label(cfg: &crate::StageRoleCfgV1) -> String {
67 let mut label = String::from("Stage");
68 label.push_str("\\nrole=");
69 label.push_str(&dot_escape(&cfg.name));
70 label.push_str("\\nstage=");
71 label.push_str(&dot_escape(&cfg.stage));
72 label
73}
74
75#[cfg(feature = "alloc")]
76fn bridge_label(cfg: &crate::BridgeRoleCfgV1) -> String {
77 let mut label = String::from("Bridge");
78 label.push_str("\\nrole=");
79 label.push_str(&dot_escape(&cfg.name));
80 label
81}
82
83#[cfg(feature = "alloc")]
84fn router_label(cfg: &crate::RouterRoleCfgV1) -> String {
85 let mut label = String::from("Router");
86 label.push_str("\\nrole=");
87 label.push_str(&dot_escape(&cfg.name));
88 label
89}
90
91#[cfg(feature = "alloc")]
92fn merge_label(cfg: &crate::MergeRoleCfgV1) -> String {
93 let mut label = String::from("Merge");
94 label.push_str("\\nrole=");
95 label.push_str(&dot_escape(&cfg.name));
96 label.push_str("\\npolicy=");
97 match cfg.policy {
98 MergePolicyV1::RoundRobin => label.push_str("round_robin"),
99 }
100 label
101}
102
103#[cfg(feature = "alloc")]
104fn lane_endpoints(lg: &LaneGraphV1, lane_name: &str) -> Result<(String, Vec<String>), SpecError> {
105 let mut producer: Option<String> = None;
106 let mut consumers: Vec<String> = Vec::new();
107
108 for ep in &lg.endpoints {
109 if ep.lane == lane_name {
110 match ep.kind {
111 EndpointKindV1::Ingress => {
112 producer = Some(node_id("endpoint", &ep.name));
113 }
114 EndpointKindV1::Egress => {
115 consumers.push(node_id("endpoint", &ep.name));
116 }
117 }
118 }
119 }
120
121 for role in &lg.roles {
122 match role {
123 RoleCfgV1::Stage(cfg) => {
124 if cfg.tx == lane_name {
125 producer = Some(node_id("role", &cfg.name));
126 }
127 if cfg.rx == lane_name {
128 consumers.push(node_id("role", &cfg.name));
129 }
130 }
131 RoleCfgV1::Bridge(cfg) => {
132 if cfg.tx == lane_name {
133 producer = Some(node_id("role", &cfg.name));
134 }
135 if cfg.rx == lane_name {
136 consumers.push(node_id("role", &cfg.name));
137 }
138 }
139 RoleCfgV1::Router(cfg) => {
140 if cfg.rx == lane_name {
141 consumers.push(node_id("role", &cfg.name));
142 }
143 if cfg.tx.iter().any(|tx| tx == lane_name) {
144 producer = Some(node_id("role", &cfg.name));
145 }
146 }
147 RoleCfgV1::Merge(cfg) => {
148 if cfg.tx == lane_name {
149 producer = Some(node_id("role", &cfg.name));
150 }
151 if cfg.rx.iter().any(|rx| rx == lane_name) {
152 consumers.push(node_id("role", &cfg.name));
153 }
154 }
155 }
156 }
157
158 let producer = producer.ok_or(SpecError::new(
159 "lane graph dot rendering requires a producer for every lane",
160 ))?;
161 if consumers.is_empty() {
162 return Err(SpecError::new(
163 "lane graph dot rendering requires at least one consumer for every lane",
164 ));
165 }
166 Ok((producer, consumers))
167}
168
169#[cfg(feature = "alloc")]
170fn single_ring_stage_label(op: &StageOpV1) -> String {
171 match op {
172 StageOpV1::Identity => "Identity".to_string(),
173 StageOpV1::AddU8 { delta } => {
174 let mut label = String::from("AddU8");
175 label.push_str("\\ndelta=");
176 label.push_str(&delta.to_string());
177 label
178 }
179 StageOpV1::ResolverKey { stage } => {
180 let mut label = String::from("ResolverKey");
181 label.push_str("\\nstage=");
182 label.push_str(&dot_escape(stage));
183 label
184 }
185 }
186}
187
188#[cfg(feature = "alloc")]
190pub fn dot_v1(spec: &PipelineSpecV1) -> Result<String, SpecError> {
191 match spec {
192 PipelineSpecV1::LaneGraph(_) => dot_lane_graph_v1(spec),
193 PipelineSpecV1::SingleRing(ring) => {
194 if ring.stages.is_empty() {
195 return Err(SpecError::new("dot rendering requires at least one single ring stage"));
196 }
197
198 let mut out = String::from("digraph byteor_single_ring_v1 {\n");
199 out.push_str(" rankdir=LR;\n");
200 out.push_str(" graph [fontname=\"Helvetica\"];\n");
201 out.push_str(" node [fontname=\"Helvetica\", shape=box];\n");
202 out.push_str(" ingress [shape=oval, style=rounded, label=\"Ingress\"];\n");
203 out.push_str(" egress [shape=oval, style=rounded, label=\"Egress\"];\n");
204
205 for (index, stage) in ring.stages.iter().enumerate() {
206 let mut label = String::from("Stage ");
207 label.push_str(&index.to_string());
208 label.push_str("\\n");
209 label.push_str(&single_ring_stage_label(&stage.op));
210 out.push_str(" stage_");
211 out.push_str(&index.to_string());
212 out.push_str(" [label=\"");
213 out.push_str(&label);
214 out.push_str("\"];\n");
215 }
216
217 for (index, stage) in ring.stages.iter().enumerate() {
218 if stage.depends_on.is_empty() {
219 out.push_str(" ingress -> stage_");
220 out.push_str(&index.to_string());
221 out.push_str(" [label=\"ring\"];\n");
222 } else {
223 for dependency in &stage.depends_on {
224 out.push_str(" stage_");
225 out.push_str(&dependency.to_string());
226 out.push_str(" -> stage_");
227 out.push_str(&index.to_string());
228 out.push_str(" [label=\"depends\"];\n");
229 }
230 }
231 }
232
233 out.push_str(" stage_");
234 out.push_str(&(ring.stages.len() - 1).to_string());
235 out.push_str(" -> egress [label=\"ring\"];\n");
236 out.push_str("}\n");
237 Ok(out)
238 }
239 }
240}
241
242#[cfg(feature = "alloc")]
244pub fn dot_lane_graph_v1(spec: &PipelineSpecV1) -> Result<String, SpecError> {
245 let PipelineSpecV1::LaneGraph(lg) = spec else {
246 return Err(SpecError::new("dot rendering requires a lane graph spec"));
247 };
248
249 let mut out = String::from("digraph byteor_lane_graph_v1 {\n");
250 out.push_str(" rankdir=LR;\n");
251 out.push_str(" graph [fontname=\"Helvetica\"];\n");
252 out.push_str(" node [fontname=\"Helvetica\"];\n");
253 out.push_str(" edge [fontname=\"Helvetica\"];\n");
254
255 for ep in &lg.endpoints {
256 let id = node_id("endpoint", &ep.name);
257 let label = match ep.kind {
258 EndpointKindV1::Ingress => {
259 let mut s = String::from("Ingress");
260 s.push_str("\\nendpoint=");
261 s.push_str(&dot_escape(&ep.name));
262 s
263 }
264 EndpointKindV1::Egress => {
265 let mut s = String::from("Egress");
266 s.push_str("\\nendpoint=");
267 s.push_str(&dot_escape(&ep.name));
268 s
269 }
270 };
271 out.push_str(" ");
272 out.push_str(&id);
273 out.push_str(" [shape=oval, style=rounded, label=\"");
274 out.push_str(&label);
275 out.push_str("\"];\n");
276 }
277
278 for role in &lg.roles {
279 match role {
280 RoleCfgV1::Stage(cfg) => {
281 out.push_str(" ");
282 out.push_str(&node_id("role", &cfg.name));
283 out.push_str(" [shape=box, label=\"");
284 out.push_str(&stage_label(cfg));
285 out.push_str("\"];\n");
286 }
287 RoleCfgV1::Bridge(cfg) => {
288 out.push_str(" ");
289 out.push_str(&node_id("role", &cfg.name));
290 out.push_str(" [shape=diamond, label=\"");
291 out.push_str(&bridge_label(cfg));
292 out.push_str("\"];\n");
293 }
294 RoleCfgV1::Router(cfg) => {
295 out.push_str(" ");
296 out.push_str(&node_id("role", &cfg.name));
297 out.push_str(" [shape=hexagon, label=\"");
298 out.push_str(&router_label(cfg));
299 out.push_str("\"];\n");
300 }
301 RoleCfgV1::Merge(cfg) => {
302 out.push_str(" ");
303 out.push_str(&node_id("role", &cfg.name));
304 out.push_str(" [shape=invtriangle, label=\"");
305 out.push_str(&merge_label(cfg));
306 out.push_str("\"];\n");
307 }
308 }
309 }
310
311 for lane in &lg.lanes {
312 let (producer, consumers) = lane_endpoints(lg, &lane.name)?;
313 for consumer in consumers {
314 out.push_str(" ");
315 out.push_str(&producer);
316 out.push_str(" -> ");
317 out.push_str(&consumer);
318 out.push_str(" [label=\"");
319 out.push_str(&dot_escape(&lane.name));
320 out.push_str("\\n");
321 out.push_str(&lane_kind_label(lane.kind));
322 out.push_str("\"];\n");
323 }
324 }
325
326 out.push_str("}\n");
327 Ok(out)
328}