Skip to content

Commit

Permalink
Bug 1903977 - Add a blob tile pool. r=gfx-reviewers,gw
Browse files Browse the repository at this point in the history
The tile pool keeps a strong reference to all of its allocations and reuses them when their reference count gets down to one.
Jemalloc only uses thread-local arenas for small allocations (<496 bytes) so it does not matter what thread the blob tiles which are typically much larger, are deallocated in.

Differential Revision: https://phabricator.services.mozilla.com/D218547
  • Loading branch information
nical authored and web-flow committed Aug 11, 2024
1 parent ce78ab0 commit 2289781
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 8 deletions.
4 changes: 3 additions & 1 deletion examples/blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use webrender::api::{self, DisplayListBuilder, DocumentId, PipelineId, PrimitiveFlags};
use webrender::api::{ColorF, CommonItemProperties, SpaceAndClipInfo, ImageDescriptorFlags};
use webrender::api::BlobTilePool;
use webrender::api::units::*;
use webrender::render_api::*;
use webrender::euclid::size2;
Expand Down Expand Up @@ -183,7 +184,8 @@ impl api::AsyncBlobImageRasterizer for Rasterizer {
fn rasterize(
&mut self,
requests: &[api::BlobImageParams],
_low_priority: bool
_low_priority: bool,
_tile_pool: &mut BlobTilePool,
) -> Vec<(api::BlobImageRequest, api::BlobImageResult)> {
let requests: Vec<(&api::BlobImageParams, Arc<ImageRenderingCommands>)> = requests.into_iter().map(|params| {
(params, Arc::clone(&self.image_cmds[&params.request.key]))
Expand Down
3 changes: 2 additions & 1 deletion webrender/src/renderer/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* 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/. */

use api::{BlobImageHandler, ColorF, IdNamespace, DocumentId, CrashAnnotator};
use api::{BlobImageHandler, ColorF, CrashAnnotator, DocumentId, IdNamespace};
use api::{VoidPtrToSizeFn, FontRenderMode, ImageFormat};
use api::{RenderNotifier, ImageBufferKind};
use api::units::*;
Expand Down Expand Up @@ -617,6 +617,7 @@ pub fn create_webrender_instance(
let lp_builder = LowPrioritySceneBuilderThread {
rx: low_priority_scene_rx,
tx: scene_tx.clone(),
tile_pool: api::BlobTilePool::new(),
};

thread::Builder::new().name(lp_scene_thread_name.clone()).spawn(move || {
Expand Down
14 changes: 10 additions & 4 deletions webrender/src/scene_builder_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ use crate::util::drain_filter;
use std::thread;
use std::time::Duration;

fn rasterize_blobs(txn: &mut TransactionMsg, is_low_priority: bool) {
fn rasterize_blobs(txn: &mut TransactionMsg, is_low_priority: bool, tile_pool: &mut api::BlobTilePool) {
profile_scope!("rasterize_blobs");

if let Some(ref mut rasterizer) = txn.blob_rasterizer {
let mut rasterized_blobs = rasterizer.rasterize(&txn.blob_requests, is_low_priority);
let mut rasterized_blobs = rasterizer.rasterize(&txn.blob_requests, is_low_priority, tile_pool);
// try using the existing allocation if our current list is empty
if txn.rasterized_blobs.is_empty() {
txn.rasterized_blobs = rasterized_blobs;
Expand Down Expand Up @@ -244,6 +244,7 @@ pub struct SceneBuilderThread {
capture_config: Option<CaptureConfig>,
debug_flags: DebugFlags,
recycler: SceneRecycler,
tile_pool: api::BlobTilePool,
}

pub struct SceneBuilderThreadChannels {
Expand Down Expand Up @@ -290,6 +291,8 @@ impl SceneBuilderThread {
capture_config: None,
debug_flags: DebugFlags::default(),
recycler: SceneRecycler::new(),
// TODO: tile size is hard-coded here.
tile_pool: api::BlobTilePool::new(),
}
}

Expand Down Expand Up @@ -331,6 +334,7 @@ impl SceneBuilderThread {

// Now that we off the critical path, do some memory bookkeeping.
self.recycler.recycle_built_scene();
self.tile_pool.cleanup();
}
Ok(SceneBuilderRequest::AddDocument(document_id, initial_size)) => {
let old = self.documents.insert(document_id, Document::new(
Expand Down Expand Up @@ -641,7 +645,7 @@ impl SceneBuilderThread {
profile.start_time(profiler::BLOB_RASTERIZATION_TIME);

let is_low_priority = false;
rasterize_blobs(&mut txn, is_low_priority);
rasterize_blobs(&mut txn, is_low_priority, &mut self.tile_pool);

profile.end_time(profiler::BLOB_RASTERIZATION_TIME);
Telemetry::record_rasterize_blobs_time(Duration::from_micros((profile.get(profiler::BLOB_RASTERIZATION_TIME).unwrap() * 1000.00) as u64));
Expand Down Expand Up @@ -771,6 +775,7 @@ impl SceneBuilderThread {
pub struct LowPrioritySceneBuilderThread {
pub rx: Receiver<SceneBuilderRequest>,
pub tx: Sender<SceneBuilderRequest>,
pub tile_pool: api::BlobTilePool,
}

impl LowPrioritySceneBuilderThread {
Expand All @@ -782,6 +787,7 @@ impl LowPrioritySceneBuilderThread {
.map(|txn| self.process_transaction(txn))
.collect();
self.tx.send(SceneBuilderRequest::Transactions(txns)).unwrap();
self.tile_pool.cleanup();
}
Ok(SceneBuilderRequest::ShutDown(sync)) => {
self.tx.send(SceneBuilderRequest::ShutDown(sync)).unwrap();
Expand All @@ -800,7 +806,7 @@ impl LowPrioritySceneBuilderThread {
fn process_transaction(&mut self, mut txn: Box<TransactionMsg>) -> Box<TransactionMsg> {
let is_low_priority = true;
txn.profile.start_time(profiler::BLOB_RASTERIZATION_TIME);
rasterize_blobs(&mut txn, is_low_priority);
rasterize_blobs(&mut txn, is_low_priority, &mut self.tile_pool);
txn.profile.end_time(profiler::BLOB_RASTERIZATION_TIME);
Telemetry::record_rasterize_blobs_time(Duration::from_micros((txn.profile.get(profiler::BLOB_RASTERIZATION_TIME).unwrap() * 1000.00) as u64));
txn.blob_requests = Vec::new();
Expand Down
3 changes: 2 additions & 1 deletion webrender_api/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,8 @@ pub trait AsyncBlobImageRasterizer : Send {
fn rasterize(
&mut self,
requests: &[BlobImageParams],
low_priority: bool
low_priority: bool,
tile_pool: &mut crate::BlobTilePool,
) -> Vec<(BlobImageRequest, BlobImageResult)>;
}

Expand Down
2 changes: 2 additions & 0 deletions webrender_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod display_list;
mod font;
mod gradient_builder;
mod image;
mod tile_pool;
pub mod units;

pub use crate::color::*;
Expand All @@ -51,6 +52,7 @@ pub use crate::display_list::*;
pub use crate::font::*;
pub use crate::gradient_builder::*;
pub use crate::image::*;
pub use crate::tile_pool::*;

use crate::units::*;
use crate::channel::Receiver;
Expand Down
151 changes: 151 additions & 0 deletions webrender_api/src/tile_pool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// 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/.

use std::sync::Arc;

const NUM_TILE_BUCKETS: usize = 6;

/// A pool of blob tile buffers to mitigate the overhead of
/// allocating and deallocating blob tiles.
///
/// The pool keeps a strong reference to each allocated buffers and
/// reuses the ones with a strong count of 1.
pub struct BlobTilePool {
largest_size_class: usize,
buckets: [Vec<Arc<Vec<u8>>>; NUM_TILE_BUCKETS],
}

impl BlobTilePool {
pub fn new() -> Self {
// The default max tile size is actually 256, using 512 here
// so that this still works when experimenting with larger
// tile sizes. If we ever make larger adjustments, the buckets
// should be changed accordingly.
let max_tile_size = 512;
BlobTilePool {
largest_size_class: max_tile_size * max_tile_size * 4,
buckets: [
Vec::with_capacity(32),
Vec::with_capacity(32),
Vec::with_capacity(32),
Vec::with_capacity(32),
Vec::with_capacity(32),
Vec::with_capacity(32),
],
}
}

/// Get or allocate a tile buffer of the requested size.
///
/// The returned buffer is zero-inizitalized.
/// The length of the returned buffer is equal to the requested size,
/// however the buffer may be allocated with a larger capacity to
/// confirm to the pool's corresponding bucket tile size.
pub fn get_buffer(&mut self, requested_size: usize) -> MutableTileBuffer {
assert!(requested_size <= self.largest_size_class);

let (bucket_idx, cap) = self.bucket_and_size(requested_size);
let bucket = &mut self.buckets[bucket_idx];
let mut selected_idx = None;
for (buf_idx, buffer) in bucket.iter().enumerate() {
if Arc::strong_count(buffer) == 1 {
selected_idx = Some(buf_idx);
break;
}
}

let ptr;
let strong_ref;
if let Some(idx) = selected_idx {
{
// This works because we just ensured the pool has the only strong
// ref to the buffer.
let buffer = Arc::get_mut(&mut bucket[idx]).unwrap();
debug_assert!(buffer.capacity() >= requested_size);
// Ensure the length is equal to the requested size. It's not
// strictly necessay for the tile pool but the texture upload
// code relies on it.
unsafe { buffer.set_len(requested_size); }

// zero-initialize
buffer.fill(0);

ptr = buffer.as_mut_ptr();
}
strong_ref = Arc::clone(&bucket[idx]);
} else {
// Allocate a buffer with the adequate capacity for the requested
// size's bucket.
let mut buf = vec![0; cap];
// Force the length to be the requested size.
unsafe { buf.set_len(requested_size) };

ptr = buf.as_mut_ptr();
strong_ref = Arc::new(buf);
// Track the new buffer.
bucket.push(Arc::clone(&strong_ref));
};

MutableTileBuffer {
ptr,
strong_ref,
}
}

fn bucket_and_size(&self, size: usize) -> (usize, usize) {
let mut next_size_class = self.largest_size_class / 4;
let mut idx = 0;
while size < next_size_class && idx < NUM_TILE_BUCKETS - 1 {
next_size_class /= 4;
idx += 1;
}

(idx, next_size_class * 4)
}

/// Go over all allocated tile buffers. For each bucket, deallocate some buffers
/// until the number of unused buffer is more than half of the buffers for that
/// bucket.
///
/// In practice, if called regularly, this gradually lets go of blob tiles when
/// they are not used.
pub fn cleanup(&mut self) {
for bucket in &mut self.buckets {
let threshold = bucket.len() / 2;
let mut num_available = 0;
bucket.retain(&mut |buffer: &Arc<Vec<u8>>| {
if Arc::strong_count(buffer) > 1 {
return true;
}

num_available += 1;
num_available < threshold
});
}
}
}


// The role of tile buffer is to encapsulate an Arc to the underlying buffer
// with a reference count of at most 2 and a way to view the buffer's content
// as a mutable slice, even though the reference count may be more than 1.
// The safety of this relies on the other strong reference being held by the
// tile pool which never accesses the buffer's content, so the only reference
// that can access it is the `TileBuffer` itself.
pub struct MutableTileBuffer {
strong_ref: Arc<Vec<u8>>,
ptr: *mut u8,
}

impl MutableTileBuffer {
pub fn as_mut_slice(&mut self) -> &mut[u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr, self.strong_ref.len()) }
}

pub fn into_arc(self) -> Arc<Vec<u8>> {
self.strong_ref
}
}

unsafe impl Send for MutableTileBuffer {}
3 changes: 2 additions & 1 deletion wrench/src/blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ impl AsyncBlobImageRasterizer for Rasterizer {
fn rasterize(
&mut self,
requests: &[BlobImageParams],
_low_priority: bool
_low_priority: bool,
_tile_pool: &mut BlobTilePool,
) -> Vec<(BlobImageRequest, BlobImageResult)> {
let requests: Vec<Command> = requests.iter().map(
|item| {
Expand Down

0 comments on commit 2289781

Please sign in to comment.