| // 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 static com.google.common.truth.Truth.assertThat; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.devtools.build.lib.events.StoredEventHandler; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Random; |
| import java.util.Set; |
| import javax.annotation.Nullable; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * The place where eventual tests for Skyframe eviction will live. |
| * |
| * <p>For now, since eviction is not implemented yet, it's just a fuzz test. Eventually, once |
| * eviction is there, we'll add that to the mix of events that can happen. |
| * |
| * <p>We construct a Skyframe graph of depth {@code LAYERS} with {@code NODES_PER_LAYER} nodes per |
| * layer. Each node has a randomly chosen dependency structure (see {@link #getBatches(LayerKey)}) |
| * with each edge pointing to a random node in one of the layers below. |
| * |
| * <p>The value of each leaf node is {@code <node id>_<generation>} where "generation" is |
| * incremented on each invalidation. The value of each node is a set that contains the union of the |
| * sets in its direct deps. |
| */ |
| @RunWith(JUnit4.class) |
| public final class SkyframeEvictionTest { |
| |
| // Number of layers in the test Skyframe graph |
| private static final int LAYERS = 50; |
| |
| // Number of nodes in each layer |
| private static final int NODES_PER_LAYER = 100; |
| |
| // Random seed (for reproducibility) |
| private static final long BASE_SEED = 12345L; |
| |
| private static final SkyFunctionName LAYER_NODE_TYPE = |
| SkyFunctionName.createNonHermetic("LAYER_NODE"); |
| |
| private record LayerKey(int layer, int nodeNumber) implements SkyKey { |
| @Override |
| public SkyFunctionName functionName() { |
| return LAYER_NODE_TYPE; |
| } |
| } |
| |
| private record LayerValue(ImmutableSortedSet<String> values) implements SkyValue {} |
| |
| // Returns how many SkyKeys the given SkyValue should depend on. Each item in the returned list |
| // represents a dependency group and the integer is the number of random dependencies in that |
| // group. |
| private static ImmutableList<ImmutableList<LayerKey>> getBatches(LayerKey key) { |
| Random random = new Random(BASE_SEED ^ ((long) key.layer() << 32) ^ key.nodeNumber()); |
| int roll = random.nextInt(100); |
| ImmutableList<Integer> batchSizes; |
| if (roll < 5) { |
| batchSizes = ImmutableList.of(); // 1. No dependencies (5%) |
| } else if (roll < 8) { |
| batchSizes = ImmutableList.of(10); // 2. 10 dependencies in a single restart (3%) |
| } else if (roll < 10) { |
| batchSizes = ImmutableList.of(1, 1, 1, 1, 1, 1, 1, 1, 1, 1); // 3. 10 deps, 1 per restart (2%) |
| } else if (roll < 40) { |
| batchSizes = ImmutableList.of(1); // 4. One dependency (30%) |
| } else if (roll < 65) { |
| batchSizes = ImmutableList.of(2); // 5. Two dependencies in a single restart (25%) |
| } else if (roll < 90) { |
| batchSizes = ImmutableList.of(1, 1); // 6. Two dependencies, each in its own restart (25%) |
| } else if (roll < 95) { |
| batchSizes = ImmutableList.of(1, 1, 1); // 7. Three dependencies, 1 in each of 3 restarts (5%) |
| } else { |
| batchSizes = ImmutableList.of(2, 2); // 8. Four dependencies, 2 in each of 2 restarts (5%) |
| } |
| |
| ImmutableList.Builder<ImmutableList<LayerKey>> batches = ImmutableList.builder(); |
| for (int size : batchSizes) { |
| ImmutableList.Builder<LayerKey> batch = ImmutableList.builder(); |
| for (int i = 0; i < size; i++) { |
| int targetLayer = key.layer() + 1 + random.nextInt(LAYERS - key.layer()); |
| batch.add(new LayerKey(targetLayer, random.nextInt(NODES_PER_LAYER))); |
| } |
| batches.add(batch.build()); |
| } |
| return batches.build(); |
| } |
| |
| private final class LayerFunction implements SkyFunction { |
| @Override |
| @Nullable |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| LayerKey key = (LayerKey) skyKey; |
| if (key.layer() == LAYERS) { |
| // Leaf node. The value is <node id>_<generation>. |
| return new LayerValue( |
| ImmutableSortedSet.of(key.nodeNumber() + "_" + leafVersions[key.nodeNumber()])); |
| } |
| |
| // Non-leaf node. The value is the union of the strings in the direct deps. |
| ImmutableSortedSet.Builder<String> union = ImmutableSortedSet.naturalOrder(); |
| for (ImmutableList<LayerKey> batch : getBatches(key)) { |
| SkyframeLookupResult values = env.getValuesAndExceptions(batch); |
| if (env.valuesMissing()) { |
| return null; |
| } |
| for (LayerKey dep : batch) { |
| LayerValue depValue = (LayerValue) values.get(dep); |
| union.addAll(depValue.values()); |
| } |
| } |
| return new LayerValue(union.build()); |
| } |
| } |
| |
| private static List<LayerKey> getTopLevelKeys() { |
| List<LayerKey> keys = new ArrayList<>(); |
| for (int node = 0; node < NODES_PER_LAYER; node++) { |
| keys.add(new LayerKey(1, node)); |
| } |
| return keys; |
| } |
| |
| private Map<LayerKey, ImmutableSortedSet<String>> computeExpectedValues() { |
| // This is basically a non-incremental algorithm to do the same thing Skyframe should be doing: |
| // Walk up the graph from leaf nodes and compute the unions all the way to the top level. |
| Map<LayerKey, ImmutableSortedSet<String>> expectedValues = new HashMap<>(); |
| for (int layer = LAYERS; layer >= 1; layer--) { |
| for (int node = 0; node < NODES_PER_LAYER; node++) { |
| LayerKey key = new LayerKey(layer, node); |
| if (layer == LAYERS) { |
| expectedValues.put(key, ImmutableSortedSet.of(node + "_" + leafVersions[node])); |
| } else { |
| ImmutableSortedSet.Builder<String> union = ImmutableSortedSet.naturalOrder(); |
| for (ImmutableList<LayerKey> batch : getBatches(key)) { |
| for (LayerKey dep : batch) { |
| union.addAll(expectedValues.get(dep)); |
| } |
| } |
| expectedValues.put(key, union.build()); |
| } |
| } |
| } |
| return expectedValues; |
| } |
| |
| private final SequencedRecordingDifferencer differencer = new SequencedRecordingDifferencer(); |
| private InMemoryMemoizingEvaluator evaluator; |
| private EvaluationContext evaluationContext; |
| |
| // The generation for each leaf node. Leaf node X in generation Y will yield the value "X_Y". |
| private final int[] leafVersions = new int[NODES_PER_LAYER]; |
| |
| @Before |
| public void setUp() { |
| ImmutableMap<SkyFunctionName, SkyFunction> functions = |
| ImmutableMap.of(LAYER_NODE_TYPE, new LayerFunction()); |
| evaluator = |
| new InMemoryMemoizingEvaluator( |
| functions, |
| differencer, |
| new TrackingProgressReceiver(/* checkEvaluationResults= */ true), |
| GraphInconsistencyReceiver.THROWING, |
| EventFilter.FULL_STORAGE, |
| new EmittedEventState(), |
| /* keepEdges= */ true, |
| /* usePooledInterning= */ true); |
| evaluationContext = |
| EvaluationContext.newBuilder() |
| .setKeepGoing(false) |
| .setParallelism(100) |
| .setEventHandler(new StoredEventHandler()) |
| .build(); |
| } |
| |
| private void evaluateAndAssert() throws Exception { |
| List<LayerKey> topLevelKeys = getTopLevelKeys(); |
| Map<LayerKey, ImmutableSortedSet<String>> expectedValues = computeExpectedValues(); |
| EvaluationResult<LayerValue> result = evaluator.evaluate(topLevelKeys, evaluationContext); |
| assertThat(result.hasError()).isFalse(); |
| for (LayerKey key : topLevelKeys) { |
| LayerValue value = result.get(key); |
| assertThat(value).isNotNull(); |
| assertThat(value.values()).isEqualTo(expectedValues.get(key)); |
| } |
| } |
| |
| @Test |
| public void randomGraphEvaluationWithInvalidation() throws Exception { |
| evaluateAndAssert(); |
| |
| // Make the seed different from the seed of the per-SkyFunction random generator in |
| // getBatches(). |
| Random invalidationRandom = new Random(BASE_SEED - 1); |
| for (int iteration = 0; iteration < 100; iteration++) { |
| int numToInvalidate = invalidationRandom.nextInt(21); |
| Set<SkyKey> keysToInvalidate = new HashSet<>(); |
| while (keysToInvalidate.size() < numToInvalidate) { |
| int leafNode = invalidationRandom.nextInt(NODES_PER_LAYER); |
| if (keysToInvalidate.add(new LayerKey(LAYERS, leafNode))) { |
| leafVersions[leafNode]++; |
| } |
| } |
| differencer.invalidate(keysToInvalidate); |
| evaluateAndAssert(); |
| } |
| } |
| } |