diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index 6640ee13..2c41defd 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -74,6 +74,7 @@ function createJavaContextResolver(): ContextResolverFunction { async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise { const items: SupportedContextItem[] = []; const start = performance.now(); + let duration: number; let dependenciesResult: CopilotHelper.IResolveResult | undefined; let importsResult: CopilotHelper.IResolveResult | undefined; @@ -103,9 +104,10 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode items.push(...importsResult.items); } catch (error: any) { if (error instanceof CopilotCancellationError) { + duration = Math.round(performance.now() - start); sendContextResolutionTelemetry( request, - start, + duration, items, "cancelled_by_copilot", undefined, @@ -117,9 +119,10 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode throw error; } if (error instanceof vscode.CancellationError || error.message === CancellationError.CANCELED) { + duration = Math.round(performance.now() - start); sendContextResolutionTelemetry( request, - start, + duration, items, "cancelled_internally", undefined, @@ -132,9 +135,10 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode } // Send telemetry for general errors (but continue with partial results) + duration = Math.round(performance.now() - start); sendContextResolutionTelemetry( request, - start, + duration, items, "error_partial_results", error.message || "unknown_error", @@ -149,9 +153,10 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode } // Send telemetry data once at the end for success case + duration = Math.round(performance.now() - start); sendContextResolutionTelemetry( request, - start, + duration, items, "succeeded", undefined, diff --git a/src/copilot/utils.ts b/src/copilot/utils.ts index 4a2b424a..f45eb907 100644 --- a/src/copilot/utils.ts +++ b/src/copilot/utils.ts @@ -8,6 +8,31 @@ import { type ContextProvider, } from '@github/copilot-language-server'; import { sendInfo } from "vscode-extension-telemetry-wrapper"; + +/** + * TelemetryQueue - Asynchronous telemetry queue to avoid blocking main thread + * Based on the PromiseQueue pattern from copilot-client + */ +class TelemetryQueue { + private promises = new Set>(); + + register(promise: Promise): void { + this.promises.add(promise); + // Use void to avoid blocking - the key pattern from PromiseQueue + void promise.finally(() => this.promises.delete(promise)); + } + + async flush(): Promise { + await Promise.allSettled(this.promises); + } + + get size(): number { + return this.promises.size; + } +} + +// Global telemetry queue instance +const globalTelemetryQueue = new TelemetryQueue(); /** * Error classes for Copilot context provider cancellation handling */ @@ -211,14 +236,59 @@ export class ContextProviderResolverError extends Error { } /** - * Send consolidated telemetry data for Java context resolution - * This is the centralized function for sending context resolution telemetry + * Asynchronously send telemetry data preparation and sending + * This function prepares telemetry data and handles the actual sending asynchronously + */ +async function _sendContextResolutionTelemetry( + request: ResolveRequest, + duration: number, + items: SupportedContextItem[], + status: string, + error?: string, + dependenciesEmptyReason?: string, + importsEmptyReason?: string, + dependenciesCount?: number, + importsCount?: number +): Promise { + try { + const tokenCount = JavaContextProviderUtils.calculateTokenCount(items); + const telemetryData: any = { + "action": "resolveJavaContext", + "completionId": request.completionId, + "duration": duration, + "itemCount": items.length, + "tokenCount": tokenCount, + "status": status, + "dependenciesCount": dependenciesCount ?? 0, + "importsCount": importsCount ?? 0 + }; + + // Add empty reasons if present + if (dependenciesEmptyReason) { + telemetryData.dependenciesEmptyReason = dependenciesEmptyReason; + } + if (importsEmptyReason) { + telemetryData.importsEmptyReason = importsEmptyReason; + } + if (error) { + telemetryData.error = error; + } + + // Actual telemetry sending - this is synchronous but network is async + sendInfo("", telemetryData); + } catch (telemetryError) { + // Silently ignore telemetry errors to not affect main functionality + } +} + +/** + * Send consolidated telemetry data for Java context resolution asynchronously + * This function immediately returns and sends telemetry in the background without blocking * * @param request The resolve request from Copilot - * @param start Performance timestamp when resolution started + * @param duration Duration of the resolution in milliseconds * @param items The resolved context items * @param status Status of the resolution ("succeeded", "cancelled_by_copilot", "cancelled_internally", "error_partial_results") - * @param sendInfo The sendInfo function from vscode-extension-telemetry-wrapper * @param error Optional error message * @param dependenciesEmptyReason Optional reason why dependencies were empty * @param importsEmptyReason Optional reason why imports were empty @@ -227,7 +297,7 @@ export class ContextProviderResolverError extends Error { */ export function sendContextResolutionTelemetry( request: ResolveRequest, - start: number, + duration: number, items: SupportedContextItem[], status: string, error?: string, @@ -236,29 +306,26 @@ export function sendContextResolutionTelemetry( dependenciesCount?: number, importsCount?: number ): void { - const duration = Math.round(performance.now() - start); - const tokenCount = JavaContextProviderUtils.calculateTokenCount(items); - const telemetryData: any = { - "action": "resolveJavaContext", - "completionId": request.completionId, - "duration": duration, - "itemCount": items.length, - "tokenCount": tokenCount, - "status": status, - "dependenciesCount": dependenciesCount ?? 0, - "importsCount": importsCount ?? 0 - }; - - // Add empty reasons if present - if (dependenciesEmptyReason) { - telemetryData.dependenciesEmptyReason = dependenciesEmptyReason; - } - if (importsEmptyReason) { - telemetryData.importsEmptyReason = importsEmptyReason; - } - if (error) { - telemetryData.error = error; - } + // Register the telemetry promise for non-blocking execution + // This follows the PromiseQueue pattern from copilot-client + globalTelemetryQueue.register( + _sendContextResolutionTelemetry( + request, + duration, + items, + status, + error, + dependenciesEmptyReason, + importsEmptyReason, + dependenciesCount, + importsCount + ) + ); +} - sendInfo("", telemetryData); +/** + * Get the global telemetry queue instance (useful for testing and monitoring) + */ +export function getTelemetryQueue(): TelemetryQueue { + return globalTelemetryQueue; } \ No newline at end of file