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