servo-script 0.1.0

A component of the servo web-engine.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::rc::Rc;

use servo_arc::Arc as ServoArc;
use style::context::QuirksMode;
use style::shared_lock::SharedRwLock;
use style::stylesheets::{AllowImportRules, CssRule, Origin, StylesheetContents, UrlExtraData};
use stylo_atoms::Atom;

use crate::dom::node::NodeTraits;
use crate::dom::types::HTMLElement;
use crate::stylesheet_loader::ElementStylesheetLoader;

const MAX_LENGTH_OF_TEXT_INSERTED_INTO_TABLE: usize = 1024;
const UNIQUE_OWNED: usize = 2;

/// Using [`Atom`] as a cache key to avoid inefficient string content comparison. Although
/// the [`Atom`] is already based on reference counting, an extra [`Rc`] is introduced
/// to trace how many [`style::stylesheets::Stylesheet`]s of style elements are sharing
/// same [`StylesheetContents`], based on the following considerations:
/// * The reference count within [`Atom`] is dedicated to lifecycle management and is not
///   suitable for tracking the number of [`StylesheetContents`]s owners.
/// * The reference count within [`Atom`] is not publicly acessible.
#[derive(Clone, Eq, Hash, MallocSizeOf, PartialEq)]
pub(crate) struct StylesheetContentsCacheKey {
    #[conditional_malloc_size_of]
    stylesheet_text: Rc<Atom>,
    base_url: Atom,
    #[ignore_malloc_size_of = "defined in style crate"]
    quirks_mode: QuirksMode,
}

impl StylesheetContentsCacheKey {
    fn new(stylesheet_text: &str, base_url: &str, quirks_mode: QuirksMode) -> Self {
        // The stylesheet text may be quite lengthy, exceeding hundreds of kilobytes.
        // Instead of directly inserting such a huge string into AtomicString table,
        // take its hash value and use that. (This is not a cryptographic hash, so a
        // page could cause collisions if it wanted to.)
        let contents_atom = if stylesheet_text.len() > MAX_LENGTH_OF_TEXT_INSERTED_INTO_TABLE {
            let mut hasher = DefaultHasher::new();
            stylesheet_text.hash(&mut hasher);
            Atom::from(hasher.finish().to_string().as_str())
        } else {
            Atom::from(stylesheet_text)
        };

        Self {
            stylesheet_text: Rc::new(contents_atom),
            base_url: Atom::from(base_url),
            quirks_mode,
        }
    }

    pub(crate) fn is_uniquely_owned(&self) -> bool {
        // The cache itself already holds one reference.
        Rc::strong_count(&self.stylesheet_text) <= UNIQUE_OWNED
    }
}

thread_local! {
    static STYLESHEETCONTENTS_CACHE: RefCell<HashMap<StylesheetContentsCacheKey, ServoArc<StylesheetContents>>> =
       RefCell::default();
}

pub(crate) struct StylesheetContentsCache;

impl StylesheetContentsCache {
    fn contents_can_be_cached(contents: &StylesheetContents, shared_lock: &SharedRwLock) -> bool {
        let guard = shared_lock.read();
        let rules = contents.rules(&guard);
        // The copy-on-write can not be performed when the modification happens on the
        // imported stylesheet, because it containing cssom has no owner dom node.
        !(rules.is_empty() || rules.iter().any(|rule| matches!(rule, CssRule::Import(_))))
    }

    pub(crate) fn get_or_insert_with(
        stylesheet_text: &str,
        shared_lock: &SharedRwLock,
        url_data: UrlExtraData,
        quirks_mode: QuirksMode,
        element: &HTMLElement,
    ) -> (
        Option<StylesheetContentsCacheKey>,
        ServoArc<StylesheetContents>,
    ) {
        let cache_key =
            StylesheetContentsCacheKey::new(stylesheet_text, url_data.as_str(), quirks_mode);
        STYLESHEETCONTENTS_CACHE.with_borrow_mut(|stylesheetcontents_cache| {
            let entry = stylesheetcontents_cache.entry(cache_key);
            match entry {
                Entry::Occupied(occupied_entry) => {
                    // Use a copy of the cache key from `Entry` instead of the newly created one above
                    // to correctly update and track to owner count of `StylesheetContents`.
                    (
                        Some(occupied_entry.key().clone()),
                        occupied_entry.get().clone(),
                    )
                },
                Entry::Vacant(vacant_entry) => {
                    let contents = {
                        let _span = profile_traits::trace_span!("ParseStylesheet").entered();
                        StylesheetContents::from_str(
                            stylesheet_text,
                            url_data,
                            Origin::Author,
                            shared_lock,
                            Some(&ElementStylesheetLoader::new(element)),
                            Some(element.owner_window().css_error_reporter()),
                            quirks_mode,
                            AllowImportRules::Yes,
                            /* sanitized_output = */ None,
                        )
                    };
                    if Self::contents_can_be_cached(&contents, shared_lock) {
                        let occupied_entry = vacant_entry.insert_entry(contents.clone());
                        // Use a copy of the cache key from `Entry` instead of the newly created one above
                        // to correctly update and track to owner count of `StylesheetContents`.
                        (Some(occupied_entry.key().clone()), contents)
                    } else {
                        (None, contents)
                    }
                },
            }
        })
    }

    pub(crate) fn remove(cache_key: StylesheetContentsCacheKey) {
        STYLESHEETCONTENTS_CACHE.with_borrow_mut(|stylesheetcontents_cache| {
            stylesheetcontents_cache.remove(&cache_key)
        });
    }
}