Skip to main content

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}