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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! Resource timing for HTTP requests, shared by the browser-side
//! [`Request::timing`](crate::protocol::Request::timing) and the API-side
//! `APIResponse::timing`.
//!
//! Split into its own module so the pure parse/merge logic sits in mutation
//! scope (`.cargo/mutants.toml`); the protocol objects it serves are only
//! testable against a live driver, which mutation testing excludes.
/// Resource timing information for an HTTP request.
///
/// All time values are in milliseconds relative to the navigation start.
/// A value of `-1` indicates the timing phase was not reached.
///
/// See: <https://playwright.dev/docs/api/class-request#request-timing>
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ResourceTiming {
/// Request start time in milliseconds since epoch.
pub start_time: f64,
/// Time immediately before the browser starts the domain name lookup
/// for the resource. The value is given in milliseconds relative to
/// `startTime`, -1 if not available.
pub domain_lookup_start: f64,
/// Time immediately after the browser starts the domain name lookup
/// for the resource. The value is given in milliseconds relative to
/// `startTime`, -1 if not available.
pub domain_lookup_end: f64,
/// Time immediately before the user agent starts establishing the
/// connection to the server to retrieve the resource.
pub connect_start: f64,
/// Time immediately after the browser starts the handshake process
/// to secure the current connection.
pub secure_connection_start: f64,
/// Time immediately after the browser finishes establishing the connection
/// to the server to retrieve the resource.
pub connect_end: f64,
/// Time immediately before the browser starts requesting the resource from
/// the server, cache, or local resource.
pub request_start: f64,
/// Time immediately after the browser starts requesting the resource from
/// the server, cache, or local resource.
pub response_start: f64,
/// Time immediately after the browser receives the last byte of the resource
/// or immediately before the transport connection is closed, whichever comes first.
pub response_end: f64,
}
impl ResourceTiming {
/// Folds the separately-reported response-end time into a timing object.
///
/// The driver builds the timing before the body has finished arriving, so
/// `responseEnd` is absent there and the real value comes alongside as
/// `responseEndTiming`. Both the browser-side and API-side paths merge it,
/// through here, so a `ResourceTiming` means the same thing whichever
/// origin produced it.
pub(crate) fn merge_response_end(
timing: &mut serde_json::Value,
response_end_timing: Option<f64>,
) {
if let (Some(end), Some(obj)) = (response_end_timing, timing.as_object_mut())
&& let Some(n) = serde_json::Number::from_f64(end)
{
obj.insert("responseEnd".to_string(), serde_json::Value::Number(n));
}
}
/// Parses the protocol's `ResourceTiming` shape.
///
/// Every phase is optional on the wire and absent means "not reached",
/// which Playwright represents as `-1` rather than a missing value. Shared
/// by [`Request::timing`] and `APIResponse::timing` so the two cannot
/// disagree about that defaulting. Callers that also have a
/// `responseEndTiming` should run [`Self::merge_response_end`] first.
///
/// Returns `None` if the value is not a timing object at all.
pub(crate) fn from_protocol(value: &serde_json::Value) -> Option<Self> {
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawTiming {
start_time: Option<f64>,
domain_lookup_start: Option<f64>,
domain_lookup_end: Option<f64>,
connect_start: Option<f64>,
connect_end: Option<f64>,
secure_connection_start: Option<f64>,
request_start: Option<f64>,
response_start: Option<f64>,
response_end: Option<f64>,
}
let raw: RawTiming = serde_json::from_value(value.clone()).ok()?;
Some(Self {
start_time: raw.start_time.unwrap_or(-1.0),
domain_lookup_start: raw.domain_lookup_start.unwrap_or(-1.0),
domain_lookup_end: raw.domain_lookup_end.unwrap_or(-1.0),
connect_start: raw.connect_start.unwrap_or(-1.0),
connect_end: raw.connect_end.unwrap_or(-1.0),
secure_connection_start: raw.secure_connection_start.unwrap_or(-1.0),
request_start: raw.request_start.unwrap_or(-1.0),
response_start: raw.response_start.unwrap_or(-1.0),
response_end: raw.response_end.unwrap_or(-1.0),
})
}
}
#[cfg(test)]
mod tests {
use super::ResourceTiming;
use serde_json::json;
#[test]
fn absent_phases_become_minus_one() {
// The driver omits phases that were never reached; Playwright's
// contract is that those read as -1, not as a missing value.
let timing = ResourceTiming::from_protocol(&json!({ "startTime": 1000.0 }))
.expect("an object with only startTime is still a timing");
assert_eq!(timing.start_time, 1000.0);
assert_eq!(timing.domain_lookup_start, -1.0);
assert_eq!(timing.domain_lookup_end, -1.0);
assert_eq!(timing.connect_start, -1.0);
assert_eq!(timing.connect_end, -1.0);
assert_eq!(timing.secure_connection_start, -1.0);
assert_eq!(timing.request_start, -1.0);
assert_eq!(timing.response_start, -1.0);
assert_eq!(timing.response_end, -1.0);
}
#[test]
fn every_phase_is_read_from_its_own_wire_name() {
// Pins the camelCase mapping: a swapped or misspelled rename would
// otherwise silently read as -1 and look like "phase not reached".
let timing = ResourceTiming::from_protocol(&json!({
"startTime": 1.0,
"domainLookupStart": 2.0,
"domainLookupEnd": 3.0,
"connectStart": 4.0,
"connectEnd": 5.0,
"secureConnectionStart": 6.0,
"requestStart": 7.0,
"responseStart": 8.0,
"responseEnd": 9.0,
}))
.expect("full timing parses");
assert_eq!(timing.start_time, 1.0);
assert_eq!(timing.domain_lookup_start, 2.0);
assert_eq!(timing.domain_lookup_end, 3.0);
assert_eq!(timing.connect_start, 4.0);
assert_eq!(timing.connect_end, 5.0);
assert_eq!(timing.secure_connection_start, 6.0);
assert_eq!(timing.request_start, 7.0);
assert_eq!(timing.response_start, 8.0);
assert_eq!(timing.response_end, 9.0);
}
#[test]
fn an_empty_timing_defaults_every_phase_including_start() {
// The other absent-phase test always supplies startTime, so this is
// the only place start_time's own default is exercised.
let timing = ResourceTiming::from_protocol(&json!({})).expect("empty object parses");
assert_eq!(timing.start_time, -1.0);
}
#[test]
fn a_non_object_is_not_a_timing() {
assert!(ResourceTiming::from_protocol(&json!("nope")).is_none());
assert!(ResourceTiming::from_protocol(&json!(null)).is_none());
}
}
#[cfg(test)]
mod merge_tests {
use super::ResourceTiming;
use serde_json::json;
#[test]
fn response_end_is_folded_into_the_timing() {
// The driver reports the end separately because the timing object is
// built before the body finishes. Both origins fold it back in here,
// so a ResourceTiming means the same thing whichever produced it.
let mut timing = json!({ "startTime": 100.0, "requestStart": 5.0 });
ResourceTiming::merge_response_end(&mut timing, Some(42.0));
let parsed = ResourceTiming::from_protocol(&timing).expect("parses");
assert_eq!(parsed.response_end, 42.0);
}
#[test]
fn without_a_reported_end_the_phase_stays_unreached() {
let mut timing = json!({ "startTime": 100.0 });
ResourceTiming::merge_response_end(&mut timing, None);
let parsed = ResourceTiming::from_protocol(&timing).expect("parses");
assert_eq!(parsed.response_end, -1.0);
}
#[test]
fn a_non_object_timing_is_left_alone() {
let mut timing = json!("not a timing");
ResourceTiming::merge_response_end(&mut timing, Some(42.0));
assert_eq!(timing, json!("not a timing"));
}
}