提交 f0942786 编写于 作者: M Matt Bierner

Add experimental setting to use separate server to compute project level diagnostics

For #13953

**Problem**
We'd like to show project wide diagnostics, however at the moment TS server is single threaded. This means that computing all these diagnostics would interrupt other user operations such as completions.

Right now, our advice is to use tasks to get around this limitation (since tasks always run as separate process) however few people actually use tasks.

**Change**
This change adds an experimental `tsserver.experimental.enableProjectDiagnostics` setting (default false) that makes VS Code spawn a separate TS Server that is only used for computing diagnostics. This should help keep the primary syntax server responsive while letting the diagnostics server churn away at project level diagnostics

**Why experimental?**

- We are comporting too many diagnostics. This is bad for larger projects. I don't think TS provides the right APIs to know which files we actually need to request diagnostics on when a file changes.

- This hasn't been fully extensively tested to make sure it plays nicely with feature such as automatic type acquisition or in complex workspace with multiple projects
上级 aca46ac4
......@@ -730,6 +730,12 @@
"default": 3072,
"description": "%configuration.tsserver.maxTsServerMemory%",
"scope": "window"
},
"typescript.tsserver.experimental.enableProjectDiagnostics": {
"type": "boolean",
"default": false,
"description": "%configuration.tsserver.experimental.enableProjectDiagnostics%",
"scope": "window"
}
}
},
......
......@@ -58,6 +58,7 @@
"configuration.suggest.paths": "Enable/disable suggestions for paths in import statements and require calls.",
"configuration.tsserver.useSeparateSyntaxServer": "Enable/disable spawning a separate TypeScript server that can more quickly respond to syntax related operations, such as calculating folding or computing document symbols. Requires using TypeScript 3.4.0 or newer in the workspace.",
"configuration.tsserver.maxTsServerMemory": "Set the maximum amount of memory (in MB) to allocate to the TypeScript server process",
"configuration.tsserver.experimental.enableProjectDiagnostics": "(Experimental) Enables project wide error reporting. Requires using TypeScript 3.8 or newer in the workspace.",
"typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Requires using TypeScript 2.6.0 or newer in the workspace. Default of `null` uses VS Code's locale.",
"javascript.implicitProjectConfig.experimentalDecorators": "Enable/disable `experimentalDecorators` for JavaScript files that are not part of a project. Existing jsconfig.json or tsconfig.json files override this setting. Requires using TypeScript 2.3.1 or newer in the workspace.",
"configuration.suggest.autoImports": "Enable/disable auto import suggestions. Requires using TypeScript 2.6.1 or newer in the workspace.",
......
......@@ -289,19 +289,25 @@ class GetErrRequest {
public readonly files: ResourceMap<void>,
onDone: () => void
) {
const args: Proto.GeterrRequestArgs = {
delay: 0,
files: coalesce(Array.from(files.entries).map(entry => client.normalizedPath(entry.resource)))
};
const allFiles = coalesce(Array.from(files.entries).map(entry => client.normalizedPath(entry.resource)));
if (!allFiles.length) {
this._done = true;
onDone();
} else {
const request = client.configuration.enableProjectDiagnostics
// Note that geterrForProject is almost certainly not the api we want here as it ends up computing far
// too many diagnostics
? client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token)
: client.executeAsync('geterr', { delay: 0, files: allFiles }, this._token.token);
client.executeAsync('geterr', args, this._token.token)
.finally(() => {
request.finally(() => {
if (this._done) {
return;
}
this._done = true;
onDone();
});
}
}
public cancel(): any {
......@@ -454,7 +460,9 @@ export default class BufferSyncSupport extends Disposable {
}
public interuptGetErr<R>(f: () => R): R {
if (!this.pendingGetErr) {
if (!this.pendingGetErr
|| this.client.configuration.enableProjectDiagnostics // `geterr` happens on seperate server so no need to cancel it.
) {
return f();
}
......
......@@ -297,19 +297,120 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
}
class RequestRouter {
private static readonly sharedCommands = new Set<keyof TypeScriptRequests>([
'change',
'close',
'open',
'updateOpen',
'configure',
'configurePlugin',
]);
constructor(
private readonly servers: ReadonlyArray<{ readonly server: ITypeScriptServer, readonly preferredCommands?: ReadonlySet<keyof TypeScriptRequests> }>,
private readonly delegate: TsServerDelegate,
) { }
public execute(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
if (RequestRouter.sharedCommands.has(command)) {
// Dispatch shared commands to all server but only return from first one one
const requestStates: RequestState.State[] = this.servers.map(() => RequestState.Unresolved);
// Also make sure we never cancel requests to just one server
let token: vscode.CancellationToken | undefined = undefined;
if (executeInfo.token) {
const source = new vscode.CancellationTokenSource();
executeInfo.token.onCancellationRequested(() => {
if (requestStates.some(state => state === RequestState.Resolved)) {
// Don't cancel.
// One of the servers completed this request so we don't want to leave the other
// in a different state.
return;
}
source.cancel();
});
token = source.token;
}
let firstRequest: Promise<ServerResponse.Response<Proto.Response>> | undefined;
for (let serverIndex = 0; serverIndex < this.servers.length; ++serverIndex) {
const server = this.servers[serverIndex].server;
const request = server.executeImpl(command, args, { ...executeInfo, token });
if (serverIndex === 0) {
firstRequest = request;
}
if (request) {
request
.then(result => {
requestStates[serverIndex] = RequestState.Resolved;
const erroredRequest = requestStates.find(state => state.type === RequestState.Type.Errored) as RequestState.Errored | undefined;
if (erroredRequest) {
// We've gone out of sync
this.delegate.onFatalError(command, erroredRequest.err);
}
return result;
}, err => {
requestStates[serverIndex] = new RequestState.Errored(err);
if (requestStates.some(state => state === RequestState.Resolved)) {
// We've gone out of sync
this.delegate.onFatalError(command, err);
}
throw err;
});
}
}
return firstRequest;
}
for (const { preferredCommands, server } of this.servers) {
if (!preferredCommands || preferredCommands.has(command)) {
return server.executeImpl(command, args, executeInfo);
}
}
throw new Error(`Could not find server for command: '${command}'`);
}
}
export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer {
private static readonly syntaxCommands = new Set<keyof TypeScriptRequests>([
'navtree',
'getOutliningSpans',
'jsxClosingTag',
'selectionRange',
'format',
'formatonkey',
'docCommentTemplate',
]);
private readonly syntaxServer: ITypeScriptServer;
private readonly semanticServer: ITypeScriptServer;
private readonly router: RequestRouter;
public constructor(
servers: { syntax: ITypeScriptServer, semantic: ITypeScriptServer },
private readonly _delegate: TsServerDelegate,
delegate: TsServerDelegate,
) {
super();
this.syntaxServer = servers.syntax;
this.semanticServer = servers.semantic;
this.router = new RequestRouter(
[
{ server: this.syntaxServer, preferredCommands: SyntaxRoutingTsServer.syntaxCommands },
{ server: this.semanticServer, preferredCommands: undefined /* gets all other commands */ }
],
delegate);
this._register(this.syntaxServer.onEvent(e => this._onEvent.fire(e)));
this._register(this.semanticServer.onEvent(e => this._onEvent.fire(e)));
......@@ -338,95 +439,87 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
this.semanticServer.kill();
}
private static readonly syntaxCommands = new Set<keyof TypeScriptRequests>([
'navtree',
'getOutliningSpans',
'jsxClosingTag',
'selectionRange',
'format',
'formatonkey',
'docCommentTemplate',
]);
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)) {
// Dispatch to both server but only return from syntax one
return this.router.execute(command, args, executeInfo);
}
}
let syntaxRequestState: RequestState.State = RequestState.Unresolved;
let semanticRequestState: RequestState.State = RequestState.Unresolved;
// Also make sure we never cancel requests to just one server
let token: vscode.CancellationToken | undefined = undefined;
if (executeInfo.token) {
const source = new vscode.CancellationTokenSource();
executeInfo.token.onCancellationRequested(() => {
if (syntaxRequestState !== RequestState.Unresolved && semanticRequestState === RequestState.Unresolved
|| syntaxRequestState === RequestState.Unresolved && semanticRequestState !== RequestState.Unresolved
) {
// Don't cancel.
// One of the servers completed this request so we don't want to leave the other
// in a different state
return;
}
source.cancel();
});
token = source.token;
}
export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServer {
const semanticRequest = this.semanticServer.executeImpl(command, args, { ...executeInfo, token });
if (semanticRequest) {
semanticRequest
.then(result => {
semanticRequestState = RequestState.Resolved;
if (syntaxRequestState.type === RequestState.Type.Errored) {
// We've gone out of sync
this._delegate.onFatalError(command, syntaxRequestState.err);
}
return result;
}, err => {
semanticRequestState = new RequestState.Errored(err);
if (syntaxRequestState === RequestState.Resolved) {
// We've gone out of sync
this._delegate.onFatalError(command, err);
}
throw err;
});
private static readonly diagnosticEvents = new Set([
'configFileDiag',
'syntaxDiag',
'semanticDiag',
'suggestionDiag'
]);
private readonly getErrServer: ITypeScriptServer;
private readonly mainServer: ITypeScriptServer;
private readonly router: RequestRouter;
public constructor(
servers: { getErr: ITypeScriptServer, primary: ITypeScriptServer },
delegate: TsServerDelegate,
) {
super();
this.getErrServer = servers.getErr;
this.mainServer = servers.primary;
this.router = new RequestRouter(
[
{ server: this.getErrServer, preferredCommands: new Set<keyof TypeScriptRequests>(['geterr', 'geterrForProject']) },
{ server: this.mainServer, preferredCommands: undefined /* gets all other commands */ }
],
delegate);
this._register(this.getErrServer.onEvent(e => {
if (GetErrRoutingTsServer.diagnosticEvents.has(e.event)) {
this._onEvent.fire(e);
}
const syntaxRequest = this.syntaxServer.executeImpl(command, args, { ...executeInfo, token });
if (syntaxRequest) {
syntaxRequest
.then(result => {
syntaxRequestState = RequestState.Resolved;
if (semanticRequestState.type === RequestState.Type.Errored) {
// We've gone out of sync
this._delegate.onFatalError(command, semanticRequestState.err);
}
return result;
}, err => {
syntaxRequestState = new RequestState.Errored(err);
if (semanticRequestState === RequestState.Resolved) {
// We've gone out of sync
this._delegate.onFatalError(command, err);
}
throw err;
});
// Ignore all other events
}));
this._register(this.mainServer.onEvent(e => {
if (!GetErrRoutingTsServer.diagnosticEvents.has(e.event)) {
this._onEvent.fire(e);
}
return syntaxRequest;
} else {
return this.semanticServer.executeImpl(command, args, executeInfo);
}
// Ignore all other events
}));
this._register(this.getErrServer.onError(e => this._onError.fire(e)));
this._register(this.mainServer.onError(e => this._onError.fire(e)));
this._register(this.mainServer.onExit(e => {
this._onExit.fire(e);
this.getErrServer.kill();
}));
}
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.mainServer.onReaderError; }
public get tsServerLogFile() { return this.mainServer.tsServerLogFile; }
public kill(): void {
this.getErrServer.kill();
this.mainServer.kill();
}
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 {
return this.router.execute(command, args, executeInfo);
}
}
......
......@@ -18,9 +18,14 @@ import { PluginManager } from '../utils/plugins';
import { TelemetryReporter } from '../utils/telemetry';
import Tracer from '../utils/tracer';
import { TypeScriptVersion, TypeScriptVersionProvider } from '../utils/versionProvider';
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess, TsServerDelegate } from './server';
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess, TsServerDelegate, GetErrRoutingTsServer } from './server';
type ServerKind = 'main' | 'syntax' | 'semantic';
const enum ServerKind {
Main = 'main',
Syntax = 'syntax',
Semantic = 'semantic',
Diagnostics = 'diagnostics'
}
export class TypeScriptServerSpawner {
public constructor(
......@@ -38,13 +43,24 @@ export class TypeScriptServerSpawner {
pluginManager: PluginManager,
delegate: TsServerDelegate,
): ITypeScriptServer {
let primaryServer: ITypeScriptServer;
if (this.shouldUseSeparateSyntaxServer(version, configuration)) {
const syntaxServer = this.spawnTsServer('syntax', version, configuration, pluginManager);
const semanticServer = this.spawnTsServer('semantic', version, configuration, pluginManager);
return new SyntaxRoutingTsServer({ syntax: syntaxServer, semantic: semanticServer }, delegate);
primaryServer = new SyntaxRoutingTsServer({
syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager),
semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager)
}, delegate);
} else {
primaryServer = this.spawnTsServer(ServerKind.Main, version, configuration, pluginManager);
}
return this.spawnTsServer('main', version, configuration, pluginManager);
if (this.shouldUseSeparateDiagnosticsServer(version, configuration)) {
return new GetErrRoutingTsServer({
getErr: this.spawnTsServer(ServerKind.Diagnostics, version, configuration, pluginManager),
primary: primaryServer,
}, delegate);
}
return primaryServer;
}
private shouldUseSeparateSyntaxServer(
......@@ -54,6 +70,13 @@ export class TypeScriptServerSpawner {
return configuration.useSeparateSyntaxServer && !!version.apiVersion && version.apiVersion.gte(API.v340);
}
private shouldUseSeparateDiagnosticsServer(
version: TypeScriptVersion,
configuration: TypeScriptServiceConfiguration,
): boolean {
return configuration.enableProjectDiagnostics && !!version.apiVersion && version.apiVersion.gte(API.v380);
}
private spawnTsServer(
kind: ServerKind,
version: TypeScriptVersion,
......@@ -107,7 +130,7 @@ export class TypeScriptServerSpawner {
const args: string[] = [];
let tsServerLogFile: string | undefined;
if (kind === 'syntax') {
if (kind === ServerKind.Syntax) {
args.push('--syntaxOnly');
}
......@@ -117,11 +140,11 @@ export class TypeScriptServerSpawner {
args.push('--useSingleInferredProject');
}
if (configuration.disableAutomaticTypeAcquisition || kind === 'syntax') {
if (configuration.disableAutomaticTypeAcquisition || kind === ServerKind.Syntax || kind === ServerKind.Diagnostics) {
args.push('--disableAutomaticTypingAcquisition');
}
if (kind !== 'syntax') {
if (kind === ServerKind.Semantic || kind === ServerKind.Main) {
args.push('--enableTelemetry');
}
......
......@@ -74,6 +74,7 @@ interface NoResponseTsServerRequests {
interface AsyncTsServerRequests {
'geterr': [Proto.GeterrRequestArgs, Proto.Response];
'geterrForProject': [Proto.GeterrForProjectRequestArgs, Proto.Response];
}
export type TypeScriptRequests = StandardTsServerRequests & NoResponseTsServerRequests & AsyncTsServerRequests;
......@@ -137,7 +138,11 @@ export interface ITypeScriptServiceClient {
args: NoResponseTsServerRequests[K][0]
): void;
executeAsync(command: 'geterr', args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise<ServerResponse.Response<Proto.Response>>;
executeAsync<K extends keyof AsyncTsServerRequests>(
command: K,
args: AsyncTsServerRequests[K][0],
token: vscode.CancellationToken
): Promise<ServerResponse.Response<Proto.Response>>;
/**
* Cancel on going geterr requests and re-queue them after `f` has been evaluated.
......
......@@ -55,6 +55,7 @@ export class TypeScriptServiceConfiguration {
public readonly experimentalDecorators: boolean;
public readonly disableAutomaticTypeAcquisition: boolean;
public readonly useSeparateSyntaxServer: boolean;
public readonly enableProjectDiagnostics: boolean;
public readonly maxTsServerMemory: number;
public static loadFromWorkspace(): TypeScriptServiceConfiguration {
......@@ -74,6 +75,7 @@ export class TypeScriptServiceConfiguration {
this.experimentalDecorators = TypeScriptServiceConfiguration.readExperimentalDecorators(configuration);
this.disableAutomaticTypeAcquisition = TypeScriptServiceConfiguration.readDisableAutomaticTypeAcquisition(configuration);
this.useSeparateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration);
this.enableProjectDiagnostics = TypeScriptServiceConfiguration.readEnableProjectDiagnostics(configuration);
this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration);
}
......@@ -88,6 +90,7 @@ export class TypeScriptServiceConfiguration {
&& this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition
&& arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths)
&& this.useSeparateSyntaxServer === other.useSeparateSyntaxServer
&& this.enableProjectDiagnostics === other.enableProjectDiagnostics
&& this.maxTsServerMemory === other.maxTsServerMemory;
}
......@@ -150,6 +153,10 @@ export class TypeScriptServiceConfiguration {
return configuration.get<boolean>('typescript.tsserver.useSeparateSyntaxServer', true);
}
private static readEnableProjectDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.tsserver.experimental.enableProjectDiagnostics', false);
}
private static readMaxTsServerMemory(configuration: vscode.WorkspaceConfiguration): number {
const defaultMaxMemory = 3072;
const minimumMaxMemory = 128;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册