Improve the FileSystem inheritance hierarchy.

By repurposing AbstractFileSystem as DiskBackedFileSystem, to be extended only by disk-backed subclasses. This encourages other subclasses to make an explicit decision on how to open files instead of inheriting a potentially incorrect implementation.

As a bonus, we can also dispense with the duplicate profiling logic in UnixFileSystem.

PiperOrigin-RevId: 837473162
Change-Id: I970d0a9711ecc583b1ecae650703d14d4a5db2a4
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java
index cc35b39..b47645a 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java
@@ -40,7 +40,6 @@
 import com.google.devtools.build.lib.clock.Clock;
 import com.google.devtools.build.lib.remote.common.BulkTransferException;
 import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
-import com.google.devtools.build.lib.vfs.AbstractFileSystem;
 import com.google.devtools.build.lib.vfs.DigestHashFunction;
 import com.google.devtools.build.lib.vfs.Dirent;
 import com.google.devtools.build.lib.vfs.FileStatus;
@@ -101,8 +100,7 @@
  * sources, such as the same path existing in multiple underlying sources with different type or
  * contents.
  */
-public class RemoteActionFileSystem extends AbstractFileSystem
-    implements PathCanonicalizer.Resolver {
+public class RemoteActionFileSystem extends FileSystem implements PathCanonicalizer.Resolver {
   private final PathFragment execRoot;
   private final PathFragment outputBase;
   private final InputMetadataProvider inputArtifactData;
@@ -901,7 +899,7 @@
   @Override
   public void createFSDependentHardLink(PathFragment linkPath, PathFragment originalPath)
       throws IOException {
-    // Only called by the AbstractFileSystem#createHardLink base implementation, overridden below.
+    // Only called by the FileSystem#createHardLink base implementation, overridden below.
     throw new UnsupportedOperationException();
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java b/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
index 0efcb81..1b06c71 100644
--- a/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
@@ -21,28 +21,26 @@
 import com.google.devtools.build.lib.util.Blocker;
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.StringEncoding;
-import com.google.devtools.build.lib.vfs.AbstractFileSystem;
 import com.google.devtools.build.lib.vfs.DigestHashFunction;
 import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.DiskBackedFileSystem;
 import com.google.devtools.build.lib.vfs.FileStatus;
 import com.google.devtools.build.lib.vfs.FileSymlinkLoopException;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.SymlinkTargetType;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.ArrayDeque;
 import java.util.Collection;
 import javax.annotation.Nullable;
 
-/** This class implements the FileSystem interface using direct calls to the UNIX filesystem. */
+/**
+ * A disk-backed filesystem suitable for Unix systems, implemented using a mix of JNI and standard
+ * library calls.
+ */
 @ThreadSafe
-public class UnixFileSystem extends AbstractFileSystem {
+public class UnixFileSystem extends DiskBackedFileSystem {
   protected final String hashAttributeName;
 
   public UnixFileSystem(DigestHashFunction hashFunction, String hashAttributeName) {
@@ -441,80 +439,4 @@
   public java.nio.file.Path getNioPath(PathFragment path) {
     return java.nio.file.Path.of(StringEncoding.internalToPlatform(path.getPathString()));
   }
-
-  @Override
-  protected InputStream createFileInputStream(PathFragment path) throws IOException {
-    return new FileInputStream(StringEncoding.internalToPlatform(path.getPathString()));
-  }
-
-  protected OutputStream createFileOutputStream(PathFragment path, boolean append)
-      throws FileNotFoundException {
-    return createFileOutputStream(path, append, /* internal= */ false);
-  }
-
-  @Override
-  protected OutputStream createFileOutputStream(PathFragment path, boolean append, boolean internal)
-      throws FileNotFoundException {
-    String name = path.getPathString();
-    var profiler = Profiler.instance();
-    if (!internal
-        && profiler.isActive()
-        && (profiler.isProfiling(ProfilerTask.VFS_WRITE)
-            || profiler.isProfiling(ProfilerTask.VFS_OPEN))) {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      var comp = Blocker.begin();
-      try {
-        return new ProfiledFileOutputStream(name, /* append= */ append);
-      } finally {
-        Blocker.end(comp);
-        profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, name);
-      }
-    } else {
-      var comp = Blocker.begin();
-      try {
-        return new FileOutputStream(StringEncoding.internalToPlatform(name), /* append= */ append);
-      } finally {
-        Blocker.end(comp);
-      }
-    }
-  }
-
-  private static final class ProfiledFileOutputStream extends FileOutputStream {
-    private final String name;
-
-    private ProfiledFileOutputStream(String name, boolean append) throws FileNotFoundException {
-      super(StringEncoding.internalToPlatform(name), append);
-      this.name = name;
-    }
-
-    @Override
-    public void write(int b) throws IOException {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      try {
-        super.write(b);
-      } finally {
-        Profiler.instance().logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
-      }
-    }
-
-    @Override
-    public void write(byte[] b) throws IOException {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      try {
-        super.write(b);
-      } finally {
-        Profiler.instance().logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
-      }
-    }
-
-    @Override
-    public void write(byte[] b, int off, int len) throws IOException {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      try {
-        super.write(b, off, len);
-      } finally {
-        Profiler.instance().logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
-      }
-    }
-  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
deleted file mode 100644
index ab08288..0000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
+++ /dev/null
@@ -1,204 +0,0 @@
-// Copyright 2019 The Bazel Authors. 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.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;
-import static java.nio.file.StandardOpenOption.WRITE;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
-import com.google.devtools.build.lib.profiler.Profiler;
-import com.google.devtools.build.lib.profiler.ProfilerTask;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.channels.SeekableByteChannel;
-import java.nio.file.Files;
-import java.nio.file.StandardOpenOption;
-
-/** This class implements the FileSystem interface using direct calls to the UNIX filesystem. */
-@ThreadSafe
-public abstract class AbstractFileSystem extends FileSystem {
-
-  public AbstractFileSystem(DigestHashFunction digestFunction) {
-    super(digestFunction);
-  }
-
-  @Override
-  public InputStream getInputStream(PathFragment path) throws IOException {
-    try {
-      return createMaybeProfiledInputStream(path);
-    } catch (FileNotFoundException e) {
-      // FileInputStream throws FileNotFoundException if opening fails for any reason, including
-      // permissions. Fix it up here.  TODO(tjgq): Migrate to java.nio.
-      if (e.getMessage().equals(path + ERR_PERMISSION_DENIED)) {
-        throw new FileAccessException(e.getMessage());
-      }
-      throw e;
-    }
-  }
-
-  /** Allows the mapping of PathFragment to InputStream to be overridden in subclasses. */
-  protected InputStream createFileInputStream(PathFragment path) throws IOException {
-    return new FileInputStream(checkNotNull(getIoFile(path)));
-  }
-
-  /** Returns either normal or profiled FileInputStream. */
-  private InputStream createMaybeProfiledInputStream(PathFragment path) throws IOException {
-    final String name = path.toString();
-    var profiler = Profiler.instance();
-    if (profiler.isActive()
-        && (profiler.isProfiling(ProfilerTask.VFS_READ)
-            || profiler.isProfiling(ProfilerTask.VFS_OPEN))) {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      try {
-        // Replace default FileInputStream instance with the custom one that does profiling.
-        return new ProfiledInputStream(createFileInputStream(path), name);
-      } finally {
-        profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, name);
-      }
-    } else {
-      // Use normal FileInputStream instance if profiler is not enabled.
-      return createFileInputStream(path);
-    }
-  }
-
-  private static final ImmutableSet<StandardOpenOption> READ_WRITE_BYTE_CHANNEL_OPEN_OPTIONS =
-      Sets.immutableEnumSet(READ, WRITE, CREATE, TRUNCATE_EXISTING);
-
-  @Override
-  public SeekableByteChannel createReadWriteByteChannel(PathFragment path) throws IOException {
-    var profiler = Profiler.instance();
-    boolean shouldProfile = profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_OPEN);
-
-    long startTime = Profiler.instance().nanoTimeMaybe();
-
-    try {
-      // Currently, we do not proxy SeekableByteChannel for profiling reads and writes.
-      return Files.newByteChannel(getNioPath(path), READ_WRITE_BYTE_CHANNEL_OPEN_OPTIONS);
-    } finally {
-      if (shouldProfile) {
-        profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, path.toString());
-      }
-    }
-  }
-
-  /**
-   * Returns either normal or profiled FileOutputStream. Should be used by subclasses to create
-   * default OutputStream instance.
-   */
-  protected OutputStream createFileOutputStream(PathFragment path, boolean append, boolean internal)
-      throws FileNotFoundException {
-    var profiler = Profiler.instance();
-    if (!internal
-        && profiler.isActive()
-        && (profiler.isProfiling(ProfilerTask.VFS_WRITE)
-            || profiler.isProfiling(ProfilerTask.VFS_OPEN))) {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      try {
-        return new ProfiledFileOutputStream(getIoFile(path), append);
-      } finally {
-        profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, path.toString());
-      }
-    } else {
-      return new FileOutputStream(getIoFile(path), append);
-    }
-  }
-
-  @Override
-  public OutputStream getOutputStream(PathFragment path, boolean append, boolean internal)
-      throws IOException {
-    try {
-      return createFileOutputStream(path, append, internal);
-    } catch (FileNotFoundException e) {
-      // FileOutputStream throws FileNotFoundException if opening fails for any reason,
-      // including permissions. Fix it up here.
-      // TODO(tjgq): Migrate to java.nio.
-      if (e.getMessage().equals(path + ERR_PERMISSION_DENIED)) {
-        throw new FileAccessException(e.getMessage());
-      }
-      throw e;
-    }
-  }
-
-  private static final class ProfiledInputStream extends FilterInputStream {
-    private final InputStream impl;
-    private final String name;
-
-    public ProfiledInputStream(InputStream impl, String name) {
-      super(impl);
-      this.impl = impl;
-      this.name = name;
-    }
-
-    @Override
-    public int read() throws IOException {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      try {
-        return impl.read();
-      } finally {
-        Profiler.instance().logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
-      }
-    }
-
-    @Override
-    public int read(byte[] b) throws IOException {
-      return read(b, 0, b.length);
-    }
-
-    @Override
-    public int read(byte[] b, int off, int len) throws IOException {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      try {
-        return impl.read(b, off, len);
-      } finally {
-        Profiler.instance().logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
-      }
-    }
-  }
-
-  private static final class ProfiledFileOutputStream extends FileOutputStream {
-    private final File file;
-
-    public ProfiledFileOutputStream(File file, boolean append) throws FileNotFoundException {
-      super(file, append);
-      this.file = file;
-    }
-
-    @Override
-    public void write(byte[] b) throws IOException {
-      write(b, 0, b.length);
-    }
-
-    @Override
-    public void write(byte[] b, int off, int len) throws IOException {
-      long startTime = Profiler.instance().nanoTimeMaybe();
-      try {
-        super.write(b, off, len);
-      } finally {
-        Profiler.instance().logSimpleTask(startTime, ProfilerTask.VFS_WRITE, file.toString());
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/DiskBackedFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/DiskBackedFileSystem.java
new file mode 100644
index 0000000..cdec542
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/DiskBackedFileSystem.java
@@ -0,0 +1,212 @@
+// Copyright 2019 The Bazel Authors. 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.vfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+
+/**
+ * This class extends {@link FileSystem} with default implementations providing access to files on
+ * disk through standard library APIs.
+ */
+@ThreadSafe
+public abstract class DiskBackedFileSystem extends FileSystem {
+  private static final Profiler profiler = Profiler.instance();
+
+  private static final ImmutableSet<StandardOpenOption> READ_WRITE_BYTE_CHANNEL_OPEN_OPTIONS =
+      ImmutableSet.of(
+          StandardOpenOption.READ,
+          StandardOpenOption.WRITE,
+          StandardOpenOption.CREATE,
+          StandardOpenOption.TRUNCATE_EXISTING);
+
+  protected DiskBackedFileSystem(DigestHashFunction hashFunction) {
+    super(hashFunction);
+  }
+
+  // Force subclasses to override getIoFile and getNioPath, as the methods below require them.
+
+  @Override
+  public abstract File getIoFile(PathFragment path);
+
+  @Override
+  public abstract java.nio.file.Path getNioPath(PathFragment path);
+
+  @Override
+  public InputStream getInputStream(PathFragment path) throws IOException {
+    File file = checkNotNull(getIoFile(path), "getIoFile() must not be null");
+
+    boolean profileOpen = profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_OPEN);
+    boolean profileRead = profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_READ);
+
+    long startTime = profiler.nanoTimeMaybe();
+    try {
+      return profileRead
+          ? new ProfiledFileInputStream(file, path.getPathString())
+          : new FileInputStream(file);
+    } catch (FileNotFoundException e) {
+      // FileInputStream throws FileNotFoundException if opening fails for any reason, including
+      // permissions. Fix it up here.
+      if (e.getMessage().endsWith(ERR_PERMISSION_DENIED)) {
+        throw new FileAccessException(e.getMessage());
+      }
+      throw e;
+    } finally {
+      if (profileOpen) {
+        profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, path.getPathString());
+      }
+    }
+  }
+
+  @Override
+  public OutputStream getOutputStream(PathFragment path, boolean append, boolean internal)
+      throws IOException {
+    File file = checkNotNull(getIoFile(path), "getIoFile() must not be null");
+
+    boolean profileOpen =
+        !internal && profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_OPEN);
+    boolean profileWrite =
+        !internal && profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_WRITE);
+
+    long startTime = profiler.nanoTimeMaybe();
+    try {
+      return profileWrite
+          ? new ProfiledFileOutputStream(file, append, path.getPathString())
+          : new FileOutputStream(file, append);
+    } catch (FileNotFoundException e) {
+      // FileOutputStream throws FileNotFoundException if opening fails for any reason, including
+      // permissions. Fix it up here.
+      if (e.getMessage().endsWith(ERR_PERMISSION_DENIED)) {
+        throw new FileAccessException(e.getMessage());
+      }
+      throw e;
+    } finally {
+      if (profileOpen) {
+        profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, path.getPathString());
+      }
+    }
+  }
+
+  @Override
+  public SeekableByteChannel createReadWriteByteChannel(PathFragment path) throws IOException {
+    java.nio.file.Path nioPath = checkNotNull(getNioPath(path), "getNioPath() must not be null");
+
+    boolean profileOpen = profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_OPEN);
+
+    long startTime = Profiler.instance().nanoTimeMaybe();
+    try {
+      // TODO: add profiling for read/write operations.
+      return Files.newByteChannel(nioPath, READ_WRITE_BYTE_CHANNEL_OPEN_OPTIONS);
+    } finally {
+      if (profileOpen) {
+        profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, path.toString());
+      }
+    }
+  }
+
+  /**
+   * A {@link FileInputStream} that adds profile traces around read operations.
+   *
+   * <p>Implementation note: this class extends {@link FileInputStream} instead of wrapping around
+   * it so that {@code instanceof FileInputStream} checks still work.
+   */
+  private static class ProfiledFileInputStream extends FileInputStream {
+    private final String name;
+
+    private ProfiledFileInputStream(File file, String name) throws IOException {
+      super(file);
+      this.name = name;
+    }
+
+    @Override
+    public int read() throws IOException {
+      long startTime = profiler.nanoTimeMaybe();
+      try {
+        return super.read();
+      } finally {
+        profiler.logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
+      }
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+      return read(b, 0, b.length);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+      long startTime = profiler.nanoTimeMaybe();
+      try {
+        return super.read(b, off, len);
+      } finally {
+        profiler.logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
+      }
+    }
+  }
+
+  /**
+   * A {@link FileOutputStream} that adds profile traces around write operations.
+   *
+   * <p>Implementation note: this class extends {@link FileOutputStream} instead of wrapping around
+   * it so that {@code instanceof FileOutputStream} checks still work.
+   */
+  private static class ProfiledFileOutputStream extends FileOutputStream {
+    private final String name;
+
+    private ProfiledFileOutputStream(File file, boolean append, String name) throws IOException {
+      super(file, append);
+      this.name = name;
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+      long startTime = profiler.nanoTimeMaybe();
+      try {
+        super.write(b);
+      } finally {
+        profiler.logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
+      }
+    }
+
+    @Override
+    public void write(byte[] b) throws IOException {
+      write(b, 0, b.length);
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+      long startTime = profiler.nanoTimeMaybe();
+      try {
+        super.write(b, off, len);
+      } finally {
+        profiler.logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
index 369cef1..6f4ceeb 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
@@ -699,24 +699,18 @@
   }
 
   /**
-   * Creates an InputStream accessing the file denoted by the path.
+   * Creates an {@link InputStream}.
    *
+   * @param path the path to open
    * @throws FileNotFoundException if the file does not exist
    * @throws IOException if there was an error opening the file for reading
    */
   public abstract InputStream getInputStream(PathFragment path) throws IOException;
 
   /**
-   * Returns a {@link SeekableByteChannel} for writing to a file at provided path.
+   * Creates an {@link OutputStream}.
    *
-   * <p>Truncates the target file, therefore it cannot be used to read already existing files.
-   */
-  public abstract SeekableByteChannel createReadWriteByteChannel(PathFragment path)
-      throws IOException;
-
-  /**
-   * Creates an OutputStream accessing the file denoted by path.
-   *
+   * @param path the path to open
    * @throws IOException if there was an error opening the file for writing
    */
   protected final OutputStream getOutputStream(PathFragment path) throws IOException {
@@ -724,9 +718,10 @@
   }
 
   /**
-   * Creates an OutputStream accessing the file denoted by path.
+   * Creates an {@link OutputStream}.
    *
-   * @param append whether to open the output stream in append mode
+   * @param path the path to open
+   * @param append whether to open the file in append mode
    * @throws IOException if there was an error opening the file for writing
    */
   protected final OutputStream getOutputStream(PathFragment path, boolean append)
@@ -735,16 +730,26 @@
   }
 
   /**
-   * Creates an OutputStream accessing the file denoted by path.
+   * Creates an {@link OutputStream}.
    *
-   * @param append whether to open the output stream in append mode
-   * @param internal whether the file is a Bazel internal file
+   * @param path the path to open
+   * @param append whether to open the file in append mode
+   * @param internal whether the file is an internal file whose I/O should not be profiled
    * @throws IOException if there was an error opening the file for writing
    */
   public abstract OutputStream getOutputStream(PathFragment path, boolean append, boolean internal)
       throws IOException;
 
   /**
+   * Creates a {@link SeekableByteChannel}, truncating the file if it already exists.
+   *
+   * @param path the path to open
+   * @throws IOException if there was an error opening the file for reading
+   */
+  public abstract SeekableByteChannel createReadWriteByteChannel(PathFragment path)
+      throws IOException;
+
+  /**
    * Renames the file denoted by "sourceNode" to the location "targetNode". See {@link
    * Path#renameTo} for specification.
    *
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
index 364238d..167d204 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
@@ -43,7 +43,7 @@
  * system call - they all are associated with the VFS_STAT task.
  */
 @ThreadSafe
-public class JavaIoFileSystem extends AbstractFileSystem {
+public class JavaIoFileSystem extends DiskBackedFileSystem {
   private static final LinkOption[] NO_LINK_OPTION = new LinkOption[0];
   // This isn't generally safe; we rely on the file system APIs not modifying the array.
   private static final LinkOption[] NOFOLLOW_LINKS_OPTION =
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java
index 8400ce1..9e1a850 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java
@@ -15,9 +15,10 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.channels.SeekableByteChannel;
 
-/** Functionally like a read-only {@link AbstractFileSystem}. */
-public abstract class ReadonlyFileSystem extends AbstractFileSystem {
+/** Functionally like a read-only {@link FileSystem}. */
+public abstract class ReadonlyFileSystem extends FileSystem {
   public ReadonlyFileSystem(DigestHashFunction hashFunction) {
     super(hashFunction);
   }
@@ -36,6 +37,11 @@
   }
 
   @Override
+  public SeekableByteChannel createReadWriteByteChannel(PathFragment path) throws IOException {
+    throw modificationException();
+  }
+
+  @Override
   public void setReadable(PathFragment path, boolean readable) throws IOException {
     throw modificationException();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
index 8e1cbcd..64e9851 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
@@ -22,11 +22,11 @@
 import com.google.devtools.build.lib.clock.Clock;
 import com.google.devtools.build.lib.clock.JavaClock;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
-import com.google.devtools.build.lib.vfs.AbstractFileSystem;
 import com.google.devtools.build.lib.vfs.DigestHashFunction;
 import com.google.devtools.build.lib.vfs.FileAccessException;
 import com.google.devtools.build.lib.vfs.FileStatus;
 import com.google.devtools.build.lib.vfs.FileSymlinkLoopException;
+import com.google.devtools.build.lib.vfs.FileSystem;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.SymlinkTargetType;
@@ -59,7 +59,7 @@
  * bits are considered for the purpose of determining whether a file is accessible.
  */
 @ThreadSafe
-public class InMemoryFileSystem extends AbstractFileSystem {
+public class InMemoryFileSystem extends FileSystem {
 
   protected final Clock clock;
 
diff --git a/src/test/java/com/google/devtools/build/lib/unix/BUILD b/src/test/java/com/google/devtools/build/lib/unix/BUILD
index afded47..f74c191 100644
--- a/src/test/java/com/google/devtools/build/lib/unix/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/unix/BUILD
@@ -27,6 +27,7 @@
         "no_windows",
     ],
     deps = [
+        "//src/main/java/com/google/devtools/build/lib/profiler",
         "//src/main/java/com/google/devtools/build/lib/unix",
         "//src/main/java/com/google/devtools/build/lib/unix:procmeminfo_parser",
         "//src/main/java/com/google/devtools/build/lib/util:os",
@@ -39,7 +40,9 @@
         "//src/test/java/com/google/devtools/build/lib/vfs/util",
         "//third_party:guava-testlib",
         "//third_party:junit4",
+        "//third_party:mockito",
         "//third_party:truth",
+        "@maven//:com_google_testparameterinjector_test_parameter_injector",
     ],
 )
 
diff --git a/src/test/java/com/google/devtools/build/lib/unix/UnixFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/unix/UnixFileSystemTest.java
index 8b35e2d..5b4dabd 100644
--- a/src/test/java/com/google/devtools/build/lib/unix/UnixFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/unix/UnixFileSystemTest.java
@@ -16,7 +16,13 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.profiler.TraceProfilerService;
 import com.google.devtools.build.lib.vfs.DigestHashFunction;
 import com.google.devtools.build.lib.vfs.Dirent;
 import com.google.devtools.build.lib.vfs.FileAccessException;
@@ -25,7 +31,12 @@
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.SymlinkAwareFileSystemTest;
 import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import org.junit.Test;
 
 /** Tests for the {@link com.google.devtools.build.lib.unix.UnixFileSystem} class. */
@@ -119,4 +130,42 @@
     assertThrows(FileAccessException.class, dir::getDirectoryEntries);
     assertThrows(FileAccessException.class, () -> dir.readdir(Symlinks.NOFOLLOW));
   }
+
+  @Test
+  public void testInputStreamIsFileInputStream(@TestParameter boolean profiling) throws Exception {
+    try (var m = new MaybeWithMockProfiler(profiling);
+        InputStream in = xFile.getInputStream()) {
+      assertThat(in).isInstanceOf(FileInputStream.class);
+    }
+  }
+
+  @Test
+  public void testOutputStreamIsFileOutputStream(@TestParameter boolean profiling)
+      throws Exception {
+    try (var m = new MaybeWithMockProfiler(profiling);
+        OutputStream out = xFile.getOutputStream()) {
+      assertThat(out).isInstanceOf(FileOutputStream.class);
+    }
+  }
+
+  private static class MaybeWithMockProfiler implements AutoCloseable {
+    private final boolean enabled;
+
+    MaybeWithMockProfiler(boolean enabled) {
+      if (enabled) {
+        TraceProfilerService mock = mock(TraceProfilerService.class);
+        when(mock.isActive()).thenReturn(true);
+        when(mock.isProfiling(any(ProfilerTask.class))).thenReturn(true);
+        Profiler.setTraceProfilerServiceForTesting(mock);
+      }
+      this.enabled = enabled;
+    }
+
+    @Override
+    public void close() {
+      if (enabled) {
+        Profiler.setTraceProfilerServiceForTesting(null);
+      }
+    }
+  }
 }