I've been trying to fine-tune the performance of titiler, and encountered a strange behavior.
I'm not sure where exactly in the stack the problem lies — if it's at titiler's level, or rio-tiler, or rasterio. Feel free to redirect me if this is not the appropriate place to post such an issue.
TL;DR
The short version is that, when trying to get tiles from a COG stored on S3, if boto3 is installed, the time to get the tiles is roughly doubled compared to when boto3 is not installed (note: titiler still manages to access files on S3 without boto3).
This performance drop may be related to the specific setup I have (which I will describe below), but in any case I would like to have your opinion on what is going on.
Reproducing locally
I have a COG stored on a local MinIO instance at s3://bucket/cog.tif.
Details to reproduce locally
```bash
docker run -p 9000:9000 -p 9001:9001 --rm --name minio minio/minio:latest server /data --console-address ":9001"
# Create a bucket called `bucket`
docker exec -it minio mc mb --ignore-existing data/bucket
```
Then go to [the MinIO dashboard](http://localhost:9001/browser/bucket) and upload a `cog.tif` file.
In all further steps, I have exported the following environment variables:
I use the below script to profile the access time to get tiles, launched with python profile.py
Code for profiling
```python
import argparse
import asyncio
import random
import statistics
from collections import Counter
import aiohttp
import morecantile
from rio_tiler.io import COGReader
random.seed(42)
async def fetch(session, url, cog_url):
async with session.get(url, params={"url": cog_url}) as response:
return float(response.headers["server-timing"].split("=")[1]), response.status
async def main(titiler_url, cog_url, tile_number):
tilematrixset = morecantile.tms.get("WebMercatorQuad")
with COGReader(cog_url, tms=tilematrixset) as cog:
minzoom = cog.minzoom
maxzoom = cog.maxzoom
w, s, e, n = cog.geographic_bounds
urls = []
for zoom in range(minzoom, maxzoom + 1):
ul_tile = tilematrixset.tile(w, n, zoom)
lr_tile = tilematrixset.tile(e, s, zoom)
for x in range(ul_tile.x, lr_tile.x + 1):
for y in range(ul_tile.y, lr_tile.y + 1):
urls.append(f"{titiler_url}/cog/tiles/{zoom}/{x}/{y}")
urls = random.choices(urls, k=tile_number)
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url, cog_url) for url in urls]
results = await asyncio.gather(*tasks)
times = [el[0] for el in results]
codes = [el[1] for el in results]
print("Got the following response codes:")
for code, nb in Counter(codes).items():
print(f"- HTTP {code}: {nb} responses")
mean = statistics.mean(times)
stdev = statistics.stdev(times)
print()
print(f"Server response time per tile: {mean:.0f} ± {stdev:.0f} ms")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--titiler", help="URL to the titiler server", default="http://localhost:8000")
parser.add_argument("--cog", help="URL to the Cloud Optimized GeoTIFF", default="s3://bucket/cog.tif")
parser.add_argument("--number", type=int, help="Number of tiles to fetch", default=50)
args = parser.parse_args()
asyncio.run(main(args.titiler, args.cog, args.number))
```
Results
If I run minititiler.py in a python environment which doesn't have boto3 installed (pip install titiler uvicorn):
The tile request times are on the order of 862 ± 240 ms according to profile.py.
In the titiler server logs, I see one rasterio.session:boto3 not available, falling back to a DummySession. log line per requested tile.
If I run minititiler.py in a python environment which has boto3 installed (pip install titiler uvicorn boto3):
The tile request times are on the order of 1916 ± 298 ms according to profile.py.
In the titiler server logs, I see lots of botocore.credentials:Found credentials in environment variables. lines (in my case, 140 lines for 50 requested tiles).
Do you have an explanation for the slowdown caused by the presence of boto3? It's most certainly related to this conditional import in rasterio, but I don't understand why it impacts the performance of titiler.
As you can see in my code for minititiler.py, I've tried to use the environment_dependency field from the TileFactory to pass an already instanciated rasterio.session.AWSSession to titiler's tiling route, but it doesn't seem to make much of a difference. Maybe I'm using it wrong?
Hi,
Thanks for this amazing library!
I've been trying to fine-tune the performance of
titiler
, and encountered a strange behavior. I'm not sure where exactly in the stack the problem lies — if it's attitiler
's level, orrio-tiler
, orrasterio
. Feel free to redirect me if this is not the appropriate place to post such an issue.TL;DR
The short version is that, when trying to get tiles from a COG stored on S3, if
boto3
is installed, the time to get the tiles is roughly doubled compared to whenboto3
is not installed (note:titiler
still manages to access files on S3 withoutboto3
).This performance drop may be related to the specific setup I have (which I will describe below), but in any case I would like to have your opinion on what is going on.
Reproducing locally
I have a COG stored on a local MinIO instance at
s3://bucket/cog.tif
.Details to reproduce locally
```bash docker run -p 9000:9000 -p 9001:9001 --rm --name minio minio/minio:latest server /data --console-address ":9001" # Create a bucket called `bucket` docker exec -it minio mc mb --ignore-existing data/bucket ``` Then go to [the MinIO dashboard](http://localhost:9001/browser/bucket) and upload a `cog.tif` file.In all further steps, I have exported the following environment variables:
I have a minimal
titiler
instance, defined as below, and launched withpython minititiler.py
:Code for minimal titiler
```python import logging import uvicorn from fastapi import FastAPI from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from titiler.core.factory import TilerFactory from titiler.core.middleware import LoggerMiddleware, TotalTimeMiddleware logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="My simple app") try: import boto3 from rasterio.session import AWSSession cog = TilerFactory(environment_dependency=lambda: {"session": AWSSession(boto3.Session())}) except ImportError: cog = TilerFactory() app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"], prefix="/cog") app.add_middleware(LoggerMiddleware, headers=True, querystrings=True) app.add_middleware(TotalTimeMiddleware) add_exception_handlers(app, DEFAULT_STATUS_CODES) if __name__ == "__main__": uvicorn.run(app, log_level="info") ```I use the below script to profile the access time to get tiles, launched with
python profile.py
Code for profiling
```python import argparse import asyncio import random import statistics from collections import Counter import aiohttp import morecantile from rio_tiler.io import COGReader random.seed(42) async def fetch(session, url, cog_url): async with session.get(url, params={"url": cog_url}) as response: return float(response.headers["server-timing"].split("=")[1]), response.status async def main(titiler_url, cog_url, tile_number): tilematrixset = morecantile.tms.get("WebMercatorQuad") with COGReader(cog_url, tms=tilematrixset) as cog: minzoom = cog.minzoom maxzoom = cog.maxzoom w, s, e, n = cog.geographic_bounds urls = [] for zoom in range(minzoom, maxzoom + 1): ul_tile = tilematrixset.tile(w, n, zoom) lr_tile = tilematrixset.tile(e, s, zoom) for x in range(ul_tile.x, lr_tile.x + 1): for y in range(ul_tile.y, lr_tile.y + 1): urls.append(f"{titiler_url}/cog/tiles/{zoom}/{x}/{y}") urls = random.choices(urls, k=tile_number) async with aiohttp.ClientSession() as session: tasks = [fetch(session, url, cog_url) for url in urls] results = await asyncio.gather(*tasks) times = [el[0] for el in results] codes = [el[1] for el in results] print("Got the following response codes:") for code, nb in Counter(codes).items(): print(f"- HTTP {code}: {nb} responses") mean = statistics.mean(times) stdev = statistics.stdev(times) print() print(f"Server response time per tile: {mean:.0f} ± {stdev:.0f} ms") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--titiler", help="URL to the titiler server", default="http://localhost:8000") parser.add_argument("--cog", help="URL to the Cloud Optimized GeoTIFF", default="s3://bucket/cog.tif") parser.add_argument("--number", type=int, help="Number of tiles to fetch", default=50) args = parser.parse_args() asyncio.run(main(args.titiler, args.cog, args.number)) ```Results
minititiler.py
in a python environment which doesn't haveboto3
installed (pip install titiler uvicorn
):profile.py
.titiler
server logs, I see onerasterio.session:boto3 not available, falling back to a DummySession.
log line per requested tile.minititiler.py
in a python environment which hasboto3
installed (pip install titiler uvicorn boto3
):profile.py
.titiler
server logs, I see lots ofbotocore.credentials:Found credentials in environment variables.
lines (in my case, 140 lines for 50 requested tiles).boto3
? It's most certainly related to this conditional import in rasterio, but I don't understand why it impacts the performance oftitiler
.minititiler.py
, I've tried to use theenvironment_dependency
field from theTileFactory
to pass an already instanciatedrasterio.session.AWSSession
totitiler
's tiling route, but it doesn't seem to make much of a difference. Maybe I'm using it wrong?