From bf71317d216d29c43a0e8b9d3be647ff40d3d540 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Tue, 27 Jan 2026 16:49:26 -0800 Subject: [PATCH] fix: prevent WASM embedding from blocking event loop during highlight() embedBatch() with large inputs (e.g. 500 chunks from highlight()) runs a single synchronous WASM forward pass that blocks the event loop for 200-500ms. Split large batches into micro-batches of 20 with setTimeout(0) yields between each, keeping max blocking per batch to ~10-30ms. Also change Cargo.toml opt-level from "z" (size) to 3 (speed) for ~15-20% faster WASM inference. Requires WASM rebuild to take effect. --- src/embeddings/EmbeddingManager.ts | 32 ++++++++++++++++++++++++--- src/embeddings/candle-wasm/Cargo.toml | 2 +- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/embeddings/EmbeddingManager.ts b/src/embeddings/EmbeddingManager.ts index 1f9bfe6b..91b7db0d 100644 --- a/src/embeddings/EmbeddingManager.ts +++ b/src/embeddings/EmbeddingManager.ts @@ -212,6 +212,10 @@ export class EmbeddingManager { * Uses the engine's embedBatch() for a single WASM forward pass * instead of N individual embed() calls. * + * Large batches (>MICRO_BATCH_SIZE) are split into micro-batches + * with event loop yielding between each, preventing the synchronous + * WASM call from blocking the server for hundreds of milliseconds. + * * @param texts Array of strings to embed * @returns Array of embedding vectors (384 dimensions each) */ @@ -230,9 +234,31 @@ export class EmbeddingManager { } await this.init() - const results = await this.engine.embedBatch(texts) - this.embedCount += texts.length - return results + + // Small batches: single WASM call (no overhead) + const MICRO_BATCH_SIZE = 20 + if (texts.length <= MICRO_BATCH_SIZE) { + const results = await this.engine.embedBatch(texts) + this.embedCount += texts.length + return results + } + + // Large batches: micro-batch with event loop yielding + // Each micro-batch of ~20 texts blocks ~10-30ms, then yields + // so other requests (HTTP, timers, I/O) can proceed + const allResults: number[][] = [] + for (let i = 0; i < texts.length; i += MICRO_BATCH_SIZE) { + const batch = texts.slice(i, i + MICRO_BATCH_SIZE) + const batchResults = await this.engine.embedBatch(batch) + allResults.push(...batchResults) + this.embedCount += batch.length + + // Yield to event loop between micro-batches + if (i + MICRO_BATCH_SIZE < texts.length) { + await new Promise(resolve => setTimeout(resolve, 0)) + } + } + return allResults } /** diff --git a/src/embeddings/candle-wasm/Cargo.toml b/src/embeddings/candle-wasm/Cargo.toml index 6c9d3390..10538fec 100644 --- a/src/embeddings/candle-wasm/Cargo.toml +++ b/src/embeddings/candle-wasm/Cargo.toml @@ -45,7 +45,7 @@ getrandom = { version = "0.3", features = ["wasm_js"] } wasm-bindgen-test = "0.3" [profile.release] -opt-level = "z" # Optimize for size +opt-level = 3 # Optimize for speed (~15-20% faster inference, ~1MB larger binary) lto = true # Link-time optimization codegen-units = 1 # Single codegen unit for better optimization panic = "abort" # Abort on panic (smaller binary)