java-topology/defects/bazel/patch/bazel-0003-feature-selection-hashset.md

2 KiB
Raw Blame History

UNDF: UNDF-2026-000000581

bazel-0003: FeatureSelection ImmutableList.contains O(P×S×L) → O(P×S) with HashSet

Classification

Field Value
CWE CWE-407 Inefficient Algorithmic Complexity
Severity MEDIUM
Component src/main/java/com/google/devtools/build/lib/rules/cpp/FeatureSelection.java:159
Function FeatureSelection.run() — provides-conflict check
Hot path Once per action type per C++ target in computeFeatureConfiguration()
Status PATCHED (unit test PASS)

Defect

FeatureSelection.run() iterates over provides.keys() (P unique provide-strings) and for each iterates provides.get(provided) (S selectables), calling enabledActivatablesInOrder.contains(...) — an O(L) scan of an ImmutableList:

// FeatureSelection.java:159
ImmutableList<CrosstoolSelectable> enabledActivatablesInOrder = ...;  // L entries

for (String provided : provides.keys()) {                              // P provide-strings
    for (CrosstoolSelectable selectable : provides.get(provided)) {    // S selectables
        if (enabledActivatablesInOrder.contains(                       // O(L) linear scan
                selectableProvidingString)) {
            ...
        }
    }
}

ImmutableList.contains() is O(L) — walks every element. With P=7, S=6, L=80: ~3,360 comparisons per call vs ~42 with a HashSet (80×).

computeFeatureConfiguration() is called once per action type per target in C++ builds. Large monorepo builds with thousands of C++ targets hit this path repeatedly.

Fix

Build a HashSet<CrosstoolSelectable> from enabledActivatablesInOrder once before the outer loop:

Set<CrosstoolSelectable> enabledSet = new HashSet<>(enabledActivatablesInOrder);

for (String provided : provides.keys()) {
    for (CrosstoolSelectable selectable : provides.get(provided)) {
        if (enabledSet.contains(selectableProvidingString)) {   // O(1)
            ...
        }
    }
}

Speedup: ~80× at realistic scale (L=80, P=7, S=6).