Skip to content

Commit c05db06

Browse files
committed
perf(linter/plugins): speed up initTokensWithComments (#16117)
Follow-on after #16071. Seeing that more performant implementation made me see another couple of optimizations. 1. Fast path for when file has no comments (probably not so uncommon). 2. Extend the "push in runs" approach. Ensure that in the main loop, there's always at least 1 token and 1 comment remaining, and you know which one it is, which reduces branches. 3. Avoid ever reading out of bounds of the arrays (expensive).
1 parent 74e1f3d commit c05db06

File tree

1 file changed

+76
-26
lines changed

1 file changed

+76
-26
lines changed

apps/oxlint/src-js/plugins/tokens.ts

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { createRequire } from "node:module";
66
import { sourceText, initSourceText } from "./source_code.js";
7-
import { debugAssertIsNonNull } from "../utils/asserts.js";
7+
import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.js";
88

99
import type { Comment, Node, NodeOrToken } from "./types.ts";
1010
import type { Span } from "./location.ts";
@@ -184,40 +184,90 @@ function initTokensWithComments() {
184184
debugAssertIsNonNull(tokens);
185185
debugAssertIsNonNull(comments);
186186

187-
// TODO: Replace `range[0]` with `start` once we have our own tokens which have `start` property.
187+
// TODO: Replace `range[0]` with `start` throughout this function
188+
// once we have our own tokens which have `start` property
189+
190+
// Fast paths for file with no comments, or file which is only comments
191+
const commentsLength = comments.length;
192+
if (commentsLength === 0) {
193+
tokensWithComments = tokens;
194+
return;
195+
}
196+
197+
const tokensLength = tokens.length;
198+
if (tokensLength === 0) {
199+
tokensWithComments = comments;
200+
return;
201+
}
202+
203+
// File contains both tokens and comments.
204+
// Fill `tokensWithComments` with the 2 arrays interleaved in source order.
188205
tokensWithComments = [];
189206

190-
const tokensLength = tokens.length,
191-
commentsLength = comments.length;
207+
let tokenIndex = 0,
208+
commentIndex = 0,
209+
token = tokens[0],
210+
comment = comments[0],
211+
tokenStart = token.range[0],
212+
commentStart = comment.range[0];
213+
214+
// Push any leading comments
215+
while (commentStart < tokenStart) {
216+
// Push current comment
217+
tokensWithComments.push(comment);
218+
219+
// If that was last comment, push all remaining tokens, and exit
220+
if (++commentIndex === commentsLength) {
221+
tokensWithComments.push(...tokens.slice(tokenIndex));
222+
debugCheckTokensWithComments();
223+
return;
224+
}
192225

193-
let tokensIndex = 0,
194-
commentsIndex = 0;
195-
while (tokensIndex < tokensLength && commentsIndex < commentsLength) {
196-
let token = tokens[tokensIndex],
197-
comment = comments[commentsIndex];
226+
// Get next comment
227+
comment = comments[commentIndex];
228+
commentStart = comment.range[0];
229+
}
198230

199-
// TODO: Replace `range[0]` with `start` once we have our own tokens which have `start` property.
200-
const nextTokenStart = token.range[0],
201-
nextCommentStart = comment.range[0];
231+
// Push a run of tokens, then a run of comments, and so on, until all tokens and comments are exhausted
232+
while (true) {
233+
// There's at least 1 token and 1 comment remaining, and token is first.
234+
// Push tokens until we reach the next comment or the end.
235+
do {
236+
// Push current token
237+
tokensWithComments.push(token);
202238

203-
if (nextTokenStart < nextCommentStart) {
204-
while (tokensIndex < tokensLength && token.range[0] <= nextCommentStart) {
205-
tokensWithComments.push(token);
206-
token = tokens[++tokensIndex];
239+
// If that was last token, push all remaining comments, and exit
240+
if (++tokenIndex === tokensLength) {
241+
tokensWithComments.push(...comments.slice(commentIndex));
242+
debugCheckTokensWithComments();
243+
return;
207244
}
208-
} else {
209-
while (commentsIndex < commentsLength && comment.range[0] <= nextTokenStart) {
210-
tokensWithComments.push(comment);
211-
comment = comments[++commentsIndex];
245+
246+
// Get next token
247+
token = tokens[tokenIndex];
248+
tokenStart = token.range[0];
249+
} while (tokenStart < commentStart);
250+
251+
// There's at least 1 token and 1 comment remaining, and comment is first.
252+
// Push comments until we reach the next token or the end.
253+
do {
254+
// Push current comment
255+
tokensWithComments.push(comment);
256+
257+
// If that was last comment, push all remaining tokens, and exit
258+
if (++commentIndex === commentsLength) {
259+
tokensWithComments.push(...tokens.slice(tokenIndex));
260+
debugCheckTokensWithComments();
261+
return;
212262
}
213-
}
214-
}
215263

216-
// After one of `tokens` or `comments` is exhausted, directly push the other's elements
217-
while (commentsIndex < commentsLength) tokensWithComments.push(comments[commentsIndex++]);
218-
while (tokensIndex < tokensLength) tokensWithComments.push(tokens[tokensIndex++]);
264+
// Get next comment
265+
comment = comments[commentIndex];
266+
commentStart = comment.range[0];
267+
} while (commentStart < tokenStart);
268+
}
219269

220-
debugCheckTokensWithComments();
270+
debugAssert(false, "Unreachable");
221271
}
222272

223273
/**

0 commit comments

Comments
 (0)