Skip to content

perf: json.load parse still stalls the event loop (~3 ms/MB) on video-detail read #1306

Description

@groupthinking

Follow-up from #1304, split out rather than widening that PR.

Problem

_read_video_analysis_sync in src/youtube_extension/backend/real_api_endpoints.py is dispatched via asyncio.to_thread. That successfully removes filesystem latency from the event loop, but not the parse:

  • open()/read() release the GIL → genuinely off-loop
  • json.load() is CPU-bound C code that holds the GIL → still stalls the loop for its full duration

Measured on CPython 3.12 / macOS arm64 by counting loop ticks during the work:

Work Loop ticks
to_thread(time.sleep, 0.30) 22,847
to_thread(open + json.load) 179
to_thread(json.loads) alone 3

Three ticks across the whole parse — the stall is effectively the entire wall time. Scales linearly at roughly 3 ms/MB: 1.58 MB → 4.61 ms, 8.02 MB → 23.07 ms, 24.42 MB → 68.02 ms.

Why it matters

Payload size is unbounded and grows with video duration. services/real_video_processor.py:294 persists the full transcript object into the cache entry, so entry size tracks video length — roughly 0.15 MB for an hour, ~1.6 MB long-form. Nothing caps it. A long enough video produces a proportionally long loop stall affecting every concurrent request.

There is also a regression case already disclosed in #1304: on a warm page cache with a small (0.10 MB) payload, the executor dispatch hop (~0.5 ms) costs more than the 0.4 ms parse it defers — 0.40 ms → 0.94 ms. The offload is a net negative in exactly the case where the filesystem is fast.

Options

  1. Cap entry size — cheapest. Reject or truncate cache entries above a threshold; bounds the stall by construction. Needs a decision on what to do with oversized entries.
  2. Stop storing the full transcript — attacks the cause rather than the symptom. Store a reference or a trimmed projection. Largest win, largest blast radius.
  3. Streaming/incremental parse — yields between chunks so the loop can interleave. Adds a dependency or hand-rolled parsing.
  4. Process pool — genuinely parallel, sidesteps the GIL, but adds IPC serialisation cost that may exceed the parse for small payloads.
  5. Size-conditional dispatch — parse inline below a threshold, offload above it. Fixes the warm-cache regression directly; needs the threshold measured, not guessed.

Options 1 and 5 are complementary and both cheap. Option 2 is the real fix.

Acceptance

Not in scope

Anything touching real_video_processor.py while PR #1237 holds it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions