Skip to main content

qlog_dancer/plots/
pending.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//! Pending charts
28//! A single image file containing a view of pending vs. in-flight requests
29
30use crate::datastore::{
31    self,
32};
33
34use crate::plots::*;
35
36use crate::datastore::Datastore;
37use crate::seriesstore::SeriesStore;
38
39#[derive(Debug, Default)]
40pub struct PendingPlotParams {
41    pub clamp: ClampParams,
42    pub chart_size: ChartSize,
43    pub colors: PlotColors,
44    pub display_chart_title: bool,
45}
46
47struct PendingStack {
48    pub time: f64,
49    pub pending: i32,
50    pub in_flight: i32,
51}
52
53pub fn plot_pending(
54    params: &PendingPlotParams, filename: &str, ss: &SeriesStore, ds: &Datastore,
55    ty: &ChartOutputType,
56) {
57    // TODO: put some stuff in series store
58    let x_max = match ds.vantage_point {
59        datastore::VantagePoint::Client => ss.received_x_max,
60        datastore::VantagePoint::Server => ss.sent_x_max,
61    };
62
63    let mut y_max = 0;
64
65    let chart_config = ChartConfig {
66        title: "pending".into(),
67        input_filename: filename.into(),
68        clamp: params.clamp.clone(),
69        app_proto: ds.application_proto,
70        host: ds.host.clone(),
71        session_id: ds.session_id,
72        ty: ty.clone(),
73    };
74
75    chart_config.init_chart_dir();
76
77    let mut x = 0f64;
78    let step_size = 1f64;
79
80    let mut series = vec![];
81
82    while x < x_max {
83        let mut pending = 0;
84        let mut in_flight = 0;
85
86        for req in ds.http_requests.values() {
87            match (
88                req.time_discovery,
89                req.time_first_headers_tx,
90                req.time_fin_rx,
91            ) {
92                (Some(dt), Some(hdrs_tx), Some(fin)) => {
93                    if x >= dt && x < hdrs_tx && x < fin {
94                        pending += 1;
95                    } else if x >= hdrs_tx && x < fin {
96                        in_flight += 1;
97                    }
98                },
99
100                (Some(dt), None, None) =>
101                    if x >= dt {
102                        pending += 1;
103                    },
104
105                (Some(dt), Some(hdrs_tx), None) =>
106                    if x >= dt {
107                        pending += 1;
108                    } else if x >= hdrs_tx {
109                        in_flight += 1;
110                    },
111
112                _ => (),
113            }
114        }
115        series.push(PendingStack {
116            time: x,
117            pending,
118            in_flight,
119        });
120        x += step_size;
121
122        y_max = y_max.max(pending + in_flight);
123    }
124
125    // let size = ChartSize{ width: 1600, height: 600};
126
127    #[cfg(not(target_arch = "wasm32"))]
128    let chart_path = chart_config.chart_filepath();
129
130    #[cfg(not(target_arch = "wasm32"))]
131    let root = make_chart_bitmap_area(
132        &chart_path,
133        params.chart_size,
134        params.colors,
135        ChartMargin::default(),
136    );
137
138    #[cfg(target_arch = "wasm32")]
139    let canvas_id: String = chart_config.canvas_id().unwrap_or_default();
140
141    #[cfg(target_arch = "wasm32")]
142    let root =
143        make_chart_canvas_area(&canvas_id, params.colors, ChartMargin::default());
144
145    let mut builder = ChartBuilder::on(&root);
146    builder
147        .set_label_area_size(LabelAreaPosition::Left, 40)
148        .set_label_area_size(LabelAreaPosition::Bottom, 40);
149
150    if params.display_chart_title {
151        let caption =
152            format!("{} Pending vs. In-flight requests (stacked)", filename);
153        builder.caption(caption, chart_title_style(&params.colors.caption));
154    }
155
156    let mut ctx = builder.build_cartesian_2d(0f64..x_max, 0..y_max).unwrap();
157
158    ctx.configure_mesh()
159        .axis_style(params.colors.axis)
160        .bold_line_style(params.colors.bold_line.mix(0.5))
161        .light_line_style(params.colors.light_line.mix(0.2))
162        .label_style(chart_label_style(&params.colors.caption))
163        .draw()
164        .unwrap();
165
166    ctx.draw_series(series.iter().map(|stack| {
167        // make sure bars are narrow enough that they don't bleed over each
168        // step. If we don't we'll miss periods where there's no requests in
169        // flight.
170        let shrink = step_size / 4f64;
171        let x0 = stack.time + shrink;
172        let x1 = x0 + (step_size - shrink);
173
174        // Clippy wants to remove the variable assignment that helps understand
175        // WTF this stuff is doing, screw clippy.
176        #[allow(clippy::let_and_return)]
177        let in_flight_bar = Rectangle::new(
178            [(x0, 0), (x1, stack.in_flight)],
179            colors::FOREST_GREEN.filled(),
180        );
181        in_flight_bar
182    }))
183    .unwrap()
184    .label("In-flight requests")
185    .legend(|(x, y)| {
186        PathElement::new(vec![(x, y), (x + 20, y)], colors::FOREST_GREEN)
187    });
188
189    ctx.draw_series(series.iter().map(|stack| {
190        // make sure bars are narrow enough that they don't bleed over each
191        // step. If we don't we'll miss periods where there's no requests in
192        // flight.
193        let shrink = step_size / 3f64;
194        let x0 = stack.time + shrink;
195        let x1 = x0 + (step_size - shrink);
196
197        // pending is stack on top of in-flight
198        // Clippy wants to remove the variable assignment that helps understand
199        // WTF this stuff is doing, screw clippy.
200        #[allow(clippy::let_and_return)]
201        let pending_bar = Rectangle::new(
202            [(x0, stack.in_flight), (x1, stack.in_flight + stack.pending)],
203            colors::ORANGE.filled(),
204        );
205        pending_bar
206    }))
207    .unwrap()
208    .label("Pending requests")
209    .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], colors::ORANGE));
210
211    ctx.configure_series_labels()
212        .label_font(chart_label_style(&params.colors.caption))
213        .background_style(params.colors.fill.mix(0.8))
214        .border_style(params.colors.axis)
215        .position(SeriesLabelPosition::UpperRight)
216        .draw()
217        .unwrap();
218}