mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Use LRU cache for caching presistent chunks instead of time-based cache
This commit is contained in:
parent
4e136e2961
commit
5c4c47b517
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -5617,6 +5617,17 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"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]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -7283,6 +7294,7 @@ dependencies = [
|
|||||||
"rusqlite",
|
"rusqlite",
|
||||||
"rustls 0.20.8",
|
"rustls 0.20.8",
|
||||||
"rustls-pemfile 1.0.2",
|
"rustls-pemfile 1.0.2",
|
||||||
|
"schnellru",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"slab",
|
"slab",
|
||||||
|
@ -73,3 +73,4 @@ refinery = { version = "0.8.8", features = ["rusqlite"] }
|
|||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"}
|
plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"}
|
||||||
|
schnellru = "0.2.1"
|
||||||
|
@ -4,24 +4,24 @@ use common::{
|
|||||||
vol::{RectRasterableVol, WriteVol},
|
vol::{RectRasterableVol, WriteVol},
|
||||||
};
|
};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
|
use schnellru::{Limiter, LruMap};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
any::{type_name, Any},
|
any::{type_name, Any},
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{self, Read as _, Write as _},
|
io::{self, Read as _, Write as _},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
const UNLOAD_DELAY: Duration = Duration::from_secs(600); // Wait 10 minutes until unloading chunk from IO
|
const MAX_BLOCK_CACHE: usize = 5_000_000;
|
||||||
|
|
||||||
pub struct TerrainPersistence {
|
pub struct TerrainPersistence {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
chunks: HashMap<Vec2<i32>, LoadedChunk>,
|
chunks: HashMap<Vec2<i32>, LoadedChunk>,
|
||||||
/// A cache of recently unloaded chunks
|
/// A cache of recently unloaded chunks
|
||||||
cached_chunks: HashMap<Vec2<i32>, (Instant, Chunk)>,
|
cached_chunks: LruMap<Vec2<i32>, Chunk, ByBlockLimiter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper over a [`Chunk`] that keeps track of modifications
|
/// Wrapper over a [`Chunk`] that keeps track of modifications
|
||||||
@ -51,7 +51,7 @@ impl TerrainPersistence {
|
|||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
chunks: HashMap::default(),
|
chunks: HashMap::default(),
|
||||||
cached_chunks: HashMap::default(),
|
cached_chunks: LruMap::new(ByBlockLimiter::new(MAX_BLOCK_CACHE)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,29 +74,24 @@ impl TerrainPersistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let removed = resets.len();
|
||||||
|
|
||||||
// Reset any unchanged blocks (this is an optimisation only)
|
// Reset any unchanged blocks (this is an optimisation only)
|
||||||
for rpos in resets {
|
for rpos in resets {
|
||||||
loaded_chunk.chunk.reset_block(rpos);
|
loaded_chunk.chunk.reset_block(rpos);
|
||||||
loaded_chunk.modified = true;
|
loaded_chunk.modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.cached_chunks.limiter_mut().remove_blocks(removed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maintain terrain persistence (writing changes changes back to
|
/// Maintain terrain persistence (writing changes changes back to
|
||||||
/// filesystem, unload cached chunks, etc.)
|
/// filesystem, etc.)
|
||||||
pub fn maintain(&mut self) {
|
pub fn maintain(&mut self) {
|
||||||
// Currently, this does nothing because filesystem writeback occurs on
|
// Currently, this does nothing because filesystem writeback occurs on
|
||||||
// chunk unload However, this is not a particularly reliable
|
// chunk unload However, this is not a particularly reliable
|
||||||
// mechanism (it doesn't survive power loss, say). Later, a more
|
// mechanism (it doesn't survive power loss, say). Later, a more
|
||||||
// reliable strategy should be implemented here.
|
// reliable strategy should be implemented here.
|
||||||
|
|
||||||
// Remove chunks from cache that are older than [`UNLOAD_DELAY`]
|
|
||||||
let now = Instant::now();
|
|
||||||
|
|
||||||
self.cached_chunks = self
|
|
||||||
.cached_chunks
|
|
||||||
.drain()
|
|
||||||
.filter(|(_, (unloaded, _))| unloaded.duration_since(now) > UNLOAD_DELAY)
|
|
||||||
.collect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_for(&self, key: Vec2<i32>) -> PathBuf {
|
fn path_for(&self, key: Vec2<i32>) -> PathBuf {
|
||||||
@ -110,7 +105,7 @@ impl TerrainPersistence {
|
|||||||
self.chunks.entry(key).or_insert_with(|| {
|
self.chunks.entry(key).or_insert_with(|| {
|
||||||
// If the chunk has been recently unloaded and is still cached, dont read it
|
// If the chunk has been recently unloaded and is still cached, dont read it
|
||||||
// from disk
|
// from disk
|
||||||
if let Some((_, chunk)) = self.cached_chunks.remove(&key) {
|
if let Some(chunk) = self.cached_chunks.remove(&key) {
|
||||||
return LoadedChunk {
|
return LoadedChunk {
|
||||||
chunk,
|
chunk,
|
||||||
modified: false,
|
modified: false,
|
||||||
@ -165,26 +160,16 @@ impl TerrainPersistence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn unload_chunk(&mut self, key: Vec2<i32>) {
|
pub fn unload_chunk(&mut self, key: Vec2<i32>) {
|
||||||
self.chunks.remove(&key);
|
|
||||||
|
|
||||||
if let Some(LoadedChunk { chunk, modified }) = self.chunks.remove(&key) {
|
if let Some(LoadedChunk { chunk, modified }) = self.chunks.remove(&key) {
|
||||||
let now = Instant::now();
|
match (self.cached_chunks.peek(&key), modified) {
|
||||||
|
(Some(_), false) => {},
|
||||||
match (self.cached_chunks.get_mut(&key), modified) {
|
|
||||||
// Chunk exists in cache but has been modified -> full save
|
|
||||||
(Some(cached), true) => *cached = (now, chunk.clone()),
|
|
||||||
// Chunk exists in cache but has not been modified -> update timestamp
|
|
||||||
(Some((last_unload, _)), false) => *last_unload = now,
|
|
||||||
// Not in cache -> save to cache!
|
|
||||||
_ => {
|
_ => {
|
||||||
self.cached_chunks.insert(key, (now, chunk.clone()));
|
self.cached_chunks.insert(key, chunk.clone());
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to write if no blocks have ever been written
|
// Prevent any uneccesarry IO when nothing in this chunk has changed
|
||||||
// Prevent unecesarry IO by only writing the chunk to disk if blocks have
|
if !modified {
|
||||||
// changed
|
|
||||||
if chunk.blocks.is_empty() || !modified {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,11 +200,17 @@ impl TerrainPersistence {
|
|||||||
.xy()
|
.xy()
|
||||||
.map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32));
|
.map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32));
|
||||||
let loaded_chunk = self.load_chunk(key);
|
let loaded_chunk = self.load_chunk(key);
|
||||||
loaded_chunk
|
let old_block = loaded_chunk
|
||||||
.chunk
|
.chunk
|
||||||
.blocks
|
.blocks
|
||||||
.insert(pos - key * TerrainChunk::RECT_SIZE.map(|e| e as i32), block);
|
.insert(pos - key * TerrainChunk::RECT_SIZE.map(|e| e as i32), block);
|
||||||
loaded_chunk.modified = true;
|
if old_block != Some(block) {
|
||||||
|
loaded_chunk.modified = true;
|
||||||
|
|
||||||
|
if old_block.is_none() {
|
||||||
|
self.cached_chunks.limiter_mut().add_block();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +235,89 @@ impl Chunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reset_block(&mut self, rpos: Vec3<i32>) { self.blocks.remove(&rpos); }
|
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
|
/// # Adding a new chunk format version
|
||||||
|
Loading…
x
Reference in New Issue
Block a user