/*
* Copyright 2016-present Facebook, Inc.
*
* 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.facebook.buck.rules;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import com.facebook.buck.event.ActionGraphEvent;
import com.facebook.buck.event.BuckEvent;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.BuckEventBusFactory;
import com.facebook.buck.event.listener.BroadcastEventListener;
import com.facebook.buck.jvm.java.JavaLibraryBuilder;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetFactory;
import com.facebook.buck.rules.keys.ContentAgnosticRuleKeyFactory;
import com.facebook.buck.rules.keys.RuleKeyFieldLoader;
import com.facebook.buck.testutil.TargetGraphFactory;
import com.facebook.buck.testutil.integration.TemporaryPaths;
import com.facebook.buck.timing.IncrementingFakeClock;
import com.facebook.buck.util.WatchmanOverflowEvent;
import com.facebook.buck.util.WatchmanPathEvent;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.Subscribe;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class ActionGraphCacheTest {
private static final boolean CHECK_GRAPHS = true;
private static final boolean NOT_CHECK_GRAPHS = false;
private TargetNode<?, ?> nodeA;
private TargetNode<?, ?> nodeB;
private TargetGraph targetGraph;
private BuckEventBus eventBus;
private BroadcastEventListener broadcastEventListener;
private BlockingQueue<BuckEvent> trackedEvents = new LinkedBlockingQueue<>();
private final int keySeed = 0;
@Rule public ExpectedException expectedException = ExpectedException.none();
@Rule public TemporaryPaths tmpFilePath = new TemporaryPaths();
@Before
public void setUp() {
// Creates the following target graph:
// A
// /
// B
nodeB = createTargetNode("B");
nodeA = createTargetNode("A", nodeB);
targetGraph = TargetGraphFactory.newInstance(nodeA, nodeB);
eventBus =
BuckEventBusFactory.newInstance(new IncrementingFakeClock(TimeUnit.SECONDS.toNanos(1)));
broadcastEventListener = new BroadcastEventListener();
broadcastEventListener.addEventBus(eventBus);
eventBus.register(
new Object() {
@Subscribe
public void actionGraphCacheEvent(ActionGraphEvent.Cache event) {
trackedEvents.add(event);
}
});
}
@Test
public void hitOnCache() throws InterruptedException {
ActionGraphCache cache = new ActionGraphCache(broadcastEventListener);
ActionGraphAndResolver resultRun1 =
cache.getActionGraph(
eventBus, CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
// The 1st time you query the ActionGraph it's a cache miss.
assertEquals(countEventsOf(ActionGraphEvent.Cache.Hit.class), 0);
assertEquals(countEventsOf(ActionGraphEvent.Cache.Miss.class), 1);
ActionGraphAndResolver resultRun2 =
cache.getActionGraph(
eventBus, CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
// The 2nd time it should be a cache hit and the ActionGraphs should be exactly the same.
assertEquals(countEventsOf(ActionGraphEvent.Cache.Hit.class), 1);
assertEquals(countEventsOf(ActionGraphEvent.Cache.Miss.class), 1);
// Check all the RuleKeys are the same between the 2 ActionGraphs.
Map<BuildRule, RuleKey> resultRun1RuleKeys =
getRuleKeysFromBuildRules(resultRun1.getActionGraph().getNodes(), resultRun1.getResolver());
Map<BuildRule, RuleKey> resultRun2RuleKeys =
getRuleKeysFromBuildRules(resultRun2.getActionGraph().getNodes(), resultRun2.getResolver());
assertThat(resultRun1RuleKeys, Matchers.equalTo(resultRun2RuleKeys));
}
@Test
public void missOnCache() {
ActionGraphCache cache = new ActionGraphCache(broadcastEventListener);
ActionGraphAndResolver resultRun1 =
cache.getActionGraph(
eventBus, CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
// Each time you call it for a different TargetGraph so all calls should be misses.
assertEquals(countEventsOf(ActionGraphEvent.Cache.Hit.class), 0);
assertEquals(countEventsOf(ActionGraphEvent.Cache.Miss.class), 1);
ActionGraphAndResolver resultRun2 =
cache.getActionGraph(
eventBus,
CHECK_GRAPHS,
/* skipActionGraphCache */ false,
targetGraph.getSubgraph(ImmutableSet.of(nodeB)),
keySeed);
assertEquals(countEventsOf(ActionGraphEvent.Cache.Hit.class), 0);
assertEquals(countEventsOf(ActionGraphEvent.Cache.Miss.class), 2);
ActionGraphAndResolver resultRun3 =
cache.getActionGraph(
eventBus, CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
assertEquals(countEventsOf(ActionGraphEvent.Cache.Hit.class), 0);
assertEquals(countEventsOf(ActionGraphEvent.Cache.Miss.class), 3);
// Run1 and Run2 should not match, but Run1 and Run3 should
Map<BuildRule, RuleKey> resultRun1RuleKeys =
getRuleKeysFromBuildRules(resultRun1.getActionGraph().getNodes(), resultRun1.getResolver());
Map<BuildRule, RuleKey> resultRun2RuleKeys =
getRuleKeysFromBuildRules(resultRun2.getActionGraph().getNodes(), resultRun2.getResolver());
Map<BuildRule, RuleKey> resultRun3RuleKeys =
getRuleKeysFromBuildRules(resultRun3.getActionGraph().getNodes(), resultRun3.getResolver());
// Run2 is done in a subgraph and it should not have the same ActionGraph.
assertThat(resultRun1RuleKeys, Matchers.not(Matchers.equalTo(resultRun2RuleKeys)));
// Run1 and Run3 should match.
assertThat(resultRun1RuleKeys, Matchers.equalTo(resultRun3RuleKeys));
}
@Test
public void missWithTargetGraphHashMatch() {
ActionGraphCache cache = new ActionGraphCache(broadcastEventListener);
cache.getActionGraph(
eventBus, CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
assertEquals(1, countEventsOf(ActionGraphEvent.Cache.Miss.class));
cache.getActionGraph(
eventBus,
CHECK_GRAPHS,
/* skipActionGraphCache */ false,
TargetGraphFactory.newInstance(nodeA, createTargetNode("B")),
keySeed);
assertEquals(1, countEventsOf(ActionGraphEvent.Cache.MissWithTargetGraphHashMatch.class));
assertEquals(2, countEventsOf(ActionGraphEvent.Cache.Miss.class));
}
// If this breaks it probably means the ActionGraphCache checking also breaks.
@Test
public void compareActionGraphsBasedOnRuleKeys() {
ActionGraphAndResolver resultRun1 =
ActionGraphCache.getFreshActionGraph(
eventBus, new DefaultTargetNodeToBuildRuleTransformer(), targetGraph);
ActionGraphAndResolver resultRun2 =
ActionGraphCache.getFreshActionGraph(
eventBus, new DefaultTargetNodeToBuildRuleTransformer(), targetGraph);
// Check all the RuleKeys are the same between the 2 ActionGraphs.
Map<BuildRule, RuleKey> resultRun1RuleKeys =
getRuleKeysFromBuildRules(resultRun1.getActionGraph().getNodes(), resultRun1.getResolver());
Map<BuildRule, RuleKey> resultRun2RuleKeys =
getRuleKeysFromBuildRules(resultRun2.getActionGraph().getNodes(), resultRun2.getResolver());
assertThat(resultRun1RuleKeys, Matchers.equalTo(resultRun2RuleKeys));
}
@Test
public void cacheInvalidationBasedOnEvents() throws IOException, InterruptedException {
ActionGraphCache cache = new ActionGraphCache(broadcastEventListener);
Path file = tmpFilePath.newFile("foo.txt");
// Fill the cache. An overflow event should invalidate the cache.
cache.getActionGraph(
eventBus, NOT_CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
assertFalse(cache.isCacheEmpty());
cache.invalidateBasedOn(WatchmanOverflowEvent.of(tmpFilePath.getRoot(), "testing"));
assertTrue(cache.isCacheEmpty());
// Fill the cache. Add a file and ActionGraphCache should be invalidated.
cache.getActionGraph(
eventBus, NOT_CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
assertFalse(cache.isCacheEmpty());
cache.invalidateBasedOn(
WatchmanPathEvent.of(tmpFilePath.getRoot(), WatchmanPathEvent.Kind.CREATE, file));
assertTrue(cache.isCacheEmpty());
//Re-fill cache. Remove a file and ActionGraphCache should be invalidated.
cache.getActionGraph(
eventBus, NOT_CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
assertFalse(cache.isCacheEmpty());
cache.invalidateBasedOn(
WatchmanPathEvent.of(tmpFilePath.getRoot(), WatchmanPathEvent.Kind.DELETE, file));
assertTrue(cache.isCacheEmpty());
// Re-fill cache. Modify contents of a file, ActionGraphCache should NOT be invalidated.
cache.getActionGraph(
eventBus, CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
assertFalse(cache.isCacheEmpty());
cache.invalidateBasedOn(
WatchmanPathEvent.of(tmpFilePath.getRoot(), WatchmanPathEvent.Kind.MODIFY, file));
cache.getActionGraph(
eventBus, NOT_CHECK_GRAPHS, /* skipActionGraphCache */ false, targetGraph, keySeed);
assertFalse(cache.isCacheEmpty());
// We should have 4 cache misses and 1 hit from when you request the same graph after a file
// modification.
assertEquals(countEventsOf(ActionGraphEvent.Cache.Hit.class), 1);
assertEquals(countEventsOf(ActionGraphEvent.Cache.Miss.class), 4);
}
private TargetNode<?, ?> createTargetNode(String name, TargetNode<?, ?>... deps) {
BuildTarget buildTarget = BuildTargetFactory.newInstance("//foo:" + name);
JavaLibraryBuilder targetNodeBuilder = JavaLibraryBuilder.createBuilder(buildTarget);
for (TargetNode<?, ?> dep : deps) {
targetNodeBuilder.addDep(dep.getBuildTarget());
}
return targetNodeBuilder.build();
}
private int countEventsOf(Class<? extends ActionGraphEvent> trackedClass) {
int i = 0;
for (BuckEvent event : trackedEvents) {
if (trackedClass.isInstance(event)) {
i++;
}
}
return i;
}
private Map<BuildRule, RuleKey> getRuleKeysFromBuildRules(
Iterable<BuildRule> buildRules, BuildRuleResolver buildRuleResolver) {
RuleKeyFieldLoader ruleKeyFieldLoader = new RuleKeyFieldLoader(0);
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(buildRuleResolver);
SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder);
ContentAgnosticRuleKeyFactory factory =
new ContentAgnosticRuleKeyFactory(ruleKeyFieldLoader, pathResolver, ruleFinder);
HashMap<BuildRule, RuleKey> ruleKeysMap = new HashMap<>();
for (BuildRule rule : buildRules) {
ruleKeysMap.put(rule, factory.build(rule));
}
return ruleKeysMap;
}
}