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