Skip to content

Reuse SignatureSolver across videos (cache per player JS + preprocessed player) - #137

Open
Matth-93 wants to merge 6 commits into
alexeichhorn:mainfrom
Matth-93:perf/signature-solver-cache
Open

Matth-93 wants to merge 6 commits into
alexeichhorn:mainfrom
Matth-93:perf/signature-solver-cache

Conversation

@Matth-93

@Matth-93 Matth-93 commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Extraction.applySignature constructs a fresh SignatureSolver for every video: a new JSContext, re-evaluation of the meriyah/astring UMD bundles, and a full parse of the ~2 MB player JS on the first solve. On platforms where JavaScriptCore has no JIT (tvOS/iOS), that first parse dominates — it cost ~15 s per video in our measurements on Apple TV, paid again for every title.

Since the player JS changes only every few days, this work is almost entirely redundant across videos in a session.

Fix

  • SignatureSolver.shared(forJS:) caches one instance keyed by the player-JS hash and reuses it across videos.
  • The first batchSolve requests output_preprocessed and stores the returned preprocessed player; subsequent solves send it back via the preprocessed_player input type and skip the full parse.
  • Solves are serialized with a lock (a shared JSContext isn't thread-safe).
  • Task.checkCancellation() runs before the expensive solver work so a cancelled extraction doesn't burn CPU.

Measurements (Apple TV, tvOS)

before after
first extraction of session ~18–25 s ~6 s (one-time parse)
subsequent extractions ~18–25 s each ~1.7–2.5 s

Verification

Builds clean across the CI matrix locally, including explicit Swift 6 language mode (nonisolated(unsafe) cache guarded by NSLock).


Note

Overview

Reusable signature solving. Reuses SignatureSolver instances across videos through a bounded four-entry, most-recently-used cache keyed by the exact player JavaScript, avoiding repeated JSContext setup for recurring player versions.

Preprocessed players. Stores the preprocessed player returned by the first batchSolve and uses it for subsequent signature and n challenge resolution. Solver access is serialized to protect the shared JavaScript context.

Cancellation handling. Checks for cancellation around expensive solver work and propagates CancellationError directly, preventing cancellation from being treated as stale player JavaScript and triggering an unnecessary cache refresh.

…path

applySignature built a fresh SignatureSolver per video: JSContext setup,
meriyah/astring bundle eval, and a full parse of the ~2MB player JS every
time — devastating on JIT-less JavaScriptCore (tvOS/iOS), where it costs
15s+ per video.

- SignatureSolver.shared(forJS:) caches one solver per player-JS version
- First batchSolve requests output_preprocessed and stores the
  preprocessed player; subsequent solves send it back and skip the parse
- Solves serialized with a lock (JSContext is not thread-safe)
- Task.checkCancellation before solver work so cancelled extractions
  don't burn CPU
- Timing os_log (.default) around init and solve
@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

SignatureSolver now caches instances by player JavaScript hash and serializes batch solving per instance. It reuses previously computed player preprocessing and checks task cancellation during extraction and solving. applySignature now obtains solvers through the shared factory while retaining the existing batch-solving and manifest mutation flow.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: reusing SignatureSolver instances with per-player JavaScript caching and preprocessed-player reuse.
Description check ✅ Passed The description directly explains the performance problem, caching fix, serialized solver access, cancellation handling, and verification results.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/YouTubeKit/SignatureSolver.swift`:
- Around line 23-37: Update SignatureSolver.shared(forJS:) to compare
cached.playerJS directly with js instead of computing and storing js.hashValue,
eliminating collision-prone caching. While holding sharedLock and before
SignatureSolver initialization, check for task cancellation and abort using the
existing throwing cancellation mechanism.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 235617a3-b1fb-4339-8a86-5189a00b0d60

📥 Commits

Reviewing files that changed from the base of the PR and between 7cc8190 and dfd0a0d.

📒 Files selected for processing (2)
  • Sources/YouTubeKit/Extraction.swift
  • Sources/YouTubeKit/SignatureSolver.swift

Comment thread Sources/YouTubeKit/SignatureSolver.swift Outdated
… init

Avoids the O(N) hashValue computation and its (rare) collision risk that
could return a wrong solver; checks task cancellation inside the lock
before the expensive SignatureSolver init.
Comment thread Sources/YouTubeKit/Extraction.swift
The solver's checkCancellation throws CancellationError out of
applySignature, which the retry catch treated as a stale-player-JS
failure — clearing the shared JS cache and retrying. Propagate
cancellation immediately instead, so a cancelled extraction stops
cleanly without churning the cache for concurrent extractions.
Comment thread Sources/YouTubeKit/SignatureSolver.swift Outdated
A single-entry cache means alternating between two player-JS variants in
one session (e.g. web vs TV/embed) evicts and rebuilds each time. Keep a
small MRU list keyed by player JS so each variant's prepared solver is
reused, bounded to cap JSContext/player memory.
Comment thread Sources/YouTubeKit/SignatureSolver.swift
Matth-93 and others added 2 commits July 14, 2026 11:57
Matches the existing pattern for the __js caches; the package supports
swift-tools 5.8, where the unconditional attribute fails to parse.
Comment on lines +253 to +256
} catch is CancellationError {
// Cancellation is not a stale-JS failure — propagate it
// immediately instead of clearing the cache and retrying.
throw CancellationError()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancellation is swallowed by the outer extraction-method retry

When methods contains another entry after .local, throwing here does not propagate immediately: the enclosing Task.retry(with: methods) catches every error, including CancellationError, and proceeds to the next extraction method. A cancelled extraction can therefore start the remote WebSocket fallback and potentially return or wait for it instead of terminating promptly.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants