Add experimental dual TS server

Fixes #75866
This commit is contained in:
Matt Bierner
2019-06-20 17:04:04 -07:00
parent 8ec2559029
commit 87b8402b59
8 changed files with 151 additions and 50 deletions

View File

@@ -62,7 +62,7 @@ suite('Server', () => {
test('should send requests with increasing sequence numbers', async () => {
const process = new FakeServerProcess();
const server = new ProcessBasedTsServer(process, undefined, new PipeRequestCanceller(undefined, tracer), undefined!, NoopTelemetryReporter, tracer);
const server = new ProcessBasedTsServer('semantic', process, undefined, new PipeRequestCanceller('semantic', undefined, tracer), undefined!, NoopTelemetryReporter, tracer);
const onWrite1 = process.onWrite();
server.executeImpl('geterr', {}, { isAsync: false, token: nulToken, expectsResult: true });

View File

@@ -7,7 +7,7 @@ import * as fs from 'fs';
import * as stream from 'stream';
import * as vscode from 'vscode';
import * as Proto from '../protocol';
import { ServerResponse } from '../typescriptService';
import { ServerResponse, TypeScriptRequests } from '../typescriptService';
import { Disposable } from '../utils/dispose';
import TelemetryReporter from '../utils/telemetry';
import Tracer from '../utils/tracer';
@@ -23,6 +23,7 @@ export interface OngoingRequestCanceller {
export class PipeRequestCanceller implements OngoingRequestCanceller {
public constructor(
private readonly _serverId: string,
private readonly _cancellationPipeName: string | undefined,
private readonly _tracer: Tracer,
) { }
@@ -31,7 +32,7 @@ export class PipeRequestCanceller implements OngoingRequestCanceller {
if (!this._cancellationPipeName) {
return false;
}
this._tracer.logTrace(`TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`);
this._tracer.logTrace(this._serverId, `TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`);
try {
fs.writeFileSync(this._cancellationPipeName + seq, '');
} catch {
@@ -51,9 +52,9 @@ export interface ITypeScriptServer {
kill(): void;
executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined;
dispose(): void;
}
@@ -75,6 +76,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
private readonly _pendingResponses = new Set<number>();
constructor(
private readonly _serverId: string,
private readonly _process: TsServerProcess,
private readonly _tsServerLogFile: string | undefined,
private readonly _requestCanceller: OngoingRequestCanceller,
@@ -136,11 +138,11 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
const seq = (event as Proto.RequestCompletedEvent).body.request_seq;
const p = this._callbacks.fetch(seq);
if (p) {
this._tracer.traceRequestCompleted('requestCompleted', seq, p.startTime);
this._tracer.traceRequestCompleted(this._serverId, 'requestCompleted', seq, p.startTime);
p.onSuccess(undefined);
}
} else {
this._tracer.traceEvent(event);
this._tracer.traceEvent(this._serverId, event);
this._onEvent.fire(event);
}
break;
@@ -156,7 +158,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
private tryCancelRequest(seq: number, command: string): boolean {
try {
if (this._requestQueue.tryDeletePendingRequest(seq)) {
this._tracer.logTrace(`TypeScript Server: canceled request with sequence number ${seq}`);
this.logTrace(`Canceled request with sequence number ${seq}`);
return true;
}
@@ -164,7 +166,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
return true;
}
this._tracer.logTrace(`TypeScript Server: tried to cancel request with sequence number ${seq}. But request got already delivered.`);
this.logTrace(`Tried to cancel request with sequence number ${seq}. But request got already delivered.`);
return false;
} finally {
const callback = this.fetchCallback(seq);
@@ -180,7 +182,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
return;
}
this._tracer.traceResponse(response, callback.startTime);
this._tracer.traceResponse(this._serverId, response, callback.startTime);
if (response.success) {
callback.onSuccess(response);
} else if (response.message === 'No content available.') {
@@ -191,9 +193,9 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
}
}
public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
const request = this._requestQueue.createRequest(command, args);
const requestInfo: RequestItem = {
request,
@@ -255,7 +257,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
private sendRequest(requestItem: RequestItem): void {
const serverRequest = requestItem.request;
this._tracer.traceRequest(serverRequest, requestItem.expectsResponse, this._requestQueue.length);
this._tracer.traceRequest(this._serverId, serverRequest, requestItem.expectsResponse, this._requestQueue.length);
if (requestItem.expectsResponse && !requestItem.isAsync) {
this._pendingResponses.add(requestItem.request.seq);
@@ -281,6 +283,10 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
return callback;
}
private logTrace(message: string) {
this._tracer.logTrace(this._serverId, message);
}
private static readonly fenceCommands = new Set(['change', 'close', 'open', 'updateOpen']);
private static getQueueingType(
@@ -294,3 +300,53 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
}
}
export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer {
public constructor(
private readonly syntaxServer: ITypeScriptServer,
private readonly semanticServer: ITypeScriptServer,
) {
super();
this._register(syntaxServer.onEvent(e => this._onEvent.fire(e)));
this._register(semanticServer.onEvent(e => this._onEvent.fire(e)));
this._register(semanticServer.onExit(e => this._onExit.fire(e)));
this._register(semanticServer.onError(e => this._onError.fire(e)));
}
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
public readonly onEvent = this._onEvent.event;
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
public readonly onExit = this._onExit.event;
private readonly _onError = this._register(new vscode.EventEmitter<any>());
public readonly onError = this._onError.event;
public get onReaderError() { return this.semanticServer.onReaderError; }
public get tsServerLogFile() { return this.semanticServer.tsServerLogFile; }
public kill(): void {
this.syntaxServer.kill();
this.semanticServer.kill();
}
private static readonly syntaxCommands = new Set<keyof TypeScriptRequests>(['navtree', 'getOutliningSpans', 'jsxClosingTag', 'selectionRange']);
private static readonly sharedCommands = new Set<keyof TypeScriptRequests>(['change', 'close', 'open', 'updateOpen', 'configure', 'configurePlugin']);
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
if (SyntaxRoutingTsServer.syntaxCommands.has(command)) {
return this.syntaxServer.executeImpl(command, args, executeInfo);
} else if (SyntaxRoutingTsServer.sharedCommands.has(command)) {
this.syntaxServer.executeImpl(command, args, executeInfo);
return this.semanticServer.executeImpl(command, args, executeInfo);
} else {
return this.semanticServer.executeImpl(command, args, executeInfo);
}
}
}

View File

@@ -18,7 +18,7 @@ import { PluginManager } from '../utils/plugins';
import TelemetryReporter from '../utils/telemetry';
import Tracer from '../utils/tracer';
import { TypeScriptVersion, TypeScriptVersionProvider } from '../utils/versionProvider';
import { ITypeScriptServer, TsServerProcess, ProcessBasedTsServer, PipeRequestCanceller } from './server';
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess } from './server';
export class TypeScriptServerSpawner {
public constructor(
@@ -34,6 +34,30 @@ export class TypeScriptServerSpawner {
version: TypeScriptVersion,
configuration: TypeScriptServiceConfiguration,
pluginManager: PluginManager
): ITypeScriptServer {
if (this.shouldUserSeparateSyntaxServer(version)) {
const syntaxServer = this.spawnProcessBasedTsServer('syntax', version, configuration, pluginManager, ['--syntaxOnly', '--disableAutomaticTypingAcquisition']);
const semanticServer = this.spawnProcessBasedTsServer('semantic', version, configuration, pluginManager, []);
return new SyntaxRoutingTsServer(syntaxServer, semanticServer);
}
return this.spawnProcessBasedTsServer('main', version, configuration, pluginManager, []);
}
private shouldUserSeparateSyntaxServer(version: TypeScriptVersion): boolean {
if (!version.version || version.version.lt(API.v340)) {
return false;
}
return vscode.workspace.getConfiguration('typescript')
.get<boolean>('experimental.useSeparateSyntaxServer', false);
}
private spawnProcessBasedTsServer(
serverId: string,
version: TypeScriptVersion,
configuration: TypeScriptServiceConfiguration,
pluginManager: PluginManager,
extraForkArgs: readonly string[],
): ITypeScriptServer {
const apiVersion = version.version || API.defaultVersion;
@@ -41,20 +65,21 @@ export class TypeScriptServerSpawner {
if (TypeScriptServerSpawner.isLoggingEnabled(apiVersion, configuration)) {
if (tsServerLogFile) {
this._logger.info(`TSServer log file: ${tsServerLogFile}`);
this._logger.info(`<${serverId}> Log file: ${tsServerLogFile}`);
} else {
this._logger.error('Could not create TSServer log directory');
this._logger.error(`<${serverId}> Could not create log directory`);
}
}
this._logger.info('Forking TSServer');
const childProcess = electron.fork(version.tsServerPath, args, this.getForkOptions());
this._logger.info('Started TSServer');
this._logger.info(`<${serverId}> Forking...`);
const childProcess = electron.fork(version.tsServerPath, [...args, ...extraForkArgs], this.getForkOptions());
this._logger.info(`<${serverId}> Starting...`);
return new ProcessBasedTsServer(
serverId,
new ChildServerProcess(childProcess),
tsServerLogFile,
new PipeRequestCanceller(cancellationPipeName, this._tracer),
new PipeRequestCanceller(serverId, cancellationPipeName, this._tracer),
version,
this._telemetryReporter,
this._tracer);

View File

@@ -26,7 +26,7 @@ export namespace ServerResponse {
export type Response<T extends Proto.Response> = T | Cancelled | typeof NoContent;
}
export interface TypeScriptRequestTypes {
interface StandardTsServerRequests {
'applyCodeActionCommand': [Proto.ApplyCodeActionCommandRequestArgs, Proto.ApplyCodeActionCommandResponse];
'completionEntryDetails': [Proto.CompletionDetailsRequestArgs, Proto.CompletionDetailsResponse];
'completionInfo': [Proto.CompletionsRequestArgs, Proto.CompletionInfoResponse];
@@ -59,6 +59,22 @@ export interface TypeScriptRequestTypes {
'typeDefinition': [Proto.FileLocationRequestArgs, Proto.TypeDefinitionResponse];
}
interface NoResponseTsServerRequests {
'open': [Proto.OpenRequestArgs, null];
'close': [Proto.FileRequestArgs];
'change': [Proto.ChangeRequestArgs, null];
'updateOpen': [Proto.UpdateOpenRequestArgs, null];
'compilerOptionsForInferredProjects': [Proto.SetCompilerOptionsForInferredProjectsArgs, null];
'reloadProjects': [null, null];
'configurePlugin': [Proto.ConfigurePluginRequest, Proto.ConfigurePluginResponse];
}
interface AsyncTsServerRequests {
'geterr': [Proto.GeterrRequestArgs, Proto.Response];
}
export type TypeScriptRequests = StandardTsServerRequests & NoResponseTsServerRequests & AsyncTsServerRequests;
export interface ITypeScriptServiceClient {
/**
* Convert a resource (VS Code) to a normalized path (TypeScript).
@@ -100,19 +116,17 @@ export interface ITypeScriptServiceClient {
readonly logger: Logger;
readonly bufferSyncSupport: BufferSyncSupport;
execute<K extends keyof TypeScriptRequestTypes>(
execute<K extends keyof StandardTsServerRequests>(
command: K,
args: TypeScriptRequestTypes[K][0],
args: StandardTsServerRequests[K][0],
token: vscode.CancellationToken,
lowPriority?: boolean
): Promise<ServerResponse.Response<TypeScriptRequestTypes[K][1]>>;
): Promise<ServerResponse.Response<StandardTsServerRequests[K][1]>>;
executeWithoutWaitingForResponse(command: 'open', args: Proto.OpenRequestArgs): void;
executeWithoutWaitingForResponse(command: 'close', args: Proto.FileRequestArgs): void;
executeWithoutWaitingForResponse(command: 'change', args: Proto.ChangeRequestArgs): void;
executeWithoutWaitingForResponse(command: 'updateOpen', args: Proto.UpdateOpenRequestArgs): void;
executeWithoutWaitingForResponse(command: 'compilerOptionsForInferredProjects', args: Proto.SetCompilerOptionsForInferredProjectsArgs): void;
executeWithoutWaitingForResponse(command: 'reloadProjects', args: null): void;
executeWithoutWaitingForResponse<K extends keyof NoResponseTsServerRequests>(
command: K,
args: NoResponseTsServerRequests[K][0]
): void;
executeAsync(command: 'geterr', args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise<ServerResponse.Response<Proto.Response>>;

View File

@@ -11,7 +11,7 @@ import BufferSyncSupport from './features/bufferSyncSupport';
import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics';
import * as Proto from './protocol';
import { ITypeScriptServer } from './tsServer/server';
import { ITypeScriptServiceClient, ServerResponse } from './typescriptService';
import { ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService';
import API from './utils/api';
import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration';
import { Disposable } from './utils/dispose';
@@ -607,7 +607,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
return undefined;
}
public execute(command: string, args: any, token: vscode.CancellationToken, lowPriority?: boolean): Promise<ServerResponse.Response<Proto.Response>> {
public execute(command: keyof TypeScriptRequests, args: any, token: vscode.CancellationToken, lowPriority?: boolean): Promise<ServerResponse.Response<Proto.Response>> {
return this.executeImpl(command, args, {
isAsync: false,
token,
@@ -616,7 +616,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
});
}
public executeWithoutWaitingForResponse(command: string, args: any): void {
public executeWithoutWaitingForResponse(command: keyof TypeScriptRequests, args: any): void {
this.executeImpl(command, args, {
isAsync: false,
token: undefined,
@@ -624,7 +624,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
});
}
public executeAsync(command: string, args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise<ServerResponse.Response<Proto.Response>> {
public executeAsync(command: keyof TypeScriptRequests, args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise<ServerResponse.Response<Proto.Response>> {
return this.executeImpl(command, args, {
isAsync: true,
token,
@@ -632,9 +632,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType
});
}
private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
this.bufferSyncSupport.beforeCommand(command);
const runningServerState = this.service();
return runningServerState.server.executeImpl(command, args, executeInfo);
@@ -769,7 +769,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.logTelemetry(telemetryData.telemetryEventName, properties);
}
private configurePlugin(pluginName: string, configuration: {}): any {
if (this.apiVersion.gte(API.v314)) {
this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration });

View File

@@ -50,7 +50,7 @@ export default class Tracer {
return result;
}
public traceRequest(request: Proto.Request, responseExpected: boolean, queueLength: number): void {
public traceRequest(serverId: string, request: Proto.Request, responseExpected: boolean, queueLength: number): void {
if (this.trace === Trace.Off) {
return;
}
@@ -58,10 +58,10 @@ export default class Tracer {
if (this.trace === Trace.Verbose && request.arguments) {
data = `Arguments: ${JSON.stringify(request.arguments, null, 4)}`;
}
this.logTrace(`Sending request: ${request.command} (${request.seq}). Response expected: ${responseExpected ? 'yes' : 'no'}. Current queue length: ${queueLength}`, data);
this.logTrace(serverId, `Sending request: ${request.command} (${request.seq}). Response expected: ${responseExpected ? 'yes' : 'no'}. Current queue length: ${queueLength}`, data);
}
public traceResponse(response: Proto.Response, startTime: number): void {
public traceResponse(serverId: string, response: Proto.Response, startTime: number): void {
if (this.trace === Trace.Off) {
return;
}
@@ -69,17 +69,17 @@ export default class Tracer {
if (this.trace === Trace.Verbose && response.body) {
data = `Result: ${JSON.stringify(response.body, null, 4)}`;
}
this.logTrace(`Response received: ${response.command} (${response.request_seq}). Request took ${Date.now() - startTime} ms. Success: ${response.success} ${!response.success ? '. Message: ' + response.message : ''}`, data);
this.logTrace(serverId, `Response received: ${response.command} (${response.request_seq}). Request took ${Date.now() - startTime} ms. Success: ${response.success} ${!response.success ? '. Message: ' + response.message : ''}`, data);
}
public traceRequestCompleted(command: string, request_seq: number, startTime: number): any {
public traceRequestCompleted(serverId: string, command: string, request_seq: number, startTime: number): any {
if (this.trace === Trace.Off) {
return;
}
this.logTrace(`Async response received: ${command} (${request_seq}). Request took ${Date.now() - startTime} ms.`);
this.logTrace(serverId, `Async response received: ${command} (${request_seq}). Request took ${Date.now() - startTime} ms.`);
}
public traceEvent(event: Proto.Event): void {
public traceEvent(serverId: string, event: Proto.Event): void {
if (this.trace === Trace.Off) {
return;
}
@@ -87,12 +87,12 @@ export default class Tracer {
if (this.trace === Trace.Verbose && event.body) {
data = `Data: ${JSON.stringify(event.body, null, 4)}`;
}
this.logTrace(`Event received: ${event.event} (${event.seq}).`, data);
this.logTrace(serverId, `Event received: ${event.event} (${event.seq}).`, data);
}
public logTrace(message: string, data?: any): void {
public logTrace(serverId: string, message: string, data?: any): void {
if (this.trace !== Trace.Off) {
this.logger.logLevel('Trace', message, data);
this.logger.logLevel('Trace', `<${serverId}> ${message}`, data);
}
}
}