| // 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) |
| } |
| }) |
| }) |
| } |
| } |