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_base::id::LCPCandidateID;
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_first_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
209 set_metric(
210 self,
211 Some(self.make_metadata(first_reflow)),
212 ProgressiveWebMetricType::FirstPaint,
213 ProfilerCategory::TimeToFirstPaint,
214 &self.first_paint,
215 paint_time,
216 &self.url,
217 );
218 }
219
220 pub fn set_first_contentful_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
221 set_metric(
222 self,
223 Some(self.make_metadata(first_reflow)),
224 ProgressiveWebMetricType::FirstContentfulPaint,
225 ProfilerCategory::TimeToFirstContentfulPaint,
226 &self.first_contentful_paint,
227 paint_time,
228 &self.url,
229 );
230 }
231
232 pub fn set_largest_contentful_paint(
233 &self,
234 id: LCPCandidateID,
235 paint_time: CrossProcessInstant,
236 ) {
237 set_metric(
238 self,
239 Some(self.make_metadata(false)),
240 ProgressiveWebMetricType::LargestContentfulPaint { id },
241 ProfilerCategory::TimeToLargestContentfulPaint,
242 &self.largest_contentful_paint,
243 paint_time,
244 &self.url,
245 );
246 }
247
248 pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
251 if self.get_tti().is_some() {
252 return;
253 }
254 match metric {
255 InteractiveFlag::DOMContentLoaded => self.set_dom_content_loaded(),
256 InteractiveFlag::TimeToInteractive(time) => self.set_main_thread_available(time),
257 }
258
259 let dcl = self.dom_content_loaded.get();
260 let mta = self.main_thread_available.get();
261 let (dcl, mta) = match (dcl, mta) {
262 (Some(dcl), Some(mta)) => (dcl, mta),
263 _ => return,
264 };
265 let metric_time = match dcl.partial_cmp(&mta) {
266 Some(Ordering::Less) => mta,
267 Some(_) => dcl,
268 None => panic!("no ordering possible. something bad happened"),
269 };
270 set_metric(
271 self,
272 Some(self.make_metadata(true)),
273 ProgressiveWebMetricType::TimeToInteractive,
274 ProfilerCategory::TimeToInteractive,
275 &self.time_to_interactive,
276 metric_time,
277 &self.url,
278 );
279 }
280
281 pub fn get_tti(&self) -> Option<CrossProcessInstant> {
282 self.time_to_interactive.get()
283 }
284
285 pub fn needs_tti(&self) -> bool {
286 self.get_tti().is_none()
287 }
288
289 pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
290 self.navigation_start
291 }
292
293 pub fn set_navigation_start(&mut self, time: CrossProcessInstant) {
294 self.navigation_start = Some(time);
295 }
296
297 pub fn time_profiler_chan(&self) -> &ProfilerChan {
298 &self.time_profiler_chan
299 }
300}
301
302#[cfg(test)]
303mod test {
304 use servo_base::generic_channel;
305
306 use super::*;
307
308 fn test_metrics() -> ProgressiveWebMetrics {
309 let (sender, _) = generic_channel::channel().unwrap();
310 let profiler_chan = ProfilerChan(Some(sender));
311 let mut metrics = ProgressiveWebMetrics::new(
312 profiler_chan,
313 ServoUrl::parse("about:blank").unwrap(),
314 TimerMetadataFrameType::RootWindow,
315 );
316
317 assert!((&metrics).navigation_start().is_none());
318 assert!(metrics.get_tti().is_none());
319 assert!(metrics.first_contentful_paint().is_none());
320 assert!(metrics.first_paint().is_none());
321
322 metrics.set_navigation_start(CrossProcessInstant::now());
323
324 metrics
325 }
326
327 #[test]
328 fn test_set_dcl() {
329 let metrics = test_metrics();
330 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
331 let dcl = metrics.dom_content_loaded();
332 assert!(dcl.is_some());
333
334 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
336 assert_eq!(metrics.dom_content_loaded(), dcl);
337 assert_eq!(metrics.get_tti(), None);
338 }
339
340 #[test]
341 fn test_set_mta() {
342 let metrics = test_metrics();
343 let now = CrossProcessInstant::now();
344 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
345 let main_thread_available_time = metrics.main_thread_available();
346 assert!(main_thread_available_time.is_some());
347 assert_eq!(main_thread_available_time, Some(now));
348
349 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
351 CrossProcessInstant::now(),
352 ));
353 assert_eq!(metrics.main_thread_available(), main_thread_available_time);
354 assert_eq!(metrics.get_tti(), None);
355 }
356
357 #[test]
358 fn test_set_tti_dcl() {
359 let metrics = test_metrics();
360 let now = CrossProcessInstant::now();
361 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
362 let main_thread_available_time = metrics.main_thread_available();
363 assert!(main_thread_available_time.is_some());
364
365 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
366 let dom_content_loaded_time = metrics.dom_content_loaded();
367 assert!(dom_content_loaded_time.is_some());
368
369 assert_eq!(metrics.get_tti(), dom_content_loaded_time);
370 }
371
372 #[test]
373 fn test_set_tti_mta() {
374 let metrics = test_metrics();
375 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
376 let dcl = metrics.dom_content_loaded();
377 assert!(dcl.is_some());
378
379 let time = CrossProcessInstant::now();
380 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time));
381 let mta = metrics.main_thread_available();
382 assert!(mta.is_some());
383
384 assert_eq!(metrics.get_tti(), mta);
385 }
386
387 #[test]
388 fn test_first_paint_setter() {
389 let metrics = test_metrics();
390 metrics.set_first_paint(CrossProcessInstant::now(), false);
391 assert!(metrics.first_paint().is_some());
392 }
393
394 #[test]
395 fn test_first_contentful_paint_setter() {
396 let metrics = test_metrics();
397 metrics.set_first_contentful_paint(CrossProcessInstant::now(), false);
398 assert!(metrics.first_contentful_paint().is_some());
399 }
400}