From 5cc28b0424d23b87b92355ce172f389e05e1c5ac Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Tue, 29 Sep 2026 00:07:37 -0300 Subject: [PATCH 1/2] mutate: every test process held to its share of the cores A test binary took every core, and each of its t.Parallel tests too: four control-plane edits asked at once were nineteen processes running and a load of 24 on a 20-core machine, on a fraction of its CPU. GOMAXPROCS is the cores over -j, for coverage, a schema binary and an edit built alone. Co-Authored-By: Claude Opus 5.5 (1M context) --- cmd/mutate/main.go | 1 + internal/mutate/cover.go | 1 + internal/mutate/mutate.go | 1 + internal/mutate/schema.go | 2 +- internal/mutate/share.go | 29 +++++++++++++ internal/mutate/share_test.go | 82 +++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 internal/mutate/share.go create mode 100644 internal/mutate/share_test.go diff --git a/cmd/mutate/main.go b/cmd/mutate/main.go index 8025567..511b9a8 100644 --- a/cmd/mutate/main.go +++ b/cmd/mutate/main.go @@ -83,6 +83,7 @@ func main() { os.Exit(2) } + mutate.Share(*jobs) started := time.Now() t := runAll(chosen, importers, *timeout, *jobs) reportCosts(time.Since(started)) diff --git a/internal/mutate/cover.go b/internal/mutate/cover.go index baa4e8f..44c4288 100644 --- a/internal/mutate/cover.go +++ b/internal/mutate/cover.go @@ -110,6 +110,7 @@ func Cover(dir, timeout string, slots chan struct{}) (*Coverage, error) { run := exec.Command(bin, "-test.run", "^"+test+"$", "-test.count=1", //nolint:gosec // the binary this built "-test.timeout", timeout, "-test.coverprofile", profile) run.Dir = dir + run.Env = testEnv() // A failure here is the package's own tests' to report, not this gate's; what this // gate does with it is ask the test about no edit, since it fails whatever the edit - // and say so, since an edit only it reaches then reads as one nothing holds. diff --git a/internal/mutate/mutate.go b/internal/mutate/mutate.go index fe3f1f0..c055e6a 100644 --- a/internal/mutate/mutate.go +++ b/internal/mutate/mutate.go @@ -719,6 +719,7 @@ func Run(m Mutation, a Asked) (Outcome, string) { } cmd := exec.Command("go", args...) //nolint:gosec // go test over this repository's packages cmd.Dir = set.dir + cmd.Env = testEnv() out, err := combined(set.what, cmd) said.Write(out) if err == nil { diff --git a/internal/mutate/schema.go b/internal/mutate/schema.go index 8053d59..c26ce9a 100644 --- a/internal/mutate/schema.go +++ b/internal/mutate/schema.go @@ -378,7 +378,7 @@ func Ask(bin, dir string, id int, timeout string, only []string) (Outcome, strin } cmd := exec.Command(bin, args...) //nolint:gosec // a test binary this run built cmd.Dir = dir - cmd.Env = append(os.Environ(), switchEnv+"="+strconv.Itoa(id)) + cmd.Env = testEnv(switchEnv + "=" + strconv.Itoa(id)) out, err := combined("schema: run one edit", cmd) if err != nil { return Killed, string(out) diff --git a/internal/mutate/share.go b/internal/mutate/share.go new file mode 100644 index 0000000..1750928 --- /dev/null +++ b/internal/mutate/share.go @@ -0,0 +1,29 @@ +package mutate + +import ( + "os" + "runtime" + "strconv" +) + +// testProcs is the GOMAXPROCS of every test process a run starts; 0 leaves each the machine's. +// A test binary takes every core by default, and so does each of its t.Parallel tests: four +// mutations of the control plane's app package asked at once were nineteen processes running and +// a load of 24 on a 20-core machine, though they used a fraction of its CPU. +var testProcs int + +// Share divides the machine among jobs test processes run at once, each held to its share of the +// cores. Call it once, before anything is asked. +func Share(jobs int) { + testProcs = max(runtime.NumCPU()/max(jobs, 1), 1) +} + +// testEnv is the environment a test process runs in: this one's, its share of the machine, and +// extra. +func testEnv(extra ...string) []string { + env := os.Environ() + if testProcs > 0 { + env = append(env, "GOMAXPROCS="+strconv.Itoa(testProcs)) + } + return append(env, extra...) +} diff --git a/internal/mutate/share_test.go b/internal/mutate/share_test.go new file mode 100644 index 0000000..d1f8731 --- /dev/null +++ b/internal/mutate/share_test.go @@ -0,0 +1,82 @@ +package mutate + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "slices" + "testing" +) + +// oneProc is a module whose one test passes only in a process held to one core, which is what +// Share(more jobs than cores) gives every test process. +func oneProc(t *testing.T) string { + t.Helper() + dir := t.TempDir() + for name, body := range map[string]string{ + "go.mod": "module example.com/oneproc\n\ngo 1.24\n", + "code.go": "package oneproc\n\nfunc One() int {\n\treturn 1\n}\n", + "code_test.go": "package oneproc\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n)\n\n" + + "func TestHeldToOneCore(t *testing.T) {\n\tif runtime.GOMAXPROCS(0) != 1 || One() != 1 {\n\t\tt.Fatal(\"not held to one core\")\n\t}\n}\n", + } { + if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil { + t.Fatal(err) + } + } + return dir +} + +// share is Share for one test, the machine's again after it. +func share(t *testing.T, jobs int) { + t.Helper() + Share(jobs) + t.Cleanup(func() { testProcs = 0 }) +} + +// The machine is divided among the jobs that run at once, and a process never gets less than a +// core. +func TestEachJobIsHeldToItsShareOfTheCores(t *testing.T) { + share(t, 2) + if want := max(runtime.NumCPU()/2, 1); testProcs != want { + t.Errorf("two jobs are held to %d core(s) each, want %d", testProcs, want) + } + share(t, 1<<20) + env := testEnv("A=b") + if !slices.Contains(env, "GOMAXPROCS=1") || env[len(env)-1] != "A=b" { + t.Errorf("more jobs than cores: the environment ends %q", env[max(len(env)-3, 0):]) + } + testProcs = 0 + if slices.Contains(testEnv(), "GOMAXPROCS=1") && os.Getenv("GOMAXPROCS") != "1" { + t.Error("a run that shares nothing held its tests to one core") + } +} + +// Every way a test process is started holds it to its share: measuring coverage, asking a schema +// binary, and building an edit alone. +func TestEveryTestProcessIsHeldToItsShare(t *testing.T) { + dir := oneProc(t) + share(t, 1<<20) + + c, err := Cover(dir, "1m", make(chan struct{}, 1)) + if err != nil { + t.Fatal(err) + } + if got := c.Reaching(filepath.Join(dir, "code.go"), 4); !slices.Equal(got, []string{"TestHeldToOneCore"}) { + t.Errorf("coverage ran its test on every core: the line is reached by %q", got) + } + + bin := filepath.Join(t.TempDir(), "pkg.test") + build := exec.Command("go", "test", "-c", "-o", bin, ".") + build.Dir = dir + if out, err := build.CombinedOutput(); err != nil { + t.Fatalf("%v: %s", err, out) + } + if outcome, out := Ask(bin, dir, 0, "1m", nil); outcome != Survived { + t.Errorf("a schema binary was asked on every core: %s", out) + } + + if outcome, out := Run(Mutation{File: filepath.Join(dir, "code.go")}, Asked{Timeout: "1m"}); outcome != Survived { + t.Errorf("an edit built alone was asked on every core: %s", out) + } +} From 65ac460e9ea2323c3ea48b3fa4b2415804eafe88 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Tue, 29 Sep 2026 00:23:30 -0300 Subject: [PATCH 2/2] mutate: one job, or none, is the whole machine Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/mutate/share_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/mutate/share_test.go b/internal/mutate/share_test.go index d1f8731..d633f1f 100644 --- a/internal/mutate/share_test.go +++ b/internal/mutate/share_test.go @@ -41,6 +41,13 @@ func TestEachJobIsHeldToItsShareOfTheCores(t *testing.T) { if want := max(runtime.NumCPU()/2, 1); testProcs != want { t.Errorf("two jobs are held to %d core(s) each, want %d", testProcs, want) } + // One job, or a -j of nothing, is the whole machine. + for _, jobs := range []int{1, 0} { + share(t, jobs) + if testProcs != runtime.NumCPU() { + t.Errorf("-j %d holds its job to %d core(s), want all %d", jobs, testProcs, runtime.NumCPU()) + } + } share(t, 1<<20) env := testEnv("A=b") if !slices.Contains(env, "GOMAXPROCS=1") || env[len(env)-1] != "A=b" {