Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//
package com.google.devtools.build.lib.vfs;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
Expand Down Expand Up @@ -62,7 +63,7 @@ public InputStream getInputStream(PathFragment path) throws IOException {

/** Allows the mapping of PathFragment to InputStream to be overridden in subclasses. */
protected InputStream createFileInputStream(PathFragment path) throws IOException {
return new FileInputStream(getIoFile(path));
return new FileInputStream(checkNotNull(getIoFile(path)));
}

/** Returns either normal or profiled FileInputStream. */
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -797,21 +797,21 @@ public abstract void createFSDependentHardLink(PathFragment linkPath, PathFragme
public void prefetchPackageAsync(PathFragment path, int maxDirs) {}

/**
* Returns a {@link File} object for the given path. This method is only supported by file system
* implementations that are backed by the local file system.
* Returns a {@link File} object for the given path or null if this file system implementation is
* not backed by the local file system.
*/
@Nullable
public File getIoFile(PathFragment path) {
throw new UnsupportedOperationException(
"getIoFile() not supported for " + getClass().getName());
return null;
}

/**
* Returns a {@link java.nio.file.Path} object for the given path. This method is only supported
* by file system implementations that are backed by the local file system.
* Returns a {@link java.nio.file.Path} object for the given path or null if this file system
* implementation is not backed by the local file system.
*/
@Nullable
public java.nio.file.Path getNioPath(PathFragment path) {
throw new UnsupportedOperationException(
"getNioPath() not supported for " + getClass().getName());
return null;
}

// Mapping from FileSystemException reason strings on various platforms to the corresponding Unix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
// limitations under the License.
package com.google.devtools.build.lib.vfs;

import static com.google.devtools.build.lib.vfs.FileSystem.translateNioToIoException;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -374,15 +377,31 @@ public static ByteSink asByteSink(final Path path) {
*/
@ThreadSafe // but not atomic
public static void copyFile(Path from, Path to) throws IOException {
var fromNio = from.getFileSystem().getNioPath(from.asFragment());
var toNio = to.getFileSystem().getNioPath(to.asFragment());
if (fromNio != null && toNio != null) {
// Fast path: Files.copy uses various optimizations such as kernel buffers (sendfile on Unix)
// or copy-on-write (clonefile on macOS, copy_file_range on Linux with a supported file system).
try {
java.nio.file.Files.copy(fromNio, toNio, REPLACE_EXISTING, COPY_ATTRIBUTES);
} catch (IOException e) {
throw translateNioToIoException(from.asFragment(), e);
}
return;
}
Copy link
Contributor

@tjgq tjgq Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also replace ByteStreams.copy(in, out) with in.transferTo(out) below? It can still do in-kernel I/O when both sides are files, and falls back to essentially equivalent code otherwise.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that and realized that the copy fallback in moveFile can also be optimized in this way.

try {
// Target may be a symlink, in which case opening a stream below would not actually replace
// it.
to.delete();
} catch (IOException e) {
throw new IOException("error copying file: "
+ "couldn't delete destination: " + e.getMessage());
}
try (InputStream in = from.getInputStream();
OutputStream out = to.getOutputStream()) {
ByteStreams.copy(in, out);
// This may use a faster copy method (such as via an in-kernel buffer) if both streams are
// backed by files.
in.transferTo(out);
}
to.setLastModifiedTime(from.getLastModifiedTime()); // Preserve mtime.
if (!from.isWritable()) {
Expand All @@ -400,25 +419,6 @@ public enum MoveResult {
FILE_COPIED,
}

/**
* copyLargeBuffer is a replacement for ByteStreams.copy which uses a larger buffer. Increasing
* the buffer size is a performance improvement when copying from/to FUSE file systems, where
* individual requests are more costly, but can also be larger.
*/
private static long copyLargeBuffer(InputStream from, OutputStream to) throws IOException {
byte[] buf = new byte[1 * 1024 * 1024]; // Match libfuse3 maximum FUSE request size of 1 MB.
long total = 0;
while (true) {
int r = from.read(buf);
if (r == -1) {
break;
}
to.write(buf, 0, r);
total += r;
}
return total;
}

/**
* Moves the file from location "from" to location "to", while overwriting a potentially existing
* "to". If "from" is a regular file, its last modified time, executable and writable bits are
Expand Down Expand Up @@ -449,29 +449,17 @@ public static MoveResult moveFile(Path from, Path to) throws IOException {
// Fallback to a copy.
FileStatus stat = from.stat(Symlinks.NOFOLLOW);
if (stat.isFile()) {
// Target may be a symlink, in which case opening a stream below would not actually replace
// it.
to.delete();
try (InputStream in = from.getInputStream();
OutputStream out = to.getOutputStream()) {
copyLargeBuffer(in, out);
try {
copyFile(from, to);
} catch (FileAccessException e) {
// Rules can accidentally make output non-readable, let's fix that (b/150963503)
if (!from.isReadable()) {
from.setReadable(true);
try (InputStream in = from.getInputStream();
OutputStream out = to.getOutputStream()) {
copyLargeBuffer(in, out);
}
copyFile(from, to);
} else {
throw e;
}
}
to.setLastModifiedTime(stat.getLastModifiedTime()); // Preserve mtime.
if (!from.isWritable()) {
to.setWritable(false); // Make file read-only if original was read-only.
}
to.setExecutable(from.isExecutable()); // Copy executable bit.
} else if (stat.isSymbolicLink()) {
PathFragment fromTarget = from.readSymbolicLink();
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.util.Collection;
import javax.annotation.Nullable;

/**
* FileSystem implementation which delegates all operations to a provided instance with a
Expand Down Expand Up @@ -305,11 +306,13 @@ public void prefetchPackageAsync(PathFragment path, int maxDirs) {
delegateFs.prefetchPackageAsync(toDelegatePath(path), maxDirs);
}

@Nullable
@Override
public File getIoFile(PathFragment path) {
return delegateFs.getIoFile(toDelegatePath(path));
}

@Nullable
@Override
public java.nio.file.Path getNioPath(PathFragment path) {
return delegateFs.getNioPath(toDelegatePath(path));
Expand Down