Skip to main content

ppt_rs/generator/
memory_profile.rs

1//! Memory and performance profiling helpers for large presentations
2
3use std::time::{Duration, Instant};
4
5use super::slide_content::SlideContent;
6use super::{create_pptx_lazy_to_writer, create_pptx_with_content, LazySlideSource};
7use std::io::Cursor;
8
9/// Metrics collected while generating a presentation.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct GenerationMetrics {
12    pub slide_count: usize,
13    pub output_bytes: usize,
14    pub slide_payload_bytes: usize,
15    pub duration: Duration,
16}
17
18impl GenerationMetrics {
19    /// Output size per slide in bytes.
20    pub fn bytes_per_slide(&self) -> usize {
21        if self.slide_count == 0 {
22            0
23        } else {
24            self.output_bytes / self.slide_count
25        }
26    }
27}
28
29/// Estimate in-memory slide payload size from slide text and notes.
30pub fn estimate_slide_payload(slides: &[SlideContent]) -> usize {
31    slides.iter().fold(0, |total, slide| {
32        let title_bytes = slide.title.len();
33        let bullet_bytes: usize = slide.bullets.iter().map(|b| b.text.len()).sum();
34        let notes_bytes = slide.notes.as_ref().map(|n| n.len()).unwrap_or(0);
35        total + title_bytes + bullet_bytes + notes_bytes
36    })
37}
38
39/// Pre-size the output ZIP buffer to reduce reallocations during generation.
40pub fn estimate_output_capacity(slide_count: usize, slides: Option<&[SlideContent]>) -> usize {
41    const BASE_BYTES: usize = 32_768;
42    const PER_SLIDE_BYTES: usize = 4_096;
43
44    let mut cap = BASE_BYTES + slide_count.saturating_mul(PER_SLIDE_BYTES);
45
46    if let Some(slides) = slides {
47        cap += estimate_slide_payload(slides);
48        for slide in slides {
49            cap += slide.charts.len().saturating_mul(8_192);
50            for image in &slide.images {
51                if let Some(bytes) = image.get_bytes() {
52                    cap += bytes.len();
53                }
54            }
55        }
56    }
57
58    cap.max(8_192)
59}
60
61/// Profile an eager (all-slides-in-memory) generation path.
62pub fn profile_eager_generation(title: &str, slides: Vec<SlideContent>) -> GenerationMetrics {
63    let slide_count = slides.len();
64    let slide_payload_bytes = estimate_slide_payload(&slides);
65    let start = Instant::now();
66    let output = create_pptx_with_content(title, slides).expect("pptx generation");
67    let duration = start.elapsed();
68
69    GenerationMetrics {
70        slide_count,
71        output_bytes: output.len(),
72        slide_payload_bytes,
73        duration,
74    }
75}
76
77struct IndexedSlideSource {
78    slides: Vec<SlideContent>,
79}
80
81impl LazySlideSource for IndexedSlideSource {
82    fn slide_count(&self) -> usize {
83        self.slides.len()
84    }
85
86    fn generate_slide(&self, index: usize) -> Option<SlideContent> {
87        self.slides.get(index).cloned()
88    }
89}
90
91/// Profile the lazy slide loading generation path.
92pub fn profile_lazy_generation(title: &str, slides: Vec<SlideContent>) -> GenerationMetrics {
93    let slide_count = slides.len();
94    let slide_payload_bytes = estimate_slide_payload(&slides);
95    let start = Instant::now();
96    let cursor = Cursor::new(Vec::new());
97    let source = Box::new(IndexedSlideSource { slides });
98    let output = create_pptx_lazy_to_writer(cursor, title, source, None)
99        .expect("lazy pptx generation")
100        .into_inner();
101    let duration = start.elapsed();
102
103    GenerationMetrics {
104        slide_count,
105        output_bytes: output.len(),
106        slide_payload_bytes,
107        duration,
108    }
109}
110
111/// Build simple title/bullet slides for profiling.
112pub fn sample_slides(count: usize) -> Vec<SlideContent> {
113    (0..count)
114        .map(|index| {
115            let title = format!("Slide {index}");
116            let bullet_a = format!("Bullet A on slide {index}");
117            let bullet_b = format!("Bullet B on slide {index}");
118            SlideContent::new(&title)
119                .add_bullet(&bullet_a)
120                .add_bullet(&bullet_b)
121        })
122        .collect()
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_estimate_slide_payload() {
131        let slides = sample_slides(2);
132        let bytes = estimate_slide_payload(&slides);
133        assert!(bytes > 0);
134    }
135
136    #[test]
137    fn test_profile_eager_10_slides() {
138        let metrics = profile_eager_generation("Profile", sample_slides(10));
139        assert_eq!(metrics.slide_count, 10);
140        assert!(metrics.output_bytes > 1_000);
141        assert!(metrics.duration < Duration::from_secs(5));
142    }
143}