From 0c28bab84282208660fc01c534f4944a580ff10c Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 21 Jun 2022 15:49:40 -0700 Subject: [PATCH 1/2] Add a simple queue implementation with better performance than `Array.shift` This lets us clean up the hack introduced in #49581 --- src/compiler/binder.ts | 13 +++++----- src/compiler/core.ts | 40 +++++++++++++++++++++++++++++++ src/compiler/types.ts | 7 ++++++ src/harness/client.ts | 6 ++--- src/server/session.ts | 32 ++++++++----------------- src/services/findAllReferences.ts | 10 ++++---- src/tsserver/nodeServer.ts | 16 ++++++------- 7 files changed, 80 insertions(+), 44 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 2bd96fe334e8d..1a817ee006ae2 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -3509,10 +3509,11 @@ namespace ts { export function isExportsOrModuleExportsOrAlias(sourceFile: SourceFile, node: Expression): boolean { let i = 0; - const q = [node]; - while (q.length && i < 100) { + const q = createQueue(); + q.enqueue(node); + while (!q.isEmpty() && i < 100) { i++; - node = q.shift()!; + node = q.dequeue(); if (isExportsIdentifier(node) || isModuleExportsAccessExpression(node)) { return true; } @@ -3520,10 +3521,10 @@ namespace ts { const symbol = lookupSymbolForName(sourceFile, node.escapedText); if (!!symbol && !!symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && !!symbol.valueDeclaration.initializer) { const init = symbol.valueDeclaration.initializer; - q.push(init); + q.enqueue(init); if (isAssignmentExpression(init, /*excludeCompoundAssignment*/ true)) { - q.push(init.left); - q.push(init.right); + q.enqueue(init.left); + q.enqueue(init.right); } } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 2875f4235a691..48854004be4fe 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1492,6 +1492,46 @@ namespace ts { return createMultiMap() as UnderscoreEscapedMultiMap; } + export function createQueue(items?: readonly T[]): Queue { + const elements: (T | undefined)[] = items?.slice() || []; + let headIndex = 0; + + function isEmpty() { + return headIndex === elements.length; + } + + function enqueue(...items: T[]) { + elements.push(...items); + } + + function dequeue(): T { + if (isEmpty()) { + throw new Error("Queue is empty"); + } + + const result = elements[headIndex] as T; + elements[headIndex] = undefined; // Don't keep referencing dequeued item + headIndex++; + + // If more than half of the queue is empty, copy the remaining elements to the + // front and shrink the array (unless we'd be saving fewer than 100 slots) + if (headIndex > 100 && headIndex > (elements.length >> 1)) { + const newLength = elements.length - headIndex; + elements.copyWithin(/*target*/ 0, /*&tart*/ headIndex); + elements.length = newLength; + headIndex = 0; + } + + return result; + } + + return { + enqueue, + dequeue, + isEmpty, + }; + } + /** * Creates a Set with custom equality and hash code functionality. This is useful when you * want to use something looser than object identity - e.g. "has the same span". diff --git a/src/compiler/types.ts b/src/compiler/types.ts index caaad5fb9ece0..cfe6b2c660b1a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8998,4 +8998,11 @@ namespace ts { negative: boolean; base10Value: string; } + + /* @internal */ + export interface Queue { + enqueue(...items: T[]): void; + dequeue(): T; + isEmpty(): boolean; + } } diff --git a/src/harness/client.ts b/src/harness/client.ts index b72103d363044..96f720d526eba 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -36,7 +36,7 @@ namespace ts.server { export class SessionClient implements LanguageService { private sequence = 0; private lineMaps = new Map(); - private messages: string[] = []; + private messages = createQueue(); private lastRenameEntry: RenameEntry | undefined; private preferences: UserPreferences | undefined; @@ -44,7 +44,7 @@ namespace ts.server { } public onMessage(message: string): void { - this.messages.push(message); + this.messages.enqueue(message); } private writeMessage(message: string): void { @@ -95,7 +95,7 @@ namespace ts.server { let foundResponseMessage = false; let response!: T; while (!foundResponseMessage) { - const lastMessage = this.messages.shift()!; + const lastMessage = this.messages.dequeue()!; Debug.assert(!!lastMessage, "Did not receive any responses."); const responseBody = extractMessage(lastMessage); try { diff --git a/src/server/session.ts b/src/server/session.ts index 38bbe0d53c849..0fe57694d1adf 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -504,18 +504,18 @@ namespace ts.server { // If `getResultsForPosition` returns results for a project, they go in here const resultsMap = new Map(); - const queue: ProjectAndLocation[] = []; + const queue = createQueue(); // In order to get accurate isDefinition values for `defaultProject`, // we need to ensure that it is searched from `initialLocation`. // The easiest way to do this is to search it first. - queue.push({ project: defaultProject, location: initialLocation }); + queue.enqueue({ project: defaultProject, location: initialLocation }); // This will queue `defaultProject` a second time, but it will be dropped // as a dup when it is dequeued. forEachProjectInProjects(projects, initialLocation.fileName, (project, path) => { const location = { fileName: path!, pos: initialLocation.pos }; - queue.push({ project, location }); + queue.enqueue({ project, location }); }); const projectService = defaultProject.projectService; @@ -536,25 +536,13 @@ namespace ts.server { const searchedProjectKeys = new Set(); onCancellation: - while (queue.length) { - while (queue.length) { + while (!queue.isEmpty()) { + while (!queue.isEmpty()) { if (cancellationToken.isCancellationRequested()) break onCancellation; - let skipCount = 0; - for (; skipCount < queue.length && resultsMap.has(queue[skipCount].project); skipCount++); - - if (skipCount === queue.length) { - queue.length = 0; - break; - } - - if (skipCount > 0) { - queue.splice(0, skipCount); - } - - // NB: we may still skip if it's a project reference redirect - const { project, location } = queue.shift()!; + const { project, location } = queue.dequeue(); + if (resultsMap.has(project)) continue; if (isLocationProjectReferenceRedirect(project, location)) continue; const projectResults = searchPosition(project, location); @@ -574,7 +562,7 @@ namespace ts.server { if (resultsMap.has(project)) return; // Can loop forever without this (enqueue here, dequeue above, repeat) const location = mapDefinitionInProject(defaultDefinition, project, getGeneratedDefinition, getSourceDefinition); if (location) { - queue.push({ project, location }); + queue.enqueue({ project, location }); } }); } @@ -604,7 +592,7 @@ namespace ts.server { for (const project of originalScriptInfo.containingProjects) { if (!project.isOrphan() && !resultsMap.has(project)) { // Optimization: don't enqueue if will be discarded - queue.push({ project, location: originalLocation }); + queue.enqueue({ project, location: originalLocation }); } } @@ -613,7 +601,7 @@ namespace ts.server { symlinkedProjectsMap.forEach((symlinkedProjects, symlinkedPath) => { for (const symlinkedProject of symlinkedProjects) { if (!symlinkedProject.isOrphan() && !resultsMap.has(symlinkedProject)) { // Optimization: don't enqueue if will be discarded - queue.push({ project: symlinkedProject, location: { fileName: symlinkedPath as string, pos: originalLocation.pos } }); + queue.enqueue({ project: symlinkedProject, location: { fileName: symlinkedPath as string, pos: originalLocation.pos } }); } } }); diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index b9ba94daef0fa..6caef92ac12f3 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -240,18 +240,18 @@ namespace ts.FindAllReferences { ) { referenceEntries = entries && [...entries]; } - else { - const queue = entries && [...entries]; + else if (entries) { + const queue = createQueue(entries); const seenNodes = new Map(); - while (queue && queue.length) { - const entry = queue.shift() as NodeEntry; + while (!queue.isEmpty()) { + const entry = queue.dequeue() as NodeEntry; if (!addToSeen(seenNodes, getNodeId(entry.node))) { continue; } referenceEntries = append(referenceEntries, entry); const entries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, entry.node, entry.node.pos); if (entries) { - queue.push(...entries); + queue.enqueue(...entries); } } } diff --git a/src/tsserver/nodeServer.ts b/src/tsserver/nodeServer.ts index e17a7dd37e467..984e6adbbc6b1 100644 --- a/src/tsserver/nodeServer.ts +++ b/src/tsserver/nodeServer.ts @@ -194,7 +194,7 @@ namespace ts.server { } }; - const pending: Buffer[] = []; + const pending = createQueue(); let canWrite = true; if (useWatchGuard) { @@ -334,7 +334,7 @@ namespace ts.server { function writeMessage(buf: Buffer) { if (!canWrite) { - pending.push(buf); + pending.enqueue(buf); } else { canWrite = false; @@ -344,8 +344,8 @@ namespace ts.server { function setCanWriteFlagAndWriteMessageIfNecessary() { canWrite = true; - if (pending.length) { - writeMessage(pending.shift()!); + if (!pending.isEmpty()) { + writeMessage(pending.dequeue()); } } @@ -430,7 +430,7 @@ namespace ts.server { private installer!: NodeChildProcess; private projectService!: ProjectService; private activeRequestCount = 0; - private requestQueue: QueuedOperation[] = []; + private requestQueue = createQueue(); private requestMap = new Map(); // Maps operation ID to newest requestQueue entry with that ID /** We will lazily request the types registry on the first call to `isKnownTypesPackageName` and store it in `typesRegistryCache`. */ private requestedRegistry = false; @@ -567,7 +567,7 @@ namespace ts.server { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Deferring request for: ${operationId}`); } - this.requestQueue.push(queuedRequest); + this.requestQueue.enqueue(queuedRequest); this.requestMap.set(operationId, queuedRequest); } } @@ -649,8 +649,8 @@ namespace ts.server { Debug.fail("Received too many responses"); } - while (this.requestQueue.length > 0) { - const queuedRequest = this.requestQueue.shift()!; + while (!this.requestQueue.isEmpty()) { + const queuedRequest = this.requestQueue.dequeue(); if (this.requestMap.get(queuedRequest.operationId) === queuedRequest) { this.requestMap.delete(queuedRequest.operationId); this.scheduleRequest(queuedRequest); From ec9c6e55055f07ae3a6a41d647d37f4c5a5128f9 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 23 Jun 2022 10:18:36 -0700 Subject: [PATCH 2/2] Correct typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz BurzyƄski --- src/compiler/core.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 48854004be4fe..9ae196feefade 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1517,7 +1517,8 @@ namespace ts { // front and shrink the array (unless we'd be saving fewer than 100 slots) if (headIndex > 100 && headIndex > (elements.length >> 1)) { const newLength = elements.length - headIndex; - elements.copyWithin(/*target*/ 0, /*&tart*/ headIndex); + elements.copyWithin(/*target*/ 0, /*start*/ headIndex); + elements.length = newLength; headIndex = 0; }