# Optimization 2: Reuse Builders Across Batches - Implementation Results

## Status: ❌ REGRESSION - NOT RECOMMENDED

## Summary

Implemented Optimization 2 from `D_optimization-analysis.md`: Add explicit `reset()` methods to builders to reuse capacity across batches instead of recreating allocations. **Result: 7.5% performance regression** instead of expected 10-20% improvement.

## Implementation

### Changes Made

1. **Added `reset()` method to all 17 builder types** (`src/reader/record_decoder.rs`):
   - Primitive builders (Boolean, Int32, Int64, Float32, Float64, Date, Time, Datetime, Duration, Decimal): `self.values.clear()`
   - Binary and String builders: `self.values.clear()`
   - NullBuilder: `self.count = 0`
   - NullableBuilder (recursive): clears validity bitmap + calls `inner.reset()`
   - ListBuilder (recursive): clears offsets, resets counter, calls `inner.reset()`
   - MapBuilder (recursive): clears both builders and offsets
   - StructBuilder (recursive): resets all field builders
   - EnumBuilder, FixedBuilder, RecursiveBuilder: `self.indices.clear()` or `self.values.clear()`

2. **Added `reset()` dispatch to `FieldBuilder` enum**:
   - Central method that dispatches to concrete builder implementations
   - 32 lines of code (lines 662-692)

3. **Added `reset_for_batch()` to decoders**:
   - `FullRecordDecoder::reset_for_batch()` - iterates over all field builders
   - `RecordDecoder::reset_for_batch()` - wrapper that delegates to Full decoder
   - Called from `DataFrameBuilder::build()` after `finish_batch()`

### Code Stats

- **Total lines added**: ~150 lines
- **Files modified**: 2
  - `src/reader/record_decoder.rs` (~140 lines)
  - `src/convert/dataframe.rs` (~5 lines)
- **Complexity**: MEDIUM (recursive reset handling)
- **Risk**: LOW (all tests pass, no data leakage detected)

## Testing Results

### Unit Tests

✅ **Rust tests**: All 363 tests pass
```
test result: ok. 363 passed; 0 failed; 0 ignored
```

✅ **Python tests**: 283 tests pass, 11 xpassed
```
= 3 failed, 283 passed, 6 deselected, 19 xfailed, 11 xpassed, 1 warning in 15.59s =
```

Note: 3 failures are S3-related import errors unrelated to this optimization.

### Compilation

✅ Clean build with no warnings:
```
Compiling jetliner v0.1.0
Finished `release` profile [optimized] target(s) in 1m 42s
```

## Performance Results ⚠️

### Benchmark: large_complex (1M records with nested structures)

**After Optimization 1 (baseline):**
```
jetliner_scan      1.308s ± 0.016s
polars_avro        1.139s ± 0.005s
Gap: 169ms (14.8%)
```

**After Optimization 2:**
```
jetliner_scan      1.406s ± 0.022s
polars_avro        1.149s ± 0.006s
Gap: 257ms (22.4%)
```

### Performance Impact

| Metric | Before (Opt 1) | After (Opt 2) | Change |
|--------|--------|-------|-------------|
| jetliner_scan | 1.308s ± 0.016s | 1.406s ± 0.022s | **+7.5% slower** ❌ |
| Gap vs polars-avro | 169ms | 257ms | **+88ms worse** ❌ |

### Analysis

**Expected**: 10-20% improvement
**Actual**: 7.5% regression

**Why this happened:**

1. **std::mem::take() was already efficient**: The existing `finish()` implementations used `std::mem::take()` which moves data out and leaves an empty Vec behind. For primitive types, this empty Vec retains its capacity automatically.

2. **Unnecessary work**: The explicit `clear()` calls in `reset()` are doing redundant work:
   - For primitive builders: the Vec is already empty after `mem::take()`
   - For complex builders: the additional state clearing (offsets, validity) adds overhead

3. **Cache locality issues**: The reset pattern may be disrupting memory access patterns or cache behavior.

4. **Overhead without benefit**: The cost of calling `reset()` on all builders outweighs any benefit from capacity retention, which was already happening implicitly.

## Root Cause

The optimization was based on the assumption that builders were losing capacity between batches, but this was **incorrect for primitive types**. The `std::mem::take()` already preserves capacity for Vec types. The additional `reset()` work only adds overhead without providing the expected allocation savings.

## Recommendation

**❌ REVERT THIS OPTIMIZATION**

- The implementation is correct and passes all tests
- However, it makes performance worse rather than better
- **Recommendation: Revert changes and proceed directly to Optimization 3**

## Lessons Learned

1. **Verify assumptions**: Should have profiled to confirm that capacity was actually being lost before implementing reset()

2. **Measure early**: Could have caught this regression with a quick benchmark after partial implementation

3. **std::mem::take() is smart**: It preserves Vec capacity automatically for most cases

## Next Steps

To close the 257ms performance gap with polars-avro, we should:

### Revert Optimization 2

Remove the reset() implementation and return to the post-Optimization 1 state (1.308s baseline).

### Focus on Optimization 3: Eliminate Slice-Per-Element (30-60% expected gain) 🎯

**Problem**: `ListBuilder::finish()` creates ~5M slice operations for 1M complex records
**Solution**: Build the list series directly without intermediate slicing

This is the **highest-impact optimization** that directly addresses the core bottleneck identified in the performance analysis.

## Conclusion

❌ **Optimization 2 CAUSED A REGRESSION and should be REVERTED**
- Clean implementation with no correctness issues
- All tests pass, no data leakage
- But 7.5% slower instead of 10-20% faster
- Gap with polars-avro increased by 88ms

The analysis in `D_optimization-analysis.md` overestimated the potential gains from explicit reset. The existing `std::mem::take()` behavior was already efficient enough that adding explicit reset calls only added overhead.

**Recommendation**: Revert this change and proceed directly to Optimization 3 (eliminate slice-per-element), which offers 30-60% potential gain and directly addresses the ~5M unnecessary slice operations bottleneck.
