// Copyright 2025 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package runtime_test import ( "fmt" "internal/cgrouptest" "runtime" "strings" "syscall" "testing" "unsafe" ) func mustHaveFourCPUs(t *testing.T) { // If NumCPU is lower than the cgroup limit, GOMAXPROCS will use // NumCPU. // // cgroup GOMAXPROCS also have a minimum of 2. We need some room above // that to test interesting properies. if runtime.NumCPU() < 4 { t.Helper() t.Skip("skipping test: fewer than 4 CPUs") } } func TestCgroupGOMAXPROCS(t *testing.T) { mustHaveFourCPUs(t) exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } tests := []struct { godebug int want int }{ // With containermaxprocs=1, GOMAXPROCS should use the cgroup // limit. { godebug: 1, want: 3, }, // With containermaxprocs=0, it should be ignored. { godebug: 0, want: runtime.NumCPU(), }, } for _, tc := range tests { t.Run(fmt.Sprintf("containermaxprocs=%d", tc.godebug), func(t *testing.T) { cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { if err := c.SetCPUMax(300000, 100000); err != nil { t.Fatalf("unable to set CPU limit: %v", err) } got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS", fmt.Sprintf("GODEBUG=containermaxprocs=%d", tc.godebug)) want := fmt.Sprintf("%d\n", tc.want) if got != want { t.Fatalf("output got %q want %q", got, want) } }) }) } } // Without a cgroup limit, GOMAXPROCS uses NumCPU. func TestCgroupGOMAXPROCSNoLimit(t *testing.T) { exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { if err := c.SetCPUMax(-1, 100000); err != nil { t.Fatalf("unable to set CPU limit: %v", err) } got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS") want := fmt.Sprintf("%d\n", runtime.NumCPU()) if got != want { t.Fatalf("output got %q want %q", got, want) } }) } // If the cgroup limit is higher than NumCPU, GOMAXPROCS uses NumCPU. func TestCgroupGOMAXPROCSHigherThanNumCPU(t *testing.T) { exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { if err := c.SetCPUMax(2*int64(runtime.NumCPU())*100000, 100000); err != nil { t.Fatalf("unable to set CPU limit: %v", err) } got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS") want := fmt.Sprintf("%d\n", runtime.NumCPU()) if got != want { t.Fatalf("output got %q want %q", got, want) } }) } func TestCgroupGOMAXPROCSRound(t *testing.T) { mustHaveFourCPUs(t) exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } tests := []struct { quota int64 want int }{ // We always round the fractional component up. { quota: 200001, want: 3, }, { quota: 250000, want: 3, }, { quota: 299999, want: 3, }, // Anything less than two rounds up to a minimum of 2. { quota: 50000, // 0.5 want: 2, }, { quota: 100000, want: 2, }, { quota: 150000, want: 2, }, } for _, tc := range tests { t.Run(fmt.Sprintf("%d", tc.quota), func(t *testing.T) { cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { if err := c.SetCPUMax(tc.quota, 100000); err != nil { t.Fatalf("unable to set CPU limit: %v", err) } got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS") want := fmt.Sprintf("%d\n", tc.want) if got != want { t.Fatalf("output got %q want %q", got, want) } }) }) } } // Environment variable takes precedence over defaults. func TestCgroupGOMAXPROCSEnvironment(t *testing.T) { mustHaveFourCPUs(t) exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { if err := c.SetCPUMax(200000, 100000); err != nil { t.Fatalf("unable to set CPU limit: %v", err) } got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS", "GOMAXPROCS=3") want := "3\n" if got != want { t.Fatalf("output got %q want %q", got, want) } }) } // CPU affinity takes priority if lower than cgroup limit. func TestCgroupGOMAXPROCSSchedAffinity(t *testing.T) { exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { if err := c.SetCPUMax(300000, 100000); err != nil { t.Fatalf("unable to set CPU limit: %v", err) } // CPU affinity is actually a per-thread attribute. runtime.LockOSThread() defer runtime.UnlockOSThread() const maxCPUs = 64 * 1024 var orig [maxCPUs / 8]byte _, _, errno := syscall.Syscall6(syscall.SYS_SCHED_GETAFFINITY, 0, unsafe.Sizeof(orig), uintptr(unsafe.Pointer(&orig[0])), 0, 0, 0) if errno != 0 { t.Fatalf("unable to get CPU affinity: %v", errno) } // We're going to restrict to CPUs 0 and 1. Make sure those are already available. if orig[0]&0b11 != 0b11 { t.Skipf("skipping test: CPUs 0 and 1 not available") } var mask [maxCPUs / 8]byte mask[0] = 0b11 _, _, errno = syscall.Syscall6(syscall.SYS_SCHED_SETAFFINITY, 0, unsafe.Sizeof(mask), uintptr(unsafe.Pointer(&mask[0])), 0, 0, 0) if errno != 0 { t.Fatalf("unable to set CPU affinity: %v", errno) } defer func() { _, _, errno = syscall.Syscall6(syscall.SYS_SCHED_SETAFFINITY, 0, unsafe.Sizeof(orig), uintptr(unsafe.Pointer(&orig[0])), 0, 0, 0) if errno != 0 { t.Fatalf("unable to restore CPU affinity: %v", errno) } }() got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS") want := "2\n" if got != want { t.Fatalf("output got %q want %q", got, want) } }) } func TestCgroupGOMAXPROCSSetDefault(t *testing.T) { mustHaveFourCPUs(t) exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } tests := []struct { godebug int want int }{ // With containermaxprocs=1, SetDefaultGOMAXPROCS should observe // the cgroup limit. { godebug: 1, want: 3, }, // With containermaxprocs=0, it should be ignored. { godebug: 0, want: runtime.NumCPU(), }, } for _, tc := range tests { t.Run(fmt.Sprintf("containermaxprocs=%d", tc.godebug), func(t *testing.T) { cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { env := []string{ fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()), "GO_TEST_CPU_MAX_QUOTA=300000", fmt.Sprintf("GODEBUG=containermaxprocs=%d", tc.godebug), } got := runBuiltTestProg(t, exe, "SetLimitThenDefaultGOMAXPROCS", env...) want := fmt.Sprintf("%d\n", tc.want) if got != want { t.Fatalf("output got %q want %q", got, want) } }) }) } } func TestCgroupGOMAXPROCSUpdate(t *testing.T) { mustHaveFourCPUs(t) if testing.Short() { t.Skip("skipping test: long sleeps") } exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { got := runBuiltTestProg(t, exe, "UpdateGOMAXPROCS", fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath())) if !strings.Contains(got, "OK") { t.Fatalf("output got %q want OK", got) } }) } func TestCgroupGOMAXPROCSDontUpdate(t *testing.T) { mustHaveFourCPUs(t) if testing.Short() { t.Skip("skipping test: long sleeps") } exe, err := buildTestProg(t, "testprog") if err != nil { t.Fatal(err) } // Two ways to disable updates: explicit GOMAXPROCS or GODEBUG for // update feature. for _, v := range []string{"GOMAXPROCS=4", "GODEBUG=updatemaxprocs=0"} { t.Run(v, func(t *testing.T) { cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { got := runBuiltTestProg(t, exe, "DontUpdateGOMAXPROCS", fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()), v) if !strings.Contains(got, "OK") { t.Fatalf("output got %q want OK", got) } }) }) } }