# Performance Conventions

## Overview

The SDK is designed with performance as a top priority. This document defines performance guidelines and optimization patterns.

> **Note**: Use `@performance-review` agent to identify performance issues, and `@dependency-review` agent when adding new dependencies.

## Performance Targets

### Latency Goals

- **Client initialization**: < 10ms
- **Simple API call**: < 100ms overhead
- **Batch operations**: Linear scaling with size
- **Memory overhead**: < 50MB base

### Throughput Goals

- **HTTP/2 connection pooling**: 100+ concurrent requests
- **Async operations**: 1000+ requests/second
- **Vector serialization**: 1M vectors/second

## Fast Libraries

### Core Dependencies

**Use performance-optimized libraries**:

```python
# ✅ Good: Fast libraries
httpx       # HTTP/2, connection pooling
msgspec     # 10-30x faster than Pydantic
orjson      # 2-3x faster than json
uvloop      # 2x faster event loop (optional)

# ❌ Bad: Slow alternatives
requests    # No HTTP/2, no async
pydantic    # Slower validation
json        # Slower serialization
```

### msgspec over Pydantic

**Use msgspec for data models**:

```python
from msgspec import Struct

# ✅ Good: msgspec (fast)
class Vector(Struct):
    id: str
    values: list[float]
    metadata: dict[str, Any] | None = None

# Benchmark: ~100ns per decode

# ❌ Bad: Pydantic (slow)
from pydantic import BaseModel

class Vector(BaseModel):
    id: str
    values: list[float]
    metadata: dict[str, Any] | None = None

# Benchmark: ~3000ns per decode (30x slower)
```

### orjson over json

**Use orjson for JSON operations**:

```python
import orjson

# ✅ Good: orjson (fast)
data = orjson.loads(response.content)
body = orjson.dumps({"vectors": vectors})

# Benchmark: ~500ns for 1KB

# ❌ Bad: json (slow)
import json

data = json.loads(response.text)
body = json.dumps({"vectors": vectors})

# Benchmark: ~1500ns for 1KB (3x slower)
```

## HTTP Performance

### HTTP/2 with httpx

**Use HTTP/2 for multiplexing**:

```python
# ✅ Good: HTTP/2 enabled
client = httpx.Client(http2=True)

# Multiple requests on same connection
client.get("/indexes")
client.get("/index/my-index")
client.post("/indexes", json=data)

# ❌ Bad: HTTP/1.1 only
client = httpx.Client()  # http2=False by default in old versions
```

### Connection Pooling

**Reuse connections**:

```python
# ✅ Good: Connection pool configuration
client = httpx.Client(
    http2=True,
    limits=httpx.Limits(
        max_connections=100,
        max_keepalive_connections=20
    )
)

# ❌ Bad: No pooling
def make_request(url):
    with httpx.Client() as client:  # New client per request!
        return client.get(url)
```

### Request Batching

**Batch requests when possible**:

```python
# ✅ Good: Single batch request
vectors = [
    {"id": f"vec{i}", "values": [0.1] * 1536}
    for i in range(1000)
]
client.upsert(vectors=vectors)  # Single HTTP request

# ❌ Bad: Individual requests
for i in range(1000):
    vector = {"id": f"vec{i}", "values": [0.1] * 1536}
    client.upsert(vectors=[vector])  # 1000 HTTP requests
```

## Async Performance

### Concurrent Requests

**Use asyncio.gather for concurrency**:

```python
# ✅ Good: Concurrent requests
async def fetch_all_indexes(names: list[str]) -> list[Index]:
    tasks = [client.get_index(name) for name in names]
    return await asyncio.gather(*tasks)

# 10 requests in parallel
indexes = await fetch_all_indexes([f"index{i}" for i in range(10)])

# ❌ Bad: Sequential requests
async def fetch_all_indexes(names: list[str]) -> list[Index]:
    indexes = []
    for name in names:
        index = await client.get_index(name)  # One at a time
        indexes.append(index)
    return indexes
```

### uvloop for Production

**Use uvloop for faster event loop**:

```python
# ✅ Good: uvloop in production
try:
    import uvloop
    uvloop.install()
except ImportError:
    pass  # Fall back to default event loop

asyncio.run(main())

# Benchmark: 2x throughput improvement

# ❌ Bad: Not using uvloop
asyncio.run(main())
```

## Memory Optimization

### Avoid Copying

**Use views and references**:

```python
# ✅ Good: Reference, no copy
def process_vectors(vectors: list[Vector]) -> None:
    for vector in vectors:  # Iterate without copying
        process(vector)

# ❌ Bad: Unnecessary copies
def process_vectors(vectors: list[Vector]) -> None:
    vectors_copy = vectors.copy()  # Unnecessary
    for vector in vectors_copy:
        process(vector)
```

### Generator Expressions

**Use generators for large datasets**:

```python
# ✅ Good: Generator (low memory)
def fetch_all_vectors(index: Index) -> Iterator[Vector]:
    for batch in index.query_batches():
        yield from batch

# Memory: O(batch_size)

# ❌ Bad: List (high memory)
def fetch_all_vectors(index: Index) -> list[Vector]:
    vectors = []
    for batch in index.query_batches():
        vectors.extend(batch)  # Builds full list in memory
    return vectors

# Memory: O(total_vectors)
```

### Struct Defaults

**Use omit_defaults=True**:

```python
# ✅ Good: Omit defaults (smaller payloads)
class HTTPConfig(Struct, kw_only=True, omit_defaults=True):
    timeout: int = 30
    max_retries: int = 3
    max_connections: int = 100

config = HTTPConfig()
# Serializes to {} instead of full dict

# ❌ Bad: Always include defaults
class HTTPConfig(Struct, kw_only=True):
    timeout: int = 30
    max_retries: int = 3
    max_connections: int = 100

config = HTTPConfig()
# Serializes to {"timeout": 30, "max_retries": 3, ...}
```

## Serialization Performance

### msgspec Encoding

**Use msgspec for fast serialization**:

```python
import msgspec

# ✅ Good: msgspec encoding
encoder = msgspec.json.Encoder()
data = encoder.encode(vectors)

# Benchmark: ~100ns per vector

# ❌ Bad: Manual dict conversion
data = json.dumps([
    {"id": v.id, "values": v.values, "metadata": v.metadata}
    for v in vectors
])

# Benchmark: ~1000ns per vector (10x slower)
```

### Reuse Encoders

**Create encoder once, reuse many times**:

```python
# ✅ Good: Reuse encoder
class HTTPClient:
    def __init__(self):
        self._encoder = msgspec.json.Encoder()
        self._decoder = msgspec.json.Decoder()

    def post(self, url: str, data: Any) -> dict:
        body = self._encoder.encode(data)
        response = self._client.post(url, content=body)
        return self._decoder.decode(response.content)

# ❌ Bad: Create encoder per request
class HTTPClient:
    def post(self, url: str, data: Any) -> dict:
        body = msgspec.json.encode(data)  # New encoder
        response = self._client.post(url, content=body)
        return msgspec.json.decode(response.content)  # New decoder
```

## Caching

### Property Caching

**Cache expensive computed properties**:

```python
from functools import cached_property

# ✅ Good: Cached property
class Index:
    @cached_property
    def dimension(self) -> int:
        """Fetch dimension from API (cached)."""
        response = self._http.get(f"/indexes/{self.name}")
        return response.json()["dimension"]

# First access: API call
dim = index.dimension  # API call

# Subsequent access: cached
dim = index.dimension  # No API call

# ❌ Bad: No caching
class Index:
    @property
    def dimension(self) -> int:
        response = self._http.get(f"/indexes/{self.name}")  # Every access
        return response.json()["dimension"]
```

### Client-Side Caching

**Cache immutable data**:

```python
from functools import lru_cache

# ✅ Good: Cache index list
class Client:
    @lru_cache(maxsize=1)
    def list_indexes(self) -> tuple[str, ...]:
        """List indexes (cached for 60s)."""
        response = self._http.get("/indexes")
        return tuple(response.json()["indexes"])

# ❌ Bad: No caching
class Client:
    def list_indexes(self) -> list[str]:
        response = self._http.get("/indexes")  # Every call
        return response.json()["indexes"]
```

## Lazy Loading

### Lazy Imports for Namespaces

**Pattern**: Use `TYPE_CHECKING` imports with lazy runtime loading for namespace classes.

**When to use this pattern**:
- Namespace classes that users may not need (e.g., Index, Collection, Inference)
- Classes that import heavy dependencies (msgspec models, protocols)
- Multiple namespace imports that accumulate cost (~7-8ms each)
- User typically only uses 1-2 namespaces per script

**Benefits**:
- Users only pay import cost for what they use
- With 5 namespaces, saves ~80ms for users who only need 1
- Scales well as SDK grows

```python
# ✅ Good: Lazy namespace loading (Client pattern)
from typing import TYPE_CHECKING

from pinecone._internal.http import HTTPClient

if TYPE_CHECKING:
    from pinecone.inference import Inference

class Pinecone:
    def __init__(self, api_key: str, base_url: str = "..."):
        self._http_client = HTTPClient(api_key=api_key)
        self._base_url = base_url
        
        # Namespace objects lazy-loaded on first access
        self._inference: Inference | None = None

    @property
    def inference(self) -> Inference:
        """Access Inference namespace (lazy-loaded on first access)."""
        if self._inference is None:
            from pinecone.inference import Inference  # Runtime import
            
            self._inference = Inference(
                http_client=self._http_client,
                base_url=self._base_url,
            )
        return self._inference

# ❌ Bad: Eager namespace loading
from pinecone.inference import Inference  # Loaded at import time

class Pinecone:
    def __init__(self, api_key: str):
        self._http_client = HTTPClient(api_key)
        # All namespaces created even if unused
        self._inference = Inference(self._http_client)
```

**Measurement**: Use `scripts/benchmark_imports.py` to measure import costs as namespaces are added.

### Lazy Imports for Optional Dependencies

**Pattern**: Import heavy optional dependencies only when needed.

```python
# ✅ Good: Lazy import for optional heavy dependency
def embed_with_numpy(text: str) -> list[float]:
    import numpy as np  # Heavy optional dependency, only if used
    return np.array([...]).tolist()

# ❌ Bad: Top-level import for optional dependency
import numpy as np  # Slows startup even if never used

def embed_with_numpy(text: str) -> list[float]:
    return np.array([...]).tolist()
```

### When NOT to Use Lazy Imports

**Don't lazy-load:**
- Core utilities always needed (HTTP client, exceptions)
- Lightweight standard library modules (os, sys, time)
- Internal utilities within a namespace implementation
- Type-only imports (use `TYPE_CHECKING` instead)

```python
# ✅ Good: Top-level imports for core modules
from pinecone._internal.http import HTTPClient  # Always needed
from pinecone.exceptions import APIError  # Lightweight

# ❌ Bad: Lazy importing lightweight core modules
class Inference:
    def __init__(self):
        from pinecone.exceptions import APIError  # Don't do this
        self._error_class = APIError
```

**Rationale**:
- Python's import caching makes subsequent imports very fast
- Top-level imports improve readability and IDE support
- Lazy imports add complexity and hide dependencies
- Only cumulative cost (>20ms) or optional dependencies justify the trade-off

## Import Performance

### Module Import Time Target

**Goal**: Cold import of `from pinecone import Pinecone` should be **< 10ms** on modern hardware.

**Why it matters**:
- Test discovery speed (pytest startup)
- IDE responsiveness (language server initialization)
- CLI tool startup time
- Lambda cold starts
- Script execution overhead

### Optimization Strategy

**Defer heavy dependencies** until first use:

```python
# ✅ Good: Defer httpx import until instantiation
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from pinecone._internal.http import HTTPClient

class Pinecone:
    def __init__(self, api_key: str):
        # Import httpx only when creating client instance
        from pinecone._internal.http import HTTPClient
        
        self._http_client = HTTPClient(api_key=api_key)

# ❌ Bad: Import httpx at module load time
from pinecone._internal.http import HTTPClient  # ~120ms for httpx

class Pinecone:
    def __init__(self, api_key: str):
        self._http_client = HTTPClient(api_key=api_key)
```

### Import Performance Benchmark Results

**Before optimization:**
- `from pinecone import Pinecone`: **173ms**
- Main bottleneck: httpx imported eagerly (120ms)
- Secondary: msgspec imported eagerly (40ms)

**After optimization:**
- `from pinecone import Pinecone`: **5ms** (35x faster)
- httpx imported at instantiation time
- msgspec imported at instantiation time

**Trade-off:**
- Import time: 173ms → 5ms (-97%) ✅
- Instantiation time: 122ms → 296ms (+174ms) ⚠️

The trade-off is acceptable because:
- Import time savings benefit ALL imports (test discovery, IDE, CLI)
- Instantiation cost is paid once per application lifecycle
- Subsequent instantiations benefit from cached imports

### Measuring Import Performance

**Use the import performance test suite:**

```bash
# Run all import performance tests
uv run pytest tests/unit/test_import_performance.py -v

# Measure cold import time
uv run pytest tests/unit/test_import_performance.py::test_cold_import_pinecone_class -v

# Get detailed breakdown
python -X importtime -c "from pinecone import Pinecone" 2>&1 | grep pinecone
```

**Example output:**
```
Statement: from pinecone import Pinecone
Time:      4.91ms  ✅ Under 10ms target
```

### Lazy Loading Pattern for Heavy Dependencies

**Pattern**: Defer httpx/msgspec imports using `TYPE_CHECKING` and runtime imports.

```python
# In __init__.py: Use __getattr__ for lazy loading
def __getattr__(name: str) -> Any:
    """Lazy-load HTTP clients to avoid importing httpx until needed."""
    if name == "HTTPClient":
        from pinecone._internal.http import HTTPClient
        return HTTPClient
    # ... other lazy-loaded classes
    
    raise AttributeError(f"module 'pinecone' has no attribute '{name}'")

# In client.py: Import at instantiation time
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from pinecone._internal.http import HTTPClient

class Pinecone:
    def __init__(self, api_key: str):
        # Import httpx here, not at module level
        from pinecone._internal.http import HTTPClient
        
        self._http_client = HTTPClient(api_key=api_key)
```

### When to Defer Imports

**Defer imports for:**
- Heavy dependencies (httpx: 120ms, msgspec: 40ms, orjson: 26ms)
- Optional functionality not always used
- External libraries with many transitive dependencies
- Cumulative cost > 20ms

**Don't defer imports for:**
- Exception classes (lightweight, always needed)
- Type hints (use `TYPE_CHECKING` instead)
- Standard library modules (< 1ms each)
- Internal utilities within same namespace

### Import Time Breakdown

**Current import costs (after optimization):**

| Component | Time | Loaded When |
|-----------|------|-------------|
| `pinecone` module | 5ms | Import time ✅ |
| Exception classes | 0ms | Import time ✅ |
| Client classes | 0ms | Import time ✅ |
| `httpx` | 120ms | First `Pinecone()` instantiation |
| `msgspec` | 40ms | First `Pinecone()` instantiation |
| Namespace classes | 70ms | First namespace access (e.g., `pc.inference`) |
| Model classes | 60ms | First model import |

**Total cold start for typical usage:**
```python
from pinecone import Pinecone  # 5ms ✅

pc = Pinecone(api_key="...")   # 296ms (includes httpx)
response = pc.inference.embed(...)  # 70ms first access (namespace)
```

### Preventing Import Performance Regressions

**CI Performance Gate** (recommended):

```python
# tests/unit/test_import_performance.py
def test_cold_import_pinecone_class() -> None:
    """Ensure import time stays under threshold."""
    import_time = measure_cold_import("from pinecone import Pinecone")
    
    # Fail if import time exceeds threshold
    assert import_time < 0.05, f"Import took {import_time * 1000:.2f}ms, expected <50ms"
```

**Monitor these metrics:**
- Cold import time (< 10ms target, < 50ms max)
- Namespace loading time (< 100ms per namespace)
- Client instantiation time (< 500ms)

## Benchmarking

### Profile Code

**Use cProfile for profiling**:

```python
import cProfile
import pstats

# Profile function
profiler = cProfile.Profile()
profiler.enable()

client.list_indexes()  # Code to profile

profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(10)  # Top 10 functions
```

### Benchmark Tests

**Use pytest-benchmark**:

```python
import pytest

def test_msgspec_encoding(benchmark):
    """Benchmark msgspec encoding."""
    vectors = [
        {"id": f"vec{i}", "values": [0.1] * 1536}
        for i in range(1000)
    ]

    def encode():
        return msgspec.json.encode(vectors)

    result = benchmark(encode)
    assert result is not None

# Run benchmarks
# pytest --benchmark-only
```

### Compare Performance

**Set performance baselines**:

```python
def test_performance_baseline(benchmark):
    """Encoding should be under 10ms for 1000 vectors."""
    vectors = [{"id": f"vec{i}", "values": [0.1] * 1536} for i in range(1000)]

    def encode():
        return msgspec.json.encode(vectors)

    stats = benchmark(encode)
    assert stats.stats.mean < 0.010  # Under 10ms
```

## Performance Monitoring

### Logging Performance

**Log slow operations**:

```python
import time
import logging

logger = logging.getLogger(__name__)

# ✅ Good: Log slow operations
def request(method: str, url: str, **kwargs) -> httpx.Response:
    start = time.perf_counter()
    response = self._client.request(method, url, **kwargs)
    duration = time.perf_counter() - start

    if duration > 1.0:  # Slow request
        logger.warning(
            "Slow request",
            extra={"method": method, "url": url, "duration": duration}
        )

    return response

# ❌ Bad: No performance tracking
def request(method: str, url: str, **kwargs) -> httpx.Response:
    return self._client.request(method, url, **kwargs)
```

### Metrics Collection

**Track key metrics**:

```python
# ✅ Good: Collect metrics
class HTTPClient:
    def __init__(self):
        self.total_requests = 0
        self.total_duration = 0.0
        self.errors = 0

    def request(self, method: str, url: str) -> httpx.Response:
        self.total_requests += 1
        start = time.perf_counter()

        try:
            response = self._client.request(method, url)
            self.total_duration += time.perf_counter() - start
            return response
        except Exception:
            self.errors += 1
            raise
```

## Anti-Patterns

### ❌ Premature Optimization

```python
# Bad: Optimizing before profiling
def process(data):
    # Complex optimization that's hard to read
    return [x for x in (y for y in data if y) if x > 0]
```

**Solution**: Profile first, optimize bottlenecks.

```python
# Good: Clear code, optimize if needed
def process(data):
    filtered = [item for item in data if item and item > 0]
    return filtered
```

### ❌ Excessive Validation

```python
# Bad: Validating everything
class Vector(Struct):
    id: str
    values: list[float]

    def __post_init__(self):
        if not self.id:
            raise ValueError("id required")
        if not self.values:
            raise ValueError("values required")
        if not all(isinstance(v, float) for v in self.values):
            raise ValueError("values must be floats")
```

**Solution**: Trust types, validate only user input.

```python
# Good: Trust types
class Vector(Struct):
    id: str
    values: list[float]
```

### ❌ Synchronous in Async

```python
# Bad: Blocking call in async
async def fetch_data():
    response = requests.get(url)  # Blocks event loop!
    return response.json()
```

**Solution**: Use async libraries.

```python
# Good: Async HTTP
async def fetch_data():
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
    return response.json()
```

## Performance Checklist

Before releasing:

- [ ] Use msgspec for data models
- [ ] Use orjson for JSON operations
- [ ] Enable HTTP/2 in httpx
- [ ] Configure connection pooling
- [ ] Implement request batching
- [ ] Use asyncio.gather for concurrency
- [ ] Add uvloop for production
- [ ] Profile hot paths
- [ ] Benchmark critical operations
- [ ] Log slow operations
- [ ] Minimize memory copies
- [ ] Use generators for large datasets
- [ ] Cache expensive operations
- [ ] Lazy load heavy dependencies

## References

- See [types.md](./types.md) for msgspec patterns
- See [async.md](./async.md) for async performance
- See [http-client.md](./http-client.md) for HTTP optimization
- See [testing.md](./testing.md) for performance testing
