Skip to content

Commit 5570a54

Browse files
nicalweb-flow
authored andcommittedJul 14, 2024
Bug 1903981 - Do scene-related memory deallocations on the scene builder thread. r=gw
The scene is sent back to the scene builder thread where the SceneRecycler will reuse some allocations and drop the rest. Deallocating the memory on the scene builder thread where it was allocated removes lock contention in jemalloc and as a bonus gives an opportunity to recycle some of it. This patch picks only the lowest hanging fruits when it comes to recycling allocations from the scene and the scene builder. Differential Revision: https://phabricator.services.mozilla.com/D215371
1 parent eb5eef4 commit 5570a54

File tree

8 files changed

+249
-29
lines changed

8 files changed

+249
-29
lines changed
 

‎webrender/src/clip.rs

+55-2
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,20 @@ impl ClipTree {
187187
}
188188
}
189189

190+
pub fn reset(&mut self) {
191+
self.nodes.clear();
192+
self.nodes.push(ClipTreeNode {
193+
handle: ClipDataHandle::INVALID,
194+
children: Vec::new(),
195+
parent: ClipNodeId::NONE,
196+
});
197+
198+
self.leaves.clear();
199+
200+
self.clip_root_stack.clear();
201+
self.clip_root_stack.push(ClipNodeId::NONE);
202+
}
203+
190204
/// Add a set of clips to the provided tree node id, reusing existing
191205
/// nodes in the tree where possible
192206
fn add_impl(
@@ -430,6 +444,24 @@ impl ClipTreeBuilder {
430444
}
431445
}
432446

447+
pub fn begin(&mut self) {
448+
self.clip_map.clear();
449+
self.clip_chain_map.clear();
450+
self.clip_chains.clear();
451+
self.clip_stack.clear();
452+
self.clip_stack.push(ClipStackEntry {
453+
clip_node_id: ClipNodeId::NONE,
454+
last_clip_chain_cache: None,
455+
seen_clips: FastHashSet::default(),
456+
});
457+
self.tree.reset();
458+
self.clip_handles_buffer.clear();
459+
}
460+
461+
pub fn recycle_tree(&mut self, tree: ClipTree) {
462+
self.tree = tree;
463+
}
464+
433465
/// Define a new rect clip
434466
pub fn define_rect_clip(
435467
&mut self,
@@ -685,8 +717,15 @@ impl ClipTreeBuilder {
685717
}
686718

687719
/// Finalize building and return the clip-tree
688-
pub fn finalize(self) -> ClipTree {
689-
self.tree
720+
pub fn finalize(&mut self) -> ClipTree {
721+
// Note: After this, the builder's clip tree does not hold allocations and
722+
// is not in valid state. `ClipTreeBuilder::begin()` must be called before
723+
// building can happen again.
724+
std::mem::replace(&mut self.tree, ClipTree {
725+
nodes: Vec::new(),
726+
leaves: Vec::new(),
727+
clip_root_stack: Vec::new(),
728+
})
690729
}
691730

692731
/// Get a clip node by id
@@ -1251,6 +1290,14 @@ impl ClipStore {
12511290
}
12521291
}
12531292

1293+
pub fn reset(&mut self) {
1294+
self.clip_node_instances.clear();
1295+
self.mask_tiles.clear();
1296+
self.active_clip_node_info.clear();
1297+
self.active_local_clip_rect = None;
1298+
self.active_pic_coverage_rect = PictureRect::max_rect();
1299+
}
1300+
12541301
pub fn get_instance_from_range(
12551302
&self,
12561303
node_range: &ClipNodeRange,
@@ -1550,6 +1597,12 @@ impl ClipStore {
15501597
}
15511598
}
15521599

1600+
impl Default for ClipStore {
1601+
fn default() -> Self {
1602+
ClipStore::new()
1603+
}
1604+
}
1605+
15531606
// The ClipItemKey is a hashable representation of the contents
15541607
// of a clip item. It is used during interning to de-duplicate
15551608
// clip nodes between frames and display lists. This allows quick

‎webrender/src/hit_test.rs

+5
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ impl HitTestingScene {
179179
}
180180
}
181181

182+
pub fn reset(&mut self) {
183+
self.clip_nodes.clear();
184+
self.items.clear();
185+
}
186+
182187
/// Get stats about the current scene allocation sizes.
183188
pub fn get_stats(&self) -> HitTestingSceneStats {
184189
HitTestingSceneStats {

‎webrender/src/picture_graph.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub struct PictureInfo {
1616
pub parent: Option<PictureIndex>,
1717
}
1818

19+
#[derive(Default)]
1920
/// A graph of picture dependencies, allowing updates to be processed without recursion
2021
/// by building a list of passes.
2122
pub struct PictureGraph {
@@ -26,11 +27,13 @@ pub struct PictureGraph {
2627

2728
impl PictureGraph {
2829
pub fn new() -> Self {
29-
PictureGraph {
30-
roots: Vec::new(),
31-
pic_info: Vec::new(),
32-
update_passes: Vec::new(),
33-
}
30+
PictureGraph::default()
31+
}
32+
33+
pub fn reset(&mut self) {
34+
self.roots.clear();
35+
self.pic_info.clear();
36+
self.update_passes.clear();
3437
}
3538

3639
/// Add a root picture to the graph

‎webrender/src/prim_store/mod.rs

+14
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,14 @@ impl PrimitiveStore {
14711471
}
14721472
}
14731473

1474+
pub fn reset(&mut self) {
1475+
self.pictures.clear();
1476+
self.text_runs.clear();
1477+
self.images.clear();
1478+
self.color_bindings.clear();
1479+
self.linear_gradients.clear();
1480+
}
1481+
14741482
pub fn get_stats(&self) -> PrimitiveStoreStats {
14751483
PrimitiveStoreStats {
14761484
picture_count: self.pictures.len(),
@@ -1489,6 +1497,12 @@ impl PrimitiveStore {
14891497
}
14901498
}
14911499

1500+
impl Default for PrimitiveStore {
1501+
fn default() -> Self {
1502+
PrimitiveStore::new(&PrimitiveStoreStats::empty())
1503+
}
1504+
}
1505+
14921506
/// Trait for primitives that are directly internable.
14931507
/// see SceneBuilder::add_primitive<P>
14941508
pub trait InternablePrimitive: intern::Internable<InternData = ()> + Sized {

‎webrender/src/render_backend.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,10 @@ impl Document {
667667
resource_cache,
668668
);
669669

670-
self.scene = built_scene;
670+
671+
let old_scene = std::mem::replace(&mut self.scene, built_scene);
672+
old_scene.recycle();
673+
671674
self.scratch.recycle(recycler);
672675
}
673676
}

‎webrender/src/scene.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
use api::{BuiltDisplayList, DisplayListWithCache, ColorF, DynamicProperties, Epoch, FontRenderMode};
66
use api::{PipelineId, PropertyBinding, PropertyBindingId, PropertyValue, MixBlendMode, StackingContext};
77
use api::units::*;
8+
use api::channel::Sender;
89
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
910
use crate::render_api::MemoryReport;
1011
use crate::composite::CompositorKind;
1112
use crate::clip::{ClipStore, ClipTree};
1213
use crate::spatial_tree::SpatialTree;
13-
use crate::frame_builder::{FrameBuilderConfig};
14+
use crate::frame_builder::FrameBuilderConfig;
1415
use crate::hit_test::{HitTester, HitTestingScene, HitTestingSceneStats};
1516
use crate::internal_types::FastHashMap;
1617
use crate::picture::SurfaceInfo;
@@ -284,6 +285,12 @@ pub struct BuiltScene {
284285
pub prim_instances: Vec<PrimitiveInstance>,
285286
pub surfaces: Vec<SurfaceInfo>,
286287
pub clip_tree: ClipTree,
288+
289+
/// Deallocating memory outside of the thread that allocated it causes lock
290+
/// contention in jemalloc. To avoid this we send the built scene back to
291+
/// the scene builder thread when we don't need it anymore, and in the process,
292+
/// also reuse some allocations.
293+
pub recycler_tx: Option<Sender<BuiltScene>>,
287294
}
288295

289296
impl BuiltScene {
@@ -302,6 +309,7 @@ impl BuiltScene {
302309
prim_instances: Vec::new(),
303310
surfaces: Vec::new(),
304311
clip_tree: ClipTree::new(),
312+
recycler_tx: None,
305313
config: FrameBuilderConfig {
306314
default_font_render_mode: FontRenderMode::Mono,
307315
dual_source_blending_is_supported: false,
@@ -326,6 +334,14 @@ impl BuiltScene {
326334
}
327335
}
328336

337+
/// Send the scene back to the scene builder thread so that recycling/deallocations
338+
/// can happen there.
339+
pub fn recycle(mut self) {
340+
if let Some(tx) = self.recycler_tx.take() {
341+
let _ = tx.send(self);
342+
}
343+
}
344+
329345
/// Get the memory usage statistics to pre-allocate for the next scene.
330346
pub fn get_stats(&self) -> SceneStats {
331347
SceneStats {

‎webrender/src/scene_builder_thread.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::box_shadow::BoxShadow;
1313
#[cfg(feature = "capture")]
1414
use crate::capture::CaptureConfig;
1515
use crate::frame_builder::FrameBuilderConfig;
16-
use crate::scene_building::SceneBuilder;
16+
use crate::scene_building::{SceneBuilder, SceneRecycler};
1717
use crate::clip::{ClipIntern, PolygonIntern};
1818
use crate::filterdata::FilterDataIntern;
1919
use glyph_rasterizer::SharedFontResources;
@@ -30,7 +30,7 @@ use crate::prim_store::text_run::TextRun;
3030
use crate::profiler::{self, TransactionProfile};
3131
use crate::render_backend::SceneView;
3232
use crate::renderer::{FullFrameStats, PipelineInfo};
33-
use crate::scene::{Scene, BuiltScene, SceneStats};
33+
use crate::scene::{BuiltScene, Scene, SceneStats};
3434
use crate::spatial_tree::{SceneSpatialTree, SpatialTreeUpdates};
3535
use crate::telemetry::Telemetry;
3636
use crate::SceneBuilderHooks;
@@ -243,6 +243,7 @@ pub struct SceneBuilderThread {
243243
#[cfg(feature = "capture")]
244244
capture_config: Option<CaptureConfig>,
245245
debug_flags: DebugFlags,
246+
recycler: SceneRecycler,
246247
}
247248

248249
pub struct SceneBuilderThreadChannels {
@@ -288,6 +289,7 @@ impl SceneBuilderThread {
288289
#[cfg(feature = "capture")]
289290
capture_config: None,
290291
debug_flags: DebugFlags::default(),
292+
recycler: SceneRecycler::new(),
291293
}
292294
}
293295

@@ -326,6 +328,9 @@ impl SceneBuilderThread {
326328
_ => {},
327329
}
328330
self.forward_built_transactions(built_txns);
331+
332+
// Now that we off the critical path, do some memory bookkeeping.
333+
self.recycler.recycle_built_scene();
329334
}
330335
Ok(SceneBuilderRequest::AddDocument(document_id, initial_size)) => {
331336
let old = self.documents.insert(document_id, Document::new(
@@ -605,6 +610,7 @@ impl SceneBuilderThread {
605610
&self.config,
606611
&mut doc.interners,
607612
&mut doc.spatial_tree,
613+
&mut self.recycler,
608614
&doc.stats,
609615
self.debug_flags,
610616
);

‎webrender/src/scene_building.rs

+138-18
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,27 @@ use api::{APZScrollGeneration, HasScrollLinkedEffect, Shadow, SpatialId, StickyF
4646
use api::{ClipMode, PrimitiveKeyKind, TransformStyle, YuvColorSpace, ColorRange, YuvData, TempFilterData};
4747
use api::{ReferenceTransformBinding, Rotation, FillRule, SpatialTreeItem, ReferenceFrameDescriptor};
4848
use api::FilterOpGraphPictureBufferId;
49+
use api::channel::{unbounded_channel, Receiver, Sender};
4950
use api::units::*;
5051
use crate::image_tiling::simplify_repeated_primitive;
5152
use crate::box_shadow::BLUR_SAMPLE_SCALE;
52-
use crate::clip::{ClipItemKey, ClipStore, ClipItemKeyKind, ClipIntern};
53+
use crate::clip::{ClipIntern, ClipItemKey, ClipItemKeyKind, ClipStore};
5354
use crate::clip::{ClipInternData, ClipNodeId, ClipLeafId};
5455
use crate::clip::{PolygonDataHandle, ClipTreeBuilder};
5556
use crate::segment::EdgeAaSegmentMask;
5657
use crate::spatial_tree::{SceneSpatialTree, SpatialNodeContainer, SpatialNodeIndex, get_external_scroll_offset};
57-
use crate::frame_builder::{FrameBuilderConfig};
58+
use crate::frame_builder::FrameBuilderConfig;
5859
use glyph_rasterizer::{FontInstance, SharedFontResources};
5960
use crate::hit_test::HitTestingScene;
6061
use crate::intern::Interner;
6162
use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo, Filter, FilterGraphNode, FilterGraphOp, FilterGraphPictureReference, PlaneSplitterIndex, PipelineInstanceId};
6263
use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive};
6364
use crate::picture::{BlitReason, OrderedPictureChild, PrimitiveList, SurfaceInfo, PictureFlags};
6465
use crate::picture_graph::PictureGraph;
65-
use crate::prim_store::{PrimitiveInstance};
66+
use crate::prim_store::{PrimitiveInstance, PrimitiveStoreStats};
6667
use crate::prim_store::{PrimitiveInstanceKind, NinePatchDescriptor, PrimitiveStore};
6768
use crate::prim_store::{InternablePrimitive, PictureIndex};
68-
use crate::prim_store::{PolygonKey};
69+
use crate::prim_store::PolygonKey;
6970
use crate::prim_store::backdrop::{BackdropCapture, BackdropRender};
7071
use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
7172
use crate::prim_store::gradient::{
@@ -79,7 +80,7 @@ use crate::prim_store::picture::{Picture, PictureCompositeKey, PictureKey};
7980
use crate::prim_store::text_run::TextRun;
8081
use crate::render_backend::SceneView;
8182
use crate::resource_cache::ImageRequest;
82-
use crate::scene::{Scene, ScenePipeline, BuiltScene, SceneStats, StackingContextHelpers};
83+
use crate::scene::{BuiltScene, Scene, ScenePipeline, SceneStats, StackingContextHelpers};
8384
use crate::scene_builder_thread::Interners;
8485
use crate::space::SpaceSnapper;
8586
use crate::spatial_node::{
@@ -539,6 +540,7 @@ impl<'a> SceneBuilder<'a> {
539540
frame_builder_config: &FrameBuilderConfig,
540541
interners: &mut Interners,
541542
spatial_tree: &mut SceneSpatialTree,
543+
recycler: &mut SceneRecycler,
542544
stats: &SceneStats,
543545
debug_flags: DebugFlags,
544546
) -> BuiltScene {
@@ -560,17 +562,17 @@ impl<'a> SceneBuilder<'a> {
560562
spatial_tree,
561563
fonts,
562564
config: *frame_builder_config,
563-
id_to_index_mapper_stack: Vec::new(),
564-
hit_testing_scene: HitTestingScene::new(&stats.hit_test_stats),
565-
pending_shadow_items: VecDeque::new(),
566-
sc_stack: Vec::new(),
567-
containing_block_stack: Vec::new(),
568-
raster_space_stack: vec![RasterSpace::Screen],
569-
prim_store: PrimitiveStore::new(&stats.prim_store_stats),
570-
clip_store: ClipStore::new(),
565+
id_to_index_mapper_stack: mem::take(&mut recycler.id_to_index_mapper_stack),
566+
hit_testing_scene: recycler.hit_testing_scene.take().unwrap_or_else(|| HitTestingScene::new(&stats.hit_test_stats)),
567+
pending_shadow_items: mem::take(&mut recycler.pending_shadow_items),
568+
sc_stack: mem::take(&mut recycler.sc_stack),
569+
containing_block_stack: mem::take(&mut recycler.containing_block_stack),
570+
raster_space_stack: mem::take(&mut recycler.raster_space_stack),
571+
prim_store: mem::take(&mut recycler.prim_store),
572+
clip_store: mem::take(&mut recycler.clip_store),
571573
interners,
572574
external_scroll_mapper: ScrollOffsetMapper::new(),
573-
iframe_size: Vec::new(),
575+
iframe_size: mem::take(&mut recycler.iframe_size),
574576
root_iframe_clip: None,
575577
quality_settings: view.quality_settings,
576578
tile_cache_builder: TileCacheBuilder::new(
@@ -579,14 +581,32 @@ impl<'a> SceneBuilder<'a> {
579581
debug_flags,
580582
),
581583
snap_to_device,
582-
picture_graph: PictureGraph::new(),
584+
picture_graph: mem::take(&mut recycler.picture_graph),
583585
next_plane_splitter_index: 0,
584-
prim_instances: Vec::new(),
586+
prim_instances: mem::take(&mut recycler.prim_instances),
585587
pipeline_instance_ids: FastHashMap::default(),
586-
surfaces: Vec::new(),
587-
clip_tree_builder: ClipTreeBuilder::new(),
588+
surfaces: mem::take(&mut recycler.surfaces),
589+
clip_tree_builder: recycler.clip_tree_builder.take().unwrap_or_else(|| ClipTreeBuilder::new()),
588590
};
589591

592+
// Reset
593+
builder.hit_testing_scene.reset();
594+
builder.prim_store.reset();
595+
builder.clip_store.reset();
596+
builder.picture_graph.reset();
597+
builder.prim_instances.clear();
598+
builder.surfaces.clear();
599+
builder.sc_stack.clear();
600+
builder.containing_block_stack.clear();
601+
builder.id_to_index_mapper_stack.clear();
602+
builder.pending_shadow_items.clear();
603+
builder.iframe_size.clear();
604+
605+
builder.raster_space_stack.clear();
606+
builder.raster_space_stack.push(RasterSpace::Screen);
607+
608+
builder.clip_tree_builder.begin();
609+
590610
builder.build_all(
591611
root_pipeline_id,
592612
&root_pipeline,
@@ -617,6 +637,14 @@ impl<'a> SceneBuilder<'a> {
617637

618638
let clip_tree = builder.clip_tree_builder.finalize();
619639

640+
recycler.clip_tree_builder = Some(builder.clip_tree_builder);
641+
recycler.sc_stack = builder.sc_stack;
642+
recycler.id_to_index_mapper_stack = builder.id_to_index_mapper_stack;
643+
recycler.containing_block_stack = builder.containing_block_stack;
644+
recycler.raster_space_stack = builder.raster_space_stack;
645+
recycler.pending_shadow_items = builder.pending_shadow_items;
646+
recycler.iframe_size = builder.iframe_size;
647+
620648
BuiltScene {
621649
has_root_pipeline: scene.has_root_pipeline(),
622650
pipeline_epochs: scene.pipeline_epochs.clone(),
@@ -632,6 +660,7 @@ impl<'a> SceneBuilder<'a> {
632660
prim_instances: builder.prim_instances,
633661
surfaces: builder.surfaces,
634662
clip_tree,
663+
recycler_tx: Some(recycler.tx.clone()),
635664
}
636665
}
637666

@@ -4722,3 +4751,94 @@ fn read_gradient_stops(stops: ItemRange<GradientStop>) -> Vec<GradientStopKey> {
47224751
}
47234752
}).collect()
47244753
}
4754+
4755+
/// A helper for reusing the scene builder's memory allocations and dropping
4756+
/// scene allocations on the scene builder thread to avoid lock contention in
4757+
/// jemalloc.
4758+
pub struct SceneRecycler {
4759+
pub tx: Sender<BuiltScene>,
4760+
rx: Receiver<BuiltScene>,
4761+
4762+
// Allocations recycled from BuiltScene:
4763+
4764+
pub prim_store: PrimitiveStore,
4765+
pub clip_store: ClipStore,
4766+
pub picture_graph: PictureGraph,
4767+
pub prim_instances: Vec<PrimitiveInstance>,
4768+
pub surfaces: Vec<SurfaceInfo>,
4769+
pub hit_testing_scene: Option<HitTestingScene>,
4770+
pub clip_tree_builder: Option<ClipTreeBuilder>,
4771+
//Could also attempt to recycle the following:
4772+
//pub tile_cache_config: TileCacheConfig,
4773+
//pub pipeline_epochs: FastHashMap<PipelineId, Epoch>,
4774+
//pub tile_cache_pictures: Vec<PictureIndex>,
4775+
4776+
4777+
// Allocations recycled from SceneBuilder
4778+
4779+
id_to_index_mapper_stack: Vec<NodeIdToIndexMapper>,
4780+
sc_stack: Vec<FlattenedStackingContext>,
4781+
containing_block_stack: Vec<SpatialNodeIndex>,
4782+
raster_space_stack: Vec<RasterSpace>,
4783+
pending_shadow_items: VecDeque<ShadowItem>,
4784+
iframe_size: Vec<LayoutSize>,
4785+
}
4786+
4787+
impl SceneRecycler {
4788+
pub fn new() -> Self {
4789+
let (tx, rx) = unbounded_channel();
4790+
SceneRecycler {
4791+
tx,
4792+
rx,
4793+
4794+
prim_instances: Vec::new(),
4795+
surfaces: Vec::new(),
4796+
prim_store: PrimitiveStore::new(&PrimitiveStoreStats::empty()),
4797+
clip_store: ClipStore::new(),
4798+
picture_graph: PictureGraph::new(),
4799+
hit_testing_scene: None,
4800+
clip_tree_builder: None,
4801+
4802+
id_to_index_mapper_stack: Vec::new(),
4803+
sc_stack: Vec::new(),
4804+
containing_block_stack: Vec::new(),
4805+
raster_space_stack: Vec::new(),
4806+
pending_shadow_items: VecDeque::new(),
4807+
iframe_size: Vec::new(),
4808+
}
4809+
}
4810+
4811+
/// Do some bookkeeping of past memory allocations, retaining some of them for
4812+
/// reuse and dropping the rest.
4813+
///
4814+
/// Should be called once between scene builds, ideally outside of the critical
4815+
/// path since deallocations can take some time.
4816+
#[inline(never)]
4817+
pub fn recycle_built_scene(&mut self) {
4818+
let Ok(scene) = self.rx.try_recv() else {
4819+
return;
4820+
};
4821+
4822+
self.prim_store = scene.prim_store;
4823+
self.clip_store = scene.clip_store;
4824+
// We currently retain top-level allocations but don't attempt to retain leaf
4825+
// allocations in the prim store and clip store. We don't have to reset it here
4826+
// but doing so avoids dropping the leaf allocations in the
4827+
self.prim_store.reset();
4828+
self.clip_store.reset();
4829+
self.hit_testing_scene = Arc::try_unwrap(scene.hit_testing_scene).ok();
4830+
self.picture_graph = scene.picture_graph;
4831+
self.prim_instances = scene.prim_instances;
4832+
self.surfaces = scene.surfaces;
4833+
if let Some(clip_tree_builder) = &mut self.clip_tree_builder {
4834+
clip_tree_builder.recycle_tree(scene.clip_tree);
4835+
}
4836+
4837+
while let Ok(_) = self.rx.try_recv() {
4838+
// If for some reason more than one scene accumulated in the queue, drop
4839+
// the rest.
4840+
}
4841+
4842+
// Note: fields of the scene we don't recycle get dropped here.
4843+
}
4844+
}

0 commit comments

Comments
 (0)
Please sign in to comment.