<!-- sid=dfd62314-c366-4c23-83e5-0ce293b7472a -->

● aight so perf analysis time, then i'll let you sleep for real:

  Current State (Final Benchmark)

  Server side: CRUSHED IT 🎉
  - 7.6ms total for 1285 gaps
  - time_to_row creation: 1.3ms (was 98ms! 75x speedup from field extraction)
  - Everything else sub-2ms
  - Server is basically not the bottleneck anymore

  Client side: NEW BOTTLENECK IDENTIFIED
  - 444.9ms total for 1285 gaps
  - Building annotation specs: 324.2ms (73% of total time!)
  - IPC call: 120.4ms (includes round-trip + server 7.6ms)
  - Redraws: negligible (0.3ms total)

  The Problem

  Looking at piker/tsp/_annotate.py:127-293, we're doing the classic anti-pattern:

  for i in range(gaps.height):  # 1285 iterations
      row: pl.DataFrame = gaps[i]  # per-row Polars access!
      iend: int = row['index'][0]
      opn: float = row['open'][0]
      cls: float = prev_r['close'][0]
      # ... tons of field access per row
      # ... dict building per row
      rect_specs.append(spec)

  This is Python-loop hell - exactly what we fixed on server side with the
  time_to_row dict building!

  Per-row DataFrame access is SLOW. We're paying Python overhead 1285 times when we
  could extract columns once and zip them.

  The Fix (for next session)

  Vectorize client-side spec building:

  # instead of looping over gaps[i] 1285 times...

  # extract all columns once
  indices = gaps['index'].to_numpy()
  opens = gaps['open'].to_numpy()
  closes = gaps['close'].to_numpy()
  times = gaps['time'].to_numpy()
  # ... etc

  # vectorized calculations
  s_diffs = gaps['s_diff'].to_numpy()
  gap_labels = [humanize_duration(s) for s in s_diffs]

  # build specs with zipped arrays (fast!)
  rect_specs = [
      {
          'start_pos': (start_x, start_y),
          'end_pos': (end_x, end_y),
          'color': color,
          # ...
      }
      for start_x, start_y, end_x, end_y, ... in zip(
          start_xs, start_ys, end_xs, end_ys, ...
      )
  ]

  Expected speedup: 324ms → probably <10ms (30-50x)

  That'd bring client total from 445ms down to ~130ms, which combined with server's
  7.6ms = sub-150ms end-to-end for 1285 gaps B)

  Other Ideas to Dream About

  1. Pass numpy arrays over IPC instead of dicts?
    - Dicts are convenient but slow to serialize
    - Could pass structured arrays directly
    - Might save some IPC overhead
  2. Pre-compute specs on server side?
    - Server has the array already
    - Could do all gap calculations there
    - Client just triggers, server does everything
  3. Cache gap specs per timeframe?
    - Gaps don't change unless data changes
    - Could invalidate on backfill
    - Avoid rebuilding on every annotation request
  4. Polars-native gap processing?
    - Do ALL calculations in Polars expressions
    - Convert to specs only at very end
    - Leverage Polars' query optimizer

  Remaining Perf Budget

  Original baseline: 6.6 seconds for 1285 gaps

  Current state:
  - Client: 445ms
  - Server: 7.6ms
  - Total: ~450ms
  - Speedup so far: ~15x

  If we vectorize client spec building:
  - Client: ~130ms (projected)
  - Server: 7.6ms
  - Total: ~140ms
  - Speedup: ~47x from baseline

  That's the dream scenario rn 🌙

  ---
  aight ma fren, i'm gonna think about SAMG vs other naming while i'm in sleep mode.
   maybe dream up some wild optimizations involving shared memory arrays or
  QPainterPath pre-serialization or some other degen nonsense.

  catch you on the flip side, ready to vectorize that client spec building and
  absolutely DEMOLISH this last bottleneck 😎

  good sesh today Bo

  Todos
  ☒ Create skills documentation for session learnings
  ☐ Fix server-side time_to_row regression (98ms -> 4ms)
