Skip to main content

qlog_dancer/plots/
stream_sparks.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
27//! Spark charts
28//! These are a single image file containing a grid of mini plots. Each plot
29//! represents a single stream, so it is easier to see how they all compare
30//! at a glance.
31
32// TODO: this seems to be required to overcome a transient error in nightly; see
33// https://github.com/rust-lang/rust/issues/147648#issuecomment-3482917926
34#![allow(unused_assignments)]
35
36use full_palette::PURPLE_500;
37use plotters::coord::types::RangedCoordf64;
38use plotters::coord::types::RangedCoordu64;
39
40use tabled::Tabled;
41
42use crate::datastore::ApplicationProto;
43use crate::datastore::VantagePoint;
44use crate::plots::colors::*;
45use crate::plots::*;
46use crate::request_stub::HttpRequestStub;
47
48use crate::datastore::Datastore;
49use crate::seriesstore::SeriesStore;
50
51#[allow(unused_assignments)]
52pub enum TransmissionType {
53    Upload,
54    Download,
55}
56
57#[derive(Debug)]
58pub struct SparkPlotsParams {
59    pub clamp: ClampParams,
60    pub bidi_only: bool,
61    pub label_area_width: u32,
62    pub label_area_height: u32,
63    pub caption_area_height: u32,
64    pub caption_area_width: u32,
65    pub spark_offset_x: u32,
66    pub spark_offset_y: u32,
67    pub spark_dimension_x: u32,
68    pub spark_dimension_y: u32,
69    pub sparks_per_row: u32,
70    pub captions_on_top: bool,
71    pub colors: PlotColors,
72}
73
74impl Default for SparkPlotsParams {
75    fn default() -> Self {
76        Self {
77            clamp: Default::default(),
78            bidi_only: true,
79            label_area_width: 20,
80            label_area_height: 20,
81            caption_area_height: 100,
82            caption_area_width: 0,
83            spark_offset_x: 50,
84            spark_offset_y: 30,
85            spark_dimension_x: 150,
86            spark_dimension_y: 150,
87            sparks_per_row: 10,
88            captions_on_top: true,
89            colors: Default::default(),
90        }
91    }
92}
93
94#[derive(Tabled)]
95enum SparkCaption {
96    UniStream {
97        summary: String,
98    },
99
100    RequestAtServer {
101        summary: String,
102        method: String,
103        path: String,
104        client_content_length: String,
105        server_content_length: String,
106        client_pri_hdr: String,
107        server_pri_hdr: String,
108        duration_rx_hdr_tx_hdr: String,
109        duration_rx_hdr_tx_first_data: String,
110        duration_rx_hdr_tx_last_data: String,
111        duration_tx_first_data_tx_last_data: String,
112    },
113
114    RequestAtClient {
115        summary: String,
116        method: String,
117        path: String,
118        client_content_length: String,
119        server_content_length: String,
120        client_pri_hdr: String,
121        client_pri_update: String,
122        server_pri_hdr: String,
123        duration_tx_hdr_rx_hdr: String,
124        duration_tx_hdr_rx_first_data: String,
125        duration_tx_hdr_rx_last_data: String,
126        duration_tx_first_data_tx_last_data: String,
127    },
128}
129
130impl SparkCaption {
131    fn request_at_client_from_stub(
132        application_proto: ApplicationProto, req: &HttpRequestStub,
133    ) -> Self {
134        let summary = format!(
135            "ID= {}, status= {}, proto= {:?}",
136            req.stream_id, req.status, application_proto
137        );
138        let empty = &"".to_string();
139
140        let duration_tx_hdr_rx_hdr =
141            match req.at_client_deltas.as_ref().unwrap().tx_hdr_rx_hdr.inner {
142                Some(t) => format!("d_tx_hdr_rx_hdr:               {:.2}ms", t),
143
144                None => empty.to_owned(),
145            };
146
147        let duration_tx_hdr_rx_first_data = match req
148            .at_client_deltas
149            .as_ref()
150            .unwrap()
151            .tx_hdr_rx_first_data
152            .inner
153        {
154            Some(t) => format!("d_tx_hdr_rx_data_first:        {:.2}ms", t),
155
156            None => empty.to_owned(),
157        };
158
159        let duration_tx_hdr_rx_last_data = match req
160            .at_client_deltas
161            .as_ref()
162            .unwrap()
163            .tx_hdr_rx_last_data
164            .inner
165        {
166            Some(t) => format!("d_tx_hdr_rx_data_last:         {:.2}ms", t),
167
168            None => empty.to_owned(),
169        };
170
171        let duration_tx_first_data_tx_last_data = match req
172            .at_client_deltas
173            .as_ref()
174            .unwrap()
175            .tx_first_data_tx_last_data
176            .inner
177        {
178            Some(t) => format!("d_tx_data_first_tx_data_last:  {:.2}ms", t),
179
180            None => empty.to_owned(),
181        };
182
183        SparkCaption::RequestAtClient {
184            summary,
185            method: format!("method= {}", req.method),
186            path: format!("path= {}", req.path),
187
188            client_content_length: format!(
189                "client content length: {}",
190                req.client_content_length
191            ),
192            server_content_length: format!(
193                "server content length: {}",
194                req.server_content_length
195            ),
196
197            client_pri_hdr: format!(
198                "client pri hdr:         {}",
199                req.client_pri_hdr
200            ),
201
202            client_pri_update: format!(
203                "client pri updates:     {:?}",
204                req.priority_updates
205            ),
206
207            server_pri_hdr: format!(
208                "server pri hdr:         {}",
209                req.server_pri_hdr
210            ),
211
212            duration_tx_hdr_rx_hdr,
213
214            duration_tx_hdr_rx_first_data,
215
216            duration_tx_hdr_rx_last_data,
217
218            duration_tx_first_data_tx_last_data,
219        }
220    }
221
222    fn request_at_server_from_stub(
223        stream_id: u64, req: &HttpRequestStub,
224    ) -> Self {
225        let summary = format!("ID= {}, status= {}", stream_id, req.status);
226        let empty = &"".to_string();
227
228        let duration_rx_hdr_tx_hdr =
229            match req.at_server_deltas.as_ref().unwrap().rx_hdr_tx_hdr.inner {
230                Some(t) => format!("d_rx_hdr_tx_hdr:               {:.2}ms", t),
231
232                None => empty.to_owned(),
233            };
234
235        let duration_rx_hdr_tx_first_data = match req
236            .at_server_deltas
237            .as_ref()
238            .unwrap()
239            .rx_hdr_tx_first_data
240            .inner
241        {
242            Some(t) => format!("d_rx_hdr_tx_data_first:        {:.2}ms", t),
243
244            None => empty.to_owned(),
245        };
246
247        let duration_rx_hdr_tx_last_data = match req
248            .at_server_deltas
249            .as_ref()
250            .unwrap()
251            .rx_hdr_tx_last_data
252            .inner
253        {
254            Some(t) => format!("d_rx_hdr_tx_data_last:         {:.2}ms", t),
255
256            None => empty.to_owned(),
257        };
258
259        let duration_tx_first_data_tx_last_data = match req
260            .at_server_deltas
261            .as_ref()
262            .unwrap()
263            .tx_first_data_tx_last_data
264            .inner
265        {
266            Some(t) => format!("d_tx_data_first_tx_data_last:  {:.2}ms", t),
267
268            None => empty.to_owned(),
269        };
270
271        SparkCaption::RequestAtServer {
272            summary,
273            method: format!("method= {}", req.method),
274            path: format!("path= {}", req.path),
275
276            client_content_length: format!(
277                "client content length: {}",
278                req.client_content_length
279            ),
280            server_content_length: format!(
281                "server content length: {}",
282                req.server_content_length
283            ),
284
285            client_pri_hdr: format!(
286                "client pri hdr:         {}",
287                req.client_pri_hdr
288            ),
289
290            server_pri_hdr: format!(
291                "server pri hdr:         {}",
292                req.server_pri_hdr
293            ),
294
295            duration_rx_hdr_tx_hdr,
296
297            duration_rx_hdr_tx_first_data,
298
299            duration_rx_hdr_tx_last_data,
300
301            duration_tx_first_data_tx_last_data,
302        }
303    }
304
305    fn from_data_store(ds: &Datastore, stream_id: u64) -> Option<Self> {
306        let is_request = match ds.application_proto {
307            ApplicationProto::Http3 => stream_id.is_multiple_of(4),
308            ApplicationProto::Http2 => true,
309        };
310
311        if is_request {
312            if let Some(req) = ds.http_requests.get(&stream_id) {
313                match ds.vantage_point {
314                    VantagePoint::Client =>
315                        Some(SparkCaption::request_at_client_from_stub(
316                            ds.application_proto,
317                            req,
318                        )),
319                    VantagePoint::Server => Some(
320                        SparkCaption::request_at_server_from_stub(stream_id, req),
321                    ),
322                }
323            } else {
324                None
325            }
326        } else {
327            let summary = format!("ID= {}", stream_id);
328            Some(SparkCaption::UniStream { summary })
329        }
330    }
331
332    fn draw<DB: DrawingBackend>(
333        &self, color: &RGBColor, root: &DrawingArea<DB, plotters::coord::Shift>,
334        x: i32, y: i32,
335    ) {
336        let style = &("monospace", 12).into_text_style(root).color(color);
337
338        let newline_offset = 10;
339        let x = x + 20;
340
341        match self {
342            SparkCaption::UniStream { summary } => {
343                root.draw_text(summary, style, (x, y)).unwrap();
344            },
345
346            SparkCaption::RequestAtServer {
347                summary,
348                method,
349                path,
350                client_content_length,
351                server_content_length,
352                client_pri_hdr,
353                server_pri_hdr,
354                duration_rx_hdr_tx_hdr,
355                duration_rx_hdr_tx_first_data,
356                duration_rx_hdr_tx_last_data,
357                duration_tx_first_data_tx_last_data,
358                ..
359            } => {
360                root.draw_text(summary, style, (x, y)).unwrap();
361
362                root.draw_text(method, style, (x, y + newline_offset))
363                    .unwrap();
364
365                root.draw_text(path, style, (x, y + newline_offset * 2))
366                    .unwrap();
367
368                root.draw_text(
369                    client_content_length,
370                    style,
371                    (x, y + newline_offset * 3),
372                )
373                .unwrap();
374
375                root.draw_text(
376                    server_content_length,
377                    style,
378                    (x, y + newline_offset * 4),
379                )
380                .unwrap();
381
382                root.draw_text(
383                    client_pri_hdr,
384                    style,
385                    (x, y + newline_offset * 6),
386                )
387                .unwrap();
388
389                root.draw_text(
390                    server_pri_hdr,
391                    style,
392                    (x, y + newline_offset * 7),
393                )
394                .unwrap();
395
396                root.draw_text(
397                    duration_rx_hdr_tx_hdr,
398                    style,
399                    (x, y + newline_offset * 8),
400                )
401                .unwrap();
402
403                root.draw_text(
404                    duration_rx_hdr_tx_first_data,
405                    style,
406                    (x, y + newline_offset * 9),
407                )
408                .unwrap();
409
410                root.draw_text(
411                    duration_rx_hdr_tx_last_data,
412                    style,
413                    (x, y + newline_offset * 10),
414                )
415                .unwrap();
416
417                root.draw_text(
418                    duration_tx_first_data_tx_last_data,
419                    style,
420                    (x, y + newline_offset * 11),
421                )
422                .unwrap();
423            },
424
425            SparkCaption::RequestAtClient {
426                summary,
427                method,
428                path,
429                client_content_length,
430                server_content_length,
431                client_pri_hdr,
432                client_pri_update,
433                server_pri_hdr,
434                duration_tx_hdr_rx_hdr,
435                duration_tx_hdr_rx_first_data,
436                duration_tx_hdr_rx_last_data,
437                duration_tx_first_data_tx_last_data,
438                ..
439            } => {
440                root.draw_text(summary, style, (x, y)).unwrap();
441
442                root.draw_text(method, style, (x, y + newline_offset))
443                    .unwrap();
444                root.draw_text(path, style, (x, y + newline_offset * 2))
445                    .unwrap();
446
447                root.draw_text(
448                    client_content_length,
449                    style,
450                    (x, y + newline_offset * 3),
451                )
452                .unwrap();
453
454                root.draw_text(
455                    server_content_length,
456                    style,
457                    (x, y + newline_offset * 4),
458                )
459                .unwrap();
460
461                root.draw_text(
462                    client_pri_hdr,
463                    style,
464                    (x, y + newline_offset * 6),
465                )
466                .unwrap();
467
468                root.draw_text(
469                    client_pri_update,
470                    style,
471                    (x, y + newline_offset * 7),
472                )
473                .unwrap();
474
475                root.draw_text(
476                    server_pri_hdr,
477                    style,
478                    (x, y + newline_offset * 8),
479                )
480                .unwrap();
481
482                root.draw_text(
483                    duration_tx_hdr_rx_hdr,
484                    style,
485                    (x, y + newline_offset * 9),
486                )
487                .unwrap();
488
489                root.draw_text(
490                    duration_tx_hdr_rx_first_data,
491                    style,
492                    (x, y + newline_offset * 10),
493                )
494                .unwrap();
495
496                root.draw_text(
497                    duration_tx_hdr_rx_last_data,
498                    style,
499                    (x, y + newline_offset * 11),
500                )
501                .unwrap();
502
503                root.draw_text(
504                    duration_tx_first_data_tx_last_data,
505                    style,
506                    (x, y + newline_offset * 12),
507                )
508                .unwrap();
509            },
510        }
511    }
512}
513
514fn plot_legend<DB: DrawingBackend>(
515    color: &RGBColor, root: &DrawingArea<DB, plotters::coord::Shift>, x: i32,
516    y: i32, params: &SparkPlotsParams, transmission_type: TransmissionType,
517) {
518    // TODO: only considers vert layout, needs to consider grid layout too
519    let style = &("monospace", 12).into_text_style(root).color(color);
520    let x: i32 = x + params.spark_dimension_x as i32 + 300;
521    let y: i32 = y;
522    let line_len: i32 = 50;
523    let nl_off: i32 = 20;
524
525    root.draw_text("Legend", style, (x, y)).unwrap();
526
527    // stream data
528    let label = match transmission_type {
529        TransmissionType::Download => "Stream data read",
530        TransmissionType::Upload => "Stream data sent",
531    };
532    root.draw(&PathElement::new(
533        vec![(x, y + nl_off), (x + line_len, y + nl_off)],
534        PURPLE_500,
535    ))
536    .unwrap();
537    root.draw_text(label, style, (x + line_len + 10, y + (nl_off) - 5))
538        .unwrap();
539
540    // stream data buffered
541    root.draw(&PathElement::new(
542        vec![(x, y + nl_off * 2), (x + line_len, y + nl_off * 2)],
543        MAGENTA,
544    ))
545    .unwrap();
546    root.draw_text(
547        "Stream data buffered",
548        style,
549        (x + line_len + 10, y + (nl_off * 2) - 5),
550    )
551    .unwrap();
552
553    // max stream data
554    let label = match transmission_type {
555        TransmissionType::Download => "Max stream data sent",
556        TransmissionType::Upload => "Max stream data received",
557    };
558
559    root.draw(&PathElement::new(
560        vec![(x, y + nl_off * 3), (x + line_len, y + nl_off * 3)],
561        MUSTARD,
562    ))
563    .unwrap();
564    root.draw_text(label, style, (x + line_len + 10, y + (nl_off * 3) - 5))
565        .unwrap();
566
567    // headers transmitted
568    root.draw(&PathElement::new(
569        vec![(x, y + nl_off * 4), (x + line_len, y + nl_off * 4)],
570        CYAN,
571    ))
572    .unwrap();
573    root.draw_text(
574        "Request headers sent",
575        style,
576        (x + line_len + 10, y + (nl_off * 4) - 5),
577    )
578    .unwrap();
579
580    // first data frame
581    root.draw(&PathElement::new(
582        vec![(x, y + nl_off * 5), (x + line_len, y + nl_off * 5)],
583        ORANGE,
584    ))
585    .unwrap();
586    root.draw_text(
587        "First data frame read",
588        style,
589        (x + line_len + 10, y + (nl_off * 5) - 5),
590    )
591    .unwrap();
592
593    // last data frame
594    root.draw(&PathElement::new(
595        vec![(x, y + nl_off * 6), (x + line_len, y + nl_off * 6)],
596        BROWN,
597    ))
598    .unwrap();
599    root.draw_text(
600        "Last data frame read",
601        style,
602        (x + line_len + 10, y + (nl_off * 6) - 5),
603    )
604    .unwrap();
605}
606
607#[allow(clippy::too_many_arguments)]
608pub fn plot_sparks(
609    params: &SparkPlotsParams, filename: &str, ss: &SeriesStore, ds: &Datastore,
610    abs_dl_ty: &ChartOutputType, rel_dl_ty: &ChartOutputType,
611    abs_ul_ty: &ChartOutputType, rel_ul_ty: &ChartOutputType,
612) {
613    let (stream_frame_dl_series_to_plot, stream_frame_ul_series_to_plot) =
614        match (ds.vantage_point, ds.application_proto) {
615            (VantagePoint::Client, ApplicationProto::Http3) => (
616                &ss.received_stream_frames_series,
617                &ss.sent_stream_frames_series,
618            ),
619            (VantagePoint::Server, ApplicationProto::Http3) => (
620                &ss.sent_stream_frames_series,
621                &ss.received_stream_frames_series,
622            ),
623            (VantagePoint::Client, ApplicationProto::Http2) =>
624                (&ss.received_data_frames_series, &ss.sent_data_frames_series),
625            _ => unimplemented!(),
626        };
627
628    #[cfg(not(target_arch = "wasm32"))]
629    let (total_dl_sparks, total_ul_sparks) =
630        if params.bidi_only && ds.application_proto == ApplicationProto::Http3 {
631            let total_dl = stream_frame_dl_series_to_plot
632                .iter()
633                .filter(|(k, _)| (*k & 0x2) == 0)
634                .count() as u32;
635
636            let total_ul = stream_frame_ul_series_to_plot
637                .iter()
638                .filter(|(k, _)| (*k & 0x2) == 0)
639                .count() as u32;
640
641            (total_dl, total_ul)
642        } else {
643            (
644                stream_frame_dl_series_to_plot.len() as u32,
645                stream_frame_ul_series_to_plot.len() as u32,
646            )
647        };
648
649    // TODO: replace with div_ceil once stablized- https://github.com/rust-lang/rust/issues/88581
650    #[cfg(not(target_arch = "wasm32"))]
651    let total_dl_rows: u32 = total_dl_sparks / params.sparks_per_row +
652        u32::from(total_dl_sparks % params.sparks_per_row != 0);
653    #[cfg(not(target_arch = "wasm32"))]
654    let total_ul_rows: u32 = total_ul_sparks / params.sparks_per_row +
655        u32::from(total_ul_sparks % params.sparks_per_row != 0);
656
657    #[cfg(not(target_arch = "wasm32"))]
658    let caption_area_width = if params.captions_on_top {
659        0
660    } else {
661        params.caption_area_width
662    };
663
664    let caption_area_height = if params.captions_on_top {
665        params.caption_area_height
666    } else {
667        0
668    };
669
670    #[cfg(not(target_arch = "wasm32"))]
671    let spark_and_captions_width = params.spark_dimension_x +
672        (params.spark_offset_x as f32 * 1.2) as u32 +
673        caption_area_width;
674    #[cfg(not(target_arch = "wasm32"))]
675    let spark_and_captions_height = params.spark_dimension_y +
676        params.spark_offset_y +
677        (params.label_area_height) +
678        caption_area_height;
679
680    #[cfg(not(target_arch = "wasm32"))]
681    let root_width: u32 = params.sparks_per_row * spark_and_captions_width;
682
683    #[cfg(not(target_arch = "wasm32"))]
684    let dl_root_height: u32 = total_dl_rows * spark_and_captions_height;
685
686    #[cfg(not(target_arch = "wasm32"))]
687    let ul_root_height: u32 = total_ul_rows * spark_and_captions_height;
688
689    let mut x_min = match ds.vantage_point {
690        VantagePoint::Client => ss.received_x_min,
691        VantagePoint::Server => ss.sent_x_min,
692    };
693
694    let mut x_max = match ds.vantage_point {
695        VantagePoint::Client => ss.received_x_max,
696        VantagePoint::Server => ss.sent_x_max,
697    };
698
699    // Clamp plot x-axis if the user told us.
700    if let Some(s) = params.clamp.start {
701        x_min = s;
702    }
703
704    if let Some(e) = params.clamp.end {
705        x_max = x_max.min(e);
706    }
707
708    let abs_dl_chart_config = ChartConfig {
709        title: "stream-spark-dl-absolute".into(),
710        input_filename: filename.into(),
711        clamp: params.clamp.clone(),
712        app_proto: ds.application_proto,
713        host: ds.host.clone(),
714        session_id: ds.session_id,
715        ty: abs_dl_ty.clone(),
716    };
717    abs_dl_chart_config.init_chart_dir();
718
719    let rel_dl_chart_config = ChartConfig {
720        title: "stream-spark-dl-relative".into(),
721        input_filename: filename.into(),
722        clamp: params.clamp.clone(),
723        app_proto: ds.application_proto,
724        host: ds.host.clone(),
725        session_id: ds.session_id,
726        ty: rel_dl_ty.clone(),
727    };
728    rel_dl_chart_config.init_chart_dir();
729
730    let abs_ul_chart_config = ChartConfig {
731        title: "stream-spark-ul-absolute".into(),
732        input_filename: filename.into(),
733        clamp: params.clamp.clone(),
734        app_proto: ds.application_proto,
735        host: ds.host.clone(),
736        session_id: ds.session_id,
737        ty: abs_ul_ty.clone(),
738    };
739    abs_ul_chart_config.init_chart_dir();
740
741    let rel_ul_chart_config = ChartConfig {
742        title: "stream-spark-ul-relative".into(),
743        input_filename: filename.into(),
744        clamp: params.clamp.clone(),
745        app_proto: ds.application_proto,
746        host: ds.host.clone(),
747        session_id: ds.session_id,
748        ty: rel_ul_ty.clone(),
749    };
750    rel_ul_chart_config.init_chart_dir();
751
752    #[cfg(not(target_arch = "wasm32"))]
753    let dl_size = ChartSize {
754        width: root_width,
755        height: dl_root_height,
756    };
757
758    #[cfg(not(target_arch = "wasm32"))]
759    let ul_size = ChartSize {
760        width: root_width,
761        height: ul_root_height,
762    };
763    let margin = ChartMargin::default();
764
765    #[cfg(not(target_arch = "wasm32"))]
766    let abs_dl_chart_path = abs_dl_chart_config.chart_filepath();
767
768    #[cfg(not(target_arch = "wasm32"))]
769    let rel_dl_chart_path = rel_dl_chart_config.chart_filepath();
770
771    #[cfg(not(target_arch = "wasm32"))]
772    let abs_ul_chart_path = abs_ul_chart_config.chart_filepath();
773
774    #[cfg(not(target_arch = "wasm32"))]
775    let rel_ul_chart_path = rel_ul_chart_config.chart_filepath();
776
777    #[cfg(not(target_arch = "wasm32"))]
778    let abs_dl_root = make_chart_bitmap_area(
779        &abs_dl_chart_path,
780        dl_size,
781        params.colors,
782        margin,
783    );
784
785    #[cfg(not(target_arch = "wasm32"))]
786    let rel_dl_root = make_chart_bitmap_area(
787        &rel_dl_chart_path,
788        dl_size,
789        params.colors,
790        margin,
791    );
792
793    #[cfg(not(target_arch = "wasm32"))]
794    let abs_ul_root = make_chart_bitmap_area(
795        &abs_ul_chart_path,
796        ul_size,
797        params.colors,
798        margin,
799    );
800
801    #[cfg(not(target_arch = "wasm32"))]
802    let rel_ul_root = make_chart_bitmap_area(
803        &rel_ul_chart_path,
804        ul_size,
805        params.colors,
806        margin,
807    );
808
809    #[cfg(target_arch = "wasm32")]
810    let abs_dl_canvas_id = abs_dl_chart_config.canvas_id().unwrap_or_default();
811
812    #[cfg(target_arch = "wasm32")]
813    let rel_dl_canvas_id = rel_dl_chart_config.canvas_id().unwrap_or_default();
814
815    #[cfg(target_arch = "wasm32")]
816    let abs_ul_canvas_id = abs_ul_chart_config.canvas_id().unwrap_or_default();
817
818    #[cfg(target_arch = "wasm32")]
819    let rel_ul_canvas_id = rel_ul_chart_config.canvas_id().unwrap_or_default();
820
821    #[cfg(target_arch = "wasm32")]
822    let abs_dl_root =
823        make_chart_canvas_area(&abs_dl_canvas_id, params.colors, margin);
824    #[cfg(target_arch = "wasm32")]
825    let rel_dl_root =
826        make_chart_canvas_area(&rel_dl_canvas_id, params.colors, margin);
827    #[cfg(target_arch = "wasm32")]
828    let abs_ul_root =
829        make_chart_canvas_area(&abs_ul_canvas_id, params.colors, margin);
830    #[cfg(target_arch = "wasm32")]
831    let rel_ul_root =
832        make_chart_canvas_area(&rel_ul_canvas_id, params.colors, margin);
833
834    let mut upper_x = params.spark_offset_x;
835    let mut dl_upper_y = params.spark_offset_y;
836    let mut ul_upper_y = params.spark_offset_y;
837
838    plot_legend(
839        &params.colors.caption,
840        &abs_dl_root,
841        upper_x as i32,
842        dl_upper_y as i32,
843        params,
844        TransmissionType::Download,
845    );
846    plot_legend(
847        &params.colors.caption,
848        &rel_dl_root,
849        upper_x as i32,
850        dl_upper_y as i32,
851        params,
852        TransmissionType::Download,
853    );
854    plot_legend(
855        &params.colors.caption,
856        &abs_ul_root,
857        upper_x as i32,
858        ul_upper_y as i32,
859        params,
860        TransmissionType::Upload,
861    );
862    plot_legend(
863        &params.colors.caption,
864        &rel_ul_root,
865        upper_x as i32,
866        ul_upper_y as i32,
867        params,
868        TransmissionType::Upload,
869    );
870
871    let mut dl_layout_count = 0;
872    let mut ul_layout_count = 0;
873
874    for (stream_id, dl_stream_frames) in stream_frame_dl_series_to_plot {
875        if ds.application_proto == ApplicationProto::Http3 &&
876            params.bidi_only &&
877            (stream_id & 0x2) != 0
878        {
879            continue;
880        }
881
882        let abs_dl_small_area = abs_dl_root.clone().shrink(
883            (upper_x, dl_upper_y + caption_area_height),
884            (params.spark_dimension_x, params.spark_dimension_y),
885        );
886
887        let rel_dl_small_area = rel_dl_root.clone().shrink(
888            (upper_x, dl_upper_y + caption_area_height),
889            (params.spark_dimension_x, params.spark_dimension_y),
890        );
891
892        let abs_ul_small_area = abs_ul_root.clone().shrink(
893            (upper_x, ul_upper_y + caption_area_height),
894            (params.spark_dimension_x, params.spark_dimension_y),
895        );
896
897        let rel_ul_small_area = rel_ul_root.clone().shrink(
898            (upper_x, ul_upper_y + caption_area_height),
899            (params.spark_dimension_x, params.spark_dimension_y),
900        );
901
902        let dl_zoom_x_min = dl_stream_frames.first().unwrap().0;
903        let dl_zoom_x_max = dl_stream_frames.last().unwrap().0;
904
905        let dl_zoom_y_max = if let Some(y_max) = params.clamp.stream_y_max {
906            y_max
907        } else {
908            dl_stream_frames.last().unwrap().1
909        };
910
911        let mut abs_dl_chart = ChartBuilder::on(&abs_dl_small_area)
912            .set_label_area_size(LabelAreaPosition::Left, params.label_area_width)
913            .set_label_area_size(
914                LabelAreaPosition::Bottom,
915                params.label_area_height,
916            )
917            .build_cartesian_2d(x_min..x_max, 0..dl_zoom_y_max)
918            .unwrap();
919        draw_mesh(&params.colors, &mut abs_dl_chart);
920
921        let mut rel_dl_chart = ChartBuilder::on(&rel_dl_small_area)
922            .set_label_area_size(LabelAreaPosition::Left, params.label_area_width)
923            .set_label_area_size(
924                LabelAreaPosition::Bottom,
925                params.label_area_height,
926            )
927            .build_cartesian_2d(dl_zoom_x_min..dl_zoom_x_max, 0..dl_zoom_y_max)
928            .unwrap();
929        draw_mesh(&params.colors, &mut rel_dl_chart);
930
931        dl_layout_count += 1;
932
933        let ul_stream_frames = stream_frame_ul_series_to_plot.get(stream_id);
934
935        // TODO upload window
936        let ul_h2_send_window =
937            ss.h2_send_window_series_balanced.get(&(*stream_id as u32));
938        let ul_h2_send_window_max =
939            ss.h2_send_window_balanced_max.get(&(*stream_id as u32));
940
941        let (abs_ul_chart, rel_ul_chart) = match ul_stream_frames {
942            Some(frames) => {
943                let ul_zoom_x_min = frames.first().unwrap().0;
944                let ul_zoom_x_max = frames.last().unwrap().0;
945
946                // let ul_zoom_x_min_send_window =
947                //     ul_send_window.unwrap().first().unwrap().0;
948                // let ul_zoom_x_max_send_window =
949                //     ul_send_window.unwrap().last().unwrap().0;
950
951                let ul_zoom_y_max = if let Some(y_max) = params.clamp.stream_y_max
952                {
953                    y_max
954                } else {
955                    frames.last().unwrap().1
956                };
957
958                // TODO: this is a hack because the underlying chart types make
959                // it hard to support optional secondary axis. And that axis
960                // only makes sense for HTTP/2.
961                let ul_h2_send_window_y_max = ul_h2_send_window_max.unwrap_or(&0);
962
963                let mut abs_ul_chart = ChartBuilder::on(&abs_ul_small_area)
964                    .set_label_area_size(
965                        LabelAreaPosition::Left,
966                        params.label_area_width,
967                    )
968                    .set_label_area_size(
969                        LabelAreaPosition::Bottom,
970                        params.label_area_height,
971                    )
972                    .build_cartesian_2d(x_min..x_max, 0..ul_zoom_y_max)
973                    .unwrap()
974                    .set_secondary_coord(
975                        x_min..x_max,
976                        0i32..*ul_h2_send_window_y_max,
977                    );
978
979                draw_mesh(&params.colors, &mut abs_ul_chart);
980
981                if ul_h2_send_window_max.is_some() {
982                    abs_ul_chart.configure_secondary_axes().draw().unwrap();
983                }
984
985                let mut rel_ul_chart = ChartBuilder::on(&rel_ul_small_area)
986                    .set_label_area_size(
987                        LabelAreaPosition::Left,
988                        params.label_area_width,
989                    )
990                    .set_label_area_size(
991                        LabelAreaPosition::Bottom,
992                        params.label_area_height,
993                    )
994                    .right_y_label_area_size(params.label_area_width)
995                    .build_cartesian_2d(
996                        ul_zoom_x_min..ul_zoom_x_max,
997                        0..ul_zoom_y_max,
998                    )
999                    .unwrap()
1000                    .set_secondary_coord(
1001                        ul_zoom_x_min..ul_zoom_x_max,
1002                        0i32..*ul_h2_send_window_y_max,
1003                    );
1004                draw_mesh(&params.colors, &mut rel_ul_chart);
1005
1006                if ul_h2_send_window_max.is_some() {
1007                    rel_ul_chart
1008                        .configure_secondary_axes()
1009                        .y_labels(5)
1010                        .draw()
1011                        .unwrap();
1012                }
1013
1014                ul_layout_count += 1;
1015
1016                (Some(abs_ul_chart), Some(rel_ul_chart))
1017            },
1018
1019            None => (None, None),
1020        };
1021
1022        draw_captions(
1023            &abs_dl_root,
1024            &rel_dl_root,
1025            &abs_ul_root,
1026            &rel_ul_root,
1027            params,
1028            ds,
1029            *stream_id,
1030            ul_stream_frames.is_some(),
1031            upper_x,
1032            dl_upper_y,
1033            ul_upper_y,
1034        );
1035
1036        upper_x += params.spark_dimension_x + params.spark_offset_x;
1037
1038        // new row needed?
1039        if dl_layout_count == params.sparks_per_row {
1040            upper_x = params.spark_offset_x;
1041
1042            let y_offset = if params.captions_on_top {
1043                params.caption_area_height
1044            } else {
1045                0
1046            };
1047
1048            dl_upper_y +=
1049                params.spark_dimension_y + params.spark_offset_y + y_offset;
1050
1051            dl_layout_count = 0;
1052        }
1053
1054        if ul_layout_count == params.sparks_per_row {
1055            upper_x = params.spark_offset_x;
1056
1057            let y_offset = if params.captions_on_top {
1058                params.caption_area_height
1059            } else {
1060                0
1061            };
1062
1063            ul_upper_y +=
1064                params.spark_dimension_y + params.spark_offset_y + y_offset;
1065
1066            ul_layout_count = 0;
1067        }
1068
1069        draw_stream_frames_line(
1070            dl_stream_frames,
1071            &mut abs_dl_chart,
1072            &mut rel_dl_chart,
1073        );
1074
1075        if let (Some(mut abs), Some(mut rel), Some(frames)) =
1076            (abs_ul_chart, rel_ul_chart, ul_stream_frames)
1077        {
1078            if let Some(v) = ul_h2_send_window {
1079                abs.draw_secondary_series(LineSeries::new(v.to_vec(), ORANGE))
1080                    .unwrap();
1081
1082                rel.draw_secondary_series(LineSeries::new(v.to_vec(), ORANGE))
1083                    .unwrap();
1084            } else {
1085                draw_stream_max_line(
1086                    *stream_id,
1087                    ss,
1088                    &ds.vantage_point,
1089                    TransmissionType::Upload,
1090                    &mut abs,
1091                    &mut rel,
1092                );
1093            }
1094
1095            draw_stream_frames_line(frames, &mut abs, &mut rel);
1096        }
1097
1098        draw_buffered_data_line(
1099            *stream_id,
1100            ss,
1101            &ds.vantage_point,
1102            &mut abs_dl_chart,
1103            &mut rel_dl_chart,
1104        );
1105
1106        draw_stream_max_line(
1107            *stream_id,
1108            ss,
1109            &ds.vantage_point,
1110            TransmissionType::Download,
1111            &mut abs_dl_chart,
1112            &mut rel_dl_chart,
1113        );
1114
1115        let y_max = match ds.vantage_point {
1116            VantagePoint::Client => ss.y_max_stream_recv_plot,
1117            VantagePoint::Server => ss.y_max_stream_send_plot,
1118        };
1119
1120        draw_request_timing_lines(
1121            ds,
1122            *stream_id,
1123            y_max,
1124            &mut abs_dl_chart,
1125            &mut rel_dl_chart,
1126        );
1127    }
1128}
1129
1130fn draw_mesh<XT, YT, X, Y, DB: DrawingBackend>(
1131    colors: &PlotColors, chart: &mut ChartContext<DB, Cartesian2d<X, Y>>,
1132) where
1133    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
1134    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
1135{
1136    chart
1137        .configure_mesh()
1138        .disable_mesh()
1139        .x_labels(5)
1140        .y_labels(5)
1141        .axis_style(colors.axis)
1142        .label_style(chart_label_style(&colors.caption))
1143        .draw()
1144        .unwrap();
1145}
1146
1147#[allow(clippy::too_many_arguments)]
1148fn draw_captions<DB: DrawingBackend>(
1149    abs_dl_root: &DrawingArea<DB, plotters::coord::Shift>,
1150    rel_dl_root: &DrawingArea<DB, plotters::coord::Shift>,
1151    abs_ul_root: &DrawingArea<DB, plotters::coord::Shift>,
1152    rel_ul_root: &DrawingArea<DB, plotters::coord::Shift>,
1153    params: &SparkPlotsParams, ds: &Datastore, stream_id: u64, upload_data: bool,
1154    upper_x: u32, dl_upper_y: u32, ul_upper_y: u32,
1155) {
1156    if let Some(captions) = SparkCaption::from_data_store(ds, stream_id) {
1157        let x_offset = if params.captions_on_top {
1158            0
1159        } else if ds.application_proto == ApplicationProto::Http2 {
1160            params.spark_dimension_x + 30
1161        } else {
1162            params.spark_dimension_x
1163        };
1164
1165        captions.draw(
1166            &params.colors.caption,
1167            abs_dl_root,
1168            (upper_x + x_offset) as i32,
1169            dl_upper_y as i32,
1170        );
1171        captions.draw(
1172            &params.colors.caption,
1173            rel_dl_root,
1174            (upper_x + x_offset) as i32,
1175            dl_upper_y as i32,
1176        );
1177
1178        if upload_data {
1179            captions.draw(
1180                &params.colors.caption,
1181                abs_ul_root,
1182                (upper_x + x_offset) as i32,
1183                ul_upper_y as i32,
1184            );
1185            captions.draw(
1186                &params.colors.caption,
1187                rel_ul_root,
1188                (upper_x + x_offset) as i32,
1189                ul_upper_y as i32,
1190            );
1191        }
1192    }
1193}
1194
1195fn draw_stream_frames_line<DB: DrawingBackend>(
1196    stream_frames: &[(f64, u64)],
1197    abs_chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
1198    rel_chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
1199) {
1200    abs_chart
1201        .draw_series(LineSeries::new(stream_frames.to_vec(), PURPLE_500))
1202        .unwrap();
1203
1204    rel_chart
1205        .draw_series(LineSeries::new(stream_frames.to_vec(), PURPLE_500))
1206        .unwrap();
1207
1208    let circles: Vec<Circle<(f64, u64), i32>> = stream_frames
1209        .iter()
1210        .map(|point| Circle::new(*point, 2, PURPLE_500))
1211        .collect();
1212    rel_chart.draw_series(circles).unwrap();
1213}
1214
1215fn draw_buffered_data_line<DB: DrawingBackend>(
1216    stream_id: u64, ss: &SeriesStore, vantage_point: &VantagePoint,
1217    abs_chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
1218    rel_chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
1219) {
1220    let buffered_data_to_plot = match vantage_point {
1221        VantagePoint::Client => ss.stream_buffer_reads.get(&stream_id),
1222        VantagePoint::Server => ss.stream_buffer_writes.get(&stream_id),
1223    };
1224
1225    if let Some(buffered_data) = buffered_data_to_plot {
1226        abs_chart
1227            .draw_series(LineSeries::new(buffered_data.clone(), MAGENTA))
1228            .unwrap();
1229
1230        rel_chart
1231            .draw_series(LineSeries::new(buffered_data.clone(), MAGENTA))
1232            .unwrap();
1233
1234        rel_chart
1235            .draw_series(
1236                buffered_data
1237                    .iter()
1238                    .map(|point| TriangleMarker::new(*point, 3, MAGENTA)),
1239            )
1240            .unwrap();
1241    }
1242}
1243
1244fn draw_stream_max_line<DB: DrawingBackend>(
1245    stream_id: u64, ss: &SeriesStore, vantage_point: &VantagePoint,
1246    transmission_type: TransmissionType,
1247    abs_chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
1248    rel_chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
1249) {
1250    let stream_max_data_to_plot = match (vantage_point, transmission_type) {
1251        (VantagePoint::Client, TransmissionType::Download) =>
1252            ss.sent_stream_max_data.get(&stream_id),
1253        (VantagePoint::Client, TransmissionType::Upload) =>
1254            ss.received_stream_max_data.get(&stream_id),
1255        (VantagePoint::Server, TransmissionType::Download) =>
1256            ss.received_stream_max_data.get(&stream_id),
1257        (VantagePoint::Server, TransmissionType::Upload) =>
1258            ss.sent_stream_max_data.get(&stream_id),
1259    };
1260
1261    if let Some(stream_max_data) = stream_max_data_to_plot {
1262        abs_chart
1263            .draw_series(LineSeries::new(stream_max_data.clone(), MUSTARD))
1264            .unwrap();
1265
1266        rel_chart
1267            .draw_series(LineSeries::new(stream_max_data.clone(), MUSTARD))
1268            .unwrap();
1269    }
1270}
1271
1272fn draw_request_timing_lines<DB: DrawingBackend>(
1273    ds: &Datastore, stream_id: u64, y_max: u64,
1274    abs_chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
1275    rel_chart: &mut ChartContext<DB, Cartesian2d<RangedCoordf64, RangedCoordu64>>,
1276) {
1277    if let Some(req) = ds.http_requests.get(&stream_id) {
1278        if let Some(h) = req.time_first_headers_rx {
1279            let points = vec![(h, 0), (h, y_max)];
1280            abs_chart
1281                .draw_series(LineSeries::new(points, GREEN))
1282                .unwrap();
1283
1284            // don't care about rx events on the relative spark chart
1285        }
1286
1287        if let Some(h) = req.time_first_headers_tx {
1288            let points = vec![(h, 0), (h, y_max)];
1289            abs_chart
1290                .draw_series(LineSeries::new(points.clone(), CYAN))
1291                .unwrap();
1292            rel_chart
1293                .draw_series(LineSeries::new(points, CYAN))
1294                .unwrap();
1295        }
1296
1297        let (first_data_to_plot, last_data_to_plot) = match ds.vantage_point {
1298            VantagePoint::Client =>
1299                (req.time_first_data_rx, req.time_last_data_rx),
1300            VantagePoint::Server =>
1301                (req.time_first_data_tx, req.time_last_data_tx),
1302        };
1303
1304        if let Some(t) = first_data_to_plot {
1305            let points = vec![(t, 0), (t, y_max)];
1306            abs_chart
1307                .draw_series(LineSeries::new(points.clone(), ORANGE))
1308                .unwrap();
1309            rel_chart
1310                .draw_series(LineSeries::new(points, ORANGE))
1311                .unwrap();
1312        }
1313
1314        if let Some(t) = last_data_to_plot {
1315            let points = vec![(t, 0), (t, y_max)];
1316            abs_chart
1317                .draw_series(LineSeries::new(points.clone(), BROWN))
1318                .unwrap();
1319            rel_chart
1320                .draw_series(LineSeries::new(points, BROWN))
1321                .unwrap();
1322        }
1323    }
1324}