1use std::collections::{HashMap, HashSet};
2use std::fmt::Write;
3
4use serde::Serialize;
5
6use super::render::{HydroEdgeProp, HydroGraphWrite, HydroNodeType};
7use crate::compile::ir::HydroRoot;
8use crate::compile::ir::backtrace::Backtrace;
9
10#[derive(Serialize)]
13struct BacktraceFrame {
14 #[serde(rename = "fn")]
16 fn_name: String,
17 function: String,
19 file: String,
21 filename: String,
23 line: Option<u32>,
25 #[serde(rename = "lineNumber")]
27 line_number: Option<u32>,
28}
29
30#[derive(Serialize)]
32struct NodeData {
33 #[serde(rename = "locationId")]
34 location_id: Option<usize>,
35 #[serde(rename = "locationType")]
36 location_type: Option<String>,
37 backtrace: serde_json::Value,
38}
39
40#[derive(Serialize)]
42struct Node {
43 id: String,
44 #[serde(rename = "nodeType")]
45 node_type: String,
46 #[serde(rename = "fullLabel")]
47 full_label: String,
48 #[serde(rename = "shortLabel")]
49 short_label: String,
50 label: String,
51 data: NodeData,
52}
53
54#[derive(Serialize)]
56struct Edge {
57 id: String,
58 source: String,
59 target: String,
60 #[serde(rename = "semanticTags")]
61 semantic_tags: Vec<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 label: Option<String>,
64}
65
66pub struct HydroJson<W> {
69 write: W,
70 nodes: Vec<serde_json::Value>,
71 edges: Vec<serde_json::Value>,
72 locations: HashMap<usize, (String, Vec<usize>)>, node_locations: HashMap<usize, usize>, edge_count: usize,
75 process_names: HashMap<usize, String>,
77 cluster_names: HashMap<usize, String>,
78 external_names: HashMap<usize, String>,
79 node_backtraces: HashMap<usize, Backtrace>,
81 use_short_labels: bool,
83}
84
85impl<W> HydroJson<W> {
86 pub fn new(write: W, config: &super::render::HydroWriteConfig) -> Self {
87 let process_names: HashMap<usize, String> =
88 config.process_id_name.iter().cloned().collect();
89 let cluster_names: HashMap<usize, String> =
90 config.cluster_id_name.iter().cloned().collect();
91 let external_names: HashMap<usize, String> =
92 config.external_id_name.iter().cloned().collect();
93
94 Self {
95 write,
96 nodes: Vec::new(),
97 edges: Vec::new(),
98 locations: HashMap::new(),
99 node_locations: HashMap::new(),
100 edge_count: 0,
101 process_names,
102 cluster_names,
103 external_names,
104 node_backtraces: HashMap::new(),
105 use_short_labels: config.use_short_labels,
106 }
107 }
108
109 fn node_type_to_string(node_type: HydroNodeType) -> &'static str {
111 super::render::node_type_utils::to_string(node_type)
112 }
113
114 fn edge_type_to_string(edge_type: HydroEdgeProp) -> String {
116 match edge_type {
117 HydroEdgeProp::Bounded => "Bounded".to_string(),
118 HydroEdgeProp::Unbounded => "Unbounded".to_string(),
119 HydroEdgeProp::TotalOrder => "TotalOrder".to_string(),
120 HydroEdgeProp::NoOrder => "NoOrder".to_string(),
121 HydroEdgeProp::Keyed => "Keyed".to_string(),
122 HydroEdgeProp::Stream => "Stream".to_string(),
123 HydroEdgeProp::KeyedSingleton => "KeyedSingleton".to_string(),
124 HydroEdgeProp::KeyedStream => "KeyedStream".to_string(),
125 HydroEdgeProp::Singleton => "Singleton".to_string(),
126 HydroEdgeProp::Optional => "Optional".to_string(),
127 HydroEdgeProp::Network => "Network".to_string(),
128 HydroEdgeProp::Cycle => "Cycle".to_string(),
129 }
130 }
131
132 fn get_node_type_definitions() -> Vec<serde_json::Value> {
134 let mut types: Vec<(usize, &'static str)> =
136 super::render::node_type_utils::all_types_with_strings()
137 .into_iter()
138 .enumerate()
139 .map(|(idx, (_, type_str))| (idx, type_str))
140 .collect();
141 types.sort_by(|a, b| a.1.cmp(b.1));
142 types
143 .into_iter()
144 .enumerate()
145 .map(|(color_index, (_, type_str))| {
146 serde_json::json!({
147 "id": type_str,
148 "label": type_str,
149 "colorIndex": color_index
150 })
151 })
152 .collect()
153 }
154
155 fn get_legend_items() -> Vec<serde_json::Value> {
157 Self::get_node_type_definitions()
158 .into_iter()
159 .map(|def| {
160 serde_json::json!({
161 "type": def["id"],
162 "label": def["label"]
163 })
164 })
165 .collect()
166 }
167
168 fn get_edge_style_config() -> serde_json::Value {
170 serde_json::json!({
171 "semanticPriorities": [
172 ["Unbounded", "Bounded"],
173 ["NoOrder", "TotalOrder"],
174 ["Keyed", "NotKeyed"],
175 ["Network", "Local"]
176 ],
177 "semanticMappings": {
178 "NetworkGroup": {
180 "Local": {
181 "line-pattern": "solid",
182 "animation": "static"
183 },
184 "Network": {
185 "line-pattern": "dashed",
186 "animation": "animated"
187 }
188 },
189
190 "OrderingGroup": {
192 "TotalOrder": {
193 "waviness": "straight"
194 },
195 "NoOrder": {
196 "waviness": "wavy"
197 }
198 },
199
200 "BoundednessGroup": {
202 "Bounded": {
203 "halo": "none"
204 },
205 "Unbounded": {
206 "halo": "light-blue"
207 }
208 },
209
210 "KeyednessGroup": {
212 "NotKeyed": {
213 "line-style": "single"
214 },
215 "Keyed": {
216 "line-style": "hash-marks"
217 }
218 },
219
220 "CollectionGroup": {
222 "Stream": {
223 "color": "#2563eb",
224 "arrowhead": "triangle-filled"
225 },
226 "Singleton": {
227 "color": "#000000",
228 "arrowhead": "circle-filled"
229 },
230 "Optional": {
231 "color": "#6b7280",
232 "arrowhead": "diamond-open"
233 }
234 },
235 },
236 "note": "Edge styles are now computed per-edge using the unified edge style system. This config is provided for reference and compatibility."
237 })
238 }
239
240 fn optimize_backtrace(&self, backtrace: &Backtrace) -> serde_json::Value {
245 #[cfg(feature = "build")]
246 {
247 let elements = backtrace.elements();
248
249 let relevant_frames: Vec<BacktraceFrame> = elements
251 .iter()
252 .map(|elem| {
253 let short_filename = elem
255 .filename
256 .as_deref()
257 .map(|f| Self::truncate_path(f))
258 .unwrap_or_else(|| "unknown".to_string());
259
260 let short_fn_name = Self::truncate_function_name(&elem.fn_name);
261
262 BacktraceFrame {
263 fn_name: short_fn_name.clone(),
264 function: short_fn_name,
265 file: short_filename.clone(),
266 filename: short_filename,
267 line: elem.lineno,
268 line_number: elem.lineno,
269 }
270 })
271 .collect();
272
273 serde_json::to_value(relevant_frames).unwrap_or_else(|_| serde_json::json!([]))
274 }
275 #[cfg(not(feature = "build"))]
276 {
277 serde_json::json!([])
278 }
279 }
280
281 fn truncate_path(path: &str) -> String {
283 let parts: Vec<&str> = path.split('/').collect();
284
285 if let Some(src_idx) = parts.iter().rposition(|&p| p == "src") {
287 parts[src_idx..].join("/")
288 } else if parts.len() > 2 {
289 parts[parts.len().saturating_sub(2)..].join("/")
291 } else {
292 path.to_string()
293 }
294 }
295
296 fn truncate_function_name(fn_name: &str) -> String {
298 fn_name.split("::").last().unwrap_or(fn_name).to_string()
300 }
301}
302
303impl<W> HydroGraphWrite for HydroJson<W>
304where
305 W: Write,
306{
307 type Err = super::render::GraphWriteError;
308
309 fn write_prologue(&mut self) -> Result<(), Self::Err> {
310 self.nodes.clear();
312 self.edges.clear();
313 self.locations.clear();
314 self.node_locations.clear();
315 self.edge_count = 0;
316 Ok(())
317 }
318
319 fn write_node_definition(
320 &mut self,
321 node_id: usize,
322 node_label: &super::render::NodeLabel,
323 node_type: HydroNodeType,
324 location_id: Option<usize>,
325 location_type: Option<&str>,
326 backtrace: Option<&Backtrace>,
327 ) -> Result<(), Self::Err> {
328 let full_label = match node_label {
330 super::render::NodeLabel::Static(s) => s.clone(),
331 super::render::NodeLabel::WithExprs { op_name, exprs } => {
332 if exprs.is_empty() {
333 format!("{}()", op_name)
334 } else {
335 let expr_strs: Vec<String> = exprs.iter().map(|e| e.to_string()).collect();
337 format!("{}({})", op_name, expr_strs.join(", "))
338 }
339 }
340 };
341
342 let short_label = super::render::extract_short_label(&full_label);
344
345 let full_len = full_label.len();
348 let enhanced_full_label = if short_label.len() >= full_len.saturating_sub(2) {
349 match short_label.as_str() {
351 "inspect" => "inspect [debug output]".to_string(),
352 "persist" => "persist [state storage]".to_string(),
353 "tee" => "tee [branch dataflow]".to_string(),
354 "delta" => "delta [change detection]".to_string(),
355 "spin" => "spin [delay/buffer]".to_string(),
356 "send_bincode" => "send_bincode [send data to process/cluster]".to_string(),
357 "broadcast_bincode" => {
358 "broadcast_bincode [send data to all cluster members]".to_string()
359 }
360 "source_iter" => "source_iter [iterate over collection]".to_string(),
361 "source_stream" => "source_stream [receive external data stream]".to_string(),
362 "network(recv)" => "network(recv) [receive from network]".to_string(),
363 "network(send)" => "network(send) [send to network]".to_string(),
364 "dest_sink" => "dest_sink [output destination]".to_string(),
365 _ => {
366 if full_label.len() < 15 {
367 format!("{} [{}]", node_label, "hydro operator")
368 } else {
369 node_label.to_string()
370 }
371 }
372 }
373 } else {
374 node_label.to_string()
375 };
376
377 let backtrace_json = if let Some(bt) = backtrace {
379 self.node_backtraces.insert(node_id, bt.clone());
381 self.optimize_backtrace(bt)
382 } else {
383 serde_json::json!([])
384 };
385
386 let node_type_str = Self::node_type_to_string(node_type);
388
389 let node = Node {
390 id: node_id.to_string(),
391 node_type: node_type_str.to_string(),
392 full_label: enhanced_full_label,
393 short_label: short_label.clone(),
394 label: if self.use_short_labels {
396 short_label
397 } else {
398 full_label
399 },
400 data: NodeData {
401 location_id,
402 location_type: location_type.map(|s| s.to_string()),
403 backtrace: backtrace_json,
404 },
405 };
406 self.nodes
407 .push(serde_json::to_value(node).expect("Node serialization should not fail"));
408
409 if let Some(loc_id) = location_id {
411 self.node_locations.insert(node_id, loc_id);
412 }
413
414 Ok(())
415 }
416
417 fn write_edge(
418 &mut self,
419 src_id: usize,
420 dst_id: usize,
421 edge_properties: &HashSet<HydroEdgeProp>,
422 label: Option<&str>,
423 ) -> Result<(), Self::Err> {
424 let edge_id = format!("e{}", self.edge_count);
425 self.edge_count = self.edge_count.saturating_add(1);
426
427 let mut semantic_tags: Vec<String> = edge_properties
429 .iter()
430 .map(|p| Self::edge_type_to_string(*p))
431 .collect();
432
433 let src_loc = self.node_locations.get(&src_id).copied();
435 let dst_loc = self.node_locations.get(&dst_id).copied();
436
437 if let (Some(src), Some(dst)) = (src_loc, dst_loc)
439 && src != dst
440 && !semantic_tags.iter().any(|t| t == "Network")
441 {
442 semantic_tags.push("Network".to_string());
443 } else if semantic_tags.iter().all(|t| t != "Network") {
444 semantic_tags.push("Local".to_string());
446 }
447
448 semantic_tags.sort();
450
451 let edge = Edge {
452 id: edge_id,
453 source: src_id.to_string(),
454 target: dst_id.to_string(),
455 semantic_tags,
456 label: label.map(|s| s.to_string()),
457 };
458
459 self.edges
460 .push(serde_json::to_value(edge).expect("Edge serialization should not fail"));
461 Ok(())
462 }
463
464 fn write_location_start(
465 &mut self,
466 location_id: usize,
467 location_type: &str,
468 ) -> Result<(), Self::Err> {
469 let location_label = match location_type {
470 "Process" => {
471 if let Some(name) = self.process_names.get(&location_id) {
472 if name == "()" {
474 format!("Process {}", location_id)
475 } else {
476 name.clone()
477 }
478 } else {
479 format!("Process {}", location_id)
480 }
481 }
482 "Cluster" => {
483 if let Some(name) = self.cluster_names.get(&location_id) {
484 name.clone()
485 } else {
486 format!("Cluster {}", location_id)
487 }
488 }
489 "External" => {
490 if let Some(name) = self.external_names.get(&location_id) {
491 name.clone()
492 } else {
493 format!("External {}", location_id)
494 }
495 }
496 _ => location_type.to_string(),
497 };
498
499 self.locations
500 .insert(location_id, (location_label, Vec::new()));
501 Ok(())
502 }
503
504 fn write_node(&mut self, node_id: usize) -> Result<(), Self::Err> {
505 if let Some((_, node_ids)) = self.locations.values_mut().last() {
507 node_ids.push(node_id);
508 }
509 Ok(())
510 }
511
512 fn write_location_end(&mut self) -> Result<(), Self::Err> {
513 Ok(())
515 }
516
517 fn write_epilogue(&mut self) -> Result<(), Self::Err> {
518 let mut hierarchy_choices = Vec::new();
520 let mut node_assignments_choices = serde_json::Map::new();
521
522 let (location_hierarchy, location_assignments) = self.create_location_hierarchy();
524 hierarchy_choices.push(serde_json::json!({
525 "id": "location",
526 "name": "Location",
527 "children": location_hierarchy
528 }));
529 node_assignments_choices.insert(
530 "location".to_string(),
531 serde_json::Value::Object(location_assignments),
532 );
533
534 if self.has_backtrace_data() {
536 let (backtrace_hierarchy, backtrace_assignments) = self.create_backtrace_hierarchy();
537 hierarchy_choices.push(serde_json::json!({
538 "id": "backtrace",
539 "name": "Backtrace",
540 "children": backtrace_hierarchy
541 }));
542 node_assignments_choices.insert(
543 "backtrace".to_string(),
544 serde_json::Value::Object(backtrace_assignments),
545 );
546 }
547
548 let mut nodes_sorted = self.nodes.clone();
550 nodes_sorted.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
551 let mut edges_sorted = self.edges.clone();
552 edges_sorted.sort_by(|a, b| {
553 let a_src = a["source"].as_str();
554 let b_src = b["source"].as_str();
555 match a_src.cmp(&b_src) {
556 std::cmp::Ordering::Equal => {
557 let a_dst = a["target"].as_str();
558 let b_dst = b["target"].as_str();
559 match a_dst.cmp(&b_dst) {
560 std::cmp::Ordering::Equal => a["id"].as_str().cmp(&b["id"].as_str()),
561 other => other,
562 }
563 }
564 other => other,
565 }
566 });
567
568 let node_type_definitions = Self::get_node_type_definitions();
570 let legend_items = Self::get_legend_items();
571
572 let node_type_config = serde_json::json!({
573 "types": node_type_definitions,
574 "defaultType": "Transform"
575 });
576 let legend = serde_json::json!({
577 "title": "Node Types",
578 "items": legend_items
579 });
580
581 let selected_hierarchy = if !hierarchy_choices.is_empty() {
583 hierarchy_choices[0]["id"].as_str().map(|s| s.to_string())
584 } else {
585 None
586 };
587
588 #[derive(serde::Serialize)]
589 struct GraphPayload {
590 nodes: Vec<serde_json::Value>,
591 edges: Vec<serde_json::Value>,
592 #[serde(rename = "hierarchyChoices")]
593 hierarchy_choices: Vec<serde_json::Value>,
594 #[serde(rename = "nodeAssignments")]
595 node_assignments: serde_json::Map<String, serde_json::Value>,
596 #[serde(rename = "selectedHierarchy", skip_serializing_if = "Option::is_none")]
597 selected_hierarchy: Option<String>,
598 #[serde(rename = "edgeStyleConfig")]
599 edge_style_config: serde_json::Value,
600 #[serde(rename = "nodeTypeConfig")]
601 node_type_config: serde_json::Value,
602 legend: serde_json::Value,
603 }
604
605 let payload = GraphPayload {
606 nodes: nodes_sorted,
607 edges: edges_sorted,
608 hierarchy_choices,
609 node_assignments: node_assignments_choices,
610 selected_hierarchy,
611 edge_style_config: Self::get_edge_style_config(),
612 node_type_config,
613 legend,
614 };
615
616 let final_json = serde_json::to_string_pretty(&payload).unwrap();
617
618 write!(self.write, "{}", final_json)
619 }
620}
621
622impl<W> HydroJson<W> {
623 fn has_backtrace_data(&self) -> bool {
625 self.nodes.iter().any(|node| {
626 if let Some(backtrace_array) = node["data"]["backtrace"].as_array() {
627 backtrace_array.iter().any(|frame| {
629 let filename = frame["file"].as_str().unwrap_or("");
630 let fn_name = frame["fn"].as_str().unwrap_or("");
631 !filename.is_empty() || !fn_name.is_empty()
632 })
633 } else {
634 false
635 }
636 })
637 }
638
639 fn create_location_hierarchy(
641 &self,
642 ) -> (
643 Vec<serde_json::Value>,
644 serde_json::Map<String, serde_json::Value>,
645 ) {
646 let mut locs: Vec<(&usize, &(String, Vec<usize>))> = self.locations.iter().collect();
648 locs.sort_by(|a, b| a.0.cmp(b.0));
649 let hierarchy: Vec<serde_json::Value> = locs
650 .into_iter()
651 .map(|(location_id, (label, _))| {
652 serde_json::json!({
653 "id": format!("loc_{}", location_id),
654 "name": label,
655 "children": [] })
657 })
658 .collect();
659
660 let mut tmp: Vec<(String, String)> = Vec::new();
664 for node in &self.nodes {
665 if let (Some(node_id), Some(location_id)) =
666 (node["id"].as_str(), node["data"]["locationId"].as_u64())
667 {
668 let location_key = format!("loc_{}", location_id);
669 tmp.push((node_id.to_string(), location_key));
670 }
671 }
672 tmp.sort_by(|a, b| a.0.cmp(&b.0));
673 let mut node_assignments = serde_json::Map::new();
674 for (k, v) in tmp {
675 node_assignments.insert(k, serde_json::Value::String(v));
676 }
677
678 (hierarchy, node_assignments)
679 }
680
681 fn create_backtrace_hierarchy(
683 &self,
684 ) -> (
685 Vec<serde_json::Value>,
686 serde_json::Map<String, serde_json::Value>,
687 ) {
688 use std::collections::HashMap;
689
690 let mut hierarchy_map: HashMap<String, (String, usize, Option<String>)> = HashMap::new(); let mut path_to_node_assignments: HashMap<String, Vec<String>> = HashMap::new(); for node in &self.nodes {
695 if let Some(node_id_str) = node["id"].as_str()
696 && let Ok(node_id) = node_id_str.parse::<usize>()
697 && let Some(backtrace) = self.node_backtraces.get(&node_id)
698 {
699 let elements = backtrace.elements();
700 if elements.is_empty() {
701 continue;
702 }
703
704 let user_frames = elements;
706 if user_frames.is_empty() {
707 continue;
708 }
709
710 let mut hierarchy_path = Vec::new();
712 for (i, elem) in user_frames.iter().rev().enumerate() {
713 let label = if i == 0 {
714 if let Some(filename) = &elem.filename {
715 Self::extract_file_path(filename)
716 } else {
717 format!("fn_{}", Self::truncate_function_name(&elem.fn_name))
718 }
719 } else {
720 Self::truncate_function_name(&elem.fn_name)
721 };
722 hierarchy_path.push(label);
723 }
724
725 let mut current_path = String::new();
727 let mut parent_path: Option<String> = None;
728 let mut deepest_path = String::new();
729 let mut deduped: Vec<String> = Vec::new();
731 for seg in hierarchy_path {
732 if deduped.last().map(|s| s == &seg).unwrap_or(false) {
733 continue;
734 }
735 deduped.push(seg);
736 }
737 for (depth, label) in deduped.iter().enumerate() {
738 current_path = if current_path.is_empty() {
739 label.clone()
740 } else {
741 format!("{}/{}", current_path, label)
742 };
743 if !hierarchy_map.contains_key(¤t_path) {
744 hierarchy_map.insert(
745 current_path.clone(),
746 (label.clone(), depth, parent_path.clone()),
747 );
748 }
749 deepest_path = current_path.clone();
750 parent_path = Some(current_path.clone());
751 }
752
753 if !deepest_path.is_empty() {
754 path_to_node_assignments
755 .entry(deepest_path)
756 .or_default()
757 .push(node_id_str.to_string());
758 }
759 }
760 }
761 let (mut hierarchy, mut path_to_id_map, id_remapping) =
763 self.build_hierarchy_tree_with_ids(&hierarchy_map);
764
765 let root_id = "bt_root".to_string();
767 let mut nodes_without_backtrace = Vec::new();
768
769 for node in &self.nodes {
771 if let Some(node_id_str) = node["id"].as_str() {
772 nodes_without_backtrace.push(node_id_str.to_string());
773 }
774 }
775
776 for node_ids in path_to_node_assignments.values() {
778 for node_id in node_ids {
779 nodes_without_backtrace.retain(|id| id != node_id);
780 }
781 }
782
783 if !nodes_without_backtrace.is_empty() {
785 hierarchy.push(serde_json::json!({
786 "id": root_id.clone(),
787 "name": "(no backtrace)",
788 "children": []
789 }));
790 path_to_id_map.insert("__root__".to_string(), root_id.clone());
791 }
792
793 let mut node_assignments = serde_json::Map::new();
795 let mut pairs: Vec<(String, Vec<String>)> = path_to_node_assignments.into_iter().collect();
796 pairs.sort_by(|a, b| a.0.cmp(&b.0));
797 for (path, mut node_ids) in pairs {
798 node_ids.sort();
799 if let Some(hierarchy_id) = path_to_id_map.get(&path) {
800 for node_id in node_ids {
801 node_assignments
802 .insert(node_id, serde_json::Value::String(hierarchy_id.clone()));
803 }
804 }
805 }
806
807 for node_id in nodes_without_backtrace {
809 node_assignments.insert(node_id, serde_json::Value::String(root_id.clone()));
810 }
811
812 let mut remapped_assignments = serde_json::Map::new();
816 for (node_id, container_id_value) in node_assignments.iter() {
817 if let Some(container_id) = container_id_value.as_str() {
818 let final_container_id = id_remapping
820 .get(container_id)
821 .map(|s| s.as_str())
822 .unwrap_or(container_id);
823 remapped_assignments.insert(
824 node_id.clone(),
825 serde_json::Value::String(final_container_id.to_string()),
826 );
827 }
828 }
829
830 (hierarchy, remapped_assignments)
831 }
832
833 fn build_hierarchy_tree_with_ids(
835 &self,
836 hierarchy_map: &HashMap<String, (String, usize, Option<String>)>,
837 ) -> (
838 Vec<serde_json::Value>,
839 HashMap<String, String>,
840 HashMap<String, String>,
841 ) {
842 let mut keys: Vec<&String> = hierarchy_map.keys().collect();
844 keys.sort();
845 let mut path_to_id: HashMap<String, String> = HashMap::new();
846 for (i, path) in keys.iter().enumerate() {
847 path_to_id.insert((*path).clone(), format!("bt_{}", i.saturating_add(1)));
848 }
849
850 let mut roots: Vec<(String, String)> = hierarchy_map
852 .iter()
853 .filter_map(|(path, (name, depth, _))| {
854 if *depth == 0 {
855 Some((path.clone(), name.clone()))
856 } else {
857 None
858 }
859 })
860 .collect();
861 roots.sort_by(|a, b| a.1.cmp(&b.1));
862 let mut root_nodes = Vec::new();
863 for (path, name) in roots {
864 let tree_node = Self::build_tree_node(&path, &name, hierarchy_map, &path_to_id);
865 root_nodes.push(tree_node);
866 }
867
868 let mut id_remapping: HashMap<String, String> = HashMap::new();
871 root_nodes = root_nodes
872 .into_iter()
873 .map(|node| Self::collapse_single_child_containers(node, None, &mut id_remapping))
874 .collect();
875
876 let mut updated_path_to_id = path_to_id.clone();
878 for (path, old_id) in path_to_id.iter() {
879 if let Some(new_id) = id_remapping.get(old_id) {
880 updated_path_to_id.insert(path.clone(), new_id.clone());
881 }
882 }
883
884 (root_nodes, updated_path_to_id, id_remapping)
885 }
886
887 fn build_tree_node(
889 current_path: &str,
890 name: &str,
891 hierarchy_map: &HashMap<String, (String, usize, Option<String>)>,
892 path_to_id: &HashMap<String, String>,
893 ) -> serde_json::Value {
894 let current_id = path_to_id.get(current_path).unwrap().clone();
895
896 let mut child_specs: Vec<(&String, &String)> = hierarchy_map
898 .iter()
899 .filter_map(|(child_path, (child_name, _, parent_path))| {
900 if let Some(parent) = parent_path {
901 if parent == current_path {
902 Some((child_path, child_name))
903 } else {
904 None
905 }
906 } else {
907 None
908 }
909 })
910 .collect();
911 child_specs.sort_by(|a, b| a.1.cmp(b.1));
912 let mut children = Vec::new();
913 for (child_path, child_name) in child_specs {
914 let child_node =
915 Self::build_tree_node(child_path, child_name, hierarchy_map, path_to_id);
916 children.push(child_node);
917 }
918
919 if children.is_empty() {
920 serde_json::json!({
921 "id": current_id,
922 "name": name
923 })
924 } else {
925 serde_json::json!({
926 "id": current_id,
927 "name": name,
928 "children": children
929 })
930 }
931 }
932
933 fn collapse_single_child_containers(
939 node: serde_json::Value,
940 parent_name: Option<String>,
941 id_remapping: &mut HashMap<String, String>,
942 ) -> serde_json::Value {
943 let mut node_obj = match node {
944 serde_json::Value::Object(obj) => obj,
945 _ => return node,
946 };
947
948 let current_name = node_obj
949 .get("name")
950 .and_then(|v| v.as_str())
951 .unwrap_or("")
952 .to_string();
953
954 let current_id = node_obj
955 .get("id")
956 .and_then(|v| v.as_str())
957 .unwrap_or("")
958 .to_string();
959
960 let effective_name = if let Some(parent) = parent_name.clone() {
963 format!("{} → {}", parent, current_name)
964 } else {
965 current_name.clone()
966 };
967
968 if let Some(serde_json::Value::Array(children)) = node_obj.get("children") {
970 if children.len() == 1
972 && let Some(child) = children.first()
973 {
974 let child_is_container = child
975 .get("children")
976 .and_then(|v| v.as_array())
977 .map(|arr| !arr.is_empty())
978 .unwrap_or(false);
979
980 if child_is_container {
981 let child_id = child
982 .get("id")
983 .and_then(|v| v.as_str())
984 .unwrap_or("")
985 .to_string();
986
987 if !current_id.is_empty() && !child_id.is_empty() {
989 id_remapping.insert(current_id.clone(), child_id.clone());
990 }
991
992 return Self::collapse_single_child_containers(
994 child.clone(),
995 Some(effective_name),
996 id_remapping,
997 );
998 }
999 }
1000
1001 let processed_children: Vec<serde_json::Value> = children
1003 .iter()
1004 .map(|child| {
1005 Self::collapse_single_child_containers(child.clone(), None, id_remapping)
1006 })
1007 .collect();
1008
1009 node_obj.insert(
1010 "name".to_string(),
1011 serde_json::Value::String(effective_name),
1012 );
1013 node_obj.insert(
1014 "children".to_string(),
1015 serde_json::Value::Array(processed_children),
1016 );
1017 } else {
1018 node_obj.insert(
1020 "name".to_string(),
1021 serde_json::Value::String(effective_name),
1022 );
1023 }
1024
1025 serde_json::Value::Object(node_obj)
1026 }
1027
1028 fn extract_file_path(filename: &str) -> String {
1030 if filename.is_empty() {
1031 return "unknown".to_string();
1032 }
1033
1034 let parts: Vec<&str> = filename.split('/').collect();
1036 let file_name = parts.last().unwrap_or(&"unknown");
1037
1038 if file_name.ends_with(".rs") && parts.len() > 1 {
1040 let parent_dir = parts[parts.len() - 2];
1041 format!("{}/{}", parent_dir, file_name)
1042 } else {
1043 file_name.to_string()
1044 }
1045 }
1046}
1047
1048pub fn hydro_ir_to_json(
1050 ir: &[HydroRoot],
1051 process_names: Vec<(usize, String)>,
1052 cluster_names: Vec<(usize, String)>,
1053 external_names: Vec<(usize, String)>,
1054) -> Result<String, Box<dyn std::error::Error>> {
1055 let mut output = String::new();
1056
1057 let config = super::render::HydroWriteConfig {
1058 show_metadata: false,
1059 show_location_groups: true,
1060 use_short_labels: true, process_id_name: process_names,
1062 cluster_id_name: cluster_names,
1063 external_id_name: external_names,
1064 };
1065
1066 super::render::write_hydro_ir_json(&mut output, ir, &config)?;
1067
1068 Ok(output)
1069}
1070
1071pub fn open_json_browser(
1073 ir: &[HydroRoot],
1074 process_names: Vec<(usize, String)>,
1075 cluster_names: Vec<(usize, String)>,
1076 external_names: Vec<(usize, String)>,
1077) -> Result<(), Box<dyn std::error::Error>> {
1078 let config = super::render::HydroWriteConfig {
1079 process_id_name: process_names,
1080 cluster_id_name: cluster_names,
1081 external_id_name: external_names,
1082 ..Default::default()
1083 };
1084
1085 super::debug::open_json_visualizer(ir, Some(config))
1086 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
1087}
1088
1089pub fn save_json(
1091 ir: &[HydroRoot],
1092 process_names: Vec<(usize, String)>,
1093 cluster_names: Vec<(usize, String)>,
1094 external_names: Vec<(usize, String)>,
1095 filename: &str,
1096) -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
1097 let config = super::render::HydroWriteConfig {
1098 process_id_name: process_names,
1099 cluster_id_name: cluster_names,
1100 external_id_name: external_names,
1101 ..Default::default()
1102 };
1103
1104 super::debug::save_json(ir, Some(filename), Some(config))
1105 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
1106}
1107
1108#[cfg(feature = "build")]
1110pub fn open_browser(
1111 built_flow: &crate::compile::built::BuiltFlow,
1112) -> Result<(), Box<dyn std::error::Error>> {
1113 open_json_browser(
1114 built_flow.ir(),
1115 built_flow.process_id_name().clone(),
1116 built_flow.cluster_id_name().clone(),
1117 built_flow.external_id_name().clone(),
1118 )
1119}