Skip to content

Worker Threads

VS Code extensions run on a single Node thread shared with language services, file watchers, and UI. If the extension host stalls, so does typing. CodeBuddy pushes every CPU-heavy operation to a worker so the main thread stays under the 16 ms frame budget.

The primary win isn’t throughput — worker spawn overhead is ~50 ms — it’s UI responsiveness. Big analyses no longer freeze the editor.

WorkerFileKindPurpose
Codebase Analysissrc/workers/codebase-analysis.worker.tsworker_threadsFull-project analysis + call graph
AST Analyzersrc/workers/ast-analyzer.worker.tsworker_threadsFile-level chunking for the vector index
Embeddingsrc/workers/embedding-worker.tsworker_threads (pooled)Google-GenAI embedding generation
Vector DBsrc/workers/vector-db-worker.tsDisabled stubLegacy LanceDB worker (WORKER_DISABLED = true). The live vector store is SQLite (sqlite-vector-store.ts + hnsw-index.ts) — see WASM & SQLite.
Chat Historysrc/services/chat-history-worker.tsAsync class (not real thread)SQLite chat persistence

The heaviest. Spawned for CodeBuddy: Analyze Codebase and auto-analysis triggers.

What it produces:

  1. Dependency analysispackage.json, requirements.txt, go.mod, Cargo.toml, pom.xml, composer.json, Pipfile, pyproject.toml.
  2. Framework detection — Express, NestJS, Fastify, Flask, Django, Spring, Gin, Actix, Laravel, etc.
  3. File-content analysis — endpoints, data models, schemas, snippets, import graph (Tree-sitter or regex fallback).
  4. Architecture — monolith / microservices / CLI / library + patterns (MVC, hexagonal, event-driven).
  5. Call graph — file import DAG → entry points, hot nodes, cycles.
  6. Middleware — chains, auth strategies (JWT, OAuth, Session, API Key), error handlers.

Message protocol:

Main → Worker:
{ type: "ANALYZE_CODEBASE", payload: { workspacePath, files, grammarsPath } }
{ type: "CANCEL" }
Worker → Main:
{ type: "ANALYSIS_PROGRESS", progress: { current, total, message } }
{ type: "ANALYSIS_COMPLETE", payload: AnalysisResult }
{ type: "ANALYSIS_ERROR", error }
{ type: "LOG", level, message, data }

Memory discipline: snippets capped at 30 files × 75 lines × 3000 chars. Call graph disposed after summary extraction. Import arrays zeroed post-graph. Tree-sitter analyzer disposed in a finally to release WASM. Paths relativized at serialization boundary — no absolute paths leak into LLM context.

Cancellation: service passes a CancellationToken; worker checks isCancelled between phases.

Security: file-path regexes use [^\\/]* instead of .* to avoid catastrophic backtracking. TOML parsing is a line-by-line state machine so # inside URLs/quoted values doesn’t break parsing.

Lightweight. Chunks single files for the vector index.

SettingValue
Chunk size1000 chars
Overlap200 chars
Min chunk50 chars (smaller discarded)
Chunk ID{filePath}::{byteOffset}

Uses web-tree-sitter for AST-aware chunking; falls back to text splitter if WASM fails. No data loss — just less precise chunk boundaries.

Protocol:

Main → Worker: { type: "INDEX_FILE", data: { filePath, content } }
Worker → Main: { type: "RESULT", data: { filePath, chunks[] } }
Worker → Main: { type: "ERROR", error }

Only pooled worker. Up to min(4, os.cpus().length) concurrent.

sequenceDiagram participant S as WorkerEmbeddingService participant W1 as Worker 1 participant W2 as Worker 2 participant W3 as Worker 3 participant API as Google GenAI Note over S: Pool boot (ping-verify each worker) S->>W1: generateBatch (items 0-9) S->>W2: generateBatch (items 10-19) S->>W3: generateBatch (items 20-29) par W1->>API: embed and W2->>API: embed and W3->>API: embed end W1-->>S: progress 50% W2-->>S: progress 30% W1-->>S: { success, data } W3-->>S: progress 80% W2-->>S: { success, data } W3-->>S: { success, data }

How it works:

  1. Init min(4, os.cpus().length) workers, ping-verify each.
  2. Round-robin batch assignment (workerId % workers.length).
  3. Each worker iterates its batch, 100 ms between items (rate-limit protection).
  4. Progress messages aggregate into overall progress (“batch 3/10 at 60%”).
  5. Per-item error isolation — one failure doesn’t kill the batch.
  6. 30 s task timeout — silent worker → promise rejects.

Simulates worker semantics (request/response IDs + busy-checking) without spawning a real thread. Keeps SQLite writes off the hot path.

Operations: GET_CHAT_HISTORY, SAVE_CHAT_HISTORY, ADD_CHAT_MESSAGE, GET_RECENT_HISTORY, CLEAR_CHAT_HISTORY, CLEANUP_OLD_HISTORY, SAVE_SUMMARY, GET_SUMMARY, GET_SESSIONS, CREATE_SESSION, GET_CURRENT_SESSION, SWITCH_SESSION, UPDATE_SESSION_TITLE, DELETE_SESSION, GET_SESSION_HISTORY.

Concurrency guard: single-request-at-a-time. Second concurrent request throws immediately — no WAL / mutex needed.

Workers can’t reach vscode.window or OutputChannel. WorkerLogger is a drop-in for the main Logger that serializes messages over parentPort.postMessage({ type: "LOG", ... }). Parent service forwards to the main Logger → OutputChannel + file + telemetry.

Medium codebase (~1500 files):

OperationWithout workersWith workersImprovement
Codebase analysis~12 s (UI frozen)~8 s (UI responsive)UI never blocks
Embedding 500 chunks~45 s (serial)~15 s (4 workers)~3× throughput
AST chunking 200 files~4 s (UI frozen)~3 s (UI responsive)UI never blocks
Chat history save~200 ms (blocks)~200 ms (async)Input never delayed
  • WASM — the Tree-sitter + sql.js runtimes workers use
  • Code indexing — the AST + embedding pipelines wired together
  • Semantic search — consumer of the indexed data