Skip to main content

qlog_dancer/plots/
conn_overview.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 full_palette::PURPLE_500;
28use minmax::XMinMax;
29use plotters::coord::types::RangedCoordf64;
30use plotters::coord::types::RangedCoordu64;
31use plotters::coord::Shift;
32
33use crate::plots::colors::*;
34use crate::plots::*;
35
36#[cfg(not(target_arch = "wasm32"))]
37use crate::datastore::Datastore;
38use crate::seriesstore::SeriesStore;
39
40#[cfg(not(target_arch = "wasm32"))]
41use super::congestion_control::draw_congestion_plot;
42#[cfg(not(target_arch = "wasm32"))]
43use super::rtt::draw_rtt_plot;
44
45struct XYMinMax {
46    pub x: XMinMax,
47    pub y_min: u64,
48    pub y_max: u64,
49}
50
51impl XYMinMax {
52    fn init(params: &PlotParameters, ss: &SeriesStore, y_max: u64) -> Self {
53        let x = XMinMax::new(
54            ss.sent_x_min,
55            ss.sent_x_max,
56            params.clamp.start,
57            params.clamp.end,
58        );
59
60        Self { x, y_min: 0, y_max }
61    }
62
63    fn y_range(&self) -> std::ops::Range<u64> {
64        self.y_min..self.y_max
65    }
66}
67
68#[derive(Clone, Debug)]
69pub enum OverviewChartOutputType {
70    Png {
71        output_dir: String,
72        cwnd_y_max: Option<u64>,
73        stream_y_max: Option<u64>,
74    },
75
76    Canvas {
77        main_plot_canvas_id: String,
78        congestion_plot_canvas_id: String,
79        rtt_plot_canvas_id: String,
80    },
81}
82
83impl From<OverviewChartOutputType> for ChartOutputType {
84    fn from(val: OverviewChartOutputType) -> Self {
85        match val {
86            OverviewChartOutputType::Png {
87                output_dir,
88                cwnd_y_max,
89                stream_y_max,
90            } => ChartOutputType::Png {
91                output_dir,
92                cwnd_y_max,
93                stream_y_max,
94            },
95
96            // Where we use this type in this module, this variant is a no-op,
97            // so we can put anything in the output TBH
98            OverviewChartOutputType::Canvas {
99                main_plot_canvas_id,
100                ..
101            } => ChartOutputType::Canvas {
102                canvas_id: main_plot_canvas_id,
103            },
104        }
105    }
106}
107
108fn draw_sent_max_data<DB: DrawingBackend>(
109    ss: &SeriesStore,
110    stream_chart: &mut ChartContext<
111        DB,
112        Cartesian2d<RangedCoordf64, RangedCoordu64>,
113    >,
114) {
115    draw_line(
116        &ss.sent_max_data,
117        Some("Sent MAX_DATA"),
118        BLACK,
119        stream_chart,
120    );
121}
122
123fn draw_cumulative_sent_max_data<DB: DrawingBackend>(
124    ss: &SeriesStore,
125    stream_chart: &mut ChartContext<
126        DB,
127        Cartesian2d<RangedCoordf64, RangedCoordu64>,
128    >,
129) {
130    draw_line(
131        &ss.sum_sent_stream_max_data,
132        Some("Cumulative sent MAX_STREAM_DATA"),
133        CYAN,
134        stream_chart,
135    );
136}
137
138fn draw_sent_max_stream_data<DB: DrawingBackend>(
139    ss: &SeriesStore,
140    stream_chart: &mut ChartContext<
141        DB,
142        Cartesian2d<RangedCoordf64, RangedCoordu64>,
143    >,
144) {
145    let mut label = Some("Sent MAX_STREAM_DATA");
146
147    let streams = &ss.sent_stream_max_data;
148    for series in streams {
149        draw_line(series.1, label, BLUE, stream_chart);
150        label = None;
151    }
152}
153
154fn draw_buffer_reads<DB: DrawingBackend>(
155    ss: &SeriesStore,
156    stream_chart: &mut ChartContext<
157        DB,
158        Cartesian2d<RangedCoordf64, RangedCoordu64>,
159    >,
160) {
161    let mut label = Some("Stream buffer read");
162
163    let streams = &ss.stream_buffer_reads;
164    for series in streams {
165        draw_line(series.1, label, FOREST_GREEN, stream_chart);
166        label = None;
167    }
168}
169
170fn draw_cumulative_buffer_reads<DB: DrawingBackend>(
171    ss: &SeriesStore,
172    stream_chart: &mut ChartContext<
173        DB,
174        Cartesian2d<RangedCoordf64, RangedCoordu64>,
175    >,
176) {
177    draw_line(
178        &ss.sum_stream_buffer_reads,
179        Some("Cumulative stream buffer read"),
180        GREEN,
181        stream_chart,
182    );
183}
184
185fn draw_buffer_writes<DB: DrawingBackend>(
186    ss: &SeriesStore,
187    stream_chart: &mut ChartContext<
188        DB,
189        Cartesian2d<RangedCoordf64, RangedCoordu64>,
190    >,
191) {
192    let mut label = Some("Stream buffer write");
193
194    let streams = &ss.stream_buffer_writes;
195    for series in streams {
196        draw_line(series.1, label, MAGENTA, stream_chart);
197        label = None;
198    }
199}
200
201fn draw_cumulative_buffer_writes<DB: DrawingBackend>(
202    ss: &SeriesStore,
203    stream_chart: &mut ChartContext<
204        DB,
205        Cartesian2d<RangedCoordf64, RangedCoordu64>,
206    >,
207) {
208    let data = &ss.sum_stream_buffer_writes;
209    let label = Some("Cumulative stream buffer writes");
210
211    draw_line(data, label, RGBColor(255, 0, 0), stream_chart);
212}
213
214fn draw_buffer_dropped<DB: DrawingBackend>(
215    ss: &SeriesStore,
216    stream_chart: &mut ChartContext<
217        DB,
218        Cartesian2d<RangedCoordf64, RangedCoordu64>,
219    >,
220) {
221    let mut label = Some("Stream buffer dropped");
222
223    let streams = &ss.stream_buffer_dropped;
224    for series in streams {
225        draw_line(series.1, label, ORANGE, stream_chart);
226        label = None;
227    }
228}
229
230fn draw_cumulative_buffer_dropped<DB: DrawingBackend>(
231    ss: &SeriesStore,
232    stream_chart: &mut ChartContext<
233        DB,
234        Cartesian2d<RangedCoordf64, RangedCoordu64>,
235    >,
236) {
237    let data = &ss.sum_stream_buffer_dropped;
238    let label = Some("Cumulative stream buffer dropped");
239
240    draw_line(data, label, RGBColor(0, 0, 255), stream_chart);
241}
242
243fn draw_sent_stream_data<DB: DrawingBackend>(
244    ss: &SeriesStore,
245    stream_chart: &mut ChartContext<
246        DB,
247        Cartesian2d<RangedCoordf64, RangedCoordu64>,
248    >,
249) {
250    let mut label = Some("Sent stream data");
251
252    let streams = &ss.sent_stream_frames_series;
253    for series in streams {
254        draw_line(series.1, label, PURPLE_500, stream_chart);
255        label = None;
256    }
257}
258
259fn draw_received_max_data<DB: DrawingBackend>(
260    ss: &SeriesStore,
261    stream_chart: &mut ChartContext<
262        DB,
263        Cartesian2d<RangedCoordf64, RangedCoordu64>,
264    >,
265) {
266    draw_line(
267        &ss.received_max_data,
268        Some("Cumulative received MAX_DATA"),
269        MID_GREY,
270        stream_chart,
271    );
272}
273
274fn draw_cumulative_received_stream_max_data<DB: DrawingBackend>(
275    ss: &SeriesStore,
276    stream_chart: &mut ChartContext<
277        DB,
278        Cartesian2d<RangedCoordf64, RangedCoordu64>,
279    >,
280) {
281    draw_line(
282        &ss.sum_received_stream_max_data,
283        Some("Cumulative received MAX_STREAM_DATA"),
284        MUSTARD,
285        stream_chart,
286    );
287}
288
289#[cfg(target_arch = "wasm32")]
290fn draw_main_plot<'a, DB: DrawingBackend + 'a>(
291    filename: &str, params: &PlotParameters, axis: XYMinMax, ss: &SeriesStore,
292    plot: &plotters::drawing::DrawingArea<DB, Shift>,
293) -> ChartContext<'a, DB, Cartesian2d<RangedCoordf64, RangedCoordu64>> {
294    let mut builder = ChartBuilder::on(plot);
295    builder
296        .x_label_area_size(params.area_margin.x)
297        .y_label_area_size(params.area_margin.y);
298
299    if params.display_chart_title {
300        let caption = format!("{} Connection overview", filename);
301        builder.caption(caption, chart_title_style(&params.colors.caption));
302    }
303
304    let mut chart = builder
305        .build_cartesian_2d(axis.x.range(), axis.y_range())
306        .unwrap();
307
308    draw_mesh(
309        &params.colors,
310        "Relative time (ms)",
311        "Data (bytes)",
312        params.display_minor_lines,
313        &mut chart,
314    );
315
316    draw_sent_max_data(ss, &mut chart);
317    draw_cumulative_sent_max_data(ss, &mut chart);
318    draw_sent_max_stream_data(ss, &mut chart);
319    draw_buffer_reads(ss, &mut chart);
320    draw_cumulative_buffer_reads(ss, &mut chart);
321    draw_buffer_writes(ss, &mut chart);
322    draw_cumulative_buffer_writes(ss, &mut chart);
323    draw_buffer_dropped(ss, &mut chart);
324    draw_cumulative_buffer_dropped(ss, &mut chart);
325    draw_sent_stream_data(ss, &mut chart);
326    draw_received_max_data(ss, &mut chart);
327    draw_cumulative_received_stream_max_data(ss, &mut chart);
328
329    if params.display_legend {
330        chart
331            .configure_series_labels()
332            .label_font(chart_label_style(&params.colors.caption))
333            .background_style(params.colors.fill.mix(0.8))
334            .border_style(params.colors.axis)
335            .position(SeriesLabelPosition::UpperLeft)
336            .draw()
337            .unwrap();
338    }
339
340    chart
341}
342
343fn draw_stream_send_plot<'a, DB: DrawingBackend + 'a>(
344    params: &PlotParameters, axis: XYMinMax, ss: &SeriesStore,
345    plot: &plotters::drawing::DrawingArea<DB, Shift>,
346) -> ChartContext<'a, DB, Cartesian2d<RangedCoordf64, RangedCoordu64>> {
347    let mut builder = ChartBuilder::on(plot);
348    builder
349        .x_label_area_size(params.area_margin.x)
350        .y_label_area_size(params.area_margin.y);
351
352    if params.display_chart_title {
353        builder.caption(
354            "Stream sends",
355            chart_subtitle_style(&params.colors.caption),
356        );
357    }
358
359    let mut chart = builder
360        .build_cartesian_2d(axis.x.range(), axis.y_range())
361        .unwrap();
362
363    draw_mesh(
364        &params.colors,
365        "Relative time (ms)",
366        "Data (bytes)",
367        false,
368        &mut chart,
369    );
370
371    draw_cumulative_buffer_writes(ss, &mut chart);
372    draw_cumulative_buffer_dropped(ss, &mut chart);
373    draw_buffer_writes(ss, &mut chart);
374    draw_buffer_dropped(ss, &mut chart);
375    draw_sent_stream_data(ss, &mut chart);
376    draw_received_max_data(ss, &mut chart);
377    draw_cumulative_received_stream_max_data(ss, &mut chart);
378
379    if params.display_legend {
380        chart
381            .configure_series_labels()
382            .label_font(chart_label_style(&params.colors.caption))
383            .background_style(params.colors.fill.mix(0.8))
384            .border_style(params.colors.axis)
385            .position(SeriesLabelPosition::UpperLeft)
386            .draw()
387            .unwrap();
388    }
389
390    chart
391}
392
393fn draw_stream_recv_plot<'a, DB: DrawingBackend + 'a>(
394    params: &PlotParameters, axis: XYMinMax, ss: &SeriesStore,
395    plot: &plotters::drawing::DrawingArea<DB, Shift>,
396) -> ChartContext<'a, DB, Cartesian2d<RangedCoordf64, RangedCoordu64>> {
397    let mut builder = ChartBuilder::on(plot);
398    builder
399        .x_label_area_size(params.area_margin.x)
400        .y_label_area_size(params.area_margin.y);
401
402    if params.display_chart_title {
403        builder.caption(
404            "Stream receives",
405            chart_subtitle_style(&params.colors.caption),
406        );
407    }
408
409    let mut chart = builder
410        .build_cartesian_2d(axis.x.range(), axis.y_range())
411        .unwrap();
412
413    draw_mesh(
414        &params.colors,
415        "Relative time (ms)",
416        "Data (bytes)",
417        false,
418        &mut chart,
419    );
420
421    draw_sent_max_data(ss, &mut chart);
422    draw_cumulative_sent_max_data(ss, &mut chart);
423    draw_sent_max_stream_data(ss, &mut chart);
424    draw_buffer_reads(ss, &mut chart);
425    draw_cumulative_buffer_reads(ss, &mut chart);
426
427    if params.display_legend {
428        chart
429            .configure_series_labels()
430            .label_font(chart_label_style(&params.colors.caption))
431            .background_style(params.colors.fill.mix(0.8))
432            .border_style(params.colors.axis)
433            .position(SeriesLabelPosition::UpperLeft)
434            .draw()
435            .unwrap();
436    }
437
438    chart
439}
440
441#[cfg(not(target_arch = "wasm32"))]
442pub fn plot_connection_overview(
443    params: &PlotParameters, filename: &str, ss: &SeriesStore, ds: &Datastore,
444    ty: &OverviewChartOutputType,
445) {
446    let chart_config = ChartConfig {
447        title: "conn-overview".into(),
448        input_filename: filename.into(),
449        clamp: params.clamp.clone(),
450        app_proto: ds.application_proto,
451        host: ds.host.clone(),
452        session_id: ds.session_id,
453        ty: ty.clone().into(),
454    };
455
456    chart_config.init_chart_dir();
457
458    let chart_path = chart_config.chart_filepath();
459
460    let root = make_chart_bitmap_area(
461        &chart_path,
462        params.chart_size,
463        params.colors,
464        params.chart_margin,
465    );
466
467    let (top_margin, bottom) = root.split_vertically((5).percent());
468    let (stream_plots, cwnd_rtt_area) = bottom.split_vertically((60).percent());
469    let (stream_send_plot, stream_recv_plot) =
470        stream_plots.split_vertically((50).percent());
471    let (congestion_plot, rtt_plot) =
472        cwnd_rtt_area.split_vertically((60).percent());
473
474    let stream_send_y_max = if let Some(y_max) = params.clamp.stream_y_max {
475        y_max
476    } else {
477        ss.y_max_stream_send_plot
478    };
479
480    let stream_recv_y_max = if let Some(y_max) = params.clamp.stream_y_max {
481        y_max
482    } else {
483        ss.y_max_stream_recv_plot
484    };
485
486    let stream_send_axis = XYMinMax::init(params, ss, stream_send_y_max);
487    let stream_recv_axis = XYMinMax::init(params, ss, stream_recv_y_max);
488
489    let cwnd_y_max = if let Some(y_max) = params.cwnd_y_max {
490        y_max
491    } else {
492        // add a bit of margin
493        ss.y_max_congestion_plot + ss.y_max_congestion_plot / 10
494    };
495
496    let common_axis = super::minmax::XYMinMax::init(
497        ss.sent_x_min..ss.sent_x_max,
498        params.clamp.start,
499        params.clamp.end,
500        0..cwnd_y_max,
501    );
502
503    top_margin
504        .draw_text(
505            format!("{} Connection overview", filename).as_str(),
506            &chart_title_style(&params.colors.caption),
507            (0, 0),
508        )
509        .unwrap();
510    draw_stream_send_plot(params, stream_send_axis, ss, &stream_send_plot);
511    draw_stream_recv_plot(params, stream_recv_axis, ss, &stream_recv_plot);
512    draw_congestion_plot(params, &common_axis, ss, ds, &congestion_plot);
513    draw_rtt_plot(params, &common_axis, ss, &rtt_plot);
514}
515
516#[cfg(target_arch = "wasm32")]
517pub fn plot_main_plot<'a>(
518    params: &PlotParameters, filename: &str, ss: &SeriesStore, canvas_id: &str,
519) -> ChartContext<'a, CanvasBackend, Cartesian2d<RangedCoordf64, RangedCoordu64>>
520{
521    let root =
522        make_chart_canvas_area(&canvas_id, params.colors, params.chart_margin);
523
524    let stream_y_max = if let Some(y_max) = params.clamp.stream_y_max {
525        y_max
526    } else {
527        ss.y_max_stream_send_plot.max(ss.y_max_stream_recv_plot)
528    };
529
530    let stream_axis = XYMinMax::init(params, ss, stream_y_max);
531
532    draw_main_plot(filename, params, stream_axis, ss, &root)
533}