comment cleanup
This commit is contained in:
+60
-31
@@ -23,23 +23,26 @@ impl Range {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Blob {
|
pub struct Blob {
|
||||||
// todo: consider fallible collections to detect OOM on append
|
|
||||||
|
|
||||||
// space complexity O(n + m)
|
// space complexity O(n + m)
|
||||||
// where n is number of bytes
|
// where n is number of bytes
|
||||||
// and m is number of ranges
|
// where m is number of ranges
|
||||||
|
// (m should usually be smaller than n, unless reads are very sparse)
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Selected VecDeque<Box<[u8;N]>>> for fast push/pop/access
|
// Selected VecDeque<Box<[u8;N]>>> for
|
||||||
// pop_front may re-allocate, but better than vector most of time
|
// fast pop_front and random access, both are O(1)
|
||||||
// also since we use box, we are only copying pointers to the arrays, not each u8
|
// push_back is usually O(1), but may re-allocate and copy O(n)
|
||||||
|
// also since we use box, we are only copying pointers to the arrays, not each u8_array
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// also tried using LinkedList, but slow reading when ranges were inside nodes
|
// also tried using LinkedList, but slow reading O(n^2) when ranges were inside nodes
|
||||||
// and indexing into LinkedList nodes doesn't seem to jive well with Rust's ownership model
|
// and indexing into LinkedList nodes doesn't seem to jive well with Rust's ownership model
|
||||||
// especially when considering multi-threaded access
|
// especially when considering multi-threaded access
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Vec<Option<Box<[u8;N]>>> also worth considering, but instead of pop
|
// Vec<Option<Box<[u8;N]>>> also worth considering, but instead of pop
|
||||||
// using None slots to maintain indexes, if arrays are likely to be sparse
|
// using None slots to maintain indexes, if arrays are likely to be sparse
|
||||||
// this would probably be preferrable
|
// this would probably be preferrable
|
||||||
|
// -------------------------
|
||||||
|
// note: consider fallible collections to detect OOM on append if needed
|
||||||
|
// this code will panic if we run out of memory
|
||||||
arrays: VecDeque<Box<[u8; ARRAY_SIZE]>>, // complexity variable n
|
arrays: VecDeque<Box<[u8; ARRAY_SIZE]>>, // complexity variable n
|
||||||
dropped: usize,
|
dropped: usize,
|
||||||
ranges: Vec<Range>, // valid, global // complexity variable m
|
ranges: Vec<Range>, // valid, global // complexity variable m
|
||||||
@@ -64,10 +67,10 @@ impl Blob {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// appending complexity time, worst: O(n + a + m)
|
// appending complexity time, worst: O(n + a + r)
|
||||||
// where n is bytes written
|
// where n is bytes written
|
||||||
// and a is arrays added
|
// where a is arrays added
|
||||||
// and m is ranges added, usually 0 or 1, but reallocation could copy (m)
|
// where r is ranges added, usually 0 or 1, but reallocation could copy (r)
|
||||||
fn append(&mut self, input: &[u8]) -> usize {
|
fn append(&mut self, input: &[u8]) -> usize {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -80,16 +83,18 @@ impl Blob {
|
|||||||
let within = global_offset % ARRAY_SIZE;
|
let within = global_offset % ARRAY_SIZE;
|
||||||
let physical = global_offset / ARRAY_SIZE - self.dropped;
|
let physical = global_offset / ARRAY_SIZE - self.dropped;
|
||||||
if physical == self.arrays.len() {
|
if physical == self.arrays.len() {
|
||||||
// O(1) to push_back
|
// O(1) for single push_back
|
||||||
|
// O(a) for all arrays that are necessary for the write
|
||||||
self.arrays.push_back(Box::new([0u8; ARRAY_SIZE]));
|
self.arrays.push_back(Box::new([0u8; ARRAY_SIZE]));
|
||||||
}
|
}
|
||||||
// write to array - worst: O(n)
|
// write to array - O(n)
|
||||||
let take = (ARRAY_SIZE - within).min(input.len() - written);
|
let take = (ARRAY_SIZE - within).min(input.len() - written);
|
||||||
let array = &mut self.arrays[physical];
|
let array = &mut self.arrays[physical];
|
||||||
array[within..within + take].copy_from_slice(&input[written..written + take]);
|
array[within..within + take].copy_from_slice(&input[written..written + take]);
|
||||||
written += take;
|
written += take;
|
||||||
}
|
}
|
||||||
// update the ranges vector - O(1)
|
// update the ranges vector - usually O(1)
|
||||||
|
// but could push could re-allocate and copy: O(r)
|
||||||
let new_range = Range::new(self.end_index, self.end_index + input.len());
|
let new_range = Range::new(self.end_index, self.end_index + input.len());
|
||||||
match self.ranges.last_mut() {
|
match self.ranges.last_mut() {
|
||||||
Some(last) if last.end == new_range.start => last.end = new_range.end,
|
Some(last) if last.end == new_range.start => last.end = new_range.end,
|
||||||
@@ -100,10 +105,10 @@ impl Blob {
|
|||||||
written
|
written
|
||||||
}
|
}
|
||||||
|
|
||||||
// read time complexity, worst: O(n + m + d)
|
// read time complexity, worst: O(n + r + d)
|
||||||
// where n is bytes read
|
// where n is bytes read
|
||||||
// and m is ranges added. Usually O(1), but could be O(m) on re-allocation copy
|
// where r is ranges checked for already-read bytes
|
||||||
// and d is arrays dropped
|
// where d is arrays dropped
|
||||||
fn read(&mut self, start: usize, len: usize) -> Result<Vec<u8>, BlobError> {
|
fn read(&mut self, start: usize, len: usize) -> Result<Vec<u8>, BlobError> {
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
@@ -113,6 +118,8 @@ impl Blob {
|
|||||||
return Err(BlobError::InvalidRange);
|
return Err(BlobError::InvalidRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if range is valid - O(r)
|
||||||
|
// todo: we should be able to early exit here once we've gone too far
|
||||||
let read_range = Range::new(start, start + len);
|
let read_range = Range::new(start, start + len);
|
||||||
let Some(idx) = self.ranges.iter().position(|r| r.contains(&read_range)) else {
|
let Some(idx) = self.ranges.iter().position(|r| r.contains(&read_range)) else {
|
||||||
return Err(BlobError::BytesAlreadyRead);
|
return Err(BlobError::BytesAlreadyRead);
|
||||||
@@ -128,7 +135,7 @@ impl Blob {
|
|||||||
copied += take;
|
copied += take;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update valid ranges - worst case: O(m), usual case O(1)
|
// update valid ranges - usually: O(1), but could re-allocate O(r)
|
||||||
let vr = self.ranges[idx];
|
let vr = self.ranges[idx];
|
||||||
let mut remainder = Vec::new();
|
let mut remainder = Vec::new();
|
||||||
if vr.start < read_range.start {
|
if vr.start < read_range.start {
|
||||||
@@ -139,17 +146,17 @@ impl Blob {
|
|||||||
}
|
}
|
||||||
self.ranges.splice(idx..idx + 1, remainder);
|
self.ranges.splice(idx..idx + 1, remainder);
|
||||||
|
|
||||||
// reclaim arrays below the lowest unread byte
|
// reclaim arrays below the lowest unread byte - O(d)
|
||||||
// this will keep memory footprint relatively small
|
// this will keep memory footprint relatively small unless reads are sparse
|
||||||
// at low cost O(1) or worst on re-allocation: O(number_of_arrays)
|
// using Vec<Option<Box<...>>> would handle sparse data better for more stable addressing,
|
||||||
// compared to a Vec<u8> O(n)
|
// but it leaves None slots that cannot be reclaimed as effectively
|
||||||
// could also use a Vec<Option<Box>>, but this would leave None slots
|
|
||||||
let min_unread = self
|
let min_unread = self
|
||||||
.ranges
|
.ranges
|
||||||
.first()
|
.first()
|
||||||
.map(|r| r.start)
|
.map(|r| r.start)
|
||||||
.unwrap_or(self.end_index);
|
.unwrap_or(self.end_index);
|
||||||
while self.dropped < min_unread / ARRAY_SIZE {
|
while self.dropped < min_unread / ARRAY_SIZE {
|
||||||
|
// note: if data is particularly sensitive we should consider zeroing the array on drop
|
||||||
self.arrays.pop_front();
|
self.arrays.pop_front();
|
||||||
self.dropped += 1;
|
self.dropped += 1;
|
||||||
}
|
}
|
||||||
@@ -167,18 +174,35 @@ enum BlobError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct BlobManager {
|
struct BlobManager {
|
||||||
// fine if we assume:
|
// blob indexing is left up to the caller, but
|
||||||
// * IDs are sequentially generated
|
// vector is fine if we assume IDs are sequentially generated
|
||||||
// * and IDs can be re-used
|
// fast access: O(1)
|
||||||
blobs: RwLock<Vec<Option<Arc<Mutex<Blob>>>>>,
|
// usually fast push_back: O(1), unless re-allocation: O(number_of_blobs)
|
||||||
|
// space complexity: O(number_of_blobs * blob_size)
|
||||||
|
// ------------------------
|
||||||
// Also note RwLock (create) starvation might be a concern if caller is
|
// Also note RwLock (create) starvation might be a concern if caller is
|
||||||
// append/read heavy due to platform-defined fairness
|
// append/read heavy due to platform-defined fairness
|
||||||
// todo: better approach?
|
|
||||||
// RwLock benefits shrink as create traffic becomes heavy
|
// RwLock benefits shrink as create traffic becomes heavy
|
||||||
|
// Could address with sharding the collection into n collection shards
|
||||||
|
// and determine which shard to use with id % n
|
||||||
|
// Each shard would have it's own RwLock
|
||||||
|
// ------------------------
|
||||||
|
// If blob creation is arbitrary by ID then
|
||||||
|
// RwLock<HashMap<usize, Arc<Mutex<Blob>>>> would be a better fit.
|
||||||
|
// There wouldn't be a bunch of empty slots so we save on memory
|
||||||
|
// but we would pay a bit in access for hash computation and collision handling
|
||||||
|
// average insert: O(1), but can be long if there are many collisions O(n)
|
||||||
|
// average access: O(1), but can be long if there are many collisions O(n)
|
||||||
|
// ------------------------
|
||||||
|
// If blob IDs are created more arbitrarily
|
||||||
|
// it would probably good to provide an API that quickly pops unused IDs
|
||||||
|
// from a collection (probably VecDeque) so the user doesn't need to poll
|
||||||
|
// the create function on Error
|
||||||
|
// This would make adding blob deletion easier which could get us more
|
||||||
|
// memory space when using big numbers of blobs
|
||||||
|
blobs: RwLock<Vec<Option<Arc<Mutex<Blob>>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: blob indexing is left up to the caller maybe add ID generator
|
|
||||||
// for cross-thread synchronization or change approach
|
|
||||||
impl BlobManager {
|
impl BlobManager {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -191,7 +215,11 @@ impl BlobManager {
|
|||||||
if let Some(Some(_)) = blobs.get(id) {
|
if let Some(Some(_)) = blobs.get(id) {
|
||||||
return Err(BlobError::BlobExists);
|
return Err(BlobError::BlobExists);
|
||||||
} else if id >= blobs.len() {
|
} else if id >= blobs.len() {
|
||||||
// todo: better strategy possible?
|
// note on resizing tradeoff: we assumed above that IDs are sequentially generated
|
||||||
|
// so it might be better to extend the vector by a larger amount all at once (say 2*id)
|
||||||
|
// if we are more interested in paying that cost upfront.
|
||||||
|
// though moving to the cost upfront would make even less sense if IDs are added arbitrarily.
|
||||||
|
// leaving as is for now
|
||||||
blobs.resize_with(id + 1, || None);
|
blobs.resize_with(id + 1, || None);
|
||||||
}
|
}
|
||||||
blobs[id] = Some(Arc::new(Mutex::new(Blob::new())));
|
blobs[id] = Some(Arc::new(Mutex::new(Blob::new())));
|
||||||
@@ -219,7 +247,8 @@ impl BlobManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let read = blob.lock().unwrap().read(start, len)?; // propagate error to caller
|
let read = blob.lock().unwrap().read(start, len)?; // propagate error to caller
|
||||||
// todo: if read clears a blob entirely should the blob be removed?
|
// note: if read clears a blob entirely we maintain the blob for eternal blobs
|
||||||
|
// though could free up some memory if we delete when we don't need anymore
|
||||||
Ok(read)
|
Ok(read)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user