/*
* Copyright 2012-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.util.cache;
import com.facebook.buck.hashing.PathHashing;
import com.facebook.buck.io.ArchiveMemberPath;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.util.FileSystemMap;
import com.facebook.buck.util.MoreCollectors;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
public class DefaultFileHashCache implements ProjectFileHashCache {
private static final boolean SHOULD_CHECK_IGNORED_PATHS =
Boolean.getBoolean("buck.DefaultFileHashCache.check_ignored_paths");
private final ProjectFilesystem projectFilesystem;
private final Optional<Path> buckOutPath;
@VisibleForTesting final FileSystemMap<HashCodeAndFileType> newLoadingCache;
@VisibleForTesting final FileSystemMap<Long> newSizeCache;
// TODO(rvitale): remove block below after the file hash cache experiment is over.
/* *****************************************************************************/
@VisibleForTesting final LoadingCache<Path, HashCodeAndFileType> loadingCache;
@VisibleForTesting final LoadingCache<Path, Long> sizeCache;
/* *****************************************************************************/
@VisibleForTesting
DefaultFileHashCache(ProjectFilesystem projectFilesystem, Optional<Path> buckOutPath) {
this.projectFilesystem = projectFilesystem;
this.buckOutPath = buckOutPath;
this.newLoadingCache =
new FileSystemMap<>(
path -> {
try {
return getHashCodeAndFileType(path);
} catch (IOException e) {
throw new RuntimeException(e.getCause());
}
});
this.newSizeCache =
new FileSystemMap<>(
path -> {
try {
return getPathSize(path);
} catch (IOException e) {
throw new RuntimeException(e.getCause());
}
});
// TODO(rvitale): remove block below after the file hash cache experiment is over.
/* *****************************************************************************/
this.loadingCache =
CacheBuilder.newBuilder()
.build(
new CacheLoader<Path, HashCodeAndFileType>() {
@Override
public HashCodeAndFileType load(Path path) throws Exception {
return getHashCodeAndFileType(path);
}
});
this.sizeCache =
CacheBuilder.newBuilder()
.build(
new CacheLoader<Path, Long>() {
@Override
public Long load(Path path) throws Exception {
return getPathSize(path);
}
});
/* *****************************************************************************/
}
public static DefaultFileHashCache createBuckOutFileHashCache(
ProjectFilesystem projectFilesystem, Path buckOutPath) {
return new DefaultFileHashCache(projectFilesystem, Optional.of(buckOutPath));
}
public static DefaultFileHashCache createDefaultFileHashCache(
ProjectFilesystem projectFilesystem) {
return new DefaultFileHashCache(projectFilesystem, Optional.empty());
}
public static ImmutableList<? extends ProjectFileHashCache> createOsRootDirectoriesCaches()
throws InterruptedException {
ImmutableList.Builder<ProjectFileHashCache> allCaches = ImmutableList.builder();
for (Path root : FileSystems.getDefault().getRootDirectories()) {
if (!root.toFile().exists()) {
// On Windows, it is possible that the system will have a
// drive for something that does not exist such as a floppy
// disk or SD card. The drive exists, but it does not
// contain anything useful, so Buck should not consider it
// as a cacheable location.
continue;
}
ProjectFilesystem projectFilesystem =
ProjectFilesystem.createNewOrThrowHumanReadableException(root);
// A cache which caches hashes of absolute paths which my be accessed by certain
// rules (e.g. /usr/bin/gcc), and only serves to prevent rehashing the same file
// multiple times in a single run.
allCaches.add(DefaultFileHashCache.createDefaultFileHashCache(projectFilesystem));
}
return allCaches.build();
}
private void checkNotIgnored(Path relativePath) {
if (SHOULD_CHECK_IGNORED_PATHS) {
Preconditions.checkArgument(!projectFilesystem.isIgnored(relativePath));
}
}
// TODO(rvitale): restrict visibility of this method after the file hash cache experiment is over.
HashCodeAndFileType getHashCodeAndFileType(Path path) throws IOException {
if (projectFilesystem.isDirectory(path)) {
return getDirHashCode(path);
} else if (path.toString().endsWith(".jar")) {
return HashCodeAndFileType.ofArchive(getFileHashCode(path), projectFilesystem, path);
}
return HashCodeAndFileType.ofFile(getFileHashCode(path));
}
private HashCode getFileHashCode(Path path) throws IOException {
return projectFilesystem.computeSha1(path).asHashCode();
}
private long getPathSize(Path path) throws IOException {
long size = 0;
for (Path child : projectFilesystem.getFilesUnderPath(path)) {
size += projectFilesystem.getFileSize(child);
}
return size;
}
private HashCodeAndFileType getDirHashCode(Path path) throws IOException {
Hasher hasher = Hashing.sha1().newHasher();
ImmutableSet<Path> children = PathHashing.hashPath(hasher, this, projectFilesystem, path);
return HashCodeAndFileType.ofDirectory(hasher.hash(), children);
}
@Override
public boolean willGet(Path relativePath) {
Preconditions.checkState(!relativePath.isAbsolute());
checkNotIgnored(relativePath);
return loadingCache.getIfPresent(relativePath) != null
|| (projectFilesystem.exists(relativePath) && !isIgnored(relativePath));
}
private boolean isIgnored(Path path) {
if (buckOutPath.isPresent()) {
return !path.startsWith(buckOutPath.get());
}
return projectFilesystem.isIgnored(path);
}
@Override
public boolean willGet(ArchiveMemberPath archiveMemberPath) {
Preconditions.checkState(!archiveMemberPath.getArchivePath().isAbsolute());
checkNotIgnored(archiveMemberPath.getArchivePath());
return willGet(archiveMemberPath.getArchivePath());
}
// TODO(rvitale): remove block below after the file hash cache experiment is over.
/* *****************************************************************************/
private void invalidateImmediate(Path path) {
loadingCache.invalidate(path);
sizeCache.invalidate(path);
}
@Override
public void invalidate(Path relativePath) {
Preconditions.checkArgument(!relativePath.isAbsolute());
checkNotIgnored(relativePath);
HashCodeAndFileType cached = loadingCache.getIfPresent(relativePath);
invalidateImmediate(relativePath);
if (cached != null) {
for (Path child : cached.getChildren()) {
invalidateImmediate(relativePath.resolve(child));
}
}
}
@Override
public void invalidateAll() {
loadingCache.invalidateAll();
sizeCache.invalidateAll();
}
/* *****************************************************************************/
// TODO(rvitale): rename functions below after the file hash cache experiment is over.
/* *****************************************************************************/
public void invalidateNew(Path relativePath) {
Preconditions.checkArgument(!relativePath.isAbsolute());
checkNotIgnored(relativePath);
newLoadingCache.remove(relativePath);
newSizeCache.remove(relativePath);
}
public void invalidateAllNew() {
newLoadingCache.removeAll();
newSizeCache.removeAll();
}
/* *****************************************************************************/
/** @return The {@link com.google.common.hash.HashCode} of the contents of path. */
@Override
public HashCode get(Path relativePath) throws IOException {
Preconditions.checkArgument(!relativePath.isAbsolute());
checkNotIgnored(relativePath);
HashCode sha1;
// TODO(rvitale): remove block below after the file hash cache experiment is over.
/* *****************************************************************************/
try {
sha1 = loadingCache.get(relativePath.normalize()).getHashCode();
} catch (ExecutionException e) {
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
throw new RuntimeException(e.getCause());
}
/* *****************************************************************************/
return Preconditions.checkNotNull(sha1, "Failed to find a HashCode for %s.", relativePath);
}
@Override
public long getSize(Path relativePath) throws IOException {
Preconditions.checkArgument(!relativePath.isAbsolute());
checkNotIgnored(relativePath);
newSizeCache.get(relativePath.normalize());
// TODO(rvitale): remove block below after the file hash cache experiment is over.
/* *****************************************************************************/
try {
return sizeCache.get(relativePath.normalize());
} catch (ExecutionException e) {
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
throw new RuntimeException(e.getCause());
}
/* *****************************************************************************/
}
@Override
public HashCode get(ArchiveMemberPath archiveMemberPath) throws IOException {
Preconditions.checkArgument(!archiveMemberPath.isAbsolute());
checkNotIgnored(archiveMemberPath.getArchivePath());
Path relativeFilePath = archiveMemberPath.getArchivePath().normalize();
// TODO(rvitale): remove try-catch and un-indent after file hash cache experiment is over.
/* *****************************************************************************/
try {
HashCodeAndFileType fileHashCodeAndFileType = loadingCache.get(relativeFilePath);
Path memberPath = archiveMemberPath.getMemberPath();
HashCodeAndFileType memberHashCodeAndFileType =
fileHashCodeAndFileType.getContents().get(memberPath);
if (memberHashCodeAndFileType == null) {
throw new NoSuchFileException(archiveMemberPath.toString());
}
return memberHashCodeAndFileType.getHashCode();
} catch (ExecutionException e) {
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
throw new RuntimeException(e.getCause());
}
/* *****************************************************************************/
}
@Override
public ProjectFilesystem getFilesystem() {
return projectFilesystem;
}
@Override
public void set(Path relativePath, HashCode hashCode) throws IOException {
Preconditions.checkArgument(!relativePath.isAbsolute());
checkNotIgnored(relativePath);
HashCodeAndFileType value;
if (projectFilesystem.isDirectory(relativePath)) {
value =
HashCodeAndFileType.ofDirectory(
hashCode,
projectFilesystem
.getFilesUnderPath(relativePath)
.stream()
.map(relativePath::relativize)
.collect(MoreCollectors.toImmutableSet()));
} else if (relativePath.toString().endsWith(".jar")) {
value =
HashCodeAndFileType.ofArchive(
hashCode,
projectFilesystem,
projectFilesystem.getPathRelativeToProjectRoot(relativePath).get());
} else {
value = HashCodeAndFileType.ofFile(hashCode);
}
newLoadingCache.put(relativePath, value);
}
@Override
public FileHashCacheVerificationResult verify() throws IOException {
List<String> errors = new ArrayList<>();
Map<Path, HashCodeAndFileType> cacheMap = loadingCache.asMap();
for (Map.Entry<Path, HashCodeAndFileType> entry : cacheMap.entrySet()) {
Path path = entry.getKey();
HashCodeAndFileType cached = entry.getValue();
HashCodeAndFileType current = getHashCodeAndFileType(path);
if (!cached.equals(current)) {
errors.add(path.toString());
}
}
return FileHashCacheVerificationResult.builder()
.setCachesExamined(1)
.setFilesExamined(cacheMap.size())
.addAllVerificationErrors(errors)
.build();
}
}