Bug 1560520 - limit the size of WebRender's glyph cache. r?kvark
authorLee Salzman <lsalzman@mozilla.com>
Wed, 17 Jul 2019 20:52:44 +0300
changeset 544696 472132eeee82d6ee852d27867151e5fb92057ffe
parent 544695 b7de4c7bb68d5f89ff1600cf103ef0d911dfc638
child 544697 74c9645ad84e607738dce8601efe207051bd5f9f
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskvark
bugs1560520
milestone69.0
Bug 1560520 - limit the size of WebRender's glyph cache. r?kvark Differential Revision: https://phabricator.services.mozilla.com//D37982
gfx/wr/webrender/src/glyph_cache.rs
gfx/wr/webrender/src/glyph_rasterizer/mod.rs
gfx/wr/webrender/src/glyph_rasterizer/no_pathfinder.rs
gfx/wr/webrender/src/glyph_rasterizer/pathfinder.rs
gfx/wr/webrender/src/render_backend.rs
gfx/wr/webrender/src/render_task.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/resource_cache.rs
gfx/wr/webrender/src/texture_cache.rs
--- a/gfx/wr/webrender/src/glyph_cache.rs
+++ b/gfx/wr/webrender/src/glyph_cache.rs
@@ -1,16 +1,17 @@
 /* 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/. */
 
 #[cfg(feature = "pathfinder")]
 use crate::api::units::DeviceIntPoint;
 use crate::glyph_rasterizer::{FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer};
 use crate::internal_types::FastHashMap;
+use crate::render_backend::{FrameId, FrameStamp};
 use crate::render_task::RenderTaskCache;
 #[cfg(feature = "pathfinder")]
 use crate::render_task::RenderTaskCacheKey;
 use crate::resource_cache::ResourceClassCache;
 use std::sync::Arc;
 use crate::texture_cache::{EvictionNotice, TextureCache};
 #[cfg(not(feature = "pathfinder"))]
 use crate::texture_cache::TextureCacheHandle;
@@ -38,77 +39,142 @@ pub enum GlyphCacheEntry {
     // A glyph that has been submitted to the font backend for rasterization,
     // but is still pending a result.
     #[allow(dead_code)]
     Pending,
 }
 
 impl GlyphCacheEntry {
     #[cfg(feature = "pathfinder")]
-    fn is_allocated(&self, texture_cache: &TextureCache, render_task_cache: &RenderTaskCache)
-                    -> bool {
+    fn get_allocated_size(&self, texture_cache: &TextureCache, render_task_cache: &RenderTaskCache)
+                          -> Option<usize> {
         match *self {
             GlyphCacheEntry::Cached(ref glyph) => {
                 let render_task_cache_key = &glyph.render_task_cache_key;
-                render_task_cache.cache_item_is_allocated_for_render_task(texture_cache,
-                                                                          &render_task_cache_key)
+                render_task_cache.get_allocated_size_for_render_task(texture_cache,
+                                                                     &render_task_cache_key)
             }
-            GlyphCacheEntry::Pending => true,
+            GlyphCacheEntry::Pending => Some(0),
             // If the cache only has blank glyphs left, just get rid of it.
-            GlyphCacheEntry::Blank => false,
+            GlyphCacheEntry::Blank => None,
+        }
+    }
+
+    #[cfg(feature = "pathfinder")]
+    fn mark_unused(&self, _: &mut TextureCache) {
+    }
+
+    #[cfg(not(feature = "pathfinder"))]
+    fn get_allocated_size(&self, texture_cache: &TextureCache, _: &RenderTaskCache)
+                          -> Option<usize> {
+        match *self {
+            GlyphCacheEntry::Cached(ref glyph) => {
+                texture_cache.get_allocated_size(&glyph.texture_cache_handle)
+            }
+            GlyphCacheEntry::Pending => Some(0),
+            // If the cache only has blank glyphs left, just get rid of it.
+            GlyphCacheEntry::Blank => None,
         }
     }
 
     #[cfg(not(feature = "pathfinder"))]
-    fn is_allocated(&self, texture_cache: &TextureCache, _: &RenderTaskCache) -> bool {
-        match *self {
-            GlyphCacheEntry::Cached(ref glyph) => {
-                texture_cache.is_allocated(&glyph.texture_cache_handle)
-            }
-            GlyphCacheEntry::Pending => true,
-            // If the cache only has blank glyphs left, just get rid of it.
-            GlyphCacheEntry::Blank => false,
+    fn mark_unused(&self, texture_cache: &mut TextureCache) {
+        if let GlyphCacheEntry::Cached(ref glyph) = *self {
+            texture_cache.mark_unused(&glyph.texture_cache_handle);
         }
     }
 }
 
 #[allow(dead_code)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Clone)]
 pub enum CachedGlyphData {
     Memory(Arc<Vec<u8>>),
     Gpu,
 }
 
-pub type GlyphKeyCache = ResourceClassCache<GlyphKey, GlyphCacheEntry, EvictionNotice>;
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Default)]
+pub struct GlyphKeyCacheInfo {
+    eviction_notice: EvictionNotice,
+    last_frame_used: FrameId,
+    bytes_used: usize,
+}
+
+pub type GlyphKeyCache = ResourceClassCache<GlyphKey, GlyphCacheEntry, GlyphKeyCacheInfo>;
 
 impl GlyphKeyCache {
+    const DIRTY: usize = !0;
+
     pub fn eviction_notice(&self) -> &EvictionNotice {
-        &self.user_data
+        &self.user_data.eviction_notice
+    }
+
+    fn clear_glyphs(&mut self, texture_cache: &mut TextureCache) {
+        for (_, entry) in self.iter() {
+            entry.mark_unused(texture_cache);
+        }
+        self.clear();
+        self.user_data.bytes_used = 0;
+    }
+
+    pub fn add_glyph(&mut self, key: GlyphKey, value: GlyphCacheEntry) {
+        self.insert(key, value);
+        self.user_data.bytes_used = Self::DIRTY;
+    }
+
+    fn clear_evicted(
+        &mut self,
+        texture_cache: &TextureCache,
+        render_task_cache: &RenderTaskCache,
+    ) {
+        if self.eviction_notice().check() || self.user_data.bytes_used == Self::DIRTY {
+            // If there are evictions, filter out any glyphs evicted from the
+            // texture cache from the glyph key cache.
+            let mut usage = 0;
+            self.retain(|_, entry| {
+                let size = entry.get_allocated_size(texture_cache, render_task_cache);
+                usage += size.unwrap_or(0);
+                size.is_some()
+            });
+            self.user_data.bytes_used = usage;
+        }
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct GlyphCache {
     glyph_key_caches: FastHashMap<FontInstance, GlyphKeyCache>,
+    current_frame: FrameId,
+    bytes_used: usize,
+    max_bytes_used: usize,
 }
 
 impl GlyphCache {
-    pub fn new() -> Self {
+    /// The default space usage threshold, in bytes, after which to start pruning away old fonts.
+    pub const DEFAULT_MAX_BYTES_USED: usize = 5 * 1024 * 1024;
+
+    pub fn new(max_bytes_used: usize) -> Self {
         GlyphCache {
             glyph_key_caches: FastHashMap::default(),
+            current_frame: Default::default(),
+            bytes_used: 0,
+            max_bytes_used,
         }
     }
 
     pub fn get_glyph_key_cache_for_font_mut(&mut self, font: FontInstance) -> &mut GlyphKeyCache {
-        self.glyph_key_caches
-            .entry(font)
-            .or_insert_with(GlyphKeyCache::new)
+        let cache = self.glyph_key_caches
+                        .entry(font)
+                        .or_insert_with(GlyphKeyCache::new);
+        cache.user_data.last_frame_used = self.current_frame;
+        cache
     }
 
     pub fn get_glyph_key_cache_for_font(&self, font: &FontInstance) -> &GlyphKeyCache {
         self.glyph_key_caches
             .get(font)
             .expect("BUG: Unable to find glyph key cache!")
     }
 
@@ -116,60 +182,85 @@ impl GlyphCache {
         for (_, glyph_key_cache) in &mut self.glyph_key_caches {
             glyph_key_cache.clear()
         }
         // We use this in on_memory_pressure where retaining memory allocations
         // isn't desirable, so we completely remove the hash map instead of clearing it.
         self.glyph_key_caches = FastHashMap::default();
     }
 
-    pub fn clear_fonts<F>(&mut self, key_fun: F)
+    pub fn clear_fonts<F>(&mut self, texture_cache: &mut TextureCache, key_fun: F)
     where
         for<'r> F: Fn(&'r &FontInstance) -> bool,
     {
         self.glyph_key_caches.retain(|k, cache| {
             let should_clear = key_fun(&k);
             if !should_clear {
                 return true;
             }
 
-            cache.clear();
+            cache.clear_glyphs(texture_cache);
             false
         })
     }
 
-    // Clear out evicted entries from glyph key caches and, if possible,
-    // also remove entirely any subsequently empty glyph key caches.
+    /// Clear out evicted entries from glyph key caches.
     fn clear_evicted(
         &mut self,
         texture_cache: &TextureCache,
         render_task_cache: &RenderTaskCache,
-        glyph_rasterizer: &mut GlyphRasterizer,
     ) {
-        self.glyph_key_caches.retain(|key, cache| {
+        let mut usage = 0;
+        for cache in self.glyph_key_caches.values_mut() {
             // Scan for any glyph key caches that have evictions.
-            if cache.eviction_notice().check() {
-                // If there are evictions, filter out any glyphs evicted from the
-                // texture cache from the glyph key cache.
-                let mut keep_cache = false;
-                cache.retain(|_, entry| {
-                    let keep_glyph = entry.is_allocated(texture_cache, render_task_cache);
-                    keep_cache |= keep_glyph;
-                    keep_glyph
-                });
-                if !keep_cache {
-                    glyph_rasterizer.delete_font_instance(key);
-                }
-                // Only keep the glyph key cache if it still has valid glyphs.
-                keep_cache
+            cache.clear_evicted(texture_cache, render_task_cache);
+            usage += cache.user_data.bytes_used;
+        }
+        self.bytes_used = usage;
+    }
+
+    /// If possible, remove entirely any empty glyph key caches.
+    fn clear_empty_caches(&mut self, glyph_rasterizer: &mut GlyphRasterizer) {
+        self.glyph_key_caches.retain(|key, cache| {
+            // Discard the glyph key cache if it has no valid glyphs.
+            if cache.is_empty() {
+                glyph_rasterizer.delete_font_instance(key);
+                false
             } else {
                 true
             }
         });
     }
 
+    /// Check the total space usage of the glyph cache. If it exceeds the maximum usage threshold,
+    /// then start clearing the oldest glyphs until below the threshold.
+    fn prune_excess_usage(&mut self, texture_cache: &mut TextureCache) {
+        if self.bytes_used < self.max_bytes_used {
+            return;
+        }
+        // Usage is above the threshold. Get a last-recently-used ordered list of caches to clear.
+        let mut caches: Vec<_> = self.glyph_key_caches.values_mut().collect();
+        caches.sort_unstable_by(|a, b| {
+            a.user_data.last_frame_used.cmp(&b.user_data.last_frame_used)
+        });
+        // Clear out the oldest caches until below the threshold.
+        for cache in caches {
+            self.bytes_used -= cache.user_data.bytes_used;
+            cache.clear_glyphs(texture_cache);
+            if self.bytes_used < self.max_bytes_used {
+                break;
+            }
+        }
+    }
+
     pub fn begin_frame(&mut self,
-                       texture_cache: &TextureCache,
+                       stamp: FrameStamp,
+                       texture_cache: &mut TextureCache,
                        render_task_cache: &RenderTaskCache,
                        glyph_rasterizer: &mut GlyphRasterizer) {
-        self.clear_evicted(texture_cache, render_task_cache, glyph_rasterizer);
+        self.current_frame = stamp.frame_id();
+        self.clear_evicted(texture_cache, render_task_cache);
+        self.prune_excess_usage(texture_cache);
+        // Clearing evicted glyphs and pruning excess usage might have produced empty caches,
+        // so get rid of them if possible.
+        self.clear_empty_caches(glyph_rasterizer);
     }
 }
--- a/gfx/wr/webrender/src/glyph_rasterizer/mod.rs
+++ b/gfx/wr/webrender/src/glyph_rasterizer/mod.rs
@@ -858,17 +858,17 @@ mod test_glyph_rasterizer {
         let worker = ThreadPoolBuilder::new()
             .thread_name(|idx|{ format!("WRWorker#{}", idx) })
             .start_handler(move |idx| {
                 register_thread_with_profiler(format!("WRWorker#{}", idx));
             })
             .build();
         let workers = Arc::new(worker.unwrap());
         let mut glyph_rasterizer = GlyphRasterizer::new(workers).unwrap();
-        let mut glyph_cache = GlyphCache::new();
+        let mut glyph_cache = GlyphCache::new(GlyphCache::DEFAULT_MAX_BYTES_USED);
         let mut gpu_cache = GpuCache::new_for_testing();
         let mut texture_cache = TextureCache::new_for_testing(2048, 1024);
         let mut render_task_cache = RenderTaskCache::new();
         let mut render_task_tree = RenderTaskGraph::new(FrameId::INVALID, &RenderTaskGraphCounters::new());
         let mut font_file =
             File::open("../wrench/reftests/text/VeraBd.ttf").expect("Couldn't open font file");
         let mut font_data = vec![];
         font_file
--- a/gfx/wr/webrender/src/glyph_rasterizer/no_pathfinder.rs
+++ b/gfx/wr/webrender/src/glyph_rasterizer/no_pathfinder.rs
@@ -15,17 +15,16 @@ use crate::platform::font::FontContext;
 use crate::glyph_rasterizer::{FontInstance, FontContexts, GlyphKey};
 use crate::glyph_rasterizer::{GlyphRasterizer, GlyphRasterJob, GlyphRasterJobs};
 use crate::glyph_cache::{GlyphCache, CachedGlyphInfo, GlyphCacheEntry};
 use crate::resource_cache::CachedImageData;
 use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction};
 use crate::gpu_cache::GpuCache;
 use crate::render_task::{RenderTaskGraph, RenderTaskCache};
 use crate::profiler::TextureCacheProfileCounters;
-use std::collections::hash_map::Entry;
 
 impl FontContexts {
     /// Get access to the font context associated to the current thread.
     pub fn lock_current_context(&self) -> MutexGuard<FontContext> {
         let id = self.current_worker_id();
         self.lock_context(id)
     }
 
@@ -52,42 +51,33 @@ impl GlyphRasterizer {
                 .has_font(&font.font_key)
         );
         let mut new_glyphs = Vec::new();
 
         let glyph_key_cache = glyph_cache.get_glyph_key_cache_for_font_mut(font.clone());
 
         // select glyphs that have not been requested yet.
         for key in glyph_keys {
-            match glyph_key_cache.entry(key.clone()) {
-                Entry::Occupied(entry) => {
-                    let value = entry.into_mut();
-                    match *value {
-                        GlyphCacheEntry::Cached(ref glyph) => {
-                            // Skip the glyph if it is already has a valid texture cache handle.
-                            if !texture_cache.request(&glyph.texture_cache_handle, gpu_cache) {
-                                continue;
-                            }
+            if let Some(entry) = glyph_key_cache.try_get(key) {
+                match entry {
+                    GlyphCacheEntry::Cached(ref glyph) => {
+                        // Skip the glyph if it is already has a valid texture cache handle.
+                        if !texture_cache.request(&glyph.texture_cache_handle, gpu_cache) {
+                            continue;
                         }
-                        // Otherwise, skip the entry if it is blank or pending.
-                        GlyphCacheEntry::Blank | GlyphCacheEntry::Pending => continue,
+                        // This case gets hit when we already rasterized the glyph, but the
+                        // glyph has been evicted from the texture cache. Just force it to
+                        // pending so it gets rematerialized.
                     }
-
-                    // This case gets hit when we already rasterized the glyph, but the
-                    // glyph has been evicted from the texture cache. Just force it to
-                    // pending so it gets rematerialized.
-                    *value = GlyphCacheEntry::Pending;
-                    new_glyphs.push((*key).clone());
-                }
-                Entry::Vacant(entry) => {
-                    // This is the first time we've seen the glyph, so mark it as pending.
-                    entry.insert(GlyphCacheEntry::Pending);
-                    new_glyphs.push((*key).clone());
+                    // Otherwise, skip the entry if it is blank or pending.
+                    GlyphCacheEntry::Blank | GlyphCacheEntry::Pending => continue,
                 }
             }
+            new_glyphs.push(key.clone());
+            glyph_key_cache.add_glyph(key.clone(), GlyphCacheEntry::Pending);
         }
 
         if new_glyphs.is_empty() {
             return;
         }
 
         self.pending_glyphs += 1;
 
@@ -192,17 +182,17 @@ impl GlyphRasterizer {
                             Eviction::Auto,
                         );
                         GlyphCacheEntry::Cached(CachedGlyphInfo {
                             texture_cache_handle,
                             format: glyph.format,
                         })
                     }
                 };
-                glyph_key_cache.insert(key, glyph_info);
+                glyph_key_cache.add_glyph(key, glyph_info);
             }
         }
 
         // Now that we are done with the critical path (rendering the glyphs),
         // we can schedule removing the fonts if needed.
         self.remove_dead_fonts();
     }
 }
--- a/gfx/wr/webrender/src/glyph_rasterizer/pathfinder.rs
+++ b/gfx/wr/webrender/src/glyph_rasterizer/pathfinder.rs
@@ -14,17 +14,16 @@ use crate::render_task::{RenderTask, Ren
                          RenderTaskCacheEntryHandle, RenderTaskCacheKeyKind, RenderTaskId,
                          RenderTaskLocation};
 use crate::resource_cache::CacheItem;
 use std::ops::Deref;
 use std::sync::{Arc, Mutex, MutexGuard};
 use crate::glyph_rasterizer::AddFont;
 use crate::internal_types::ResourceCacheError;
 use crate::glyph_cache::{GlyphCache, GlyphCacheEntry, CachedGlyphInfo};
-use std::collections::hash_map::Entry;
 use std::f32;
 use crate::glyph_rasterizer::{FontInstance, GlyphRasterizer, GlyphFormat, GlyphKey, FontContexts};
 use crate::texture_cache::TextureCache;
 use crate::gpu_cache::GpuCache;
 use crate::profiler::TextureCacheProfileCounters;
 
 /// Should match macOS 10.13 High Sierra.
 ///
@@ -164,30 +163,19 @@ impl GlyphRasterizer {
         let glyph_key_cache = glyph_cache.get_glyph_key_cache_for_font_mut(font.clone());
 
         let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
         let scale = font.oversized_scale_factor(x_scale, y_scale) as f32;
 
         // select glyphs that have not been requested yet.
         for glyph_key in glyph_keys {
             let mut cached_glyph_info = None;
-            match glyph_key_cache.entry(glyph_key.clone()) {
-                Entry::Occupied(entry) => {
-                    let value = entry.into_mut();
-                    match *value {
-                        GlyphCacheEntry::Cached(ref glyph_info) => {
-                            cached_glyph_info = Some(glyph_info.clone())
-                        }
-                        GlyphCacheEntry::Blank | GlyphCacheEntry::Pending => {}
-                    }
-                }
-                Entry::Vacant(_) => {}
-            }
-
-            if cached_glyph_info.is_none() {
+            if let Some(GlyphCacheEntry::Cached(ref info)) = glyph_key_cache.try_get(glyph_key) {
+                cached_glyph_info = Some(info.clone());
+            } else {
                 let pathfinder_font_context = self.font_contexts.lock_pathfinder_context();
 
                 let pathfinder_font_instance = pathfinder_font_renderer::FontInstance {
                     font_key: font.font_key.clone(),
                     size: font.size.scale_by(scale.recip()),
                 };
 
                 // TODO: pathfinder will need to support 2D subpixel offset
@@ -229,17 +217,17 @@ impl GlyphRasterizer {
                     ) {
                         Ok(_) => GlyphCacheEntry::Cached(glyph_info),
                         Err(_) => GlyphCacheEntry::Blank,
                     }
                 }
                 None => GlyphCacheEntry::Blank,
             };
 
-            glyph_key_cache.insert(glyph_key.clone(), handle);
+            glyph_key_cache.add_glyph(glyph_key.clone(), handle);
         }
     }
 
     pub fn resolve_glyphs(
         &mut self,
         _: &mut GlyphCache,
         _: &mut TextureCache,
         _: &mut GpuCache,
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -113,16 +113,22 @@ impl FrameId {
         self.0 += 1;
     }
 
     /// An invalid sentinel FrameId, which will always compare less than
     /// any valid FrameId.
     pub const INVALID: FrameId = FrameId(0);
 }
 
+impl Default for FrameId {
+    fn default() -> Self {
+        FrameId::INVALID
+    }
+}
+
 impl ::std::ops::Add<usize> for FrameId {
     type Output = Self;
     fn add(self, other: usize) -> FrameId {
         FrameId(self.0 + other)
     }
 }
 
 impl ::std::ops::Sub<usize> for FrameId {
--- a/gfx/wr/webrender/src/render_task.rs
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -1778,23 +1778,23 @@ impl RenderTaskCache {
                                           -> CacheItem {
         // Get the texture cache handle for this cache key.
         let handle = self.map.get(key).unwrap();
         let cache_entry = self.cache_entries.get(handle);
         texture_cache.get(&cache_entry.handle)
     }
 
     #[allow(dead_code)]
-    pub fn cache_item_is_allocated_for_render_task(&self,
-                                                   texture_cache: &TextureCache,
-                                                   key: &RenderTaskCacheKey)
-                                                   -> bool {
+    pub fn get_allocated_size_for_render_task(&self,
+                                              texture_cache: &TextureCache,
+                                              key: &RenderTaskCacheKey)
+                                              -> Option<usize> {
         let handle = self.map.get(key).unwrap();
         let cache_entry = self.cache_entries.get(handle);
-        texture_cache.is_allocated(&cache_entry.handle)
+        texture_cache.get_allocated_size(&cache_entry.handle)
     }
 }
 
 // TODO(gw): Rounding the content rect here to device pixels is not
 // technically correct. Ideally we should ceil() here, and ensure that
 // the extra part pixel in the case of fractional sizes is correctly
 // handled. For now, just use rounding which passes the existing
 // Gecko tests.
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -53,16 +53,17 @@ use crate::device::{DepthFunction, Devic
 use crate::device::{DrawTarget, ExternalTexture, FBOId, ReadTarget, TextureSlot};
 use crate::device::{ShaderError, TextureFilter, TextureFlags,
              VertexUsageHint, VAO, VBO, CustomVAO};
 use crate::device::{ProgramCache};
 use crate::device::query::GpuTimer;
 use euclid::{rect, Transform3D, TypedScale};
 use crate::frame_builder::{ChasePrimitive, FrameBuilderConfig};
 use gleam::gl;
+use crate::glyph_cache::GlyphCache;
 use crate::glyph_rasterizer::{GlyphFormat, GlyphRasterizer};
 use crate::gpu_cache::{GpuBlockData, GpuCacheUpdate, GpuCacheUpdateList};
 use crate::gpu_cache::{GpuCacheDebugChunk, GpuCacheDebugCmd};
 #[cfg(feature = "pathfinder")]
 use crate::gpu_glyph_renderer::GpuGlyphRenderer;
 use crate::gpu_types::{PrimitiveHeaderI, PrimitiveHeaderF, ScalingInstance, TransformData, ResolveInstanceData};
 use crate::internal_types::{TextureSource, ORTHO_FAR_PLANE, ORTHO_NEAR_PLANE, ResourceCacheError};
 use crate::internal_types::{CacheTextureId, DebugOutput, FastHashMap, FastHashSet, LayerIndex, RenderedDocument, ResultMsg};
@@ -2006,16 +2007,17 @@ impl Renderer {
                             thread_listener.thread_stopped(&format!("WRWorker#{}", idx));
                         }
                     })
                     .build();
                 Arc::new(worker.unwrap())
             });
         let sampler = options.sampler;
         let namespace_alloc_by_client = options.namespace_alloc_by_client;
+        let max_glyph_cache_size = options.max_glyph_cache_size.unwrap_or(GlyphCache::DEFAULT_MAX_BYTES_USED);
 
         let blob_image_handler = options.blob_image_handler.take();
         let thread_listener_for_render_backend = thread_listener.clone();
         let thread_listener_for_scene_builder = thread_listener.clone();
         let thread_listener_for_lp_scene_builder = thread_listener.clone();
         let scene_builder_hooks = options.scene_builder_hooks;
         let rb_thread_name = format!("WRRenderBackend#{}", options.renderer_id.unwrap_or(0));
         let scene_thread_name = format!("WRSceneBuilder#{}", options.renderer_id.unwrap_or(0));
@@ -2085,19 +2087,22 @@ impl Renderer {
                 if config.enable_picture_caching {
                     picture_tile_sizes
                 } else {
                     &[]
                 },
                 start_size,
             );
 
+            let glyph_cache = GlyphCache::new(max_glyph_cache_size);
+
             let resource_cache = ResourceCache::new(
                 texture_cache,
                 glyph_rasterizer,
+                glyph_cache,
                 blob_image_handler,
             );
 
             let mut backend = RenderBackend::new(
                 api_rx,
                 payload_rx_for_backend,
                 result_tx,
                 scene_tx,
@@ -5359,16 +5364,17 @@ pub struct RendererOptions {
     pub enable_dithering: bool,
     pub max_recorded_profiles: usize,
     pub precache_flags: ShaderPrecacheFlags,
     pub renderer_kind: RendererKind,
     pub enable_subpixel_aa: bool,
     pub clear_color: Option<ColorF>,
     pub enable_clear_scissor: bool,
     pub max_texture_size: Option<i32>,
+    pub max_glyph_cache_size: Option<usize>,
     pub scatter_gpu_cache_updates: bool,
     pub upload_method: UploadMethod,
     pub workers: Option<Arc<ThreadPool>>,
     pub blob_image_handler: Option<Box<dyn BlobImageHandler>>,
     pub recorder: Option<Box<dyn ApiRecordingReceiver>>,
     pub thread_listener: Option<Box<dyn ThreadListener + Send + Sync>>,
     pub size_of_op: Option<VoidPtrToSizeFn>,
     pub enclosing_size_of_op: Option<VoidPtrToSizeFn>,
@@ -5413,16 +5419,17 @@ impl Default for RendererOptions {
             debug_flags: DebugFlags::empty(),
             max_recorded_profiles: 0,
             precache_flags: ShaderPrecacheFlags::empty(),
             renderer_kind: RendererKind::Native,
             enable_subpixel_aa: false,
             clear_color: Some(ColorF::new(1.0, 1.0, 1.0, 1.0)),
             enable_clear_scissor: true,
             max_texture_size: None,
+            max_glyph_cache_size: None,
             // Scattered GPU cache updates haven't met a test that would show their superiority yet.
             scatter_gpu_cache_updates: false,
             // This is best as `Immediate` on Angle, or `Pixelbuffer(Dynamic)` on GL,
             // but we are unable to make this decision here, so picking the reasonable medium.
             upload_method: UploadMethod::PixelBuffer(VertexUsageHint::Stream),
             workers: None,
             blob_image_handler: None,
             recorder: None,
--- a/gfx/wr/webrender/src/resource_cache.rs
+++ b/gfx/wr/webrender/src/resource_cache.rs
@@ -28,17 +28,17 @@ use crate::gpu_types::UvRectKind;
 use crate::image::{compute_tile_size, compute_tile_range, for_each_tile_in_range};
 use crate::internal_types::{FastHashMap, FastHashSet, TextureSource, TextureUpdateList};
 use crate::profiler::{ResourceProfileCounters, TextureCacheProfileCounters};
 use crate::render_backend::{FrameId, FrameStamp};
 use crate::render_task::{RenderTaskCache, RenderTaskCacheKey, RenderTaskId};
 use crate::render_task::{RenderTaskCacheEntry, RenderTaskCacheEntryHandle, RenderTaskGraph};
 use smallvec::SmallVec;
 use std::collections::hash_map::Entry::{self, Occupied, Vacant};
-use std::collections::hash_map::IterMut;
+use std::collections::hash_map::{Iter, IterMut};
 use std::collections::VecDeque;
 use std::{cmp, mem};
 use std::fmt::Debug;
 use std::hash::Hash;
 use std::os::raw::c_void;
 #[cfg(any(feature = "capture", feature = "replay"))]
 use std::path::PathBuf;
 use std::sync::{Arc, RwLock};
@@ -268,20 +268,28 @@ where
     pub fn try_get_mut(&mut self, key: &K) -> Option<&mut V> {
         self.resources.get_mut(key)
     }
 
     pub fn entry(&mut self, key: K) -> Entry<K, V> {
         self.resources.entry(key)
     }
 
+    pub fn iter(&self) -> Iter<K, V> {
+        self.resources.iter()
+    }
+
     pub fn iter_mut(&mut self) -> IterMut<K, V> {
         self.resources.iter_mut()
     }
 
+    pub fn is_empty(&mut self) -> bool {
+        self.resources.is_empty()
+    }
+
     pub fn clear(&mut self) {
         self.resources.clear();
     }
 
     pub fn retain<F>(&mut self, f: F)
     where
         F: FnMut(&K, &mut V) -> bool,
     {
@@ -484,20 +492,21 @@ pub struct ResourceCache {
     /// invalidations for picture caching tiles.
     dirty_image_keys: FastHashSet<ImageKey>,
 }
 
 impl ResourceCache {
     pub fn new(
         texture_cache: TextureCache,
         glyph_rasterizer: GlyphRasterizer,
+        cached_glyphs: GlyphCache,
         blob_image_handler: Option<Box<dyn BlobImageHandler>>,
     ) -> Self {
         ResourceCache {
-            cached_glyphs: GlyphCache::new(),
+            cached_glyphs,
             cached_images: ResourceClassCache::new(),
             cached_render_tasks: RenderTaskCache::new(),
             resources: Resources::default(),
             cached_glyph_dimensions: FastHashMap::default(),
             texture_cache,
             state: State::Idle,
             current_frame_id: FrameId::INVALID,
             pending_image_requests: FastHashSet::default(),
@@ -770,17 +779,17 @@ impl ResourceCache {
         self.glyph_rasterizer.add_font(font_key, template.clone());
         self.resources.font_templates.insert(font_key, template);
     }
 
     pub fn delete_font_template(&mut self, font_key: FontKey) {
         self.glyph_rasterizer.delete_font(font_key);
         self.resources.font_templates.remove(&font_key);
         self.cached_glyphs
-            .clear_fonts(|font| font.font_key == font_key);
+            .clear_fonts(&mut self.texture_cache, |font| font.font_key == font_key);
         if let Some(ref mut r) = self.blob_image_handler {
             r.delete_font(font_key);
         }
     }
 
     pub fn add_font_instance(
         &mut self,
         instance_key: FontInstanceKey,
@@ -1573,17 +1582,22 @@ impl ResourceCache {
     pub fn requires_frame_build(&self) -> bool {
         self.texture_cache.requires_frame_build()
     }
 
     pub fn begin_frame(&mut self, stamp: FrameStamp) {
         debug_assert_eq!(self.state, State::Idle);
         self.state = State::AddResources;
         self.texture_cache.begin_frame(stamp);
-        self.cached_glyphs.begin_frame(&self.texture_cache, &self.cached_render_tasks, &mut self.glyph_rasterizer);
+        self.cached_glyphs.begin_frame(
+            stamp,
+            &mut self.texture_cache,
+            &self.cached_render_tasks,
+            &mut self.glyph_rasterizer,
+        );
         self.cached_render_tasks.begin_frame(&mut self.texture_cache);
         self.current_frame_id = stamp.frame_id();
 
         // pop the old frame and push a new one
         self.deleted_blob_keys.pop_front();
         self.deleted_blob_keys.push_back(Vec::new());
     }
 
@@ -1810,17 +1824,17 @@ impl ResourceCache {
             .retain(|key, _| key.0 != namespace);
         for &key in self.resources.font_templates.keys().filter(|key| key.0 == namespace) {
             self.glyph_rasterizer.delete_font(key);
         }
         self.resources
             .font_templates
             .retain(|key, _| key.0 != namespace);
         self.cached_glyphs
-            .clear_fonts(|font| font.font_key.0 == namespace);
+            .clear_fonts(&mut self.texture_cache, |font| font.font_key.0 == namespace);
 
         if let Some(ref mut r) = self.blob_image_handler {
             r.clear_namespace(namespace);
         }
     }
 
     /// Reports the CPU heap usage of this ResourceCache.
     ///
--- a/gfx/wr/webrender/src/texture_cache.rs
+++ b/gfx/wr/webrender/src/texture_cache.rs
@@ -911,16 +911,24 @@ impl TextureCache {
     }
 
     // Check if a given texture handle has a valid allocation
     // in the texture cache.
     pub fn is_allocated(&self, handle: &TextureCacheHandle) -> bool {
         self.entries.get_opt(handle).is_some()
     }
 
+    // Return the allocated size of the texture handle's associated data,
+    // or otherwise indicate the handle is invalid.
+    pub fn get_allocated_size(&self, handle: &TextureCacheHandle) -> Option<usize> {
+        self.entries.get_opt(handle).map(|entry| {
+            (entry.format.bytes_per_pixel() * entry.size.area()) as usize
+        })
+    }
+
     // Retrieve the details of an item in the cache. This is used
     // during batch creation to provide the resource rect address
     // to the shaders and texture ID to the batching logic.
     // This function will assert in debug modes if the caller
     // tries to get a handle that was not requested this frame.
     pub fn get(&self, handle: &TextureCacheHandle) -> CacheItem {
         let (texture_id, layer_index, uv_rect, uv_rect_handle) = self.get_cache_location(handle);
         CacheItem {