diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 92107158a..44b537d1e 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -585,6 +585,9 @@ export interface CopilotCompletionContextParams { uri: string; caretOffset: number; featureFlag: CopilotCompletionContextFeatures; + maxSnippetCount: number; + maxSnippetLength: number; + doAggregateSnippets: boolean; } // Requests @@ -843,7 +846,7 @@ export interface Client { getIncludes(uri: vscode.Uri, maxDepth: number): Promise; getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise; filesEncodingChanged(filesEncodingChanged: FilesEncodingChanged): void; - getCompletionContext(fileName: vscode.Uri, caretOffset: number, featureFlag: CopilotCompletionContextFeatures, token: vscode.CancellationToken): Promise; + getCompletionContext(fileName: vscode.Uri, caretOffset: number, featureFlag: CopilotCompletionContextFeatures, maxSnippetCount: number, maxSnippetLength: number, doAggregateSnippets: boolean, token: vscode.CancellationToken): Promise; } export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { @@ -2352,11 +2355,12 @@ export class DefaultClient implements Client { } public async getCompletionContext(file: vscode.Uri, caretOffset: number, featureFlag: CopilotCompletionContextFeatures, + maxSnippetCount: number, maxSnippetLength: number, doAggregateSnippets: boolean, token: vscode.CancellationToken): Promise { await withCancellation(this.ready, token); return DefaultClient.withLspCancellationHandling( () => this.languageClient.sendRequest(CopilotCompletionContextRequest, - { uri: file.toString(), caretOffset, featureFlag }, token), token); + { uri: file.toString(), caretOffset, featureFlag, maxSnippetCount, maxSnippetLength, doAggregateSnippets }, token), token); } /** @@ -4277,5 +4281,5 @@ class NullClient implements Client { getIncludes(uri: vscode.Uri, maxDepth: number): Promise { return Promise.resolve({} as GetIncludesResult); } getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise { return Promise.resolve({} as ChatContextResult); } filesEncodingChanged(filesEncodingChanged: FilesEncodingChanged): void { } - getCompletionContext(file: vscode.Uri, caretOffset: number, featureFlag: CopilotCompletionContextFeatures, token: vscode.CancellationToken): Promise { return Promise.resolve({} as CopilotCompletionContextResult); } + getCompletionContext(file: vscode.Uri, caretOffset: number, featureFlag: CopilotCompletionContextFeatures, maxSnippetCount: number, maxSnippetLength: number, doAggregateSnippets: boolean, token: vscode.CancellationToken): Promise { return Promise.resolve({} as CopilotCompletionContextResult); } } diff --git a/Extension/src/LanguageServer/copilotCompletionContextProvider.ts b/Extension/src/LanguageServer/copilotCompletionContextProvider.ts index 17ec88028..d255f24a3 100644 --- a/Extension/src/LanguageServer/copilotCompletionContextProvider.ts +++ b/Extension/src/LanguageServer/copilotCompletionContextProvider.ts @@ -6,7 +6,7 @@ import { ContextResolver, ResolveRequest, SupportedContextItem } from '@github/c import { randomUUID } from 'crypto'; import * as vscode from 'vscode'; import { DocumentSelector } from 'vscode-languageserver-protocol'; -import { isNumber, isString } from '../common'; +import { isBoolean, isNumber, isString } from '../common'; import { getOutputChannelLogger, Logger } from '../logger'; import * as telemetry from '../telemetry'; import { CopilotCompletionContextResult } from './client'; @@ -75,11 +75,21 @@ export class CopilotCompletionContextProvider implements ContextResolver = new Map(); private static readonly defaultCppDocumentSelector: DocumentSelector = [{ language: 'cpp' }, { language: 'c' }, { language: 'cuda-cpp' }]; - // A percentage expressed as an integer number, i.e. 50 means 50%. - private static readonly defaultTimeBudgetFactor: number = 50; - private static readonly defaultMaxCaretDistance = 4096; + // The default time budget for providing a value from resolve(). + private static readonly defaultTimeBudgetMs: number = 7; + // Assume the cache is stale when the distance to the current caret is greater than this value. + private static readonly defaultMaxCaretDistance = 8192; + private static readonly defaultMaxSnippetCount = 15; + private static readonly defaultMaxSnippetLength = 10 * 1024; // 10KB + private static readonly defaultDoAggregateSnippets = true; private completionContextCancellation = new vscode.CancellationTokenSource(); private contextProviderDisposable: vscode.Disposable | undefined; + static readonly CppContextProviderEnabledFeatures = 'enabledFeatures'; + static readonly CppContextProviderTimeBudgetMs = 'timeBudgetMs'; + static readonly CppContextProviderMaxSnippetCount = 'maxSnippetCount'; + static readonly CppContextProviderMaxSnippetLength = 'maxSnippetLength'; + static readonly CppContextProviderMaxDistanceToCaret = 'maxDistanceToCaret'; + static readonly CppContextProviderDoAggregateSnippets = 'doAggregateSnippets'; constructor(private readonly logger: Logger) { } @@ -125,7 +135,8 @@ export class CopilotCompletionContextProvider implements ContextResolver { const documentUri = context.documentContext.uri; const caretOffset = context.documentContext.offset; @@ -143,7 +154,7 @@ export class CopilotCompletionContextProvider implements ContextResolver"}:${copilotC telemetry.send("cache"); } } - static readonly CppCodeSnippetsEnabledFeatures = 'CppCodeSnippetsEnabledFeatures'; - static readonly CppCodeSnippetsTimeBudgetFactor = 'CppCodeSnippetsTimeBudgetFactor'; - static readonly CppCodeSnippetsMaxDistanceToCaret = 'CppCodeSnippetsMaxDistanceToCaret'; - private async fetchTimeBudgetFactor(context: ResolveRequest): Promise { + static readonly paramsCache: Record = {}; + static paramsCacheCreated = false; + private getContextProviderParam(paramName: string): T | undefined { try { - const budgetFactor = context.activeExperiments.get(CopilotCompletionContextProvider.CppCodeSnippetsTimeBudgetFactor); - return (isNumber(budgetFactor) ? budgetFactor : CopilotCompletionContextProvider.defaultTimeBudgetFactor) / 100.0; + if (!CopilotCompletionContextProvider.paramsCacheCreated) { + CopilotCompletionContextProvider.paramsCacheCreated = true; + const paramsJson = new CppSettings().cppContextProviderParams; + if (isString(paramsJson)) { + try { + const params = JSON.parse(paramsJson.replaceAll(/'/g, '"')); + for (const key in params) { + CopilotCompletionContextProvider.paramsCache[key] = params[key]; + } + } catch (e) { + console.warn(`getContextProviderParam(): error parsing getContextProviderParam: `, e); + } + } + } + return CopilotCompletionContextProvider.paramsCache[paramName] as T; + } catch (e) { + console.warn(`getContextProviderParam(): error fetching getContextProviderParam: `, e); + return undefined; + } + } + + private async fetchTimeBudgetMs(context: ResolveRequest): Promise { + try { + const timeBudgetMs = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs); + return isNumber(timeBudgetMs) ? timeBudgetMs : CopilotCompletionContextProvider.defaultTimeBudgetMs; } catch (e) { - console.warn(`fetchTimeBudgetFactor(): error fetching ${CopilotCompletionContextProvider.CppCodeSnippetsTimeBudgetFactor}, using default: `, e); - return CopilotCompletionContextProvider.defaultTimeBudgetFactor; + console.warn(`fetchTimeBudgetMs(): error fetching ${CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs}, using default: `, e); + return CopilotCompletionContextProvider.defaultTimeBudgetMs; } } private async fetchMaxDistanceToCaret(context: ResolveRequest): Promise { try { - const maxDistance = context.activeExperiments.get(CopilotCompletionContextProvider.CppCodeSnippetsMaxDistanceToCaret); + const maxDistance = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret); return isNumber(maxDistance) ? maxDistance : CopilotCompletionContextProvider.defaultMaxCaretDistance; } catch (e) { - console.warn(`fetchMaxDistanceToCaret(): error fetching ${CopilotCompletionContextProvider.CppCodeSnippetsMaxDistanceToCaret}, using default: `, e); + console.warn(`fetchMaxDistanceToCaret(): error fetching ${CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret}, using default: `, e); return CopilotCompletionContextProvider.defaultMaxCaretDistance; } } + private async fetchMaxSnippetCount(context: ResolveRequest): Promise { + try { + const maxSnippetCount = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxSnippetCount) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxSnippetCount); + return isNumber(maxSnippetCount) ? maxSnippetCount : CopilotCompletionContextProvider.defaultMaxSnippetCount; + } catch (e) { + console.warn(`fetchMaxSnippetCount(): error fetching ${CopilotCompletionContextProvider.defaultMaxSnippetCount}, using default: `, e); + return CopilotCompletionContextProvider.defaultMaxSnippetCount; + } + } + + private async fetchMaxSnippetLength(context: ResolveRequest): Promise { + try { + const maxSnippetLength = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxSnippetLength) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxSnippetLength); + return isNumber(maxSnippetLength) ? maxSnippetLength : CopilotCompletionContextProvider.defaultMaxSnippetLength; + } catch (e) { + console.warn(`fetchMaxSnippetLength(): error fetching ${CopilotCompletionContextProvider.defaultMaxSnippetLength}, using default: `, e); + return CopilotCompletionContextProvider.defaultMaxSnippetLength; + } + } + + private async fetchDoAggregateSnippets(context: ResolveRequest): Promise { + try { + const doAggregateSnippets = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderDoAggregateSnippets) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderDoAggregateSnippets); + return isBoolean(doAggregateSnippets) ? doAggregateSnippets : CopilotCompletionContextProvider.defaultDoAggregateSnippets; + } catch (e) { + console.warn(`fetchDoAggregateSnippets(): error fetching ${CopilotCompletionContextProvider.defaultDoAggregateSnippets}, using default: `, e); + return CopilotCompletionContextProvider.defaultDoAggregateSnippets; + } + } + private async getEnabledFeatureNames(context: ResolveRequest): Promise { try { - const enabledFeatureNames = new CppSettings().cppCodeSnippetsFeatureNames ?? context.activeExperiments.get(CopilotCompletionContextProvider.CppCodeSnippetsEnabledFeatures); + const enabledFeatureNames = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderEnabledFeatures) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderEnabledFeatures); if (isString(enabledFeatureNames)) { return enabledFeatureNames.split(',').map(s => s.trim()); } } catch (e) { - console.warn(`getEnabledFeatures(): error fetching ${CopilotCompletionContextProvider.CppCodeSnippetsEnabledFeatures}: `, e); + console.warn(`getEnabledFeatureNames(): error fetching ${CopilotCompletionContextProvider.CppContextProviderEnabledFeatures}: `, e); } return undefined; } @@ -251,11 +320,32 @@ response.uri:${copilotCompletionContext.sourceFileUri || ""}:${copilotC this.completionContextCache.delete(fileUri); } + private computeSnippetsResolved: boolean = true; + + private async resolveResultAndKind(context: ResolveRequest, featureFlag: CopilotCompletionContextFeatures, + telemetry: CopilotCompletionContextTelemetry, defaultValue: CopilotCompletionContextResult | undefined, + resolveStartTime: number, timeBudgetMs: number, maxSnippetCount: number, maxSnippetLength: number, doAggregateSnippets: boolean, + copilotCancel: vscode.CancellationToken): Promise<[CopilotCompletionContextResult | undefined, CopilotCompletionKind]> { + if (this.computeSnippetsResolved) { + this.computeSnippetsResolved = false; + const computeSnippetsPromise = this.getCompletionContextWithCancellation(context, featureFlag, + maxSnippetCount, maxSnippetLength, doAggregateSnippets, resolveStartTime, telemetry.fork(), this.completionContextCancellation.token).finally( + () => this.computeSnippetsResolved = true + ); + const res = await this.waitForCompletionWithTimeoutAndCancellation( + computeSnippetsPromise, defaultValue, timeBudgetMs, copilotCancel); + return res; + } else { return [defaultValue, defaultValue ? CopilotCompletionKind.GotFromCache : CopilotCompletionKind.MissingCacheMiss]; } + } + public async resolve(context: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise { const resolveStartTime = performance.now(); - let logMessage = `Copilot: resolve(${context.documentContext.uri}:${context.documentContext.offset}):`; - const timeBudgetFactor = await this.fetchTimeBudgetFactor(context); + let logMessage = `Copilot: resolve(${context.documentContext.uri}: ${context.documentContext.offset}):`; + const cppTimeBudgetMs = await this.fetchTimeBudgetMs(context); const maxCaretDistance = await this.fetchMaxDistanceToCaret(context); + const maxSnippetCount = await this.fetchMaxSnippetCount(context); + const maxSnippetLength = await this.fetchMaxSnippetLength(context); + const doAggregateSnippets = await this.fetchDoAggregateSnippets(context); const telemetry = new CopilotCompletionContextTelemetry(); let copilotCompletionContext: CopilotCompletionContextResult | undefined; let copilotCompletionContextKind: CopilotCompletionKind = CopilotCompletionKind.Unknown; @@ -265,16 +355,12 @@ response.uri:${copilotCompletionContext.sourceFileUri || ""}:${copilotC try { featureFlag = await this.getEnabledFeatureFlag(context); telemetry.addRequestMetadata(context.documentContext.uri, context.documentContext.offset, - context.completionId, context.documentContext.languageId, { featureFlag, timeBudgetFactor, maxCaretDistance }); + context.completionId, context.documentContext.languageId, { featureFlag, timeBudgetMs: cppTimeBudgetMs, maxCaretDistance }); if (featureFlag === undefined) { return []; } - this.completionContextCancellation.cancel(); - this.completionContextCancellation = new vscode.CancellationTokenSource(); const cacheEntry: CacheEntry | undefined = this.completionContextCache.get(docUri.toString()); const defaultValue = cacheEntry?.[1]; - const computeSnippetsPromise = this.getCompletionContextWithCancellation(context, featureFlag, - resolveStartTime, telemetry.fork(), this.completionContextCancellation.token); - [copilotCompletionContext, copilotCompletionContextKind] = await this.waitForCompletionWithTimeoutAndCancellation( - computeSnippetsPromise, defaultValue, context.timeBudget * timeBudgetFactor, copilotCancel); + [copilotCompletionContext, copilotCompletionContextKind] = await this.resolveResultAndKind(context, featureFlag, + telemetry.fork(), defaultValue, resolveStartTime, cppTimeBudgetMs, maxSnippetCount, maxSnippetLength, doAggregateSnippets, copilotCancel); // Fix up copilotCompletionContextKind accounting for stale-cache-hits. if (copilotCompletionContextKind === CopilotCompletionKind.GotFromCache && copilotCompletionContext && cacheEntry) { @@ -292,12 +378,12 @@ response.uri:${copilotCompletionContext.sourceFileUri || ""}:${copilotC telemetry.addCopilotCanceled(duration); throw new CopilotCancellationError(); } - logMessage += ` (id:${copilotCompletionContext?.requestId}) `; + logMessage += ` (id: ${copilotCompletionContext?.requestId})`; return [...copilotCompletionContext?.snippets ?? [], ...copilotCompletionContext?.traits ?? []] as SupportedContextItem[]; } catch (e: any) { if (e instanceof CopilotCancellationError) { telemetry.addCopilotCanceled(CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime)); - logMessage += ` (copilot cancellation) `; + logMessage += ` (copilot cancellation)`; throw e; } if (e instanceof InternalCancellationError) { @@ -312,12 +398,12 @@ response.uri:${copilotCompletionContext.sourceFileUri || ""}:${copilotC throw e; } finally { const duration: number = CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime); - logMessage += `featureFlag:${featureFlag?.toString()},`; + logMessage += `featureFlag:${featureFlag?.toString()}, `; if (copilotCompletionContext === undefined) { - logMessage += ` result is undefined and no snippets provided (${copilotCompletionContextKind.toString()}), elapsed time:${duration}ms`; + logMessage += `result is undefined and no code snippets provided(${copilotCompletionContextKind.toString()}), elapsed time:${duration} ms`; } else { - logMessage += ` for ${docUri}:${docOffset} provided ${copilotCompletionContext.snippets.length} code-snippet(s) (${copilotCompletionContextKind.toString()}\ -${copilotCompletionContext?.areSnippetsMissing ? ", missing code-snippets" : ""}) and ${copilotCompletionContext.traits.length} trait(s), elapsed time:${duration}ms`; + logMessage += `for ${docUri}:${docOffset} provided ${copilotCompletionContext.snippets.length} code snippet(s)(${copilotCompletionContextKind.toString()}\ + ${copilotCompletionContext?.areSnippetsMissing ? ", missing code snippets" : ""}) and ${copilotCompletionContext.traits.length} trait(s), elapsed time:${duration} ms`; } telemetry.addResponseMetadata(copilotCompletionContext?.areSnippetsMissing ?? true, copilotCompletionContext?.snippets.length, copilotCompletionContext?.traits.length, @@ -349,7 +435,7 @@ ${copilotCompletionContext?.areSnippetsMissing ? ", missing code-snippets" : ""} console.debug("Failed to register the Copilot Context Provider."); properties["error"] = "Failed to register the Copilot Context Provider"; if (e instanceof CopilotContextProviderException) { - properties["error"] += `: ${e.message}`; + properties["error"] += `: ${e.message} `; } } finally { telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); diff --git a/Extension/src/LanguageServer/copilotCompletionContextTelemetry.ts b/Extension/src/LanguageServer/copilotCompletionContextTelemetry.ts index 26ecc7537..802204e9c 100644 --- a/Extension/src/LanguageServer/copilotCompletionContextTelemetry.ts +++ b/Extension/src/LanguageServer/copilotCompletionContextTelemetry.ts @@ -88,16 +88,19 @@ export class CopilotCompletionContextTelemetry { } public addRequestMetadata(uri: string, caretOffset: number, completionId: string, - languageId: string, { featureFlag, timeBudgetFactor, maxCaretDistance }: { - featureFlag?: CopilotCompletionContextFeatures; - timeBudgetFactor?: number; maxCaretDistance?: number; + languageId: string, { featureFlag, timeBudgetMs, maxCaretDistance, maxSnippetCount, maxSnippetLength, doAggregateSnippets }: { + featureFlag?: CopilotCompletionContextFeatures; timeBudgetMs?: number; maxCaretDistance?: number; + maxSnippetCount?: number; maxSnippetLength?: number; doAggregateSnippets?: boolean; } = {}): void { this.addProperty('request.completionId', completionId); this.addProperty('request.languageId', languageId); this.addMetric('request.caretOffset', caretOffset); this.addProperty('request.featureFlag', featureFlag?.toString() ?? ''); - if (timeBudgetFactor !== undefined) { this.addMetric('request.timeBudgetFactor', timeBudgetFactor); } + if (timeBudgetMs !== undefined) { this.addMetric('request.timeBudgetMs', timeBudgetMs); } if (maxCaretDistance !== undefined) { this.addMetric('request.maxCaretDistance', maxCaretDistance); } + if (maxSnippetCount !== undefined) { this.addMetric('request.maxSnippetCount', maxSnippetCount); } + if (maxSnippetLength !== undefined) { this.addMetric('request.maxSnippetLength', maxSnippetLength); } + if (doAggregateSnippets !== undefined) { this.addProperty('request.doAggregateSnippets', doAggregateSnippets.toString()); } } public addCppStandardVersionMetadata(standardVersion: string, elapsedMs: number): void { diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index 999e8114e..cff19f2d9 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -478,8 +478,8 @@ export class CppSettings extends Settings { } return this.getAsString("copilotHover"); } - public get cppCodeSnippetsFeatureNames(): string | undefined { - const value = super.Section.get("cppCodeSnippetsFeatureNames"); + public get cppContextProviderParams(): string | undefined { + const value = super.Section.get("copilotContextProviderParams"); if (isString(value)) { return value; }