Skip to content
Merged
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
7 changes: 5 additions & 2 deletions packages/amazonq/src/app/inline/recommendationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ import { CancellationToken, InlineCompletionContext, Position, TextDocument } fr
import { LanguageClient } from 'vscode-languageclient'
import { SessionManager } from './sessionManager'
import { InlineGeneratingMessage } from './inlineGeneratingMessage'
import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer'
import { CodeWhispererStatusBarManager, inlineCompletionsDebounceDelay } from 'aws-core-vscode/codewhisperer'
import { TelemetryHelper } from './telemetryHelper'
import { debounce } from 'aws-core-vscode/utils'

export class RecommendationService {
constructor(
private readonly sessionManager: SessionManager,
private readonly inlineGeneratingMessage: InlineGeneratingMessage
) {}

async getAllRecommendations(
getAllRecommendations = debounce(this._getAllRecommendations.bind(this), inlineCompletionsDebounceDelay, true)

private async _getAllRecommendations(
languageClient: LanguageClient,
document: TextDocument,
position: Position,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { Position, CancellationToken, InlineCompletionItem } from 'vscode'
import assert from 'assert'
import { RecommendationService } from '../../../../../src/app/inline/recommendationService'
import { SessionManager } from '../../../../../src/app/inline/sessionManager'
import { createMockDocument } from 'aws-core-vscode/test'
import { createMockDocument, installFakeClock } from 'aws-core-vscode/test'
import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker'
import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage'
import { inlineCompletionsDebounceDelay } from 'aws-core-vscode/codewhisperer'

describe('RecommendationService', () => {
let languageClient: LanguageClient
Expand Down Expand Up @@ -119,5 +120,141 @@ describe('RecommendationService', () => {
const items2 = sessionManager.getActiveRecommendation()
assert.deepStrictEqual(items2, [mockInlineCompletionItemTwo, { insertText: '1' } as InlineCompletionItem])
})

describe('debounce functionality', () => {
let clock: ReturnType<typeof installFakeClock>

beforeEach(() => {
clock = installFakeClock()
})

afterEach(() => {
clock.uninstall()
})

it('debounces multiple rapid calls', async () => {
const mockResult = {
sessionId: 'test-session',
items: [mockInlineCompletionItemOne],
partialResultToken: undefined,
}

sendRequestStub.resolves(mockResult)

// Make multiple rapid calls
const promise1 = service.getAllRecommendations(
languageClient,
mockDocument,
mockPosition,
mockContext,
mockToken
)
const promise2 = service.getAllRecommendations(
languageClient,
mockDocument,
mockPosition,
mockContext,
mockToken
)
const promise3 = service.getAllRecommendations(
languageClient,
mockDocument,
mockPosition,
mockContext,
mockToken
)

// Verify that the promises are the same object (debounced)
assert.strictEqual(promise1, promise2)
assert.strictEqual(promise2, promise3)

await clock.tickAsync(inlineCompletionsDebounceDelay + 1000)

await promise1
await promise2
await promise3
})

it('allows new calls after debounce period', async () => {
const mockResult = {
sessionId: 'test-session',
items: [mockInlineCompletionItemOne],
partialResultToken: undefined,
}

sendRequestStub.resolves(mockResult)

const promise1 = service.getAllRecommendations(
languageClient,
mockDocument,
mockPosition,
mockContext,
mockToken
)

await clock.tickAsync(inlineCompletionsDebounceDelay + 1000)

await promise1

const promise2 = service.getAllRecommendations(
languageClient,
mockDocument,
mockPosition,
mockContext,
mockToken
)

assert.notStrictEqual(
promise1,
promise2,
'promises should be different when seperated by debounce period'
)

await clock.tickAsync(inlineCompletionsDebounceDelay + 1000)

await promise2
})

it('makes request with the last call', async () => {
const mockResult = {
sessionId: 'test-session',
items: [mockInlineCompletionItemOne],
partialResultToken: undefined,
}

sendRequestStub.resolves(mockResult)

const promise1 = service.getAllRecommendations(
languageClient,
mockDocument,
mockPosition,
mockContext,
mockToken
)

const promise2 = service.getAllRecommendations(
languageClient,
mockDocument,
{ line: 2, character: 2 } as Position,
mockContext,
mockToken
)

await clock.tickAsync(inlineCompletionsDebounceDelay + 1000)

await promise1
await promise2

const expectedRequestArgs = {
textDocument: {
uri: 'file:///test.py',
},
position: { line: 2, character: 2 } as Position,
context: mockContext,
}
const firstCallArgs = sendRequestStub.firstCall.args[1]
assert.deepStrictEqual(firstCallArgs, expectedRequestArgs)
})
})
})
})
10 changes: 2 additions & 8 deletions packages/core/src/codewhisperer/models/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,9 @@ export const securityScanLearnMoreUri = 'https://docs.aws.amazon.com/amazonq/lat
export const identityPoolID = 'us-east-1:70717e99-906f-4add-908c-bd9074a2f5b9'

/**
* the interval of the background thread invocation, which is triggered by the timer
* Delay for making requests once the user stops typing. Without a delay, inline suggestions request is triggered every keystroke.
*/
export const defaultCheckPeriodMillis = 1000 * 60 * 5
Copy link
Contributor Author

Choose a reason for hiding this comment

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

didn't see these used anymore, so I deleted for clarity.

Copy link
Contributor

Choose a reason for hiding this comment

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

these were probably missed when I was deleting code. One of the problems with exporting in typescript is that it makes it very hard to detect which exports are actually used


// suggestion show delay, in milliseconds
export const suggestionShowDelay = 250

// add 200ms more delay on top of inline default 30-50ms
export const inlineSuggestionShowDelay = 200
export const inlineCompletionsDebounceDelay = 25

export const referenceLog = 'Code Reference Log'

Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/shared/utilities/functionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ export function memoize<T, U extends any[]>(fn: (...args: U) => T): (...args: U)
*/
export function debounce<Input extends any[], Output>(
cb: (...args: Input) => Output | Promise<Output>,
delay: number = 0
delay: number = 0,
useLastCall: boolean = false
): (...args: Input) => Promise<Output> {
return cancellableDebounce(cb, delay).promise
return cancellableDebounce(cb, delay, useLastCall).promise
}

/**
Expand All @@ -104,10 +105,12 @@ export function debounce<Input extends any[], Output>(
*/
export function cancellableDebounce<Input extends any[], Output>(
cb: (...args: Input) => Output | Promise<Output>,
delay: number = 0
delay: number = 0,
useLastCall: boolean = false
): { promise: (...args: Input) => Promise<Output>; cancel: () => void } {
let timeout: Timeout | undefined
let promise: Promise<Output> | undefined
let lastestArgs: Input | undefined

const cancel = (): void => {
if (timeout) {
Expand All @@ -119,14 +122,16 @@ export function cancellableDebounce<Input extends any[], Output>(

return {
promise: (...args: Input) => {
lastestArgs = args
timeout?.refresh()

return (promise ??= new Promise<Output>((resolve, reject) => {
timeout = new Timeout(delay)
timeout.onCompletion(async () => {
timeout = promise = undefined
try {
resolve(await cb(...args))
const argsToUse = useLastCall ? lastestArgs! : args
resolve(await cb(...argsToUse))
} catch (err) {
reject(err)
}
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/test/shared/utilities/functionUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,33 @@ describe('debounce', function () {
assert.strictEqual(counter, 2)
})

describe('useLastCall option', function () {
let args: number[]
let clock: ReturnType<typeof installFakeClock>
let addToArgs: (i: number) => void

before(function () {
args = []
clock = installFakeClock()
addToArgs = (n: number) => args.push(n)
})

afterEach(function () {
clock.uninstall()
args.length = 0
})

it('only calls with the last args', async function () {
const debounced = debounce(addToArgs, 10, true)
const p1 = debounced(1)
const p2 = debounced(2)
const p3 = debounced(3)
await clock.tickAsync(100)
await Promise.all([p1, p2, p3])
assert.deepStrictEqual(args, [3])
})
})

describe('window rolling', function () {
let clock: ReturnType<typeof installFakeClock>
const calls: ReturnType<typeof fn>[] = []
Expand Down