Skip to content

Commit

Permalink
v0.7.0: rework features, tile_id perf (#31)
Browse files Browse the repository at this point in the history
* optimize `tile_id` to compute base 15-22x faster
* use the same features approach as reqwest - introduce "hidden" features to simplify conditional compilation
* use `just fmt` to clean up some formatting
* slightly shorter vars in a few places
* sort mods first, uses second in the lib.rs
* move all backends into simple files because this way we can incrementally append new functions to the base reader without any conditionals - each backend file defines its own
* add a few `reqwest` crate features to simplify their enablement
* reexport `tilejson` crate


# tile_id perf optimization

I ran criterion benchmarks for zoom 28 and zoom 12. The hardcoded table values only has 0..20 zooms.  Note that this is only base computation, not the hilbert part.

For z28, the performance is 15x better (77.1ns -> 5.3ns)
For z12, the performance is 22x better (24.523ns -> 1.1221ns)

```
tile_id_original_28     time:   [76.980 ns 77.155 ns 77.386 ns]
Found 11 outliers among 100 measurements (11.00%)
  1 (1.00%) high mild
  10 (10.00%) high severe

tile_id_original_12     time:   [24.518 ns 24.523 ns 24.529 ns]
Found 16 outliers among 100 measurements (16.00%)
  9 (9.00%) high mild
  7 (7.00%) high severe

tile_id_new             time:   [5.3056 ns 5.3072 ns 5.3094 ns]
                        change: [-0.4333% -0.1397% +0.0256%] (p = 0.38 > 0.05)
                        No change in performance detected.
Found 8 outliers among 100 measurements (8.00%)
  1 (1.00%) low mild
  4 (4.00%) high mild
  3 (3.00%) high severe

tile_id_new_12          time:   [1.1171 ns 1.1221 ns 1.1313 ns]
Found 13 outliers among 100 measurements (13.00%)
  6 (6.00%) high mild
  7 (7.00%) high severe
```

<details><summary>Benchmarking code</summary>
<p>

add these lines right after the `[dependencies]` section:

```toml
criterion = "0.5.1"
[[bench]]
name = "tile_id"
harness = false
```

Create a new file `benches/tile_id.rs`:

```rust
#![allow(clippy::unreadable_literal)]

use criterion::{black_box, criterion_group, criterion_main, Criterion};

const PYRAMID_SIZE_BY_ZOOM: [u64; 21] = [
    /*  0 */ 0,
    /*  1 */ 1,
    /*  2 */ 5,
    /*  3 */ 21,
    /*  4 */ 85,
    /*  5 */ 341,
    /*  6 */ 1365,
    /*  7 */ 5461,
    /*  8 */ 21845,
    /*  9 */ 87381,
    /* 10 */ 349525,
    /* 11 */ 1398101,
    /* 12 */ 5592405,
    /* 13 */ 22369621,
    /* 14 */ 89478485,
    /* 15 */ 357913941,
    /* 16 */ 1431655765,
    /* 17 */ 5726623061,
    /* 18 */ 22906492245,
    /* 19 */ 91625968981,
    /* 20 */ 366503875925,
];

pub fn tile_id_new(z: u8) -> u64 {
    // The 0/0/0 case is not needed for the base id computation, but it will fail hilbert_2d::u64::xy2h_discrete
    if z == 0 {
        return 0;
    }

    let z_ind = usize::from(z);
    let base_id = if z_ind < PYRAMID_SIZE_BY_ZOOM.len() {
        PYRAMID_SIZE_BY_ZOOM[z_ind]
    } else {
        let last_ind = PYRAMID_SIZE_BY_ZOOM.len() - 1;
        PYRAMID_SIZE_BY_ZOOM[last_ind] + (last_ind..z_ind).map(|i| 1_u64 << (i << 1)).sum::<u64>()
    };

    base_id
}

pub fn tile_id_original(z: u8) -> u64 {
    if z == 0 {
        return 0;
    }

    // TODO: minor optimization with bit shifting
    let base_id: u64 = 1 + (1..z).map(|i| 4u64.pow(u32::from(i))).sum::<u64>();

    base_id
}

fn bench_original(c: &mut Criterion) {
    c.bench_function("tile_id_original_28", |b| {
        b.iter(|| {
            assert_eq!(tile_id_original(black_box(28)), 24019198012642645);
        })
    });

    c.bench_function("tile_id_original_12", |b| {
        b.iter(|| {
            assert_eq!(tile_id_original(black_box(12)), 5592405);
        })
    });
}

fn bench_new(c: &mut Criterion) {
    c.bench_function("tile_id_new", |b| {
        b.iter(|| {
            assert_eq!(tile_id_new(black_box(28)), 24019198012642645);
        })
    });

    c.bench_function("tile_id_new_12", |b| {
        b.iter(|| {
            assert_eq!(tile_id_new(black_box(12)), 5592405);
        })
    });
}

criterion_group!(benches, bench_original, bench_new);
criterion_main!(benches);

```

</p>
</details>
  • Loading branch information
nyurik authored Feb 5, 2024
1 parent 29be5e0 commit b5a9f82
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 241 deletions.
26 changes: 17 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pmtiles"
version = "0.6.0"
version = "0.7.0"
edition = "2021"
authors = ["Luke Seelenbinder <[email protected]>"]
license = "MIT OR Apache-2.0"
Expand All @@ -12,31 +12,39 @@ categories = ["science::geo"]

[features]
default = []
http-async = ["dep:tokio", "dep:reqwest"]
s3-async-native = ["dep:tokio", "dep:rust-s3", "rust-s3/tokio-native-tls"]
s3-async-rustls = ["dep:tokio", "dep:rust-s3", "rust-s3/tokio-rustls-tls"]
mmap-async-tokio = ["dep:tokio", "dep:fmmap", "fmmap?/tokio-async"]
http-async = ["__async", "dep:reqwest"]
mmap-async-tokio = ["__async", "dep:fmmap", "fmmap?/tokio-async"]
s3-async-native = ["__async-s3"]
s3-async-rustls = ["__async-s3"]
tilejson = ["dep:tilejson", "dep:serde", "dep:serde_json"]

# TODO: support other async libraries
# Forward some of the common features to reqwest dependency
reqwest-default = ["reqwest?/default"]
reqwest-native-tls = ["reqwest?/native-tls"]
reqwest-rustls-tls = ["reqwest?/rustls-tls"]
reqwest-rustls-tls-webpki-roots = ["reqwest?/rustls-tls-webpki-roots"]
reqwest-rustls-tls-native-roots = ["reqwest?/rustls-tls-native-roots"]

# Internal features, do not use
__async = ["dep:tokio", "async-compression/tokio"]
__async-s3 = ["__async", "dep:rust-s3", "rust-s3?/tokio-native-tls"]

[dependencies]
# TODO: determine how we want to handle compression in async & sync environments
# TODO: tokio is always requested here, but the tokio dependency is optional below - maybe make it required?
async-compression = { version = "0.4", features = ["gzip", "zstd", "brotli", "tokio"] }
async-compression = { version = "0.4", features = ["gzip", "zstd", "brotli"] }
async-recursion = "1"
async-trait = "0.1"
bytes = "1"
fmmap = { version = "0.3", default-features = false, optional = true }
hilbert_2d = "1"
reqwest = { version = "0.11", default-features = false, optional = true }
rust-s3 = { version = "0.33.0", optional = true, default-features = false, features = ["fail-on-err"] }
serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true }
thiserror = "1"
tilejson = { version = "0.4", optional = true }
tokio = { version = "1", default-features = false, features = ["io-util"], optional = true }
varint-rs = "2"
rust-s3 = { version = "0.33.0", optional = true, default-features = false, features = ["fail-on-err"] }

[dev-dependencies]
flate2 = "1"
Expand Down
101 changes: 3 additions & 98 deletions src/async_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,14 @@
// so any file larger than 4GB, or an untrusted file with bad data may crash.
#![allow(clippy::cast_possible_truncation)]

#[cfg(feature = "mmap-async-tokio")]
use std::path::Path;

use async_recursion::async_recursion;
use async_trait::async_trait;
use bytes::Bytes;
#[cfg(feature = "http-async")]
use reqwest::{Client, IntoUrl};
#[cfg(any(
feature = "http-async",
feature = "mmap-async-tokio",
feature = "s3-async-rustls",
feature = "s3-async-native"
))]
#[cfg(feature = "__async")]
use tokio::io::AsyncReadExt;

#[cfg(feature = "http-async")]
use crate::backend::HttpBackend;
#[cfg(feature = "mmap-async-tokio")]
use crate::backend::MmapBackend;
#[cfg(any(feature = "s3-async-rustls", feature = "s3-async-native"))]
use crate::backend::S3Backend;
use crate::cache::DirCacheResult;
#[cfg(any(
feature = "http-async",
feature = "mmap-async-tokio",
feature = "s3-async-native",
feature = "s3-async-rustls"
))]
#[cfg(feature = "__async")]
use crate::cache::{DirectoryCache, NoCache};
use crate::directory::{DirEntry, Directory};
use crate::error::{PmtError, PmtResult};
Expand Down Expand Up @@ -227,80 +206,6 @@ impl<B: AsyncBackend + Sync + Send, C: DirectoryCache + Sync + Send> AsyncPmTile
}
}

#[cfg(feature = "http-async")]
impl AsyncPmTilesReader<HttpBackend, NoCache> {
/// Creates a new `PMTiles` reader from a URL using the Reqwest backend.
///
/// Fails if [url] does not exist or is an invalid archive. (Note: HTTP requests are made to validate it.)
pub async fn new_with_url<U: IntoUrl>(client: Client, url: U) -> PmtResult<Self> {
Self::new_with_cached_url(NoCache, client, url).await
}
}

#[cfg(feature = "http-async")]
impl<C: DirectoryCache + Sync + Send> AsyncPmTilesReader<HttpBackend, C> {
/// Creates a new `PMTiles` reader with cache from a URL using the Reqwest backend.
///
/// Fails if [url] does not exist or is an invalid archive. (Note: HTTP requests are made to validate it.)
pub async fn new_with_cached_url<U: IntoUrl>(
cache: C,
client: Client,
url: U,
) -> PmtResult<Self> {
let backend = HttpBackend::try_from(client, url)?;

Self::try_from_cached_source(backend, cache).await
}
}

#[cfg(feature = "mmap-async-tokio")]
impl AsyncPmTilesReader<MmapBackend, NoCache> {
/// Creates a new `PMTiles` reader from a file path using the async mmap backend.
///
/// Fails if [p] does not exist or is an invalid archive.
pub async fn new_with_path<P: AsRef<Path>>(path: P) -> PmtResult<Self> {
Self::new_with_cached_path(NoCache, path).await
}
}

#[cfg(feature = "mmap-async-tokio")]
impl<C: DirectoryCache + Sync + Send> AsyncPmTilesReader<MmapBackend, C> {
/// Creates a new cached `PMTiles` reader from a file path using the async mmap backend.
///
/// Fails if [p] does not exist or is an invalid archive.
pub async fn new_with_cached_path<P: AsRef<Path>>(cache: C, path: P) -> PmtResult<Self> {
let backend = MmapBackend::try_from(path).await?;

Self::try_from_cached_source(backend, cache).await
}
}

#[cfg(any(feature = "s3-async-native", feature = "s3-async-rustls"))]
impl AsyncPmTilesReader<S3Backend, NoCache> {
/// Creates a new `PMTiles` reader from a URL using the Reqwest backend.
///
/// Fails if [url] does not exist or is an invalid archive. (Note: HTTP requests are made to validate it.)
pub async fn new_with_bucket_path(bucket: s3::Bucket, path: String) -> PmtResult<Self> {
Self::new_with_cached_bucket_path(NoCache, bucket, path).await
}
}

#[cfg(any(feature = "s3-async-native", feature = "s3-async-rustls"))]
impl<C: DirectoryCache + Sync + Send> AsyncPmTilesReader<S3Backend, C> {
/// Creates a new `PMTiles` reader with cache from a URL using the Reqwest backend.
///
/// Fails if [url] does not exist or is an invalid archive. (Note: HTTP requests are made to validate it.)
pub async fn new_with_cached_bucket_path(
cache: C,
bucket: s3::Bucket,
path: String,
) -> PmtResult<Self> {
let backend = S3Backend::from(bucket, path);

Self::try_from_cached_source(backend, cache).await
}
}

#[async_trait]
pub trait AsyncBackend {
/// Reads exactly `length` bytes starting at `offset`
Expand All @@ -314,8 +219,8 @@ pub trait AsyncBackend {
#[cfg(feature = "mmap-async-tokio")]
mod tests {
use super::AsyncPmTilesReader;
use crate::backend::MmapBackend;
use crate::tests::{RASTER_FILE, VECTOR_FILE};
use crate::MmapBackend;

#[tokio::test]
async fn open_sanity_check() {
Expand Down
17 changes: 0 additions & 17 deletions src/backend/mod.rs

This file was deleted.

55 changes: 0 additions & 55 deletions src/backend/s3.rs

This file was deleted.

41 changes: 33 additions & 8 deletions src/backend/http.rs → src/backend_http.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,47 @@
use async_trait::async_trait;
use bytes::Bytes;
use reqwest::{
header::{HeaderValue, RANGE},
Client, IntoUrl, Method, Request, StatusCode, Url,
};
use reqwest::header::{HeaderValue, RANGE};
use reqwest::{Client, IntoUrl, Method, Request, StatusCode, Url};

use crate::{async_reader::AsyncBackend, error::PmtResult, PmtError};
use crate::async_reader::{AsyncBackend, AsyncPmTilesReader};
use crate::cache::{DirectoryCache, NoCache};
use crate::error::PmtResult;
use crate::PmtError;

impl AsyncPmTilesReader<HttpBackend, NoCache> {
/// Creates a new `PMTiles` reader from a URL using the Reqwest backend.
///
/// Fails if [url] does not exist or is an invalid archive. (Note: HTTP requests are made to validate it.)
pub async fn new_with_url<U: IntoUrl>(client: Client, url: U) -> PmtResult<Self> {
Self::new_with_cached_url(NoCache, client, url).await
}
}

impl<C: DirectoryCache + Sync + Send> AsyncPmTilesReader<HttpBackend, C> {
/// Creates a new `PMTiles` reader with cache from a URL using the Reqwest backend.
///
/// Fails if [url] does not exist or is an invalid archive. (Note: HTTP requests are made to validate it.)
pub async fn new_with_cached_url<U: IntoUrl>(
cache: C,
client: Client,
url: U,
) -> PmtResult<Self> {
let backend = HttpBackend::try_from(client, url)?;

Self::try_from_cached_source(backend, cache).await
}
}

pub struct HttpBackend {
client: Client,
pmtiles_url: Url,
url: Url,
}

impl HttpBackend {
pub fn try_from<U: IntoUrl>(client: Client, url: U) -> PmtResult<Self> {
Ok(HttpBackend {
client,
pmtiles_url: url.into_url()?,
url: url.into_url()?,
})
}
}
Expand All @@ -41,7 +66,7 @@ impl AsyncBackend for HttpBackend {
let range = format!("bytes={offset}-{end}");
let range = HeaderValue::try_from(range)?;

let mut req = Request::new(Method::GET, self.pmtiles_url.clone());
let mut req = Request::new(Method::GET, self.url.clone());
req.headers_mut().insert(RANGE, range);

let response = self.client.execute(req).await?.error_for_status()?;
Expand Down
23 changes: 22 additions & 1 deletion src/backend/mmap.rs → src/backend_mmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,30 @@ use async_trait::async_trait;
use bytes::{Buf, Bytes};
use fmmap::tokio::{AsyncMmapFile, AsyncMmapFileExt as _, AsyncOptions};

use crate::async_reader::AsyncBackend;
use crate::async_reader::{AsyncBackend, AsyncPmTilesReader};
use crate::cache::{DirectoryCache, NoCache};
use crate::error::{PmtError, PmtResult};

impl AsyncPmTilesReader<MmapBackend, NoCache> {
/// Creates a new `PMTiles` reader from a file path using the async mmap backend.
///
/// Fails if [p] does not exist or is an invalid archive.
pub async fn new_with_path<P: AsRef<Path>>(path: P) -> PmtResult<Self> {
Self::new_with_cached_path(NoCache, path).await
}
}

impl<C: DirectoryCache + Sync + Send> AsyncPmTilesReader<MmapBackend, C> {
/// Creates a new cached `PMTiles` reader from a file path using the async mmap backend.
///
/// Fails if [p] does not exist or is an invalid archive.
pub async fn new_with_cached_path<P: AsRef<Path>>(cache: C, path: P) -> PmtResult<Self> {
let backend = MmapBackend::try_from(path).await?;

Self::try_from_cached_source(backend, cache).await
}
}

pub struct MmapBackend {
file: AsyncMmapFile,
}
Expand Down
Loading

0 comments on commit b5a9f82

Please sign in to comment.