Skip to content
Closed
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
13 changes: 9 additions & 4 deletions src/copilot/contextProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function createJavaContextResolver(): ContextResolverFunction {
async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise<SupportedContextItem[]> {
const items: SupportedContextItem[] = [];
const start = performance.now();
let duration: number;

let dependenciesResult: CopilotHelper.IResolveResult | undefined;
let importsResult: CopilotHelper.IResolveResult | undefined;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -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,
Expand Down
125 changes: 96 additions & 29 deletions src/copilot/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Promise<unknown>>();

register(promise: Promise<unknown>): 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<void> {
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
*/
Expand Down Expand Up @@ -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<void> {
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
Expand All @@ -227,7 +297,7 @@ export class ContextProviderResolverError extends Error {
*/
export function sendContextResolutionTelemetry(
request: ResolveRequest,
start: number,
duration: number,
items: SupportedContextItem[],
status: string,
error?: string,
Expand All @@ -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;
}
Loading