/* 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 http://mozilla.org/MPL/2.0/. */ //! The interning module provides a generic data structure //! interning container. It is similar in concept to a //! traditional string interning container, but it is //! specialized to the WR thread model. //! //! There is an Interner structure, that lives in the //! scene builder thread, and a DataStore structure //! that lives in the frame builder thread. //! //! Hashing, interning and handle creation is done by //! the interner structure during scene building. //! //! Delta changes for the interner are pushed during //! a transaction to the frame builder. The frame builder //! is then able to access the content of the interned //! handles quickly, via array indexing. //! //! Epoch tracking ensures that the garbage collection //! step which the interner uses to remove items is //! only invoked on items that the frame builder thread //! is no longer referencing. //! //! Items in the data store are stored in a traditional //! free-list structure, for content access and memory //! usage efficiency. //! //! The epoch is incremented each time a scene is //! built. The most recently used scene epoch is //! stored inside each handle. This is then used for //! cache invalidation. use crate::internal_types::FastHashMap; use malloc_size_of::MallocSizeOf; use std::fmt::Debug; use std::hash::Hash; use std::marker::PhantomData; use std::{ops, u64}; use crate::util::VecHelper; use crate::profiler::TransactionProfile; #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)] struct Epoch(u32); /// A list of updates to be applied to the data store, /// provided by the interning structure. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(MallocSizeOf)] pub struct UpdateList { /// Items to insert. pub insertions: Vec>, /// Items to remove. pub removals: Vec, } #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(MallocSizeOf)] pub struct Insertion { pub index: usize, pub uid: ItemUid, pub value: S, } #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(MallocSizeOf)] pub struct Removal { pub index: usize, pub uid: ItemUid, } impl UpdateList { fn new() -> UpdateList { UpdateList { insertions: Vec::new(), removals: Vec::new(), } } fn take_and_preallocate(&mut self) -> UpdateList { UpdateList { insertions: self.insertions.take_and_preallocate(), removals: self.removals.take_and_preallocate(), } } } /// A globally, unique identifier #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq)] pub struct ItemUid { uid: u64, } impl ItemUid { // Intended for debug usage only pub fn get_uid(&self) -> u64 { self.uid } } #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(Debug, MallocSizeOf, PartialEq)] pub struct Handle { index: u32, epoch: Epoch, _marker: PhantomData, } impl Clone for Handle { fn clone(&self) -> Self { Handle { index: self.index, epoch: self.epoch, _marker: self._marker, } } } impl Copy for Handle {} impl Handle { pub fn uid(&self) -> ItemUid { ItemUid { // The index in the freelist + the epoch it was interned generates a stable // unique id for an interned element. uid: ((self.index as u64) << 32) | self.epoch.0 as u64 } } } pub trait InternDebug { fn on_interned(&self, _uid: ItemUid) {} } /// The data store lives in the frame builder thread. It /// contains a free-list of items for fast access. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(MallocSizeOf)] pub struct DataStore { items: Vec>, } impl Default for DataStore { fn default() -> Self { DataStore { items: Vec::new(), } } } impl DataStore { /// Apply any updates from the scene builder thread to /// this data store. pub fn apply_updates( &mut self, update_list: UpdateList, profile: &mut TransactionProfile, ) { for insertion in update_list.insertions { self.items .entry(insertion.index) .set(Some(insertion.value.into())); } for removal in update_list.removals { self.items[removal.index] = None; } profile.set(I::PROFILE_COUNTER, self.items.len()); } } /// Retrieve an item from the store via handle impl ops::Index> for DataStore { type Output = I::StoreData; fn index(&self, handle: Handle) -> &I::StoreData { self.items[handle.index as usize].as_ref().expect("Bad datastore lookup") } } /// Retrieve a mutable item from the store via handle /// Retrieve an item from the store via handle impl ops::IndexMut> for DataStore { fn index_mut(&mut self, handle: Handle) -> &mut I::StoreData { self.items[handle.index as usize].as_mut().expect("Bad datastore lookup") } } #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(MallocSizeOf)] struct ItemDetails { /// Frame that this element was first interned interned_epoch: Epoch, /// Last frame this element was referenced (used to GC intern items) last_used_epoch: Epoch, /// Index into the freelist this item is located index: usize, /// Type marker for create_handle method _marker: PhantomData, } impl ItemDetails { /// Construct a stable handle value from the item details fn create_handle(&self) -> Handle { Handle { index: self.index as u32, epoch: self.interned_epoch, _marker: PhantomData, } } } /// The main interning data structure. This lives in the /// scene builder thread, and handles hashing and interning /// unique data structures. It also manages a free-list for /// the items in the data store, which is synchronized via /// an update list of additions / removals. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(MallocSizeOf)] pub struct Interner { /// Uniquely map an interning key to a handle map: FastHashMap>, /// List of free slots in the data store for re-use. free_list: Vec, /// Pending list of updates that need to be applied. update_list: UpdateList, /// The current epoch for the interner. current_epoch: Epoch, /// The information associated with each interned /// item that can be accessed by the interner. local_data: Vec, } impl Default for Interner { fn default() -> Self { Interner { map: FastHashMap::default(), free_list: Vec::new(), update_list: UpdateList::new(), current_epoch: Epoch(1), local_data: Vec::new(), } } } impl Interner { /// Intern a data structure, and return a handle to /// that data. The handle can then be stored in the /// frame builder, and safely accessed via the data /// store that lives in the frame builder thread. /// The provided closure is invoked to build the /// local data about an interned structure if the /// key isn't already interned. pub fn intern( &mut self, data: &I::Key, fun: F, ) -> Handle where F: FnOnce() -> I::InternData { // Use get_mut rather than entry here to avoid // cloning the (sometimes large) key in the common // case, where the data already exists in the interner. if let Some(details) = self.map.get_mut(data) { // Update the last referenced frame for this element details.last_used_epoch = self.current_epoch; // Return a stable handle value for dependency checking return details.create_handle(); } // We need to intern a new data item. First, find out // if there is a spare slot in the free-list that we // can use. Otherwise, append to the end of the list. let index = match self.free_list.pop() { Some(index) => index, None => self.local_data.len(), }; // Generate a handle for access via the data store. let handle = Handle { index: index as u32, epoch: self.current_epoch, _marker: PhantomData, }; let uid = handle.uid(); // Add a pending update to insert the new data. self.update_list.insertions.push(Insertion { index, uid, value: data.clone(), }); #[cfg(debug_assertions)] data.on_interned(uid); // Store this handle so the next time it is // interned, it gets re-used. self.map.insert(data.clone(), ItemDetails { interned_epoch: self.current_epoch, last_used_epoch: self.current_epoch, index, _marker: PhantomData, }); // Create the local data for this item that is // being interned. self.local_data.entry(index).set(fun()); handle } /// Retrieve the pending list of updates for an interner /// that need to be applied to the data store. Also run /// a GC step that removes old entries. pub fn end_frame_and_get_pending_updates(&mut self) -> UpdateList { let mut update_list = self.update_list.take_and_preallocate(); let free_list = &mut self.free_list; let current_epoch = self.current_epoch.0; // First, run a GC step. Walk through the handles, and // if we find any that haven't been used for some time, // remove them. If this ever shows up in profiles, we // can make the GC step partial (scan only part of the // map each frame). It also might make sense in the // future to adjust how long items remain in the cache // based on the current size of the list. self.map.retain(|_, details| { if details.last_used_epoch.0 + 10 < current_epoch { // To expire an item: // - Add index to the free-list for re-use. // - Add an update to the data store to invalidate this slot. // - Remove from the hash map. free_list.push(details.index); update_list.removals.push(Removal { index: details.index, uid: details.create_handle().uid(), }); return false; } true }); // Begin the next epoch self.current_epoch = Epoch(self.current_epoch.0 + 1); update_list } } /// Retrieve the local data for an item from the interner via handle impl ops::Index> for Interner { type Output = I::InternData; fn index(&self, handle: Handle) -> &I::InternData { &self.local_data[handle.index as usize] } } /// Meta-macro to enumerate the various interner identifiers and types. /// /// IMPORTANT: Keep this synchronized with the list in mozilla-central located at /// gfx/webrender_bindings/webrender_ffi.h /// /// Note that this could be a lot less verbose if concat_idents! were stable. :-( #[macro_export] macro_rules! enumerate_interners { ($macro_name: ident) => { $macro_name! { clip: ClipIntern, prim: PrimitiveKeyKind, normal_border: NormalBorderPrim, image_border: ImageBorder, image: Image, yuv_image: YuvImage, line_decoration: LineDecoration, linear_grad: LinearGradient, radial_grad: RadialGradient, conic_grad: ConicGradient, picture: Picture, text_run: TextRun, filter_data: FilterDataIntern, backdrop: Backdrop, } } } macro_rules! declare_interning_memory_report { ( $( $name:ident: $ty:ident, )+ ) => { /// #[repr(C)] #[derive(AddAssign, Clone, Debug, Default)] pub struct InternerSubReport { $( /// pub $name: usize, )+ } } } enumerate_interners!(declare_interning_memory_report); /// Memory report for interning-related data structures. /// cbindgen:derive-eq=false /// cbindgen:derive-ostream=false #[repr(C)] #[derive(Clone, Debug, Default)] pub struct InterningMemoryReport { /// pub interners: InternerSubReport, /// pub data_stores: InternerSubReport, } impl ::std::ops::AddAssign for InterningMemoryReport { fn add_assign(&mut self, other: InterningMemoryReport) { self.interners += other.interners; self.data_stores += other.data_stores; } } // The trick to make trait bounds configurable by features. mod dummy { #[cfg(not(feature = "capture"))] pub trait Serialize {} #[cfg(not(feature = "capture"))] impl Serialize for T {} #[cfg(not(feature = "replay"))] pub trait Deserialize<'a> {} #[cfg(not(feature = "replay"))] impl<'a, T> Deserialize<'a> for T {} } #[cfg(feature = "capture")] use serde::Serialize as InternSerialize; #[cfg(not(feature = "capture"))] use self::dummy::Serialize as InternSerialize; #[cfg(feature = "replay")] use serde::Deserialize as InternDeserialize; #[cfg(not(feature = "replay"))] use self::dummy::Deserialize as InternDeserialize; /// Implement `Internable` for a type that wants to participate in interning. pub trait Internable: MallocSizeOf { type Key: Eq + Hash + Clone + Debug + MallocSizeOf + InternDebug + InternSerialize + for<'a> InternDeserialize<'a>; type StoreData: From + MallocSizeOf + InternSerialize + for<'a> InternDeserialize<'a>; type InternData: MallocSizeOf + InternSerialize + for<'a> InternDeserialize<'a>; // Profile counter indices, see the list in profiler.rs const PROFILE_COUNTER: usize; }