Skip to content

Latest commit

 

History

History
511 lines (355 loc) · 12.5 KB

File metadata and controls

511 lines (355 loc) · 12.5 KB

Performance Optimization Guide

This document explains the performance optimizations implemented in the transcript-create project, including database query optimization, caching strategies, and API response improvements.

Table of Contents

Overview

The performance optimization implementation focuses on three key areas:

  1. Database Optimization: Strategic indices for hot paths and complex queries
  2. Caching Layer: Redis-backed caching with intelligent TTLs and invalidation
  3. Response Optimization: Compression, cache headers, and efficient data transfer

Performance Targets

  • API p95 latency: < 500ms
  • Search results: < 1 second
  • Database queries: No slow query warnings (> 100ms)
  • Cache hit rate: > 80%

Database Query Optimization

New Indices

Five strategic indices have been added to optimize the most frequent database operations:

1. Job Queue Ordering Index

CREATE INDEX jobs_queue_ordering_idx ON jobs(state, priority, created_at);

Purpose: Accelerates worker job selection queries Impact: Reduces job queue scan time by 40-60%

2. Pending Jobs Partial Index

CREATE INDEX jobs_pending_idx ON jobs(created_at)
WHERE state IN ('pending', 'downloading');

Purpose: Optimizes the worker's hot path for finding next job Impact: Smaller index size, faster scans for active jobs

3. User Email Lookup Index

CREATE INDEX users_email_idx ON users(email);

Purpose: Speeds up authentication lookups Impact: 70-90% faster login/session validation

4. Event User Created Index

CREATE INDEX events_user_created_idx ON events(user_id, created_at DESC);

Purpose: Optimizes quota check queries Impact: 50-80% faster rate limit checks

5. Session User ID Index

CREATE INDEX sessions_user_id_idx ON sessions(user_id);

Purpose: Enables fast reverse session lookups Impact: Supports efficient session management

Query Patterns Optimized

Worker job selection:

SELECT * FROM jobs
WHERE state IN ('pending', 'downloading')
ORDER BY priority, created_at
LIMIT 10;

Quota checks:

SELECT COUNT(*) FROM events
WHERE user_id = ?
AND created_at >= date_trunc('day', now() AT TIME ZONE 'UTC');

User authentication:

SELECT id FROM users WHERE email = ?;

Applying the Migration

The indices are applied via Alembic migration:

# Run migration
python scripts/run_migrations.py upgrade

# Or with Docker Compose (automatic)
docker compose up migrations

Redis Caching Layer

Architecture

The caching layer uses Redis as a high-performance in-memory store with the following features:

  • Decorator-based: Simple @cache() decorator for functions
  • Automatic key generation: Hashes complex arguments
  • TTL-based expiration: Different lifetimes for different resource types
  • Graceful degradation: Works without Redis (falls back to direct DB queries)
  • Pattern invalidation: Bulk cache clearing by key pattern

Cache Configuration

Default TTL values (configurable via environment variables):

Resource Type TTL Reason
Video metadata 5 minutes Relatively stable, occasional updates
Transcript segments 1 hour Immutable once created
Search results 10 minutes Balance freshness vs performance
Session data 1 hour User-specific, moderate freshness

Usage Examples

Caching a Function

from app.cache import cache
from app.settings import settings

@cache(prefix="video", ttl=settings.CACHE_VIDEO_TTL)
def get_video(db, video_id: uuid.UUID):
    return db.execute(
        text("SELECT * FROM videos WHERE id=:v"),
        {"v": str(video_id)}
    ).mappings().first()

Cache Invalidation

from app.cache import invalidate_cache, invalidate_cache_pattern

# Invalidate specific cache entry
invalidate_cache("video", video_id)

# Invalidate all video caches
invalidate_cache_pattern("video:*")

# Invalidate segments for a video
invalidate_cache_pattern(f"segments:{video_id}*")

Custom Cache Key

def my_cache_key(*args, **kwargs):
    user_id = kwargs.get('user_id')
    query = kwargs.get('query')
    return f"search:{user_id}:{hashlib.md5(query.encode()).hexdigest()}"

@cache(prefix="custom", ttl=600, key_func=my_cache_key)
def search_with_user_context(db, user_id, query):
    # Implementation
    pass

Cache Invalidation Strategies

On resource update:

def update_video(db, video_id, **data):
    # Update in database
    db.execute(text("UPDATE videos SET ... WHERE id=:v"), {...})
    db.commit()

    # Invalidate caches
    invalidate_cache("video", video_id)
    invalidate_cache_pattern(f"segments:{video_id}*")

On job completion:

def complete_transcription(db, video_id):
    # Mark video as completed
    mark_completed(db, video_id)

    # Clear any pending caches
    invalidate_cache_pattern(f"video:{video_id}*")

API Response Optimization

Compression Middleware

Automatic gzip compression for responses > 1KB:

  • Compression level: 6 (balanced speed/ratio)
  • Minimum size: 1024 bytes
  • Automatic detection: Only compresses when client accepts gzip
  • Smart behavior: Only uses compressed version if smaller

Cache-Control Headers

Intelligent caching headers based on endpoint patterns:

Endpoint Pattern Cache-Control Reasoning
/static/* public, max-age=31536000, immutable Static assets never change
/videos/{id} public, max-age=300, stale-while-revalidate=60 Metadata rarely changes
/videos/{id}/transcript public, max-age=3600, stale-while-revalidate=300 Immutable once created
/search public, max-age=600, stale-while-revalidate=60 Balanced freshness
/auth/*, /favorites/* private, max-age=60 User-specific data
/health, /metrics no-store Never cache

Pagination Support

Cursor-based pagination schemas added for efficient large dataset navigation:

# Response schema
class PaginatedVideos(BaseModel):
    items: List[VideoInfo]
    page_info: PageInfo  # Contains next_cursor, has_next_page, etc.

Benefits:

  • No offset/limit performance degradation
  • Stable pagination even with inserts/deletes
  • Efficient for large datasets

Configuration

Environment Variables

Add to your .env file:

# Redis Configuration
REDIS_URL=redis://localhost:6379/0  # Or redis://redis:6379/0 in Docker
ENABLE_CACHING=true

# Cache TTL Configuration (seconds)
CACHE_DEFAULT_TTL=300       # 5 minutes
CACHE_VIDEO_TTL=300         # 5 minutes
CACHE_TRANSCRIPT_TTL=3600   # 1 hour
CACHE_SEARCH_TTL=600        # 10 minutes

# Metrics
ENABLE_METRICS=true

Docker Compose

Redis is automatically started with the stack:

services:
    redis:
        image: redis:7-alpine
        ports:
            - '6380:6379'
        volumes:
            - redis-data:/data

Start the full stack:

docker compose up -d

Development Without Redis

The caching layer gracefully degrades when Redis is unavailable:

  1. Set REDIS_URL= (empty) in .env
  2. Or simply don't start Redis
  3. Application continues to work, just without caching benefits

Monitoring and Metrics

Prometheus Metrics

New metrics available at /metrics:

Cache metrics:

  • cache_hits_total{cache_type} - Total cache hits by type
  • cache_misses_total{cache_type} - Total cache misses by type
  • cache_size_bytes - Current cache size in bytes
  • cache_keys_total - Total number of keys in cache

Existing metrics:

  • http_request_duration_seconds - Request latency histogram
  • db_query_duration_seconds - Database query duration
  • search_queries_total - Search query counter

Cache Statistics Endpoint

Get real-time cache statistics:

from app.cache import get_cache_stats

stats = get_cache_stats()
# Returns:
# {
#     "available": True,
#     "used_memory": "1.5M",
#     "total_keys": 42,
#     "connected_clients": 5,
#     "uptime_seconds": 3600,
#     "hit_rate": 0.85
# }

Monitoring Cache Hit Rate

Calculate cache effectiveness:

hit_rate = cache_hits_total / (cache_hits_total + cache_misses_total)

Target: > 80% hit rate for optimal performance

Grafana Dashboards

Import the provided Grafana dashboard for visualization:

  • Cache hit/miss rates over time
  • API latency percentiles (p50, p95, p99)
  • Database query duration
  • Resource-specific cache performance

Troubleshooting

High Cache Miss Rate

Symptoms: Cache hit rate < 50%

Possible causes:

  1. TTLs too short
  2. High traffic with cold cache
  3. Frequent cache invalidation

Solutions:

# Increase TTLs
CACHE_VIDEO_TTL=600
CACHE_TRANSCRIPT_TTL=7200

# Check invalidation patterns in logs
docker compose logs api | grep "Cache invalidated"

Redis Connection Issues

Symptoms: Log messages about Redis connection failures

Solutions:

# Check Redis is running
docker compose ps redis

# Check Redis logs
docker compose logs redis

# Restart Redis
docker compose restart redis

# Test connection manually
redis-cli -h localhost -p 6379 ping

Memory Usage

Monitor Redis memory:

redis-cli info memory

Set memory limit in docker-compose.yml:

redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

Slow Queries Despite Indices

Check if indices are being used:

EXPLAIN ANALYZE
SELECT * FROM jobs
WHERE state IN ('pending', 'downloading')
ORDER BY priority, created_at
LIMIT 10;

Look for "Index Scan" in the output.

Force index rebuild if needed:

REINDEX INDEX jobs_queue_ordering_idx;

Cache Stampede

Symptoms: Sudden spike in cache misses and database queries

Cause: Multiple requests hitting expired cache simultaneously

Solution: Use stale-while-revalidate headers (already implemented)

Performance Testing

Load Testing

Use locust or k6 for load testing:

# locustfile.py
from locust import HttpUser, task, between

class TranscriptUser(HttpUser):
    wait_time = between(1, 3)

    @task(3)
    def get_video(self):
        self.client.get("/videos/123e4567-e89b-12d3-a456-426614174000")

    @task(2)
    def get_transcript(self):
        self.client.get("/videos/123e4567-e89b-12d3-a456-426614174000/transcript")

    @task(1)
    def search(self):
        self.client.get("/search?q=example&limit=50")

Run test:

locust -f locustfile.py --host=http://localhost:8000

Baseline Performance

Before optimization:

  • p95 latency: ~800ms
  • Search queries: 1.5-2s
  • Cache hit rate: N/A (no caching)

After optimization (expected):

  • p95 latency: <500ms ✅
  • Search queries: <1s ✅
  • Cache hit rate: >80% ✅

Best Practices

  1. Set appropriate TTLs: Balance freshness with performance
  2. Invalidate strategically: Clear caches only when data actually changes
  3. Monitor cache hit rates: Aim for >80%
  4. Use compression: Enabled by default for all responses >1KB
  5. Leverage browser caching: Cache-Control headers set automatically
  6. Index new query patterns: Add indices for new hot paths
  7. Test with production-like data: Ensure indices are effective
  8. Monitor memory usage: Set Redis memory limits
  9. Use cursor pagination: For large result sets
  10. Profile slow queries: Use EXPLAIN ANALYZE regularly

Additional Resources