1use crate::{ProfileEvent, TorshResult};
4use serde_json::{json, Value};
5use std::collections::HashMap;
6use std::fs::File;
7use std::io::Write;
8use torsh_core::TorshError;
9
10#[derive(Debug, Clone)]
12pub struct CustomExportFormat {
13 pub name: String,
14 pub description: String,
15 pub file_extension: String,
16 pub schema: ExportSchema,
17}
18
19#[derive(Debug, Clone)]
21pub enum ExportSchema {
22 Json {
24 field_mapping: HashMap<String, String>,
25 include_metadata: bool,
26 pretty_print: bool,
27 },
28 Csv {
30 columns: Vec<CsvColumn>,
31 delimiter: char,
32 include_header: bool,
33 },
34 Xml {
36 root_element: String,
37 event_element: String,
38 field_mapping: HashMap<String, String>,
39 },
40 Text { template: String, separator: String },
42}
43
44#[derive(Debug, Clone)]
46pub struct CsvColumn {
47 pub name: String,
48 pub field: String,
49 pub formatter: Option<CsvFormatter>,
50}
51
52#[derive(Debug, Clone)]
54pub enum CsvFormatter {
55 Duration(DurationFormat),
56 Memory(MemoryFormat),
57 Number(NumberFormat),
58 Text(TextFormat),
59}
60
61#[derive(Debug, Clone)]
62pub enum DurationFormat {
63 Microseconds,
64 Milliseconds,
65 Seconds,
66 HumanReadable,
67}
68
69#[derive(Debug, Clone)]
70pub enum MemoryFormat {
71 Bytes,
72 Kilobytes,
73 Megabytes,
74 Gigabytes,
75 HumanReadable,
76}
77
78#[derive(Debug, Clone)]
79pub enum NumberFormat {
80 Default,
81 Scientific,
82 Percentage,
83 WithCommas,
84}
85
86#[derive(Debug, Clone)]
87pub enum TextFormat {
88 Default,
89 Uppercase,
90 Lowercase,
91 Truncate(usize),
92}
93
94#[derive(Debug, Clone)]
96pub struct CustomExporter {
97 formats: HashMap<String, CustomExportFormat>,
98}
99
100impl CustomExporter {
101 pub fn new() -> Self {
103 let mut exporter = Self {
104 formats: HashMap::new(),
105 };
106
107 exporter.register_default_formats();
109 exporter
110 }
111
112 pub fn register_format(&mut self, format: CustomExportFormat) {
114 self.formats.insert(format.name.clone(), format);
115 }
116
117 pub fn get_format_names(&self) -> Vec<String> {
119 self.formats.keys().cloned().collect()
120 }
121
122 pub fn export(
124 &self,
125 events: &[ProfileEvent],
126 format_name: &str,
127 path: &str,
128 ) -> TorshResult<()> {
129 let format = self.formats.get(format_name).ok_or_else(|| {
130 TorshError::InvalidArgument(format!("Unknown export format: {format_name}"))
131 })?;
132
133 match &format.schema {
134 ExportSchema::Json {
135 field_mapping,
136 include_metadata,
137 pretty_print,
138 } => self.export_json(
139 events,
140 field_mapping,
141 *include_metadata,
142 *pretty_print,
143 path,
144 ),
145 ExportSchema::Csv {
146 columns,
147 delimiter,
148 include_header,
149 } => self.export_csv(events, columns, *delimiter, *include_header, path),
150 ExportSchema::Xml {
151 root_element,
152 event_element,
153 field_mapping,
154 } => self.export_xml(events, root_element, event_element, field_mapping, path),
155 ExportSchema::Text {
156 template,
157 separator,
158 } => self.export_text(events, template, separator, path),
159 }
160 }
161
162 fn register_default_formats(&mut self) {
164 let compact_json = CustomExportFormat {
166 name: "compact_json".to_string(),
167 description: "Compact JSON format with minimal fields".to_string(),
168 file_extension: "json".to_string(),
169 schema: ExportSchema::Json {
170 field_mapping: [
171 ("name".to_string(), "n".to_string()),
172 ("duration_us".to_string(), "d".to_string()),
173 ("category".to_string(), "c".to_string()),
174 ]
175 .iter()
176 .cloned()
177 .collect(),
178 include_metadata: false,
179 pretty_print: false,
180 },
181 };
182 self.register_format(compact_json);
183
184 let perf_csv = CustomExportFormat {
186 name: "performance_csv".to_string(),
187 description: "CSV focused on performance metrics".to_string(),
188 file_extension: "csv".to_string(),
189 schema: ExportSchema::Csv {
190 columns: vec![
191 CsvColumn {
192 name: "Event".to_string(),
193 field: "name".to_string(),
194 formatter: None,
195 },
196 CsvColumn {
197 name: "Duration (ms)".to_string(),
198 field: "duration_us".to_string(),
199 formatter: Some(CsvFormatter::Duration(DurationFormat::Milliseconds)),
200 },
201 CsvColumn {
202 name: "FLOPS".to_string(),
203 field: "flops".to_string(),
204 formatter: Some(CsvFormatter::Number(NumberFormat::WithCommas)),
205 },
206 CsvColumn {
207 name: "Bandwidth (MB)".to_string(),
208 field: "bytes_transferred".to_string(),
209 formatter: Some(CsvFormatter::Memory(MemoryFormat::Megabytes)),
210 },
211 ],
212 delimiter: ',',
213 include_header: true,
214 },
215 };
216 self.register_format(perf_csv);
217
218 let simple_text = CustomExportFormat {
220 name: "simple_text".to_string(),
221 description: "Simple text format for quick viewing".to_string(),
222 file_extension: "txt".to_string(),
223 schema: ExportSchema::Text {
224 template: "{name}: {duration_us}μs ({category})".to_string(),
225 separator: "\n".to_string(),
226 },
227 };
228 self.register_format(simple_text);
229 }
230
231 fn export_json(
233 &self,
234 events: &[ProfileEvent],
235 field_mapping: &HashMap<String, String>,
236 include_metadata: bool,
237 pretty_print: bool,
238 path: &str,
239 ) -> TorshResult<()> {
240 let mut mapped_events = Vec::new();
241
242 for event in events {
243 let mut mapped_event = serde_json::Map::new();
244
245 self.map_field(&mut mapped_event, "name", &event.name, field_mapping);
247 self.map_field(
248 &mut mapped_event,
249 "category",
250 &event.category,
251 field_mapping,
252 );
253 self.map_field(
254 &mut mapped_event,
255 "start_us",
256 &event.start_us,
257 field_mapping,
258 );
259 self.map_field(
260 &mut mapped_event,
261 "duration_us",
262 &event.duration_us,
263 field_mapping,
264 );
265 self.map_field(
266 &mut mapped_event,
267 "thread_id",
268 &event.thread_id,
269 field_mapping,
270 );
271
272 if let Some(ops) = event.operation_count {
273 self.map_field(&mut mapped_event, "operation_count", &ops, field_mapping);
274 }
275 if let Some(flops) = event.flops {
276 self.map_field(&mut mapped_event, "flops", &flops, field_mapping);
277 }
278 if let Some(bytes) = event.bytes_transferred {
279 self.map_field(
280 &mut mapped_event,
281 "bytes_transferred",
282 &bytes,
283 field_mapping,
284 );
285 }
286 if let Some(ref stack_trace) = event.stack_trace {
287 self.map_field(&mut mapped_event, "stack_trace", stack_trace, field_mapping);
288 }
289
290 mapped_events.push(Value::Object(mapped_event));
291 }
292
293 let mut output = serde_json::Map::new();
294 output.insert("events".to_string(), Value::Array(mapped_events));
295
296 if include_metadata {
297 let metadata = json!({
298 "export_timestamp": chrono::Utc::now().to_rfc3339(),
299 "event_count": events.len(),
300 "format": "custom_json"
301 });
302 output.insert("metadata".to_string(), metadata);
303 }
304
305 let json_output = Value::Object(output);
306 let mut file = File::create(path).map_err(|e| {
307 TorshError::InvalidArgument(format!("Failed to create file {path}: {e}"))
308 })?;
309
310 let json_string = if pretty_print {
311 serde_json::to_string_pretty(&json_output)
312 } else {
313 serde_json::to_string(&json_output)
314 }
315 .map_err(|e| TorshError::InvalidArgument(format!("Failed to serialize JSON: {e}")))?;
316
317 file.write_all(json_string.as_bytes())
318 .map_err(|e| TorshError::InvalidArgument(format!("Failed to write file: {e}")))?;
319
320 Ok(())
321 }
322
323 fn export_csv(
325 &self,
326 events: &[ProfileEvent],
327 columns: &[CsvColumn],
328 delimiter: char,
329 include_header: bool,
330 path: &str,
331 ) -> TorshResult<()> {
332 let mut file = File::create(path).map_err(|e| {
333 TorshError::InvalidArgument(format!("Failed to create file {path}: {e}"))
334 })?;
335
336 if include_header {
338 let header: Vec<String> = columns.iter().map(|col| col.name.clone()).collect();
339 writeln!(file, "{}", header.join(&delimiter.to_string()))
340 .map_err(|e| TorshError::InvalidArgument(format!("Failed to write header: {e}")))?;
341 }
342
343 for event in events {
345 let row: Vec<String> = columns
346 .iter()
347 .map(|col| self.format_field_value(event, &col.field, &col.formatter))
348 .collect();
349 writeln!(file, "{}", row.join(&delimiter.to_string()))
350 .map_err(|e| TorshError::InvalidArgument(format!("Failed to write row: {e}")))?;
351 }
352
353 Ok(())
354 }
355
356 fn export_xml(
358 &self,
359 events: &[ProfileEvent],
360 root_element: &str,
361 event_element: &str,
362 field_mapping: &HashMap<String, String>,
363 path: &str,
364 ) -> TorshResult<()> {
365 let mut file = File::create(path).map_err(|e| {
366 TorshError::InvalidArgument(format!("Failed to create file {path}: {e}"))
367 })?;
368
369 writeln!(file, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
370 .map_err(|e| TorshError::InvalidArgument(format!("Failed to write XML header: {e}")))?;
371
372 writeln!(file, "<{root_element}>").map_err(|e| {
373 TorshError::InvalidArgument(format!("Failed to write root element: {e}"))
374 })?;
375
376 for event in events {
377 writeln!(file, " <{event_element}>").map_err(|e| {
378 TorshError::InvalidArgument(format!("Failed to write event element: {e}"))
379 })?;
380
381 self.write_xml_field(&mut file, "name", &event.name, field_mapping)?;
382 self.write_xml_field(&mut file, "category", &event.category, field_mapping)?;
383 self.write_xml_field(
384 &mut file,
385 "start_us",
386 &event.start_us.to_string(),
387 field_mapping,
388 )?;
389 self.write_xml_field(
390 &mut file,
391 "duration_us",
392 &event.duration_us.to_string(),
393 field_mapping,
394 )?;
395 self.write_xml_field(
396 &mut file,
397 "thread_id",
398 &event.thread_id.to_string(),
399 field_mapping,
400 )?;
401
402 if let Some(ops) = event.operation_count {
403 self.write_xml_field(
404 &mut file,
405 "operation_count",
406 &ops.to_string(),
407 field_mapping,
408 )?;
409 }
410 if let Some(flops) = event.flops {
411 self.write_xml_field(&mut file, "flops", &flops.to_string(), field_mapping)?;
412 }
413 if let Some(bytes) = event.bytes_transferred {
414 self.write_xml_field(
415 &mut file,
416 "bytes_transferred",
417 &bytes.to_string(),
418 field_mapping,
419 )?;
420 }
421
422 writeln!(file, " </{event_element}>").map_err(|e| {
423 TorshError::InvalidArgument(format!("Failed to write closing event element: {e}"))
424 })?;
425 }
426
427 writeln!(file, "</{root_element}>").map_err(|e| {
428 TorshError::InvalidArgument(format!("Failed to write closing root element: {e}"))
429 })?;
430
431 Ok(())
432 }
433
434 fn export_text(
436 &self,
437 events: &[ProfileEvent],
438 template: &str,
439 separator: &str,
440 path: &str,
441 ) -> TorshResult<()> {
442 let mut file = File::create(path).map_err(|e| {
443 TorshError::InvalidArgument(format!("Failed to create file {path}: {e}"))
444 })?;
445
446 for (i, event) in events.iter().enumerate() {
447 let formatted = self.format_template(template, event);
448 write!(file, "{formatted}")
449 .map_err(|e| TorshError::InvalidArgument(format!("Failed to write event: {e}")))?;
450
451 if i < events.len() - 1 {
452 write!(file, "{separator}").map_err(|e| {
453 TorshError::InvalidArgument(format!("Failed to write separator: {e}"))
454 })?;
455 }
456 }
457
458 Ok(())
459 }
460
461 fn map_field<T: serde::Serialize>(
463 &self,
464 object: &mut serde_json::Map<String, Value>,
465 original_field: &str,
466 value: &T,
467 field_mapping: &HashMap<String, String>,
468 ) {
469 let field_name = field_mapping
470 .get(original_field)
471 .unwrap_or(&original_field.to_string())
472 .clone();
473 object.insert(field_name, json!(value));
474 }
475
476 fn write_xml_field(
478 &self,
479 file: &mut File,
480 original_field: &str,
481 value: &str,
482 field_mapping: &HashMap<String, String>,
483 ) -> TorshResult<()> {
484 let default_field = original_field.to_string();
485 let field_name = field_mapping.get(original_field).unwrap_or(&default_field);
486
487 writeln!(file, " <{field_name}>{value}</{field_name}>")
488 .map_err(|e| TorshError::InvalidArgument(format!("Failed to write XML field: {e}")))
489 }
490
491 fn format_field_value(
493 &self,
494 event: &ProfileEvent,
495 field: &str,
496 formatter: &Option<CsvFormatter>,
497 ) -> String {
498 let raw_value = match field {
499 "name" => event.name.clone(),
500 "category" => event.category.clone(),
501 "start_us" => event.start_us.to_string(),
502 "duration_us" => event.duration_us.to_string(),
503 "thread_id" => event.thread_id.to_string(),
504 "operation_count" => event
505 .operation_count
506 .map_or("".to_string(), |v| v.to_string()),
507 "flops" => event.flops.map_or("".to_string(), |v| v.to_string()),
508 "bytes_transferred" => event
509 .bytes_transferred
510 .map_or("".to_string(), |v| v.to_string()),
511 "stack_trace" => event.stack_trace.as_deref().unwrap_or("").to_string(),
512 _ => "".to_string(),
513 };
514
515 if let Some(formatter) = formatter {
516 self.apply_formatter(&raw_value, field, formatter)
517 } else {
518 raw_value
519 }
520 }
521
522 fn apply_formatter(&self, value: &str, _field: &str, formatter: &CsvFormatter) -> String {
524 match formatter {
525 CsvFormatter::Duration(format) => {
526 if let Ok(duration_us) = value.parse::<u64>() {
527 match format {
528 DurationFormat::Microseconds => format!("{duration_us}μs"),
529 DurationFormat::Milliseconds => {
530 format!("{:.3}ms", duration_us as f64 / 1000.0)
531 }
532 DurationFormat::Seconds => {
533 format!("{:.6}s", duration_us as f64 / 1_000_000.0)
534 }
535 DurationFormat::HumanReadable => {
536 if duration_us < 1000 {
537 format!("{duration_us}μs")
538 } else if duration_us < 1_000_000 {
539 format!("{:.2}ms", duration_us as f64 / 1000.0)
540 } else {
541 format!("{:.3}s", duration_us as f64 / 1_000_000.0)
542 }
543 }
544 }
545 } else {
546 value.to_string()
547 }
548 }
549 CsvFormatter::Memory(format) => {
550 if let Ok(bytes) = value.parse::<u64>() {
551 match format {
552 MemoryFormat::Bytes => format!("{bytes}B"),
553 MemoryFormat::Kilobytes => format!("{:.2}KB", bytes as f64 / 1024.0),
554 MemoryFormat::Megabytes => format!("{:.2}MB", bytes as f64 / 1_048_576.0),
555 MemoryFormat::Gigabytes => {
556 format!("{:.2}GB", bytes as f64 / 1_073_741_824.0)
557 }
558 MemoryFormat::HumanReadable => {
559 if bytes < 1024 {
560 format!("{bytes}B")
561 } else if bytes < 1_048_576 {
562 format!("{:.2}KB", bytes as f64 / 1024.0)
563 } else if bytes < 1_073_741_824 {
564 format!("{:.2}MB", bytes as f64 / 1_048_576.0)
565 } else {
566 format!("{:.2}GB", bytes as f64 / 1_073_741_824.0)
567 }
568 }
569 }
570 } else {
571 value.to_string()
572 }
573 }
574 CsvFormatter::Number(format) => {
575 if let Ok(num) = value.parse::<f64>() {
576 match format {
577 NumberFormat::Default => value.to_string(),
578 NumberFormat::Scientific => format!("{num:.2e}"),
579 NumberFormat::Percentage => format!("{:.2}%", num * 100.0),
580 NumberFormat::WithCommas => {
581 let parts: Vec<&str> = value.split('.').collect();
582 let integer_part = parts[0];
583 let mut formatted = String::new();
584 for (i, c) in integer_part.chars().rev().enumerate() {
585 if i > 0 && i % 3 == 0 {
586 formatted.insert(0, ',');
587 }
588 formatted.insert(0, c);
589 }
590 if parts.len() > 1 {
591 formatted.push('.');
592 formatted.push_str(parts[1]);
593 }
594 formatted
595 }
596 }
597 } else {
598 value.to_string()
599 }
600 }
601 CsvFormatter::Text(format) => match format {
602 TextFormat::Default => value.to_string(),
603 TextFormat::Uppercase => value.to_uppercase(),
604 TextFormat::Lowercase => value.to_lowercase(),
605 TextFormat::Truncate(len) => {
606 if value.len() > *len {
607 format!("{}...", &value[..*len])
608 } else {
609 value.to_string()
610 }
611 }
612 },
613 }
614 }
615
616 fn format_template(&self, template: &str, event: &ProfileEvent) -> String {
618 template
619 .replace("{name}", &event.name)
620 .replace("{category}", &event.category)
621 .replace("{start_us}", &event.start_us.to_string())
622 .replace("{duration_us}", &event.duration_us.to_string())
623 .replace("{thread_id}", &event.thread_id.to_string())
624 .replace(
625 "{operation_count}",
626 &event
627 .operation_count
628 .map_or("".to_string(), |v| v.to_string()),
629 )
630 .replace(
631 "{flops}",
632 &event.flops.map_or("".to_string(), |v| v.to_string()),
633 )
634 .replace(
635 "{bytes_transferred}",
636 &event
637 .bytes_transferred
638 .map_or("".to_string(), |v| v.to_string()),
639 )
640 .replace("{stack_trace}", event.stack_trace.as_deref().unwrap_or(""))
641 }
642}
643
644impl Default for CustomExporter {
645 fn default() -> Self {
646 Self::new()
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 fn create_test_event() -> ProfileEvent {
655 ProfileEvent {
656 name: "test_event".to_string(),
657 category: "test".to_string(),
658 start_us: 1000,
659 duration_us: 5000,
660 thread_id: 123,
661 operation_count: Some(100),
662 flops: Some(1000000),
663 bytes_transferred: Some(1024),
664 stack_trace: None,
665 }
666 }
667
668 #[test]
669 fn test_custom_exporter_creation() {
670 let exporter = CustomExporter::new();
671 let formats = exporter.get_format_names();
672
673 assert!(formats.contains(&"compact_json".to_string()));
674 assert!(formats.contains(&"performance_csv".to_string()));
675 assert!(formats.contains(&"simple_text".to_string()));
676 }
677
678 #[test]
679 fn test_custom_json_export() {
680 let exporter = CustomExporter::new();
681 let events = vec![create_test_event()];
682
683 let compact_path = std::env::temp_dir().join("test_compact.json");
684 let compact_str = compact_path.display().to_string();
685 let result = exporter.export(&events, "compact_json", &compact_str);
686 assert!(result.is_ok());
687
688 let _ = std::fs::remove_file(&compact_path);
690 }
691
692 #[test]
693 fn test_custom_csv_export() {
694 let exporter = CustomExporter::new();
695 let events = vec![create_test_event()];
696
697 let perf_path = std::env::temp_dir().join("test_perf.csv");
698 let perf_str = perf_path.display().to_string();
699 let result = exporter.export(&events, "performance_csv", &perf_str);
700 assert!(result.is_ok());
701
702 let _ = std::fs::remove_file(&perf_path);
704 }
705
706 #[test]
707 fn test_custom_text_export() {
708 let exporter = CustomExporter::new();
709 let events = vec![create_test_event()];
710
711 let simple_path = std::env::temp_dir().join("test_simple.txt");
712 let simple_str = simple_path.display().to_string();
713 let result = exporter.export(&events, "simple_text", &simple_str);
714 assert!(result.is_ok());
715
716 let _ = std::fs::remove_file(&simple_path);
718 }
719
720 #[test]
721 fn test_duration_formatting() {
722 let exporter = CustomExporter::new();
723
724 let microseconds = exporter.apply_formatter(
725 "5000",
726 "duration_us",
727 &CsvFormatter::Duration(DurationFormat::Microseconds),
728 );
729 assert_eq!(microseconds, "5000μs");
730
731 let milliseconds = exporter.apply_formatter(
732 "5000",
733 "duration_us",
734 &CsvFormatter::Duration(DurationFormat::Milliseconds),
735 );
736 assert_eq!(milliseconds, "5.000ms");
737
738 let human_readable = exporter.apply_formatter(
739 "5000",
740 "duration_us",
741 &CsvFormatter::Duration(DurationFormat::HumanReadable),
742 );
743 assert_eq!(human_readable, "5.00ms");
744 }
745
746 #[test]
747 fn test_memory_formatting() {
748 let exporter = CustomExporter::new();
749
750 let bytes = exporter.apply_formatter(
751 "1024",
752 "bytes_transferred",
753 &CsvFormatter::Memory(MemoryFormat::Bytes),
754 );
755 assert_eq!(bytes, "1024B");
756
757 let kilobytes = exporter.apply_formatter(
758 "1024",
759 "bytes_transferred",
760 &CsvFormatter::Memory(MemoryFormat::Kilobytes),
761 );
762 assert_eq!(kilobytes, "1.00KB");
763
764 let human_readable = exporter.apply_formatter(
765 "1048576",
766 "bytes_transferred",
767 &CsvFormatter::Memory(MemoryFormat::HumanReadable),
768 );
769 assert_eq!(human_readable, "1.00MB");
770 }
771
772 #[test]
773 fn test_register_custom_format() {
774 let mut exporter = CustomExporter::new();
775
776 let custom_format = CustomExportFormat {
777 name: "test_format".to_string(),
778 description: "Test format".to_string(),
779 file_extension: "test".to_string(),
780 schema: ExportSchema::Text {
781 template: "Event: {name}".to_string(),
782 separator: " | ".to_string(),
783 },
784 };
785
786 exporter.register_format(custom_format);
787 let formats = exporter.get_format_names();
788 assert!(formats.contains(&"test_format".to_string()));
789 }
790}