1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! Request and response metrics tracking.

use crossbeam_utils::atomic::AtomicCell;
use std::{fmt, sync::Arc, time::Duration};

/// An object that holds status updates and progress statistics on a particular
/// request. A [`Metrics`] can be shared between threads, which allows an agent
/// thread to post updates to the object while consumers can read from the
/// object simultaneously.
///
/// Reading stats is not always guaranteed to be up-to-date.
#[derive(Clone)]
pub struct Metrics {
    pub(crate) inner: Arc<Inner>,
}

#[derive(Default)]
pub(crate) struct Inner {
    pub(crate) upload_progress: AtomicCell<f64>,
    pub(crate) upload_total: AtomicCell<f64>,
    pub(crate) download_progress: AtomicCell<f64>,
    pub(crate) download_total: AtomicCell<f64>,

    pub(crate) upload_speed: AtomicCell<f64>,
    pub(crate) download_speed: AtomicCell<f64>,

    // An overview of the six time values (taken from the curl documentation):
    //
    // curl_easy_perform()
    //     |
    //     |--NAMELOOKUP
    //     |--|--CONNECT
    //     |--|--|--APPCONNECT
    //     |--|--|--|--PRETRANSFER
    //     |--|--|--|--|--STARTTRANSFER
    //     |--|--|--|--|--|--TOTAL
    //     |--|--|--|--|--|--REDIRECT
    //
    // The numbers we expose in the API are a little more "high-level" than the
    // ones written here.
    pub(crate) namelookup_time: AtomicCell<f64>,
    pub(crate) connect_time: AtomicCell<f64>,
    pub(crate) appconnect_time: AtomicCell<f64>,
    pub(crate) pretransfer_time: AtomicCell<f64>,
    pub(crate) starttransfer_time: AtomicCell<f64>,
    pub(crate) total_time: AtomicCell<f64>,
    pub(crate) redirect_time: AtomicCell<f64>,
}

impl Metrics {
    pub(crate) fn new() -> Self {
        Self {
            inner: Arc::default(),
        }
    }

    /// Number of bytes uploaded / estimated total.
    pub fn upload_progress(&self) -> (u64, u64) {
        (
            self.inner.upload_progress.load() as u64,
            self.inner.upload_total.load() as u64,
        )
    }

    /// Average upload speed so far in bytes/second.
    pub fn upload_speed(&self) -> f64 {
        self.inner.upload_speed.load()
    }

    /// Number of bytes downloaded / estimated total.
    pub fn download_progress(&self) -> (u64, u64) {
        (
            self.inner.download_progress.load() as u64,
            self.inner.download_total.load() as u64,
        )
    }

    /// Average download speed so far in bytes/second.
    pub fn download_speed(&self) -> f64 {
        self.inner.download_speed.load()
    }

    /// Get the total time from the start of the request until DNS name
    /// resolving was completed.
    ///
    /// When a redirect is followed, the time from each request is added
    /// together.
    pub fn name_lookup_time(&self) -> Duration {
        Duration::from_secs_f64(self.inner.namelookup_time.load())
    }

    /// Get the amount of time taken to establish a connection to the server
    /// (not including TLS connection time).
    ///
    /// When a redirect is followed, the time from each request is added
    /// together.
    pub fn connect_time(&self) -> Duration {
        Duration::from_secs_f64(
            (self.inner.connect_time.load() - self.inner.namelookup_time.load()).max(0f64),
        )
    }

    /// Get the amount of time spent on TLS handshakes.
    ///
    /// When a redirect is followed, the time from each request is added
    /// together.
    pub fn secure_connect_time(&self) -> Duration {
        let app_connect_time = self.inner.appconnect_time.load();

        if app_connect_time > 0f64 {
            Duration::from_secs_f64(app_connect_time - self.inner.connect_time.load())
        } else {
            Duration::new(0, 0)
        }
    }

    /// Get the time it took from the start of the request until the first
    /// byte is either sent or received.
    ///
    /// When a redirect is followed, the time from each request is added
    /// together.
    pub fn transfer_start_time(&self) -> Duration {
        Duration::from_secs_f64(self.inner.starttransfer_time.load())
    }

    /// Get the amount of time spent performing the actual request transfer. The
    /// "transfer" includes both sending the request and receiving the response.
    ///
    /// When a redirect is followed, the time from each request is added
    /// together.
    pub fn transfer_time(&self) -> Duration {
        Duration::from_secs_f64(
            (self.inner.total_time.load() - self.inner.starttransfer_time.load()).max(0f64),
        )
    }

    /// Get the total time for the entire request. This will continuously
    /// increase until the entire response body is consumed and completed.
    ///
    /// When a redirect is followed, the time from each request is added
    /// together.
    pub fn total_time(&self) -> Duration {
        Duration::from_secs_f64(self.inner.total_time.load())
    }

    /// If automatic redirect following is enabled, gets the total time taken
    /// for all redirection steps including name lookup, connect, pretransfer
    /// and transfer before final transaction was started.
    pub fn redirect_time(&self) -> Duration {
        Duration::from_secs_f64(self.inner.redirect_time.load())
    }
}

impl fmt::Debug for Metrics {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Metrics")
            .field("upload_progress", &self.upload_progress())
            .field("upload_speed", &self.upload_speed())
            .field("download_progress", &self.download_progress())
            .field("download_speed", &self.download_speed())
            .field("name_lookup_time", &self.name_lookup_time())
            .field("connect_time", &self.connect_time())
            .field("secure_connect_time", &self.secure_connect_time())
            .field("transfer_start_time", &self.transfer_start_time())
            .field("transfer_time", &self.transfer_time())
            .field("total_time", &self.total_time())
            .field("redirect_time", &self.redirect_time())
            .finish()
    }
}