Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>()` incurs heavy allocation and insertion overhead compared to a fixed-size byte array.
**Action:** Replace `Set<number>` 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<number>` 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<number>`), 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.
29 changes: 29 additions & 0 deletions language-server/benchmarks/results_burst.json
Original file line number Diff line number Diff line change
@@ -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
}
]
52 changes: 38 additions & 14 deletions language-server/src/core/search-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ export class SearchEngine implements ISearchProvider {
// Map Scope -> Array of Indices
private readonly scopedIndices: Map<SearchScope, number[]> = new Map();

// ⚡ Bolt: Fast Dense Integer Set Tracking
// Reusable arrays for O(1) visited checks instead of new Set<number>() 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<string, number[]> = new Map();
private readonly itemIndexById: Map<string, number> = new Map();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -1658,16 +1672,25 @@ export class SearchEngine implements ISearchProvider {
const heap = new MinHeap<SearchResult>(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<number>() : 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();
Expand Down Expand Up @@ -1785,16 +1808,17 @@ export class SearchEngine implements ISearchProvider {
context: ReturnType<typeof this.prepareSearchContext>,
heap: MinHeap<SearchResult>,
token?: CancellationToken,
visited?: Set<number>,
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);
}
Expand All @@ -1804,12 +1828,12 @@ export class SearchEngine implements ISearchProvider {
context: ReturnType<typeof this.prepareSearchContext>,
heap: MinHeap<SearchResult>,
token?: CancellationToken,
visited?: Set<number>,
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);
Expand Down
Loading