Skip to main content

qlog_dancer/plots/
packet_sent.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 minmax::XMinMax;
28
29use plotters::coord::types::RangedCoordf64;
30use plotters::coord::types::RangedCoordu64;
31use plotters::coord::Shift;
32
33use crate::plots::colors::*;
34use crate::plots::*;
35
36use crate::datastore::Datastore;
37use crate::seriesstore::SeriesStore;
38
39struct XYMinMax {
40    pub x: XMinMax,
41    pub y_min: u64,
42    pub y_max: u64,
43}
44
45impl XYMinMax {
46    fn init(params: &PlotParameters, ss: &SeriesStore, y_max: u64) -> Self {
47        let x = XMinMax::new(
48            ss.sent_x_min,
49            ss.sent_x_max,
50            params.clamp.start,
51            params.clamp.end,
52        );
53
54        Self { x, y_min: 0, y_max }
55    }
56
57    fn y_range(&self) -> std::ops::Range<u64> {
58        self.y_min..self.y_max
59    }
60}
61
62const Y_WIGGLE: f64 = 1.1;
63
64pub fn draw_packet_sent_received_plot<'a, DB: DrawingBackend + 'a>(
65    is_sent: bool, filename: &str, params: &PlotParameters, ss: &SeriesStore,
66    plot: &plotters::drawing::DrawingArea<DB, Shift>,
67) -> ChartContext<'a, DB, Cartesian2d<RangedCoordf64, RangedCoordu64>> {
68    let (caption, y_max) = if is_sent {
69        let y_max = (ss.y_max_onertt_pkt_sent_plot as f64 * Y_WIGGLE) as u64;
70
71        (format!("{} Packet Sent timeline", filename), y_max)
72    } else {
73        let y_max = (ss.y_max_onertt_pkt_received_plot as f64 * Y_WIGGLE) as u64;
74
75        (format!("{} Packet Received timeline", filename), y_max)
76    };
77
78    let axis = XYMinMax::init(params, ss, y_max);
79
80    let mut builder = ChartBuilder::on(plot);
81
82    builder
83        .x_label_area_size(params.area_margin.x)
84        .y_label_area_size(params.area_margin.y);
85
86    if params.display_chart_title {
87        builder.caption(caption, chart_title_style(&params.colors.caption));
88    }
89
90    let mut chart = builder
91        .build_cartesian_2d(axis.x.range(), axis.y_range())
92        .unwrap();
93
94    draw_mesh(
95        &params.colors,
96        "Relative time (ms)",
97        "Packet Number",
98        params.display_minor_lines,
99        &mut chart,
100    );
101
102    if is_sent {
103        chart
104            .draw_series(LineSeries::new(ss.onertt_packet_created.clone(), GREEN))
105            .unwrap()
106            .label("packet created")
107            .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], GREEN));
108
109        chart
110            .draw_series(LineSeries::new(ss.onertt_packet_sent.clone(), ORANGE))
111            .unwrap()
112            .label("packet sent (packet number)")
113            .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], ORANGE));
114
115        let packet_losses = ss
116            .onertt_packet_lost_hacky
117            .iter()
118            .map(|point| Cross::new(*point, 2, PURPLE));
119
120        chart
121            .draw_series(packet_losses)
122            .unwrap()
123            .label("packet lost (packet number)")
124            .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], PURPLE));
125    } else {
126        let missing_packets_line = ss
127            .netlog_missing_packets
128            .iter()
129            .map(|t| PathElement::new([(*t, 0), (*t, y_max)], BLUE));
130
131        chart
132            .draw_series(missing_packets_line)
133            .unwrap()
134            .label("missing packets")
135            .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
136
137        chart
138            .draw_series(LineSeries::new(
139                ss.onertt_packet_received.clone(),
140                ORANGE,
141            ))
142            .unwrap()
143            .label("packet received")
144            .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], ORANGE));
145    }
146
147    if params.display_legend {
148        chart
149            .configure_series_labels()
150            .label_font(chart_label_style(&params.colors.caption))
151            .background_style(params.colors.fill.mix(0.8))
152            .border_style(params.colors.axis)
153            .position(SeriesLabelPosition::UpperLeft)
154            .draw()
155            .unwrap();
156    }
157
158    chart
159}
160
161pub fn draw_packet_sent_lost_delivered_count_plot<'a, DB: DrawingBackend + 'a>(
162    params: &PlotParameters, ss: &SeriesStore,
163    plot: &plotters::drawing::DrawingArea<DB, Shift>,
164) -> ChartContext<'a, DB, Cartesian2d<RangedCoordf64, RangedCoordu64>> {
165    let caption = "Packet sent/lost/delivered counts";
166
167    let y_max = (ss.y_max_onertt_pkt_sent_plot as f64 * Y_WIGGLE) as u64;
168
169    let axis = XYMinMax::init(params, ss, y_max);
170
171    let mut builder = ChartBuilder::on(plot);
172
173    builder
174        .x_label_area_size(params.area_margin.x)
175        .y_label_area_size(params.area_margin.y);
176
177    if params.display_chart_title {
178        builder.caption(caption, chart_subtitle_style(&params.colors.caption));
179    }
180
181    let mut chart = builder
182        .build_cartesian_2d(axis.x.range(), axis.y_range())
183        .unwrap();
184
185    draw_mesh(
186        &params.colors,
187        "Relative time (ms)",
188        "Packet Count",
189        params.display_minor_lines,
190        &mut chart,
191    );
192
193    chart
194        .draw_series(LineSeries::new(
195            ss.onertt_packet_sent_aggregate_count.clone(),
196            TAUPE,
197        ))
198        .unwrap()
199        .label("packet sent count")
200        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], TAUPE));
201
202    chart
203        .draw_series(LineSeries::new(
204            ss.onertt_packet_delivered_aggregate_count.clone(),
205            BLUE,
206        ))
207        .unwrap()
208        .label("packet delivered count")
209        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
210
211    chart
212        .draw_series(LineSeries::new(
213            ss.onertt_packet_lost_aggregate_count.clone(),
214            SOFT_PINK,
215        ))
216        .unwrap()
217        .label("packet lost count")
218        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], SOFT_PINK));
219
220    if params.display_legend {
221        chart
222            .configure_series_labels()
223            .label_font(chart_label_style(&params.colors.caption))
224            .background_style(params.colors.fill.mix(0.8))
225            .border_style(params.colors.axis)
226            .position(SeriesLabelPosition::UpperLeft)
227            .draw()
228            .unwrap();
229    }
230
231    chart
232}
233
234fn draw_delta_plot<'a, DB: DrawingBackend + 'a>(
235    params: &PlotParameters, ss: &SeriesStore,
236    plot: &plotters::drawing::DrawingArea<DB, Shift>,
237) -> ChartContext<'a, DB, Cartesian2d<RangedCoordu64, RangedCoordf64>> {
238    let y_range = ss.y_min_onertt_packet_created_sent_delta..
239        (ss.y_max_onertt_packet_created_sent_delta * Y_WIGGLE);
240
241    let mut builder = ChartBuilder::on(plot);
242    builder
243        .x_label_area_size(params.area_margin.x)
244        .y_label_area_size(params.area_margin.y);
245
246    if params.display_chart_title {
247        builder.caption(
248            "Packet created/sent Delta timing",
249            chart_subtitle_style(&params.colors.caption),
250        );
251    }
252
253    let mut chart = builder
254        .build_cartesian_2d(0..ss.y_max_onertt_pkt_sent_plot, y_range)
255        .unwrap();
256
257    draw_mesh(
258        &params.colors,
259        "Packet Number",
260        "Delta time (ms)",
261        params.display_minor_lines,
262        &mut chart,
263    );
264
265    // Draw the series
266
267    // not sure best way to render these, lines or points?
268    let lines =
269        LineSeries::new(ss.onertt_packet_created_sent_delta.clone(), BLUE);
270
271    let _crosses = ss
272        .onertt_packet_created_sent_delta
273        .iter()
274        .map(|point| Cross::new(*point, 2, BLUE));
275
276    chart
277        .draw_series(lines)
278        .unwrap()
279        .label("packet created/sent delta")
280        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLACK));
281
282    if params.display_legend {
283        chart
284            .configure_series_labels()
285            .label_font(chart_label_style(&params.colors.caption))
286            .background_style(params.colors.fill.mix(0.8))
287            .border_style(params.colors.axis)
288            .position(SeriesLabelPosition::MiddleMiddle)
289            .draw()
290            .unwrap();
291    }
292
293    chart
294}
295
296fn draw_pacing_rate_plot<'a, DB: DrawingBackend + 'a>(
297    params: &PlotParameters, ss: &SeriesStore, _ds: &Datastore,
298    plot: &plotters::drawing::DrawingArea<DB, Shift>,
299) -> ChartContext<'a, DB, Cartesian2d<RangedCoordf64, RangedCoordu64>> {
300    let y_max = ss
301        .max_pacing_rate
302        .max(ss.max_delivery_rate)
303        .max(ss.max_send_rate)
304        .max(ss.max_ack_rate);
305    let y_max = (y_max as f64 * Y_WIGGLE) as u64;
306    let axis = XYMinMax::init(params, ss, y_max);
307    let mut builder = ChartBuilder::on(plot);
308
309    builder
310        .x_label_area_size(params.area_margin.x)
311        .y_label_area_size(params.area_margin.y);
312
313    if params.display_chart_title {
314        builder.caption(
315            "Pacing Rate vs Delivery Rate",
316            chart_subtitle_style(&params.colors.caption),
317        );
318    }
319
320    let mut chart = builder
321        .build_cartesian_2d(axis.x.range(), axis.y_range())
322        .unwrap();
323
324    draw_mesh(
325        &params.colors,
326        "Relative time (ms)",
327        "Rate (bytes/sec)",
328        params.display_minor_lines,
329        &mut chart,
330    );
331
332    // Draw all 4 rate series
333    chart
334        .draw_series(LineSeries::new(ss.local_pacing_rate.clone(), PURPLE))
335        .unwrap()
336        .label("pacing rate")
337        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], PURPLE));
338
339    chart
340        .draw_series(LineSeries::new(ss.local_delivery_rate.clone(), TEAL))
341        .unwrap()
342        .label("delivery rate")
343        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], TEAL));
344
345    chart
346        .draw_series(LineSeries::new(ss.local_send_rate.clone(), ORANGE))
347        .unwrap()
348        .label("send rate")
349        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], ORANGE));
350
351    chart
352        .draw_series(LineSeries::new(ss.local_ack_rate.clone(), BLUE))
353        .unwrap()
354        .label("ack rate")
355        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
356
357    if params.display_legend {
358        chart
359            .configure_series_labels()
360            .label_font(chart_label_style(&params.colors.caption))
361            .background_style(params.colors.fill.mix(0.8))
362            .border_style(params.colors.axis)
363            .position(SeriesLabelPosition::UpperLeft)
364            .draw()
365            .unwrap();
366    }
367
368    chart
369}
370
371pub fn plot_packet_sent(
372    params: &PlotParameters, filename: &str, ss: &SeriesStore, ds: &Datastore,
373    ty: &ChartOutputType,
374) {
375    let chart_config = make_chart_config("packet-sent", params, filename, ds, ty);
376
377    chart_config.init_chart_dir();
378
379    #[cfg(not(target_arch = "wasm32"))]
380    let chart_path = chart_config.chart_filepath();
381
382    #[cfg(not(target_arch = "wasm32"))]
383    let root = make_chart_bitmap_area(
384        &chart_path,
385        params.chart_size,
386        params.colors,
387        params.chart_margin,
388    );
389
390    #[cfg(target_arch = "wasm32")]
391    let canvas_id: String = chart_config.canvas_id().unwrap_or_default();
392
393    #[cfg(target_arch = "wasm32")]
394    let root =
395        make_chart_canvas_area(&canvas_id, params.colors, params.chart_margin);
396    let (raw_timings, remainder) = root.split_vertically((33).percent());
397    let (counts, remainder) = remainder.split_vertically((33).percent());
398
399    let (delta_timings, pacing_rate) = remainder.split_vertically((50).percent());
400
401    draw_packet_sent_received_plot(true, filename, params, ss, &raw_timings);
402    draw_packet_sent_lost_delivered_count_plot(params, ss, &counts);
403    draw_delta_plot(params, ss, &delta_timings);
404    draw_pacing_rate_plot(params, ss, ds, &pacing_rate);
405}
406
407#[cfg(target_arch = "wasm32")]
408pub fn plot_packet_sent_plot_canvas<'a>(
409    params: &PlotParameters, filename: &str, ss: &SeriesStore, canvas_id: &str,
410) -> ChartContext<'a, CanvasBackend, Cartesian2d<RangedCoordf64, RangedCoordu64>>
411{
412    let root =
413        make_chart_canvas_area(canvas_id, params.colors, params.chart_margin);
414
415    draw_packet_sent_received_plot(true, filename, params, ss, &root)
416}
417
418#[cfg(target_arch = "wasm32")]
419pub fn plot_packet_sent_lost_delivered_count_plot<'a>(
420    params: &PlotParameters, ss: &SeriesStore, canvas_id: &str,
421) -> ChartContext<'a, CanvasBackend, Cartesian2d<RangedCoordf64, RangedCoordu64>>
422{
423    let root =
424        make_chart_canvas_area(canvas_id, params.colors, params.chart_margin);
425
426    draw_packet_sent_lost_delivered_count_plot(params, ss, &root)
427}
428
429#[cfg(target_arch = "wasm32")]
430pub fn plot_packet_sent_delta_plot_canvas<'a>(
431    params: &PlotParameters, ss: &SeriesStore, canvas_id: &str,
432) -> ChartContext<'a, CanvasBackend, Cartesian2d<RangedCoordu64, RangedCoordf64>>
433{
434    let root =
435        make_chart_canvas_area(canvas_id, params.colors, params.chart_margin);
436
437    draw_delta_plot(params, ss, &root)
438}
439
440#[cfg(target_arch = "wasm32")]
441pub fn plot_packet_sent_pacing_rate_plot_canvas<'a>(
442    params: &PlotParameters, ss: &SeriesStore, ds: &Datastore, canvas_id: &str,
443) -> ChartContext<'a, CanvasBackend, Cartesian2d<RangedCoordf64, RangedCoordu64>>
444{
445    let root =
446        make_chart_canvas_area(canvas_id, params.colors, params.chart_margin);
447
448    draw_pacing_rate_plot(params, ss, ds, &root)
449}