Skip to content

Commit 56ee5d6

Browse files
authored
Merge pull request #4232 from Kilo-Org/mark/adaptive-debounce-delay
feat(ghost): implement adaptive debounce delay for autocomplete
2 parents 63780cd + 4cf6fc6 commit 56ee5d6

File tree

2 files changed

+216
-2
lines changed

2 files changed

+216
-2
lines changed

src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,21 @@ import { ClineProvider } from "../../../core/webview/ClineProvider"
2929
import * as telemetry from "./AutocompleteTelemetry"
3030

3131
const MAX_SUGGESTIONS_HISTORY = 20
32-
const DEBOUNCE_DELAY_MS = 300
32+
33+
/**
34+
* Initial debounce delay in milliseconds.
35+
* This value is used as the starting debounce delay before enough latency samples
36+
* are collected. Once LATENCY_SAMPLE_SIZE samples are collected, the debounce delay
37+
* is dynamically adjusted to the average of recent request latencies.
38+
*/
39+
const INITIAL_DEBOUNCE_DELAY_MS = 300
40+
41+
/**
42+
* Number of latency samples to collect before using adaptive debounce delay.
43+
* Once this many samples are collected, the debounce delay becomes the average
44+
* of the stored latencies, updated after each request.
45+
*/
46+
const LATENCY_SAMPLE_SIZE = 10
3347

3448
export type { CostTrackingCallback, GhostPrompt, MatchingSuggestionResult, LLMRetrievalResult }
3549

@@ -118,6 +132,8 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
118132
private isFirstCall: boolean = true
119133
private ignoreController?: Promise<RooIgnoreController>
120134
private acceptedCommand: vscode.Disposable | null = null
135+
private debounceDelayMs: number = INITIAL_DEBOUNCE_DELAY_MS
136+
private latencyHistory: number[] = []
121137

122138
constructor(
123139
context: vscode.ExtensionContext,
@@ -240,6 +256,28 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
240256
}
241257
}
242258

259+
/**
260+
* Records a latency measurement and updates the adaptive debounce delay.
261+
* Maintains a rolling window of the last LATENCY_SAMPLE_SIZE latencies.
262+
* Once enough samples are collected, the debounce delay is set to the
263+
* average of all stored latencies.
264+
*
265+
* @param latencyMs - The latency of the most recent request in milliseconds
266+
*/
267+
public recordLatency(latencyMs: number): void {
268+
// Add the new latency to the history
269+
this.latencyHistory.push(latencyMs)
270+
271+
// Remove oldest if we exceed the sample size
272+
if (this.latencyHistory.length > LATENCY_SAMPLE_SIZE) {
273+
this.latencyHistory.shift()
274+
275+
// Once we have enough samples, update the debounce delay to the average
276+
const sum = this.latencyHistory.reduce((acc, val) => acc + val, 0)
277+
this.debounceDelayMs = Math.round(sum / this.latencyHistory.length)
278+
}
279+
}
280+
243281
public dispose(): void {
244282
if (this.debounceTimer !== null) {
245283
clearTimeout(this.debounceTimer)
@@ -432,7 +470,7 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
432470
// Remove this request from pending when done
433471
this.removePendingRequest(pendingRequest)
434472
resolve()
435-
}, DEBOUNCE_DELAY_MS)
473+
}, this.debounceDelayMs)
436474
})
437475

438476
// Complete the pending request object
@@ -482,6 +520,9 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
482520
telemetryContext,
483521
)
484522

523+
// Record latency for adaptive debounce delay
524+
this.recordLatency(latencyMs)
525+
485526
this.costTrackingCallback(result.cost, result.inputTokens, result.outputTokens)
486527

487528
// Always update suggestions, even if text is empty (for caching)

src/services/ghost/classic-auto-complete/__tests__/GhostInlineCompletionProvider.test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,6 +1954,179 @@ describe("GhostInlineCompletionProvider", () => {
19541954
})
19551955
})
19561956

1957+
describe("adaptive debounce delay", () => {
1958+
it("should start with initial debounce delay of 300ms", async () => {
1959+
let callCount = 0
1960+
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
1961+
callCount++
1962+
return {
1963+
cost: 0.01,
1964+
inputTokens: 100,
1965+
outputTokens: 50,
1966+
cacheWriteTokens: 0,
1967+
cacheReadTokens: 0,
1968+
}
1969+
})
1970+
1971+
// First call - executes immediately (leading edge)
1972+
await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken)
1973+
expect(callCount).toBe(1)
1974+
1975+
// Second call - should be debounced with initial 300ms delay
1976+
const mockDocument2 = new MockTextDocument(vscode.Uri.file("/test2.ts"), "const a = 1\nconst b = 2")
1977+
const mockPosition2 = new vscode.Position(0, 11)
1978+
const promise2 = provider.provideInlineCompletionItems(mockDocument2, mockPosition2, mockContext, mockToken)
1979+
1980+
// Should not have called yet (debounced)
1981+
expect(callCount).toBe(1)
1982+
1983+
// Advance 200ms - should still be waiting
1984+
await vi.advanceTimersByTimeAsync(200)
1985+
expect(callCount).toBe(1)
1986+
1987+
// Advance remaining 100ms to complete the 300ms debounce
1988+
await vi.advanceTimersByTimeAsync(100)
1989+
await promise2
1990+
expect(callCount).toBe(2)
1991+
})
1992+
1993+
it("should record latency and not update debounce delay until 10 samples collected", () => {
1994+
// Record 9 latencies - should not update debounce delay yet
1995+
for (let i = 0; i < 9; i++) {
1996+
provider.recordLatency(100 + i * 10) // 100, 110, 120, ..., 180
1997+
}
1998+
1999+
// Access private field via any cast for testing
2000+
const providerAny = provider as any
2001+
expect(providerAny.latencyHistory.length).toBe(9)
2002+
expect(providerAny.debounceDelayMs).toBe(300) // Still initial value
2003+
})
2004+
2005+
it("should update debounce delay to average after exceeding 10 samples", () => {
2006+
// Record 10 latencies of 200ms each - debounce delay not updated yet
2007+
for (let i = 0; i < 10; i++) {
2008+
provider.recordLatency(200)
2009+
}
2010+
2011+
// Access private field via any cast for testing
2012+
const providerAny = provider as any
2013+
expect(providerAny.latencyHistory.length).toBe(10)
2014+
expect(providerAny.debounceDelayMs).toBe(300) // Still initial value (not updated until > 10)
2015+
2016+
// Record 11th latency - now debounce delay is updated
2017+
provider.recordLatency(200)
2018+
expect(providerAny.latencyHistory.length).toBe(10) // Still 10 (oldest removed)
2019+
expect(providerAny.debounceDelayMs).toBe(200) // Now updated to average
2020+
})
2021+
2022+
it("should maintain rolling window of 10 latencies", () => {
2023+
// Record 15 latencies
2024+
for (let i = 0; i < 15; i++) {
2025+
provider.recordLatency(100 + i * 10) // 100, 110, 120, ..., 240
2026+
}
2027+
2028+
// Access private field via any cast for testing
2029+
const providerAny = provider as any
2030+
expect(providerAny.latencyHistory.length).toBe(10) // Only last 10 kept
2031+
2032+
// Last 10 values should be 150, 160, 170, 180, 190, 200, 210, 220, 230, 240
2033+
// Average = (150+160+170+180+190+200+210+220+230+240) / 10 = 195
2034+
expect(providerAny.debounceDelayMs).toBe(195)
2035+
})
2036+
2037+
it("should update debounce delay on each new latency after exceeding 10 samples", () => {
2038+
// Record 11 latencies of 200ms each (need > 10 to trigger update)
2039+
for (let i = 0; i < 11; i++) {
2040+
provider.recordLatency(200)
2041+
}
2042+
2043+
const providerAny = provider as any
2044+
expect(providerAny.debounceDelayMs).toBe(200)
2045+
2046+
// Add one more latency of 300ms
2047+
// New average = (200*9 + 300) / 10 = 210
2048+
provider.recordLatency(300)
2049+
expect(providerAny.debounceDelayMs).toBe(210)
2050+
2051+
// Add another latency of 400ms
2052+
// New average = (200*8 + 300 + 400) / 10 = 230
2053+
provider.recordLatency(400)
2054+
expect(providerAny.debounceDelayMs).toBe(230)
2055+
})
2056+
2057+
it("should use adaptive debounce delay after collecting enough samples", async () => {
2058+
let callCount = 0
2059+
vi.mocked(mockModel.generateResponse).mockImplementation(async () => {
2060+
callCount++
2061+
return {
2062+
cost: 0.01,
2063+
inputTokens: 100,
2064+
outputTokens: 50,
2065+
cacheWriteTokens: 0,
2066+
cacheReadTokens: 0,
2067+
}
2068+
})
2069+
2070+
// Record 11 latencies of 150ms each to set debounce delay to 150ms
2071+
// (need > 10 to trigger update)
2072+
for (let i = 0; i < 11; i++) {
2073+
provider.recordLatency(150)
2074+
}
2075+
2076+
const providerAny = provider as any
2077+
expect(providerAny.debounceDelayMs).toBe(150)
2078+
2079+
// First call - executes immediately (leading edge)
2080+
await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken)
2081+
expect(callCount).toBe(1)
2082+
2083+
// Second call - should be debounced with adaptive 150ms delay
2084+
const mockDocument2 = new MockTextDocument(vscode.Uri.file("/test2.ts"), "const a = 1\nconst b = 2")
2085+
const mockPosition2 = new vscode.Position(0, 11)
2086+
const promise2 = provider.provideInlineCompletionItems(mockDocument2, mockPosition2, mockContext, mockToken)
2087+
2088+
// Should not have called yet (debounced)
2089+
expect(callCount).toBe(1)
2090+
2091+
// Advance 100ms - should still be waiting (150ms debounce)
2092+
await vi.advanceTimersByTimeAsync(100)
2093+
expect(callCount).toBe(1)
2094+
2095+
// Advance remaining 50ms to complete the 150ms debounce
2096+
await vi.advanceTimersByTimeAsync(50)
2097+
await promise2
2098+
expect(callCount).toBe(2)
2099+
})
2100+
2101+
it("should record latency from LLM requests", async () => {
2102+
// Mock the model to simulate a delay
2103+
vi.mocked(mockModel.generateResponse).mockImplementation(async (_sys, _user, onChunk) => {
2104+
// Simulate some processing time
2105+
if (onChunk) {
2106+
onChunk({ type: "text", text: "<COMPLETION>" })
2107+
onChunk({ type: "text", text: "console.log('test');" })
2108+
onChunk({ type: "text", text: "</COMPLETION>" })
2109+
}
2110+
return {
2111+
cost: 0.01,
2112+
inputTokens: 100,
2113+
outputTokens: 50,
2114+
cacheWriteTokens: 0,
2115+
cacheReadTokens: 0,
2116+
}
2117+
})
2118+
2119+
const providerAny = provider as any
2120+
expect(providerAny.latencyHistory.length).toBe(0)
2121+
2122+
// Make a request that will record latency
2123+
await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken)
2124+
2125+
// Latency should have been recorded
2126+
expect(providerAny.latencyHistory.length).toBe(1)
2127+
})
2128+
})
2129+
19572130
describe("telemetry tracking", () => {
19582131
beforeEach(() => {
19592132
vi.mocked(telemetry.captureAcceptSuggestion).mockClear()

0 commit comments

Comments
 (0)