Skip to content

Commit 4f6a7e3

Browse files
committed
add hashmap-based directory cache traits
1 parent 2cb2cfe commit 4f6a7e3

File tree

4 files changed

+129
-15
lines changed

4 files changed

+129
-15
lines changed

src/async_reader.rs

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use reqwest::{Client, IntoUrl};
99
#[cfg(any(feature = "http-async", feature = "mmap-async-tokio"))]
1010
use tokio::io::AsyncReadExt;
1111

12+
use crate::cache::SearchResult;
13+
#[cfg(any(feature = "http-async", feature = "mmap-async-tokio"))]
14+
use crate::cache::{DirectoryCache, NoCache};
1215
use crate::directory::{Directory, Entry};
1316
use crate::error::Error;
1417
use crate::header::{HEADER_SIZE, MAX_INITIAL_BYTES};
@@ -19,17 +22,27 @@ use crate::mmap::MmapBackend;
1922
use crate::tile::tile_id;
2023
use crate::{Compression, Header};
2124

22-
pub struct AsyncPmTilesReader<B> {
25+
pub struct AsyncPmTilesReader<B, C> {
2326
backend: B,
27+
cache: C,
2428
header: Header,
2529
root_directory: Directory,
2630
}
2731

28-
impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B> {
29-
/// Creates a new reader from a specified source and validates the provided PMTiles archive is valid.
32+
impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B, NoCache> {
33+
/// Creates a new cached reader from a specified source and validates the provided PMTiles archive is valid.
3034
///
3135
/// Note: Prefer using new_with_* methods.
3236
pub async fn try_from_source(backend: B) -> Result<Self, Error> {
37+
Self::try_from_cached_source(backend, NoCache).await
38+
}
39+
}
40+
41+
impl<B: AsyncBackend + Sync + Send, C: DirectoryCache + Sync + Send> AsyncPmTilesReader<B, C> {
42+
/// Creates a new reader from a specified source and validates the provided PMTiles archive is valid.
43+
///
44+
/// Note: Prefer using new_with_* methods.
45+
pub async fn try_from_cached_source(backend: B, cache: C) -> Result<Self, Error> {
3346
// Read the first 127 and up to 16,384 bytes to ensure we can initialize the header and root directory.
3447
let mut initial_bytes = backend.read(0, MAX_INITIAL_BYTES).await?;
3548
if initial_bytes.len() < HEADER_SIZE {
@@ -47,11 +60,14 @@ impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B> {
4760

4861
Ok(Self {
4962
backend,
63+
cache,
5064
header,
5165
root_directory,
5266
})
5367
}
68+
}
5469

70+
impl<B: AsyncBackend + Sync + Send, C: DirectoryCache + Sync + Send> AsyncPmTilesReader<B, C> {
5571
/// Fetches tile bytes from the archive.
5672
pub async fn get_tile(&self, z: u8, x: u64, y: u64) -> Option<Bytes> {
5773
let tile_id = tile_id(z, x, y);
@@ -137,11 +153,21 @@ impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B> {
137153
// the recursion is done as two functions because it is a bit cleaner,
138154
// and it allows directory to be cached later without cloning it first.
139155
let offset = (self.header.leaf_offset + entry.offset) as _;
140-
let length = entry.length as _;
141-
let dir = self.read_directory(offset, length).await.ok()?;
142-
let entry = dir.find_tile_id(tile_id);
143156

144-
if let Some(entry) = entry {
157+
let entry = match self.cache.get_dir_entry(offset, tile_id) {
158+
SearchResult::NotCached => {
159+
// Cache miss - read from backend
160+
let length = entry.length as _;
161+
let dir = self.read_directory(offset, length).await.ok()?;
162+
let entry = dir.find_tile_id(tile_id).cloned();
163+
self.cache.insert_dir(offset, dir);
164+
entry
165+
}
166+
SearchResult::NotFound => None,
167+
SearchResult::Found(entry) => Some(entry),
168+
};
169+
170+
if let Some(ref entry) = entry {
145171
if entry.is_leaf() {
146172
return if depth <= 4 {
147173
self.find_entry_rec(tile_id, entry, depth + 1).await
@@ -151,7 +177,7 @@ impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B> {
151177
}
152178
}
153179

154-
entry.cloned()
180+
entry
155181
}
156182

157183
async fn read_directory(&self, offset: usize, length: usize) -> Result<Directory, Error> {
@@ -183,26 +209,50 @@ impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B> {
183209
}
184210

185211
#[cfg(feature = "http-async")]
186-
impl AsyncPmTilesReader<HttpBackend> {
212+
impl AsyncPmTilesReader<HttpBackend, NoCache> {
187213
/// Creates a new PMTiles reader from a URL using the Reqwest backend.
188214
///
189215
/// Fails if [url] does not exist or is an invalid archive. (Note: HTTP requests are made to validate it.)
190216
pub async fn new_with_url<U: IntoUrl>(client: Client, url: U) -> Result<Self, Error> {
217+
Self::new_with_cached_url(client, url, NoCache).await
218+
}
219+
}
220+
221+
#[cfg(feature = "http-async")]
222+
impl<C: DirectoryCache + Sync + Send> AsyncPmTilesReader<HttpBackend, C> {
223+
/// Creates a new PMTiles reader with cache from a URL using the Reqwest backend.
224+
///
225+
/// Fails if [url] does not exist or is an invalid archive. (Note: HTTP requests are made to validate it.)
226+
pub async fn new_with_cached_url<U: IntoUrl>(
227+
client: Client,
228+
url: U,
229+
cache: C,
230+
) -> Result<Self, Error> {
191231
let backend = HttpBackend::try_from(client, url)?;
192232

193-
Self::try_from_source(backend).await
233+
Self::try_from_cached_source(backend, cache).await
194234
}
195235
}
196236

197237
#[cfg(feature = "mmap-async-tokio")]
198-
impl AsyncPmTilesReader<MmapBackend> {
238+
impl AsyncPmTilesReader<MmapBackend, NoCache> {
199239
/// Creates a new PMTiles reader from a file path using the async mmap backend.
200240
///
201241
/// Fails if [p] does not exist or is an invalid archive.
202242
pub async fn new_with_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
243+
Self::new_with_cached_path(path, NoCache).await
244+
}
245+
}
246+
247+
#[cfg(feature = "mmap-async-tokio")]
248+
impl<C: DirectoryCache + Sync + Send> AsyncPmTilesReader<MmapBackend, C> {
249+
/// Creates a new cached PMTiles reader from a file path using the async mmap backend.
250+
///
251+
/// Fails if [p] does not exist or is an invalid archive.
252+
pub async fn new_with_cached_path<P: AsRef<Path>>(path: P, cache: C) -> Result<Self, Error> {
203253
let backend = MmapBackend::try_from(path).await?;
204254

205-
Self::try_from_source(backend).await
255+
Self::try_from_cached_source(backend, cache).await
206256
}
207257
}
208258

src/cache.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use std::collections::HashMap;
2+
use std::sync::{Arc, Mutex};
3+
4+
use crate::directory::{Directory, Entry};
5+
6+
pub enum SearchResult {
7+
NotCached,
8+
NotFound,
9+
Found(Entry),
10+
}
11+
12+
impl From<Option<&Entry>> for SearchResult {
13+
fn from(entry: Option<&Entry>) -> Self {
14+
match entry {
15+
Some(entry) => SearchResult::Found(entry.clone()),
16+
None => SearchResult::NotFound,
17+
}
18+
}
19+
}
20+
21+
/// A cache for PMTiles directories.
22+
pub trait DirectoryCache {
23+
/// Get a directory from the cache, using the offset as a key.
24+
fn get_dir_entry(&self, offset: usize, tile_id: u64) -> SearchResult;
25+
26+
/// Insert a directory into the cache, using the offset as a key.
27+
/// Note that cache must be internally mutable.
28+
fn insert_dir(&self, offset: usize, directory: Directory);
29+
}
30+
31+
pub struct NoCache;
32+
33+
impl DirectoryCache for NoCache {
34+
#[inline]
35+
fn get_dir_entry(&self, _offset: usize, _tile_id: u64) -> SearchResult {
36+
SearchResult::NotCached
37+
}
38+
39+
#[inline]
40+
fn insert_dir(&self, _offset: usize, _directory: Directory) {}
41+
}
42+
43+
/// A simple HashMap-based implementation of a PMTiles directory cache.
44+
#[derive(Default)]
45+
pub struct HashMapCache {
46+
pub cache: Arc<Mutex<HashMap<usize, Directory>>>,
47+
}
48+
49+
impl DirectoryCache for HashMapCache {
50+
fn get_dir_entry(&self, offset: usize, tile_id: u64) -> SearchResult {
51+
if let Some(dir) = self.cache.lock().unwrap().get(&offset) {
52+
return dir.find_tile_id(tile_id).into();
53+
}
54+
SearchResult::NotCached
55+
}
56+
57+
fn insert_dir(&self, offset: usize, directory: Directory) {
58+
self.cache.lock().unwrap().insert(offset, directory);
59+
}
60+
}

src/directory.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use varint_rs::VarintReader;
55

66
use crate::error::Error;
77

8-
pub(crate) struct Directory {
8+
pub struct Directory {
99
entries: Vec<Entry>,
1010
}
1111

@@ -81,7 +81,7 @@ impl TryFrom<Bytes> for Directory {
8181
}
8282

8383
#[derive(Clone, Default, Debug)]
84-
pub(crate) struct Entry {
84+
pub struct Entry {
8585
pub(crate) tile_id: u64,
8686
pub(crate) offset: u64,
8787
pub(crate) length: u32,

src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ pub mod mmap;
1414

1515
#[cfg(any(feature = "http-async", feature = "mmap-async-tokio"))]
1616
pub mod async_reader;
17-
pub mod tile;
17+
18+
#[cfg(any(feature = "http-async", feature = "mmap-async-tokio"))]
19+
pub mod cache;
20+
21+
mod tile;
1822

1923
#[cfg(test)]
2024
mod tests {

0 commit comments

Comments
 (0)