diff --git a/src/TracingLanguageClient.ts b/src/TracingLanguageClient.ts new file mode 100644 index 0000000000..3da77839ce --- /dev/null +++ b/src/TracingLanguageClient.ts @@ -0,0 +1,99 @@ +import { performance } from "perf_hooks"; +import { Event, EventEmitter } from "vscode"; +import { CancellationToken, LanguageClient, LanguageClientOptions, ProtocolRequestType, ProtocolRequestType0, RequestType, RequestType0, ServerOptions } from "vscode-languageclient/node"; +import { TraceEvent } from "./extension.api"; + +const requestEventEmitter = new EventEmitter(); +export const onDidRequestEnd: Event = requestEventEmitter.event; + +export class TracingLanguageClient extends LanguageClient { + private isStarted: boolean = false; + + constructor(id: string, name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions, forceDebug?: boolean) { + super(id, name, serverOptions, clientOptions, forceDebug); + } + + start(): Promise { + const isFirstTimeStart: boolean = !this.isStarted; + this.isStarted = true; + const startAt: number = performance.now(); + return super.start().then(value => { + if (isFirstTimeStart) { + this.fireTraceEvent("initialize", startAt); + } + return value; + }, reason => { + if (isFirstTimeStart) { + this.fireTraceEvent("initialize", startAt, reason); + } + throw reason; + }); + } + + stop(timeout?: number): Promise { + this.isStarted = false; + return super.stop(timeout); + } + + sendRequest(type: ProtocolRequestType0, token?: CancellationToken): Promise; + sendRequest(type: ProtocolRequestType, params: P, token?: CancellationToken): Promise; + sendRequest(type: RequestType0, token?: CancellationToken): Promise; + sendRequest(type: RequestType, params: P, token?: CancellationToken): Promise; + sendRequest(method: string, token?: CancellationToken): Promise; + sendRequest(method: string, param: any, token?: CancellationToken): Promise; + sendRequest(method: any, ...args) { + const startAt: number = performance.now(); + const requestType: string = this.getRequestType(method, ...args); + return this.sendRequest0(method, ...args).then(value => { + this.fireTraceEvent(requestType, startAt); + return value; + }, reason => { + this.fireTraceEvent(requestType, startAt, reason); + throw reason; + }); + } + + private sendRequest0(method: any, ...args) { + if (!args || !args.length) { + return super.sendRequest(method); + } + + const first = args[0]; + const last = args[args.length - 1]; + if (CancellationToken.is(last)) { + if (first === last) { + return super.sendRequest(method, last); + } else { + return super.sendRequest(method, first, last); + } + } + + return super.sendRequest(method, first); + } + + private getRequestType(method: any, ...args): string { + let requestType: string; + if (typeof method === 'string' || method instanceof String) { + requestType = String(method); + } else { + requestType = method?.method; + } + + if (requestType === "workspace/executeCommand") { + if (args?.[0]?.command) { + requestType = `workspace/executeCommand/${args[0].command}`; + } + } + + return requestType; + } + + private fireTraceEvent(type: string, startAt: number, reason?: any): void { + const duration: number = performance.now() - startAt; + requestEventEmitter.fire({ + type, + duration, + error: reason, + }); + } +} diff --git a/src/apiManager.ts b/src/apiManager.ts index b7daf493c2..de3f9efef1 100644 --- a/src/apiManager.ts +++ b/src/apiManager.ts @@ -9,6 +9,7 @@ import { Commands } from "./commands"; import { Emitter } from "vscode-languageclient"; import { ServerMode } from "./settings"; import { registerHoverCommand } from "./hoverAction"; +import { onDidRequestEnd } from "./TracingLanguageClient"; class ApiManager { @@ -60,6 +61,7 @@ class ApiManager { onDidServerModeChange, onDidProjectsImport, serverReady, + onDidRequestEnd, }; } diff --git a/src/extension.api.ts b/src/extension.api.ts index caa9378df1..477154bc56 100644 --- a/src/extension.api.ts +++ b/src/extension.api.ts @@ -73,7 +73,22 @@ export enum ClientStatus { stopping = "Stopping", } -export const extensionApiVersion = '0.7'; +export interface TraceEvent { + /** + * Request type. + */ + type: string; + /** + * Time (in milliseconds) taken to process a request. + */ + duration: number; + /** + * Error that occurs while processing a request. + */ + error?: any; +} + +export const extensionApiVersion = '0.8'; export interface ExtensionAPI { readonly apiVersion: string; @@ -118,4 +133,11 @@ export interface ExtensionAPI { * @since extension version 1.7.0 */ readonly serverReady: () => Promise; + + /** + * An event that's fired when a request has been responded. + * @since API version 0.8 + * @since extension version 1.16.0 + */ + readonly onDidRequestEnd: Event; } diff --git a/src/standardLanguageClient.ts b/src/standardLanguageClient.ts index 17dc34bc73..ee753697e5 100644 --- a/src/standardLanguageClient.ts +++ b/src/standardLanguageClient.ts @@ -33,6 +33,7 @@ import { excludeProjectSettingsFiles, ServerMode, setGradleWrapperChecksum } fro import { snippetCompletionProvider } from "./snippetCompletionProvider"; import * as sourceAction from './sourceAction'; import { askForProjects, projectConfigurationUpdate, upgradeGradle } from "./standardLanguageClientUtils"; +import { TracingLanguageClient } from './TracingLanguageClient'; import { TypeHierarchyDirection, TypeHierarchyItem } from "./typeHierarchy/protocol"; import { typeHierarchyTree } from "./typeHierarchy/typeHierarchyTree"; import { getAllJavaProjects, getJavaConfig, getJavaConfiguration } from "./utils"; @@ -103,7 +104,7 @@ export class StandardLanguageClient { } // Create the language client and start the client. - this.languageClient = new LanguageClient('java', extensionName, serverOptions, clientOptions); + this.languageClient = new TracingLanguageClient('java', extensionName, serverOptions, clientOptions); this.registerCommandsForStandardServer(context, jdtEventEmitter); fileEventHandler.registerFileEventHandlers(this.languageClient, context);