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.
This commit is contained in:
David Snelling 2026-01-27 16:49:26 -08:00
parent 1ffdfa70ae
commit bf71317d21
2 changed files with 30 additions and 4 deletions

View file

@ -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<void>(resolve => setTimeout(resolve, 0))
}
}
return allResults
}
/**

View file

@ -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)