// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct RustfsCodecDecodeWorkspace { shard_len: usize, } impl RustfsCodecDecodeWorkspace { #[inline] pub(crate) fn new(shard_len: usize) -> Self { Self { shard_len } } #[inline] #[allow(dead_code, reason = "workspace width asserted by decode_reader tests (backlog#1823)")] pub(crate) fn shard_len(&self) -> usize { self.shard_len } } #[derive(Debug, Default)] pub(crate) struct ShardBufferPool { buffers: Vec>>, } impl ShardBufferPool { #[inline] pub(crate) fn new(slot_count: usize) -> Self { let mut buffers = Vec::with_capacity(slot_count); buffers.resize_with(slot_count, || None); Self { buffers } } #[inline] pub(crate) fn ensure_slots(&mut self, slot_count: usize) { if self.buffers.len() < slot_count { self.buffers.resize_with(slot_count, || None); } } /// An **empty** buffer with room for at least `len` bytes. The caller fills it /// by appending (see `BitrotReader::read_appending`), so the pool never has to /// initialize the bytes it hands out. /// /// Zeroing here was pure waste: `read_appending` overwrites every byte it /// returns, and a short read is an error rather than a partial buffer, so no /// caller ever observes a byte the reader did not write. At 1 MiB shards the /// `resize(len, 0)` was ~4.8% of GET CPU (rustfs/backlog#1159) — a buffer pool /// exists to reuse an allocation, and memsetting it gives that saving straight /// back. #[inline] pub(crate) fn take(&mut self, index: usize, len: usize) -> Vec { self.ensure_slots(index + 1); let mut buf = self.buffers[index].take().unwrap_or_else(|| Vec::with_capacity(len)); buf.clear(); if buf.capacity() < len { buf.reserve_exact(len - buf.len()); } buf } #[inline] pub(crate) fn put(&mut self, index: usize, buf: Vec) { self.ensure_slots(index + 1); self.buffers[index] = Some(buf); } #[cfg(test)] pub(crate) fn stored_allocation(&self, index: usize) -> Option<(*const u8, usize)> { self.buffers .get(index) .and_then(|buf| buf.as_ref().map(|buf| (buf.as_ptr(), buf.capacity()))) } #[cfg(test)] fn stored_capacity(&self, index: usize) -> Option { self.buffers.get(index).and_then(|buf| buf.as_ref().map(Vec::capacity)) } } #[cfg(test)] mod tests { use super::*; /// `take` hands out capacity, never length: the caller appends every byte it /// will read back. Reusing a slot must keep the allocation and must not memset /// it (rustfs/backlog#1159). #[test] fn shard_buffer_pool_reuses_the_allocation_and_never_zeroes_it() { let mut pool = ShardBufferPool::new(2); let mut buf = pool.take(1, 16); assert_eq!(buf.len(), 0, "take yields an empty buffer; the caller appends"); assert!(buf.capacity() >= 16); buf.extend_from_slice(&[42u8; 16]); let capacity = buf.capacity(); let ptr = buf.as_ptr(); pool.put(1, buf); assert_eq!(pool.stored_capacity(1), Some(capacity)); let reused = pool.take(1, 8); assert_eq!(reused.len(), 0); assert!(reused.capacity() >= capacity, "the allocation must be reused, not reallocated"); assert_eq!(reused.as_ptr(), ptr, "same allocation"); } #[test] fn shard_buffer_pool_grows_for_sparse_slot_indexes() { let mut pool = ShardBufferPool::new(0); let buf = pool.take(3, 4); assert_eq!(buf.len(), 0); assert!(buf.capacity() >= 4); assert_eq!(pool.buffers.len(), 4); } #[test] fn workspace_reports_shard_len_and_pool_reserves_when_reused_slot_is_too_small() { let workspace = RustfsCodecDecodeWorkspace::new(37); assert_eq!(workspace.shard_len(), 37); let mut pool = ShardBufferPool::new(1); pool.put(0, Vec::with_capacity(2)); let grown = pool.take(0, 8); assert_eq!(grown.len(), 0); assert!(grown.capacity() >= 8, "a too-small reused slot must grow to the requested capacity"); } }