---
phase: 03-rust-hot-path
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - crates/opendeviationbar-streaming/src/live_engine.rs
autonomous: true
requirements: [RUST-03]

must_haves:
  truths:
    - "WS select loop reuses a single Vec<AggTrade> across iterations instead of allocating per trade batch"
  artifacts:
    - path: "crates/opendeviationbar-streaming/src/live_engine.rs"
      provides: "Reused burst frame Vec"
      contains: "frame.clear()"
  key_links:
    - from: "symbol_task select loop (live_engine.rs)"
      to: "burst frame Vec<AggTrade>"
      via: "clear() + push instead of Vec::with_capacity(32) per iteration"
      pattern: "frame\\.clear\\(\\)"
---

<objective>
Eliminate per-iteration heap allocation of the burst frame Vec in the WebSocket select loop (RUST-03).

Purpose: The WS processing loop runs for every trade received across all 18 symbols. `Vec::with_capacity(32)` allocates 32 \* sizeof(AggTrade) bytes on each iteration. Moving the Vec outside the loop and using `clear()` reuses the allocation.
Output: Zero per-trade heap allocation in the WS hot path.
</objective>

<execution_context>
@/Users/terryli/.claude/get-shit-done/workflows/execute-plan.md
@/Users/terryli/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md

@crates/opendeviationbar-streaming/src/live_engine.rs

<interfaces>
<!-- Key types and contracts the executor needs -->

From crates/opendeviationbar-streaming/src/live_engine.rs:

```rust
// Line 1036-1039:
const MAX_BURST_DRAIN: usize = 1024;

// Line 1510-1511: The select loop
loop {
    tokio::select! {
        // ... gap-fill command branch ...
        trade = trade_rx.recv() => {
            match trade {
                Some(first_trade) => {
                    // Line 1579: ALLOCATION HAPPENS HERE EVERY ITERATION
                    let mut frame = Vec::with_capacity(32);
                    frame.push(first_trade);
                    for _ in 0..MAX_BURST_DRAIN {
                        match trade_rx.try_recv() {
                            Ok(t) => frame.push(t),
                            Err(_) => break,
                        }
                    }
                    // ... process frame ...
                }
            }
        }
    }
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Move burst frame Vec outside select loop and reuse via clear()</name>
  <files>crates/opendeviationbar-streaming/src/live_engine.rs</files>
  <read_first>
    - crates/opendeviationbar-streaming/src/live_engine.rs lines 1500-1620 (the full select loop with burst frame logic)
    - crates/opendeviationbar-streaming/src/live_engine.rs lines 1036-1044 (MAX_BURST_DRAIN constant and doc)
  </read_first>
  <action>
    1. Find the `loop { tokio::select! { ... } }` block (starts around line 1510).

    2. BEFORE the `loop {`, add:
       ```rust
       // Issue #96 RUST-03: Reuse burst frame allocation across select loop iterations.
       // Previously allocated Vec::with_capacity(32) per iteration — now cleared and reused.
       let mut frame: Vec<AggTrade> = Vec::with_capacity(32);
       ```

    3. INSIDE the `Some(first_trade) =>` arm (around line 1579), replace:
       ```rust
       let mut frame = Vec::with_capacity(32);
       frame.push(first_trade);
       ```
       With:
       ```rust
       frame.clear();
       frame.push(first_trade);
       ```

    4. Verify no other code path takes ownership of `frame` (e.g., no `std::mem::take`, no move into a closure). The frame is only iterated/indexed after filling, then falls out of scope at the end of the match arm. With this change, it persists across iterations.

    5. Ensure `AggTrade` is imported at the top of the function (it should already be — it's used throughout).

    This is a minimal, safe change. The Vec capacity grows to accommodate the largest burst seen and stays there. Typical bursts are 1-50 trades, so the Vec stabilizes at a small size quickly. The `clear()` call is O(N) for dropping elements but AggTrade is Copy/trivially-droppable so it's effectively O(1) — just resets the length.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && cargo nextest run -p opendeviationbar-streaming --lib && cargo build -p opendeviationbar-streaming</automated>
  </verify>
  <acceptance_criteria>
    - `grep -n "frame.clear()" crates/opendeviationbar-streaming/src/live_engine.rs` shows at least 1 match inside the select loop
    - `grep -n "Vec::with_capacity(32)" crates/opendeviationbar-streaming/src/live_engine.rs` shows the allocation BEFORE the loop, NOT inside the `Some(first_trade)` arm
    - `cargo build -p opendeviationbar-streaming` compiles without warnings
    - `cargo nextest run -p opendeviationbar-streaming --lib` all tests pass
  </acceptance_criteria>
  <done>Burst frame Vec allocated once before the select loop, reused via clear() on each trade batch. Zero per-iteration heap allocation in the WS hot path.</done>
</task>

</tasks>

<verification>
```bash
# Streaming tests pass
cargo nextest run -p opendeviationbar-streaming --lib

# Build succeeds

cargo build -p opendeviationbar-streaming

```
</verification>

<success_criteria>
1. No `Vec::with_capacity` inside the `tokio::select!` trade arm
2. `frame.clear()` used to reset the Vec each iteration
3. All streaming tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/03-rust-hot-path/03-02-SUMMARY.md`
</output>
```
