/*
* Copyright 2017-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.keys;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.SimplePerfEvent;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.log.Logger;
import com.facebook.buck.rules.ActionGraph;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.util.MoreCollectors;
import com.facebook.buck.util.WatchmanOverflowEvent;
import com.facebook.buck.util.WatchmanPathEvent;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.IntStream;
import javax.annotation.Nullable;
/** Class which encapsulates all effort to cache and reuse a {@link RuleKeyCache} between builds. */
public class RuleKeyCacheRecycler<V> {
private static final Logger LOG = Logger.get(RuleKeyCacheRecycler.class);
private final RuleKeyCache<V> cache;
private final ImmutableSet<ProjectFilesystem> watchedFilesystems;
@Nullable private SettingsAffectingCache previousSettings = null;
private RuleKeyCacheRecycler(
RuleKeyCache<V> cache, ImmutableSet<ProjectFilesystem> watchedFilesystems) {
this.cache = cache;
this.watchedFilesystems = watchedFilesystems;
}
/**
* @param eventBus {@link EventBus} which delivers watchman events.
* @param watchedFilesystems all {@link ProjectFilesystem}s which use watchman to receive events
* when files are changed.
* @return a new {@link RuleKeyCacheRecycler}.
*/
public static <V> RuleKeyCacheRecycler<V> createAndRegister(
EventBus eventBus,
RuleKeyCache<V> ruleKeyCache,
ImmutableSet<ProjectFilesystem> watchedFilesystems) {
RuleKeyCacheRecycler<V> recycler = new RuleKeyCacheRecycler<>(ruleKeyCache, watchedFilesystems);
// Subscribe the recycler to receive filesystem watch events.
eventBus.register(recycler);
return recycler;
}
public static <V> RuleKeyCacheRecycler<V> create(RuleKeyCache<V> ruleKeyCache) {
return new RuleKeyCacheRecycler<>(ruleKeyCache, ImmutableSet.of());
}
@Subscribe
public void onFilesystemChange(WatchmanPathEvent event) {
// Currently, `WatchEvent`s only contain cell-relative paths, so we have no way of associating
// them with a specific filesystem. So, we assume the event can refer to any of the watched
// filesystems and forward invalidations to all of them.
for (ProjectFilesystem filesystem : watchedFilesystems) {
Path path = event.getPath().normalize();
LOG.verbose(
"invalidating path \"%s\" from filesystem at \"%s\" due to event (%s)",
path, filesystem.getRootPath(), event);
cache.invalidateInputs(
// As inputs to rule keys can be directories, make sure we also invalidate any
// directories containing this path.
IntStream.range(1, path.getNameCount() + 1)
.mapToObj(end -> RuleKeyInput.of(filesystem, path.subpath(0, end)))
.collect(MoreCollectors.toImmutableList()));
}
}
@Subscribe
public void onFilesystemChange(WatchmanOverflowEvent event) {
for (ProjectFilesystem filesystem : watchedFilesystems) {
LOG.verbose(
"invalidating filesystem at \"%s\" due to event (%s)", filesystem.getRootPath(), event);
cache.invalidateFilesystem(filesystem);
}
}
/**
* Provides access to a {@link RuleKeyCache} via a {@link RuleKeyCacheScope}. The {@link
* RuleKeyCacheScope} must be used with a try-resource block and does logging and cache
* invalidation both before and after being used.
*
* @return a {@link RuleKeyCacheScope} managing access to enclosed {@link RuleKeyCache}.
*/
public RuleKeyCacheScope<V> withRecycledCache(
BuckEventBus buckEventBus, SettingsAffectingCache currentSettings) {
return new EventPostingRuleKeyCacheScope<V>(buckEventBus, cache) {
// Cache setup which is run before the caller gets access to the cache, at the time the scope
// is allocated.
@Override
protected void setup(SimplePerfEvent.Scope scope) {
super.setup(scope);
// We invalidate everything if any of the settings we care about change.
if (!SettingsAffectingCache.areIdentical(previousSettings, currentSettings)) {
LOG.debug("invalidating entire cache due to settings change");
cache.invalidateAll();
scope.update("settings_change", true);
} else {
scope.update("settings_change", false);
}
// Record the current settings for next time.
previousSettings = currentSettings;
}
// Cache cleanup which is run after the caller is finished using the cache, at the conclusion
// of the try-resource block that wraps the scope object.
@Override
protected void cleanup(SimplePerfEvent.Scope scope) {
super.cleanup(scope);
// Invalidate all rule keys transitively built from non-watched filesystems, as we have no
// way of knowing which, if any, of its files have been modified/removed.
LOG.verbose(
"invalidating unwatched filesystems (everything except %s)", watchedFilesystems);
cache.invalidateAllExceptFilesystems(watchedFilesystems);
}
};
}
/**
* Run the given {@link Function} with access to the {@link RuleKeyCache}. This is a convenience
* method used to abstract away handling of the {@link RuleKeyCacheScope} inside a try-resource
* block.
*/
<T> T withRecycledCache(
BuckEventBus buckEventBus,
SettingsAffectingCache currentSettings,
Function<RuleKeyCache<V>, T> func) {
try (RuleKeyCacheScope<V> scope = withRecycledCache(buckEventBus, currentSettings)) {
return func.apply(scope.getCache());
}
}
/**
* Run the given {@link Consumer} with access to the {@link RuleKeyCache}. This is a convenience
* method used to abstract away handling of the {@link RuleKeyCacheScope} inside a try-resource
* block.
*/
void withRecycledCache(
BuckEventBus buckEventBus,
SettingsAffectingCache currentSettings,
Consumer<RuleKeyCache<V>> func) {
try (RuleKeyCacheScope<V> scope = withRecycledCache(buckEventBus, currentSettings)) {
func.accept(scope.getCache());
}
}
public ImmutableList<Map.Entry<BuildRule, V>> getCachedBuildRules() {
return cache.getCachedBuildRules();
}
/** Any external settings which, if changed, will cause the entire cache to be invalidated. */
public static class SettingsAffectingCache {
private final int ruleKeySeed;
private final ActionGraph actionGraph;
public SettingsAffectingCache(int ruleKeySeed, ActionGraph actionGraph) {
this.ruleKeySeed = ruleKeySeed;
this.actionGraph = actionGraph;
}
private static boolean areIdentical(
@Nullable SettingsAffectingCache previous, SettingsAffectingCache current) {
// If previous settings are null, then require an invalidation.
if (previous == null) {
return false;
}
if (previous.ruleKeySeed != current.ruleKeySeed) {
return false;
}
// NOTE: Since the cache indexes using instance equality, it's only ever useful if we get a
// hit in the action graph cache and re-use the same action graph in the next build. So, if
// we detect that a fresh action graph is being used, we eagerly dump the cache to free up
// memory.
if (previous.actionGraph != current.actionGraph) {
return false;
}
return true;
}
}
}