|
20 | 20 | import static com.google.common.collect.ImmutableList.toImmutableList; |
21 | 21 | import static com.google.common.util.concurrent.Futures.allAsList; |
22 | 22 | import static com.google.common.util.concurrent.Futures.immediateFuture; |
23 | | -import static com.google.common.util.concurrent.Futures.submitAsync; |
24 | 23 | import static com.google.common.util.concurrent.Futures.transform; |
25 | 24 | import static com.google.common.util.concurrent.MoreExecutors.directExecutor; |
26 | 25 | import static com.google.devtools.build.lib.util.StringEncoding.internalToUnicode; |
|
43 | 42 | import com.google.common.collect.Iterables; |
44 | 43 | import com.google.common.collect.Iterators; |
45 | 44 | import com.google.common.collect.Lists; |
| 45 | +import com.google.common.util.concurrent.AsyncCallable; |
| 46 | +import com.google.common.util.concurrent.Futures; |
46 | 47 | import com.google.common.util.concurrent.ListenableFuture; |
47 | 48 | import com.google.devtools.build.lib.actions.ActionInput; |
48 | 49 | import com.google.devtools.build.lib.actions.ActionInputHelper; |
|
58 | 59 | import com.google.devtools.build.lib.actions.StaticInputMetadataProvider; |
59 | 60 | import com.google.devtools.build.lib.actions.cache.VirtualActionInput; |
60 | 61 | import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| 62 | +import com.google.devtools.build.lib.concurrent.TaskDeduplicator; |
61 | 63 | import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext; |
62 | 64 | import com.google.devtools.build.lib.profiler.Profiler; |
63 | 65 | import com.google.devtools.build.lib.profiler.SilentCloseable; |
|
87 | 89 | import java.util.Objects; |
88 | 90 | import java.util.Set; |
89 | 91 | import java.util.concurrent.CancellationException; |
90 | | -import java.util.concurrent.ConcurrentHashMap; |
91 | 92 | import java.util.concurrent.ExecutionException; |
92 | 93 | import java.util.concurrent.ExecutorService; |
93 | 94 | import java.util.concurrent.Executors; |
94 | 95 | import java.util.concurrent.Future; |
95 | | -import java.util.concurrent.atomic.AtomicBoolean; |
96 | 96 | import java.util.function.Predicate; |
| 97 | +import java.util.function.Supplier; |
97 | 98 | import javax.annotation.Nullable; |
98 | 99 |
|
99 | 100 | /** |
@@ -174,8 +175,8 @@ public final class MerkleTreeComputer { |
174 | 175 | private final String workspaceName; |
175 | 176 | private final Digest emptyDigest; |
176 | 177 | private final MerkleTree.Uploadable emptyTree; |
177 | | - private final ConcurrentHashMap<InFlightCacheKey, ListenableFuture<MerkleTree.RootOnly>> |
178 | | - inFlightSubTreeCache = new ConcurrentHashMap<>(); |
| 178 | + private final TaskDeduplicator<InFlightCacheKey, MerkleTree.RootOnly> inFlightComputations = |
| 179 | + new TaskDeduplicator<>(); |
179 | 180 |
|
180 | 181 | public MerkleTreeComputer( |
181 | 182 | DigestUtil digestUtil, |
@@ -282,7 +283,6 @@ private MerkleTree doBuildForSpawn( |
282 | 283 | if (!Objects.equals(scrubber, lastScrubber)) { |
283 | 284 | persistentToolSubTreeCache.invalidateAll(); |
284 | 285 | persistentNonToolSubTreeCache.invalidateAll(); |
285 | | - inFlightSubTreeCache.clear(); |
286 | 286 | lastScrubber = scrubber; |
287 | 287 | } |
288 | 288 | } |
@@ -848,99 +848,88 @@ private ListenableFuture<MerkleTree.RootOnly> computeIfAbsent( |
848 | 848 | return immediateFuture(cachedRoot); |
849 | 849 | } |
850 | 850 | } |
851 | | - var inFlightCacheKey = new InFlightCacheKey(metadata, isTool, blobPolicy != BlobPolicy.DISCARD); |
852 | | - if (blobPolicy == BlobPolicy.KEEP_AND_REUPLOAD) { |
853 | | - inFlightSubTreeCache.remove(inFlightCacheKey); |
854 | | - } |
855 | | - var newlyComputed = new AtomicBoolean(); |
856 | | - var future = |
857 | | - inFlightSubTreeCache.computeIfAbsent( |
858 | | - inFlightCacheKey, |
859 | | - unusedKey -> { |
860 | | - newlyComputed.set(true); |
861 | | - return submitAsync( |
862 | | - () -> { |
863 | | - // There is a window in which a concurrent call may have removed the in-flight |
864 | | - // cache entry while this one had already passed the check above. Recheck the |
865 | | - // persistent cache to avoid unnecessary work. |
866 | | - var cachedRoot = persistentCache.getIfPresent(metadata); |
867 | | - if (cachedRoot != null |
868 | | - && (blobPolicy == BlobPolicy.DISCARD |
869 | | - || cachedRoot instanceof MerkleTree.RootOnly.BlobsUploaded)) { |
870 | | - return immediateFuture(cachedRoot); |
871 | | - } |
872 | | - // An ongoing computation with blobs can be reused for one that doesn't require |
873 | | - // them. |
874 | | - if (blobPolicy == BlobPolicy.DISCARD) { |
875 | | - var inFlightComputation = |
876 | | - inFlightSubTreeCache.get( |
877 | | - new InFlightCacheKey(metadata, isTool, /* uploadBlobs= */ true)); |
878 | | - if (inFlightComputation != null) { |
879 | | - return inFlightComputation; |
880 | | - } |
881 | | - } |
882 | | - ListenableFuture<MerkleTree> merkleTreeFuture; |
883 | | - try { |
884 | | - // Subtrees either consist entirely of tool inputs or don't contain any. |
885 | | - // The same applies to scrubbed inputs. |
886 | | - merkleTreeFuture = |
887 | | - build( |
888 | | - sortedInputsSupplier.compute(), |
889 | | - isTool ? alwaysTrue() : alwaysFalse(), |
890 | | - /* spawnScrubber= */ null, |
891 | | - metadataProvider, |
892 | | - artifactPathResolver, |
893 | | - remoteActionExecutionContext, |
894 | | - remotePathResolver, |
895 | | - blobPolicy); |
896 | | - } catch (IOException e) { |
897 | | - throw new WrappedException(e); |
898 | | - } catch (InterruptedException e) { |
899 | | - throw new WrappedException(e); |
| 851 | + var key = new InFlightCacheKey(metadata, isTool, blobPolicy != BlobPolicy.DISCARD); |
| 852 | + AsyncCallable<MerkleTree.RootOnly> buildMerkleTreeTask = |
| 853 | + () -> { |
| 854 | + // There is a window in which a concurrent call may have removed the in-flight cache entry |
| 855 | + // while this one had already passed the check above. Recheck the persistent cache to |
| 856 | + // avoid unnecessary work. |
| 857 | + var cachedRoot = persistentCache.getIfPresent(metadata); |
| 858 | + if (cachedRoot != null |
| 859 | + && (blobPolicy == BlobPolicy.DISCARD |
| 860 | + || cachedRoot instanceof MerkleTree.RootOnly.BlobsUploaded)) { |
| 861 | + return immediateFuture(cachedRoot); |
| 862 | + } |
| 863 | + // An ongoing computation with blobs can be reused for one that doesn't require them. |
| 864 | + if (blobPolicy == BlobPolicy.DISCARD) { |
| 865 | + var inFlightComputation = |
| 866 | + inFlightComputations.maybeJoinExecution( |
| 867 | + new InFlightCacheKey(metadata, isTool, /* uploadBlobs= */ true)); |
| 868 | + if (inFlightComputation != null) { |
| 869 | + return inFlightComputation; |
| 870 | + } |
| 871 | + } |
| 872 | + ListenableFuture<MerkleTree> merkleTreeFuture; |
| 873 | + try { |
| 874 | + // Subtrees either consist entirely of tool inputs or don't contain any. The same |
| 875 | + // applies to scrubbed inputs. |
| 876 | + merkleTreeFuture = |
| 877 | + build( |
| 878 | + sortedInputsSupplier.compute(), |
| 879 | + isTool ? alwaysTrue() : alwaysFalse(), |
| 880 | + /* spawnScrubber= */ null, |
| 881 | + metadataProvider, |
| 882 | + artifactPathResolver, |
| 883 | + remoteActionExecutionContext, |
| 884 | + remotePathResolver, |
| 885 | + blobPolicy); |
| 886 | + } catch (IOException e) { |
| 887 | + throw new WrappedException(e); |
| 888 | + } catch (InterruptedException e) { |
| 889 | + throw new WrappedException(e); |
| 890 | + } |
| 891 | + return transform( |
| 892 | + merkleTreeFuture, |
| 893 | + merkleTree -> { |
| 894 | + if (merkleTree instanceof MerkleTree.Uploadable uploadable) { |
| 895 | + try { |
| 896 | + if (merkleTreeUploader != null) { |
| 897 | + merkleTreeUploader.ensureInputsPresent( |
| 898 | + remoteActionExecutionContext, |
| 899 | + uploadable, |
| 900 | + blobPolicy == BlobPolicy.KEEP_AND_REUPLOAD, |
| 901 | + remotePathResolver); |
900 | 902 | } |
901 | | - return transform( |
902 | | - merkleTreeFuture, |
903 | | - merkleTree -> { |
904 | | - if (merkleTree instanceof MerkleTree.Uploadable uploadable) { |
905 | | - try { |
906 | | - if (merkleTreeUploader != null) { |
907 | | - merkleTreeUploader.ensureInputsPresent( |
908 | | - remoteActionExecutionContext, |
909 | | - uploadable, |
910 | | - blobPolicy == BlobPolicy.KEEP_AND_REUPLOAD, |
911 | | - remotePathResolver); |
912 | | - } |
913 | | - } catch (IOException e) { |
914 | | - throw new WrappedException(e); |
915 | | - } catch (InterruptedException e) { |
916 | | - throw new WrappedException(e); |
917 | | - } |
918 | | - } |
919 | | - // Move the computed root to the persistent cache so that it can be reused |
920 | | - // by later builds. |
921 | | - persistentCache |
922 | | - .asMap() |
923 | | - .compute( |
924 | | - metadata, |
925 | | - (unused, oldRoot) -> { |
926 | | - // Don't downgrade the cached root from one indicating that its |
927 | | - // blobs have been uploaded. |
928 | | - return oldRoot instanceof MerkleTree.RootOnly.BlobsUploaded |
929 | | - ? oldRoot |
930 | | - : merkleTree.root(); |
931 | | - }); |
932 | | - return merkleTree.root(); |
933 | | - }, |
934 | | - MERKLE_TREE_UPLOAD_POOL); |
935 | | - }, |
936 | | - MERKLE_TREE_BUILD_POOL); |
937 | | - }); |
938 | | - if (newlyComputed.get()) { |
939 | | - // Clean up the in-flight cache so that it doesn't grow unboundedly. |
940 | | - future.addListener( |
941 | | - () -> inFlightSubTreeCache.remove(inFlightCacheKey, future), directExecutor()); |
| 903 | + } catch (IOException e) { |
| 904 | + throw new WrappedException(e); |
| 905 | + } catch (InterruptedException e) { |
| 906 | + throw new WrappedException(e); |
| 907 | + } |
| 908 | + } |
| 909 | + // Move the computed root to the persistent cache so that it can be reused by later |
| 910 | + // builds. |
| 911 | + persistentCache |
| 912 | + .asMap() |
| 913 | + .compute( |
| 914 | + metadata, |
| 915 | + (unused, oldRoot) -> { |
| 916 | + // Don't downgrade the cached root from one indicating that its blobs have |
| 917 | + // been uploaded. |
| 918 | + return oldRoot instanceof MerkleTree.RootOnly.BlobsUploaded |
| 919 | + ? oldRoot |
| 920 | + : merkleTree.root(); |
| 921 | + }); |
| 922 | + return merkleTree.root(); |
| 923 | + }, |
| 924 | + MERKLE_TREE_UPLOAD_POOL); |
| 925 | + }; |
| 926 | + Supplier<ListenableFuture<MerkleTree.RootOnly>> buildMerkleTreeTaskSupplier = |
| 927 | + () -> Futures.submitAsync(buildMerkleTreeTask, MERKLE_TREE_BUILD_POOL); |
| 928 | + if (blobPolicy == BlobPolicy.KEEP_AND_REUPLOAD) { |
| 929 | + return inFlightComputations.executeUnconditionally(key, buildMerkleTreeTaskSupplier); |
| 930 | + } else { |
| 931 | + return inFlightComputations.executeIfNew(key, buildMerkleTreeTaskSupplier); |
942 | 932 | } |
943 | | - return future; |
944 | 933 | } |
945 | 934 |
|
946 | 935 | private static <T> T getFromFuture(Future<T> future) throws IOException, InterruptedException { |
|
0 commit comments