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().debug.progressive_web_metrics {
56 let navigation_start = pwm
57 .navigation_start()
58 .unwrap_or_else(CrossProcessInstant::epoch);
59 let duration = (metric_time - navigation_start).as_seconds_f64();
60 println!("{url:?} {metric_type:?} {duration:?}s");
61 }
62}
63
64#[derive(MallocSizeOf)]
74pub struct ProgressiveWebMetrics {
75 frame_type: TimerMetadataFrameType,
77 navigation_start: Option<CrossProcessInstant>,
79 dom_content_loaded: Cell<Option<CrossProcessInstant>>,
81 main_thread_available: Cell<Option<CrossProcessInstant>>,
83 time_to_interactive: Cell<Option<CrossProcessInstant>>,
85 first_paint: Cell<Option<CrossProcessInstant>>,
90 first_contentful_paint: Cell<Option<CrossProcessInstant>>,
94 largest_contentful_paint: Cell<Option<CrossProcessInstant>>,
98 time_profiler_chan: ProfilerChan,
99 url: ServoUrl,
100}
101
102#[derive(Clone, Copy, Debug, MallocSizeOf)]
103pub struct InteractiveWindow {
104 start: CrossProcessInstant,
105}
106
107impl Default for InteractiveWindow {
108 fn default() -> Self {
109 Self {
110 start: CrossProcessInstant::now(),
111 }
112 }
113}
114
115impl InteractiveWindow {
116 pub fn start_window(&mut self) {
121 self.start = CrossProcessInstant::now();
122 }
123
124 pub fn needs_check(&self) -> bool {
126 CrossProcessInstant::now() - self.start > INTERACTIVE_WINDOW_SECONDS
127 }
128
129 pub fn get_start(&self) -> CrossProcessInstant {
130 self.start
131 }
132}
133
134#[derive(Debug)]
135pub enum InteractiveFlag {
136 DOMContentLoaded,
137 TimeToInteractive(CrossProcessInstant),
138}
139
140impl ProgressiveWebMetrics {
141 pub fn new(
142 time_profiler_chan: ProfilerChan,
143 url: ServoUrl,
144 frame_type: TimerMetadataFrameType,
145 ) -> ProgressiveWebMetrics {
146 ProgressiveWebMetrics {
147 frame_type,
148 navigation_start: None,
149 dom_content_loaded: Cell::new(None),
150 main_thread_available: Cell::new(None),
151 time_to_interactive: Cell::new(None),
152 first_paint: Cell::new(None),
153 first_contentful_paint: Cell::new(None),
154 largest_contentful_paint: Cell::new(None),
155 time_profiler_chan,
156 url,
157 }
158 }
159
160 fn make_metadata(&self, first_reflow: bool) -> TimerMetadata {
161 TimerMetadata {
162 url: self.url.to_string(),
163 iframe: self.frame_type.clone(),
164 incremental: match first_reflow {
165 true => TimerMetadataReflowType::FirstReflow,
166 false => TimerMetadataReflowType::Incremental,
167 },
168 }
169 }
170
171 pub fn set_dom_content_loaded(&self) {
172 if self.dom_content_loaded.get().is_none() {
173 self.dom_content_loaded
174 .set(Some(CrossProcessInstant::now()));
175 }
176 }
177
178 pub fn set_main_thread_available(&self, time: CrossProcessInstant) {
179 if self.main_thread_available.get().is_none() {
180 self.main_thread_available.set(Some(time));
181 }
182 }
183
184 pub fn dom_content_loaded(&self) -> Option<CrossProcessInstant> {
185 self.dom_content_loaded.get()
186 }
187
188 pub fn first_paint(&self) -> Option<CrossProcessInstant> {
189 self.first_paint.get()
190 }
191
192 pub fn first_contentful_paint(&self) -> Option<CrossProcessInstant> {
193 self.first_contentful_paint.get()
194 }
195
196 pub fn largest_contentful_paint(&self) -> Option<CrossProcessInstant> {
197 self.largest_contentful_paint.get()
198 }
199
200 pub fn main_thread_available(&self) -> Option<CrossProcessInstant> {
201 self.main_thread_available.get()
202 }
203
204 pub fn set_performance_paint_metric(
205 &self,
206 paint_time: CrossProcessInstant,
207 first_reflow: bool,
208 metric_type: ProgressiveWebMetricType,
209 ) {
210 match metric_type {
211 ProgressiveWebMetricType::FirstPaint => self.set_first_paint(paint_time, first_reflow),
212 ProgressiveWebMetricType::FirstContentfulPaint => {
213 self.set_first_contentful_paint(paint_time, first_reflow)
214 },
215 _ => {},
216 }
217 }
218
219 fn set_first_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
220 set_metric(
221 self,
222 Some(self.make_metadata(first_reflow)),
223 ProgressiveWebMetricType::FirstPaint,
224 ProfilerCategory::TimeToFirstPaint,
225 &self.first_paint,
226 paint_time,
227 &self.url,
228 );
229 }
230
231 fn set_first_contentful_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
232 set_metric(
233 self,
234 Some(self.make_metadata(first_reflow)),
235 ProgressiveWebMetricType::FirstContentfulPaint,
236 ProfilerCategory::TimeToFirstContentfulPaint,
237 &self.first_contentful_paint,
238 paint_time,
239 &self.url,
240 );
241 }
242
243 pub fn set_largest_contentful_paint(&self, paint_time: CrossProcessInstant, area: usize) {
244 set_metric(
245 self,
246 Some(self.make_metadata(false)),
247 ProgressiveWebMetricType::LargestContentfulPaint { area, url: None },
248 ProfilerCategory::TimeToLargestContentfulPaint,
249 &self.largest_contentful_paint,
250 paint_time,
251 &self.url,
252 );
253 }
254
255 pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
258 if self.get_tti().is_some() {
259 return;
260 }
261 match metric {
262 InteractiveFlag::DOMContentLoaded => self.set_dom_content_loaded(),
263 InteractiveFlag::TimeToInteractive(time) => self.set_main_thread_available(time),
264 }
265
266 let dcl = self.dom_content_loaded.get();
267 let mta = self.main_thread_available.get();
268 let (dcl, mta) = match (dcl, mta) {
269 (Some(dcl), Some(mta)) => (dcl, mta),
270 _ => return,
271 };
272 let metric_time = match dcl.partial_cmp(&mta) {
273 Some(Ordering::Less) => mta,
274 Some(_) => dcl,
275 None => panic!("no ordering possible. something bad happened"),
276 };
277 set_metric(
278 self,
279 Some(self.make_metadata(true)),
280 ProgressiveWebMetricType::TimeToInteractive,
281 ProfilerCategory::TimeToInteractive,
282 &self.time_to_interactive,
283 metric_time,
284 &self.url,
285 );
286 }
287
288 pub fn get_tti(&self) -> Option<CrossProcessInstant> {
289 self.time_to_interactive.get()
290 }
291
292 pub fn needs_tti(&self) -> bool {
293 self.get_tti().is_none()
294 }
295
296 pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
297 self.navigation_start
298 }
299
300 pub fn set_navigation_start(&mut self, time: CrossProcessInstant) {
301 self.navigation_start = Some(time);
302 }
303
304 pub fn time_profiler_chan(&self) -> &ProfilerChan {
305 &self.time_profiler_chan
306 }
307}
308
309#[cfg(test)]
310mod test {
311 use base::generic_channel;
312
313 use super::*;
314
315 fn test_metrics() -> ProgressiveWebMetrics {
316 let (sender, _) = generic_channel::channel().unwrap();
317 let profiler_chan = ProfilerChan(Some(sender));
318 let mut metrics = ProgressiveWebMetrics::new(
319 profiler_chan,
320 ServoUrl::parse("about:blank").unwrap(),
321 TimerMetadataFrameType::RootWindow,
322 );
323
324 assert!((&metrics).navigation_start().is_none());
325 assert!(metrics.get_tti().is_none());
326 assert!(metrics.first_contentful_paint().is_none());
327 assert!(metrics.first_paint().is_none());
328
329 metrics.set_navigation_start(CrossProcessInstant::now());
330
331 metrics
332 }
333
334 #[test]
335 fn test_set_dcl() {
336 let metrics = test_metrics();
337 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
338 let dcl = metrics.dom_content_loaded();
339 assert!(dcl.is_some());
340
341 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
343 assert_eq!(metrics.dom_content_loaded(), dcl);
344 assert_eq!(metrics.get_tti(), None);
345 }
346
347 #[test]
348 fn test_set_mta() {
349 let metrics = test_metrics();
350 let now = CrossProcessInstant::now();
351 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
352 let main_thread_available_time = metrics.main_thread_available();
353 assert!(main_thread_available_time.is_some());
354 assert_eq!(main_thread_available_time, Some(now));
355
356 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
358 CrossProcessInstant::now(),
359 ));
360 assert_eq!(metrics.main_thread_available(), main_thread_available_time);
361 assert_eq!(metrics.get_tti(), None);
362 }
363
364 #[test]
365 fn test_set_tti_dcl() {
366 let metrics = test_metrics();
367 let now = CrossProcessInstant::now();
368 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
369 let main_thread_available_time = metrics.main_thread_available();
370 assert!(main_thread_available_time.is_some());
371
372 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
373 let dom_content_loaded_time = metrics.dom_content_loaded();
374 assert!(dom_content_loaded_time.is_some());
375
376 assert_eq!(metrics.get_tti(), dom_content_loaded_time);
377 }
378
379 #[test]
380 fn test_set_tti_mta() {
381 let metrics = test_metrics();
382 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
383 let dcl = metrics.dom_content_loaded();
384 assert!(dcl.is_some());
385
386 let time = CrossProcessInstant::now();
387 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time));
388 let mta = metrics.main_thread_available();
389 assert!(mta.is_some());
390
391 assert_eq!(metrics.get_tti(), mta);
392 }
393
394 #[test]
395 fn test_first_paint_setter() {
396 let metrics = test_metrics();
397 metrics.set_first_paint(CrossProcessInstant::now(), false);
398 assert!(metrics.first_paint().is_some());
399 }
400
401 #[test]
402 fn test_first_contentful_paint_setter() {
403 let metrics = test_metrics();
404 metrics.set_first_contentful_paint(CrossProcessInstant::now(), false);
405 assert!(metrics.first_contentful_paint().is_some());
406 }
407}