Dev Logs
/System Design/ Chapter 18: Performance Optimization
Chapters
  • 01Chapter 1: Introduction to System Design
  • 02Chapter 2: Load Balancing and Traffic Distribution
  • 03Chapter 3: Caching Strategies and Implementation
  • 04Chapter 4: Database Design and Scaling
  • 05Chapter 5: API Design and Gateway Patterns
  • 06Chapter 6: Message Queues and Event Processing
  • 07Chapter 7: Microservices Architecture
  • 08Chapter 8: Distributed Systems Fundamentals
  • 09Chapter 9: Observability & Monitoring
  • 10Chapter 10: Security & Authentication
  • 11Chapter 11: Horizontal vs Vertical Scaling
  • 12Chapter 12: Content Delivery & Edge Computing
  • 13Chapter 13: Reliability Patterns
  • 14Chapter 14: Data Modeling & Storage
  • 15Chapter 15: Consistency and Consensus
  • 16Chapter 16: Deployment Strategies
  • 17Chapter 17: Disaster Recovery and Business Continuity
  • 18Chapter 18: Performance Optimization
    • Overview
    • Performance Fundamentals
    • Key Metrics
    • Performance Monitoring
    • Profiling and Bottleneck Identification
    • CPU Profiling
    • Database Query Optimization
    • Caching Optimization
    • Application-Level Optimization
    • Asynchronous Programming
    • System-Level Optimization
    • Resource Management
    • Performance Testing and Benchmarking
    • Best Practices
    • Performance Optimization Checklist
  • 19Chapter 19: Testing Strategies
  • 20Chapter 20: Real-World System Design
  • 21Chapter 21: System Design Interview Preparation
All chapters

Chapter 18: Performance Optimization

Overview

Performance optimization is crucial for building scalable systems that can handle increasing load while maintaining responsiveness. This chapter covers systematic approaches to identifying bottlenecks, optimizing code, and tuning system components.

Performance Fundamentals

Key Metrics

Latency Metrics:

  • Response Time: Time to complete a single request
  • Percentiles: P50, P95, P99 response times
  • Time to First Byte (TTFB)
  • Round Trip Time (RTT)

Throughput Metrics:

  • Requests Per Second (RPS)
  • Transactions Per Second (TPS)
  • Bandwidth utilization
  • Queue processing rate

Resource Metrics:

  • CPU utilization
  • Memory usage
  • Disk I/O
  • Network I/O
  • Connection pool usage

Performance Monitoring

python
import time
import psutil
import threading
from collections import defaultdict, deque
from typing import Dict, List, Optional
from dataclasses import dataclass
from datetime import datetime, timedelta

@dataclass
class PerformanceMetric:
    timestamp: datetime
    metric_name: str
    value: float
    tags: Dict[str, str] = None

class PerformanceMonitor:
    def __init__(self, window_size: int = 1000):
        self.metrics: Dict[str, deque] = defaultdict(lambda: deque(maxlen=window_size))
        self.counters: Dict[str, int] = defaultdict(int)
        self.gauges: Dict[str, float] = defaultdict(float)
        self.histograms: Dict[str, List[float]] = defaultdict(list)
        self._lock = threading.Lock()
    
    def record_timing(self, metric_name: str, duration: float, tags: Dict[str, str] = None):
        """Record timing metric"""
        with self._lock:
            metric = PerformanceMetric(
                timestamp=datetime.utcnow(),
                metric_name=metric_name,
                value=duration,
                tags=tags or {}
            )
            self.metrics[metric_name].append(metric)
            self.histograms[metric_name].append(duration)
    
    def increment_counter(self, metric_name: str, value: int = 1):
        """Increment counter metric"""
        with self._lock:
            self.counters[metric_name] += value
    
    def set_gauge(self, metric_name: str, value: float):
        """Set gauge metric"""
        with self._lock:
            self.gauges[metric_name] = value
    
    def get_percentile(self, metric_name: str, percentile: float) -> Optional[float]:
        """Calculate percentile for timing metrics"""
        if metric_name not in self.histograms or not self.histograms[metric_name]:
            return None
        
        values = sorted(self.histograms[metric_name])
        index = int(len(values) * percentile / 100)
        return values[min(index, len(values) - 1)]
    
    def get_average(self, metric_name: str) -> Optional[float]:
        """Calculate average for timing metrics"""
        if metric_name not in self.histograms or not self.histograms[metric_name]:
            return None
        
        return sum(self.histograms[metric_name]) / len(self.histograms[metric_name])
    
    def get_rate(self, metric_name: str, window_seconds: int = 60) -> float:
        """Calculate rate per second for counter metrics"""
        if metric_name not in self.metrics:
            return 0.0
        
        cutoff_time = datetime.utcnow() - timedelta(seconds=window_seconds)
        recent_metrics = [
            m for m in self.metrics[metric_name] 
            if m.timestamp >= cutoff_time
        ]
        
        return len(recent_metrics) / window_seconds if recent_metrics else 0.0
    
    def get_system_metrics(self) -> Dict[str, float]:
        """Get current system metrics"""
        return {
            'cpu_percent': psutil.cpu_percent(),
            'memory_percent': psutil.virtual_memory().percent,
            'disk_io_read_mb': psutil.disk_io_counters().read_bytes / 1024 / 1024,
            'disk_io_write_mb': psutil.disk_io_counters().write_bytes / 1024 / 1024,
            'network_sent_mb': psutil.net_io_counters().bytes_sent / 1024 / 1024,
            'network_recv_mb': psutil.net_io_counters().bytes_recv / 1024 / 1024,
        }
    
    def generate_report(self) -> Dict:
        """Generate performance report"""
        report = {
            'timestamp': datetime.utcnow().isoformat(),
            'system_metrics': self.get_system_metrics(),
            'timing_metrics': {},
            'counters': dict(self.counters),
            'gauges': dict(self.gauges)
        }
        
        for metric_name in self.histograms:
            if self.histograms[metric_name]:
                report['timing_metrics'][metric_name] = {
                    'count': len(self.histograms[metric_name]),
                    'average': self.get_average(metric_name),
                    'p50': self.get_percentile(metric_name, 50),
                    'p95': self.get_percentile(metric_name, 95),
                    'p99': self.get_percentile(metric_name, 99),
                    'rate_per_second': self.get_rate(metric_name)
                }
        
        return report

# Global monitor instance
monitor = PerformanceMonitor()

# Decorator for timing functions
def time_it(metric_name: str = None):
    def decorator(func):
        def wrapper(*args, **kwargs):
            name = metric_name or f"{func.__module__}.{func.__name__}"
            start_time = time.time()
            try:
                result = func(*args, **kwargs)
                monitor.increment_counter(f"{name}.success")
                return result
            except Exception as e:
                monitor.increment_counter(f"{name}.error")
                raise
            finally:
                duration = (time.time() - start_time) * 1000  # Convert to ms
                monitor.record_timing(name, duration)
        return wrapper
    return decorator

# Context manager for timing code blocks
class Timer:
    def __init__(self, metric_name: str):
        self.metric_name = metric_name
        self.start_time = None
    
    def __enter__(self):
        self.start_time = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        duration = (time.time() - self.start_time) * 1000
        monitor.record_timing(self.metric_name, duration)
        
        if exc_type is not None:
            monitor.increment_counter(f"{self.metric_name}.error")
        else:
            monitor.increment_counter(f"{self.metric_name}.success")

# Usage examples
@time_it("user_service.create_user")
def create_user(username, email, password):
    # User creation logic
    time.sleep(0.1)  # Simulate work
    return {"id": "user123", "username": username}

def process_batch():
    with Timer("batch_processing"):
        # Batch processing logic
        time.sleep(0.5)
        monitor.set_gauge("batch_size", 100)

Profiling and Bottleneck Identification

CPU Profiling

python
import cProfile
import pstats
import io
from functools import wraps
from typing import Callable, Any

class CPUProfiler:
    def __init__(self):
        self.profiler = cProfile.Profile()
        self.is_profiling = False
    
    def start(self):
        """Start CPU profiling"""
        if not self.is_profiling:
            self.profiler.enable()
            self.is_profiling = True
    
    def stop(self):
        """Stop CPU profiling"""
        if self.is_profiling:
            self.profiler.disable()
            self.is_profiling = False
    
    def get_stats(self, sort_by: str = 'cumulative', limit: int = 20) -> str:
        """Get profiling statistics"""
        s = io.StringIO()
        ps = pstats.Stats(self.profiler, stream=s)
        ps.sort_stats(sort_by)
        ps.print_stats(limit)
        return s.getvalue()
    
    def profile_function(self, func: Callable) -> Callable:
        """Decorator to profile a specific function"""
        @wraps(func)
        def wrapper(*args, **kwargs):
            self.start()
            try:
                result = func(*args, **kwargs)
                return result
            finally:
                self.stop()
        return wrapper

# Memory profiling
import tracemalloc
from typing import List, Tuple

class MemoryProfiler:
    def __init__(self):
        self.snapshots: List[tracemalloc.Snapshot] = []
        self.is_tracing = False
    
    def start(self):
        """Start memory tracing"""
        if not self.is_tracing:
            tracemalloc.start()
            self.is_tracing = True
    
    def take_snapshot(self) -> tracemalloc.Snapshot:
        """Take a memory snapshot"""
        if not self.is_tracing:
            raise RuntimeError("Memory tracing not started")
        
        snapshot = tracemalloc.take_snapshot()
        self.snapshots.append(snapshot)
        return snapshot
    
    def stop(self):
        """Stop memory tracing"""
        if self.is_tracing:
            tracemalloc.stop()
            self.is_tracing = False
    
    def get_top_allocations(self, snapshot: tracemalloc.Snapshot = None, limit: int = 10) -> List[str]:
        """Get top memory allocations"""
        if snapshot is None and self.snapshots:
            snapshot = self.snapshots[-1]
        elif snapshot is None:
            raise ValueError("No snapshots available")
        
        top_stats = snapshot.statistics('lineno')
        results = []
        
        for index, stat in enumerate(top_stats[:limit], 1):
            results.append(
                f"{index}. {stat.traceback.format()[-1]} - "
                f"{stat.size / 1024 / 1024:.2f} MB ({stat.count} blocks)"
            )
        
        return results
    
    def compare_snapshots(self, snapshot1: tracemalloc.Snapshot, 
                         snapshot2: tracemalloc.Snapshot, limit: int = 10) -> List[str]:
        """Compare two memory snapshots"""
        top_stats = snapshot2.compare_to(snapshot1, 'lineno')
        results = []
        
        for index, stat in enumerate(top_stats[:limit], 1):
            size_diff = stat.size_diff / 1024 / 1024
            results.append(
                f"{index}. {stat.traceback.format()[-1]} - "
                f"{size_diff:+.2f} MB ({stat.size / 1024 / 1024:.2f} MB total)"
            )
        
        return results

# Line-by-line profiling
import line_profiler

def profile_lines(func):
    """Decorator for line-by-line profiling"""
    profiler = line_profiler.LineProfiler()
    profiler.add_function(func)
    
    @wraps(func)
    def wrapper(*args, **kwargs):
        profiler.enable_by_count()
        try:
            result = func(*args, **kwargs)
            return result
        finally:
            profiler.disable_by_count()
            profiler.print_stats()
    
    return wrapper

# Usage example
cpu_profiler = CPUProfiler()
memory_profiler = MemoryProfiler()

@cpu_profiler.profile_function
@profile_lines
def expensive_function():
    # Simulate expensive computation
    data = []
    for i in range(100000):
        data.append(i ** 2)
    
    # Simulate memory allocation
    large_list = [0] * 1000000
    
    # Simulate CPU-intensive work
    result = sum(x for x in data if x % 2 == 0)
    
    return result

# Profile the function
memory_profiler.start()
snapshot1 = memory_profiler.take_snapshot()

result = expensive_function()

snapshot2 = memory_profiler.take_snapshot()
memory_profiler.stop()

# Analyze results
print("CPU Profiling Results:")
print(cpu_profiler.get_stats())

print("\nMemory Profiling Results:")
print("\n".join(memory_profiler.compare_snapshots(snapshot1, snapshot2)))

Database Query Optimization

python
import time
import logging
from typing import Dict, List, Any
from contextlib import contextmanager

class QueryOptimizer:
    def __init__(self, db_connection):
        self.db = db_connection
        self.slow_query_threshold = 1.0  # 1 second
        self.query_cache = {}
        self.query_stats = {}
    
    @contextmanager
    def query_timer(self, query: str):
        """Context manager to time database queries"""
        start_time = time.time()
        try:
            yield
        finally:
            duration = time.time() - start_time
            self._record_query_stats(query, duration)
            
            if duration > self.slow_query_threshold:
                logging.warning(f"Slow query detected ({duration:.2f}s): {query[:100]}...")
    
    def _record_query_stats(self, query: str, duration: float):
        """Record query execution statistics"""
        query_key = self._normalize_query(query)
        
        if query_key not in self.query_stats:
            self.query_stats[query_key] = {
                'count': 0,
                'total_time': 0,
                'min_time': float('inf'),
                'max_time': 0,
                'query': query
            }
        
        stats = self.query_stats[query_key]
        stats['count'] += 1
        stats['total_time'] += duration
        stats['min_time'] = min(stats['min_time'], duration)
        stats['max_time'] = max(stats['max_time'], duration)
    
    def _normalize_query(self, query: str) -> str:
        """Normalize query for statistics grouping"""
        # Remove parameter values and normalize whitespace
        import re
        normalized = re.sub(r'\$\d+|\?|\d+', '?', query)
        normalized = ' '.join(normalized.split())
        return normalized.lower()
    
    def execute_query(self, query: str, params: tuple = None) -> List[Dict]:
        """Execute query with timing and optimization"""
        # Check cache first
        cache_key = (query, params)
        if cache_key in self.query_cache:
            return self.query_cache[cache_key]
        
        with self.query_timer(query):
            cursor = self.db.cursor()
            cursor.execute(query, params or ())
            
            # Fetch results
            columns = [desc[0] for desc in cursor.description]
            results = [
                dict(zip(columns, row))
                for row in cursor.fetchall()
            ]
            
            # Cache results for read-only queries
            if query.strip().lower().startswith('select'):
                self.query_cache[cache_key] = results
            
            return results
    
    def analyze_query_plan(self, query: str) -> Dict:
        """Analyze query execution plan"""
        explain_query = f"EXPLAIN ANALYZE {query}"
        
        with self.query_timer(explain_query):
            cursor = self.db.cursor()
            cursor.execute(explain_query)
            plan = cursor.fetchall()
        
        return {
            'query': query,
            'execution_plan': plan,
            'recommendations': self._generate_recommendations(plan)
        }
    
    def _generate_recommendations(self, plan: List) -> List[str]:
        """Generate optimization recommendations based on execution plan"""
        recommendations = []
        plan_text = ' '.join(str(row) for row in plan).lower()
        
        if 'seq scan' in plan_text:
            recommendations.append("Consider adding an index to avoid sequential scans")
        
        if 'nested loop' in plan_text and 'large' in plan_text:
            recommendations.append("Consider using hash join instead of nested loop for large datasets")
        
        if 'sort' in plan_text and 'disk' in plan_text:
            recommendations.append("Increase work_mem to avoid disk-based sorting")
        
        if 'bitmap heap scan' in plan_text:
            recommendations.append("Consider creating a more selective index")
        
        return recommendations
    
    def get_slow_queries(self, limit: int = 10) -> List[Dict]:
        """Get slowest queries by average execution time"""
        sorted_queries = sorted(
            self.query_stats.values(),
            key=lambda x: x['total_time'] / x['count'],
            reverse=True
        )
        
        return [
            {
                'query': stats['query'][:100] + '...' if len(stats['query']) > 100 else stats['query'],
                'count': stats['count'],
                'avg_time': stats['total_time'] / stats['count'],
                'total_time': stats['total_time'],
                'min_time': stats['min_time'],
                'max_time': stats['max_time']
            }
            for stats in sorted_queries[:limit]
        ]
    
    def suggest_indexes(self) -> List[str]:
        """Suggest indexes based on query patterns"""
        suggestions = []
        
        for stats in self.query_stats.values():
            query = stats['query'].lower()
            
            # Look for WHERE clauses
            if 'where' in query:
                # Extract potential index candidates
                import re
                where_match = re.search(r'where\s+(.+?)(?:order by|group by|limit|$)', query)
                if where_match:
                    where_clause = where_match.group(1)
                    
                    # Look for column references
                    column_matches = re.findall(r'(\w+)\s*[=<>]', where_clause)
                    for column in column_matches:
                        if column not in ['and', 'or', 'not']:
                            suggestions.append(f"CREATE INDEX idx_{column} ON table_name ({column});")
        
        return list(set(suggestions))  # Remove duplicates

# Connection pooling for better performance
import threading
from queue import Queue, Empty

class ConnectionPool:
    def __init__(self, connection_factory, min_size: int = 5, max_size: int = 20):
        self.connection_factory = connection_factory
        self.min_size = min_size
        self.max_size = max_size
        self.pool = Queue(maxsize=max_size)
        self.active_connections = 0
        self.lock = threading.Lock()
        
        # Initialize minimum connections
        for _ in range(min_size):
            self.pool.put(self._create_connection())
            self.active_connections += 1
    
    def _create_connection(self):
        """Create a new database connection"""
        return self.connection_factory()
    
    @contextmanager
    def get_connection(self, timeout: float = 5.0):
        """Get a connection from the pool"""
        connection = None
        try:
            # Try to get existing connection
            try:
                connection = self.pool.get(timeout=timeout)
            except Empty:
                # Create new connection if under max limit
                with self.lock:
                    if self.active_connections < self.max_size:
                        connection = self._create_connection()
                        self.active_connections += 1
                    else:
                        raise Exception("Connection pool exhausted")
            
            yield connection
            
        finally:
            if connection:
                # Return connection to pool
                try:
                    self.pool.put_nowait(connection)
                except:
                    # Pool is full, close connection
                    connection.close()
                    with self.lock:
                        self.active_connections -= 1
    
    def close_all(self):
        """Close all connections in the pool"""
        while not self.pool.empty():
            try:
                connection = self.pool.get_nowait()
                connection.close()
            except Empty:
                break
        
        self.active_connections = 0

# Usage example
def create_db_connection():
    import sqlite3
    return sqlite3.connect(':memory:')

pool = ConnectionPool(create_db_connection, min_size=3, max_size=10)

with pool.get_connection() as conn:
    optimizer = QueryOptimizer(conn)
    
    # Execute queries with optimization
    results = optimizer.execute_query(
        "SELECT * FROM users WHERE age > ? AND city = ?",
        (25, 'New York')
    )
    
    # Analyze slow queries
    slow_queries = optimizer.get_slow_queries()
    for query_info in slow_queries:
        print(f"Slow query: {query_info['query']} (avg: {query_info['avg_time']:.3f}s)")

Caching Optimization

python
import time
import hashlib
import pickle
from typing import Any, Optional, Callable, Dict
from functools import wraps
from abc import ABC, abstractmethod

class CacheBackend(ABC):
    @abstractmethod
    def get(self, key: str) -> Optional[Any]:
        pass
    
    @abstractmethod
    def set(self, key: str, value: Any, ttl: int = None) -> None:
        pass
    
    @abstractmethod
    def delete(self, key: str) -> None:
        pass
    
    @abstractmethod
    def clear(self) -> None:
        pass

class MemoryCache(CacheBackend):
    def __init__(self, max_size: int = 1000):
        self.cache: Dict[str, Dict] = {}
        self.max_size = max_size
        self.access_times: Dict[str, float] = {}
    
    def get(self, key: str) -> Optional[Any]:
        if key in self.cache:
            entry = self.cache[key]
            
            # Check TTL
            if entry['expires_at'] and time.time() > entry['expires_at']:
                self.delete(key)
                return None
            
            # Update access time for LRU
            self.access_times[key] = time.time()
            return entry['value']
        
        return None
    
    def set(self, key: str, value: Any, ttl: int = None) -> None:
        # Evict if at capacity
        if len(self.cache) >= self.max_size and key not in self.cache:
            self._evict_lru()
        
        expires_at = time.time() + ttl if ttl else None
        
        self.cache[key] = {
            'value': value,
            'expires_at': expires_at,
            'created_at': time.time()
        }
        self.access_times[key] = time.time()
    
    def delete(self, key: str) -> None:
        self.cache.pop(key, None)
        self.access_times.pop(key, None)
    
    def clear(self) -> None:
        self.cache.clear()
        self.access_times.clear()
    
    def _evict_lru(self) -> None:
        """Evict least recently used item"""
        if self.access_times:
            lru_key = min(self.access_times.keys(), key=lambda k: self.access_times[k])
            self.delete(lru_key)

class RedisCache(CacheBackend):
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def get(self, key: str) -> Optional[Any]:
        data = self.redis.get(key)
        if data:
            return pickle.loads(data)
        return None
    
    def set(self, key: str, value: Any, ttl: int = None) -> None:
        data = pickle.dumps(value)
        if ttl:
            self.redis.setex(key, ttl, data)
        else:
            self.redis.set(key, data)
    
    def delete(self, key: str) -> None:
        self.redis.delete(key)
    
    def clear(self) -> None:
        self.redis.flushdb()

class MultiLevelCache:
    def __init__(self, l1_cache: CacheBackend, l2_cache: CacheBackend):
        self.l1 = l1_cache  # Fast cache (memory)
        self.l2 = l2_cache  # Slower cache (Redis)
        self.stats = {
            'l1_hits': 0,
            'l2_hits': 0,
            'misses': 0
        }
    
    def get(self, key: str) -> Optional[Any]:
        # Try L1 cache first
        value = self.l1.get(key)
        if value is not None:
            self.stats['l1_hits'] += 1
            return value
        
        # Try L2 cache
        value = self.l2.get(key)
        if value is not None:
            self.stats['l2_hits'] += 1
            # Promote to L1
            self.l1.set(key, value)
            return value
        
        self.stats['misses'] += 1
        return None
    
    def set(self, key: str, value: Any, ttl: int = None) -> None:
        # Set in both caches
        self.l1.set(key, value, ttl)
        self.l2.set(key, value, ttl)
    
    def delete(self, key: str) -> None:
        self.l1.delete(key)
        self.l2.delete(key)
    
    def get_hit_ratio(self) -> Dict[str, float]:
        total_requests = sum(self.stats.values())
        if total_requests == 0:
            return {'l1': 0.0, 'l2': 0.0, 'overall': 0.0}
        
        return {
            'l1': self.stats['l1_hits'] / total_requests,
            'l2': self.stats['l2_hits'] / total_requests,
            'overall': (self.stats['l1_hits'] + self.stats['l2_hits']) / total_requests
        }

class SmartCache:
    def __init__(self, backend: CacheBackend):
        self.backend = backend
        self.hit_stats = {}
        self.computation_times = {}
    
    def cached(self, ttl: int = 3600, key_func: Callable = None):
        """Decorator for caching function results"""
        def decorator(func: Callable) -> Callable:
            @wraps(func)
            def wrapper(*args, **kwargs):
                # Generate cache key
                if key_func:
                    cache_key = key_func(*args, **kwargs)
                else:
                    cache_key = self._generate_key(func.__name__, args, kwargs)
                
                # Try to get from cache
                cached_result = self.backend.get(cache_key)
                if cached_result is not None:
                    self._record_hit(cache_key)
                    return cached_result
                
                # Compute result
                start_time = time.time()
                result = func(*args, **kwargs)
                computation_time = time.time() - start_time
                
                # Cache result with adaptive TTL
                adaptive_ttl = self._calculate_adaptive_ttl(cache_key, computation_time, ttl)
                self.backend.set(cache_key, result, adaptive_ttl)
                
                self._record_miss(cache_key, computation_time)
                return result
            
            return wrapper
        return decorator
    
    def _generate_key(self, func_name: str, args: tuple, kwargs: dict) -> str:
        """Generate cache key from function name and arguments"""
        key_data = f"{func_name}:{args}:{sorted(kwargs.items())}"
        return hashlib.md5(key_data.encode()).hexdigest()
    
    def _calculate_adaptive_ttl(self, cache_key: str, computation_time: float, base_ttl: int) -> int:
        """Calculate adaptive TTL based on computation cost and hit rate"""
        # Store computation time
        self.computation_times[cache_key] = computation_time
        
        # Get hit rate for this key
        hit_rate = self._get_hit_rate(cache_key)
        
        # Adjust TTL based on computation cost and hit rate
        if computation_time > 1.0:  # Expensive computation
            ttl_multiplier = 2.0
        elif computation_time > 0.1:
            ttl_multiplier = 1.5
        else:
            ttl_multiplier = 1.0
        
        # Increase TTL for frequently accessed items
        if hit_rate > 0.8:
            ttl_multiplier *= 1.5
        elif hit_rate > 0.5:
            ttl_multiplier *= 1.2
        
        return int(base_ttl * ttl_multiplier)
    
    def _record_hit(self, cache_key: str):
        if cache_key not in self.hit_stats:
            self.hit_stats[cache_key] = {'hits': 0, 'misses': 0}
        self.hit_stats[cache_key]['hits'] += 1
    
    def _record_miss(self, cache_key: str, computation_time: float):
        if cache_key not in self.hit_stats:
            self.hit_stats[cache_key] = {'hits': 0, 'misses': 0}
        self.hit_stats[cache_key]['misses'] += 1
    
    def _get_hit_rate(self, cache_key: str) -> float:
        if cache_key not in self.hit_stats:
            return 0.0
        
        stats = self.hit_stats[cache_key]
        total = stats['hits'] + stats['misses']
        return stats['hits'] / total if total > 0 else 0.0
    
    def get_cache_stats(self) -> Dict:
        """Get comprehensive cache statistics"""
        total_hits = sum(stats['hits'] for stats in self.hit_stats.values())
        total_misses = sum(stats['misses'] for stats in self.hit_stats.values())
        total_requests = total_hits + total_misses
        
        return {
            'total_requests': total_requests,
            'total_hits': total_hits,
            'total_misses': total_misses,
            'hit_rate': total_hits / total_requests if total_requests > 0 else 0.0,
            'average_computation_time': sum(self.computation_times.values()) / len(self.computation_times) if self.computation_times else 0.0,
            'top_cached_functions': sorted(
                [
                    {
                        'key': key,
                        'hits': stats['hits'],
                        'misses': stats['misses'],
                        'hit_rate': stats['hits'] / (stats['hits'] + stats['misses']),
                        'computation_time': self.computation_times.get(key, 0)
                    }
                    for key, stats in self.hit_stats.items()
                ],
                key=lambda x: x['hits'],
                reverse=True
            )[:10]
        }

# Usage example
memory_cache = MemoryCache(max_size=500)
redis_cache = RedisCache(redis_client)  # Assume redis_client is configured
multi_cache = MultiLevelCache(memory_cache, redis_cache)
smart_cache = SmartCache(multi_cache)

@smart_cache.cached(ttl=1800)  # 30 minutes
def expensive_computation(n: int) -> int:
    """Simulate expensive computation"""
    time.sleep(0.1)  # Simulate work
    return sum(i ** 2 for i in range(n))

@smart_cache.cached(ttl=300, key_func=lambda user_id: f"user_profile:{user_id}")  # 5 minutes
def get_user_profile(user_id: str) -> Dict:
    """Simulate database query"""
    time.sleep(0.05)  # Simulate DB query
    return {
        'id': user_id,
        'name': f'User {user_id}',
        'email': f'user{user_id}@example.com'
    }

# Test caching performance
for i in range(100):
    result = expensive_computation(1000)
    profile = get_user_profile(f"user_{i % 10}")  # Only 10 unique users

# Print cache statistics
stats = smart_cache.get_cache_stats()
print(f"Cache hit rate: {stats['hit_rate']:.2%}")
print(f"Average computation time: {stats['average_computation_time']:.3f}s")

Application-Level Optimization

Asynchronous Programming

python
import asyncio
import aiohttp
import time
from typing import List, Dict, Any
from concurrent.futures import ThreadPoolExecutor, as_completed

class AsyncOptimizer:
    def __init__(self, max_concurrent: int = 10):
        self.max_concurrent = max_concurrent
        self.semaphore = asyncio.Semaphore(max_concurrent)
        self.session = None
    
    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self.session:
            await self.session.close()
    
    async def fetch_url(self, url: str) -> Dict[str, Any]:
        """Fetch URL with concurrency control"""
        async with self.semaphore:
            start_time = time.time()
            try:
                async with self.session.get(url) as response:
                    content = await response.text()
                    return {
                        'url': url,
                        'status': response.status,
                        'content_length': len(content),
                        'response_time': time.time() - start_time
                    }
            except Exception as e:
                return {
                    'url': url,
                    'error': str(e),
                    'response_time': time.time() - start_time
                }
    
    async def fetch_multiple_urls(self, urls: List[str]) -> List[Dict[str, Any]]:
        """Fetch multiple URLs concurrently"""
        tasks = [self.fetch_url(url) for url in urls]
        return await asyncio.gather(*tasks, return_exceptions=True)
    
    async def process_batch(self, items: List[Any], processor_func, batch_size: int = 100) -> List[Any]:
        """Process items in batches asynchronously"""
        results = []
        
        for i in range(0, len(items), batch_size):
            batch = items[i:i + batch_size]
            batch_tasks = [processor_func(item) for item in batch]
            batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
            results.extend(batch_results)
        
        return results

# CPU-bound task optimization
class CPUOptimizer:
    def __init__(self, max_workers: int = None):
        self.max_workers = max_workers or min(32, (os.cpu_count() or 1) + 4)
        self.executor = ThreadPoolExecutor(max_workers=self.max_workers)
    
    def process_parallel(self, func, items: List[Any], chunk_size: int = None) -> List[Any]:
        """Process items in parallel using thread pool"""
        if chunk_size is None:
            chunk_size = max(1, len(items) // self.max_workers)
        
        # Split items into chunks
        chunks = [items[i:i + chunk_size] for i in range(0, len(items), chunk_size)]
        
        # Submit tasks
        futures = [self.executor.submit(self._process_chunk, func, chunk) for chunk in chunks]
        
        # Collect results
        results = []
        for future in as_completed(futures):
            try:
                chunk_results = future.result()
                results.extend(chunk_results)
            except Exception as e:
                print(f"Chunk processing failed: {e}")
        
        return results
    
    def _process_chunk(self, func, chunk: List[Any]) -> List[Any]:
        """Process a chunk of items"""
        return [func(item) for item in chunk]
    
    def close(self):
        """Close the thread pool"""
        self.executor.shutdown(wait=True)

# Example usage
async def optimize_data_processing():
    # Async I/O optimization
    urls = [
        'https://api.example.com/users',
        'https://api.example.com/posts',
        'https://api.example.com/comments'
    ]
    
    async with AsyncOptimizer(max_concurrent=5) as optimizer:
        results = await optimizer.fetch_multiple_urls(urls)
        
        for result in results:
            if 'error' not in result:
                print(f"Fetched {result['url']} in {result['response_time']:.3f}s")
    
    # CPU-bound optimization
    def expensive_calculation(n: int) -> int:
        return sum(i ** 2 for i in range(n))
    
    cpu_optimizer = CPUOptimizer()
    numbers = list(range(1000, 2000))
    
    start_time = time.time()
    results = cpu_optimizer.process_parallel(expensive_calculation, numbers)
    end_time = time.time()
    
    print(f"Processed {len(numbers)} items in {end_time - start_time:.3f}s")
    cpu_optimizer.close()

# asyncio.run(optimize_data_processing())

System-Level Optimization

Resource Management

python
import os
import psutil
import resource
from typing import Dict, List
from dataclasses import dataclass

@dataclass
class ResourceLimits:
    max_memory_mb: int = 1024
    max_cpu_percent: float = 80.0
    max_open_files: int = 1024
    max_threads: int = 100

class ResourceManager:
    def __init__(self, limits: ResourceLimits):
        self.limits = limits
        self.initial_limits = self._get_current_limits()
    
    def _get_current_limits(self) -> Dict:
        """Get current system resource limits"""
        return {
            'memory': resource.getrlimit(resource.RLIMIT_AS),
            'files': resource.getrlimit(resource.RLIMIT_NOFILE),
            'processes': resource.getrlimit(resource.RLIMIT_NPROC)
        }
    
    def apply_limits(self):
        """Apply resource limits to current process"""
        try:
            # Set memory limit
            memory_limit = self.limits.max_memory_mb * 1024 * 1024
            resource.setrlimit(resource.RLIMIT_AS, (memory_limit, memory_limit))
            
            # Set file descriptor limit
            resource.setrlimit(resource.RLIMIT_NOFILE, 
                              (self.limits.max_open_files, self.limits.max_open_files))
            
            print(f"Applied resource limits: {self.limits}")
        except Exception as e:
            print(f"Failed to apply resource limits: {e}")
    
    def monitor_resources(self) -> Dict:
        """Monitor current resource usage"""
        process = psutil.Process()
        
        return {
            'cpu_percent': process.cpu_percent(),
            'memory_mb': process.memory_info().rss / 1024 / 1024,
            'memory_percent': process.memory_percent(),
            'open_files': len(process.open_files()),
            'threads': process.num_threads(),
            'connections': len(process.connections())
        }
    
    def check_limits(self) -> Dict[str, bool]:
        """Check if current usage exceeds limits"""
        usage = self.monitor_resources()
        
        return {
            'memory_ok': usage['memory_mb'] <= self.limits.max_memory_mb,
            'cpu_ok': usage['cpu_percent'] <= self.limits.max_cpu_percent,
            'files_ok': usage['open_files'] <= self.limits.max_open_files,
            'threads_ok': usage['threads'] <= self.limits.max_threads
        }
    
    def optimize_gc(self):
        """Optimize garbage collection settings"""
        import gc
        
        # Tune garbage collection thresholds
        gc.set_threshold(700, 10, 10)  # More aggressive collection
        
        # Force garbage collection
        collected = gc.collect()
        print(f"Garbage collection freed {collected} objects")
        
        return collected

# Usage example
limits = ResourceLimits(
    max_memory_mb=2048,
    max_cpu_percent=75.0,
    max_open_files=2048,
    max_threads=50
)

resource_manager = ResourceManager(limits)
resource_manager.apply_limits()

# Monitor resources periodically
usage = resource_manager.monitor_resources()
print(f"Current usage: {usage}")

limit_check = resource_manager.check_limits()
if not all(limit_check.values()):
    print("Resource limits exceeded!")
    resource_manager.optimize_gc()

Performance Testing and Benchmarking

python
import time
import statistics
from typing import Callable, List, Dict, Any
from functools import wraps

class PerformanceBenchmark:
    def __init__(self):
        self.results: Dict[str, List[float]] = {}
    
    def benchmark(self, name: str, iterations: int = 100):
        """Decorator to benchmark function performance"""
        def decorator(func: Callable) -> Callable:
            @wraps(func)
            def wrapper(*args, **kwargs):
                times = []
                
                for _ in range(iterations):
                    start_time = time.perf_counter()
                    result = func(*args, **kwargs)
                    end_time = time.perf_counter()
                    times.append((end_time - start_time) * 1000)  # Convert to ms
                
                self.results[name] = times
                return result
            
            return wrapper
        return decorator
    
    def compare_functions(self, functions: Dict[str, Callable], 
                         test_args: tuple = (), test_kwargs: Dict = None, 
                         iterations: int = 100) -> Dict[str, Dict]:
        """Compare performance of multiple functions"""
        test_kwargs = test_kwargs or {}
        comparison_results = {}
        
        for name, func in functions.items():
            times = []
            
            for _ in range(iterations):
                start_time = time.perf_counter()
                func(*test_args, **test_kwargs)
                end_time = time.perf_counter()
                times.append((end_time - start_time) * 1000)
            
            comparison_results[name] = {
                'mean': statistics.mean(times),
                'median': statistics.median(times),
                'stdev': statistics.stdev(times) if len(times) > 1 else 0,
                'min': min(times),
                'max': max(times),
                'p95': self._percentile(times, 95),
                'p99': self._percentile(times, 99)
            }
        
        return comparison_results
    
    def _percentile(self, data: List[float], percentile: float) -> float:
        """Calculate percentile"""
        sorted_data = sorted(data)
        index = int(len(sorted_data) * percentile / 100)
        return sorted_data[min(index, len(sorted_data) - 1)]
    
    def generate_report(self) -> str:
        """Generate performance report"""
        report = "Performance Benchmark Report\n"
        report += "=" * 40 + "\n\n"
        
        for name, times in self.results.items():
            if times:
                mean_time = statistics.mean(times)
                median_time = statistics.median(times)
                stdev_time = statistics.stdev(times) if len(times) > 1 else 0
                
                report += f"Function: {name}\n"
                report += f"  Iterations: {len(times)}\n"
                report += f"  Mean: {mean_time:.3f}ms\n"
                report += f"  Median: {median_time:.3f}ms\n"
                report += f"  Std Dev: {stdev_time:.3f}ms\n"
                report += f"  Min: {min(times):.3f}ms\n"
                report += f"  Max: {max(times):.3f}ms\n"
                report += f"  P95: {self._percentile(times, 95):.3f}ms\n"
                report += f"  P99: {self._percentile(times, 99):.3f}ms\n\n"
        
        return report

# Example usage
benchmark = PerformanceBenchmark()

# Different sorting algorithms for comparison
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)

def python_sort(arr):
    return sorted(arr)

# Compare sorting algorithms
test_data = list(range(100, 0, -1))  # Reverse sorted list

sort_functions = {
    'bubble_sort': lambda: bubble_sort(test_data.copy()),
    'quick_sort': lambda: quick_sort(test_data.copy()),
    'python_sort': lambda: python_sort(test_data.copy())
}

comparison = benchmark.compare_functions(sort_functions, iterations=50)

print("Sorting Algorithm Comparison:")
for name, stats in comparison.items():
    print(f"{name}: {stats['mean']:.3f}ms (±{stats['stdev']:.3f}ms)")

# Find the fastest algorithm
fastest = min(comparison.items(), key=lambda x: x[1]['mean'])
print(f"\nFastest algorithm: {fastest[0]} ({fastest[1]['mean']:.3f}ms)")

Best Practices

Performance Optimization Checklist

1. Measurement First:

  • Profile before optimizing
  • Establish baseline metrics
  • Use real-world data and scenarios
  • Monitor continuously

2. Database Optimization:

  • Add appropriate indexes
  • Optimize query patterns
  • Use connection pooling
  • Implement query caching
  • Consider read replicas

3. Application Optimization:

  • Use async programming for I/O
  • Implement efficient caching
  • Optimize data structures
  • Minimize object creation
  • Use lazy loading

4. System Optimization:

  • Tune garbage collection
  • Optimize resource limits
  • Use appropriate concurrency models
  • Monitor system resources

5. Network Optimization:

  • Minimize round trips
  • Use compression
  • Implement CDN
  • Optimize payload sizes
  • Use HTTP/2 or HTTP/3

Next Topic: Deployment Strategies Previous Topic: Testing Strategies Main Index: DEV LOGS - System Design


PreviousChapter 17: Disaster Recovery and Business ContinuityNextChapter 19: Testing Strategies

Open source, free forever. Built by iammhador.

Contribute on GitHub