From 3469777cd984ad972ac32bacd26569bb73779811 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 00:17:51 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[Fast=20Dense=20Integer=20S?= =?UTF-8?q?et=20Tracking]=20Replaced=20Set=20with=20Uint8Array=20f?= =?UTF-8?q?or=20visited=20indices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: AhmmedSamier <17784876+AhmmedSamier@users.noreply.github.com> --- .jules/bolt.md | 5 ++ language-server/benchmarks/results_burst.json | 29 +++++++++++ language-server/src/core/search-engine.ts | 52 ++++++++++++++----- 3 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 language-server/benchmarks/results_burst.json diff --git a/.jules/bolt.md b/.jules/bolt.md index 8b81675..f32d621 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -48,3 +48,8 @@ **Learning:** When keeping track of seen integer IDs that are dense and bounded (e.g. from 0 to N), using `new Set()` incurs heavy allocation and insertion overhead compared to a fixed-size byte array. **Action:** Replace `Set` with `new Uint8Array(maxIndex)` and use `array[id] = 1` to track presence, which is ~15x faster and avoids garbage collection pauses in hot paths. (Benchmark context: `N=100,000` IDs, `bun` version 1.2.14, Linux x86_64, Intel Xeon 2.30GHz, 4 cores, 8GB RAM, averaged over 100 iterations comparing `Set` addition vs `new Uint8Array(maxIndex)` indexed assignment `array[id] = 1`). + +## 2026-05-04 - [Fast Tracker Reset with Reusable TypedArray] + +**Learning:** When using a pre-allocated `Uint8Array` to track visited items (replacing `Set`), doing an O(N) operation like `array.fill(0)` to reset it defeats the purpose if N is large. Furthermore, if you don't reset the array properly, or if an error happens before the array is reset, subsequent searches will skip valid items. +**Action:** Use a separate array (e.g. `visitedIndicesTracker = []`) to push exactly which indices were set to 1. Then, wrap the entire operation that uses the buffer in a `try/finally` block. In the `finally` block, iterate over the tracker array, set those specific indices back to 0 in the buffer, and set `tracker.length = 0`. This guarantees an O(K) reset (where K is the number of visited items) and prevents cross-request state corruption. diff --git a/language-server/benchmarks/results_burst.json b/language-server/benchmarks/results_burst.json new file mode 100644 index 0000000..c8e3a37 --- /dev/null +++ b/language-server/benchmarks/results_burst.json @@ -0,0 +1,29 @@ +[ + { + "name": "Burst Search \"App\" (Matches found)", + "avgMs": 0.02318719999999985, + "totalMs": 2.318719999999985, + "minMs": 0.005485999999905289, + "maxMs": 0.20758200000000215, + "p95Ms": 0.055799999999976535, + "stdDevMs": 0.03360928002963113 + }, + { + "name": "Burst Search \"Zzz\" (No match)", + "avgMs": 20.81479831, + "totalMs": 2081.479831, + "minMs": 18.808266999999887, + "maxMs": 24.533892000000037, + "p95Ms": 23.05574999999999, + "stdDevMs": 1.4177265596926898 + }, + { + "name": "Burst Search \"S\" (Many matches)", + "avgMs": 0.5768155999999999, + "totalMs": 57.68155999999999, + "minMs": 0.4981480000001284, + "maxMs": 0.8213659999996707, + "p95Ms": 0.6419660000001386, + "stdDevMs": 0.049091876996666985 + } +] \ No newline at end of file diff --git a/language-server/src/core/search-engine.ts b/language-server/src/core/search-engine.ts index 7c8516a..27a3175 100644 --- a/language-server/src/core/search-engine.ts +++ b/language-server/src/core/search-engine.ts @@ -169,6 +169,11 @@ export class SearchEngine implements ISearchProvider { // Map Scope -> Array of Indices private readonly scopedIndices: Map = new Map(); + // ⚡ Bolt: Fast Dense Integer Set Tracking + // Reusable arrays for O(1) visited checks instead of new Set() per query. + private visitedIndicesBuffer: Uint8Array = new Uint8Array(0); + private visitedIndicesTracker: number[] = []; + // Map Normalized File Path -> Array of Item Indices (Reverse Index for O(1) lookup) private readonly fileToItemIndices: Map = new Map(); private readonly itemIndexById: Map = new Map(); @@ -242,6 +247,8 @@ export class SearchEngine implements ISearchProvider { this.preparedCache.clear(); this.lastRelativeInput = null; this.lastRelativeOutput = null; + this.visitedIndicesBuffer = new Uint8Array(items.length); + this.visitedIndicesTracker = []; this.filePaths = []; this.invalidateDerivedCaches(); for (const item of items) { @@ -279,6 +286,10 @@ export class SearchEngine implements ISearchProvider { const newNameLengths = new Uint16Array(newCapacity); newNameLengths.set(this.itemNameLengths); this.itemNameLengths = newNameLengths; + + const newVisitedIndicesBuffer = new Uint8Array(newCapacity); + newVisitedIndicesBuffer.set(this.visitedIndicesBuffer); + this.visitedIndicesBuffer = newVisitedIndicesBuffer; } // Append items @@ -466,6 +477,7 @@ export class SearchEngine implements ISearchProvider { this.itemTypeIds = this.itemTypeIds.slice(0, newCount); this.itemBitflags = this.itemBitflags.slice(0, newCount); this.itemNameBitflags = this.itemNameBitflags.slice(0, newCount); + this.visitedIndicesBuffer = this.visitedIndicesBuffer.slice(0, newCount); this.preparedNames.length = newCount; this.preparedFullNames.length = newCount; this.preparedPaths.length = newCount; @@ -759,6 +771,8 @@ export class SearchEngine implements ISearchProvider { this.itemBitflags = new Uint32Array(0); this.itemNameBitflags = new Uint32Array(0); this.itemNameLengths = new Uint16Array(0); + this.visitedIndicesBuffer = new Uint8Array(0); + this.visitedIndicesTracker = []; this.preparedNames = []; this.preparedFullNames = []; this.preparedPaths = []; @@ -1658,16 +1672,25 @@ export class SearchEngine implements ISearchProvider { const heap = new MinHeap(maxResults, (a, b) => a.score - b.score); const searchContext = this.prepareSearchContext(query, scope); const preferredIndices = this.getPreferredIndicesForQuery(scope, query, indices); - const visited = preferredIndices.length > 0 ? new Set() : undefined; + const trackVisited = preferredIndices.length > 0; - if (preferredIndices.length > 0) { - this.searchWithIndices(preferredIndices, searchContext, heap, token, visited); - } + try { + if (preferredIndices.length > 0) { + this.searchWithIndices(preferredIndices, searchContext, heap, token, trackVisited); + } - if (indices) { - this.searchWithIndices(indices, searchContext, heap, token, visited); - } else { - this.searchAllItems(searchContext, heap, token, visited); + if (indices) { + this.searchWithIndices(indices, searchContext, heap, token, trackVisited); + } else { + this.searchAllItems(searchContext, heap, token, trackVisited); + } + } finally { + if (trackVisited) { + for (let i = 0; i < this.visitedIndicesTracker.length; i++) { + this.visitedIndicesBuffer[this.visitedIndicesTracker[i]] = 0; + } + this.visitedIndicesTracker.length = 0; + } } const results = heap.getSorted(); @@ -1785,16 +1808,17 @@ export class SearchEngine implements ISearchProvider { context: ReturnType, heap: MinHeap, token?: CancellationToken, - visited?: Set, + trackVisited: boolean = false, ): void { for (let j = 0; j < indices.length; j++) { if (j % 500 === 0 && token?.isCancellationRequested) break; const i = indices[j]; - if (visited) { - if (visited.has(i)) { + if (trackVisited) { + if (this.visitedIndicesBuffer[i] === 1) { continue; } - visited.add(i); + this.visitedIndicesBuffer[i] = 1; + this.visitedIndicesTracker.push(i); } this.processItemForSearch(i, context, heap); } @@ -1804,12 +1828,12 @@ export class SearchEngine implements ISearchProvider { context: ReturnType, heap: MinHeap, token?: CancellationToken, - visited?: Set, + trackVisited: boolean = false, ): void { const count = context.items.length; for (let i = 0; i < count; i++) { if (i % 500 === 0 && token?.isCancellationRequested) break; - if (visited?.has(i)) { + if (trackVisited && this.visitedIndicesBuffer[i] === 1) { continue; } this.processItemForSearch(i, context, heap);