Skip to content

perf(linter/plugins): store specificity in a single integer#16835

Merged
graphite-app[bot] merged 1 commit intomainfrom
12-14-perf_linter_plugins_store_specificity_in_a_single_integer
Dec 14, 2025
Merged

perf(linter/plugins): store specificity in a single integer#16835
graphite-app[bot] merged 1 commit intomainfrom
12-14-perf_linter_plugins_store_specificity_in_a_single_integer

Conversation

@overlookmotel
Copy link
Copy Markdown
Member

@overlookmotel overlookmotel commented Dec 14, 2025

#16834 implemented sorting visit methods by specificity. Speed up the sorting by storing specificity as a single integer, rather than 2 separate properties identifierCount and attributeCount.

See comment in selector.ts for further explanation.

Note: I've checked that minifier const-folds and inlines all the consts in release build.

@github-actions github-actions bot added A-linter Area - Linter A-cli Area - CLI A-linter-plugins Area - Linter JS plugins C-performance Category - Solution not expected to change functional behavior, only performance labels Dec 14, 2025
Copy link
Copy Markdown
Member Author

overlookmotel commented Dec 14, 2025


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes the performance of ESLint plugin visitor sorting by consolidating the identifierCount and attributeCount properties into a single specificity integer. The optimization uses bit packing to store both counts plus an exit flag in a single 31-bit signed integer, reducing the number of comparisons needed during sorting from two integer comparisons to one.

Key changes:

  • Introduces a bit-packed specificity field (15 bits for identifier count, 14 bits for attribute count, 1 bit for exit flag)
  • Simplifies the sorting comparator from a two-field comparison to a single integer comparison
  • Adds helper functions and constants to manage the bit layout while maintaining the exact same sorting behavior

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
apps/oxlint/src-js/plugins/visitor.ts Replaces separate attributeCount and identifierCount fields with single specificity field in VisitProp interface; updates all usage sites to use bitwise operations; simplifies sorting logic
apps/oxlint/src-js/plugins/selector.ts Defines bit layout constants, exports EXIT_FLAG and IDENTIFIER_COUNT_INCREMENT; replaces separate count fields with specificity in Selector interface; adds helper functions for extracting counts; updates selector analysis to increment packed specificity

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

Packed specificity looks correct and should speed up sorting, but a few details could be tightened for long-term maintainability and perf: the decoding helpers in selector.ts are only used for debug asserts and can be simplified/inlined, and the bit-layout comment should explicitly state that the top-bit EXIT_FLAG intentionally dominates ordering. In visitor.ts, the "*" path now relies on implicit initialization for specificity and would benefit from a small explicit comment/branch to avoid future regressions.

Additional notes (4)
  • Performance | apps/oxlint/src-js/plugins/selector.ts:50-71
    identifierCount() and attributeCount() always shift by IDENTIFIER_COUNT_SHIFT (0) / ATTRIBUTE_COUNT_SHIFT and then mask. The >> IDENTIFIER_COUNT_SHIFT is currently a no-op, and more importantly the helpers are only used for debugAssert checks.

Given this code is in a hot module for perf work, keeping these helpers (and bit extraction) around may add avoidable overhead/indirection unless they’re used elsewhere. If the intent is only to bound-check during debug builds, consider either:

  • inlining the bounds checks directly where increments happen, or
  • moving these helpers behind a debug-only gate (if your build tooling supports dead-code elimination), or
  • removing IDENTIFIER_COUNT_SHIFT entirely (since it’s always 0) and simplifying extraction to specificity & IDENTIFIER_COUNT_MAX.

This will also reduce the chance someone later changes the bit layout but forgets to update the decoding helpers.

  • Maintainability | apps/oxlint/src-js/plugins/selector.ts:28-60
    The comment says: “Attribute count takes precedence in sorting, so goes in the higher bits.” With the current layout, specificity comparison sorts primarily by attribute count (higher bits) and then by identifier count (lower bits), which matches the comment.

However, the exit flag is placed above both counts, and is intended to ensure :exit comes after :enter for leaf nodes. That works, but it also means exit-ness always dominates over attribute/identifier specificity when sorting (since it’s the top bit). If you ever have both enter and exit functions in the same merged array (or if this sorting is reused elsewhere), you’re effectively saying “all exits after all enters”, even if an exit selector is more/less specific.

That may be intended, but it’s a semantic change versus the prior approach where the exit flag was also encoded into attributeCount and similarly dominated. The risk is mainly future maintainers expecting specificity-only ordering across enter/exit and getting surprising results.

  • Readability | apps/oxlint/src-js/plugins/visitor.ts:286-296
    When name === "*", this code intentionally skips parsing, leaving specificity as either 0 (enter) or EXIT_FLAG (exit). Previously, identifierCount was explicitly set to 0 for *.

This is correct, but it’s now implicit and easier to miss when someone changes the default initialization behavior (especially since the removed comment mentioned default identifier count behavior). Adding a small explicit line or comment would make the behavior less fragile.

  • Maintainability | apps/oxlint/src-js/plugins/visitor.ts:287-298
    selector.specificity already encodes identifier count, attribute count, and exit flag. Using bitwise-OR to merge (|=) assumes the destination bitfields are zeroed (except possibly the exit flag) and that no bits overlap. This is true for the current control flow, but it’s brittle: if visitProp.specificity ever accumulates other bits (or gets re-used without resetting correctly), OR will silently produce incorrect counts. It also makes the intention slightly unclear: for counts, this is conceptually an addition of disjoint fields.

Given you already assign specificity = EXIT_FLAG for exit fns, you can make this robust and self-documenting by combining via addition while preserving the exit bit explicitly (or by masking the exit bit).

Summary of changes

Summary

This change optimizes selector/visitor sorting by packing selector specificity into a single integer.

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

  • Adds a packed specificity representation combining:
    • identifier count
    • attribute count
    • an exit-flag bit
  • Introduces bit layout constants (IDENTIFIER_COUNT_BITS, ATTRIBUTE_COUNT_BITS, shifts, increments) and decoding helpers.
  • Replaces Selector.attributeCount/Selector.identifierCount with Selector.specificity and updates analyzeSelector() to increment the packed value (with debugAssert bounds checks).

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

  • Replaces VisitProp.attributeCount/VisitProp.identifierCount with VisitProp.specificity.
  • Imports EXIT_FLAG and IDENTIFIER_COUNT_INCREMENT from selector.ts and uses them when compiling visitors.
  • Simplifies visit fn sorting to a.specificity - b.specificity with selector string as tie-breaker.

@charliecreates charliecreates bot removed the request for review from CharlieHelps December 14, 2025 15:24
@overlookmotel overlookmotel force-pushed the 12-14-perf_linter_plugins_store_specificity_in_a_single_integer branch from ec410d6 to b5fcbd3 Compare December 14, 2025 15:28
@overlookmotel overlookmotel force-pushed the 12-14-fix_linter_plugins_sort_visitors_in_order_of_specificity branch from 0300b0e to 3764771 Compare December 14, 2025 15:28
@overlookmotel overlookmotel force-pushed the 12-14-perf_linter_plugins_store_specificity_in_a_single_integer branch from b5fcbd3 to 0560f45 Compare December 14, 2025 15:29
@overlookmotel overlookmotel self-assigned this Dec 14, 2025
@overlookmotel overlookmotel force-pushed the 12-14-fix_linter_plugins_sort_visitors_in_order_of_specificity branch from 3764771 to ddd1b6b Compare December 14, 2025 15:45
@overlookmotel overlookmotel force-pushed the 12-14-perf_linter_plugins_store_specificity_in_a_single_integer branch from 0560f45 to 3a8660b Compare December 14, 2025 15:45
@overlookmotel overlookmotel added the 0-merge Merge with Graphite Merge Queue label Dec 14, 2025
Copy link
Copy Markdown
Member Author

overlookmotel commented Dec 14, 2025

Merge activity

#16834 implemented sorting visit methods by specificity. Speed up the sorting by storing `specificity` as a single integer, rather than 2 separate properties `identifierCount` and `attributeCount`.

See comment in `selector.ts` for further explanation.

Note: I've checked that minifier const-folds and inlines all the consts in release build.
@graphite-app graphite-app bot force-pushed the 12-14-fix_linter_plugins_sort_visitors_in_order_of_specificity branch from ddd1b6b to 3302fcb Compare December 14, 2025 15:59
@graphite-app graphite-app bot force-pushed the 12-14-perf_linter_plugins_store_specificity_in_a_single_integer branch from 3a8660b to 6b1a7b5 Compare December 14, 2025 15:59
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Dec 14, 2025
Base automatically changed from 12-14-fix_linter_plugins_sort_visitors_in_order_of_specificity to main December 14, 2025 16:05
@graphite-app graphite-app bot merged commit 6b1a7b5 into main Dec 14, 2025
19 checks passed
@graphite-app graphite-app bot deleted the 12-14-perf_linter_plugins_store_specificity_in_a_single_integer branch December 14, 2025 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-linter Area - Linter A-linter-plugins Area - Linter JS plugins C-performance Category - Solution not expected to change functional behavior, only performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants