From 3ac4b1e3b788fe67ae74dd5964973f3acd7cd47a Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Wed, 18 Mar 2026 23:44:11 +0000 Subject: [PATCH] perf(linter/plugins): grow `Uint32Array`s by doubling --- apps/oxlint/src-js/plugins/comments.ts | 17 ++++++++++++++++- apps/oxlint/src-js/plugins/tokens.ts | 19 ++++++++++++++++++- .../src-js/plugins/tokens_and_comments.ts | 13 ++++++++++--- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/apps/oxlint/src-js/plugins/comments.ts b/apps/oxlint/src-js/plugins/comments.ts index 30f658a75d188..b267e137f1a6f 100644 --- a/apps/oxlint/src-js/plugins/comments.ts +++ b/apps/oxlint/src-js/plugins/comments.ts @@ -71,6 +71,10 @@ let activeCommentsWithLocCount = 0; let deserializedCommentIndexes = EMPTY_UINT32_ARRAY; let deserializedCommentsLen = 0; +// Minimum capacity (in `u32`s) of `deserializedCommentIndexes`, when not empty. +// 16 elements = 64 bytes = 1 cache line. +const DESERIALIZED_COMMENT_INDEXES_MIN_CAPACITY = 16; + // Empty comments array. // Reused for all files which don't have any comments. Frozen to avoid rules mutating it. const EMPTY_COMMENTS: CommentType[] = Object.freeze([]) as unknown as CommentType[]; @@ -227,7 +231,18 @@ export function initCommentsBuffer(): void { cachedComments.push(new Comment()); } while (cachedComments.length < commentsLen); - deserializedCommentIndexes = new Uint32Array(commentsLen); + // Grow `deserializedCommentIndexes` if needed. + // `Uint32Array`s can't grow in place, so allocate a new one. + // First allocation uses minimum capacity. Subsequent growths double, to avoid frequent reallocations. + const indexesLen = deserializedCommentIndexes.length; + if (indexesLen < commentsLen) { + deserializedCommentIndexes = new Uint32Array( + Math.max( + commentsLen, + indexesLen === 0 ? DESERIALIZED_COMMENT_INDEXES_MIN_CAPACITY : indexesLen << 1, + ), + ); + } } // If file has a hashbang, eagerly deserialize the first comment, and set its type to `Shebang`. diff --git a/apps/oxlint/src-js/plugins/tokens.ts b/apps/oxlint/src-js/plugins/tokens.ts index fa9f50174b5b6..3ad204a5849b7 100644 --- a/apps/oxlint/src-js/plugins/tokens.ts +++ b/apps/oxlint/src-js/plugins/tokens.ts @@ -151,6 +151,12 @@ const REGEX_INDEXES_MIN_CAPACITY = 16; let deserializedTokenIndexes = EMPTY_UINT32_ARRAY; let deserializedTokensLen = 0; +// Minimum capacity (in `u32`s) of `deserializedTokenIndexes`, when not empty. +// 16 elements = 64 bytes = 1 cache line. +// Note that this default aims to be a reasonable minimum for number of *deserialized* tokens, +// not *total* number of tokens. +const DESERIALIZED_TOKEN_INDEXES_MIN_CAPACITY = 16; + // Reset `#loc` field on a `Token` class instance let resetLoc: (token: Token) => void; @@ -318,7 +324,18 @@ export function initTokensBuffer(): void { cachedTokens.push(new Token()); } while (cachedTokens.length < tokensLen); - deserializedTokenIndexes = new Uint32Array(tokensLen); + // Grow `deserializedTokenIndexes` if needed. + // `Uint32Array`s can't grow in place, so allocate a new one. + // First allocation uses minimum capacity. Subsequent growths double, to avoid frequent reallocations. + const indexesLen = deserializedTokenIndexes.length; + if (indexesLen < tokensLen) { + deserializedTokenIndexes = new Uint32Array( + Math.max( + tokensLen, + indexesLen === 0 ? DESERIALIZED_TOKEN_INDEXES_MIN_CAPACITY : indexesLen << 1, + ), + ); + } } // Check buffer data has valid ranges and ascending order diff --git a/apps/oxlint/src-js/plugins/tokens_and_comments.ts b/apps/oxlint/src-js/plugins/tokens_and_comments.ts index e33806dd3b3eb..a1dcedffe782b 100644 --- a/apps/oxlint/src-js/plugins/tokens_and_comments.ts +++ b/apps/oxlint/src-js/plugins/tokens_and_comments.ts @@ -93,6 +93,10 @@ export let tokensAndCommentsLen = 0; // `tokensAndCommentsUint32` is a view over this buffer's prefix. let tokensAndCommentsBackingUint32 = EMPTY_UINT32_ARRAY; +// Minimum capacity (in `u32`s) of `tokensAndCommentsBackingUint32`, when not empty. +// 256 elements = 1 KiB. +const MERGED_BACKING_MIN_CAPACITY = 256; + /** * Initialize tokens-and-comments buffer. * @@ -117,14 +121,17 @@ export function initTokensAndCommentsBuffer(): void { tokensAndCommentsLen = tokensLen + commentsLen; - // Reuse backing buffer across files. Grow (doubled) if needed, never shrink. + // Reuse backing buffer across files. Grow if needed, never shrink. // After warm-up over first few files, the buffer will be large enough to hold all tokens and comments // for all files, so we avoid allocating a large buffer each time. // +1 entry for sentinel (see below). + // `Uint32Array`s can't grow in place, so allocate a new one. + // First allocation uses minimum capacity. Subsequent growths double, to avoid frequent reallocations. const requiredLen32 = (tokensAndCommentsLen + 1) << MERGED_SIZE32_SHIFT; - if (tokensAndCommentsBackingUint32.length < requiredLen32) { + const backingLen = tokensAndCommentsBackingUint32.length; + if (backingLen < requiredLen32) { tokensAndCommentsBackingUint32 = new Uint32Array( - Math.max(requiredLen32, tokensAndCommentsBackingUint32.length << 1), + Math.max(requiredLen32, backingLen === 0 ? MERGED_BACKING_MIN_CAPACITY : backingLen << 1), ); }