Skip to content

perf(lsp): lazy load vulnerability checks in background#84

Merged
mpiton merged 1 commit into
mainfrom
feature/lazy-vuln-loading
Jan 8, 2026
Merged

perf(lsp): lazy load vulnerability checks in background#84
mpiton merged 1 commit into
mainfrom
feature/lazy-vuln-loading

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented Jan 8, 2026

Summary

  • Move vulnerability fetching from OSV.dev to a background task using tokio::spawn
  • Display version hints immediately without blocking on security checks
  • Reduce perceived latency from 3-5s to 0-2s for initial display

Changes

  • Add fetch_vulnerabilities_background() static method that runs independently
  • Store document state and publish diagnostics before vulnerability check
  • Refresh inlay hints immediately after registry fetch completes
  • Spawn background task for vulnerability check (non-blocking)
  • Background task refreshes both inlay hints and diagnostics when complete

Performance Impact

Before: User must wait 3-5 seconds to see ANY version information (registry + OSV blocking)
After: Version info shown in 0-2s, vulnerability badges appear 1-3 seconds later (progressive UI)

Test plan

  • Build passes (cargo build)
  • All 199 unit tests pass (cargo test --lib)
  • Clippy passes (cargo clippy --all-targets -- -D warnings)
  • Formatting correct (cargo fmt --all -- --check)

Closes #72

Summary by CodeRabbit

Release Notes

  • Performance

    • Vulnerability security checks now run asynchronously in the background, eliminating blocking operations during document processing
  • Changes

    • All background operation logging now prefixed with "Background:" for clarity
    • Document diagnostics and state now published immediately upon processing

✏️ Tip: You can customize this high-level summary in your review settings.

Move vulnerability fetching from OSV.dev to a background task using
tokio::spawn, allowing version hints to display immediately without
blocking on security checks.

Key changes:
- Add fetch_vulnerabilities_background() static method
- Store document state and publish diagnostics before vuln check
- Refresh inlay hints immediately after registry fetch
- Spawn background task for vulnerability check
- Background task refreshes UI when complete

This reduces perceived latency from 3-5s to 0-2s for initial display.

Closes #72
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

The vulnerability fetching mechanism is refactored to execute asynchronously in the background rather than blocking the foreground document processing. The fetch_vulnerabilities method is renamed to fetch_vulnerabilities_background, its signature changed to accept shared resources as parameters instead of referencing self, and spawned as a detached background task after document state is published. This decouples vulnerability checks from document processing, enabling immediate UI response.

Changes

Cohort / File(s) Summary
Background Vulnerability Fetch Refactoring
dependi-lsp/src/backend.rs
Method fetch_vulnerabilities renamed to fetch_vulnerabilities_background with updated signature accepting Vec<Dependency>, cache, osv_client, vuln_cache, and client as parameters instead of referencing self. All references to self.vuln_cache, self.osv_client, and self.client replaced with passed parameters. OSV queries and cache updates now execute in spawned background task. Log messages prefixed with "Background:". Diagnostics publishing moved before background task spawn, making state updates immediate. Background task spawning added conditionally in process_document after inlay hints refresh.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant Backend
    participant Cache
    participant OSV as OSV Client
    participant UI as LSP Client (UI)

    Note over Client,UI: Old Flow (Synchronous)
    Client->>Backend: process_document()
    Backend->>Backend: Fetch version hints
    Backend->>Cache: Query vuln_cache
    Backend->>OSV: Query vulnerable packages
    OSV-->>Backend: Vulnerability data
    Backend->>Cache: Update version_cache
    Backend->>UI: Refresh inlay hints
    Backend->>UI: Publish diagnostics
    Backend-->>Client: Return after all done

    Note over Client,UI: New Flow (Background)
    Client->>Backend: process_document()
    Backend->>Backend: Fetch version hints
    Backend->>Cache: Store DocumentState
    Backend->>UI: Publish diagnostics (immediate)
    Backend->>UI: Refresh inlay hints
    Backend-->>Client: Return immediately
    
    rect rgb(200, 220, 240)
    Note over Backend,UI: Background Task (async, detached)
    Backend->>Cache: Query vuln_cache
    Backend->>OSV: Batch query packages
    OSV-->>Backend: Vulnerability data
    Backend->>Cache: Update version_cache
    Backend->>UI: Refresh inlay hints (2nd)
    Backend->>UI: Refresh diagnostics (2nd)
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

Poem

🐰 A flutter of tasks, background bound,
No waiting now, just swift and sound,
Hints bloom quick while checks run free,
Async harmony—responsive spree! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: moving vulnerability checks to background execution for performance improvement.
Linked Issues check ✅ Passed The changes implement all core coding requirements from #72: non-blocking vulnerability checks via tokio::spawn, immediate diagnostics publishing, background task with cache updates, and UI refresh triggers.
Out of Scope Changes check ✅ Passed All changes directly support the lazy-loading objective; no unrelated modifications detected beyond the vulnerability fetching refactoring.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
dependi-lsp/src/backend.rs (4)

221-238: Non-atomic read-modify-write on cache entry.

The pattern get → mutate → insert (lines 221-238) is not atomic. If another task modifies the same cache key between these operations, updates could be lost. Since vulnerability enrichment is the only writer after version fetch completes, the practical risk is low, but consider using an atomic update method if HybridCache exposes one.


254-263: Consider logging when UI refresh requests fail.

The .ok() calls silently discard errors from the refresh requests. While fire-and-forget is appropriate here, a debug-level log on failure would help diagnose client communication issues.

🔧 Suggested improvement
-                client
-                    .send_request::<request::InlayHintRefreshRequest>(())
-                    .await
-                    .ok();
-                client
-                    .send_request::<request::WorkspaceDiagnosticRefresh>(())
-                    .await
-                    .ok();
+                if let Err(e) = client
+                    .send_request::<request::InlayHintRefreshRequest>(())
+                    .await
+                {
+                    tracing::debug!("Background: Failed to refresh inlay hints: {}", e);
+                }
+                if let Err(e) = client
+                    .send_request::<request::WorkspaceDiagnosticRefresh>(())
+                    .await
+                {
+                    tracing::debug!("Background: Failed to refresh diagnostics: {}", e);
+                }

404-423: Unnecessary clone of dependencies.

dependencies is not used after the spawn block, so it can be moved directly into the closure instead of cloned.

♻️ Minor optimization
         // Fetch vulnerabilities from OSV.dev in BACKGROUND (non-blocking)
         if security_enabled && !dependencies.is_empty() {
-            let dependencies_clone = dependencies.clone();
             let cache_clone = Arc::clone(&self.version_cache);
             let osv_client_clone = Arc::clone(&self.osv_client);
             let vuln_cache_clone = Arc::clone(&self.vuln_cache);
             let client_clone = self.client.clone();

             tokio::spawn(async move {
                 Self::fetch_vulnerabilities_background(
-                    dependencies_clone,
+                    dependencies,
                     file_type,
                     cache_clone,
                     osv_client_clone,
                     vuln_cache_clone,
                     client_clone,
                 )
                 .await;
             });
         }

363-378: Config extraction tuple growing - consider a struct for clarity.

The 4-element tuple (diagnostics_enabled, security_show_diags, min_severity, security_enabled) is approaching readability limits. A small config struct would make field access self-documenting.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2152a3 and a3b975d.

📒 Files selected for processing (1)
  • dependi-lsp/src/backend.rs
🧰 Additional context used
🧬 Code graph analysis (1)
dependi-lsp/src/backend.rs (3)
dependi-lsp/src/vulnerabilities/cache.rs (2)
  • new (31-37)
  • new (64-71)
dependi-lsp/src/vulnerabilities/osv.rs (1)
  • new (31-41)
dependi-lsp/src/file_types.rs (1)
  • cache_key (86-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: security-audit
  • GitHub Check: test
  • GitHub Check: Analyze (rust)
🔇 Additional comments (3)
dependi-lsp/src/backend.rs (3)

171-178: Good refactor to static method for background task compatibility.

The signature change from &self to explicit Arc parameters is the right approach for detached tokio::spawn tasks, avoiding lifetime issues with self. The owned Vec<Dependency> parameter prevents borrowing complications.


353-402: Immediate state publication before background work - good UX pattern.

Document state is stored and diagnostics/inlay hints are published immediately after version fetch completes. This ensures users see version information in ~0–2s as specified in the PR objectives, with vulnerability badges appearing later.


412-422: Fire-and-forget spawn aligns with requirements but consider shutdown implications.

The detached tokio::spawn correctly allows background vulnerability fetching without blocking. Per PR objectives, the task should "continue operating if the document is closed." However, during LSP shutdown, these tasks will be aborted when the tokio runtime drops. This is likely acceptable, but verify this doesn't cause issues in practice (e.g., partial cache writes, log spam).

@mpiton mpiton merged commit 4d9fefd into main Jan 8, 2026
7 checks passed
@mpiton mpiton deleted the feature/lazy-vuln-loading branch January 10, 2026 12:39
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.

perf: Lazy loading for vulnerability checks

1 participant