From 0ea953288db13b59acca970f164d7a60a7b86390 Mon Sep 17 00:00:00 2001 From: luca cappa Date: Wed, 19 Feb 2025 23:19:00 -0800 Subject: [PATCH] -new feat: add traits for C++ lang version -telemetry: fix cancellation events. -telemetry: more diag event for registration failure. -add fallback to SimilarFiles providers such as openTabs and/or related-files --- Extension/src/LanguageServer/client.ts | 9 +- .../copilotCompletionContextProvider.ts | 148 +++++++++++------- .../copilotCompletionContextTelemetry.ts | 11 +- 3 files changed, 110 insertions(+), 58 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 76d464be9..46a359c95 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -22,6 +22,7 @@ import { SemanticToken, SemanticTokensProvider } from './Providers/semanticToken import { WorkspaceSymbolProvider } from './Providers/workspaceSymbolProvider'; // End provider imports +import { SupportedContextItem } from '@github/copilot-language-server'; import { ok } from 'assert'; import * as fs from 'fs'; import * as os from 'os'; @@ -55,7 +56,7 @@ import { } from './codeAnalysis'; import { Location, TextEdit, WorkspaceEdit } from './commonTypes'; import * as configs from './configurations'; -import { CopilotCompletionContextFeatures, CopilotCompletionContextProvider, SnippetEntry } from './copilotCompletionContextProvider'; +import { CopilotCompletionContextFeatures, CopilotCompletionContextProvider } from './copilotCompletionContextProvider'; import { DataBinding } from './dataBinding'; import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorConfig'; import { CppSourceStr, clients, configPrefix, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; @@ -568,11 +569,13 @@ interface FilesEncodingChanged { export interface CopilotCompletionContextResult { requestId: number; - isResultMissing: boolean; - snippets: SnippetEntry[]; + areCodeSnippetsMissing: boolean; + snippets: SupportedContextItem[]; translationUnitUri: string; caretOffset: number; featureFlag: CopilotCompletionContextFeatures; + codeSnippetsCount: number; + traitsCount: number; } export interface CopilotCompletionContextParams { diff --git a/Extension/src/LanguageServer/copilotCompletionContextProvider.ts b/Extension/src/LanguageServer/copilotCompletionContextProvider.ts index 96c288fd5..d3352a53d 100644 --- a/Extension/src/LanguageServer/copilotCompletionContextProvider.ts +++ b/Extension/src/LanguageServer/copilotCompletionContextProvider.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All Rights Reserved. * See 'LICENSE' in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { CodeSnippet, ContextResolver, ResolveRequest } from '@github/copilot-language-server'; +import { ContextResolver, ResolveRequest, SupportedContextItem } from '@github/copilot-language-server'; import { randomUUID } from 'crypto'; import * as vscode from 'vscode'; import { DocumentSelector } from 'vscode-languageserver-protocol'; @@ -12,16 +12,9 @@ import { CopilotCompletionContextResult } from './client'; import { CopilotCompletionContextTelemetry } from './copilotCompletionContextTelemetry'; import { getCopilotApi } from './copilotProviders'; import { clients } from './extension'; +import { ProjectContext } from './lmTool'; import { CppSettings } from './settings'; -export interface SnippetEntry { - uri: string; - value: string; - startLine: number; - endLine: number; - importance: number; -} - class DefaultValueFallback extends Error { static readonly DefaultValue = "DefaultValue"; constructor() { super(DefaultValueFallback.DefaultValue); } @@ -32,6 +25,14 @@ class CancellationError extends Error { constructor() { super(CancellationError.Canceled); } } +class InternalCancellationError extends Error { + static readonly Canceled = "CpptoolsCanceled"; + constructor() { super(InternalCancellationError.Canceled); } +} + +class CopilotCancellationError extends CancellationError { +} + class CopilotContextProviderException extends Error { } @@ -43,6 +44,7 @@ class WellKnownErrors extends Error { } } +// A bit mask for enabling features in the completion context. export enum CopilotCompletionContextFeatures { None = 0, Instant = 1, @@ -68,7 +70,7 @@ export enum CopilotCompletionKind { type CacheEntry = [string, CopilotCompletionContextResult]; -export class CopilotCompletionContextProvider implements ContextResolver { +export class CopilotCompletionContextProvider implements ContextResolver { private static readonly providerId = 'ms-vscode.cpptools'; private readonly completionContextCache: Map = new Map(); private static readonly defaultCppDocumentSelector: DocumentSelector = [{ language: 'cpp' }, { language: 'c' }, { language: 'cuda-cpp' }]; @@ -79,16 +81,16 @@ export class CopilotCompletionContextProvider implements ContextResolver(promise: Promise, defaultValue: T | undefined, - timeout: number, token: vscode.CancellationToken): Promise<[T | undefined, CopilotCompletionKind]> { + timeout: number, copilotToken: vscode.CancellationToken): Promise<[T | undefined, CopilotCompletionKind]> { const defaultValuePromise = new Promise((_resolve, reject) => setTimeout(() => { - if (token.isCancellationRequested) { + if (copilotToken.isCancellationRequested) { reject(new CancellationError()); } else { reject(new DefaultValueFallback()); } }, timeout)); const cancellationPromise = new Promise((_, reject) => { - token.onCancellationRequested(() => { + copilotToken.onCancellationRequested(() => { reject(new CancellationError()); }); }); @@ -108,63 +110,86 @@ export class CopilotCompletionContextProvider implements ContextResolver { const documentUri = context.documentContext.uri; const caretOffset = context.documentContext.offset; let logMessage = `Copilot: getCompletionContext(${documentUri}:${caretOffset}):`; try { + const snippetsFeatureFlag = CopilotCompletionContextProvider.normalizeFeatureFlag(featureFlag); telemetry.addRequestMetadata(documentUri, caretOffset, context.completionId, - context.documentContext.languageId, { featureFlag: featureFlag }); + context.documentContext.languageId, { featureFlag: snippetsFeatureFlag }); const docUri = vscode.Uri.parse(documentUri); const client = clients.getClientFor(docUri); if (!client) { throw WellKnownErrors.clientNotFound(); } const getCompletionContextStartTime = performance.now(); + + // Start collection of project traits concurrently with the completion context computation. + const projectContextPromise = (async (): Promise => { + const elapsedTimeMs = performance.now(); + const projectContext = await client.getChatContext(docUri, internalToken); + telemetry.addCppStandardVersionMetadata(projectContext.standardVersion, + CopilotCompletionContextProvider.getRoundedDuration(elapsedTimeMs)); + return projectContext; + })(); + const copilotCompletionContext: CopilotCompletionContextResult = - await client.getCompletionContext(docUri, caretOffset, featureFlag, token); + await client.getCompletionContext(docUri, caretOffset, snippetsFeatureFlag, internalToken); + copilotCompletionContext.codeSnippetsCount = copilotCompletionContext.snippets.length; telemetry.addRequestId(copilotCompletionContext.requestId); logMessage += ` (id:${copilotCompletionContext.requestId})`; - if (!copilotCompletionContext.isResultMissing) { - logMessage += `, featureFlag:${copilotCompletionContext.featureFlag},\ - ${copilotCompletionContext.translationUnitUri}:${copilotCompletionContext.caretOffset},\ - snippetsCount:${copilotCompletionContext.snippets.length}`; - const resultMismatch = copilotCompletionContext.translationUnitUri !== docUri.toString(); + // Collect project traits and if any add them to the completion context. + const projectContext = await projectContextPromise; + if (projectContext?.standardVersion) { + copilotCompletionContext.snippets.push({ name: "C++ language standard version", value: `This project uses the ${projectContext.standardVersion} language standard version.` }); + copilotCompletionContext.traitsCount = 1; + } + + let resultMismatch = false; + if (!copilotCompletionContext.areCodeSnippetsMissing) { + resultMismatch = copilotCompletionContext.translationUnitUri !== docUri.toString(); const cacheEntryId = randomUUID().toString(); this.completionContextCache.set(copilotCompletionContext.translationUnitUri, [cacheEntryId, copilotCompletionContext]); const duration = CopilotCompletionContextProvider.getRoundedDuration(startTime); telemetry.addCacheComputedData(duration, cacheEntryId); if (resultMismatch) { logMessage += `, mismatch TU vs result`; } - logMessage += `, cached ${copilotCompletionContext.snippets.length} snippets in [ms]: ${duration}`; - telemetry.addResponseMetadata(false, copilotCompletionContext.snippets.length, copilotCompletionContext.translationUnitUri, copilotCompletionContext.caretOffset, - copilotCompletionContext.featureFlag); - telemetry.addComputeContextElapsed(CopilotCompletionContextProvider.getRoundedDuration(getCompletionContextStartTime)); - return resultMismatch ? undefined : copilotCompletionContext; + logMessage += `, cached ${copilotCompletionContext.snippets.length} snippets in ${duration}ms`; + logMessage += `, response.featureFlag:${copilotCompletionContext.featureFlag},\ + ${copilotCompletionContext.translationUnitUri}:${copilotCompletionContext.caretOffset},\ + snippetsCount:${copilotCompletionContext.codeSnippetsCount}, traitsCount:${copilotCompletionContext.traitsCount}`; } else { - logMessage += `, result is missing`; - telemetry.addResponseMetadata(true); - return undefined; + logMessage += ` (snippets are missing) `; } + telemetry.addResponseMetadata(copilotCompletionContext.areCodeSnippetsMissing, copilotCompletionContext.snippets.length, copilotCompletionContext.codeSnippetsCount, + copilotCompletionContext.traitsCount, copilotCompletionContext.caretOffset, copilotCompletionContext.featureFlag); + telemetry.addComputeContextElapsed(CopilotCompletionContextProvider.getRoundedDuration(getCompletionContextStartTime)); + + return resultMismatch ? undefined : copilotCompletionContext; } catch (e) { - if (e instanceof CancellationError) { + if (e instanceof vscode.CancellationError || (e as Error)?.message === CancellationError.Canceled) { telemetry.addInternalCanceled(CopilotCompletionContextProvider.getRoundedDuration(startTime)); logMessage += `, (internal cancellation)`; - throw e; - } else if (e instanceof vscode.CancellationError || (e as Error)?.message === CancellationError.Canceled) { - telemetry.addCopilotCanceled(CopilotCompletionContextProvider.getRoundedDuration(startTime)); - logMessage += `, (copilot cancellation)`; - throw e; + throw InternalCancellationError; } if (e instanceof WellKnownErrors) { telemetry.addWellKnownError(e.message); } - const err = e as Error; - out.appendLine(`Copilot: getCompletionContextWithCancellation(${documentUri}:${caretOffset}): Error: '${err?.message}', stack '${err?.stack}`); telemetry.addError(); + const err = e as Error; + out.appendLine(`Copilot: getCompletionContextWithCancellation(${documentUri}: ${caretOffset}): Error: '${err?.message}', stack '${err?.stack}`); return undefined; } finally { out.appendLine(logMessage); @@ -198,8 +223,8 @@ export class CopilotCompletionContextProvider implements ContextResolver { try { let enabledFeatureNames = new CppSettings().cppCodeSnippetsFeatureNames; - if (!enabledFeatureNames) { enabledFeatureNames = context.activeExperiments.get(CopilotCompletionContextProvider.CppCodeSnippetsEnabledFeatures) as string; } - return (enabledFeatureNames?.split(',') as string[]) ?? undefined; + enabledFeatureNames ??= context.activeExperiments.get(CopilotCompletionContextProvider.CppCodeSnippetsEnabledFeatures) as string; + return enabledFeatureNames?.split(',').map(s => s.trim()); } catch (e) { console.warn(`getEnabledFeatures(): error fetching ${CopilotCompletionContextProvider.CppCodeSnippetsEnabledFeatures}: `, e); return undefined; @@ -234,7 +259,7 @@ export class CopilotCompletionContextProvider implements ContextResolver { + public async resolve(context: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise { const resolveStartTime = performance.now(); const out: Logger = getOutputChannelLogger(); let logMessage = `Copilot: resolve(${context.documentContext.uri}:${context.documentContext.offset}): `; @@ -243,8 +268,9 @@ export class CopilotCompletionContextProvider implements ContextResolver"; - logMessage += `for ${uri} provided ${copilotCompletionContext.snippets?.length} snippets (${copilotCompletionContextKind.toString()}), elapsed time(ms): ${duration}`; + const uri = copilotCompletionContext.translationUnitUri ? copilotCompletionContext.translationUnitUri : ""; + logMessage += ` for ${uri} provided ${copilotCompletionContext.codeSnippetsCount} code-snippets (${copilotCompletionContextKind.toString()},\ +${copilotCompletionContext?.areCodeSnippetsMissing ? " missing code-snippets" : ""}) and ${copilotCompletionContext.traitsCount} traits, elapsed time:${duration}ms`; } + telemetry.addResponseMetadata(copilotCompletionContext?.areCodeSnippetsMissing ?? true, + copilotCompletionContext?.snippets?.length, + copilotCompletionContext?.codeSnippetsCount, copilotCompletionContext?.traitsCount, + copilotCompletionContext?.caretOffset, copilotCompletionContext?.featureFlag); telemetry.addResolvedElapsed(duration); telemetry.addCacheSize(this.completionContextCache.size); telemetry.send(); @@ -305,7 +344,9 @@ export class CopilotCompletionContextProvider implements ContextResolver'); + this.addMetric('response.codeSnippetsCount', codeSnippetsCount ?? -1); + this.addMetric('response.traitsCount', traitsCount ?? -1); } public addRequestMetadata(uri: string, caretOffset: number, completionId: string, @@ -95,6 +97,11 @@ export class CopilotCompletionContextTelemetry { if (maxCaretDistance !== undefined) { this.addMetric('request.maxCaretDistance', maxCaretDistance); } } + public addCppStandardVersionMetadata(standardVersion: string, elapsedMs: number): void { + this.addProperty('response.cppStandardVersion', standardVersion); + this.addMetric('response.cppStandardVersionElapsedMs', elapsedMs); + } + public send(postfix?: string): void { try { const eventName = CopilotCompletionContextTelemetry.copilotEventName + (postfix ? `/${postfix}` : '');