blob: a4524d1c256d29e0ac7c6241bc7bad7cbdd121cb [file] [log] [blame]
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01001// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package com.google.devtools.build.lib.skyframe;
15
16import com.google.common.base.Preconditions;
17import com.google.common.base.Predicate;
18import com.google.common.base.Supplier;
19import com.google.common.base.Suppliers;
20import com.google.common.base.Throwables;
21import com.google.common.collect.ImmutableList;
22import com.google.common.collect.ImmutableSet;
23import com.google.common.collect.Iterables;
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +000024import com.google.common.collect.Range;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010025import com.google.common.collect.Sets;
26import com.google.common.util.concurrent.ThreadFactoryBuilder;
27import com.google.devtools.build.lib.actions.Artifact;
28import com.google.devtools.build.lib.concurrent.ExecutorShutdownUtil;
29import com.google.devtools.build.lib.concurrent.Sharder;
30import com.google.devtools.build.lib.concurrent.ThrowableRecordingRunnableWrapper;
31import com.google.devtools.build.lib.util.LoggingUtil;
32import com.google.devtools.build.lib.util.Pair;
33import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
34import com.google.devtools.build.lib.vfs.BatchStat;
35import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
36import com.google.devtools.build.lib.vfs.RootedPath;
37import com.google.devtools.build.skyframe.Differencer;
38import com.google.devtools.build.skyframe.MemoizingEvaluator;
39import com.google.devtools.build.skyframe.SkyFunctionName;
40import com.google.devtools.build.skyframe.SkyKey;
41import com.google.devtools.build.skyframe.SkyValue;
42
43import java.io.IOException;
44import java.util.Collection;
45import java.util.Collections;
46import java.util.HashMap;
47import java.util.List;
48import java.util.Map;
49import java.util.Set;
50import java.util.concurrent.ConcurrentHashMap;
51import java.util.concurrent.ExecutorService;
52import java.util.concurrent.Executors;
53import java.util.concurrent.atomic.AtomicInteger;
54import java.util.logging.Level;
55import java.util.logging.Logger;
56
57import javax.annotation.Nullable;
58
59/**
60 * A helper class to find dirty values by accessing the filesystem directly (contrast with
61 * {@link DiffAwareness}).
62 */
63class FilesystemValueChecker {
64
65 private static final int DIRTINESS_CHECK_THREADS = 50;
66 private static final Logger LOG = Logger.getLogger(FilesystemValueChecker.class.getName());
67
68 private static final Predicate<SkyKey> FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER =
69 SkyFunctionName.functionIsIn(ImmutableSet.of(SkyFunctions.FILE_STATE,
70 SkyFunctions.DIRECTORY_LISTING_STATE));
71 private static final Predicate<SkyKey> ACTION_FILTER =
72 SkyFunctionName.functionIs(SkyFunctions.ACTION_EXECUTION);
73
74 private final TimestampGranularityMonitor tsgm;
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +000075 private final Range<Long> lastExecutionTimeRange;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010076 private final Supplier<Map<SkyKey, SkyValue>> valuesSupplier;
77 private AtomicInteger modifiedOutputFilesCounter = new AtomicInteger(0);
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +000078 private AtomicInteger modifiedOutputFilesIntraBuildCounter = new AtomicInteger(0);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010079
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +000080 FilesystemValueChecker(final MemoizingEvaluator evaluator, TimestampGranularityMonitor tsgm,
81 Range<Long> lastExecutionTimeRange) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010082 this.tsgm = tsgm;
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +000083 this.lastExecutionTimeRange = lastExecutionTimeRange;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010084
85 // Construct the full map view of the entire graph at most once ("memoized"), lazily. If
86 // getDirtyFilesystemValues(Iterable<SkyKey>) is called on an empty Iterable, we avoid having
87 // to create the Map of value keys to values. This is useful in the case where the graph
88 // getValues() method could be slow.
89 this.valuesSupplier = Suppliers.memoize(new Supplier<Map<SkyKey, SkyValue>>() {
90 @Override
91 public Map<SkyKey, SkyValue> get() {
92 return evaluator.getValues();
93 }
94 });
95 }
96
97 Iterable<SkyKey> getFilesystemSkyKeys() {
98 return Iterables.filter(valuesSupplier.get().keySet(),
99 FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER);
100 }
101
102 Differencer.Diff getDirtyFilesystemSkyKeys() throws InterruptedException {
103 return getDirtyFilesystemValues(getFilesystemSkyKeys());
104 }
105
106 /**
107 * Check the given file and directory values for modifications. {@code values} is assumed to only
108 * have {@link FileValue}s and {@link DirectoryListingStateValue}s.
109 */
110 Differencer.Diff getDirtyFilesystemValues(Iterable<SkyKey> values)
111 throws InterruptedException {
112 return getDirtyValues(values, FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER,
113 new DirtyChecker() {
114 @Override
115 public DirtyResult check(SkyKey key, SkyValue oldValue, TimestampGranularityMonitor tsgm) {
116 if (key.functionName() == SkyFunctions.FILE_STATE) {
117 return checkFileStateValue((RootedPath) key.argument(), (FileStateValue) oldValue,
118 tsgm);
119 } else if (key.functionName() == SkyFunctions.DIRECTORY_LISTING_STATE) {
120 return checkDirectoryListingStateValue((RootedPath) key.argument(),
121 (DirectoryListingStateValue) oldValue);
122 } else {
123 throw new IllegalStateException("Unexpected key type " + key);
124 }
125 }
126 });
127 }
128
129 /**
130 * Return a collection of action values which have output files that are not in-sync with
131 * the on-disk file value (were modified externally).
132 */
133 public Collection<SkyKey> getDirtyActionValues(@Nullable final BatchStat batchStatter)
134 throws InterruptedException {
135 // CPU-bound (usually) stat() calls, plus a fudge factor.
136 LOG.info("Accumulating dirty actions");
137 final int numOutputJobs = Runtime.getRuntime().availableProcessors() * 4;
138 final Set<SkyKey> actionSkyKeys =
139 Sets.filter(valuesSupplier.get().keySet(), ACTION_FILTER);
140 final Sharder<Pair<SkyKey, ActionExecutionValue>> outputShards =
141 new Sharder<>(numOutputJobs, actionSkyKeys.size());
142
143 for (SkyKey key : actionSkyKeys) {
144 outputShards.add(Pair.of(key, (ActionExecutionValue) valuesSupplier.get().get(key)));
145 }
146 LOG.info("Sharded action values for batching");
147
148 ExecutorService executor = Executors.newFixedThreadPool(
149 numOutputJobs,
150 new ThreadFactoryBuilder().setNameFormat("FileSystem Output File Invalidator %d").build());
151
152 Collection<SkyKey> dirtyKeys = Sets.newConcurrentHashSet();
153 ThrowableRecordingRunnableWrapper wrapper =
154 new ThrowableRecordingRunnableWrapper("FileSystemValueChecker#getDirtyActionValues");
155
156 modifiedOutputFilesCounter.set(0);
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +0000157 modifiedOutputFilesIntraBuildCounter.set(0);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100158 for (List<Pair<SkyKey, ActionExecutionValue>> shard : outputShards) {
159 Runnable job = (batchStatter == null)
160 ? outputStatJob(dirtyKeys, shard)
161 : batchStatJob(dirtyKeys, shard, batchStatter);
162 executor.submit(wrapper.wrap(job));
163 }
164
165 boolean interrupted = ExecutorShutdownUtil.interruptibleShutdown(executor);
166 Throwables.propagateIfPossible(wrapper.getFirstThrownError());
167 LOG.info("Completed output file stat checks");
168 if (interrupted) {
169 throw new InterruptedException();
170 }
171 return dirtyKeys;
172 }
173
174 private Runnable batchStatJob(final Collection<SkyKey> dirtyKeys,
175 final List<Pair<SkyKey, ActionExecutionValue>> shard,
176 final BatchStat batchStatter) {
177 return new Runnable() {
178 @Override
179 public void run() {
180 Map<Artifact, Pair<SkyKey, ActionExecutionValue>> artifactToKeyAndValue = new HashMap<>();
181 for (Pair<SkyKey, ActionExecutionValue> keyAndValue : shard) {
182 ActionExecutionValue actionValue = keyAndValue.getSecond();
183 if (actionValue == null) {
184 dirtyKeys.add(keyAndValue.getFirst());
185 } else {
186 for (Artifact artifact : actionValue.getAllOutputArtifactData().keySet()) {
187 artifactToKeyAndValue.put(artifact, keyAndValue);
188 }
189 }
190 }
191
192 List<Artifact> artifacts = ImmutableList.copyOf(artifactToKeyAndValue.keySet());
193 List<FileStatusWithDigest> stats;
194 try {
195 stats = batchStatter.batchStat(/*includeDigest=*/true, /*includeLinks=*/true,
196 Artifact.asPathFragments(artifacts));
197 } catch (IOException e) {
198 // Batch stat did not work. Log an exception and fall back on system calls.
199 LoggingUtil.logToRemote(Level.WARNING, "Unable to process batch stat", e);
200 outputStatJob(dirtyKeys, shard).run();
201 return;
202 } catch (InterruptedException e) {
203 // We handle interrupt in the main thread.
204 return;
205 }
206
207 Preconditions.checkState(artifacts.size() == stats.size(),
208 "artifacts.size() == %s stats.size() == %s", artifacts.size(), stats.size());
209 for (int i = 0; i < artifacts.size(); i++) {
210 Artifact artifact = artifacts.get(i);
211 FileStatusWithDigest stat = stats.get(i);
212 Pair<SkyKey, ActionExecutionValue> keyAndValue = artifactToKeyAndValue.get(artifact);
213 ActionExecutionValue actionValue = keyAndValue.getSecond();
214 SkyKey key = keyAndValue.getFirst();
215 FileValue lastKnownData = actionValue.getAllOutputArtifactData().get(artifact);
216 try {
Janak Ramakrishnana5c1f962015-04-03 23:06:31 +0000217 FileValue newData = ActionMetadataHandler.fileValueFromArtifact(artifact, stat, tsgm);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100218 if (!newData.equals(lastKnownData)) {
Miguel Alcon Pintobdfdd092015-04-10 19:04:49 +0000219 updateIntraBuildModifiedCounter(stat != null ? stat.getLastChangeTime() : -1,
220 lastKnownData.isSymlink(), newData.isSymlink());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100221 modifiedOutputFilesCounter.getAndIncrement();
222 dirtyKeys.add(key);
223 }
224 } catch (IOException e) {
225 // This is an unexpected failure getting a digest or symlink target.
226 modifiedOutputFilesCounter.getAndIncrement();
227 dirtyKeys.add(key);
228 }
229 }
230 }
231 };
232 }
233
Miguel Alcon Pintobdfdd092015-04-10 19:04:49 +0000234 private void updateIntraBuildModifiedCounter(long time, boolean oldWasSymlink,
235 boolean newIsSymlink) throws IOException {
236 if (lastExecutionTimeRange != null
237 && lastExecutionTimeRange.contains(time)
238 && !(oldWasSymlink && newIsSymlink)) {
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +0000239 modifiedOutputFilesIntraBuildCounter.incrementAndGet();
240 }
241 }
242
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100243 private Runnable outputStatJob(final Collection<SkyKey> dirtyKeys,
244 final List<Pair<SkyKey, ActionExecutionValue>> shard) {
245 return new Runnable() {
246 @Override
247 public void run() {
248 for (Pair<SkyKey, ActionExecutionValue> keyAndValue : shard) {
249 ActionExecutionValue value = keyAndValue.getSecond();
250 if (value == null || actionValueIsDirtyWithDirectSystemCalls(value)) {
251 dirtyKeys.add(keyAndValue.getFirst());
252 }
253 }
254 }
255 };
256 }
257
258 /**
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +0000259 * Returns the number of modified output files inside of dirty actions.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100260 */
261 int getNumberOfModifiedOutputFiles() {
262 return modifiedOutputFilesCounter.get();
263 }
264
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +0000265 /**
266 * Returns the number of modified output files that occur during the previous build.
267 */
268 public int getNumberOfModifiedOutputFilesDuringPreviousBuild() {
269 return modifiedOutputFilesIntraBuildCounter.get();
270 }
271
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100272 private boolean actionValueIsDirtyWithDirectSystemCalls(ActionExecutionValue actionValue) {
273 boolean isDirty = false;
274 for (Map.Entry<Artifact, FileValue> entry :
275 actionValue.getAllOutputArtifactData().entrySet()) {
276 Artifact artifact = entry.getKey();
277 FileValue lastKnownData = entry.getValue();
278 try {
Janak Ramakrishnana5c1f962015-04-03 23:06:31 +0000279 FileValue fileValue = ActionMetadataHandler.fileValueFromArtifact(artifact, null, tsgm);
Miguel Alcon Pinto7cf23652015-03-10 21:27:48 +0000280 if (!fileValue.equals(lastKnownData)) {
281 updateIntraBuildModifiedCounter(fileValue.exists()
282 ? fileValue.realRootedPath().asPath().getLastModifiedTime()
Miguel Alcon Pintobdfdd092015-04-10 19:04:49 +0000283 : -1, lastKnownData.isSymlink(), fileValue.isSymlink());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100284 modifiedOutputFilesCounter.getAndIncrement();
285 isDirty = true;
286 }
287 } catch (IOException e) {
288 // This is an unexpected failure getting a digest or symlink target.
289 modifiedOutputFilesCounter.getAndIncrement();
290 isDirty = true;
291 }
292 }
293 return isDirty;
294 }
295
296 private BatchDirtyResult getDirtyValues(Iterable<SkyKey> values,
297 Predicate<SkyKey> keyFilter,
298 final DirtyChecker checker) throws InterruptedException {
299 ExecutorService executor = Executors.newFixedThreadPool(DIRTINESS_CHECK_THREADS,
300 new ThreadFactoryBuilder().setNameFormat("FileSystem Value Invalidator %d").build());
301
302 final BatchDirtyResult batchResult = new BatchDirtyResult();
303 ThrowableRecordingRunnableWrapper wrapper =
304 new ThrowableRecordingRunnableWrapper("FilesystemValueChecker#getDirtyValues");
305 for (final SkyKey key : values) {
306 Preconditions.checkState(keyFilter.apply(key), key);
307 final SkyValue value = valuesSupplier.get().get(key);
308 executor.execute(wrapper.wrap(new Runnable() {
309 @Override
310 public void run() {
311 if (value == null) {
312 // value will be null if the value is in error or part of a cycle.
313 // TODO(bazel-team): This is overly conservative.
314 batchResult.add(key, /*newValue=*/null);
315 return;
316 }
317 DirtyResult result = checker.check(key, value, tsgm);
318 if (result.isDirty()) {
319 batchResult.add(key, result.getNewValue());
320 }
321 }
322 }));
323 }
324
325 boolean interrupted = ExecutorShutdownUtil.interruptibleShutdown(executor);
326 Throwables.propagateIfPossible(wrapper.getFirstThrownError());
327 if (interrupted) {
328 throw new InterruptedException();
329 }
330 return batchResult;
331 }
332
333 private static DirtyResult checkFileStateValue(RootedPath rootedPath,
334 FileStateValue fileStateValue, TimestampGranularityMonitor tsgm) {
335 try {
336 FileStateValue newValue = FileStateValue.create(rootedPath, tsgm);
337 return newValue.equals(fileStateValue)
338 ? DirtyResult.NOT_DIRTY : DirtyResult.dirtyWithNewValue(newValue);
339 } catch (InconsistentFilesystemException | IOException e) {
340 // TODO(bazel-team): An IOException indicates a failure to get a file digest or a symlink
341 // target, not a missing file. Such a failure really shouldn't happen, so failing early
342 // may be better here.
343 return DirtyResult.DIRTY;
344 }
345 }
346
347 private static DirtyResult checkDirectoryListingStateValue(RootedPath dirRootedPath,
348 DirectoryListingStateValue directoryListingStateValue) {
349 try {
350 DirectoryListingStateValue newValue = DirectoryListingStateValue.create(dirRootedPath);
351 return newValue.equals(directoryListingStateValue)
352 ? DirtyResult.NOT_DIRTY : DirtyResult.dirtyWithNewValue(newValue);
353 } catch (IOException e) {
354 return DirtyResult.DIRTY;
355 }
356 }
357
358 /**
359 * Result of a batch call to {@link DirtyChecker#check}. Partitions the dirty values based on
360 * whether we have a new value available for them or not.
361 */
362 private static class BatchDirtyResult implements Differencer.Diff {
363
364 private final Set<SkyKey> concurrentDirtyKeysWithoutNewValues =
365 Collections.newSetFromMap(new ConcurrentHashMap<SkyKey, Boolean>());
366 private final ConcurrentHashMap<SkyKey, SkyValue> concurrentDirtyKeysWithNewValues =
367 new ConcurrentHashMap<>();
368
369 private void add(SkyKey key, @Nullable SkyValue newValue) {
370 if (newValue == null) {
371 concurrentDirtyKeysWithoutNewValues.add(key);
372 } else {
373 concurrentDirtyKeysWithNewValues.put(key, newValue);
374 }
375 }
376
377 @Override
378 public Iterable<SkyKey> changedKeysWithoutNewValues() {
379 return concurrentDirtyKeysWithoutNewValues;
380 }
381
382 @Override
383 public Map<SkyKey, ? extends SkyValue> changedKeysWithNewValues() {
384 return concurrentDirtyKeysWithNewValues;
385 }
386 }
387
388 private static class DirtyResult {
389
390 static final DirtyResult NOT_DIRTY = new DirtyResult(false, null);
391 static final DirtyResult DIRTY = new DirtyResult(true, null);
392
393 private final boolean isDirty;
394 @Nullable private final SkyValue newValue;
395
396 private DirtyResult(boolean isDirty, @Nullable SkyValue newValue) {
397 this.isDirty = isDirty;
398 this.newValue = newValue;
399 }
400
401 boolean isDirty() {
402 return isDirty;
403 }
404
405 /**
406 * If {@code isDirty()}, then either returns the new value for the value or {@code null} if
407 * the new value wasn't computed. In the case where the value is dirty and a new value is
408 * available, then the new value can be injected into the skyframe graph. Otherwise, the value
409 * should simply be invalidated.
410 */
411 @Nullable
412 SkyValue getNewValue() {
413 Preconditions.checkState(isDirty());
414 return newValue;
415 }
416
417 static DirtyResult dirtyWithNewValue(SkyValue newValue) {
418 return new DirtyResult(true, newValue);
419 }
420 }
421
422 private static interface DirtyChecker {
423 DirtyResult check(SkyKey key, SkyValue oldValue, TimestampGranularityMonitor tsgm);
424 }
425}