Skip to main content

qlog_dancer/plots/
mod.rs

1// Copyright (C) 2025, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27use crate::datastore::ApplicationProto;
28use crate::datastore::Datastore;
29use colors::PlotColors;
30use plotters::coord::ranged1d::ValueFormatter;
31use plotters::coord::types::RangedCoordf64;
32use plotters::coord::types::RangedCoordu64;
33use plotters::prelude::*;
34#[cfg(target_arch = "wasm32")]
35use plotters_canvas::CanvasBackend;
36
37#[derive(Clone, Debug)]
38pub enum ChartOutputType {
39    Png {
40        output_dir: String,
41        cwnd_y_max: Option<u64>,
42        stream_y_max: Option<u64>,
43    },
44
45    Canvas {
46        canvas_id: String,
47    },
48}
49
50#[derive(Clone, Debug)]
51struct ChartConfig {
52    pub title: String,
53    pub input_filename: String,
54    // pub size: ChartSize,
55    // pub margin: ChartMargin,
56    // pub colors: PlotColors,
57    pub clamp: ClampParams,
58    pub ty: ChartOutputType,
59    pub app_proto: ApplicationProto,
60    pub host: Option<String>,
61    pub session_id: Option<i64>,
62}
63
64impl ChartConfig {
65    pub fn chart_name(&self) -> String {
66        let proto = match self.app_proto {
67            ApplicationProto::Http2 => "h2",
68            ApplicationProto::Http3 => "h3",
69        };
70
71        let host = match (&self.host, &self.ty) {
72            (Some(h), ChartOutputType::Png { .. }) => {
73                // Hosts might include a port number and putting a colon in
74                // filenames might upset some systems.
75                format!("-{}", h.replace(":", "_"))
76            },
77
78            (Some(h), ChartOutputType::Canvas { .. }) => h.to_string(),
79
80            _ => "".to_string(),
81        };
82
83        let session = if let Some(s) = self.session_id {
84            format!("-session{}", s)
85        } else {
86            "".to_string()
87        };
88
89        let start = if let Some(s) = self.clamp.start {
90            format!("-s{}", s)
91        } else {
92            "".to_string()
93        };
94
95        let end = if let Some(e) = self.clamp.end {
96            format!("-e{}", e)
97        } else {
98            "".to_string()
99        };
100
101        match self.ty {
102            ChartOutputType::Png {
103                cwnd_y_max,
104                stream_y_max,
105                ..
106            } => {
107                let mut name = format!(
108                    "{}-{}{}{}-{}{}{}",
109                    self.input_filename,
110                    proto,
111                    host,
112                    session,
113                    self.title,
114                    start,
115                    end,
116                );
117
118                if let Some(y_max) = stream_y_max {
119                    name.push_str(&format!("-stream_max_y{}", y_max));
120                }
121
122                if let Some(y_max) = cwnd_y_max {
123                    name.push_str(&format!("-cwnd_max_y{}", y_max));
124                }
125
126                name
127            },
128
129            ChartOutputType::Canvas { .. } => format!(
130                "{}-{}{}{}-{}{}{}",
131                self.input_filename, proto, host, session, self.title, start, end
132            ),
133        }
134    }
135
136    pub fn chart_filepath(&self) -> String {
137        match &self.ty {
138            ChartOutputType::Png { output_dir, .. } => {
139                format!("{}/{}.png", output_dir, self.chart_name())
140            },
141
142            // Canvas doesn't have a file
143            ChartOutputType::Canvas { .. } => self.chart_name(),
144        }
145    }
146
147    fn init_chart_dir(&self) {
148        match &self.ty {
149            ChartOutputType::Png { .. } => {
150                let output_filename = self.chart_filepath();
151                let path = std::path::Path::new(&output_filename);
152                if let Some(dir) = path.parent() {
153                    std::fs::create_dir_all(dir).unwrap();
154                }
155            },
156
157            // Canvas doesn't have a file
158            ChartOutputType::Canvas { .. } => (),
159        }
160    }
161
162    #[cfg(target_arch = "wasm32")]
163    fn canvas_id(&self) -> Option<String> {
164        match &self.ty {
165            ChartOutputType::Canvas { canvas_id } => Some(canvas_id.clone()),
166            ChartOutputType::Png { .. } => None,
167        }
168    }
169}
170
171#[cfg(not(target_arch = "wasm32"))]
172pub fn make_chart_bitmap_area(
173    path: &str, size: ChartSize, colors: PlotColors, margin: ChartMargin,
174) -> DrawingArea<plotters::prelude::BitMapBackend<'_>, plotters::coord::Shift> {
175    let backend = BitMapBackend::new(path, (size.width, size.height));
176    let area = backend.into_drawing_area();
177    area.fill(&colors.fill).unwrap();
178    area.margin(margin.top, margin.bottom, margin.left, margin.right)
179}
180
181#[cfg(target_arch = "wasm32")]
182pub fn make_chart_canvas_area(
183    canvas_id: &str, colors: PlotColors, margin: ChartMargin,
184) -> plotters::drawing::DrawingArea<CanvasBackend, plotters::coord::Shift> {
185    let backend = CanvasBackend::new(canvas_id)
186        .unwrap_or_else(|| panic!("cannot find canvas {}", canvas_id));
187    let area = backend.into_drawing_area();
188    area.fill(&colors.fill).unwrap();
189    area.margin(margin.top, margin.bottom, margin.left, margin.right)
190}
191
192fn make_chart_config(
193    title: &str, params: &PlotParameters, filename: &str, ds: &Datastore,
194    ty: &ChartOutputType,
195) -> ChartConfig {
196    let chart_config = ChartConfig {
197        title: title.into(),
198        input_filename: filename.into(),
199        clamp: params.clamp.clone(),
200        app_proto: ds.application_proto,
201        host: ds.host.clone(),
202        session_id: ds.session_id,
203        ty: ty.clone(),
204    };
205
206    chart_config.init_chart_dir();
207
208    chart_config
209}
210
211pub fn chart_title_style(color: &RGBColor) -> TextStyle<'_> {
212    TextStyle::from(("sans-serif", 40).into_font()).color(color)
213}
214
215pub fn chart_subtitle_style(color: &RGBColor) -> TextStyle<'_> {
216    TextStyle::from(("sans-serif", 20).into_font()).color(color)
217}
218
219pub fn chart_label_style(color: &RGBColor) -> TextStyle<'_> {
220    TextStyle::from(("sans-serif", 15).into_font()).color(color)
221}
222
223fn draw_mesh<XT, YT, X, Y, DB: DrawingBackend>(
224    colors: &PlotColors, x_desc: &str, y_desc: &str, draw_minor_lines: bool,
225    chart: &mut ChartContext<DB, Cartesian2d<X, Y>>,
226) where
227    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
228    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
229{
230    let mut mesh = chart.configure_mesh();
231
232    mesh.axis_style(colors.axis)
233        .x_desc(x_desc)
234        .y_desc(y_desc)
235        .bold_line_style(colors.bold_line.mix(0.5));
236
237    // TODO: bizarre! From the docs, state the correct way to do this is to set
238    // number of light lines to zero. However, that causes the drawing to spin
239    // indefinatly. Perhaps some bug in plotters...
240    if !draw_minor_lines {
241        mesh.light_line_style(colors.bold_line.mix(0.0));
242        // mesh.x_max_light_lines(0);
243        // mesh.y_max_light_lines(0);
244    } else {
245        mesh.light_line_style(colors.light_line.mix(0.2));
246    }
247
248    mesh.label_style(chart_label_style(&colors.caption))
249        .draw()
250        .unwrap();
251}
252
253#[derive(Clone, Default, Debug)]
254pub struct ClampParams {
255    pub start: Option<f64>,
256    pub end: Option<f64>,
257    pub stream_y_max: Option<u64>,
258}
259
260#[derive(Clone, Copy, Debug, Default)]
261pub struct ChartSize {
262    pub width: u32,
263    pub height: u32,
264}
265
266#[derive(Clone, Copy, Debug, Default)]
267pub struct ChartMargin {
268    pub top: u32,
269    pub bottom: u32,
270    pub left: u32,
271    pub right: u32,
272}
273
274#[derive(Clone, Copy, Debug, Default)]
275pub struct AreaMargin {
276    pub x: u32,
277    pub y: u32,
278}
279
280pub struct PlotParameters {
281    pub clamp: ClampParams,
282    pub cwnd_y_max: Option<u64>,
283    pub chart_size: ChartSize,
284    pub colors: PlotColors,
285    pub chart_margin: ChartMargin,
286    pub area_margin: AreaMargin,
287    pub display_chart_title: bool,
288    pub display_legend: bool,
289    pub display_minor_lines: bool,
290}
291
292fn draw_line<DB: DrawingBackend>(
293    data: &[(f64, u64)], label: Option<&str>, colour: RGBColor,
294    chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
295) {
296    let c = chart
297        .draw_series(LineSeries::new(data.to_vec(), colour))
298        .unwrap();
299
300    if let Some(l) = label {
301        c.label(l).legend(move |(x, y)| {
302            PathElement::new(vec![(x, y), (x + 20, y)], colour)
303        });
304    }
305}
306
307pub mod colors;
308pub mod congestion_control;
309pub mod conn_flow_control;
310pub mod conn_overview;
311pub mod minmax;
312pub mod packet_received;
313pub mod packet_sent;
314pub mod pending;
315pub mod rtt;
316pub mod stream_multiplex;
317pub mod stream_sparks;