Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3509,21 +3509,22 @@ 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me this part of the code seems like that order wouldnt matter so always using last element so i wonder if always using last element and keeping tail thats used to add more items and sets array.length to compact array once in a while might be better.
Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't read it carefully when I made the change - I just checked that it would behave the same after. Reading it now, it looks like switching from a queue to a stack would change a BFS to a DFS, which may or may not be desirable.

while (!q.isEmpty() && i < 100) {
i++;
node = q.shift()!;
node = q.dequeue();
if (isExportsIdentifier(node) || isModuleExportsAccessExpression(node)) {
return true;
}
else if (isIdentifier(node)) {
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);
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
7 changes: 7 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8998,4 +8998,11 @@ namespace ts {
negative: boolean;
base10Value: string;
}

/* @internal */
export interface Queue<T> {
enqueue(...items: T[]): void;
dequeue(): T;
isEmpty(): boolean;
}
}
6 changes: 3 additions & 3 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ 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;

constructor(private host: SessionClientHost) {
}

public onMessage(message: string): void {
this.messages.push(message);
this.messages.enqueue(message);
}

private writeMessage(message: string): void {
Expand Down Expand Up @@ -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 {
Expand Down
32 changes: 10 additions & 22 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even here, does order matter if not we could get away with having to move array elements right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe order does matter, though that doesn't necessarily mean we couldn't accomplish the same thing with a stack.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does order matter ? in all find all references etc when aggregating results between project, the order shouldnt matter right ? Yes our baselines may change but from end to end result perspective order shouldnt matter ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the search order affects the value of isDefinition. I believe it's also the case that FAR is asymmetric (B being a reference to A, does not imply that A is a reference to B), but I think we've already decided to ignore that restriction, so it's probably just isDefinition. Now that isDefinition is computed in a post-pass, it's possible that we can drop the strict ordering, but I'm frankly pretty burned out on rewriting this code. I have no objections if you'd like to, but I'd suggest we wait until 4.9, now that we're in the RC period.


// 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;
Expand All @@ -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);
Expand All @@ -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 });
}
});
}
Expand Down Expand Up @@ -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 });
}
}

Expand All @@ -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 } });
}
}
});
Expand Down
10 changes: 5 additions & 5 deletions src/services/findAllReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/tsserver/nodeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ namespace ts.server {
}
};

const pending: Buffer[] = [];
const pending = createQueue<Buffer>();
let canWrite = true;

if (useWatchGuard) {
Expand Down Expand Up @@ -334,7 +334,7 @@ namespace ts.server {

function writeMessage(buf: Buffer) {
if (!canWrite) {
pending.push(buf);
pending.enqueue(buf);
}
else {
canWrite = false;
Expand All @@ -344,8 +344,8 @@ namespace ts.server {

function setCanWriteFlagAndWriteMessageIfNecessary() {
canWrite = true;
if (pending.length) {
writeMessage(pending.shift()!);
if (!pending.isEmpty()) {
writeMessage(pending.dequeue());
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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);
Expand Down