From 482e802e83598da9bb3c02adb7af71cffa2331aa Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 5 Sep 2017 16:00:19 -0700 Subject: [PATCH] Limit the number of unanswered typings installer requests If we send them all at once, we (apparently) hit a buffer limit in the node IPC channel and both TS Server and the typings installer become unresponsive. --- src/server/server.ts | 62 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index f70e2d0faf..7f6daa92d5 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -236,25 +236,35 @@ namespace ts.server { return `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}.${d.getMilliseconds()}`; } + interface QueuedOperation { + operationId: string; + operation: () => void; + } + class NodeTypingsInstaller implements ITypingsInstaller { private installer: NodeChildProcess; private installerPidReported = false; private socket: NodeSocket; private projectService: ProjectService; - private throttledOperations: ThrottledOperations; private eventSender: EventSender; + private activeRequestCount = 0; + private requestQueue: QueuedOperation[] = []; + private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID + + private static readonly maxActiveRequestCount = 10; + private static readonly requestDelayMillis = 100; + constructor( private readonly telemetryEnabled: boolean, private readonly logger: server.Logger, - host: ServerHost, + private readonly host: ServerHost, eventPort: number, readonly globalTypingsCacheLocation: string, readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, private newLine: string) { - this.throttledOperations = new ThrottledOperations(host); if (eventPort) { const s = net.connect({ port: eventPort }, () => { this.socket = s; @@ -338,12 +348,26 @@ namespace ts.server { this.logger.info(`Scheduling throttled operation: ${JSON.stringify(request)}`); } } - this.throttledOperations.schedule(project.getProjectName(), /*ms*/ 250, () => { + + const operationId = project.getProjectName(); + const operation = () => { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Sending request: ${JSON.stringify(request)}`); } this.installer.send(request); - }); + }; + const queuedRequest: QueuedOperation = { operationId, operation }; + + if (this.activeRequestCount < NodeTypingsInstaller.maxActiveRequestCount) { + this.scheduleRequest(queuedRequest); + } + else { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Deferring request for: ${operationId}`); + } + this.requestQueue.push(queuedRequest); + this.requestMap.set(operationId, queuedRequest); + } } private handleMessage(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { @@ -404,11 +428,39 @@ namespace ts.server { return; } + if (this.activeRequestCount > 0) { + this.activeRequestCount--; + } + else { + Debug.fail("Received too many responses"); + } + + while (this.requestQueue.length > 0) { + const queuedRequest = this.requestQueue.shift(); + if (this.requestMap.get(queuedRequest.operationId) == queuedRequest) { + this.requestMap.delete(queuedRequest.operationId); + this.scheduleRequest(queuedRequest); + break; + } + + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Skipping defunct request for: ${queuedRequest.operationId}`); + } + } + this.projectService.updateTypingsForProject(response); if (response.kind === ActionSet && this.socket) { this.sendEvent(0, "setTypings", response); } } + + private scheduleRequest(request: QueuedOperation) { + if(this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Scheduling request for: ${request.operationId}`); + } + this.activeRequestCount++; + this.host.setTimeout(request.operation, NodeTypingsInstaller.requestDelayMillis); + } } class IOSession extends Session {