diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 81d54197be2aa..4b47131d10c2f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -150,6 +150,15 @@ namespace ts.server { export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; + /*@internal*/ + export interface PerformanceEvent { + kind: "UpdateGraph"; + durationMs: number; + } + + /*@internal*/ + export type PerformanceEventHandler = (event: PerformanceEvent) => void; + export interface SafeList { [name: string]: { match: RegExp, exclude?: (string | number)[][], types?: string[] }; } @@ -612,6 +621,8 @@ namespace ts.server { /*@internal*/ readonly watchFactory: WatchFactory; + private performanceEventHandler?: PerformanceEventHandler; + constructor(opts: ProjectServiceOptions) { this.host = opts.host; this.logger = opts.logger; @@ -849,6 +860,13 @@ namespace ts.server { this.eventHandler(event); } + /* @internal */ + sendUpdateGraphPerformanceEvent(durationMs: number) { + if (this.performanceEventHandler) { + this.performanceEventHandler({ kind: "UpdateGraph", durationMs }); + } + } + /* @internal */ delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project: Project) { this.delayUpdateProjectGraph(project); @@ -2551,6 +2569,11 @@ namespace ts.server { return info.sourceFileLike; } + /*@internal*/ + setPerformanceEventHandler(performanceEventHandler: PerformanceEventHandler) { + this.performanceEventHandler = performanceEventHandler; + } + setHostConfiguration(args: protocol.ConfigureRequestArguments) { if (args.file) { const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); diff --git a/src/server/project.ts b/src/server/project.ts index 0013fe0da0b19..08c3b7203a3f5 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1087,6 +1087,7 @@ namespace ts.server { removed => this.detachScriptInfoFromProject(removed) ); const elapsed = timestamp() - start; + this.projectService.sendUpdateGraphPerformanceEvent(elapsed); this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} Version: ${this.getProjectVersion()} structureChanged: ${hasNewProgram} Elapsed: ${elapsed}ms`); if (this.hasAddedorRemovedFiles) { this.print(); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 00fce8b14c054..4ce464e379041 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -232,6 +232,12 @@ namespace ts.server.protocol { * Contains extra information that plugin can include to be passed on */ metadata?: unknown; + + /** + * Time spent updating the program graph, in milliseconds. + */ + /* @internal */ + updateGraphDurationMs?: number; } /** diff --git a/src/server/session.ts b/src/server/session.ts index a1690aee1802b..6ba64f36ea5cd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -591,6 +591,8 @@ namespace ts.server { protected projectService: ProjectService; private changeSeq = 0; + private updateGraphDurationMs: number | undefined; + private currentRequestId!: number; private errorCheck: MultistepOperation; @@ -648,6 +650,7 @@ namespace ts.server { syntaxOnly: opts.syntaxOnly, }; this.projectService = new ProjectService(settings); + this.projectService.setPerformanceEventHandler(this.performanceEventHandler.bind(this)); this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); } @@ -655,6 +658,15 @@ namespace ts.server { this.event({ request_seq: requestId }, "requestCompleted"); } + private performanceEventHandler(event: PerformanceEvent) { + switch (event.kind) { + case "UpdateGraph": { + this.updateGraphDurationMs = (this.updateGraphDurationMs || 0) + event.durationMs; + break; + } + } + } + private defaultEventHandler(event: ProjectServiceEvent) { switch (event.eventName) { case ProjectsUpdatedInBackgroundEvent: @@ -795,7 +807,9 @@ namespace ts.server { command: cmdName, request_seq: reqSeq, success, + updateGraphDurationMs: this.updateGraphDurationMs, }; + if (success) { let metadata: unknown; if (isArray(info)) { @@ -2549,6 +2563,9 @@ namespace ts.server { public onMessage(message: string) { this.gcTimer.scheduleCollect(); + + this.updateGraphDurationMs = undefined; + let start: number[] | undefined; if (this.logger.hasLevel(LogLevel.requestTime)) { start = this.hrtime(); diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 61ce6001c2d51..91cdc1f40532c 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -100,7 +100,8 @@ namespace ts.server { seq: 0, message: "Unrecognized JSON command: foobar", request_seq: 0, - success: false + success: false, + updateGraphDurationMs: undefined, }; expect(lastSent).to.deep.equal(expected); }); @@ -126,7 +127,8 @@ namespace ts.server { success: true, request_seq: 0, seq: 0, - body: undefined + body: undefined, + updateGraphDurationMs: undefined, }); }); it("should handle literal types in request", () => { @@ -323,7 +325,8 @@ namespace ts.server { success: true, request_seq: 0, seq: 0, - body: undefined + body: undefined, + updateGraphDurationMs: undefined, }); }); }); @@ -413,7 +416,8 @@ namespace ts.server { type: "response", command, body, - success: true + success: true, + updateGraphDurationMs: undefined, }); }); }); @@ -532,7 +536,8 @@ namespace ts.server { type: "response", command, body, - success: true + success: true, + updateGraphDurationMs: undefined, }); }); it("can add and respond to new protocol handlers", () => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 00890b067f855..f744a207fc42e 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -9033,6 +9033,7 @@ declare namespace ts.server { readonly syntaxOnly?: boolean; /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects; + private performanceEventHandler?; constructor(opts: ProjectServiceOptions); toPath(fileName: string): Path; private loadTypesMap; @@ -9262,6 +9263,7 @@ declare namespace ts.server { private readonly gcTimer; protected projectService: ProjectService; private changeSeq; + private updateGraphDurationMs; private currentRequestId; private errorCheck; protected host: ServerHost; @@ -9276,6 +9278,7 @@ declare namespace ts.server { private readonly noGetErrOnBackgroundUpdate?; constructor(opts: SessionOptions); private sendRequestCompletedEvent; + private performanceEventHandler; private defaultEventHandler; private projectsUpdatedInBackgroundEvent; logError(err: Error, cmd: string): void;