-
Notifications
You must be signed in to change notification settings - Fork 298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Autocomplete: Overhaul telemetry and fix various corner cases #412
Changes from all commits
cdf9f9a
6016a89
59cb305
64c1978
77757b2
ab7e49e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,7 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
private readonly config: Required<CodyCompletionItemProviderConfig> | ||
|
||
private requestManager: RequestManager | ||
private previousCompletionLogId?: string | ||
|
||
constructor({ | ||
responsePercentage = 0.1, | ||
|
@@ -134,13 +135,12 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
tracer?.({ error: error.toString() }) | ||
this.stopLoading() | ||
|
||
if (isAbortError(error)) { | ||
return { items: [] } | ||
if (!isAbortError(error)) { | ||
console.error(error) | ||
debug('CodyCompletionProvider:inline:error', `${error.toString()}\n${error.stack}`) | ||
} | ||
|
||
console.error(error) | ||
debug('CodyCompletionProvider:inline:error', `${error.toString()}\n${error.stack}`) | ||
return { items: [] } | ||
return emptyCompletions() | ||
} | ||
} | ||
|
||
|
@@ -160,15 +160,13 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
this.abortOpenCompletions = () => abortController.abort() | ||
} | ||
|
||
CompletionLogger.clear() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clearing the completion logger will flush all "open" completions, potentially logging completions that are being visible. This is used to measure for how long completions are necessary and thus we want to delay it as long as possible so we can get the most accurate timing. Previously, every new completion request would immediately flush the queue. Now, however, a cache hit that matches the properties that I outlined in the PR description and is this "completed forward" won't, so we'll have a more accurate number of how long such completions are visible. |
||
|
||
if (!vscode.window.activeTextEditor || document.uri.scheme === 'cody') { | ||
return { items: [] } | ||
return emptyCompletions() | ||
} | ||
|
||
const docContext = getCurrentDocContext(document, position, this.maxPrefixChars, this.maxSuffixChars) | ||
if (!docContext) { | ||
return { items: [] } | ||
return emptyCompletions() | ||
} | ||
|
||
const { prefix, suffix, prevNonEmptyLine } = docContext | ||
|
@@ -188,6 +186,36 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
this.config.providerConfig.enableExtendedMultilineTriggers | ||
) | ||
|
||
// Don't show completions if we are in the process of writing a word. | ||
const cursorAtWord = /\w$/.test(sameLinePrefix) | ||
if (cursorAtWord && !this.config.triggerMoreEagerly) { | ||
return emptyCompletions() | ||
} | ||
const triggeredMoreEagerly = this.config.triggerMoreEagerly && cursorAtWord | ||
|
||
let triggeredForSuggestWidgetSelection: string | undefined | ||
if (context.selectedCompletionInfo) { | ||
if (this.config.completeSuggestWidgetSelection) { | ||
triggeredForSuggestWidgetSelection = context.selectedCompletionInfo.text | ||
} else { | ||
// Don't show completions if the suggest widget (which shows language autocomplete) | ||
// is showing. | ||
return emptyCompletions() | ||
} | ||
} | ||
|
||
// If we have a suffix in the same line as the cursor and the suffix contains any word | ||
// characters, do not attempt to make a completion. This means we only make completions if | ||
// we have a suffix in the same line for special characters like `)]}` etc. | ||
// | ||
// VS Code will attempt to merge the remainder of the current line by characters but for | ||
// words this will easily get very confusing. | ||
if (/\w/.test(sameLineSuffix)) { | ||
return emptyCompletions() | ||
} | ||
|
||
let cachedCompletions: CachedCompletions | undefined | ||
|
||
// Avoid showing completions when we're deleting code (Cody can only insert code at the | ||
// moment) | ||
const lastChange = this.lastContentChanges.get(document.fileName) ?? 'add' | ||
|
@@ -196,36 +224,69 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
// untruncated prefix matches. This fixes some weird issues where the completion would | ||
// render if you insert whitespace but not on the original place when you delete it | ||
// again | ||
const cachedCompletions = this.config.cache?.get(prefix, false) | ||
if (cachedCompletions?.isExactPrefix) { | ||
tracer?.({ cacheHit: true }) | ||
return this.handleCacheHit( | ||
cachedCompletions, | ||
document, | ||
context, | ||
position, | ||
prefix, | ||
suffix, | ||
multiline, | ||
document.languageId, | ||
abortController.signal | ||
) | ||
cachedCompletions = this.config.cache?.get(prefix, false) | ||
if (cachedCompletions && !cachedCompletions.isExactPrefix) { | ||
return emptyCompletions() | ||
} | ||
return { items: [] } | ||
} | ||
|
||
const cachedCompletions = this.config.cache?.get(prefix) | ||
// If cachedCompletions was already set by the above logic, we don't have to query the cache | ||
// again. | ||
cachedCompletions = cachedCompletions ?? this.config.cache?.get(prefix) | ||
|
||
// We create a log entry after determining if we have a potential cache hit. This is | ||
// necessary to make sure that typing text of a displayed completion will not log a new | ||
// completion on every keystroke | ||
// | ||
// However we only log a completion as started if it's either served from cache _or_ the | ||
// debounce interval has passed to ensure we don't log too many start events where we end up | ||
// not doing any work at all | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The completion logger now has a separate I don't expect this to have a big impact on the overall latencies, certainly not over 50ms, but we should aim to be as close to the real-user experience as possible with these logs. This won't change the overall amount of events logged, though, because nothing is being logged until the |
||
const useLogIdFromPreviousCompletion = | ||
cachedCompletions?.logId && cachedCompletions?.logId === this.previousCompletionLogId | ||
Comment on lines
+244
to
+245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line makes sure that "forwarding completions" don't get a new analytics event assigned. In the future, we might be able to do that with the new |
||
if (!useLogIdFromPreviousCompletion) { | ||
CompletionLogger.clear() | ||
} | ||
const logId = useLogIdFromPreviousCompletion | ||
? cachedCompletions!.logId | ||
: CompletionLogger.create({ | ||
multiline, | ||
providerIdentifier: this.config.providerConfig.identifier, | ||
languageId: document.languageId, | ||
triggeredMoreEagerly, | ||
triggeredForSuggestWidgetSelection: triggeredForSuggestWidgetSelection !== undefined, | ||
settings: { | ||
autocompleteExperimentalTriggerMoreEagerly: this.config.triggerMoreEagerly, | ||
autocompleteExperimentalCompleteSuggestWidgetSelection: Boolean( | ||
this.config.completeSuggestWidgetSelection | ||
), | ||
}, | ||
}) | ||
this.previousCompletionLogId = logId | ||
|
||
if (cachedCompletions) { | ||
// When we serve a completion from the cache and create a new log | ||
// id, we want to ensure to only refer to the new id for future | ||
// cache retrievals. If we don't do this, every subsequent cache hit | ||
// would otherwise no longer match the previous completion ID and we | ||
// would log a new completion each time, even if the user just | ||
// continues typing on the currently displayed completion. | ||
if (logId !== cachedCompletions.logId) { | ||
this.config.cache?.updateLogId(cachedCompletions.logId, logId) | ||
} | ||
Comment on lines
+267
to
+275
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty much a corner case but updating the ids in our cache is necessary in the following scenario:
To fix this, we update the cache to refer to the new log id's whenever they're changed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is working as you described, I think this comment from me here isn't a blocker, but if we take a step back maybe we should have some centralised system about logId. It looks like that we're trying to sync log and cache IDs in order to avoid double logging when cache is actually the same, maybe we could use some cache encode function for this kind of purpose? In this case there won't be possible to have different ids if prefixes are the same. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I don't like this bookkeeping either. It's going to be interesting to see what @sqs will come up in #442 where he's currently seemingly not using a cache at all! The only reason why we need to keep the log id around is to make sure we can detect cases where the same completion is resolve again after some suffix changes. Maybe we can instead just store the whole completion somewhere and compare that instead of the generated ID? |
||
|
||
tracer?.({ cacheHit: true }) | ||
return this.handleCacheHit( | ||
cachedCompletions, | ||
CompletionLogger.start(logId) | ||
return this.prepareCompletions( | ||
logId, | ||
cachedCompletions.completions, | ||
document, | ||
context, | ||
position, | ||
prefix, | ||
suffix, | ||
multiline, | ||
document.languageId, | ||
true, | ||
abortController.signal | ||
) | ||
} | ||
|
@@ -234,34 +295,6 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
const completers: Provider[] = [] | ||
let timeout: number | ||
|
||
// Don't show completions if we are in the process of writing a word. | ||
const cursorAtWord = /\w$/.test(sameLinePrefix) | ||
if (cursorAtWord && !this.config.triggerMoreEagerly) { | ||
return { items: [] } | ||
} | ||
const triggeredMoreEagerly = this.config.triggerMoreEagerly && cursorAtWord | ||
|
||
let triggeredForSuggestWidgetSelection: string | undefined | ||
if (context.selectedCompletionInfo) { | ||
if (this.config.completeSuggestWidgetSelection) { | ||
triggeredForSuggestWidgetSelection = context.selectedCompletionInfo.text | ||
} else { | ||
// Don't show completions if the suggest widget (which shows language autocomplete) | ||
// is showing. | ||
return { items: [] } | ||
} | ||
} | ||
|
||
// If we have a suffix in the same line as the cursor and the suffix contains any word | ||
// characters, do not attempt to make a completion. This means we only make completions if | ||
// we have a suffix in the same line for special characters like `)]}` etc. | ||
// | ||
// VS Code will attempt to merge the remainder of the current line by characters but for | ||
// words this will easily get very confusing. | ||
if (/\w/.test(sameLineSuffix)) { | ||
return { items: [] } | ||
} | ||
|
||
const sharedProviderOptions: Omit<ProviderOptions, 'id' | 'n' | 'multiline'> = { | ||
prefix, | ||
suffix, | ||
|
@@ -310,9 +343,11 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
// We don't need to make a request at all if the signal is already aborted after the | ||
// debounce | ||
if (abortController.signal.aborted) { | ||
return { items: [] } | ||
return emptyCompletions() | ||
} | ||
|
||
CompletionLogger.start(logId) | ||
|
||
const contextResult = await this.config.contextFetcher({ | ||
document, | ||
prefix, | ||
|
@@ -324,25 +359,12 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
isEmbeddingsContextEnabled: this.config.isEmbeddingsContextEnabled, | ||
}) | ||
if (abortController.signal.aborted) { | ||
return { items: [] } | ||
return emptyCompletions() | ||
} | ||
tracer?.({ context: contextResult }) | ||
|
||
const logId = CompletionLogger.start({ | ||
type: 'inline', | ||
multiline, | ||
providerIdentifier: this.config.providerConfig.identifier, | ||
languageId: document.languageId, | ||
contextSummary: contextResult.logSummary, | ||
triggeredMoreEagerly, | ||
triggeredForSuggestWidgetSelection: triggeredForSuggestWidgetSelection !== undefined, | ||
settings: { | ||
autocompleteExperimentalTriggerMoreEagerly: this.config.triggerMoreEagerly, | ||
autocompleteExperimentalCompleteSuggestWidgetSelection: Boolean( | ||
this.config.completeSuggestWidgetSelection | ||
), | ||
}, | ||
}) | ||
CompletionLogger.networkRequestStarted(logId, contextResult.logSummary) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We previously appended the context summary in the completion started event. Now, however, we log these events before the context is fetched so I decided to split this into a separate event. We haven't used any of these yet, though. |
||
|
||
const stopLoading = this.config.statusBar.startLoading('Completions are being generated') | ||
this.stopLoading = stopLoading | ||
|
||
|
@@ -363,31 +385,36 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
tracer ? createCompletionProviderTracer(tracer) : undefined | ||
) | ||
|
||
// Shared post-processing logic | ||
const processedCompletions = processCompletions(completions, prefix, suffix, multiline, document.languageId) | ||
stopLoading() | ||
|
||
if (processedCompletions.length > 0) { | ||
CompletionLogger.suggest(logId) | ||
return toInlineCompletionItems(logId, document, position, processedCompletions) | ||
} | ||
|
||
CompletionLogger.noResponse(logId) | ||
return { items: [] } | ||
return this.prepareCompletions( | ||
logId, | ||
completions, | ||
document, | ||
context, | ||
position, | ||
prefix, | ||
suffix, | ||
multiline, | ||
document.languageId, | ||
false, | ||
abortController.signal | ||
) | ||
} | ||
|
||
private async handleCacheHit( | ||
cachedCompletions: CachedCompletions, | ||
private async prepareCompletions( | ||
logId: string, | ||
completions: Completion[], | ||
document: vscode.TextDocument, | ||
context: vscode.InlineCompletionContext, | ||
position: vscode.Position, | ||
prefix: string, | ||
suffix: string, | ||
multiline: boolean, | ||
languageId: string, | ||
isCacheHit: boolean, | ||
abortSignal: AbortSignal | ||
): Promise<vscode.InlineCompletionList> { | ||
const results = processCompletions(cachedCompletions.completions, prefix, suffix, multiline, languageId) | ||
const results = processCompletions(completions, prefix, suffix, multiline, languageId) | ||
|
||
// We usually resolve cached results instantly. However, if the inserted completion would | ||
// include more than one line, this can create a lot of visible UI churn. To avoid this, we | ||
|
@@ -398,6 +425,7 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
// have network latency for cache completion | ||
const visibleResult = results[0] | ||
if ( | ||
isCacheHit && | ||
visibleResult?.content.includes('\n') && | ||
!this.config.disableTimeouts && | ||
context.triggerKind !== vscode.InlineCompletionTriggerKind.Invoke | ||
|
@@ -408,7 +436,30 @@ export class CodyCompletionItemProvider implements vscode.InlineCompletionItemPr | |
} | ||
} | ||
|
||
return toInlineCompletionItems(cachedCompletions.logId, document, position, results) | ||
if (results.length > 0) { | ||
// When the VS Code completion popup is open and we suggest a completion that does not match | ||
// the currently selected completion, VS Code won't display it. For now we make sure to not | ||
// log these completions as displayed. | ||
// | ||
// TODO: Take this into account when creating the completion prefix. | ||
let isCompletionVisible = true | ||
if (context.selectedCompletionInfo) { | ||
const currentText = document.getText(context.selectedCompletionInfo.range) | ||
const selectedText = context.selectedCompletionInfo.text | ||
if (!(currentText + results[0].content).startsWith(selectedText)) { | ||
isCompletionVisible = false | ||
} | ||
} | ||
|
||
if (isCompletionVisible) { | ||
CompletionLogger.suggest(logId, isCompletionVisible) | ||
} | ||
|
||
return toInlineCompletionItems(logId, document, position, results) | ||
} | ||
|
||
CompletionLogger.noResponse(logId) | ||
return emptyCompletions() | ||
} | ||
} | ||
|
||
|
@@ -488,6 +539,11 @@ function createTracerForInvocation(tracer: ProvideInlineCompletionItemsTracer): | |
} | ||
} | ||
|
||
function emptyCompletions(): vscode.InlineCompletionList { | ||
CompletionLogger.clear() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whenever we early return, we want to flush the completions queue. This conceptually makes sense because if we return an empty array at any point, it means no completions form us are visible anymore anyways. |
||
return { items: [] } | ||
} | ||
|
||
function createCompletionProviderTracer(tracer: SingleInvocationTracer): CompletionProviderTracer { | ||
return { | ||
params: data => tracer({ completionProviderCallParams: data }), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why 3 expect calls? Just to make sure that there is no toggle state behind the
CompleationsCache
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I wanted to make sure all occurrences are updated.