blob: fb80d34ebbe31404293c06f9870bc2314e85ab23 [file]
// Copyright 2026 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.buildtool;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.ConfiguredTargetValue;
import com.google.devtools.build.lib.analysis.ProviderCollection;
import com.google.devtools.build.lib.analysis.RuleConfiguredObjectValue;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.buildtool.TransitiveTargetCounter.Counts;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.AspectClass;
import com.google.devtools.build.lib.packages.AspectDescriptor;
import com.google.devtools.build.lib.packages.AspectParameters;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.skyframe.AspectKeyCreator;
import com.google.devtools.build.lib.skyframe.AspectKeyCreator.AspectKey;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
import com.google.devtools.build.lib.skyframe.config.BuildConfigurationKey;
import com.google.devtools.build.skyframe.CycleInfo;
import com.google.devtools.build.skyframe.ErrorInfo;
import com.google.devtools.build.skyframe.InMemoryGraph;
import com.google.devtools.build.skyframe.InMemoryNodeEntry;
import com.google.devtools.build.skyframe.QueryableGraph.Reason;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.ValueWithMetadata;
import com.google.devtools.build.skyframe.Version;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link TransitiveTargetCounter}. */
@RunWith(JUnit4.class)
public final class TransitiveTargetCounterTest {
private static final BuildConfigurationKey CONFIG_KEY =
BuildConfigurationKey.create(BuildOptions.builder().build());
private static final BuildConfigurationKey TRANSITIONED_CONFIG_KEY =
BuildConfigurationKey.create(
BuildOptions.builder()
.addStarlarkOption(Label.parseCanonicalUnchecked("//pkg:flag"), "transitioned")
.build());
private static final SkyValue NON_RULE_VALUE = new SkyValue() {};
private static final SkyFunctionName OTHER_FUNCTION =
SkyFunctionName.createHermetic("OTHER_TEST_FUNCTION");
private static final AspectClass TEST_ASPECT_CLASS = () -> "test_aspect";
private InMemoryGraph graph;
@Before
public void setUp() {
graph = InMemoryGraph.create();
}
@Test
public void count_emptyRoots_returnsEmptyMap() throws Exception {
assertThat(TransitiveTargetCounter.count(graph, ImmutableSet.of())).isEmpty();
}
@Test
public void count_singleRoot_countsSelfAndActions() throws Exception {
ConfiguredTargetKey root = configuredTargetKey("//pkg:root");
addDoneNode(graph, root, ruleValue(/* actionCount= */ 3));
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(graph, ImmutableSet.of(root));
assertThat(counts).containsExactly(root, new Counts(1, 3));
}
@Test
public void count_diamondDag_countsSharedDependencyOnce() throws Exception {
// root (1 action) -> {left (2 actions), right (3 actions)} -> leaf (5 actions).
// Also attach non-configurable and non-ActionLookupKey Skyframe deps to verify they are
// ignored.
ConfiguredTargetKey leaf = configuredTargetKey("//pkg:leaf");
ConfiguredTargetKey left = configuredTargetKey("//pkg:left");
ConfiguredTargetKey right = configuredTargetKey("//pkg:right");
ConfiguredTargetKey root = configuredTargetKey("//pkg:root");
ConfiguredTargetKey sourceFile = unconfiguredTargetKey("//pkg:src.cc");
SkyKey otherKey = () -> OTHER_FUNCTION;
addDoneNode(graph, sourceFile, ruleValue(/* actionCount= */ 99));
addDoneNode(graph, otherKey, ruleValue(/* actionCount= */ 99));
addDoneNode(graph, leaf, ruleValue(/* actionCount= */ 5), sourceFile, otherKey);
addDoneNode(graph, left, ruleValue(/* actionCount= */ 2), leaf);
addDoneNode(graph, right, ruleValue(/* actionCount= */ 3), leaf);
addDoneNodeWithDepGroups(
graph,
root,
ruleValue(/* actionCount= */ 1),
ImmutableList.of(ImmutableList.of(left, sourceFile), ImmutableList.of(right, otherKey)));
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(graph, ImmutableSet.of(root));
assertThat(counts).containsExactly(root, new Counts(4, 1 + 2 + 3 + 5));
}
@Test
public void count_multipleRootsAndRootDependingOnRoot_countsEachTransitiveClosure()
throws Exception {
// r1 (1) -> r2 (2) -> shared (4)
// r3 (8) -> shared (4)
ConfiguredTargetKey shared = configuredTargetKey("//pkg:shared");
ConfiguredTargetKey r2 = configuredTargetKey("//pkg:r2");
ConfiguredTargetKey r1 = configuredTargetKey("//pkg:r1");
ConfiguredTargetKey r3 = configuredTargetKey("//pkg:r3");
addDoneNode(graph, shared, ruleValue(/* actionCount= */ 4));
addDoneNode(graph, r2, ruleValue(/* actionCount= */ 2), shared);
addDoneNode(graph, r1, ruleValue(/* actionCount= */ 1), r2);
addDoneNode(graph, r3, ruleValue(/* actionCount= */ 8), shared);
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(graph, ImmutableSet.of(r1, r2, r3));
assertThat(counts)
.containsExactly(
r1, new Counts(3, 1 + 2 + 4),
r2, new Counts(2, 2 + 4),
r3, new Counts(2, 8 + 4));
}
@Test
public void count_aspectsAndNonRuleIntermediateNodes_countsRulesAndAspectsOnly()
throws Exception {
// root (rule, 1 action)
// -> aliasLike (non-rule ConfiguredTargetValue, e.g. AliasConfiguredTarget)
// -> realDep (rule, 2 actions)
// -> aspectOnDep (aspect, 3 actions)
// -> aspectOnlyDep (rule, 4 actions)
// -> realDep (shared with aliasLike, counted once!)
ConfiguredTargetKey realDep = configuredTargetKey("//pkg:real_dep");
ConfiguredTargetKey aspectOnlyDep = configuredTargetKey("//pkg:aspect_only_dep");
ConfiguredTargetKey aliasLike = configuredTargetKey("//pkg:alias_like");
AspectKey aspectOnDep = aspectKey(realDep);
ConfiguredTargetKey root = configuredTargetKey("//pkg:root");
ConfiguredTargetKey nonRuleRoot = configuredTargetKey("//pkg:output_file");
addDoneNode(graph, realDep, ruleValue(/* actionCount= */ 2));
addDoneNode(graph, aspectOnlyDep, ruleValue(/* actionCount= */ 4));
addDoneNode(graph, aliasLike, NON_RULE_VALUE, realDep);
addDoneNode(graph, aspectOnDep, ruleValue(/* actionCount= */ 3), realDep, aspectOnlyDep);
addDoneNode(graph, root, ruleValue(/* actionCount= */ 1), aliasLike, aspectOnDep);
addDoneNode(graph, nonRuleRoot, NON_RULE_VALUE, realDep);
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(graph, ImmutableSet.of(root, nonRuleRoot));
// root reaches: root (1), realDep (2), aspectOnDep (3), aspectOnlyDep (4) -> 4 nodes, 10
// actions (aliasLike is traversed through without incrementing targetCount).
// nonRuleRoot reaches: realDep (2) -> 1 node, 2 actions.
assertThat(counts)
.containsExactly(
root, new Counts(4, 1 + 2 + 3 + 4),
nonRuleRoot, new Counts(1, 2));
}
@Test
public void count_delegatedConfiguredTarget_countedOnce() throws Exception {
// When a rule transition changes a target's configuration from CONFIG_KEY to
// TRANSITIONED_CONFIG_KEY, preTransitionKey delegates to postTransitionKey and holds the exact
// same ConfiguredTargetValue (whose ConfiguredTarget has TRANSITIONED_CONFIG_KEY).
ConfiguredTargetKey leaf =
ConfiguredTargetKey.builder()
.setLabel(Label.parseCanonicalUnchecked("//pkg:leaf"))
.setConfigurationKey(TRANSITIONED_CONFIG_KEY)
.build();
ConfiguredTargetKey preTransitionKey = configuredTargetKey("//pkg:transitioned");
ConfiguredTargetKey postTransitionKey =
ConfiguredTargetKey.builder()
.setLabel(Label.parseCanonicalUnchecked("//pkg:transitioned"))
.setConfigurationKey(TRANSITIONED_CONFIG_KEY)
.build();
ConfiguredTargetKey root = configuredTargetKey("//pkg:root");
StubRuleConfiguredTargetValue transitionedValue =
new StubRuleConfiguredTargetValue(/* actionCount= */ 3, TRANSITIONED_CONFIG_KEY);
addDoneNode(
graph,
leaf,
new StubRuleConfiguredTargetValue(/* actionCount= */ 2, TRANSITIONED_CONFIG_KEY));
addDoneNode(graph, postTransitionKey, transitionedValue, leaf);
addDoneNode(graph, preTransitionKey, transitionedValue, postTransitionKey);
addDoneNode(
graph,
root,
new StubRuleConfiguredTargetValue(/* actionCount= */ 1, CONFIG_KEY),
preTransitionKey);
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(
graph, ImmutableSet.of(root, preTransitionKey, postTransitionKey));
assertThat(counts)
.containsExactly(
root, new Counts(3, 1 + 3 + 2),
preTransitionKey, new Counts(2, 3 + 2),
postTransitionKey, new Counts(2, 3 + 2));
}
@Test
public void count_unconfiguredMissingAndUndoneNodes_handledGracefully() throws Exception {
ConfiguredTargetKey unconfiguredRoot = unconfiguredTargetKey("//pkg:input.txt");
ConfiguredTargetKey missingRoot = configuredTargetKey("//pkg:missing");
ConfiguredTargetKey undoneRoot = configuredTargetKey("//pkg:undone");
// Create `undoneRoot` in the graph without marking it done.
InMemoryNodeEntry undoneEntry =
(InMemoryNodeEntry)
graph
.createIfAbsentBatch(null, Reason.OTHER, ImmutableList.of(undoneRoot))
.get(undoneRoot);
undoneEntry.addReverseDepAndCheckIfDone(null);
assertThat(TransitiveTargetCounter.count(graph, ImmutableSet.of(unconfiguredRoot)))
.containsExactly(unconfiguredRoot, new Counts(0, 0));
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(
graph, ImmutableSet.of(unconfiguredRoot, missingRoot, undoneRoot));
assertThat(counts)
.containsExactly(
unconfiguredRoot, new Counts(0, 0),
missingRoot, new Counts(0, 0),
undoneRoot, new Counts(0, 0));
}
@Test
public void count_doneErrorAndCycleNodes_ignoredWithoutFollowingDeps() throws Exception {
// Under --keep_going, failed and cycle nodes are marked done with ErrorInfoValue
// (entry.isDone() == true, entry.getValue() == null) while retaining their direct dep edges.
ConfiguredTargetKey validDep = configuredTargetKey("//pkg:valid_dep");
ConfiguredTargetKey cycleA = configuredTargetKey("//pkg:cycle_a");
ConfiguredTargetKey cycleB = configuredTargetKey("//pkg:cycle_b");
ConfiguredTargetKey validRoot = configuredTargetKey("//pkg:valid_root");
SkyValue errorValue =
ValueWithMetadata.error(
ErrorInfo.fromCycle(CycleInfo.cycleInfoNoDetails()),
NestedSetBuilder.emptySet(Order.STABLE_ORDER));
addDoneNode(graph, validDep, ruleValue(/* actionCount= */ 4));
addDoneNode(graph, cycleA, errorValue, cycleB, validDep);
addDoneNode(graph, cycleB, errorValue, cycleA);
addDoneNode(graph, validRoot, ruleValue(/* actionCount= */ 2), cycleA, validDep);
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(graph, ImmutableSet.of(validRoot, cycleA));
assertThat(counts)
.containsExactly(
validRoot, new Counts(2, 2 + 4),
cycleA, new Counts(0, 0));
}
@Test
public void count_edgelessGraph_countsRootWithoutFollowingDeps() throws Exception {
InMemoryGraph edgelessGraph = InMemoryGraph.createEdgeless(/* usePooledInterning= */ false);
ConfiguredTargetKey dep = configuredTargetKey("//pkg:dep");
ConfiguredTargetKey root = configuredTargetKey("//pkg:root");
addDoneNode(edgelessGraph, dep, ruleValue(/* actionCount= */ 5));
addDoneNode(edgelessGraph, root, ruleValue(/* actionCount= */ 2), dep);
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(edgelessGraph, ImmutableSet.of(root));
assertThat(counts).containsExactly(root, new Counts(1, 2));
}
@Test
public void count_multiPassPropagation_matchesExpectedCountsAcrossPasses() throws Exception {
// Force 1 word (64 roots) per pass by passing maxMaskBytes = 0, with 150 roots spanning 3
// passes ([0, 64), [64, 128), [128, 150)), including cross-pass root-to-root edges in both
// directions.
ConfiguredTargetKey sharedLeaf = configuredTargetKey("//pkg:shared_leaf");
addDoneNode(graph, sharedLeaf, ruleValue(/* actionCount= */ 3));
int numRoots = 150;
List<ConfiguredTargetKey> roots = new ArrayList<>(numRoots);
for (int i = 0; i < numRoots; i++) {
roots.add(configuredTargetKey("//pkg:root_" + i));
}
// root_0 (pass 0) -> root_140 (pass 2) -> sharedLeaf
// root_145 (pass 2) -> root_10 (pass 0) -> sharedLeaf
addDoneNode(graph, roots.get(140), ruleValue(/* actionCount= */ 5), sharedLeaf);
addDoneNode(graph, roots.get(0), ruleValue(/* actionCount= */ 2), roots.get(140));
addDoneNode(graph, roots.get(10), ruleValue(/* actionCount= */ 7), sharedLeaf);
addDoneNode(graph, roots.get(145), ruleValue(/* actionCount= */ 11), roots.get(10));
for (int i = 0; i < numRoots; i++) {
if (i == 0 || i == 10 || i == 140 || i == 145) {
continue;
}
addDoneNode(graph, roots.get(i), ruleValue(/* actionCount= */ 1), sharedLeaf);
}
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(graph, ImmutableSet.copyOf(roots), /* maxMaskBytes= */ 0);
assertThat(counts).hasSize(numRoots);
assertThat(counts.get(roots.get(0))).isEqualTo(new Counts(3, 2 + 5 + 3));
assertThat(counts.get(roots.get(140))).isEqualTo(new Counts(2, 5 + 3));
assertThat(counts.get(roots.get(145))).isEqualTo(new Counts(3, 11 + 7 + 3));
assertThat(counts.get(roots.get(10))).isEqualTo(new Counts(2, 7 + 3));
for (int i = 0; i < numRoots; i++) {
if (i == 0 || i == 10 || i == 140 || i == 145) {
continue;
}
assertThat(counts.get(roots.get(i))).isEqualTo(new Counts(2, 1 + 3));
}
}
@Test
public void count_executesOnDedicatedPoolAndNotCommonPool() throws Exception {
Set<Thread> observedThreads = ConcurrentHashMap.newKeySet();
// Build > 1024 nodes (> FOR_EACH_PARALLELISM_THRESHOLD) so ConcurrentHashMap.forEachValue
// also forks subtasks, and record every thread that calls getActions().
int chainLength = 1500;
ConfiguredTargetKey prev = null;
for (int i = 0; i < chainLength; i++) {
ConfiguredTargetKey key = configuredTargetKey("//pkg:n_" + i);
RuleConfiguredObjectValue value =
new StubRuleConfiguredObjectValue(/* actionCount= */ 1) {
@Override
public ImmutableList<ActionAnalysisMetadata> getActions() {
observedThreads.add(Thread.currentThread());
return super.getActions();
}
};
if (prev == null) {
addDoneNode(graph, key, value);
} else {
addDoneNode(graph, key, value, prev);
}
prev = key;
}
long commonPoolStealsBefore = ForkJoinPool.commonPool().getStealCount();
ImmutableMap<ConfiguredTargetKey, Counts> counts =
TransitiveTargetCounter.count(graph, ImmutableSet.of(prev));
long commonPoolStealsAfter = ForkJoinPool.commonPool().getStealCount();
assertThat(counts).containsExactly(prev, new Counts(chainLength, chainLength));
assertThat(observedThreads).isNotEmpty();
for (Thread thread : observedThreads) {
assertThat(thread.getName()).startsWith("transitive-target-counter-");
}
assertThat(commonPoolStealsAfter).isEqualTo(commonPoolStealsBefore);
}
private static ConfiguredTargetKey configuredTargetKey(String label) {
return ConfiguredTargetKey.builder()
.setLabel(Label.parseCanonicalUnchecked(label))
.setConfigurationKey(CONFIG_KEY)
.build();
}
private static ConfiguredTargetKey unconfiguredTargetKey(String label) {
return ConfiguredTargetKey.builder().setLabel(Label.parseCanonicalUnchecked(label)).build();
}
private static AspectKey aspectKey(ConfiguredTargetKey baseKey) {
return AspectKeyCreator.createAspectKey(
AspectDescriptor.of(TEST_ASPECT_CLASS, AspectParameters.EMPTY), baseKey);
}
private static RuleConfiguredObjectValue ruleValue(int actionCount) {
return new StubRuleConfiguredObjectValue(actionCount);
}
private static void addDoneNode(
InMemoryGraph targetGraph, SkyKey key, SkyValue value, SkyKey... directDeps)
throws InterruptedException {
addDoneNodeWithDepGroups(
targetGraph, key, value, ImmutableList.of(ImmutableList.copyOf(directDeps)));
}
private static void addDoneNodeWithDepGroups(
InMemoryGraph targetGraph,
SkyKey key,
SkyValue value,
ImmutableList<ImmutableList<SkyKey>> depGroups)
throws InterruptedException {
InMemoryNodeEntry entry =
(InMemoryNodeEntry)
targetGraph.createIfAbsentBatch(null, Reason.OTHER, ImmutableList.of(key)).get(key);
entry.addReverseDepAndCheckIfDone(null);
entry.markRebuilding();
for (ImmutableList<SkyKey> group : depGroups) {
if (group.size() == 1) {
SkyKey dep = group.get(0);
entry.addSingletonTemporaryDirectDep(dep);
entry.signalDep(Version.constant(), dep);
} else if (!group.isEmpty()) {
entry.addTemporaryDirectDepGroup(group);
for (SkyKey dep : group) {
entry.signalDep(Version.constant(), dep);
}
}
}
entry.setValue(value, Version.constant(), /* maxTransitiveSourceVersion= */ null);
}
private static class StubRuleConfiguredObjectValue implements RuleConfiguredObjectValue {
private final ImmutableList<ActionAnalysisMetadata> actions;
StubRuleConfiguredObjectValue(int actionCount) {
ImmutableList.Builder<ActionAnalysisMetadata> builder =
ImmutableList.builderWithExpectedSize(actionCount);
for (int i = 0; i < actionCount; i++) {
builder.add(new ActionsTestUtil.NullAction());
}
this.actions = builder.build();
}
@Override
public ImmutableList<ActionAnalysisMetadata> getActions() {
return actions;
}
@Override
public ProviderCollection getConfiguredObject() {
throw new UnsupportedOperationException();
}
@Override
public NestedSet<Package.Metadata> getTransitivePackages() {
throw new UnsupportedOperationException();
}
}
private static final class StubRuleConfiguredTargetValue extends StubRuleConfiguredObjectValue
implements ConfiguredTargetValue {
private final ConfiguredTarget configuredTarget;
StubRuleConfiguredTargetValue(int actionCount, BuildConfigurationKey configKey) {
super(actionCount);
this.configuredTarget = mock(ConfiguredTarget.class);
when(this.configuredTarget.getConfigurationKey()).thenReturn(configKey);
}
@Override
public ConfiguredTarget getConfiguredTarget() {
return configuredTarget;
}
@Override
public ConfiguredTarget getConfiguredObject() {
return configuredTarget;
}
@Override
public void clear(boolean clearEverything) {
throw new UnsupportedOperationException();
}
}
}