From 4287bb20881c14b6597f3fea30e528b297daec54 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Wed, 24 Sep 2025 14:15:57 -0500 Subject: [PATCH 01/18] set up an optimized Lambda container function --- infrastructure/aws/cdk/app.py | 14 ++--- infrastructure/aws/lambda/Dockerfile | 90 +++++++++++++++++++++------- infrastructure/aws/lambda/handler.py | 69 ++++++++++++++++++++- 3 files changed, 139 insertions(+), 34 deletions(-) diff --git a/infrastructure/aws/cdk/app.py b/infrastructure/aws/cdk/app.py index eec6fb3..38af43d 100644 --- a/infrastructure/aws/cdk/app.py +++ b/infrastructure/aws/cdk/app.py @@ -14,6 +14,7 @@ from aws_cdk import aws_sns as sns from aws_cdk import aws_sns_subscriptions as subscriptions from aws_cdk.aws_apigatewayv2_integrations import HttpLambdaIntegration +from aws_cdk.aws_ecr_assets import Platform from config import AppSettings, StackSettings from constructs import Construct @@ -121,19 +122,14 @@ def __init__( role_arn=app_settings.reader_role_arn, ) - lambda_function = aws_lambda.Function( + lambda_function = aws_lambda.DockerImageFunction( self, f"{id}-lambda", - runtime=runtime, - code=aws_lambda.Code.from_docker_build( - path=os.path.abspath(context_dir), + code=aws_lambda.DockerImageCode.from_image_asset( + directory=os.path.abspath(context_dir), file="infrastructure/aws/lambda/Dockerfile", - platform="linux/amd64", - build_args={ - "PYTHON_VERSION": runtime.to_string().replace("python", ""), - }, + platform=Platform.LINUX_AMD64, ), - handler="handler.handler", memory_size=memory, reserved_concurrent_executions=concurrent, timeout=Duration.seconds(timeout), diff --git a/infrastructure/aws/lambda/Dockerfile b/infrastructure/aws/lambda/Dockerfile index 86c99ce..bb31739 100644 --- a/infrastructure/aws/lambda/Dockerfile +++ b/infrastructure/aws/lambda/Dockerfile @@ -1,37 +1,81 @@ ARG PYTHON_VERSION=3.12 -FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} +# Build stage - includes all build tools and dependencies +FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} AS builder + +# Copy uv for faster dependency management COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ -WORKDIR /tmp +# Install system dependencies needed for compilation +RUN dnf install -y gcc-c++ && dnf clean all -# Install system dependencies to compile (numexpr) -RUN dnf install -y gcc-c++ +# Set working directory for build +WORKDIR /build -COPY uv.lock .python-version pyproject.toml LICENSE README.md ./ +# Copy dependency files first for better caching +COPY README.md uv.lock .python-version pyproject.toml ./ COPY src/titiler/ ./src/titiler/ +# Install dependencies to temporary directory with Lambda-specific optimizations RUN uv export --locked --no-editable --no-dev --extra lambda --format requirements.txt -o requirements.txt && \ - uv pip install --compile-bytecode --no-binary pydantic --target /asset -r requirements.txt + uv pip install \ + --compile-bytecode \ + --no-binary pydantic \ + --target /deps \ + --no-cache-dir \ + --disable-pip-version-check \ + -r requirements.txt + +# Aggressive cleanup to minimize size and optimize for Lambda container +RUN cd /deps && \ + # Convert .pyc files and remove source .py files for faster cold starts + find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done && \ + find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf && \ + find . -type f -a -name '*.py' -print0 | xargs -0 rm -f && \ + # Remove unnecessary files for Lambda runtime + find . -type d -a -name 'tests' -print0 | xargs -0 rm -rf && \ + find . -type d -a -name 'test' -print0 | xargs -0 rm -rf && \ + find . -name '*.dist-info' -type d -exec rm -rf {} + 2>/dev/null || true && \ + find . -name '*.egg-info' -type d -exec rm -rf {} + 2>/dev/null || true && \ + rm -rf numpy/doc/ bin/ geos_license Misc/ && \ + # Remove unnecessary locale and documentation files + find . -name '*.mo' -delete && \ + find . -name '*.po' -delete && \ + find . -name 'LICENSE*' -delete && \ + find . -name 'README*' -delete && \ + find . -name '*.md' -delete && \ + # Strip debug symbols from shared libraries (preserve numpy.libs) + find . -type f -name '*.so*' -not -path "*/numpy.libs/*" -exec strip --strip-unneeded {} \; 2>/dev/null || true && \ + # Create a manifest file for debugging + du -sh . > /tmp/package_size.txt + +# Final runtime stage - minimal Lambda image optimized for container runtime +FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} + +# Set Lambda-specific environment variables for optimal performance +ENV PYTHONPATH=${LAMBDA_RUNTIME_DIR} \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + AWS_LWA_ENABLE_COMPRESSION=true + +# Copy only the cleaned dependencies from builder stage +COPY --from=builder /deps ${LAMBDA_RUNTIME_DIR}/ -# copy libexpat.so.1 into /asset which is included in LD_LIBRARY_PATH -RUN cp /usr/lib64/libexpat.so.1 /asset/ +# Copy required system library +COPY --from=builder /usr/lib64/libexpat.so.1 ${LAMBDA_RUNTIME_DIR}/ -# Reduce package size and remove useless files -RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done; -RUN cd /asset && find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf -RUN cd /asset && find . -type f -a -name '*.py' -print0 | xargs -0 rm -f -RUN find /asset -type d -a -name 'tests' -print0 | xargs -0 rm -rf -RUN rm -rdf /asset/numpy/doc/ /asset/bin /asset/geos_license /asset/Misc -RUN rm -rdf /asset/boto3* -RUN rm -rdf /asset/botocore* +# Copy package size manifest for debugging +COPY --from=builder /tmp/package_size.txt /tmp/ -# Strip debug symbols from compiled C/C++ code (except for numpy.libs!) -RUN cd /asset && \ - find . -type f -name '*.so*' \ - -not -path "./numpy.libs/*" \ - -exec strip --strip-unneeded {} \; +# Copy application handler +COPY infrastructure/aws/lambda/handler.py ${LAMBDA_RUNTIME_DIR}/ -COPY infrastructure/aws/lambda/handler.py /asset/handler.py +# Ensure handler is executable and optimize permissions +RUN chmod 644 ${LAMBDA_RUNTIME_DIR}/handler.py && \ + # Pre-compile the handler for faster cold starts + python -c "import py_compile; py_compile.compile('${LAMBDA_RUNTIME_DIR}/handler.py', doraise=True)" && \ + # Create cache directories with proper permissions + mkdir -p /tmp/.cache && chmod 777 /tmp/.cache -CMD ["echo", "hello world"] +# Set the Lambda handler +CMD ["handler.lambda_handler"] diff --git a/infrastructure/aws/lambda/handler.py b/infrastructure/aws/lambda/handler.py index 39f0c76..af05f72 100644 --- a/infrastructure/aws/lambda/handler.py +++ b/infrastructure/aws/lambda/handler.py @@ -1,12 +1,77 @@ -"""AWS Lambda handler.""" +"""AWS Lambda handler optimized for container runtime.""" import logging +import os +import warnings +from typing import Any, Dict from mangum import Mangum from titiler.multidim.main import app +# Configure logging for Lambda CloudWatch integration logging.getLogger("mangum.lifespan").setLevel(logging.ERROR) logging.getLogger("mangum.http").setLevel(logging.ERROR) +logging.getLogger("botocore").setLevel(logging.WARNING) +logging.getLogger("urllib3").setLevel(logging.WARNING) -handler = Mangum(app, lifespan="off") +# Suppress warnings for cleaner CloudWatch logs +warnings.filterwarnings("ignore", category=UserWarning) +warnings.filterwarnings("ignore", category=FutureWarning) + +# Lambda container optimizations +os.environ.setdefault("PYTHONPATH", "/var/runtime") +os.environ.setdefault("AWS_DEFAULT_REGION", os.environ.get("AWS_REGION", "us-west-2")) + +# Pre-import commonly used modules for faster cold starts +try: + import numpy # noqa: F401 + import pandas # noqa: F401 + import rioxarray # noqa: F401 + import xarray # noqa: F401 +except ImportError: + pass + +# Initialize Mangum with optimizations for Lambda containers +handler = Mangum( + app, + lifespan="off", # Disable lifespan for Lambda + api_gateway_base_path=None, + text_mime_types=[ + "application/json", + "application/javascript", + "application/xml", + "application/vnd.api+json", + ], +) + +# Global variables for connection reuse across invocations +_connections_initialized = False + + +def _initialize_connections() -> None: + """Initialize connections that can be reused across Lambda invocations.""" + global _connections_initialized + if not _connections_initialized: + # Force initialization of any global connections/pools here + # This helps with subsequent invocation performance + _connections_initialized = True + + +def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + """Lambda handler with container-specific optimizations.""" + # Initialize connections on first invocation + _initialize_connections() + + # Handle the request + response = handler(event, context) + + # Optional: Force garbage collection for memory management + # Uncomment if experiencing memory issues + # gc.collect() + + return response + + +# Alias for backward compatibility and direct Mangum usage +handler.lambda_handler = lambda_handler From 4bcb1cd4527e5dad04fa22174f8dad87e1b4925d Mon Sep 17 00:00:00 2001 From: hrodmn Date: Wed, 24 Sep 2025 14:22:40 -0500 Subject: [PATCH 02/18] ignore more things --- .dockerignore | 9 +++++---- .gitignore | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index cc90e7d..9c10098 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ -# Ignore cdk folder -cdk.out -.history -.tox .git +infrastructure/aws/cdk.out +.venv +.mypy_cache +.pytest_cache +.ruff_cache diff --git a/.gitignore b/.gitignore index 959f966..a912a05 100644 --- a/.gitignore +++ b/.gitignore @@ -101,6 +101,7 @@ ENV/ # mypy .mypy_cache/ +infrastructure/aws/cdk.out/ cdk.out/ node_modules cdk.context.json From 5bc2fe6990e4fa5a96a444404fc4e942a0163324 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Wed, 24 Sep 2025 15:09:41 -0500 Subject: [PATCH 03/18] don't delete package metadata --- infrastructure/aws/lambda/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/infrastructure/aws/lambda/Dockerfile b/infrastructure/aws/lambda/Dockerfile index bb31739..84f7cda 100644 --- a/infrastructure/aws/lambda/Dockerfile +++ b/infrastructure/aws/lambda/Dockerfile @@ -35,8 +35,6 @@ RUN cd /deps && \ # Remove unnecessary files for Lambda runtime find . -type d -a -name 'tests' -print0 | xargs -0 rm -rf && \ find . -type d -a -name 'test' -print0 | xargs -0 rm -rf && \ - find . -name '*.dist-info' -type d -exec rm -rf {} + 2>/dev/null || true && \ - find . -name '*.egg-info' -type d -exec rm -rf {} + 2>/dev/null || true && \ rm -rf numpy/doc/ bin/ geos_license Misc/ && \ # Remove unnecessary locale and documentation files find . -name '*.mo' -delete && \ From fed5205cab8fade055bd6fa80710199828c60d20 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Wed, 24 Sep 2025 16:24:39 -0500 Subject: [PATCH 04/18] add benchmark script --- scripts/benchmark.py | 274 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 scripts/benchmark.py diff --git a/scripts/benchmark.py b/scripts/benchmark.py new file mode 100644 index 0000000..3ce1a51 --- /dev/null +++ b/scripts/benchmark.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +Benchmark script for titiler-multidim Lambda performance testing. + +This script tests Lambda performance by: +1. Warming up with a tilejson request +2. Measuring tile loading performance at zoom level 4 +3. Providing comprehensive statistics + +Usage: + uv run benchmark.py --api-url https://your-lambda-url.amazonaws.com +""" + +import argparse +import asyncio +import csv +import os +import statistics +import time +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple + +import httpx + +# Test parameters +DATASET_PARAMS = { + "url": "s3://mur-sst/zarr-v1", + "variable": "analysed_sst", + "sel": "time=2018-03-02T09:00:00.000000000", + "rescale": "250,350", + "colormap_name": "viridis", +} + +# Zoom 4 covers the world with 16x16 tiles = 256 total tiles +ZOOM_LEVEL = 4 +TILES_PER_SIDE = 2**ZOOM_LEVEL # 16 tiles per side at zoom 4 + + +@dataclass +class BenchmarkResult: + """Container for benchmark results.""" + + warmup_time: float = 0.0 + warmup_success: bool = False + tile_times: List[float] = [] + tile_failures: List[Tuple[int, int]] = [] + total_runtime: float = 0.0 + start_time: float = 0.0 + + +async def fetch_tilejson( + client: httpx.AsyncClient, api_url: str +) -> Tuple[float, bool, Optional[Dict]]: + """Fetch tilejson to warm up the Lambda and get tile URL template.""" + url = f"{api_url}/WebMercatorQuad/tilejson.json" + + start_time = time.time() + try: + response = await client.get(url, params=DATASET_PARAMS, timeout=60.0) + elapsed = time.time() - start_time + + if response.status_code == 200: + return elapsed, True, response.json() + else: + print(f"Tilejson request failed with status {response.status_code}") + return elapsed, False, None + + except Exception as e: + elapsed = time.time() - start_time + print(f"Tilejson request failed: {e}") + return elapsed, False, None + + +async def fetch_tile( + client: httpx.AsyncClient, + api_url: str, + x: int, + y: int, + semaphore: asyncio.Semaphore, +) -> Tuple[int, int, float, bool]: + """Fetch a single tile and return timing information.""" + async with semaphore: + url = f"{api_url}/tiles/WebMercatorQuad/{ZOOM_LEVEL}/{x}/{y}.png" + + start_time = time.time() + try: + response = await client.get(url, params=DATASET_PARAMS, timeout=30.0) + elapsed = time.time() - start_time + success = response.status_code == 200 + return x, y, elapsed, success + + except Exception as e: + elapsed = time.time() - start_time + print(f"Tile {x},{y} failed: {e}") + return x, y, elapsed, False + + +async def benchmark_tiles( + client: httpx.AsyncClient, api_url: str, max_concurrent: int = 20 +) -> BenchmarkResult: + """Run the complete benchmark test.""" + result = BenchmarkResult() + result.start_time = time.time() + + # Step 1: Warmup with tilejson request + print("πŸš€ Warming up Lambda with tilejson request...") + warmup_time, warmup_success, tilejson_data = await fetch_tilejson(client, api_url) + result.warmup_time = warmup_time + result.warmup_success = warmup_success + + if warmup_success: + print(f"βœ… Warmup successful in {warmup_time:.2f}s") + else: + print(f"❌ Warmup failed after {warmup_time:.2f}s") + return result + + # Step 2: Generate all tile coordinates for zoom 4 + print( + f"πŸ“ Generating {TILES_PER_SIDE}x{TILES_PER_SIDE} = {TILES_PER_SIDE**2} tile coordinates..." + ) + tile_coords = [(x, y) for x in range(TILES_PER_SIDE) for y in range(TILES_PER_SIDE)] + + # Step 3: Fetch all tiles concurrently + print( + f"🌍 Fetching all zoom {ZOOM_LEVEL} tiles (max {max_concurrent} concurrent)..." + ) + semaphore = asyncio.Semaphore(max_concurrent) + + tasks = [fetch_tile(client, api_url, x, y, semaphore) for x, y in tile_coords] + + # Show progress as tiles complete + completed = 0 + for task in asyncio.as_completed(tasks): + x, y, elapsed, success = await task + completed += 1 + + if success: + result.tile_times.append(elapsed) + else: + result.tile_failures.append((x, y)) + + # Show progress every 10% completion + if completed % (len(tile_coords) // 10) == 0: + progress = (completed / len(tile_coords)) * 100 + print(f" Progress: {progress:.0f}% ({completed}/{len(tile_coords)} tiles)") + + result.total_runtime = time.time() - result.start_time + return result + + +def print_summary(result: BenchmarkResult): + """Print comprehensive benchmark statistics.""" + print("\n" + "=" * 60) + print("🏁 BENCHMARK SUMMARY") + print("=" * 60) + + # Warmup stats + print("Warmup Request:") + print(f" Status: {'βœ… Success' if result.warmup_success else '❌ Failed'}") + print(f" Time: {result.warmup_time:.2f}s") + print() + + # Overall stats + print(f"Total Runtime: {result.total_runtime:.2f}s") + print() + + # Tile request stats + total_tiles = len(result.tile_times) + len(result.tile_failures) + success_count = len(result.tile_times) + failure_count = len(result.tile_failures) + success_rate = (success_count / total_tiles * 100) if total_tiles > 0 else 0 + + print("Tile Request Summary:") + print(f" Total tiles: {total_tiles}") + print(f" Successful: {success_count} ({success_rate:.1f}%)") + print(f" Failed: {failure_count} ({100 - success_rate:.1f}%)") + print() + + if result.tile_times: + # Response time statistics + avg_time = statistics.mean(result.tile_times) + min_time = min(result.tile_times) + max_time = max(result.tile_times) + median_time = statistics.median(result.tile_times) + + # Calculate percentiles + sorted_times = sorted(result.tile_times) + p95_idx = int(0.95 * len(sorted_times)) + p95_time = sorted_times[p95_idx] + + print("Response Time Analysis:") + print(f" Average: {avg_time:.3f}s") + print(f" Minimum: {min_time:.3f}s") + print(f" Maximum: {max_time:.3f}s") + print(f" Median: {median_time:.3f}s") + print(f" 95th percentile: {p95_time:.3f}s") + print() + + # Throughput metrics + tile_loading_time = result.total_runtime - result.warmup_time + throughput = success_count / tile_loading_time if tile_loading_time > 0 else 0 + + print("Throughput Metrics:") + print(f" Tiles per second: {throughput:.1f}") + print(f" Tile loading time: {tile_loading_time:.2f}s") + + if result.tile_failures: + print( + f"\nFailed Tiles: {result.tile_failures[:10]}{'...' if len(result.tile_failures) > 10 else ''}" + ) + + +def export_csv(result: BenchmarkResult, filename: str = "benchmark_results.csv"): + """Export detailed results to CSV.""" + with open(filename, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["tile_x", "tile_y", "response_time_s", "success"]) + + # Write successful tiles + tile_coords = [ + (x, y) for x in range(TILES_PER_SIDE) for y in range(TILES_PER_SIDE) + ] + tile_idx = 0 + failure_coords = set(result.tile_failures) + + for x, y in tile_coords: + if (x, y) in failure_coords: + writer.writerow([x, y, "N/A", False]) + elif tile_idx < len(result.tile_times): + writer.writerow([x, y, f"{result.tile_times[tile_idx]:.3f}", True]) + tile_idx += 1 + + print(f"πŸ“Š Detailed results exported to {filename}") + + +async def main(): + """Main benchmark execution.""" + parser = argparse.ArgumentParser( + description="Benchmark titiler-multidim Lambda performance" + ) + parser.add_argument("--api-url", required=True, help="Lambda API URL") + parser.add_argument( + "--max-concurrent", type=int, default=20, help="Maximum concurrent requests" + ) + parser.add_argument( + "--export-csv", action="store_true", help="Export results to CSV" + ) + + args = parser.parse_args() + + # Override with environment variable if set + api_url = os.environ.get("API_URL", args.api_url) + + print(f"🎯 Benchmarking Lambda at: {api_url}") + print(f"πŸ“Š Dataset: {DATASET_PARAMS['url']}") + print(f"πŸ” Variable: {DATASET_PARAMS['variable']}") + print(f"⚑ Max concurrent requests: {args.max_concurrent}") + print() + + # Configure httpx client with appropriate timeouts + timeout = httpx.Timeout(60.0, connect=10.0) + limits = httpx.Limits(max_connections=args.max_concurrent * 2) + + async with httpx.AsyncClient(timeout=timeout, limits=limits) as client: + result = await benchmark_tiles(client, api_url, args.max_concurrent) + + print_summary(result) + + if args.export_csv: + export_csv(result) + + +if __name__ == "__main__": + asyncio.run(main()) From 7b4b2fff4ec5462cfde861df4c1fbf130f04df44 Mon Sep 17 00:00:00 2001 From: Henry Rodman Date: Thu, 25 Sep 2025 11:52:49 -0500 Subject: [PATCH 05/18] Update infrastructure/aws/lambda/Dockerfile Co-authored-by: Chuck Daniels --- infrastructure/aws/lambda/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/aws/lambda/Dockerfile b/infrastructure/aws/lambda/Dockerfile index 84f7cda..3355d49 100644 --- a/infrastructure/aws/lambda/Dockerfile +++ b/infrastructure/aws/lambda/Dockerfile @@ -1,7 +1,7 @@ ARG PYTHON_VERSION=3.12 # Build stage - includes all build tools and dependencies -FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} AS builder +FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} AS builder # Copy uv for faster dependency management COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ From e9f6f72b971763eb0505849604df60a20888b5bc Mon Sep 17 00:00:00 2001 From: Henry Rodman Date: Thu, 25 Sep 2025 11:52:58 -0500 Subject: [PATCH 06/18] Update infrastructure/aws/lambda/Dockerfile Co-authored-by: Chuck Daniels --- infrastructure/aws/lambda/Dockerfile | 41 +++++++++++++++------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/infrastructure/aws/lambda/Dockerfile b/infrastructure/aws/lambda/Dockerfile index 3355d49..4cb9cbb 100644 --- a/infrastructure/aws/lambda/Dockerfile +++ b/infrastructure/aws/lambda/Dockerfile @@ -27,25 +27,28 @@ RUN uv export --locked --no-editable --no-dev --extra lambda --format requiremen -r requirements.txt # Aggressive cleanup to minimize size and optimize for Lambda container -RUN cd /deps && \ - # Convert .pyc files and remove source .py files for faster cold starts - find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done && \ - find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf && \ - find . -type f -a -name '*.py' -print0 | xargs -0 rm -f && \ - # Remove unnecessary files for Lambda runtime - find . -type d -a -name 'tests' -print0 | xargs -0 rm -rf && \ - find . -type d -a -name 'test' -print0 | xargs -0 rm -rf && \ - rm -rf numpy/doc/ bin/ geos_license Misc/ && \ - # Remove unnecessary locale and documentation files - find . -name '*.mo' -delete && \ - find . -name '*.po' -delete && \ - find . -name 'LICENSE*' -delete && \ - find . -name 'README*' -delete && \ - find . -name '*.md' -delete && \ - # Strip debug symbols from shared libraries (preserve numpy.libs) - find . -type f -name '*.so*' -not -path "*/numpy.libs/*" -exec strip --strip-unneeded {} \; 2>/dev/null || true && \ - # Create a manifest file for debugging - du -sh . > /tmp/package_size.txt +WORKDIR /deps +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN </dev/null || true +# Create a manifest file for debugging +du -sh . > /tmp/package_size.txt +EOF # Final runtime stage - minimal Lambda image optimized for container runtime FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} From 3ef2362f4be65e4cf3461363a1fb22740578f081 Mon Sep 17 00:00:00 2001 From: Henry Rodman Date: Thu, 25 Sep 2025 11:53:12 -0500 Subject: [PATCH 07/18] Update infrastructure/aws/lambda/Dockerfile Co-authored-by: Chuck Daniels --- infrastructure/aws/lambda/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/aws/lambda/Dockerfile b/infrastructure/aws/lambda/Dockerfile index 4cb9cbb..a4b1cc0 100644 --- a/infrastructure/aws/lambda/Dockerfile +++ b/infrastructure/aws/lambda/Dockerfile @@ -51,7 +51,7 @@ du -sh . > /tmp/package_size.txt EOF # Final runtime stage - minimal Lambda image optimized for container runtime -FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} +FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} # Set Lambda-specific environment variables for optimal performance ENV PYTHONPATH=${LAMBDA_RUNTIME_DIR} \ From d4156909c4502c326a220d8044dceeb551468d7a Mon Sep 17 00:00:00 2001 From: Henry Rodman Date: Thu, 25 Sep 2025 11:53:24 -0500 Subject: [PATCH 08/18] Update infrastructure/aws/lambda/Dockerfile Co-authored-by: Chuck Daniels --- infrastructure/aws/lambda/Dockerfile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/infrastructure/aws/lambda/Dockerfile b/infrastructure/aws/lambda/Dockerfile index a4b1cc0..77d32df 100644 --- a/infrastructure/aws/lambda/Dockerfile +++ b/infrastructure/aws/lambda/Dockerfile @@ -72,11 +72,13 @@ COPY --from=builder /tmp/package_size.txt /tmp/ COPY infrastructure/aws/lambda/handler.py ${LAMBDA_RUNTIME_DIR}/ # Ensure handler is executable and optimize permissions -RUN chmod 644 ${LAMBDA_RUNTIME_DIR}/handler.py && \ - # Pre-compile the handler for faster cold starts - python -c "import py_compile; py_compile.compile('${LAMBDA_RUNTIME_DIR}/handler.py', doraise=True)" && \ - # Create cache directories with proper permissions - mkdir -p /tmp/.cache && chmod 777 /tmp/.cache +RUN < Date: Thu, 25 Sep 2025 11:53:44 -0500 Subject: [PATCH 09/18] Update .dockerignore Co-authored-by: Chuck Daniels --- .dockerignore | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.dockerignore b/.dockerignore index 9c10098..c16d32b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ -.git -infrastructure/aws/cdk.out -.venv -.mypy_cache -.pytest_cache -.ruff_cache +* +!src/ +!infrastructure/aws/lambda/*.py +!.python-version +!pyproject.toml +!README.md +!uv.lock From de4f4d03b3f08ce0bd49a8f48c5a163b23044897 Mon Sep 17 00:00:00 2001 From: Henry Rodman Date: Thu, 25 Sep 2025 11:53:50 -0500 Subject: [PATCH 10/18] Update .gitignore Co-authored-by: Chuck Daniels --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a912a05..959f966 100644 --- a/.gitignore +++ b/.gitignore @@ -101,7 +101,6 @@ ENV/ # mypy .mypy_cache/ -infrastructure/aws/cdk.out/ cdk.out/ node_modules cdk.context.json From 025a6cd0bead6a4b67269928c31b86b2f739a575 Mon Sep 17 00:00:00 2001 From: Henry Rodman Date: Thu, 25 Sep 2025 11:54:23 -0500 Subject: [PATCH 11/18] Apply suggestions from code review Co-authored-by: Chuck Daniels --- infrastructure/aws/lambda/handler.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/infrastructure/aws/lambda/handler.py b/infrastructure/aws/lambda/handler.py index af05f72..443caa9 100644 --- a/infrastructure/aws/lambda/handler.py +++ b/infrastructure/aws/lambda/handler.py @@ -45,24 +45,9 @@ ], ) -# Global variables for connection reuse across invocations -_connections_initialized = False - - -def _initialize_connections() -> None: - """Initialize connections that can be reused across Lambda invocations.""" - global _connections_initialized - if not _connections_initialized: - # Force initialization of any global connections/pools here - # This helps with subsequent invocation performance - _connections_initialized = True - def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: """Lambda handler with container-specific optimizations.""" - # Initialize connections on first invocation - _initialize_connections() - # Handle the request response = handler(event, context) From 9db95fa992daa16a57a26249ad9d7289d95aa2ec Mon Sep 17 00:00:00 2001 From: hrodmn Date: Thu, 25 Sep 2025 12:02:35 -0500 Subject: [PATCH 12/18] apply suggestions from review --- infrastructure/aws/lambda/Dockerfile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/infrastructure/aws/lambda/Dockerfile b/infrastructure/aws/lambda/Dockerfile index 77d32df..e53f88e 100644 --- a/infrastructure/aws/lambda/Dockerfile +++ b/infrastructure/aws/lambda/Dockerfile @@ -60,13 +60,8 @@ ENV PYTHONPATH=${LAMBDA_RUNTIME_DIR} \ AWS_LWA_ENABLE_COMPRESSION=true # Copy only the cleaned dependencies from builder stage -COPY --from=builder /deps ${LAMBDA_RUNTIME_DIR}/ - # Copy required system library -COPY --from=builder /usr/lib64/libexpat.so.1 ${LAMBDA_RUNTIME_DIR}/ - -# Copy package size manifest for debugging -COPY --from=builder /tmp/package_size.txt /tmp/ +COPY --from=builder /deps /usr/lib64/libexpat.so.1 ${LAMBDA_RUNTIME_DIR}/ # Copy application handler COPY infrastructure/aws/lambda/handler.py ${LAMBDA_RUNTIME_DIR}/ From bc927fd75cb1c9812fd19d9794d6fea4e1b3e557 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Thu, 25 Sep 2025 12:15:44 -0500 Subject: [PATCH 13/18] remove dockerfile from .dockerignore --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index c16d32b..1603fd7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ * !src/ !infrastructure/aws/lambda/*.py +!infrastructure/aws/lambda/Dockerfile !.python-version !pyproject.toml !README.md From 3eb1e3e3db2eea67123b7fb74998ae9709a255cc Mon Sep 17 00:00:00 2001 From: hrodmn Date: Thu, 25 Sep 2025 12:18:45 -0500 Subject: [PATCH 14/18] revert to old .dockerignore --- .dockerignore | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.dockerignore b/.dockerignore index 1603fd7..9c10098 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,6 @@ -* -!src/ -!infrastructure/aws/lambda/*.py -!infrastructure/aws/lambda/Dockerfile -!.python-version -!pyproject.toml -!README.md -!uv.lock +.git +infrastructure/aws/cdk.out +.venv +.mypy_cache +.pytest_cache +.ruff_cache From 72387a89b315d77513ecfb65a825f66fb2e82087 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Thu, 25 Sep 2025 13:09:38 -0500 Subject: [PATCH 15/18] make benchmark script more configurable --- scripts/benchmark.py | 204 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 166 insertions(+), 38 deletions(-) diff --git a/scripts/benchmark.py b/scripts/benchmark.py index 3ce1a51..9dcf056 100644 --- a/scripts/benchmark.py +++ b/scripts/benchmark.py @@ -3,24 +3,53 @@ Benchmark script for titiler-multidim Lambda performance testing. This script tests Lambda performance by: -1. Warming up with a tilejson request -2. Measuring tile loading performance at zoom level 4 -3. Providing comprehensive statistics +1. Warming up with a tilejson request to get dataset bounds +2. Generating tile coordinates that intersect the dataset bounds using morecantile +3. Measuring tile loading performance at the specified zoom level +4. Providing comprehensive statistics -Usage: +Usage Examples: + # Basic usage with default parameters (zoom 4, hardcoded dataset) uv run benchmark.py --api-url https://your-lambda-url.amazonaws.com + + # Specify custom zoom level + uv run benchmark.py --api-url https://your-lambda-url.amazonaws.com --zoom 6 + + # Use dataset parameters from JSON file + uv run benchmark.py --api-url https://your-lambda-url.amazonaws.com --dataset-json dataset.json --zoom 5 + + # Use dataset parameters from STDIN + echo '{"url": "s3://bucket/data.zarr", "variable": "temp"}' | uv run benchmark.py --api-url https://your-lambda-url.amazonaws.com --dataset-stdin + + # Export results to CSV + uv run benchmark.py --api-url https://your-lambda-url.amazonaws.com --zoom 4 --export-csv + + # Combine multiple options + uv run benchmark.py --api-url https://your-lambda-url.amazonaws.com --dataset-json my-dataset.json --zoom 7 --max-concurrent 30 --export-csv + +Dataset JSON format: + { + "url": "s3://bucket/path/to/dataset.zarr", + "variable": "temperature", + "sel": "time=2023-01-01T00:00:00.000000000", + "rescale": "250,350", + "colormap_name": "viridis" + } """ import argparse import asyncio import csv +import json import os import statistics +import sys import time -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Dict, List, Optional, Tuple import httpx +import morecantile # Test parameters DATASET_PARAMS = { @@ -31,32 +60,70 @@ "colormap_name": "viridis", } -# Zoom 4 covers the world with 16x16 tiles = 256 total tiles -ZOOM_LEVEL = 4 -TILES_PER_SIDE = 2**ZOOM_LEVEL # 16 tiles per side at zoom 4 +# Default zoom level (can be overridden by command line argument) +DEFAULT_ZOOM_LEVEL = 4 + + +def load_dataset_params( + json_file: Optional[str] = None, use_stdin: bool = False +) -> Dict: + """Load dataset parameters from JSON file, STDIN, or use defaults.""" + if use_stdin: + try: + return json.load(sys.stdin) + except json.JSONDecodeError as e: + print(f"Error parsing JSON from STDIN: {e}") + sys.exit(1) + elif json_file: + try: + with open(json_file, "r") as f: + return json.load(f) + except FileNotFoundError: + print(f"Error: Dataset JSON file '{json_file}' not found") + sys.exit(1) + except json.JSONDecodeError as e: + print(f"Error parsing JSON from file '{json_file}': {e}") + sys.exit(1) + else: + return DATASET_PARAMS + + +def get_tiles_for_bounds(bounds: List[float], zoom: int) -> List[Tuple[int, int]]: + """Generate tile coordinates for the given bounds and zoom level using morecantile.""" + west, south, east, north = bounds + + tms = morecantile.tms.get("WebMercatorQuad") + + # Generate tiles that intersect with the bounds + tiles = list(tms.tiles(west, south, east, north, [zoom])) + + # Return as (x, y) coordinate tuples + return [(tile.x, tile.y) for tile in tiles] @dataclass class BenchmarkResult: """Container for benchmark results.""" + zoom_level: int = 0 warmup_time: float = 0.0 warmup_success: bool = False - tile_times: List[float] = [] - tile_failures: List[Tuple[int, int]] = [] + tile_times: List[float] = field(default_factory=list) + tile_failures: List[Tuple[int, int]] = field(default_factory=list) + tile_coords: List[Tuple[int, int]] = field(default_factory=list) total_runtime: float = 0.0 start_time: float = 0.0 async def fetch_tilejson( - client: httpx.AsyncClient, api_url: str + client: httpx.AsyncClient, api_url: str, dataset_params: Dict ) -> Tuple[float, bool, Optional[Dict]]: - """Fetch tilejson to warm up the Lambda and get tile URL template.""" + """Fetch tilejson to warm up the Lambda and get bounds information.""" url = f"{api_url}/WebMercatorQuad/tilejson.json" start_time = time.time() try: - response = await client.get(url, params=DATASET_PARAMS, timeout=60.0) + response = await client.get(url, params=dataset_params, timeout=60.0) elapsed = time.time() - start_time if response.status_code == 200: @@ -76,15 +143,17 @@ async def fetch_tile( api_url: str, x: int, y: int, + zoom: int, + dataset_params: Dict, semaphore: asyncio.Semaphore, ) -> Tuple[int, int, float, bool]: """Fetch a single tile and return timing information.""" async with semaphore: - url = f"{api_url}/tiles/WebMercatorQuad/{ZOOM_LEVEL}/{x}/{y}.png" + url = f"{api_url}/tiles/WebMercatorQuad/{zoom}/{x}/{y}.png" start_time = time.time() try: - response = await client.get(url, params=DATASET_PARAMS, timeout=30.0) + response = await client.get(url, params=dataset_params, timeout=30.0) elapsed = time.time() - start_time success = response.status_code == 200 return x, y, elapsed, success @@ -96,40 +165,74 @@ async def fetch_tile( async def benchmark_tiles( - client: httpx.AsyncClient, api_url: str, max_concurrent: int = 20 + client: httpx.AsyncClient, + api_url: str, + zoom: int, + dataset_params: Dict, + max_concurrent: int = 20, ) -> BenchmarkResult: """Run the complete benchmark test.""" result = BenchmarkResult() + result.zoom_level = zoom result.start_time = time.time() - # Step 1: Warmup with tilejson request + # Step 1: Warmup with tilejson request and get bounds print("πŸš€ Warming up Lambda with tilejson request...") - warmup_time, warmup_success, tilejson_data = await fetch_tilejson(client, api_url) + warmup_time, warmup_success, tilejson_data = await fetch_tilejson( + client, api_url, dataset_params + ) result.warmup_time = warmup_time result.warmup_success = warmup_success if warmup_success: print(f"βœ… Warmup successful in {warmup_time:.2f}s") + + # Display dataset information from tilejson + if tilejson_data: + print("πŸ—ΊοΈ Dataset info:") + if "bounds" in tilejson_data: + print(f" Bounds: {tilejson_data['bounds']}") + if "center" in tilejson_data: + print(f" Center: {tilejson_data['center']}") + if "minzoom" in tilejson_data and "maxzoom" in tilejson_data: + print( + f" Zoom range: {tilejson_data['minzoom']} - {tilejson_data['maxzoom']}" + ) + print("πŸ“‹ Full TileJSON response:") + print(f" {json.dumps(tilejson_data, indent=2)}") + print() else: print(f"❌ Warmup failed after {warmup_time:.2f}s") return result - # Step 2: Generate all tile coordinates for zoom 4 - print( - f"πŸ“ Generating {TILES_PER_SIDE}x{TILES_PER_SIDE} = {TILES_PER_SIDE**2} tile coordinates..." - ) - tile_coords = [(x, y) for x in range(TILES_PER_SIDE) for y in range(TILES_PER_SIDE)] + # Step 2: Extract bounds and generate tile coordinates + if not tilejson_data or "bounds" not in tilejson_data: + print("❌ No bounds found in tilejson response, falling back to world bounds") + bounds = [-180.0, -90.0, 180.0, 90.0] # World bounds + else: + bounds = tilejson_data["bounds"] + + print(f"πŸ“ Using bounds: {bounds}") + print(f"πŸ“ Generating tile coordinates for zoom level {zoom}...") + + tile_coords = get_tiles_for_bounds(bounds, zoom) + result.tile_coords = tile_coords # Store for CSV export + + print(f"πŸ“ Found {len(tile_coords)} tiles intersecting dataset bounds") # Step 3: Fetch all tiles concurrently - print( - f"🌍 Fetching all zoom {ZOOM_LEVEL} tiles (max {max_concurrent} concurrent)..." - ) + print(f"🌍 Fetching zoom {zoom} tiles (max {max_concurrent} concurrent)...") semaphore = asyncio.Semaphore(max_concurrent) - tasks = [fetch_tile(client, api_url, x, y, semaphore) for x, y in tile_coords] + tasks = [ + fetch_tile(client, api_url, x, y, zoom, dataset_params, semaphore) + for x, y in tile_coords + ] # Show progress as tiles complete completed = 0 + progress_interval = max(1, len(tile_coords) // 10) if len(tile_coords) >= 10 else 1 + for task in asyncio.as_completed(tasks): x, y, elapsed, success = await task completed += 1 @@ -139,8 +242,8 @@ async def benchmark_tiles( else: result.tile_failures.append((x, y)) - # Show progress every 10% completion - if completed % (len(tile_coords) // 10) == 0: + # Show progress + if completed % progress_interval == 0 or completed == len(tile_coords): progress = (completed / len(tile_coords)) * 100 print(f" Progress: {progress:.0f}% ({completed}/{len(tile_coords)} tiles)") @@ -171,6 +274,7 @@ def print_summary(result: BenchmarkResult): success_rate = (success_count / total_tiles * 100) if total_tiles > 0 else 0 print("Tile Request Summary:") + print(f" Zoom level: {result.zoom_level}") print(f" Total tiles: {total_tiles}") print(f" Successful: {success_count} ({success_rate:.1f}%)") print(f" Failed: {failure_count} ({100 - success_rate:.1f}%)") @@ -212,23 +316,29 @@ def print_summary(result: BenchmarkResult): def export_csv(result: BenchmarkResult, filename: str = "benchmark_results.csv"): """Export detailed results to CSV.""" + if not result.tile_coords: + print("❌ No tile coordinates available for CSV export") + return + with open(filename, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["tile_x", "tile_y", "response_time_s", "success"]) - # Write successful tiles - tile_coords = [ - (x, y) for x in range(TILES_PER_SIDE) for y in range(TILES_PER_SIDE) - ] - tile_idx = 0 + # Create mapping of failed tiles failure_coords = set(result.tile_failures) - for x, y in tile_coords: + # Keep track of successful tiles in order (they align with tile_times) + tile_idx = 0 + + for x, y in result.tile_coords: if (x, y) in failure_coords: writer.writerow([x, y, "N/A", False]) elif tile_idx < len(result.tile_times): writer.writerow([x, y, f"{result.tile_times[tile_idx]:.3f}", True]) tile_idx += 1 + else: + # This shouldn't happen, but just in case + writer.writerow([x, y, "N/A", "Unknown"]) print(f"πŸ“Š Detailed results exported to {filename}") @@ -245,15 +355,31 @@ async def main(): parser.add_argument( "--export-csv", action="store_true", help="Export results to CSV" ) + parser.add_argument( + "--zoom", type=int, default=4, help="Zoom level for tile requests (default: 4)" + ) + parser.add_argument( + "--dataset-json", help="JSON file path containing dataset parameters" + ) + parser.add_argument( + "--dataset-stdin", + action="store_true", + help="Read dataset parameters from STDIN as JSON", + ) args = parser.parse_args() + # Load dataset parameters + dataset_params = load_dataset_params(args.dataset_json, args.dataset_stdin) + # Override with environment variable if set api_url = os.environ.get("API_URL", args.api_url) print(f"🎯 Benchmarking Lambda at: {api_url}") - print(f"πŸ“Š Dataset: {DATASET_PARAMS['url']}") - print(f"πŸ” Variable: {DATASET_PARAMS['variable']}") + print("πŸ“Š Dataset parameters:") + for key, value in dataset_params.items(): + print(f" {key}: {value}") + print(f"πŸ” Zoom level: {args.zoom}") print(f"⚑ Max concurrent requests: {args.max_concurrent}") print() @@ -262,7 +388,9 @@ async def main(): limits = httpx.Limits(max_connections=args.max_concurrent * 2) async with httpx.AsyncClient(timeout=timeout, limits=limits) as client: - result = await benchmark_tiles(client, api_url, args.max_concurrent) + result = await benchmark_tiles( + client, api_url, args.zoom, dataset_params, args.max_concurrent + ) print_summary(result) From 3b941a027c4c313a9ad4b16d3e36a06fbf186e54 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Thu, 25 Sep 2025 13:10:01 -0500 Subject: [PATCH 16/18] update package lock --- infrastructure/aws/package-lock.json | 37 +++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/infrastructure/aws/package-lock.json b/infrastructure/aws/package-lock.json index ad52264..89a2f4b 100644 --- a/infrastructure/aws/package-lock.json +++ b/infrastructure/aws/package-lock.json @@ -9,35 +9,37 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "cdk": "2.76.0-alpha.0" + "cdk": "^2.177.0" } }, "node_modules/aws-cdk": { - "version": "2.76.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.76.0.tgz", - "integrity": "sha512-y6VHtqUpYenn6mGIBFbcGGXIoXfKA3o0eGL/eeD/gUJ9TcPrgMLQM1NxSMb5JVsOk5BPPXzGmvB0gBu40utGqg==", + "version": "2.1029.3", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1029.3.tgz", + "integrity": "sha512-otRJP5a4r07S+SLKs/WvJH+0auZHkaRMnv1vtD4fpp1figV8Vr9MKdB4QPNjfKdLGyK9f95OEHwVlIW9xpjPBg==", + "license": "Apache-2.0", "bin": { "cdk": "bin/cdk" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.0.0" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/cdk": { - "version": "2.76.0-alpha.0", - "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.76.0-alpha.0.tgz", - "integrity": "sha512-HNfX5c7MU18LxthZRcapqEhG0IFgQeNOhtsTR1QiL/7dhy2TjvK26dYcJ67KIHfzMfm5EUjvOXdP1SPdW+eOOA==", + "version": "2.1029.3", + "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.1029.3.tgz", + "integrity": "sha512-VZMLvllt1H4e1BIeitw6ZCSoAg7TEGQisN/0WsOAjsUoEk0Bwkpl0yPBu4JzY9MX/x1AdYPqGYkSO04LvBcLnQ==", + "license": "Apache-2.0", "dependencies": { - "aws-cdk": "2.76.0" + "aws-cdk": "2.1029.3" }, "bin": { "cdk": "bin/cdk" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 18.0.0" } }, "node_modules/fsevents": { @@ -45,6 +47,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -56,19 +59,19 @@ }, "dependencies": { "aws-cdk": { - "version": "2.76.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.76.0.tgz", - "integrity": "sha512-y6VHtqUpYenn6mGIBFbcGGXIoXfKA3o0eGL/eeD/gUJ9TcPrgMLQM1NxSMb5JVsOk5BPPXzGmvB0gBu40utGqg==", + "version": "2.1029.3", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1029.3.tgz", + "integrity": "sha512-otRJP5a4r07S+SLKs/WvJH+0auZHkaRMnv1vtD4fpp1figV8Vr9MKdB4QPNjfKdLGyK9f95OEHwVlIW9xpjPBg==", "requires": { "fsevents": "2.3.2" } }, "cdk": { - "version": "2.76.0-alpha.0", - "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.76.0-alpha.0.tgz", - "integrity": "sha512-HNfX5c7MU18LxthZRcapqEhG0IFgQeNOhtsTR1QiL/7dhy2TjvK26dYcJ67KIHfzMfm5EUjvOXdP1SPdW+eOOA==", + "version": "2.1029.3", + "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.1029.3.tgz", + "integrity": "sha512-VZMLvllt1H4e1BIeitw6ZCSoAg7TEGQisN/0WsOAjsUoEk0Bwkpl0yPBu4JzY9MX/x1AdYPqGYkSO04LvBcLnQ==", "requires": { - "aws-cdk": "2.76.0" + "aws-cdk": "2.1029.3" } }, "fsevents": { From e36058b9a4840d8e5dc78db47d8c0eb7cd415229 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Tue, 7 Oct 2025 10:31:12 -0500 Subject: [PATCH 17/18] clean up handler --- infrastructure/aws/lambda/handler.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/infrastructure/aws/lambda/handler.py b/infrastructure/aws/lambda/handler.py index 443caa9..81b6568 100644 --- a/infrastructure/aws/lambda/handler.py +++ b/infrastructure/aws/lambda/handler.py @@ -1,7 +1,6 @@ """AWS Lambda handler optimized for container runtime.""" import logging -import os import warnings from typing import Any, Dict @@ -9,19 +8,14 @@ from titiler.multidim.main import app -# Configure logging for Lambda CloudWatch integration logging.getLogger("mangum.lifespan").setLevel(logging.ERROR) logging.getLogger("mangum.http").setLevel(logging.ERROR) logging.getLogger("botocore").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING) -# Suppress warnings for cleaner CloudWatch logs warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=FutureWarning) -# Lambda container optimizations -os.environ.setdefault("PYTHONPATH", "/var/runtime") -os.environ.setdefault("AWS_DEFAULT_REGION", os.environ.get("AWS_REGION", "us-west-2")) # Pre-import commonly used modules for faster cold starts try: @@ -32,10 +26,9 @@ except ImportError: pass -# Initialize Mangum with optimizations for Lambda containers handler = Mangum( app, - lifespan="off", # Disable lifespan for Lambda + lifespan="off", api_gateway_base_path=None, text_mime_types=[ "application/json", @@ -48,15 +41,9 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: """Lambda handler with container-specific optimizations.""" - # Handle the request response = handler(event, context) - # Optional: Force garbage collection for memory management - # Uncomment if experiencing memory issues - # gc.collect() - return response -# Alias for backward compatibility and direct Mangum usage handler.lambda_handler = lambda_handler From d0a27e2360fc6ccee3a873ce7f1e992db8d16bce Mon Sep 17 00:00:00 2001 From: hrodmn Date: Tue, 7 Oct 2025 10:32:06 -0500 Subject: [PATCH 18/18] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa310b6..74c01d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## unreleased + +* Convert Lambda to a containerized function + ## 0.3.1 * Upgrade to `titiler>=0.21,<0.22`