blob: 2eb4ff9b4731779ab9e7fc63f6c368dbc09b0713 [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.skyframe;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.concurrent.ForkJoinQuiescingExecutor;
import com.google.devtools.build.lib.concurrent.NamedForkJoinPool;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Finds the dirty nodes that change pruning would verify clean if it ran, which it doesn't purely
* because these nodes weren't in scope for the current evaluation.
*
* <p>Operates in two phases: a parallel scan over all candidates that builds a DAG of dirty but
* unchanged deletion candidates and their dirty rdeps, followed by a traversal of the DAG that
* keeps all nodes whose dirty deps are also kept. The scan performs all graph accesses and its cost
* scales with the total number of candidates, so it runs on multiple threads; the traversal only
* visits the nodes that end up being kept.
*/
final class ChangePrunableNodesFinder {
private static final int SCAN_THREAD_COUNT = Runtime.getRuntime().availableProcessors();
private final InMemoryGraph graph;
private final ImmutableSet<SkyKey> candidates;
// The DAG built by the scan phase: for each candidate with at least one dirty dep, the number
// of its dirty deps not yet known to be kept, together with the reverse edges. Candidates
// without dirty deps start out ready. Each entry in remainingDirtyDeps is written by a single
// scan thread, whereas dirtyRdeps values may receive edges from multiple threads.
private final ConcurrentHashMap<SkyKey, AtomicInteger> remainingDirtyDeps =
new ConcurrentHashMap<>();
private final ConcurrentHashMap<SkyKey, ArrayList<SkyKey>> dirtyRdeps = new ConcurrentHashMap<>();
private final ConcurrentLinkedQueue<SkyKey> ready = new ConcurrentLinkedQueue<>();
ChangePrunableNodesFinder(InMemoryGraph graph, ImmutableSet<SkyKey> candidates) {
this.graph = graph;
this.candidates = candidates;
}
ImmutableSet<SkyKey> find() {
ImmutableList<SkyKey> candidateList = candidates.asList();
if (candidateList.isEmpty()) {
return ImmutableSet.of();
}
var executor =
ForkJoinQuiescingExecutor.newBuilder()
.withOwnershipOf(
NamedForkJoinPool.newNamedPool("find-change-prunable-nodes", SCAN_THREAD_COUNT))
.build();
long listSize = candidateList.size();
long numJobs = Math.min(SCAN_THREAD_COUNT, listSize);
for (long i = 0; i < numJobs; i++) {
int startIndex = (int) ((i * listSize) / numJobs);
int endIndex = (int) (((i + 1) * listSize) / numJobs);
ImmutableList<SkyKey> chunk = candidateList.subList(startIndex, endIndex);
executor.execute(() -> scanCandidates(chunk));
}
try {
executor.awaitQuiescence(/* interruptWorkers= */ true);
} catch (InterruptedException e) {
// Keeping no nodes is always safe, it merely GCs more aggressively.
Thread.currentThread().interrupt();
return ImmutableSet.of();
}
// Now visit the DAG, keeping all nodes whose dirty deps are also kept.
var toKeep = ImmutableSet.<SkyKey>builder();
SkyKey readyKey;
while ((readyKey = ready.poll()) != null) {
toKeep.add(readyKey);
var rdeps = dirtyRdeps.get(readyKey);
if (rdeps == null) {
continue;
}
for (var rdep : rdeps) {
if (remainingDirtyDeps.get(rdep).decrementAndGet() == 0) {
// The last dirty dep of rdep is now known to be kept.
ready.add(rdep);
}
}
}
return toKeep.build();
}
private void scanCandidates(List<SkyKey> chunk) {
skipCandidate:
for (var skyKey : chunk) {
if (Thread.currentThread().isInterrupted()) {
// A partial scan only ever results in fewer nodes being kept, which is always safe.
return;
}
if (!(graph.getIfPresent(skyKey) instanceof IncrementalInMemoryNodeEntry entry)) {
continue;
}
Iterable<SkyKey> lastBuildDeps = entry.lastBuildDepsIfChangePrunable();
if (lastBuildDeps == null) {
continue;
}
Version lastEvaluated = entry.lastEvaluatedVersion();
var dirtyDeps = new ArrayList<SkyKey>();
for (var dep : lastBuildDeps) {
NodeEntry depEntry = graph.getIfPresent(dep);
// A dep must be present and unchanged since this node was last evaluated to be
// potentially change-prunable. Undone deps that aren't unenqueued dirty deps will never
// be considered below and thus make an entry not change-prunable.
if (depEntry == null || !depEntry.getVersion().atMost(lastEvaluated)) {
continue skipCandidate;
}
if (depEntry.isDone()) {
continue;
}
if (!candidates.contains(dep)) {
continue skipCandidate;
}
dirtyDeps.add(dep);
}
if (dirtyDeps.isEmpty()) {
ready.add(skyKey);
} else {
remainingDirtyDeps.put(skyKey, new AtomicInteger(dirtyDeps.size()));
for (var dirtyDep : dirtyDeps) {
dirtyRdeps.compute(
dirtyDep,
(unusedKey, rdeps) -> {
if (rdeps == null) {
rdeps = new ArrayList<>();
}
rdeps.add(skyKey);
return rdeps;
});
}
}
}
}
}