Introduce `InstrumentationOutput` and apply it to profile

`InstrumentationOutput` aims at providing a unified way to handle writing and publishing instrumentation outputs.

PiperOrigin-RevId: 663316138
Change-Id: Ie5d80acd1843a94045307442ed00ffd6a9f98105
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEvent.java
index fcb2f94..6c89b12 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEvent.java
@@ -13,46 +13,20 @@
 // limitations under the License.
 package com.google.devtools.build.lib.buildtool.buildevent;
 
-import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
-import com.google.devtools.build.lib.profiler.Profiler;
-import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.runtime.InstrumentationOutput;
 import javax.annotation.Nullable;
 
 /** This event is fired when the profiler is started. */
 public class ProfilerStartedEvent implements ExtendedEventHandler.Postable {
-  @Nullable private final Path profilePath;
-  @Nullable private final UploadContext streamingContext;
-  private final Profiler.Format format;
+  @Nullable private final InstrumentationOutput profile;
 
-  public ProfilerStartedEvent(
-      @Nullable Path profilePath,
-      @Nullable UploadContext streamingContext,
-      Profiler.Format format) {
-    this.profilePath = profilePath;
-    this.streamingContext = streamingContext;
-    this.format = format;
+  public ProfilerStartedEvent(@Nullable InstrumentationOutput profile) {
+    this.profile = profile;
   }
 
   @Nullable
-  public Path getProfilePath() {
-    return profilePath;
-  }
-
-  @Nullable
-  public UploadContext getStreamingContext() {
-    return streamingContext;
-  }
-
-  public String getName() {
-    switch (format) {
-      case JSON_TRACE_FILE_FORMAT -> {
-        return "command.profile.json";
-      }
-      case JSON_TRACE_FILE_COMPRESSED_FORMAT -> {
-        return "command.profile.gz";
-      }
-    }
-    throw new UnsupportedOperationException();
+  public InstrumentationOutput getProfile() {
+    return profile;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
index ec89a88..98af82e 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
@@ -43,9 +43,7 @@
 import com.google.devtools.build.lib.bugreport.BugReporter;
 import com.google.devtools.build.lib.bugreport.Crash;
 import com.google.devtools.build.lib.bugreport.CrashContext;
-import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
 import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
-import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
 import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
 import com.google.devtools.build.lib.buildtool.CommandPrecompleteEvent;
 import com.google.devtools.build.lib.buildtool.buildevent.ProfilerStartedEvent;
@@ -324,27 +322,38 @@
     ImmutableSet.Builder<ProfilerTask> profiledTasksBuilder = ImmutableSet.builder();
     Profiler.Format format = Format.JSON_TRACE_FILE_FORMAT;
     Path profilePath = null;
-    UploadContext streamingContext = null;
+    InstrumentationOutput profile = null;
     try {
       if (tracerEnabled) {
         if (options.profilePath == null) {
+          String profileName = "command.profile.gz";
           format = Format.JSON_TRACE_FILE_COMPRESSED_FORMAT;
           if (bepOptions != null && bepOptions.streamingLogFileUploads) {
             BuildEventArtifactUploader buildEventArtifactUploader =
                 newUploader(env, bepOptions.buildEventUploadStrategy);
-            streamingContext = buildEventArtifactUploader.startUpload(LocalFileType.LOG, null);
-            out = streamingContext.getOutputStream();
+            profile =
+                new BuildEventArtifactInstrumentationOutput(
+                    profileName, buildEventArtifactUploader);
+            out = profile.createOutputStream();
           } else {
-            profilePath = workspace.getOutputBase().getRelative("command.profile.gz");
-            out = profilePath.getOutputStream();
+            profilePath = workspace.getOutputBase().getRelative(profileName);
+            profile = new LocalInstrumentationOutput(profileName, profilePath);
+            out = profile.createOutputStream();
           }
         } else {
           format =
               options.profilePath.toString().endsWith(".gz")
                   ? Format.JSON_TRACE_FILE_COMPRESSED_FORMAT
                   : Format.JSON_TRACE_FILE_FORMAT;
+          String profileName =
+              (format == Format.JSON_TRACE_FILE_COMPRESSED_FORMAT)
+                  ? "command.profile.gz"
+                  : "command.profile.json";
           profilePath = workspace.getWorkspace().getRelative(options.profilePath);
-          out = profilePath.getOutputStream(/* append= */ false, /* internal= */ true);
+          profile = new LocalInstrumentationOutput(profileName, profilePath);
+          out =
+              ((LocalInstrumentationOutput) profile)
+                  .createOutputStream(/* append= */ false, /* internal= */ true);
         }
         for (ProfilerTask profilerTask : ProfilerTask.values()) {
           if (!profilerTask.isVfs()
@@ -445,7 +454,7 @@
     } catch (IOException e) {
       eventHandler.handle(Event.error("Error while creating profile file: " + e.getMessage()));
     }
-    return new ProfilerStartedEvent(profilePath, streamingContext, format);
+    return new ProfilerStartedEvent(profile);
   }
 
   public FileSystem getFileSystem() {
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutput.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutput.java
new file mode 100644
index 0000000..dad4993
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutput.java
@@ -0,0 +1,51 @@
+// Copyright 2024 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.runtime;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
+import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
+import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import java.io.OutputStream;
+import javax.annotation.Nullable;
+
+/**
+ * Used when instrumentation output should be treated as a build event artifact so that it will be
+ * uploaded to a specified location.
+ */
+final class BuildEventArtifactInstrumentationOutput implements InstrumentationOutput {
+  private final String name;
+  private final BuildEventArtifactUploader buildEventArtifactUploader;
+  @Nullable private UploadContext uploadContext;
+
+  public BuildEventArtifactInstrumentationOutput(
+      String name, BuildEventArtifactUploader buildEventArtifactUploader) {
+    this.name = checkNotNull(name);
+    this.buildEventArtifactUploader = checkNotNull(buildEventArtifactUploader);
+  }
+
+  @Override
+  public void publish(BuildToolLogCollection buildToolLogCollection) {
+    checkNotNull(uploadContext, "Cannot publish to buildToolLogCollection if upload never starts.");
+    buildToolLogCollection.addUriFuture(name, uploadContext.uriFuture());
+  }
+
+  @Override
+  public OutputStream createOutputStream() {
+    uploadContext = buildEventArtifactUploader.startUpload(LocalFileType.LOG, null);
+    return uploadContext.getOutputStream();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
index 70fe506..8a865f7 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
@@ -179,16 +179,13 @@
           }
         }
       }
-      if (profileEvent != null && profileEvent.getProfilePath() != null) {
+      if (profileEvent != null && profileEvent.getProfile() != null) {
         // This leads to missing the afterCommand profiles of the other modules in the profile.
         // Since the BEP currently shuts down at the BuildCompleteEvent, we cannot just move posting
         // the BuildToolLogs to afterCommand of this module.
         try {
           Profiler.instance().stop();
-          event
-              .getResult()
-              .getBuildToolLogCollection()
-              .addLocalFile(profileEvent.getName(), profileEvent.getProfilePath());
+          profileEvent.getProfile().publish(event.getResult().getBuildToolLogCollection());
         } catch (IOException e) {
           reporter.handle(Event.error("Error while writing profile file: " + e.getMessage()));
         }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/InstrumentationOutput.java b/src/main/java/com/google/devtools/build/lib/runtime/InstrumentationOutput.java
new file mode 100644
index 0000000..f5130e6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/InstrumentationOutput.java
@@ -0,0 +1,28 @@
+// Copyright 2024 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.runtime;
+
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Stores and publishes the instrumentation output information. */
+public interface InstrumentationOutput {
+
+  /** Creates the {@link OutputStream} for instrumentation output writes. */
+  OutputStream createOutputStream() throws IOException;
+
+  /** Publishes instrumentation output information to the {@link BuildToolLogCollection}. */
+  void publish(BuildToolLogCollection buildToolLogCollection);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutput.java b/src/main/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutput.java
new file mode 100644
index 0000000..aedb96a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutput.java
@@ -0,0 +1,44 @@
+// Copyright 2024 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.runtime;
+
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Used when instrumentation output is written to a local file. */
+final class LocalInstrumentationOutput implements InstrumentationOutput {
+  private final Path path;
+  private final String name;
+
+  LocalInstrumentationOutput(String name, Path path) {
+    this.name = name;
+    this.path = path;
+  }
+
+  @Override
+  public void publish(BuildToolLogCollection buildToolLogCollection) {
+    buildToolLogCollection.addLocalFile(name, path);
+  }
+
+  @Override
+  public OutputStream createOutputStream() throws IOException {
+    return path.getOutputStream();
+  }
+
+  public OutputStream createOutputStream(boolean append, boolean internal) throws IOException {
+    return path.getOutputStream(append, internal);
+  }
+}