bazel: add --starlark_cpu_profile=<file> flag

And an integration test.

RELNOTES:
The --starlark_cpu_profile=<file> flag writes a profile in
pprof format containing a statistical summary of CPU usage
by all Starlark execution during the bazel command. Use it
to identify slow Starlark functions in loading and analysis.
PiperOrigin-RevId: 293692329
diff --git a/site/docs/user-manual.html b/site/docs/user-manual.html
index aadaca8..3b0da8e 100644
--- a/site/docs/user-manual.html
+++ b/site/docs/user-manual.html
@@ -1642,6 +1642,38 @@
     incremental builds slower.
 </p>
 
+<h4 id='flag--starlark_cpu_profile'><code class='flag'>--starlark_cpu_profile=<i>file</i></code></h4>
+<p>
+  This flag, whose value is the name of a file, causes Bazel to gather
+  statistics about CPU usage by all Starlark threads,
+  and write the profile, in <a href='https://github.com/google/pprof'>pprof</a> format,
+  to the named file.
+
+  Use this option to help identify Starlark functions that
+  make loading and analysis slow due to excessive computation. For example:
+</p>
+<pre>
+$ bazel build --nobuild --starlark_cpu_profile=/tmp/pprof.gz my/project/...
+$ pprof /tmp/pprof.gz
+(pprof) top
+Type: CPU
+Time: Feb 6, 2020 at 12:06pm (PST)
+Duration: 5.26s, Total samples = 3.34s (63.55%)
+Showing nodes accounting for 3.34s, 100% of 3.34s total
+      flat  flat%   sum%        cum   cum%
+     1.86s 55.69% 55.69%      1.86s 55.69%  sort_source_files
+     1.02s 30.54% 86.23%      1.02s 30.54%  expand_all_combinations
+     0.44s 13.17% 99.40%      0.44s 13.17%  range
+     0.02s   0.6%   100%      3.34s   100%  sorted
+         0     0%   100%      1.38s 41.32%  my/project/main/BUILD
+         0     0%   100%      1.96s 58.68%  my/project/library.bzl
+         0     0%   100%      3.34s   100%  main
+</pre>
+<p>
+  For different views of the same data, try the <code>pprof</code> commands <code>svg</code>,
+  <code>web</code>, and <code>list</code>.
+</p>
+
 <h2 id='bazel-releng'>Using Bazel for releases</h2>
 <p>
   Bazel is used both by software engineers during the development
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
index 4e41757..2f13057 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
@@ -40,6 +40,7 @@
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.SilentCloseable;
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
+import com.google.devtools.build.lib.syntax.Starlark;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.AnsiStrippingOutputStream;
 import com.google.devtools.build.lib.util.ExitCode;
@@ -53,8 +54,10 @@
 import com.google.devtools.common.options.OptionsParser;
 import com.google.devtools.common.options.OptionsParsingResult;
 import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -326,6 +329,23 @@
       storedEventHandler.post(profilerStartedEvent);
     }
 
+    // Enable Starlark CPU profiling (--starlark_cpu_profile=/tmp/foo.pprof.gz)
+    if (!commonOptions.starlarkCpuProfile.isEmpty()) {
+      FileOutputStream out;
+      try {
+        out = new FileOutputStream(commonOptions.starlarkCpuProfile);
+      } catch (IOException ex) {
+        storedEventHandler.handle(Event.error("Starlark CPU profiler: " + ex.getMessage()));
+        return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
+      }
+      try {
+        Starlark.startCpuProfile(out, Duration.ofMillis(10));
+      } catch (IllegalStateException ex) { // e.g. SIGPROF in use
+        storedEventHandler.handle(Event.error(ex.getMessage()));
+        return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
+      }
+    }
+
     BlazeCommandResult result = BlazeCommandResult.exitCode(ExitCode.BLAZE_INTERNAL_ERROR);
     boolean afterCommandCalled = false;
     Reporter reporter = env.getReporter();
@@ -540,13 +560,27 @@
       }
       options = optionHandler.getOptionsResult();
 
+      // Run the command.
       result = command.exec(env, options);
+
       ExitCode moduleExitCode = env.precompleteCommand(result.getExitCode());
       // If Blaze did not suffer an infrastructure failure, check for errors in modules.
       if (!result.getExitCode().isInfrastructureFailure() && moduleExitCode != null) {
         result = BlazeCommandResult.exitCode(moduleExitCode);
       }
 
+      // Finalize the Starlark CPU profile.
+      if (!commonOptions.starlarkCpuProfile.isEmpty()) {
+        try {
+          Starlark.stopCpuProfile();
+        } catch (IOException ex) {
+          reporter.handle(Event.error("Starlark CPU profiler: " + ex.getMessage()));
+          if (result.getExitCode().equals(ExitCode.SUCCESS)) { // don't clobber existing error
+            result = BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
+          }
+        }
+      }
+
       afterCommandCalled = true;
       return runtime.afterCommand(env, result);
     } catch (Throwable e) {
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommonCommandOptions.java b/src/main/java/com/google/devtools/build/lib/runtime/CommonCommandOptions.java
index 5fb95f7..0659fe5 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/CommonCommandOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommonCommandOptions.java
@@ -332,6 +332,14 @@
   public PathFragment profilePath;
 
   @Option(
+      name = "starlark_cpu_profile",
+      defaultValue = "",
+      documentationCategory = OptionDocumentationCategory.LOGGING,
+      effectTags = {OptionEffectTag.BAZEL_MONITORING},
+      help = "Writes into the specified file a pprof profile of CPU usage by all Starlark threads.")
+  public String starlarkCpuProfile;
+
+  @Option(
       name = "record_full_profiler_data",
       defaultValue = "false",
       documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,