Files
gh-greyhaven-ai-claude-code…/skills/memory-profiling/reference/memory-optimization-patterns.md
2025-11-29 18:29:23 +08:00

7.6 KiB

Memory Optimization Patterns Reference

Quick reference catalog of common memory leak patterns and their fixes.

Event Listener Leaks

Pattern: EventEmitter Accumulation

Symptom: Memory grows linearly with time/requests Cause: Event listeners added but never removed

Vulnerable:

// ❌ LEAK: listener added every call
class DataProcessor {
  private emitter = new EventEmitter();

  async process() {
    this.emitter.on('data', handler);  // Never removed
  }
}

Fixed:

// ✅ FIX 1: Remove listener
this.emitter.on('data', handler);
try { /* work */ } finally {
  this.emitter.removeListener('data', handler);
}

// ✅ FIX 2: Use once()
this.emitter.once('data', handler);  // Auto-removed

// ✅ FIX 3: Use AbortController
const controller = new AbortController();
this.emitter.on('data', handler, { signal: controller.signal });
controller.abort();  // Removes listener

Detection:

// Check listener count
console.log(emitter.listenerCount('data'));  // Should be constant

// Monitor in production
process.on('warning', (warning) => {
  if (warning.name === 'MaxListenersExceededWarning') {
    console.error('Listener leak detected:', warning);
  }
});

Closure Memory Traps

Pattern: Captured Variables in Closures

Symptom: Memory not released after scope exits Cause: Closure captures large variables

Vulnerable:

// ❌ LEAK: Closure captures entire 1GB buffer
function createHandler(largeBuffer: Buffer) {
  return function handler() {
    // Only uses buffer.length, but captures entire buffer
    console.log(largeBuffer.length);
  };
}

Fixed:

// ✅ FIX: Extract only what's needed
function createHandler(largeBuffer: Buffer) {
  const length = largeBuffer.length;  // Extract value
  return function handler() {
    console.log(length);  // Only captures number, not Buffer
  };
}

Connection Pool Leaks

Pattern: Unclosed Database Connections

Symptom: Pool exhaustion, connection timeouts Cause: Connections acquired but not released

Vulnerable:

# ❌ LEAK: Connection never closed on exception
def get_orders():
    conn = pool.acquire()
    orders = conn.execute("SELECT * FROM orders")
    return orders  # conn never released

Fixed:

# ✅ FIX: Context manager guarantees cleanup
def get_orders():
    with pool.acquire() as conn:
        orders = conn.execute("SELECT * FROM orders")
        return orders  # conn auto-released

Large Dataset Patterns

Pattern 1: Loading Entire File into Memory

Vulnerable:

# ❌ LEAK: 10GB file → 20GB RAM
df = pd.read_csv("large.csv")

Fixed:

# ✅ FIX: Chunking
for chunk in pd.read_csv("large.csv", chunksize=10000):
    process(chunk)  # Constant memory

# ✅ BETTER: Polars streaming
df = pl.scan_csv("large.csv").collect(streaming=True)

Pattern 2: List Comprehension vs Generator

Vulnerable:

# ❌ LEAK: Entire list in memory
result = [process(item) for item in huge_list]

Fixed:

# ✅ FIX: Generator (lazy evaluation)
result = (process(item) for item in huge_list)
for item in result:
    use(item)  # Processes one at a time

Cache Management

Pattern: Unbounded Cache Growth

Vulnerable:

// ❌ LEAK: Cache grows forever
const cache = new Map<string, Data>();

function getData(key: string) {
  if (!cache.has(key)) {
    cache.set(key, fetchData(key));  // Never evicted
  }
  return cache.get(key);
}

Fixed:

// ✅ FIX 1: LRU cache with max size
import { LRUCache } from 'lru-cache';

const cache = new LRUCache<string, Data>({
  max: 1000,  // Max 1000 entries
  ttl: 1000 * 60 * 5  // 5 minute TTL
});

// ✅ FIX 2: WeakMap (auto-cleanup when key GC'd)
const cache = new WeakMap<object, Data>();
cache.set(key, data);  // Auto-removed when key is GC'd

Timer and Interval Leaks

Pattern: Forgotten Timers

Vulnerable:

// ❌ LEAK: Timer never cleared
class Component {
  startPolling() {
    setInterval(() => {
      this.fetchData();  // Keeps Component alive forever
    }, 1000);
  }
}

Fixed:

// ✅ FIX: Clear timer on cleanup
class Component {
  private intervalId?: NodeJS.Timeout;

  startPolling() {
    this.intervalId = setInterval(() => {
      this.fetchData();
    }, 1000);
  }

  cleanup() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
  }
}

Global Variable Accumulation

Pattern: Growing Global Arrays

Vulnerable:

// ❌ LEAK: Array grows forever
const logs: string[] = [];

function log(message: string) {
  logs.push(message);  // Never cleared
}

Fixed:

// ✅ FIX 1: Bounded array
const MAX_LOGS = 1000;
const logs: string[] = [];

function log(message: string) {
  logs.push(message);
  if (logs.length > MAX_LOGS) {
    logs.shift();  // Remove oldest
  }
}

// ✅ FIX 2: Circular buffer
import { CircularBuffer } from 'circular-buffer';
const logs = new CircularBuffer<string>(1000);

String Concatenation

Pattern: Repeated String Concatenation

Vulnerable:

# ❌ LEAK: Creates new string each iteration (O(n²))
result = ""
for item in items:
    result += str(item)  # New string allocation

Fixed:

# ✅ FIX 1: Join
result = "".join(str(item) for item in items)

# ✅ FIX 2: StringIO
from io import StringIO
buffer = StringIO()
for item in items:
    buffer.write(str(item))
result = buffer.getvalue()

React Component Leaks

Pattern: setState After Unmount

Vulnerable:

// ❌ LEAK: setState called after unmount
function Component() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);  // If unmounted, causes leak
  }, []);
}

Fixed:

// ✅ FIX: Cleanup with AbortController
function Component() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetchData(controller.signal).then(setData);

    return () => controller.abort();  // Cleanup
  }, []);
}

Detection Patterns

Memory Leak Indicators

  1. Linear growth: Memory usage increases linearly with time/requests
  2. Pool exhaustion: Connection pool hits max size
  3. EventEmitter warnings: "MaxListenersExceededWarning"
  4. GC pressure: Frequent/long GC pauses
  5. OOM errors: Process crashes with "JavaScript heap out of memory"

Monitoring Metrics

// Prometheus metrics for leak detection
const heap_used = new Gauge({
  name: 'nodejs_heap_used_bytes',
  help: 'V8 heap used bytes'
});

const event_listeners = new Gauge({
  name: 'event_listeners_total',
  help: 'Total event listeners',
  labelNames: ['event']
});

// Alert if heap grows >10% per hour
// Alert if listener count >100 for single event

Quick Fixes Checklist

  • Event listeners: Use once() or removeListener()
  • Database connections: Use context managers or try/finally
  • Large datasets: Use chunking or streaming
  • Caches: Implement LRU or WeakMap
  • Timers: Clear with clearInterval() or clearTimeout()
  • Closures: Extract values, avoid capturing large objects
  • React: Cleanup in useEffect() return
  • Strings: Use join() or StringIO, not +=


Return to reference index