| // 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; | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } |