diff --git a/Cargo.lock b/Cargo.lock index d9afd8c3ad..28c488b310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5617,6 +5617,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "schnellru" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772575a524feeb803e5b0fcbc6dd9f367e579488197c94c6e4023aad2305774d" +dependencies = [ + "ahash 0.8.3", + "cfg-if 1.0.0", + "hashbrown 0.13.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -7283,6 +7294,7 @@ dependencies = [ "rusqlite", "rustls 0.20.8", "rustls-pemfile 1.0.2", + "schnellru", "serde", "serde_json", "slab", diff --git a/server/Cargo.toml b/server/Cargo.toml index e0cc1c2b78..b47ae06a33 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -73,3 +73,4 @@ refinery = { version = "0.8.8", features = ["rusqlite"] } # Plugins plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"} +schnellru = "0.2.1" diff --git a/server/src/terrain_persistence.rs b/server/src/terrain_persistence.rs index 8df1f407eb..5f47d4873d 100644 --- a/server/src/terrain_persistence.rs +++ b/server/src/terrain_persistence.rs @@ -4,6 +4,7 @@ use common::{ vol::{RectRasterableVol, WriteVol}, }; use hashbrown::HashMap; +use schnellru::{Limiter, LruMap}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ any::{type_name, Any}, @@ -14,9 +15,20 @@ use std::{ use tracing::{debug, error, info, warn}; use vek::*; +const MAX_BLOCK_CACHE: usize = 5_000_000; + pub struct TerrainPersistence { path: PathBuf, - chunks: HashMap, Chunk>, + chunks: HashMap, LoadedChunk>, + /// A cache of recently unloaded chunks + cached_chunks: LruMap, Chunk, ByBlockLimiter>, +} + +/// Wrapper over a [`Chunk`] that keeps track of modifications +#[derive(Default)] +pub struct LoadedChunk { + chunk: Chunk, + modified: bool, } impl TerrainPersistence { @@ -39,15 +51,16 @@ impl TerrainPersistence { Self { path, chunks: HashMap::default(), + cached_chunks: LruMap::new(ByBlockLimiter::new(MAX_BLOCK_CACHE)), } } /// Apply persistence changes to a newly generated chunk. pub fn apply_changes(&mut self, key: Vec2, terrain_chunk: &mut TerrainChunk) { - let chunk = self.load_chunk(key); + let loaded_chunk = self.load_chunk(key); let mut resets = Vec::new(); - for (rpos, new_block) in chunk.blocks() { + for (rpos, new_block) in loaded_chunk.chunk.blocks() { if let Err(e) = terrain_chunk.map(rpos, |block| { if block == new_block { resets.push(rpos); @@ -61,10 +74,15 @@ impl TerrainPersistence { } } + let removed = resets.len(); + // Reset any unchanged blocks (this is an optimisation only) for rpos in resets { - chunk.reset_block(rpos); + loaded_chunk.chunk.reset_block(rpos); + loaded_chunk.modified = true; } + + self.cached_chunks.limiter_mut().remove_blocks(removed); } /// Maintain terrain persistence (writing changes changes back to @@ -82,9 +100,18 @@ impl TerrainPersistence { path } - fn load_chunk(&mut self, key: Vec2) -> &mut Chunk { + fn load_chunk(&mut self, key: Vec2) -> &mut LoadedChunk { let path = self.path_for(key); self.chunks.entry(key).or_insert_with(|| { + // If the chunk has been recently unloaded and is still cached, dont read it + // from disk + if let Some(chunk) = self.cached_chunks.remove(&key) { + return LoadedChunk { + chunk, + modified: false, + }; + } + File::open(&path) .ok() .map(|f| { @@ -95,10 +122,10 @@ impl TerrainPersistence { "Failed to read data for chunk {:?} from file: {:?}", key, err ); - return Chunk::default(); + return LoadedChunk::default(); }, }; - match Chunk::deserialize_from(io::Cursor::new(bytes)) { + let chunk = match Chunk::deserialize_from(io::Cursor::new(bytes)) { Some(chunk) => chunk, None => { // Find an untaken name for a backup @@ -120,6 +147,12 @@ impl TerrainPersistence { } Chunk::default() }, + }; + + LoadedChunk { + chunk, + + modified: false, } }) .unwrap_or_default() @@ -127,9 +160,16 @@ impl TerrainPersistence { } pub fn unload_chunk(&mut self, key: Vec2) { - if let Some(chunk) = self.chunks.remove(&key) { - // No need to write if no blocks have ever been written - if chunk.blocks.is_empty() { + if let Some(LoadedChunk { chunk, modified }) = self.chunks.remove(&key) { + match (self.cached_chunks.peek(&key), modified) { + (Some(_), false) => {}, + _ => { + self.cached_chunks.insert(key, chunk.clone()); + }, + } + + // Prevent any uneccesarry IO when nothing in this chunk has changed + if !modified { return; } @@ -159,9 +199,18 @@ impl TerrainPersistence { let key = pos .xy() .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); - self.load_chunk(key) + let loaded_chunk = self.load_chunk(key); + let old_block = loaded_chunk + .chunk .blocks .insert(pos - key * TerrainChunk::RECT_SIZE.map(|e| e as i32), block); + if old_block != Some(block) { + loaded_chunk.modified = true; + + if old_block.is_none() { + self.cached_chunks.limiter_mut().add_block(); + } + } } } @@ -169,7 +218,7 @@ impl Drop for TerrainPersistence { fn drop(&mut self) { self.unload_all(); } } -#[derive(Default, Serialize, Deserialize)] +#[derive(Default, Serialize, Deserialize, Clone)] pub struct Chunk { blocks: HashMap, Block>, } @@ -186,6 +235,89 @@ impl Chunk { } fn reset_block(&mut self, rpos: Vec3) { self.blocks.remove(&rpos); } + + /// Get the number of blocks this chunk contains + fn len(&self) -> usize { self.blocks.len() } +} + +/// LRU limiter that limits by the number of blocks +/// +/// > **Warning**: Make sure to call [`add_block`] and [`remove_block`] when +/// > performing direct mutations to a chunk +struct ByBlockLimiter { + /// Maximum number of blocks that can be contained + block_limit: usize, + /// Total number of blocks that are currently contained in the LRU + counted_blocks: usize, +} + +impl Limiter, Chunk> for ByBlockLimiter { + type KeyToInsert<'a> = Vec2; + type LinkType = u32; + + fn is_over_the_limit(&self, _length: usize) -> bool { false } + + fn on_insert( + &mut self, + _length: usize, + key: Self::KeyToInsert<'_>, + chunk: Chunk, + ) -> Option<(Vec2, Chunk)> { + let chunk_size = chunk.len(); + + if self.counted_blocks + chunk_size > self.block_limit { + None + } else { + self.counted_blocks += chunk_size; + Some((key, chunk)) + } + } + + fn on_replace( + &mut self, + _length: usize, + _old_key: &mut Vec2, + _new_key: Self::KeyToInsert<'_>, + old_chunk: &mut Chunk, + new_chunk: &mut Chunk, + ) -> bool { + let old_size = old_chunk.len() as isize; // I assume chunks are never larger than a few thousand blocks anyways, cast should be OK + let new_size = new_chunk.len() as isize; + let new_total = self.counted_blocks.wrapping_add_signed(new_size - old_size); + + if new_total > self.block_limit { + false + } else { + self.counted_blocks = new_total; + true + } + } + + fn on_removed(&mut self, _key: &mut Vec2, chunk: &mut Chunk) { + self.counted_blocks -= chunk.len(); + } + + fn on_cleared(&mut self) { self.counted_blocks = 0; } + + fn on_grow(&mut self, _new_memory_usage: usize) -> bool { true } +} + +impl ByBlockLimiter { + /// Creates a new by-block limit + fn new(block_limit: usize) -> Self { + Self { + block_limit, + counted_blocks: 0, + } + } + + /// This function should only be used when it is guaranteed that a block has + /// been added + fn add_block(&mut self) { self.counted_blocks += 1; } + + /// This function should only be used when it is guaranteed that this number + /// of blocks has been removed + fn remove_blocks(&mut self, removed: usize) { self.counted_blocks -= removed; } } /// # Adding a new chunk format version