1use std::cell::Cell;
6use std::cmp::Ordering;
7use std::time::Duration;
8
9use base::cross_process_instant::CrossProcessInstant;
10use malloc_size_of_derive::MallocSizeOf;
11use profile_traits::time::{
12 ProfilerCategory, ProfilerChan, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType,
13 send_profile_data,
14};
15use script_traits::ProgressiveWebMetricType;
16use servo_config::opts;
17use servo_url::ServoUrl;
18
19pub const MAX_TASK_NS: u128 = 50000000;
22const INTERACTIVE_WINDOW_SECONDS: Duration = Duration::from_secs(10);
24
25pub trait ToMs<T> {
26 fn to_ms(&self) -> T;
27}
28
29impl ToMs<f64> for u64 {
30 fn to_ms(&self) -> f64 {
31 *self as f64 / 1000000.
32 }
33}
34
35fn set_metric(
36 pwm: &ProgressiveWebMetrics,
37 metadata: Option<TimerMetadata>,
38 metric_type: ProgressiveWebMetricType,
39 category: ProfilerCategory,
40 attr: &Cell<Option<CrossProcessInstant>>,
41 metric_time: CrossProcessInstant,
42 url: &ServoUrl,
43) {
44 attr.set(Some(metric_time));
45
46 send_profile_data(
48 category,
49 metadata,
50 pwm.time_profiler_chan(),
51 metric_time,
52 metric_time,
53 );
54
55 if opts::get().print_pwm {
57 let navigation_start = pwm
58 .navigation_start()
59 .unwrap_or_else(CrossProcessInstant::epoch);
60 println!(
61 "{:?} {:?} {:?}",
62 url,
63 metric_type,
64 (metric_time - navigation_start).as_seconds_f64()
65 );
66 }
67}
68
69#[derive(MallocSizeOf)]
79pub struct ProgressiveWebMetrics {
80 frame_type: TimerMetadataFrameType,
82 navigation_start: Option<CrossProcessInstant>,
84 dom_content_loaded: Cell<Option<CrossProcessInstant>>,
86 main_thread_available: Cell<Option<CrossProcessInstant>>,
88 time_to_interactive: Cell<Option<CrossProcessInstant>>,
90 first_paint: Cell<Option<CrossProcessInstant>>,
95 first_contentful_paint: Cell<Option<CrossProcessInstant>>,
99 #[ignore_malloc_size_of = "can't measure channels"]
100 time_profiler_chan: ProfilerChan,
101 url: ServoUrl,
102}
103
104#[derive(Clone, Copy, Debug, MallocSizeOf)]
105pub struct InteractiveWindow {
106 start: CrossProcessInstant,
107}
108
109impl Default for InteractiveWindow {
110 fn default() -> Self {
111 Self {
112 start: CrossProcessInstant::now(),
113 }
114 }
115}
116
117impl InteractiveWindow {
118 pub fn start_window(&mut self) {
123 self.start = CrossProcessInstant::now();
124 }
125
126 pub fn needs_check(&self) -> bool {
128 CrossProcessInstant::now() - self.start > INTERACTIVE_WINDOW_SECONDS
129 }
130
131 pub fn get_start(&self) -> CrossProcessInstant {
132 self.start
133 }
134}
135
136#[derive(Debug)]
137pub enum InteractiveFlag {
138 DOMContentLoaded,
139 TimeToInteractive(CrossProcessInstant),
140}
141
142impl ProgressiveWebMetrics {
143 pub fn new(
144 time_profiler_chan: ProfilerChan,
145 url: ServoUrl,
146 frame_type: TimerMetadataFrameType,
147 ) -> ProgressiveWebMetrics {
148 ProgressiveWebMetrics {
149 frame_type,
150 navigation_start: None,
151 dom_content_loaded: Cell::new(None),
152 main_thread_available: Cell::new(None),
153 time_to_interactive: Cell::new(None),
154 first_paint: Cell::new(None),
155 first_contentful_paint: Cell::new(None),
156 time_profiler_chan,
157 url,
158 }
159 }
160
161 fn make_metadata(&self, first_reflow: bool) -> TimerMetadata {
162 TimerMetadata {
163 url: self.url.to_string(),
164 iframe: self.frame_type.clone(),
165 incremental: match first_reflow {
166 true => TimerMetadataReflowType::FirstReflow,
167 false => TimerMetadataReflowType::Incremental,
168 },
169 }
170 }
171
172 pub fn set_dom_content_loaded(&self) {
173 if self.dom_content_loaded.get().is_none() {
174 self.dom_content_loaded
175 .set(Some(CrossProcessInstant::now()));
176 }
177 }
178
179 pub fn set_main_thread_available(&self, time: CrossProcessInstant) {
180 if self.main_thread_available.get().is_none() {
181 self.main_thread_available.set(Some(time));
182 }
183 }
184
185 pub fn dom_content_loaded(&self) -> Option<CrossProcessInstant> {
186 self.dom_content_loaded.get()
187 }
188
189 pub fn first_paint(&self) -> Option<CrossProcessInstant> {
190 self.first_paint.get()
191 }
192
193 pub fn first_contentful_paint(&self) -> Option<CrossProcessInstant> {
194 self.first_contentful_paint.get()
195 }
196
197 pub fn main_thread_available(&self) -> Option<CrossProcessInstant> {
198 self.main_thread_available.get()
199 }
200
201 pub fn set_first_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
202 set_metric(
203 self,
204 Some(self.make_metadata(first_reflow)),
205 ProgressiveWebMetricType::FirstPaint,
206 ProfilerCategory::TimeToFirstPaint,
207 &self.first_paint,
208 paint_time,
209 &self.url,
210 );
211 }
212
213 pub fn set_first_contentful_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
214 set_metric(
215 self,
216 Some(self.make_metadata(first_reflow)),
217 ProgressiveWebMetricType::FirstContentfulPaint,
218 ProfilerCategory::TimeToFirstContentfulPaint,
219 &self.first_contentful_paint,
220 paint_time,
221 &self.url,
222 );
223 }
224
225 pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
228 if self.get_tti().is_some() {
229 return;
230 }
231 match metric {
232 InteractiveFlag::DOMContentLoaded => self.set_dom_content_loaded(),
233 InteractiveFlag::TimeToInteractive(time) => self.set_main_thread_available(time),
234 }
235
236 let dcl = self.dom_content_loaded.get();
237 let mta = self.main_thread_available.get();
238 let (dcl, mta) = match (dcl, mta) {
239 (Some(dcl), Some(mta)) => (dcl, mta),
240 _ => return,
241 };
242 let metric_time = match dcl.partial_cmp(&mta) {
243 Some(Ordering::Less) => mta,
244 Some(_) => dcl,
245 None => panic!("no ordering possible. something bad happened"),
246 };
247 set_metric(
248 self,
249 Some(self.make_metadata(true)),
250 ProgressiveWebMetricType::TimeToInteractive,
251 ProfilerCategory::TimeToInteractive,
252 &self.time_to_interactive,
253 metric_time,
254 &self.url,
255 );
256 }
257
258 pub fn get_tti(&self) -> Option<CrossProcessInstant> {
259 self.time_to_interactive.get()
260 }
261
262 pub fn needs_tti(&self) -> bool {
263 self.get_tti().is_none()
264 }
265
266 pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
267 self.navigation_start
268 }
269
270 pub fn set_navigation_start(&mut self, time: CrossProcessInstant) {
271 self.navigation_start = Some(time);
272 }
273
274 pub fn time_profiler_chan(&self) -> &ProfilerChan {
275 &self.time_profiler_chan
276 }
277}
278
279#[cfg(test)]
280fn test_metrics() -> ProgressiveWebMetrics {
281 let (sender, _) = ipc_channel::ipc::channel().unwrap();
282 let profiler_chan = ProfilerChan(sender);
283 let mut metrics = ProgressiveWebMetrics::new(
284 profiler_chan,
285 ServoUrl::parse("about:blank").unwrap(),
286 TimerMetadataFrameType::RootWindow,
287 );
288
289 assert!((&metrics).navigation_start().is_none());
290 assert!(metrics.get_tti().is_none());
291 assert!(metrics.first_contentful_paint().is_none());
292 assert!(metrics.first_paint().is_none());
293
294 metrics.set_navigation_start(CrossProcessInstant::now());
295
296 metrics
297}
298
299#[test]
300fn test_set_dcl() {
301 let metrics = test_metrics();
302 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
303 let dcl = metrics.dom_content_loaded();
304 assert!(dcl.is_some());
305
306 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
308 assert_eq!(metrics.dom_content_loaded(), dcl);
309 assert_eq!(metrics.get_tti(), None);
310}
311
312#[test]
313fn test_set_mta() {
314 let metrics = test_metrics();
315 let now = CrossProcessInstant::now();
316 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
317 let main_thread_available_time = metrics.main_thread_available();
318 assert!(main_thread_available_time.is_some());
319 assert_eq!(main_thread_available_time, Some(now));
320
321 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
323 CrossProcessInstant::now(),
324 ));
325 assert_eq!(metrics.main_thread_available(), main_thread_available_time);
326 assert_eq!(metrics.get_tti(), None);
327}
328
329#[test]
330fn test_set_tti_dcl() {
331 let metrics = test_metrics();
332 let now = CrossProcessInstant::now();
333 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
334 let main_thread_available_time = metrics.main_thread_available();
335 assert!(main_thread_available_time.is_some());
336
337 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
338 let dom_content_loaded_time = metrics.dom_content_loaded();
339 assert!(dom_content_loaded_time.is_some());
340
341 assert_eq!(metrics.get_tti(), dom_content_loaded_time);
342}
343
344#[test]
345fn test_set_tti_mta() {
346 let metrics = test_metrics();
347 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
348 let dcl = metrics.dom_content_loaded();
349 assert!(dcl.is_some());
350
351 let time = CrossProcessInstant::now();
352 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time));
353 let mta = metrics.main_thread_available();
354 assert!(mta.is_some());
355
356 assert_eq!(metrics.get_tti(), mta);
357}
358
359#[test]
360fn test_first_paint_setter() {
361 let metrics = test_metrics();
362 metrics.set_first_paint(CrossProcessInstant::now(), false);
363 assert!(metrics.first_paint().is_some());
364}
365
366#[test]
367fn test_first_contentful_paint_setter() {
368 let metrics = test_metrics();
369 metrics.set_first_contentful_paint(CrossProcessInstant::now(), false);
370 assert!(metrics.first_contentful_paint().is_some());
371}