blob: 3c414d8126e313459ee47c5d7d8c91800a4f1e9b [file] [log] [blame]
// Copyright 2014 Google Inc. 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.lib.skyframe;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.concurrent.ExecutorUtil;
import com.google.devtools.build.lib.concurrent.Sharder;
import com.google.devtools.build.lib.concurrent.ThrowableRecordingRunnableWrapper;
import com.google.devtools.build.lib.skyframe.SkyValueDirtinessChecker.DirtyResult;
import com.google.devtools.build.lib.util.LoggingUtil;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
import com.google.devtools.build.lib.vfs.BatchStat;
import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
import com.google.devtools.build.skyframe.Differencer;
import com.google.devtools.build.skyframe.MemoizingEvaluator;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* A helper class to find dirty values by accessing the filesystem directly (contrast with
* {@link DiffAwareness}).
*/
class FilesystemValueChecker {
private static final int DIRTINESS_CHECK_THREADS = 50;
private static final Logger LOG = Logger.getLogger(FilesystemValueChecker.class.getName());
private static final Predicate<SkyKey> ACTION_FILTER =
SkyFunctionName.functionIs(SkyFunctions.ACTION_EXECUTION);
private final TimestampGranularityMonitor tsgm;
@Nullable
private final Range<Long> lastExecutionTimeRange;
private final Supplier<Map<SkyKey, SkyValue>> valuesSupplier;
private AtomicInteger modifiedOutputFilesCounter = new AtomicInteger(0);
private AtomicInteger modifiedOutputFilesIntraBuildCounter = new AtomicInteger(0);
FilesystemValueChecker(Supplier<Map<SkyKey, SkyValue>> valuesSupplier,
TimestampGranularityMonitor tsgm, @Nullable Range<Long> lastExecutionTimeRange) {
this.valuesSupplier = valuesSupplier;
this.tsgm = tsgm;
this.lastExecutionTimeRange = lastExecutionTimeRange;
}
FilesystemValueChecker(final MemoizingEvaluator evaluator, TimestampGranularityMonitor tsgm,
@Nullable Range<Long> lastExecutionTimeRange) {
this.tsgm = tsgm;
this.lastExecutionTimeRange = lastExecutionTimeRange;
// Construct the full map view of the entire graph at most once ("memoized"), lazily. If
// getDirtyFilesystemValues(Iterable<SkyKey>) is called on an empty Iterable, we avoid having
// to create the Map of value keys to values. This is useful in the case where the graph
// getValues() method could be slow.
this.valuesSupplier = Suppliers.memoize(new Supplier<Map<SkyKey, SkyValue>>() {
@Override
public Map<SkyKey, SkyValue> get() {
return evaluator.getValues();
}
});
}
/**
* Returns a {@link Differencer.DiffWithDelta} containing keys from the backing graph (of the
* {@link MemoizingEvaluator} given at construction time) that are dirty according to the
* passed-in {@code dirtinessChecker}.
*/
Differencer.DiffWithDelta getDirtyKeys(SkyValueDirtinessChecker dirtinessChecker)
throws InterruptedException {
return getDirtyValues(valuesSupplier.get().keySet(), dirtinessChecker,
/*checkMissingValues=*/false);
}
/**
* Returns a {@link Differencer.DiffWithDelta} containing keys that are dirty according to the
* passed-in {@code dirtinessChecker}.
*/
Differencer.DiffWithDelta getNewAndOldValues(Iterable<SkyKey> keys,
SkyValueDirtinessChecker dirtinessChecker) throws InterruptedException {
return getDirtyValues(keys, dirtinessChecker, /*checkMissingValues=*/true);
}
/**
* Return a collection of action values which have output files that are not in-sync with
* the on-disk file value (were modified externally).
*/
Collection<SkyKey> getDirtyActionValues(@Nullable final BatchStat batchStatter)
throws InterruptedException {
// CPU-bound (usually) stat() calls, plus a fudge factor.
LOG.info("Accumulating dirty actions");
final int numOutputJobs = Runtime.getRuntime().availableProcessors() * 4;
final Set<SkyKey> actionSkyKeys =
Sets.filter(valuesSupplier.get().keySet(), ACTION_FILTER);
final Sharder<Pair<SkyKey, ActionExecutionValue>> outputShards =
new Sharder<>(numOutputJobs, actionSkyKeys.size());
for (SkyKey key : actionSkyKeys) {
outputShards.add(Pair.of(key, (ActionExecutionValue) valuesSupplier.get().get(key)));
}
LOG.info("Sharded action values for batching");
ExecutorService executor = Executors.newFixedThreadPool(
numOutputJobs,
new ThreadFactoryBuilder().setNameFormat("FileSystem Output File Invalidator %d").build());
Collection<SkyKey> dirtyKeys = Sets.newConcurrentHashSet();
ThrowableRecordingRunnableWrapper wrapper =
new ThrowableRecordingRunnableWrapper("FileSystemValueChecker#getDirtyActionValues");
modifiedOutputFilesCounter.set(0);
modifiedOutputFilesIntraBuildCounter.set(0);
for (List<Pair<SkyKey, ActionExecutionValue>> shard : outputShards) {
Runnable job = (batchStatter == null)
? outputStatJob(dirtyKeys, shard)
: batchStatJob(dirtyKeys, shard, batchStatter);
executor.submit(wrapper.wrap(job));
}
boolean interrupted = ExecutorUtil.interruptibleShutdown(executor);
Throwables.propagateIfPossible(wrapper.getFirstThrownError());
LOG.info("Completed output file stat checks");
if (interrupted) {
throw new InterruptedException();
}
return dirtyKeys;
}
private Runnable batchStatJob(final Collection<SkyKey> dirtyKeys,
final List<Pair<SkyKey, ActionExecutionValue>> shard,
final BatchStat batchStatter) {
return new Runnable() {
@Override
public void run() {
Map<Artifact, Pair<SkyKey, ActionExecutionValue>> artifactToKeyAndValue = new HashMap<>();
for (Pair<SkyKey, ActionExecutionValue> keyAndValue : shard) {
ActionExecutionValue actionValue = keyAndValue.getSecond();
if (actionValue == null) {
dirtyKeys.add(keyAndValue.getFirst());
} else {
for (Artifact artifact : actionValue.getAllOutputArtifactData().keySet()) {
artifactToKeyAndValue.put(artifact, keyAndValue);
}
}
}
List<Artifact> artifacts = ImmutableList.copyOf(artifactToKeyAndValue.keySet());
List<FileStatusWithDigest> stats;
try {
stats = batchStatter.batchStat(/*includeDigest=*/true, /*includeLinks=*/true,
Artifact.asPathFragments(artifacts));
} catch (IOException e) {
// Batch stat did not work. Log an exception and fall back on system calls.
LoggingUtil.logToRemote(Level.WARNING, "Unable to process batch stat", e);
outputStatJob(dirtyKeys, shard).run();
return;
} catch (InterruptedException e) {
// We handle interrupt in the main thread.
return;
}
Preconditions.checkState(artifacts.size() == stats.size(),
"artifacts.size() == %s stats.size() == %s", artifacts.size(), stats.size());
for (int i = 0; i < artifacts.size(); i++) {
Artifact artifact = artifacts.get(i);
FileStatusWithDigest stat = stats.get(i);
Pair<SkyKey, ActionExecutionValue> keyAndValue = artifactToKeyAndValue.get(artifact);
ActionExecutionValue actionValue = keyAndValue.getSecond();
SkyKey key = keyAndValue.getFirst();
FileValue lastKnownData = actionValue.getAllOutputArtifactData().get(artifact);
try {
FileValue newData = ActionMetadataHandler.fileValueFromArtifact(artifact, stat, tsgm);
if (!newData.equals(lastKnownData)) {
updateIntraBuildModifiedCounter(stat != null ? stat.getLastChangeTime() : -1,
lastKnownData.isSymlink(), newData.isSymlink());
modifiedOutputFilesCounter.getAndIncrement();
dirtyKeys.add(key);
}
} catch (IOException e) {
// This is an unexpected failure getting a digest or symlink target.
modifiedOutputFilesCounter.getAndIncrement();
dirtyKeys.add(key);
}
}
}
};
}
private void updateIntraBuildModifiedCounter(long time, boolean oldWasSymlink,
boolean newIsSymlink) {
if (lastExecutionTimeRange != null
&& lastExecutionTimeRange.contains(time)
&& !(oldWasSymlink && newIsSymlink)) {
modifiedOutputFilesIntraBuildCounter.incrementAndGet();
}
}
private Runnable outputStatJob(final Collection<SkyKey> dirtyKeys,
final List<Pair<SkyKey, ActionExecutionValue>> shard) {
return new Runnable() {
@Override
public void run() {
for (Pair<SkyKey, ActionExecutionValue> keyAndValue : shard) {
ActionExecutionValue value = keyAndValue.getSecond();
if (value == null || actionValueIsDirtyWithDirectSystemCalls(value)) {
dirtyKeys.add(keyAndValue.getFirst());
}
}
}
};
}
/**
* Returns the number of modified output files inside of dirty actions.
*/
int getNumberOfModifiedOutputFiles() {
return modifiedOutputFilesCounter.get();
}
/** Returns the number of modified output files that occur during the previous build. */
int getNumberOfModifiedOutputFilesDuringPreviousBuild() {
return modifiedOutputFilesIntraBuildCounter.get();
}
private boolean actionValueIsDirtyWithDirectSystemCalls(ActionExecutionValue actionValue) {
boolean isDirty = false;
for (Map.Entry<Artifact, FileValue> entry :
actionValue.getAllOutputArtifactData().entrySet()) {
Artifact artifact = entry.getKey();
FileValue lastKnownData = entry.getValue();
try {
FileValue fileValue = ActionMetadataHandler.fileValueFromArtifact(artifact, null, tsgm);
if (!fileValue.equals(lastKnownData)) {
updateIntraBuildModifiedCounter(fileValue.exists()
? fileValue.realRootedPath().asPath().getLastModifiedTime()
: -1, lastKnownData.isSymlink(), fileValue.isSymlink());
modifiedOutputFilesCounter.getAndIncrement();
isDirty = true;
}
} catch (IOException e) {
// This is an unexpected failure getting a digest or symlink target.
modifiedOutputFilesCounter.getAndIncrement();
isDirty = true;
}
}
return isDirty;
}
private BatchDirtyResult getDirtyValues(
Iterable<SkyKey> values, final SkyValueDirtinessChecker checker,
final boolean checkMissingValues) throws InterruptedException {
ExecutorService executor =
Executors.newFixedThreadPool(
DIRTINESS_CHECK_THREADS,
new ThreadFactoryBuilder().setNameFormat("FileSystem Value Invalidator %d").build());
final BatchDirtyResult batchResult = new BatchDirtyResult();
ThrowableRecordingRunnableWrapper wrapper =
new ThrowableRecordingRunnableWrapper("FilesystemValueChecker#getDirtyValues");
for (final SkyKey key : values) {
final SkyValue value = valuesSupplier.get().get(key);
executor.execute(
wrapper.wrap(
new Runnable() {
@Override
public void run() {
if (value != null || checkMissingValues) {
DirtyResult result = checker.maybeCheck(key, value, tsgm);
if (result != null && result.isDirty()) {
batchResult.add(key, value, result.getNewValue());
}
}
}
}));
}
boolean interrupted = ExecutorUtil.interruptibleShutdown(executor);
Throwables.propagateIfPossible(wrapper.getFirstThrownError());
if (interrupted) {
throw new InterruptedException();
}
return batchResult;
}
/**
* Result of a batch call to {@link SkyValueDirtinessChecker#maybeCheck}. Partitions the dirty
* values based on whether we have a new value available for them or not.
*/
private static class BatchDirtyResult implements Differencer.DiffWithDelta {
private final Set<SkyKey> concurrentDirtyKeysWithoutNewValues =
Collections.newSetFromMap(new ConcurrentHashMap<SkyKey, Boolean>());
private final ConcurrentHashMap<SkyKey, Delta> concurrentDirtyKeysWithNewAndOldValues =
new ConcurrentHashMap<>();
private void add(SkyKey key, @Nullable SkyValue oldValue, @Nullable SkyValue newValue) {
if (newValue == null) {
concurrentDirtyKeysWithoutNewValues.add(key);
} else {
if (oldValue == null) {
concurrentDirtyKeysWithNewAndOldValues.put(key, new Delta(newValue));
} else {
concurrentDirtyKeysWithNewAndOldValues.put(key, new Delta(oldValue, newValue));
}
}
}
@Override
public Collection<SkyKey> changedKeysWithoutNewValues() {
return concurrentDirtyKeysWithoutNewValues;
}
@Override
public Map<SkyKey, Delta> changedKeysWithNewAndOldValues() {
return concurrentDirtyKeysWithNewAndOldValues;
}
@Override
public Map<SkyKey, SkyValue> changedKeysWithNewValues() {
return Delta.newValues(concurrentDirtyKeysWithNewAndOldValues);
}
}
}