layout/display_list/paint_timing_handler.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::{HashMap, HashSet};
6use std::rc::Rc;
7
8use app_units::Au;
9use euclid::Rect;
10use layout_api::{ContainerTimingRecord, LCPCandidate};
11use paint_api::display_list::PaintTimingReport;
12use rustc_hash::{FxHashMap, FxHashSet};
13use servo_arc::Arc as ServoArc;
14use servo_base::cross_process_instant::CrossProcessInstant;
15use servo_base::id::{ContainerTimingID, LCPCandidateID};
16use servo_geometry::FastLayoutTransform;
17use servo_url::ServoUrl;
18use style::dom::OpaqueNode;
19use style::properties::ComputedValues;
20use webrender_api::units::{LayoutRect, LayoutSize};
21
22use super::painted_region::PaintedRegion;
23use crate::fragment_tree::{BaseFragment, FragmentFlags, Tag};
24use crate::query::transform_f32_rectangle;
25
26/// <https://w3c.github.io/paint-timing/#pending-image-record>
27/// Different struct from spec, but fulfulling the same purpose.
28struct PendingImageRecord {
29 /// The image element this record belongs to.
30 /// for <https://w3c.github.io/paint-timing/#pending-image-record-element>
31 tag: Option<Tag>,
32 /// The image rect (adjusted for object-fit/object-position).
33 bounds: LayoutRect,
34 /// The element's content box.
35 clip_rect: LayoutRect,
36 /// Cumulative transform to root space, computed at collection time.
37 transform: FastLayoutTransform,
38 /// The image URL, if the image has one.
39 url: Option<ServoUrl>,
40 /// <https://w3c.github.io/paint-timing/#pending-image-record-loadtime>
41 load_time: Option<CrossProcessInstant>,
42 /// Intrinsic width, used for upscaling normalization.
43 natural_width: Option<Au>,
44 /// Intrinsic height, used for upscaling normalization.
45 natural_height: Option<Au>,
46}
47
48/// <https://w3c.github.io/paint-timing/#sec-recording-paint-timing>
49/// > Each Element has a set of owned text nodes, which is an ordered set of
50/// > Text nodes, initially empty.
51///
52/// This struct corresponds to an Element for accumulating set of owned text
53/// nodes by nearest ancestor box fragment's tag during display list building.
54struct TextRecord {
55 /// The tag of containing box fragment these texts belongs to.
56 tag: Tag,
57 /// <https://w3c.github.io/paint-timing/#set-of-owned-text-nodes>
58 /// Collection of border_boxes of all Text nodes accumulated
59 border_boxes: Vec<LayoutRect>,
60 /// The containing element's computed style
61 style: ServoArc<ComputedValues>,
62}
63
64enum LCPCandidateType<'a> {
65 Image(&'a PendingImageRecord),
66 Text,
67}
68
69/// <https://wicg.github.io/container-timing/#get-the-container-root-element>
70/// One link in the chain of Container Timing roots that marks a point in the fragment
71/// tree, nearest first. The chain is passed down the stacking context tree build and the
72/// paint traversal,
73#[derive(Debug)]
74pub(crate) struct ContainerTimingRootChain {
75 root: OpaqueNode,
76 parent: ContainerTimingRoots,
77}
78
79pub(crate) type ContainerTimingRoots = Option<Rc<ContainerTimingRootChain>>;
80
81impl ContainerTimingRootChain {
82 /// Returns the roots that apply to the contents of the fragment with the given `base`,
83 /// given the `inherited` roots that apply to the fragment itself.
84 pub(crate) fn for_contents_of(
85 inherited: &ContainerTimingRoots,
86 base: &BaseFragment,
87 ) -> ContainerTimingRoots {
88 let flags = base.flags;
89 let ignore = flags.contains(FragmentFlags::IS_CONTAINER_TIMING_IGNORE);
90 let root = base
91 .tag
92 .filter(|_| flags.contains(FragmentFlags::IS_CONTAINER_TIMING_ROOT))
93 .map(|tag| tag.node);
94
95 match root {
96 // An element can produce nested fragments (e.g. an inline box split around a
97 // block), so don't push the same root twice.
98 Some(root) if inherited.as_ref().is_some_and(|chain| chain.root == root) => {
99 inherited.clone()
100 },
101 // A root that is also ignored only reports to itself, not to enclosing roots.
102 Some(root) => Some(Rc::new(Self {
103 root,
104 parent: if ignore { None } else { inherited.clone() },
105 })),
106 // We're at an ignore point, so don't report to any enclosing roots.
107 None if ignore => None,
108 // Otherwise, just inherit the enclosing roots.
109 None => inherited.clone(),
110 }
111 }
112
113 /// Iterate over the roots in this chain, nearest first.
114 pub(crate) fn iter(&self) -> impl Iterator<Item = OpaqueNode> + '_ {
115 std::iter::successors(Some(self), |chain| chain.parent.as_deref()).map(|chain| chain.root)
116 }
117}
118
119/// The Container Timing state accumulated for one container root, i.e. one element
120/// carrying a `containertiming` attribute.
121///
122/// <https://wicg.github.io/container-timing/>
123struct ContainerRecord {
124 /// Descendant nodes that have already contributed painted area to this container, at
125 /// any point in the past. Once a node is in this set it is never reconsidered.
126 /// TODO: This algorithm could change in future <https://github.com/WICG/container-timing/issues/72>
127 contributing_nodes: FxHashSet<OpaqueNode>,
128 /// Everything painted inside this container so far, de-overlapped. Only ever grows by
129 /// merging in a genuinely new node's region (see `contributing_nodes`); persists across
130 /// display list builds.
131 painted_region: PaintedRegion,
132 /// The descendant whose paint most recently grew `painted_region`, for
133 /// `PerformanceContainerTiming::lastPaintedElement`.
134 last_painted_element: Option<OpaqueNode>,
135}
136
137/// Container Timing state accumulated for one container root during the display list
138/// build currently in progress. Its later added to ContainerTiming Record, see
139/// [`PaintTimingHandler::finalize_container_timing`].
140#[derive(Default)]
141struct PendingContainer {
142 /// Every fragment touched as part of building the display list, unioned per node.
143 node_regions: FxHashMap<OpaqueNode, PaintedRegion>,
144 /// Nodes touched while building the display list, in the order the display list traversal visited them.
145 /// Only used so that, if this build does grow the container, `last_painted_element`
146 /// is deterministic.
147 /// TODO: This will change: <https://github.com/WICG/container-timing/issues/53>
148 touched_order: Vec<OpaqueNode>,
149}
150
151pub(crate) struct PaintTimingHandler {
152 /// The rect of viewport.
153 viewport_rect: LayoutRect,
154 /// Whether the current display list contains paintable items.
155 is_document_paintable: bool,
156 /// Whether the current display list contains contentful items.
157 is_document_contentful: bool,
158 /// <https://www.w3.org/TR/paint-timing/#set-of-previously-reported-paints>
159 previously_reported_paints: PaintTimingReport,
160 /// Counter for generating unique LCP candidate UUIDs.
161 lcp_next_uuid: u64,
162 /// The LCP candidate, it may be a image or text.
163 lcp_candidate: Option<LCPCandidate>,
164 /// The set of image nodes that have been reported as LCP candidates.
165 reported_image_nodes: HashSet<OpaqueNode>,
166 /// <https://www.w3.org/TR/paint-timing/#images-pending-rendering>
167 images_pending_rendering: Vec<PendingImageRecord>,
168 /// <https://www.w3.org/TR/paint-timing/#set-of-elements-with-rendered-text>
169 /// The set of text nodes that have been reported as LCP candidates.
170 elements_with_rendered_text: HashSet<OpaqueNode>,
171 /// The set of pending text nodes that will fight for LCP candidate.
172 elements_with_pending_rendered_text: HashMap<OpaqueNode, TextRecord>,
173 /// Counter for generating unique container timing UUIDs.
174 container_timing_next_uuid: u64,
175 /// Accumulated Container Timing state, keyed by container root node. Persists
176 /// across display list builds, like the LCP candidate does.
177 container_timing_records: FxHashMap<OpaqueNode, ContainerRecord>,
178 /// Container Timing state for the display list build currently in progress, keyed by
179 /// container root node. Reset every build by [`Self::finalize_container_timing`].
180 container_timing_pending: FxHashMap<OpaqueNode, PendingContainer>,
181 /// Container roots whose painted area grew during the display list build currently
182 /// in progress. Drained by [`Self::take_container_timing_records`] once the build
183 /// finishes and the records are handed to script.
184 dirty_containers: FxHashSet<OpaqueNode>,
185}
186
187impl PaintTimingHandler {
188 pub(crate) fn new(viewport_size: LayoutSize) -> Self {
189 Self {
190 is_document_paintable: false,
191 is_document_contentful: false,
192 previously_reported_paints: PaintTimingReport::default(),
193 lcp_next_uuid: 0,
194 lcp_candidate: None,
195 viewport_rect: LayoutRect::from_size(viewport_size),
196 reported_image_nodes: HashSet::new(),
197 images_pending_rendering: Vec::new(),
198 elements_with_rendered_text: HashSet::new(),
199 elements_with_pending_rendered_text: HashMap::new(),
200 container_timing_next_uuid: 0,
201 container_timing_records: FxHashMap::default(),
202 container_timing_pending: FxHashMap::default(),
203 dirty_containers: FxHashSet::default(),
204 }
205 }
206
207 /// Marks the current display list as containing a paintable item.
208 pub(crate) fn mark_document_is_paintable(&mut self) {
209 self.is_document_paintable = true;
210 }
211
212 /// Marks the current display list as containing a contentful item.
213 pub(crate) fn mark_document_is_contentful(&mut self) {
214 self.is_document_contentful = true;
215 }
216
217 #[expect(clippy::too_many_arguments)]
218 pub(crate) fn append_image_record(
219 &mut self,
220 tag: Option<Tag>,
221 bounds: LayoutRect,
222 clip_rect: LayoutRect,
223 transform: FastLayoutTransform,
224 url: Option<ServoUrl>,
225 natural_width: Option<Au>,
226 natural_height: Option<Au>,
227 load_time: Option<CrossProcessInstant>,
228 ) {
229 self.images_pending_rendering.push(PendingImageRecord {
230 tag,
231 bounds,
232 clip_rect,
233 transform,
234 url,
235 natural_width,
236 natural_height,
237 load_time,
238 });
239 }
240
241 pub(crate) fn accumulate_text_rect(
242 &mut self,
243 tag: Tag,
244 rect: LayoutRect,
245 transform: FastLayoutTransform,
246 style: &ServoArc<ComputedValues>,
247 ) {
248 let border_box = transform_f32_rectangle(rect.to_rect(), transform)
249 .unwrap_or_default()
250 .to_box2d();
251 self.elements_with_pending_rendered_text
252 .entry(tag.node)
253 .and_modify(|record| {
254 record.border_boxes.push(border_box);
255 })
256 .or_insert(TextRecord {
257 tag,
258 border_boxes: vec![border_box],
259 style: ServoArc::clone(style),
260 });
261 }
262
263 /// <https://www.w3.org/TR/paint-timing/#paintable-bounding-rect>
264 fn paintable_bounding_rect(&self, bounds: LayoutRect) -> LayoutRect {
265 bounds.intersection(&self.viewport_rect).unwrap_or_default()
266 }
267
268 /// <https://www.w3.org/TR/paint-timing/#paintable>
269 pub(crate) fn paintable(&self, bounds: LayoutRect, opacity: f32) -> bool {
270 // An element el is paintable when all of the following apply:
271 // > el is being rendered.
272 // > el’s used visibility is visible.
273 // Note: Above conditions are met, as we selectively call this API.
274
275 // > el and all of its ancestors' used opacity is greater than zero.
276 if opacity <= 0.0 {
277 return false;
278 }
279
280 // > el’s paintable bounding rect intersects with the scrolling area of the document.
281 !self.paintable_bounding_rect(bounds).is_empty()
282 }
283
284 /// Marks wether the document is having paintable element
285 ///
286 /// <https://www.w3.org/TR/paint-timing/#paintable>
287 pub(crate) fn check_if_paintable(&mut self, bounds: LayoutRect, opacity: f32) -> bool {
288 if self.paintable(bounds, opacity) {
289 self.mark_document_is_paintable();
290 return true;
291 }
292 false
293 }
294
295 /// <https://www.w3.org/TR/largest-contentful-paint/#sec-effective-visual-size>
296 fn effective_visual_size(
297 &self,
298 intersection_rect: LayoutRect,
299 candidate_type: LCPCandidateType<'_>,
300 ) -> Option<f32> {
301 // Step 1. Let width be intersectionRect's width, rounded up to the
302 // nearest integer.
303 // Step 2. Let height be intersectionRect's height, rounded up to the
304 // nearest integer.
305 // Step 3. Let size be width * height.
306 let mut size = intersection_rect.area();
307
308 // Step 4. Let root be document's browsing context's top-level browsing
309 // context's active document.
310 // Note: This is not needed as we already have the viewport rect.
311
312 // Step 5. Let rootWidth be root's visual viewport's width,
313 // excluding any scrollbars.
314 // Step 6. Let rootHeight be root's visual viewport's height excluding
315 // any scrollbars.
316 // Step 7. If size is equal to rootWidth times rootHeight, return null.
317 if size >= self.viewport_rect.area() {
318 return None;
319 }
320
321 // Step 8: If imageRequest is not null, run the following steps to
322 // adjust for image position and upscaling.
323 // Note: This is skipped for Text aka the case of null request from specs
324 if let LCPCandidateType::Image(record) = candidate_type {
325 // TODO Step 8.1: If imageRequest's response's content length in bytes
326 // is less than size * 0.004, then return null. (Not Implemented)
327
328 // Step 8.2: Let concreteDimensions be imageRequest's concrete object
329 // size within element.
330 // Step 8.3: Let visibleDimensions be concreteDimensions, adjusted for
331 // positioning by object-position or background-position and element's
332 // content box.
333 // Note: bounds are already adjusted for positioning and content box
334 let visible_dimensions = record
335 .bounds
336 .intersection(&record.clip_rect)
337 .unwrap_or(LayoutRect::zero());
338
339 // Step 8.4: Let clientContentRect be the smallest DOMRectReadOnly
340 // containing visibleDimensions with element's transforms applied.
341 let client_content_rect =
342 transform_f32_rectangle(visible_dimensions.to_rect(), record.transform)
343 .unwrap_or_default();
344
345 // Step 8.5: Let intersectingClientContentRect be the intersection of
346 // clientContentRect with intersectionRect.
347 let intersecting_client_content_rect = client_content_rect
348 .intersection(&intersection_rect.to_rect())
349 .unwrap_or(Rect::zero());
350
351 // Step 8.6: Set width to intersectingClientContentRect's width,
352 // rounded up to the nearest integer.
353 // Step 8.7: Set height to intersectingClientContentRect's height,
354 // rounded up to the nearest integer.
355 // Step 8.8: Set size to width * height.
356 size = intersecting_client_content_rect.area();
357
358 // Step 8.9: Let naturalArea be imageRequest's natural width * imageRequest's natural height.
359 if let (Some(natural_width), Some(natural_height)) =
360 (record.natural_width, record.natural_height)
361 {
362 let natural_area = natural_width.to_f32_px() * natural_height.to_f32_px();
363
364 // Step 8.10: If naturalArea is 0, then return null.
365 if natural_area == 0.0 {
366 return None;
367 }
368 // Step 8.11: Let boundingClientArea be clientContentRect's width *
369 // clientContentRect's height.
370 let bounding_client_area =
371 client_content_rect.width() * client_content_rect.height();
372
373 // Step 8.12: Let scaleFactor be boundingClientArea / naturalArea.
374 let scale_factor = bounding_client_area / natural_area;
375
376 // Step 8.13: If scaleFactor is greater than 1, then divide size by scaleFactor.
377 if scale_factor > 1.0 {
378 size /= scale_factor;
379 }
380 }
381 }
382
383 // Step 9: Return an effective visual size result with size set to size,
384 // width set to width, and height set to height.
385 Some(size)
386 }
387
388 /// <https://www.w3.org/TR/largest-contentful-paint/#compute-a-new-largest-contentful-paint-candidate>
389 #[servo_tracing::instrument(
390 name = "Compute New LCP Candidate",
391 skip_all,
392 fields(
393 image_count = painted_images.len(),
394 text_count = painted_text_nodes.len(),
395 )
396 )]
397 fn compute_new_lcp_candidate(
398 &mut self,
399 painted_images: Vec<PendingImageRecord>,
400 painted_text_nodes: HashMap<OpaqueNode, TextRecord>,
401 ) -> Option<LCPCandidate> {
402 // Step 1. Let currentSize be currentCandidate’s size if
403 // currentCandidate is not null or 0 otherwise.
404 // Step 2. Let largestSize be currentSize.
405 let mut largest_size = self
406 .lcp_candidate
407 .as_ref()
408 .map_or(0.0, |candidate| candidate.area as f32);
409
410 // Step 3. Let newCandidate be null.
411 let mut new_candidate = None;
412
413 // Step 4. For each record of paintedImages:
414 for record in painted_images {
415 // Step 4.1. Let imageElement be record’s element.
416
417 // Step 4.2. If imageElement is not exposed for paint timing, given
418 // document, continue.
419 // Note: Satisfied, as the display-list builder only visits the
420 // connected DOM tree of the fully-active document being laid out.
421
422 // Step 4.3. Let intersectionRect be the value returned by the
423 // intersection rect algorithm using imageElement as the target
424 // and viewport as the root.
425 let intersection_rect =
426 transform_f32_rectangle(record.clip_rect.to_rect(), record.transform)
427 .unwrap_or_default()
428 .intersection(&self.viewport_rect.to_rect())
429 .map(|rect| rect.to_box2d())
430 .unwrap_or_default();
431
432 // Step 4.4. Let result be the effective visual size of imageElement
433 // given intersectionRect and record's request.
434 let result =
435 self.effective_visual_size(intersection_rect, LCPCandidateType::Image(&record));
436
437 // Step 4.5. If result is null, continue.
438 let Some(result) = result else {
439 continue;
440 };
441 // Step 4.6. If result's size is less than or equal to
442 // largestSize, continue.
443 if result <= largest_size {
444 continue;
445 }
446
447 // Step 4.7. Set largestSize to result’s size.
448 largest_size = result;
449
450 // Step 4.8. Set newCandidate to be a new largest contentful paint candidate ...
451 let uuid = self.lcp_next_uuid;
452 self.lcp_next_uuid += 1;
453 new_candidate = Some(LCPCandidate::new(
454 LCPCandidateID(uuid),
455 result as usize,
456 record.url,
457 record.tag.map(|tag| tag.node),
458 record.load_time,
459 ));
460 }
461
462 // Step 5. For each textNode of paintedTextNodes,
463 for (_, record) in painted_text_nodes {
464 // Step 5.1. If textNode is not exposed for paint timing, given
465 // document, continue.
466 // Note: Satisfied, as the display-list builder only visits the
467 // connected DOM tree of the fully-active document being laid out.
468
469 // Step 5.2. If textNode has alpha channel value <=0 or opacity
470 // value <=0:
471 if record.style.get_color().alpha <= 0.0 || record.style.slow_clone_opacity() <= 0.0 {
472 // Step 5.2.1. If textNode's text-shadow value is none,
473 // textNode's stroke-color value is transparent and textNode's
474 // stroke-image value is none, continue.
475 // TODO: Update when we implement the `stroke-color`/`stroke-image`
476 // properties, as of now they are default (`transparent`/`none`)
477 if record.style.get_inherited_text().text_shadow.0.is_empty() {
478 continue;
479 }
480 }
481 // Step 5.3. Let intersectionRect be the union of the border boxes of
482 // all Text nodes in textNode’s set of owned text nodes,
483 // intersected with the visual viewport.
484 let intersection_rect = record
485 .border_boxes
486 .into_iter()
487 .reduce(|a, b| a.union(&b))
488 .unwrap_or_default()
489 .intersection(&self.viewport_rect)
490 .unwrap_or_default();
491 // Step 5.4. Let result be the effective visual size of textNode
492 // given intersectionRect and null.
493 let result = self.effective_visual_size(intersection_rect, LCPCandidateType::Text);
494
495 // Step 5.5. If result is null, continue.
496 let Some(result) = result else {
497 continue;
498 };
499 // Step 5.6. If result's size is less than or equal to
500 // largestSize, continue.
501 if result <= largest_size {
502 continue;
503 }
504
505 // Step 5.7. Set largestSize to result’s size.
506 largest_size = result;
507
508 // Step 5.8. Set newCandidate to be a new largest contentful paint candidate ...
509 let uuid = self.lcp_next_uuid;
510 self.lcp_next_uuid += 1;
511 new_candidate = Some(LCPCandidate::new(
512 LCPCandidateID(uuid),
513 result as usize,
514 None,
515 Some(record.tag.node),
516 None,
517 ));
518 }
519
520 // TODO Step 6. If newCandidate is not null and currentSize is greater than 0:
521 // TODO Step 6.1. If newCandidate’s width minus currentCandidate’s
522 // width is less than or equal to 3, and newCandidate’s height minus
523 // currentCandidate’s height is less than or equal to 3, return null.
524
525 // Step 7. Return newCandidate.
526 new_candidate
527 }
528
529 /// <https://www.w3.org/TR/largest-contentful-paint/#sec-report-largest-contentful-paint>
530 fn report_largest_contentful_paint(
531 &mut self,
532 halt_lcp: bool,
533 painted_images: Vec<PendingImageRecord>,
534 painted_text_nodes: HashMap<OpaqueNode, TextRecord>,
535 ) {
536 // Step 1. Let window be document’s relevant global object.
537 // Step 2. If either of window’s has dispatched scroll event or has
538 // dispatched input event is true, return.
539 if halt_lcp {
540 return;
541 }
542
543 // Step 3. Let newCandidate be the result of computing a new largest
544 // contentful paint candidate given document, paintedImages,
545 // paintedTextNodes, and document’s current largest contentful paint
546 // candidate.
547 self.lcp_candidate = self.compute_new_lcp_candidate(painted_images, painted_text_nodes);
548
549 // Step 4. If newCandidate is null, return.
550 // Step 5. Set document’s current largest contentful paint candidate to
551 // newCandidate.
552 // Note: Step 4-5 are fulfilled by the assignment above, as the new
553 // candidate wether `None or Some` is computed and assigned to the
554 // current candidate.
555
556 // Step 6. Let entry be the result of creating a LargestContentfulPaint
557 // entry with newCandidate, paintTimingInfo, and document.
558 // Step 7. Queue the PerformanceEntry entry.
559 // Note: Step 6-7 are handled in script.
560 }
561
562 /// Accumulate a painted text or image fragment, painted by `node`, into the Container
563 /// Timing state of each of the enclosing `container_roots`.
564 ///
565 /// Called during display list building, unlike the LCP collection which only
566 /// accumulates here and computes in [`Self::mark_paint_timing`]. Container Timing has
567 /// no per-frame winner to pick: each fragment either adds new painted area to its
568 /// container or it does not, so the work can be done as fragments are visited.
569 ///
570 /// <https://wicg.github.io/container-timing/#maybe-update-last-new-painted-area>
571 #[servo_tracing::instrument(name = "Update Container Timing", skip_all)]
572 #[expect(clippy::too_many_arguments)]
573 pub(crate) fn update_container_timing(
574 &mut self,
575 container_roots: &ContainerTimingRootChain,
576 node: OpaqueNode,
577 bounds: LayoutRect,
578 clip_rect: LayoutRect,
579 transform: FastLayoutTransform,
580 natural_width: Option<Au>,
581 natural_height: Option<Au>,
582 ) {
583 let intersection_rect = transform_f32_rectangle(clip_rect.to_rect(), transform)
584 .unwrap_or_default()
585 .intersection(&self.viewport_rect.to_rect())
586 .map(|rect| rect.to_box2d())
587 .unwrap_or_default();
588
589 // Gate on the same "is this fragment eligible to be reported at all" check LCP
590 // uses.
591 let candidate_type = PendingImageRecord {
592 tag: None,
593 bounds,
594 clip_rect,
595 transform,
596 url: None,
597 load_time: None,
598 natural_width,
599 natural_height,
600 };
601 let candidate_type = match (natural_width, natural_height) {
602 (Some(_), Some(_)) => LCPCandidateType::Image(&candidate_type),
603 _ => LCPCandidateType::Text,
604 };
605 if self
606 .effective_visual_size(intersection_rect, candidate_type)
607 .is_none()
608 {
609 return;
610 }
611
612 // Report to every enclosing root, not just the nearest one.
613 for container_root in container_roots.iter() {
614 let pending = self
615 .container_timing_pending
616 .entry(container_root)
617 .or_default();
618 if !pending.node_regions.contains_key(&node) {
619 pending.touched_order.push(node);
620 }
621 pending
622 .node_regions
623 .entry(node)
624 .or_default()
625 .union(intersection_rect);
626 }
627 }
628
629 /// A node only counts the first time it is ever seen contributing to a container
630 /// (tracked by [`ContainerRecord::contributing_nodes`]).
631 fn finalize_container_timing(&mut self) {
632 for (container_root, pending) in std::mem::take(&mut self.container_timing_pending) {
633 let record = self
634 .container_timing_records
635 .entry(container_root)
636 .or_insert_with(|| ContainerRecord {
637 contributing_nodes: FxHashSet::default(),
638 painted_region: PaintedRegion::default(),
639 last_painted_element: None,
640 });
641
642 let mut grew = false;
643 for node in pending.touched_order {
644 if !record.contributing_nodes.insert(node) {
645 continue;
646 }
647 record.painted_region.merge(&pending.node_regions[&node]);
648 record.last_painted_element = Some(node);
649 grew = true;
650 }
651
652 if grew {
653 self.dirty_containers.insert(container_root);
654 }
655 }
656 }
657
658 pub(crate) fn take_container_timing_records(&mut self) -> Vec<ContainerTimingRecord> {
659 self.finalize_container_timing();
660 let mut records = Vec::with_capacity(self.dirty_containers.len());
661 for container_root in std::mem::take(&mut self.dirty_containers) {
662 let Some(record) = self.container_timing_records.get(&container_root) else {
663 continue;
664 };
665 let id = ContainerTimingID(self.container_timing_next_uuid);
666 self.container_timing_next_uuid += 1;
667 records.push(ContainerTimingRecord {
668 id,
669 size: record.painted_region.area(),
670 intersection_rect: record.painted_region.bounds(),
671 root_element: container_root,
672 last_painted_element: record.last_painted_element,
673 });
674 }
675 records
676 }
677
678 /// <https://www.w3.org/TR/paint-timing/#first-paint>
679 fn should_report_first_paint(&self) -> bool {
680 // Step 1. If document's set of previously reported paints contains
681 // "first-paint", then return false.
682 if self
683 .previously_reported_paints
684 .contains(PaintTimingReport::FirstPaint)
685 {
686 return false;
687 }
688 // Step 2. If document contains at least one element that is
689 // paintable, then return true.
690 // Step 3. Otherwise, return false.
691 self.is_document_paintable
692 }
693
694 /// <https://www.w3.org/TR/paint-timing/#first-contentful-paint>
695 fn should_report_first_contentful_paint(&self) -> bool {
696 // Step 1. If document's set of previously reported paints contains
697 // "first-contentful-paint", then return false.
698 if self
699 .previously_reported_paints
700 .contains(PaintTimingReport::FirstContentfulPaint)
701 {
702 return false;
703 }
704 // Step 2. If document contains at least one element that is both
705 // paintable and contentful, then return true.
706 // Step 3. Otherwise, return false.
707 self.is_document_paintable && self.is_document_contentful
708 }
709
710 /// <https://www.w3.org/TR/paint-timing/#mark-paint-timing>
711 ///
712 /// Note: Step 10 is not in the current version of the specifications.
713 /// Refer <https://github.com/w3c/paint-timing/issues/122> for details on
714 /// the issue and for the modified steps yet to be merged.
715 #[servo_tracing::instrument(name = "Mark Paint Timing", skip_all, fields(halt_lcp = halt_lcp))]
716 pub(crate) fn mark_paint_timing(
717 &mut self,
718 paint_timing_eligible: bool,
719 halt_lcp: bool,
720 ) -> PaintTimingReport {
721 // Step 1. If the document's browsing context is not paint-timing
722 // eligible, return.
723 if !paint_timing_eligible {
724 return PaintTimingReport::default();
725 }
726
727 // Step 2. Let paintTimingInfo be a new paint timing info, whose
728 // rendering update end time is the current high resolution time given
729 // document's relevant global object.
730 // Note: This is satisfied in the script thread.
731
732 // Step 3. Let paintedImages be a new ordered set.
733 // Step 4. Let paintedTextNodes be a new ordered set.
734
735 // Step 5. For each record in doc's images pending rendering list:
736 // Step 5.1. If record's request is available and ready to be painted,
737 // then run the following steps:
738 // Note: Only available images are accumulated, hence it is fulfilled.
739 // Step 5.1.1. Append record to paintedImages.
740 // Step 5.1.2. Remove record from doc's images pending rendering list.
741 let painted_images: Vec<_> = std::mem::take(&mut self.images_pending_rendering)
742 .into_iter()
743 .filter(|record| {
744 record
745 .tag
746 .is_none_or(|tag| self.reported_image_nodes.insert(tag.node))
747 })
748 .collect();
749
750 // Step 6. For each Element element in doc's descendants:
751 // Step 6.1. If element is contained in doc's set of elements with
752 // rendered text, continue.
753 // Step 6.2. If element's set of owned text nodes is empty, continue.
754 // Step 6.3. Append element to doc's set of elements with rendered text.
755 // Step 6.4. Append element to paintedTextNodes.
756 let painted_text_nodes: HashMap<_, _> =
757 std::mem::take(&mut self.elements_with_pending_rendered_text)
758 .into_iter()
759 .filter(|(node, _record)| self.elements_with_rendered_text.insert(*node))
760 .collect();
761
762 // Step 7. Let reportedPaints be the document’s set of previously
763 // reported paints. (Directly accessing)
764
765 // TODO Step 8. Let frameTimingInfo be document’s current frame timing info.
766 // TODO Step 9. Set document’s current frame timing info to null.
767
768 // Step 10. Let flushPaintTimings be the following steps:
769
770 // Note: A new PaintTimingReport to accumulate the paints.
771 let mut paint_timing_report = PaintTimingReport::default();
772
773 // Step 10.1. If document should report first paint, then:
774 // Note: This step is not in the current version of the specifications,
775 // are waiting to be merged at w3c/paint-timing#123.
776 if self.should_report_first_paint() {
777 // Step 10.1.1. Report paint timing given document, "first-paint",
778 // and paintTimingInfo.
779 paint_timing_report.insert(PaintTimingReport::FirstPaint);
780 }
781
782 // Step 10.2. If document should report first contentful paint, then:
783 if self.should_report_first_contentful_paint() {
784 // Step 10.2.1. Report paint timing given document,
785 // "first-contentful-paint", and paintTimingInfo.
786 paint_timing_report.insert(PaintTimingReport::FirstContentfulPaint);
787 }
788
789 // Step 10.3. Report largest contentful paint given document,
790 // paintTimingInfo, paintedImages and paintedTextNodes.
791 self.report_largest_contentful_paint(halt_lcp, painted_images, painted_text_nodes);
792
793 // Note: Append the newly reported paints aka [`PaintTimingReport`] to
794 // the document's set of previously reported paints.
795 self.previously_reported_paints |= paint_timing_report;
796
797 paint_timing_report
798 }
799
800 pub(crate) fn largest_contentful_paint_candidate(&self) -> Option<LCPCandidate> {
801 self.lcp_candidate.clone()
802 }
803}