Skip to content
Closed
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
91 changes: 91 additions & 0 deletions language-server/apply_patch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/bin/bash
patch -p1 << 'PATCH'
--- a/src/core/search-engine.ts
+++ b/src/core/search-engine.ts
@@ -183,6 +183,9 @@
topIndices: number[];
} | null = null;

+ private visitedBuffer: Uint8Array = new Uint8Array(0);
+ private visitedTracker: number[] = [];
+
public itemsMap: Map<string, SearchableItem> = new Map();
private readonly fileItemByNormalizedPath: Map<string, SearchableItem> = new Map();
private activityWeight: number = 0.3;
@@ -1631,16 +1634,29 @@
const searchContext = this.prepareSearchContext(query, scope);
// ⚡ Bolt: Fast integer ID tracking using Uint8Array instead of Set
const preferredIndices = this.getPreferredIndicesForQuery(scope, query, indices);
- const visited = preferredIndices.length > 0 ? new Uint8Array(this.items.length) : undefined;
-
- if (preferredIndices.length > 0) {
- this.searchWithIndices(preferredIndices, searchContext, heap, token, visited);
+
+ const useVisited = preferredIndices.length > 0;
+ if (useVisited && this.visitedBuffer.length < this.items.length) {
+ this.visitedBuffer = new Uint8Array(this.items.length);
}

- if (indices) {
- this.searchWithIndices(indices, searchContext, heap, token, visited);
- } else {
- this.searchAllItems(searchContext, heap, token, visited);
+ try {
+ if (preferredIndices.length > 0) {
+ this.searchWithIndices(preferredIndices, searchContext, heap, token, useVisited);
+ }
+
+ if (indices) {
+ this.searchWithIndices(indices, searchContext, heap, token, useVisited);
+ } else {
+ this.searchAllItems(searchContext, heap, token, useVisited);
+ }
+ } finally {
+ if (useVisited) {
+ for (let i = 0; i < this.visitedTracker.length; i++) {
+ this.visitedBuffer[this.visitedTracker[i]] = 0;
+ }
+ this.visitedTracker.length = 0;
+ }
}

const results = heap.getSorted();
@@ -1776,16 +1792,17 @@
context: ReturnType<typeof this.prepareSearchContext>,
heap: MinHeap<SearchResult>,
token?: CancellationToken,
- visited?: Uint8Array,
+ useVisited: 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[i] === 1) {
+ if (useVisited) {
+ if (this.visitedBuffer[i] === 1) {
continue;
}
- visited[i] = 1;
+ this.visitedBuffer[i] = 1;
+ this.visitedTracker.push(i);
}
this.processItemForSearch(i, context, heap);
}
@@ -1796,12 +1813,12 @@
context: ReturnType<typeof this.prepareSearchContext>,
heap: MinHeap<SearchResult>,
token?: CancellationToken,
- visited?: Uint8Array,
+ useVisited: boolean = false,
): void {
const count = context.items.length;
for (let i = 0; i < count; i++) {
if (i % 500 === 0 && token?.isCancellationRequested) break;
- if (visited && visited[i] === 1) {
+ if (useVisited && this.visitedBuffer[i] === 1) {
continue;
}
this.processItemForSearch(i, context, heap);
}
PATCH
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.022927290000000086,
"totalMs": 2.2927290000000085,
"minMs": 0.005182999999988169,
"maxMs": 0.22028199999999742,
"p95Ms": 0.07572100000004411,
"stdDevMs": 0.03326143676686062
},
{
"name": "Burst Search \"Zzz\" (No match)",
"avgMs": 25.31047841,
"totalMs": 2531.047841,
"minMs": 21.281904999999824,
"maxMs": 58.99728899999991,
"p95Ms": 50.09768800000029,
"stdDevMs": 7.7481483316893245
},
{
"name": "Burst Search \"S\" (Many matches)",
"avgMs": 0.6536615299999994,
"totalMs": 65.36615299999994,
"minMs": 0.5750079999997979,
"maxMs": 0.8889819999999418,
"p95Ms": 0.7484010000002854,
"stdDevMs": 0.05215705758937176
}
]
43 changes: 30 additions & 13 deletions language-server/src/core/search-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ export class SearchEngine implements ISearchProvider {
topIndices: number[];
} | null = null;

private visitedBuffer: Uint8Array = new Uint8Array(0);
private visitedTracker: number[] = [];
Comment on lines +186 to +187
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset reusable visited state when clearing engine state.

clear() (Line 756 onward) doesn’t reset the new visitedBuffer/visitedTracker, so a previously large Uint8Array can stay retained after a full clear. Please clear these alongside other caches.

💡 Proposed fix
     clear(): void {
         this.items = [];
@@
         this.inactiveFileItems = [];
+        this.visitedBuffer = new Uint8Array(0);
+        this.visitedTracker = [];
         this.invalidateDerivedCaches();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@language-server/src/core/search-engine.ts` around lines 186 - 187, The
clear() method currently doesn't reset the reusable visited state, leaving a
previously large Uint8Array retained; update the clear() implementation to
explicitly reset the visitedBuffer to a new Uint8Array(0) and visitedTracker to
an empty array (visitedBuffer = new Uint8Array(0); visitedTracker = [];),
alongside the existing cache/field resets so the engine truly frees that memory
when clearing state.


public itemsMap: Map<string, SearchableItem> = new Map();
private readonly fileItemByNormalizedPath: Map<string, SearchableItem> = new Map();
private activityWeight: number = 0.3;
Expand Down Expand Up @@ -1628,16 +1631,29 @@ export class SearchEngine implements ISearchProvider {
const searchContext = this.prepareSearchContext(query, scope);
// ⚡ Bolt: Fast integer ID tracking using Uint8Array instead of Set
const preferredIndices = this.getPreferredIndicesForQuery(scope, query, indices);
const visited = preferredIndices.length > 0 ? new Uint8Array(this.items.length) : undefined;

if (preferredIndices.length > 0) {
this.searchWithIndices(preferredIndices, searchContext, heap, token, visited);
const useVisited = preferredIndices.length > 0;
if (useVisited && this.visitedBuffer.length < this.items.length) {
this.visitedBuffer = new Uint8Array(this.items.length);
}

if (indices) {
this.searchWithIndices(indices, searchContext, heap, token, visited);
} else {
this.searchAllItems(searchContext, heap, token, visited);
try {
if (preferredIndices.length > 0) {
this.searchWithIndices(preferredIndices, searchContext, heap, token, useVisited);
}

if (indices) {
this.searchWithIndices(indices, searchContext, heap, token, useVisited);
} else {
this.searchAllItems(searchContext, heap, token, useVisited);
}
} finally {
if (useVisited) {
for (let i = 0; i < this.visitedTracker.length; i++) {
this.visitedBuffer[this.visitedTracker[i]] = 0;
}
this.visitedTracker.length = 0;
}
}

const results = heap.getSorted();
Expand Down Expand Up @@ -1760,16 +1776,17 @@ export class SearchEngine implements ISearchProvider {
context: ReturnType<typeof this.prepareSearchContext>,
heap: MinHeap<SearchResult>,
token?: CancellationToken,
visited?: Uint8Array,
useVisited: 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[i] === 1) {
if (useVisited) {
if (this.visitedBuffer[i] === 1) {
continue;
}
visited[i] = 1;
this.visitedBuffer[i] = 1;
this.visitedTracker.push(i);
}
this.processItemForSearch(i, context, heap);
}
Expand All @@ -1779,12 +1796,12 @@ export class SearchEngine implements ISearchProvider {
context: ReturnType<typeof this.prepareSearchContext>,
heap: MinHeap<SearchResult>,
token?: CancellationToken,
visited?: Uint8Array,
useVisited: boolean = false,
): void {
const count = context.items.length;
for (let i = 0; i < count; i++) {
if (i % 500 === 0 && token?.isCancellationRequested) break;
if (visited && visited[i] === 1) {
if (useVisited && this.visitedBuffer[i] === 1) {
continue;
}
this.processItemForSearch(i, context, heap);
Expand Down
Loading