# Performance Analysis: Step 6.2 Batch Optimization Opportunities

## Overview
This document analyzes the performance characteristics of `compute_attention_seconds()` 
and evaluates potential optimization opportunities as specified in Step 6.2 of the tracking plan.

## Current Performance (Baseline)

### Test Results - Production Performance (without profiling overhead)
All performance tests pass comfortably:

| Scenario | Target | Actual | Status | Throughput |
|----------|--------|--------|--------|------------|
| 300 samples × 20 AOIs | <1.0s | **0.370s** | ✅ **Pass** (63% faster) | 811 samples/s |
| 600 samples × 10 AOIs | <2.5s | **0.547s** | ✅ **Pass** (78% faster) | 1097 samples/s |
| 100 samples × 50 AOIs | <1.0s | **0.166s** | ✅ **Pass** (83% faster) | 602 samples/s |
| 300 samples × 50 AOIs | <2.0s | **0.406s** | ✅ **Pass** (80% faster) | 739 samples/s |

**Note**: These timings reflect actual production performance (`enable_profiling=False`).
When profiling is enabled with `tracemalloc` for memory analysis, performance is ~4-5x slower.

### Profiled Performance (with tracemalloc overhead)
For comparison, when `enable_profiling=True` is used:

| Scenario | Production | With Profiling | Overhead |
|----------|------------|----------------|----------|
| 300 samples × 20 AOIs | 0.370s | ~1.93s | 5.2x |
| 600 samples × 10 AOIs | 0.547s | ~2.84s | 5.2x |
| 100 samples × 50 AOIs | 0.166s | ~0.72s | 4.3x |
| 300 samples × 50 AOIs | 0.406s | ~2.38s | 5.9x |

### Memory Usage
Peak memory usage is minimal: **0.0-0.1 MB** for all scenarios tested.

---

## Profiling Analysis: Bottlenecks Identified

### Top Functions by Cumulative Time

From cProfile analysis on 300 samples × 20 AOIs (1.93s total):

| Function | Time (s) | % Total | Calls | Time/Call |
|----------|----------|---------|-------|-----------|
| `sweep.py:compute_coverage` | 0.83 | 43% | 300 | 2.77ms |
| `clipping.py:clip_polygon_to_wedge` | 0.86 | 45% | 6000 | 0.14ms |
| `clipping.py:clip_polygon_halfplane` | 0.44 | 23% | 3963 | 0.11ms |
| `sweep.py:_find_min_distance_at_angle` | 0.24 | 12% | 6582 | 0.04ms |
| `clipping.py:compute_bounding_box` | 0.08 | 4% | 6000 | 0.01ms |

### Key Observations

1. **Sweep algorithm dominates** (~43% of time): This is the core angular sweep algorithm 
   that cannot be easily optimized without changing the fundamental approach.

2. **Polygon clipping is expensive** (~45% of time): Called once per sample per AOI 
   (e.g., 6000 times for 300 samples × 20 AOIs).

3. **Bounding box computation is repeated**: Called 6000 times (20 per sample), 
   computing the same bounding boxes repeatedly for each AOI.

4. **No obvious "low-hanging fruit"**: Most time is spent in core geometric operations 
   that are already well-optimized.

---

## Potential Optimizations Evaluated

### 1. Pre-compute AOI Bounding Boxes ⭐ RECOMMENDED
**Concept**: Compute bounding boxes once per batch, not once per sample per AOI.

**Analysis**:
- **Current**: `compute_bounding_box()` called 6000 times for 300 samples × 20 AOIs
- **Optimized**: Called once per AOI (20 times) at batch initialization
- **Savings**: ~75ms for 300×20 scenario (4% improvement)
- **Complexity**: LOW - simple caching at batch level
- **Risk**: NONE - purely additive optimization

**Implementation Approach**:
```python
# At batch initialization:
aoi_bboxes = {idx: compute_bounding_box(aoi.contour) for idx, aoi in enumerate(aois)}

# Pass bboxes to find_largest_obstacle() to avoid recomputation
```

**Trade-offs**:
- ✅ Minimal code changes
- ✅ No accuracy impact
- ✅ Minimal memory overhead (~16 bytes per AOI)
- ⚠️ Requires modifying `find_largest_obstacle()` API signature

### 2. Early AOI Filtering by Distance 🤔 EVALUATE FURTHER
**Concept**: Pre-filter AOIs that are definitely too far from viewer position.

**Analysis**:
- Use bounding box to quickly test if `distance(viewer, bbox) > max_range`
- Could eliminate entire clipping operations for distant AOIs
- **Potential savings**: 10-30% if many AOIs are typically out of range
- **Complexity**: MEDIUM - requires distance checks and AOI list filtering
- **Risk**: LOW if implemented correctly

**Implementation Approach**:
```python
def _is_aoi_potentially_visible(viewer_pos, aoi_bbox, max_range):
    # Find closest point in AABB to viewer
    closest = np.clip(viewer_pos, aoi_bbox[0], aoi_bbox[1])
    # Check distance
    return np.linalg.norm(closest - viewer_pos) <= max_range

# Filter AOIs per sample:
visible_aoi_indices = [i for i in range(len(aois)) 
                       if _is_aoi_potentially_visible(sample.position, bboxes[i], max_range)]
```

**Trade-offs**:
- ⚠️ Benefit depends on AOI spatial distribution
- ⚠️ Adds per-sample filtering overhead
- ⚠️ May provide minimal benefit if AOIs are typically in range
- ✅ No accuracy impact (purely early-rejection optimization)

### 3. Vectorize Sample Processing ❌ NOT RECOMMENDED
**Concept**: Process multiple samples in parallel using numpy vectorization.

**Analysis**:
- The core `find_largest_obstacle()` involves complex branching logic and polygon clipping
- Not amenable to vectorization without complete rewrite
- **Savings**: Potentially high, but implementation cost prohibitive
- **Complexity**: VERY HIGH - requires fundamental algorithm redesign
- **Risk**: HIGH - easy to introduce subtle bugs

**Trade-offs**:
- ❌ Massive implementation complexity
- ❌ Would require rewriting core sweep/clipping algorithms
- ❌ May not work well with variable-complexity AOI geometries

### 4. Result Caching for Similar Samples ⚠️ DEFER
**Concept**: Cache results for nearby viewer positions/directions.

**Analysis**:
- When consecutive samples are nearly identical, reuse cached winner
- **Potential savings**: High for stationary viewers (common in stores)
- **Complexity**: HIGH - requires cache key design, similarity thresholds, invalidation
- **Risk**: MEDIUM - threshold tuning, potential accuracy loss at boundaries

**Criteria for cache hit**:
```python
def samples_similar(s1, s2, pos_threshold=5.0, angle_threshold=np.deg2rad(2)):
    pos_dist = np.linalg.norm(np.array(s1.position) - np.array(s2.position))
    angle_diff = np.arccos(np.clip(np.dot(s1.direction, s2.direction), -1, 1))
    return pos_dist < pos_threshold and angle_diff < angle_threshold
```

**Trade-offs**:
- ⚠️ Threshold selection is application-specific
- ⚠️ Cache management adds complexity
- ⚠️ May introduce subtle inaccuracies at AOI boundaries
- ✅ Scoped to single batch (no cross-session persistence needed)

---

## Recommendations

### Phase 1: Minimal Optimization (LOW RISK, MODERATE GAIN)
**Status**: Deferred pending API discussion

1. **Pre-compute bounding boxes** at batch level
2. **Early distance filtering** if profiling shows many out-of-range AOIs

**Expected improvement**: 5-15% depending on AOI spatial distribution

**Implementation priority**: MEDIUM - would require changes to core `find_largest_obstacle()` API

### Phase 2: Investigate Caching (MEDIUM RISK, HIGH GAIN)
**Status**: Future enhancement

1. Implement similarity-based result caching
2. Tune thresholds based on real-world usage patterns
3. Add cache hit/miss metrics to profiling data

**Expected improvement**: 30-60% for stationary viewers, 0-20% for moving viewers

**Implementation priority**: LOW - defer until baseline API is stable and real usage patterns are known

### Phase 3: Do Nothing (CURRENT APPROACH) ✅
**Rationale**:
- Current performance **exceeds all targets by 63-83%** (602-1097 samples/second production throughput)
- All SLA benchmarks pass comfortably: 300×20 in 0.37s vs <1s target
- No critical bottleneck identified that's easily optimized
- Premature optimization may complicate codebase
- Real-world performance should be evaluated with actual data before optimizing

---

## Conclusion

**Step 6.2 Recommendation**: **Defer optimizations** until real-world usage patterns are established.

### Why This Approach?
1. **Performance exceeds requirements**: **602-1097 samples/second** production throughput is 600-1000× faster than 1 Hz input sampling rate
2. **All benchmarks pass comfortably**: 300×20 runs in **0.37s** vs <1s target (63% faster); 600×10 in **0.55s** vs <2.5s target (78% faster)
3. **No low-hanging fruit**: The 4% savings from bbox caching isn't worth API changes yet
4. **Unknown spatial distribution**: Distance filtering benefit depends on how AOIs are positioned
5. **Premature optimization risk**: Adding complexity before understanding real usage patterns

### Next Steps
1. **✅ Document analysis** (this file)
2. **✅ Establish performance baseline** (profile_tracking.py)
3. **⏸️ Monitor performance** in production/real scenarios
4. **⏸️ Revisit if** real usage shows unacceptable latency or specific bottlenecks
5. **⏸️ Consider** bounding box caching if `find_largest_obstacle()` API is refactored for other reasons

### If Optimization Becomes Necessary
The analysis above provides a clear roadmap:
1. Start with bounding box pre-computation (safest, 4-5% gain)
2. Add distance-based filtering (10-30% gain if beneficial)
3. Evaluate caching for stationary viewer scenarios (30-60% gain)
4. Only consider vectorization as last resort (massive complexity)

---

**Document Status**: ✅ Complete (Step 6.2)  
**Last Updated**: December 20, 2025  
**Author**: Performance analysis for tracking module  
**Related**: TRACKING_PLAN.md Step 6.2, profile_tracking.py
