#!/usr/bin/env python3 """ Scenario 7: Continuous Profiling with Pyroscope =============================================== A simple Flask web app instrumented with Pyroscope for continuous profiling. SETUP: 1. Start Pyroscope: docker run -p 4040:4040 grafana/pyroscope 2. Install deps: pip install flask pyroscope-io 3. Run this app: python3 app.py 4. Generate load: ./loadgen.sh (or curl in a loop) 5. View profiles: http://localhost:4040 The app has intentionally slow endpoints to demonstrate profiling. """ import os import time import math import hashlib from functools import lru_cache # Try to import pyroscope, gracefully handle if not installed try: import pyroscope PYROSCOPE_AVAILABLE = True except ImportError: PYROSCOPE_AVAILABLE = False print("Pyroscope not installed. Run: pip install pyroscope-io") print("Continuing without profiling...\n") from flask import Flask, jsonify app = Flask(__name__) # Configure Pyroscope if PYROSCOPE_AVAILABLE: pyroscope.configure( application_name="workshop.flask.app", server_address="http://localhost:4040", # Enable profiling for specific aspects tags={ "env": "workshop", "version": "1.0.0", } ) # ============================================================ # Endpoint 1: CPU-intensive computation # ============================================================ def compute_primes_slow(n): """Intentionally slow prime computation.""" primes = [] for num in range(2, n): is_prime = True for i in range(2, int(math.sqrt(num)) + 1): if num % i == 0: is_prime = False break if is_prime: primes.append(num) return primes @app.route('/api/primes/') def primes_endpoint(n): """CPU-bound endpoint - compute primes up to n.""" n = min(n, 50000) # Limit to prevent DoS start = time.time() primes = compute_primes_slow(n) elapsed = time.time() - start return jsonify({ 'count': len(primes), 'limit': n, 'elapsed_ms': round(elapsed * 1000, 2) }) # ============================================================ # Endpoint 2: Repeated expensive computation (needs caching) # ============================================================ def expensive_hash(data, iterations=1000): """Simulate expensive computation.""" result = data.encode() for _ in range(iterations): result = hashlib.sha256(result).digest() return result.hex() @app.route('/api/hash/') def hash_endpoint(data): """ This endpoint recomputes the hash every time. Profile will show expensive_hash taking lots of time. See hash_cached endpoint for improvement. """ start = time.time() result = expensive_hash(data) elapsed = time.time() - start return jsonify({ 'input': data, 'hash': result[:16] + '...', 'elapsed_ms': round(elapsed * 1000, 2) }) @lru_cache(maxsize=1000) def expensive_hash_cached(data, iterations=1000): """Cached version of expensive_hash.""" result = data.encode() for _ in range(iterations): result = hashlib.sha256(result).digest() return result.hex() @app.route('/api/hash_cached/') def hash_cached_endpoint(data): """Cached version - compare profile with /api/hash.""" start = time.time() result = expensive_hash_cached(data) elapsed = time.time() - start return jsonify({ 'input': data, 'hash': result[:16] + '...', 'elapsed_ms': round(elapsed * 1000, 2), 'cache_info': str(expensive_hash_cached.cache_info()) }) # ============================================================ # Endpoint 3: I/O simulation # ============================================================ @app.route('/api/slow_io') def slow_io_endpoint(): """ Simulate slow I/O (database query, external API, etc.) This won't show much in CPU profiles - it's I/O bound! """ time.sleep(0.1) # Simulate 100ms I/O return jsonify({'status': 'ok', 'simulated_io_ms': 100}) # ============================================================ # Endpoint 4: Mix of work types # ============================================================ @app.route('/api/mixed/') def mixed_endpoint(n): """Mixed workload: some CPU, some I/O.""" n = min(n, 1000) # CPU work total = 0 for i in range(n * 100): total += math.sin(i) * math.cos(i) # Simulated I/O time.sleep(0.01) # More CPU work data = str(total).encode() for _ in range(100): data = hashlib.md5(data).digest() return jsonify({ 'n': n, 'result': data.hex()[:16] }) # ============================================================ # Health check # ============================================================ @app.route('/health') def health(): return jsonify({'status': 'healthy', 'pyroscope': PYROSCOPE_AVAILABLE}) @app.route('/') def index(): return '''

Pyroscope Demo App

Endpoints:

Profiling:

View profiles at http://localhost:4040

''' if __name__ == '__main__': print("Starting Flask app on http://localhost:5000") print("Pyroscope dashboard: http://localhost:4040") print("\nGenerate load with: ./loadgen.sh") print("Or: for i in $(seq 100); do curl -s localhost:5000/api/primes/5000 > /dev/null; done") app.run(host='0.0.0.0', port=5000, debug=False)