perf(linter/plugins): store specificity in a single integer#16835
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
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. |
There was a problem hiding this comment.
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
specificityfield (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.
There was a problem hiding this comment.
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()andattributeCount()always shift byIDENTIFIER_COUNT_SHIFT(0) /ATTRIBUTE_COUNT_SHIFTand then mask. The>> IDENTIFIER_COUNT_SHIFTis currently a no-op, and more importantly the helpers are only used fordebugAssertchecks.
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_SHIFTentirely (since it’s always 0) and simplifying extraction tospecificity & 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,specificitycomparison 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
Whenname === "*", this code intentionally skips parsing, leavingspecificityas either0(enter) orEXIT_FLAG(exit). Previously,identifierCountwas explicitly set to0for*.
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.specificityalready 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: ifvisitProp.specificityever 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
specificityrepresentation 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.identifierCountwithSelector.specificityand updatesanalyzeSelector()to increment the packed value (withdebugAssertbounds checks).
apps/oxlint/src-js/plugins/visitor.ts
- Replaces
VisitProp.attributeCount/VisitProp.identifierCountwithVisitProp.specificity. - Imports
EXIT_FLAGandIDENTIFIER_COUNT_INCREMENTfromselector.tsand uses them when compiling visitors. - Simplifies visit fn sorting to
a.specificity - b.specificitywith selector string as tie-breaker.
ec410d6 to
b5fcbd3
Compare
0300b0e to
3764771
Compare
b5fcbd3 to
0560f45
Compare
3764771 to
ddd1b6b
Compare
0560f45 to
3a8660b
Compare
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.
ddd1b6b to
3302fcb
Compare
3a8660b to
6b1a7b5
Compare

#16834 implemented sorting visit methods by specificity. Speed up the sorting by storing
specificityas a single integer, rather than 2 separate propertiesidentifierCountandattributeCount.See comment in
selector.tsfor further explanation.Note: I've checked that minifier const-folds and inlines all the consts in release build.