Reserve CPUs for GoStdlib and hold it to them

A single GoStdlib action runs "go install", which compiles the standard
library with one compiler per core. Bazel estimates one CPU for it, so it
schedules as many GoStdlib actions at once as the machine has cores, each
of which then tries to use the whole machine.

Declare a resource_set of four CPUs and pin GOMAXPROCS to the same number
so the reservation is accurate rather than an estimate Bazel cannot
enforce.

Measured on a 16 core machine building 8 stdlib configurations, best of
two interleaved passes:

    reservation             wall    peak RSS
    none (Bazel default)   26.4s     2085 MB
    2 CPUs                 28.7s     1343 MB
    4 CPUs                 27.3s      984 MB
    8 CPUs                 32.8s      647 MB
    16 CPUs                50.2s      678 MB

Four keeps the wall time of the default while halving peak memory. The
default is close on this machine but scales the wrong way: it puts no
bound on how many stdlib builds run at once, so both the concurrency and
the memory grow with the core count of the executor.

This changes the GoStdlib action key, so standard libraries rebuild once.
diff --git a/go/private/actions/stdlib.bzl b/go/private/actions/stdlib.bzl
index 38a704a..40b9fe5 100644
--- a/go/private/actions/stdlib.bzl
+++ b/go/private/actions/stdlib.bzl
@@ -91,6 +91,20 @@
     )
     return out, cache_dir
 
+# GOMAXPROCS below pins the action to this, so the reservation is what the
+# action actually uses rather than an estimate. Kept well under the core count
+# on purpose: compiling the standard library stops getting faster at around six
+# cores, and letting several GoStdlib actions run with a slice each finishes a
+# batch sooner than giving one action the whole machine.
+_STDLIB_CPUS = 4
+
+# Around 250MB per action at that many threads, plus headroom for the extra a
+# compiler holds on to when it is given a PGO profile.
+_STDLIB_MEMORY_MB = 512
+
+def _stdlib_resource_set(_os, _inputs_size):
+    return {"cpu": _STDLIB_CPUS, "memory": _STDLIB_MEMORY_MB}
+
 def _stdlib_execution_requirements(go):
     # Non-pure stdlib actions run cgo and pass C toolchain paths through
     # CGO_CFLAGS/CGO_LDFLAGS. Bazel does not path-map environment values.
@@ -170,6 +184,12 @@
         args.add("-pgoprofile", go.mode.pgoprofile)
         inputs_direct.append(go.mode.pgoprofile)
 
+    env = dict(_build_env(go))
+
+    # "go install" defaults to one compiler per core; hold it to what was
+    # reserved for the action.
+    env["GOMAXPROCS"] = str(_STDLIB_CPUS)
+
     outputs = [pkg]
     go.actions.run(
         inputs = depset(direct = inputs_direct, transitive = inputs_transitive),
@@ -177,9 +197,10 @@
         mnemonic = "GoStdlib",
         executable = go.toolchain._builder,
         arguments = [args],
-        env = _build_env(go),
+        env = env,
         toolchain = GO_TOOLCHAIN_LABEL,
         execution_requirements = _stdlib_execution_requirements(go),
+        resource_set = _stdlib_resource_set,
     )
     list_json, cache_dir = _build_stdlib_list_json(go)
     return GoStdLib(