Add a simple queue implementation with better performance than Array.shift (#49623)

* Add a simple queue implementation with better performance than `Array.shift`

This lets us clean up the hack introduced in #49581

* Correct typo

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
This commit is contained in:
Andrew Casey 2022-06-24 10:07:28 -07:00 committed by GitHub
parent b24b6a1125
commit 020ef41543
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 81 additions and 44 deletions

View File

@ -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<Expression>();
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);
}
}
}

View File

@ -1492,6 +1492,47 @@ namespace ts {
return createMultiMap() as UnderscoreEscapedMultiMap<T>;
}
export function createQueue<T>(items?: readonly T[]): Queue<T> {
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, /*start*/ 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".

View File

@ -8998,4 +8998,11 @@ namespace ts {
negative: boolean;
base10Value: string;
}
/* @internal */
export interface Queue<T> {
enqueue(...items: T[]): void;
dequeue(): T;
isEmpty(): boolean;
}
}

View File

@ -36,7 +36,7 @@ namespace ts.server {
export class SessionClient implements LanguageService {
private sequence = 0;
private lineMaps = new Map<string, number[]>();
private messages: string[] = [];
private messages = createQueue<string>();
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 {

View File

@ -504,18 +504,18 @@ namespace ts.server {
// If `getResultsForPosition` returns results for a project, they go in here
const resultsMap = new Map<Project, readonly TResult[]>();
const queue: ProjectAndLocation[] = [];
const queue = createQueue<ProjectAndLocation>();
// 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<string>();
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 } });
}
}
});

View File

@ -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<number, true>();
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);
}
}
}

View File

@ -194,7 +194,7 @@ namespace ts.server {
}
};
const pending: Buffer[] = [];
const pending = createQueue<Buffer>();
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<QueuedOperation>();
private requestMap = new Map<string, QueuedOperation>(); // 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);