Merge branch 'maxicarlos08/terrain-persistance-perf' into 'master'

Performance improvements for persisted terrain

See merge request veloren/veloren!3914
This commit is contained in:
Joshua Barretto 2023-05-09 21:52:03 +00:00
commit 5d464ba035
3 changed files with 157 additions and 12 deletions

12
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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<Vec2<i32>, Chunk>,
chunks: HashMap<Vec2<i32>, LoadedChunk>,
/// A cache of recently unloaded chunks
cached_chunks: LruMap<Vec2<i32>, 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<i32>, 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<i32>) -> &mut Chunk {
fn load_chunk(&mut self, key: Vec2<i32>) -> &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<i32>) {
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<Vec3<i32>, Block>,
}
@ -186,6 +235,89 @@ impl Chunk {
}
fn reset_block(&mut self, rpos: Vec3<i32>) { 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<Vec2<i32>, Chunk> for ByBlockLimiter {
type KeyToInsert<'a> = Vec2<i32>;
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<i32>, 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<i32>,
_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<i32>, 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