Skip to main content

qlog_dancer/plots/
stream_multiplex.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//! Multiplexing charts
28//! A single image file containing various renderings of stream and H3 DATA
29
30use log::warn;
31
32use crate::datastore::VantagePoint;
33use crate::plots::colors::*;
34use crate::plots::*;
35
36use crate::datastore::Datastore;
37use crate::seriesstore::SeriesStore;
38
39pub struct MultiplexPlotsParams {
40    pub clamp: ClampParams,
41    pub bidi_only: bool,
42    pub width: u32,
43    pub y_spacer: u32,
44    pub right_shrink_margin: u32,
45    pub combined_plot_height: u32,
46    pub frame_swimlane_height: u32,
47    pub bubbles_swimlane_height: u32,
48    pub colors: PlotColors,
49    pub area_margin: AreaMargin,
50}
51
52impl Default for MultiplexPlotsParams {
53    fn default() -> Self {
54        Self {
55            bidi_only: true,
56            width: 4000,
57            y_spacer: 20,
58            right_shrink_margin: 50,
59            combined_plot_height: 300,
60            frame_swimlane_height: 60,
61            bubbles_swimlane_height: 100,
62            clamp: Default::default(),
63            colors: Default::default(),
64            area_margin: AreaMargin { x: 40, y: 0 },
65        }
66    }
67}
68
69pub fn plot_stream_multiplexing(
70    params: &MultiplexPlotsParams, filename: &str, ss: &SeriesStore,
71    ds: &Datastore, ty: &ChartOutputType,
72) {
73    let (mut x_min, mut x_max) = match ds.vantage_point {
74        VantagePoint::Client => (ss.received_x_min, ss.received_x_max),
75        VantagePoint::Server => (ss.sent_x_min, ss.sent_x_max),
76    };
77
78    // Clamp plot x-axis if the user told us.
79    if let Some(s) = params.clamp.start {
80        x_min = s;
81    }
82
83    if let Some(e) = params.clamp.end {
84        x_max = x_max.min(e);
85    }
86
87    let http_requests = match ds.vantage_point {
88        VantagePoint::Client => &ds.http_requests,
89        VantagePoint::Server => &ds.http_requests,
90    };
91
92    let y_spacer = 50;
93
94    let data_frames_all_combined_x = params.width - params.right_shrink_margin;
95    let data_frames_all_combined_y: i32 = params.combined_plot_height as i32;
96
97    let data_frames_mini_offset_y = data_frames_all_combined_y + y_spacer;
98    let data_frames_mini_x = params.width - params.right_shrink_margin;
99    let data_frames_mini_y = params.frame_swimlane_height;
100
101    let bubbles_x = params.width - params.right_shrink_margin;
102    let bubbles_y = params.bubbles_swimlane_height;
103    let bubbles_offset_y =
104        data_frames_mini_offset_y + (http_requests.len() * 25) as i32;
105
106    let stream_frames_all_combined_x = params.width - params.right_shrink_margin;
107    let stream_frames_all_combined_y = params.combined_plot_height;
108    let stream_frames_all_combined_offset_y =
109        bubbles_offset_y + (http_requests.len() * 70) as i32;
110
111    let stream_frames_mini_offset_y = stream_frames_all_combined_offset_y +
112        stream_frames_all_combined_y as i32 +
113        y_spacer;
114    let stream_frames_mini_x = params.width - params.right_shrink_margin;
115    let stream_frames_mini_y = params.frame_swimlane_height;
116
117    #[cfg(not(target_arch = "wasm32"))]
118    let full_y =
119        stream_frames_mini_offset_y as u32 + (http_requests.len() * 25) as u32;
120
121    let chart_config = ChartConfig {
122        title: "stream-multiplexing".into(),
123        input_filename: filename.into(),
124        clamp: params.clamp.clone(),
125        app_proto: ds.application_proto,
126        host: ds.host.clone(),
127        session_id: ds.session_id,
128        ty: ty.clone(),
129    };
130
131    chart_config.init_chart_dir();
132
133    let margin = ChartMargin {
134        top: 20,
135        bottom: 20,
136        left: 20,
137        right: 20,
138    };
139
140    #[cfg(not(target_arch = "wasm32"))]
141    let size = ChartSize {
142        width: params.width,
143        height: full_y,
144    };
145
146    #[cfg(not(target_arch = "wasm32"))]
147    let chart_path = chart_config.chart_filepath();
148
149    #[cfg(not(target_arch = "wasm32"))]
150    let root = make_chart_bitmap_area(&chart_path, size, params.colors, margin);
151
152    #[cfg(target_arch = "wasm32")]
153    let canvas_id: String = chart_config.canvas_id().unwrap_or_default();
154
155    #[cfg(target_arch = "wasm32")]
156    let root = make_chart_canvas_area(&canvas_id, params.colors, margin);
157
158    let stream_frame_series = match ds.vantage_point {
159        VantagePoint::Client => &ss.received_stream_frames_series,
160        VantagePoint::Server => &ss.sent_stream_frames_series,
161    };
162
163    //// Plot the data frames first
164    let mut color_cyle = ColorCycle::default();
165
166    color_cyle.reset();
167
168    let data_frames_all_combined_temp_root = root.clone().shrink(
169        (0, 0),
170        (data_frames_all_combined_x, data_frames_all_combined_y),
171    );
172
173    let mut data_frames_all_combined_chart =
174        ChartBuilder::on(&data_frames_all_combined_temp_root)
175            .caption(
176                "DATA frame events",
177                chart_title_style(&params.colors.caption),
178            )
179            .x_label_area_size(params.area_margin.x)
180            .y_label_area_size(params.area_margin.y)
181            .build_cartesian_2d(x_min..x_max, 0..2)
182            .unwrap();
183
184    data_frames_all_combined_chart
185        .configure_mesh()
186        .disable_mesh()
187        .axis_style(params.colors.axis)
188        .label_style(chart_label_style(&params.colors.caption))
189        .draw()
190        .unwrap();
191
192    for (id, stub) in http_requests {
193        if params.bidi_only && (id & 0x2) != 0 {
194            continue;
195        }
196
197        let data_frames = match ds.vantage_point {
198            VantagePoint::Client => &stub.time_data_rx_set,
199            VantagePoint::Server => &stub.time_data_tx_set,
200        };
201
202        let style = color_cyle.next_color();
203
204        let lines = data_frames
205            .iter()
206            .map(|point| PathElement::new([(point.0, 2), (point.0, 0)], style));
207
208        data_frames_all_combined_chart
209            .draw_series(lines.clone())
210            .unwrap();
211    }
212
213    //// Now plot the stream frames combined
214    color_cyle.reset();
215
216    let data_frames_all_combined_temp_root = root.clone().shrink(
217        (0, stream_frames_all_combined_offset_y),
218        (stream_frames_all_combined_x, stream_frames_all_combined_y),
219    );
220
221    let mut stream_frames_all_combined_chart =
222        ChartBuilder::on(&data_frames_all_combined_temp_root)
223            .caption(
224                "STREAM frame events",
225                chart_title_style(&params.colors.caption),
226            )
227            .x_label_area_size(params.area_margin.x)
228            .y_label_area_size(params.area_margin.y)
229            .build_cartesian_2d(x_min..x_max, 0..1)
230            .unwrap();
231
232    stream_frames_all_combined_chart
233        .configure_mesh()
234        .disable_mesh()
235        .axis_style(params.colors.axis)
236        .label_style(chart_label_style(&params.colors.caption))
237        .draw()
238        .unwrap();
239
240    for (id, stream_frames) in stream_frame_series {
241        if params.bidi_only && (id & 0x2) != 0 {
242            continue;
243        }
244
245        let style = color_cyle.next_color();
246
247        let lines = stream_frames
248            .iter()
249            .map(|point| PathElement::new([(point.0, 2), (point.0, 0)], style));
250
251        stream_frames_all_combined_chart.draw_series(lines).unwrap();
252    }
253
254    //// now the mini charts
255    color_cyle.reset();
256
257    let mut upper_y = 0;
258    let mut bubbles_upper_y = 0;
259    let mini_chart_axis_y_range = 0..4;
260    let marker_y = 2;
261    let marker_size = 5;
262
263    let max_data_len = match ds.vantage_point {
264        VantagePoint::Client => ds.largest_data_frame_rx_length_global,
265        VantagePoint::Server => ds.largest_data_frame_tx_length_global,
266    };
267
268    if max_data_len == 0 {
269        warn!("max_data_len = 0, skipping remaining charts");
270        return;
271    }
272
273    for (id, stub) in http_requests {
274        if params.bidi_only && (id & 0x2) != 0 {
275            continue;
276        }
277
278        let data_frames = match ds.vantage_point {
279            VantagePoint::Client => &stub.time_data_rx_set,
280            VantagePoint::Server => &stub.time_data_tx_set,
281        };
282
283        let headers_init = match ds.vantage_point {
284            VantagePoint::Client => stub.time_first_headers_tx,
285            VantagePoint::Server => stub.time_first_headers_rx,
286        };
287
288        let headers_actioned = match ds.vantage_point {
289            VantagePoint::Client => stub.time_first_headers_rx,
290            VantagePoint::Server => stub.time_first_headers_tx,
291        };
292
293        let data_frames_temp_root = root.clone().shrink(
294            (0, upper_y + data_frames_mini_offset_y),
295            (data_frames_mini_x, data_frames_mini_y),
296        );
297
298        let stream_frames_temp_root = root.clone().shrink(
299            (0, upper_y + stream_frames_mini_offset_y),
300            (stream_frames_mini_x, stream_frames_mini_y),
301        );
302
303        upper_y += 20;
304
305        let mut data_frames_mini_independent_chart =
306            ChartBuilder::on(&data_frames_temp_root)
307                .x_label_area_size(params.area_margin.x)
308                .y_label_area_size(params.area_margin.y)
309                .build_cartesian_2d(x_min..x_max, mini_chart_axis_y_range.clone())
310                .unwrap();
311
312        data_frames_mini_independent_chart
313            .configure_mesh()
314            .disable_y_mesh()
315            .disable_y_axis()
316            .axis_style(params.colors.axis)
317            .draw()
318            .unwrap();
319
320        // let's jump ahead here and plot the mini chart header events, to avoid
321        // having to loop again later
322        let mut stream_frames_mini_independent_chart =
323            ChartBuilder::on(&stream_frames_temp_root)
324                .x_label_area_size(params.area_margin.x)
325                .y_label_area_size(params.area_margin.y)
326                .build_cartesian_2d(x_min..x_max, mini_chart_axis_y_range.clone())
327                .unwrap();
328
329        stream_frames_mini_independent_chart
330            .configure_mesh()
331            .disable_y_mesh()
332            .disable_y_axis()
333            .axis_style(params.colors.axis)
334            .draw()
335            .unwrap();
336
337        let style = color_cyle.next_color();
338
339        if let Some(h) = headers_init {
340            data_frames_mini_independent_chart
341                .draw_series([TriangleMarker::new(
342                    (h, marker_y),
343                    marker_size,
344                    style,
345                )])
346                .unwrap();
347
348            stream_frames_mini_independent_chart
349                .draw_series([TriangleMarker::new(
350                    (h, marker_y),
351                    marker_size,
352                    style,
353                )])
354                .unwrap();
355        }
356
357        if let Some(h) = headers_actioned {
358            data_frames_mini_independent_chart
359                .draw_series([TriangleMarker::new(
360                    (h, marker_y),
361                    marker_size,
362                    style,
363                )])
364                .unwrap();
365
366            stream_frames_mini_independent_chart
367                .draw_series([TriangleMarker::new(
368                    (h, marker_y),
369                    marker_size,
370                    style,
371                )])
372                .unwrap();
373        }
374
375        let lines = data_frames
376            .iter()
377            .map(|point| PathElement::new([(point.0, 2), (point.0, 0)], style));
378
379        data_frames_mini_independent_chart
380            .draw_series(lines.clone())
381            .unwrap();
382
383        if let Some(stream_frames) = stream_frame_series.get(id) {
384            let lines = stream_frames.iter().map(|point| {
385                PathElement::new([(point.0, 2), (point.0, 0)], style)
386            });
387
388            stream_frames_mini_independent_chart
389                .draw_series(lines.clone())
390                .unwrap();
391        }
392
393        let bubbles_temp_root = root.clone().shrink(
394            (0, bubbles_upper_y + bubbles_offset_y),
395            (bubbles_x, bubbles_y),
396        );
397
398        bubbles_upper_y += 70;
399
400        let mut bubbles_mini_chart = ChartBuilder::on(&bubbles_temp_root)
401            .x_label_area_size(params.area_margin.x)
402            .y_label_area_size(params.area_margin.y)
403            .build_cartesian_2d(
404                x_min..x_max,
405                0..max_data_len, /*0..30*/ /*mini_chart_axis_y_range.clone()*/
406            )
407            .unwrap();
408
409        bubbles_mini_chart
410            .configure_mesh()
411            .disable_y_mesh()
412            .disable_y_axis()
413            .axis_style(params.colors.axis)
414            .draw()
415            .unwrap();
416
417        // Integer conversion will floor, so when hardly any DATA is transferred
418        // avoid 0, which would cause a divide-by-0 later.
419        let bucket_size = if max_data_len >= 5 {
420            max_data_len / 5
421        } else {
422            1
423        };
424
425        let mut bubbles: Vec<Circle<(f64, i32), i32>> = vec![];
426        let mut lines: Vec<PathElement<(f64, i32)>> = vec![];
427        let mut lines2: Vec<PathElement<(f64, u64)>> = vec![];
428
429        let bubble_sizes = [1, 6, 11, 16, 21, 26, 31];
430
431        for (time, length) in data_frames {
432            // TODO: Replace this calculation with `div_ceil`.
433            let i: usize = (length / bucket_size) as usize +
434                usize::from(length % bucket_size != 0);
435            let i = i.saturating_sub(1);
436
437            // Bubbles beyond `x_max` still appear due to a plotting bug, so
438            // omit them.
439            if time > &x_max {
440                continue;
441            }
442
443            lines.push(PathElement::new(
444                [(*time, 2), (*time, 2 + bubble_sizes[i])],
445                style,
446            ));
447
448            lines2.push(PathElement::new([(*time, 0), (*time, *length)], style));
449
450            bubbles.push(Circle::new((*time, 2), bubble_sizes[i], style));
451        }
452
453        // bubbles_mini_chart.draw_series(bubbles).unwrap();
454        bubbles_mini_chart.draw_series(lines2).unwrap();
455    }
456}