| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- package cpupick
- import (
- "slices"
- "testing"
- )
- // pairTopo builds a topology where consecutive candidate pairs are SMT
- // siblings: (cpus[0],cpus[1]) share a core, (cpus[2],cpus[3]) the next, ...
- // All CPUs land on node 0.
- func pairTopo(cpus []int) topology {
- t := topology{
- nodeOf: make(map[int]int, len(cpus)),
- coreOf: make(map[int]int, len(cpus)),
- zeroCore: -1,
- }
- for i, c := range cpus {
- t.nodeOf[c] = 0
- t.coreOf[c] = i / 2
- if c == 0 {
- t.zeroCore = i / 2
- }
- }
- return t
- }
- func TestArrangeDemotesZeroForEveryKey(t *testing.T) {
- candidates := []int{0, 1, 2, 3, 4, 5, 6, 7}
- for key := range uint64(64) {
- got := arrange(candidates, flatTopology(candidates), 4, splitmix64(key))
- if len(got) != len(candidates) {
- t.Fatalf("key %d: len=%d want %d", key, len(got), len(candidates))
- }
- if got[0] == 0 {
- t.Errorf("key %d: CPU 0 at the front: %v", key, got)
- }
- if got[len(got)-1] != 0 {
- t.Errorf("key %d: CPU 0 not demoted to last: %v", key, got)
- }
- sorted := slices.Clone(got)
- slices.Sort(sorted)
- if !slices.Equal(sorted, candidates) {
- t.Errorf("key %d: not a permutation: %v", key, got)
- }
- }
- }
- func TestArrangeDemotesZeroSiblings(t *testing.T) {
- // Pairs (0,1),(2,3),(4,5),(6,7): CPU 0's core — 0 and its sibling 1 —
- // must tail the list, sibling ahead of 0 itself.
- candidates := []int{0, 1, 2, 3, 4, 5, 6, 7}
- for key := range uint64(64) {
- got := arrange(candidates, pairTopo(candidates), 2, splitmix64(key))
- n := len(got)
- if got[n-1] != 0 || got[n-2] != 1 {
- t.Fatalf("key %d: tail = %v, want [... 1 0]", key, got)
- }
- }
- }
- func TestArrangeZeroSiblingWithoutZero(t *testing.T) {
- // CPU 0 excluded (cpuset) but its sibling 1 remains: the sibling still
- // tails the list when the topology knows which core CPU 0 lives on.
- candidates := []int{1, 2, 3, 4, 5}
- topo := pairTopo([]int{0, 1, 2, 3, 4, 5})
- got := arrange(candidates, topo, 2, splitmix64(7))
- if got[len(got)-1] != 1 {
- t.Errorf("CPU 0's sibling not demoted: %v", got)
- }
- }
- func TestArrangeRotatesByKey(t *testing.T) {
- candidates := []int{1, 2, 3, 4, 5, 6, 7, 8}
- seen := map[int]bool{}
- for key := range uint64(64) {
- seen[arrange(candidates, flatTopology(candidates), 4, splitmix64(key))[0]] = true
- }
- // 64 hashed keys over 8 slots must hit more than one starting CPU, or
- // co-located instances would all stack again.
- if len(seen) < 2 {
- t.Errorf("rotation never varied across keys: %v", seen)
- }
- }
- func TestArrangeStableForSameKey(t *testing.T) {
- candidates := []int{0, 2, 4, 6}
- topo := flatTopology(candidates)
- a := arrange(candidates, topo, 2, splitmix64(4242))
- b := arrange(candidates, topo, 2, splitmix64(4242))
- if !slices.Equal(a, b) {
- t.Errorf("same key ordered differently: %v vs %v", a, b)
- }
- }
- func TestArrangeZeroOnly(t *testing.T) {
- if got := arrange([]int{0}, flatTopology([]int{0}), 1, splitmix64(7)); !slices.Equal(got, []int{0}) {
- t.Errorf("sole CPU 0 must survive: %v", got)
- }
- }
- func TestArrangeSMTSiblingsLast(t *testing.T) {
- // Pairs (1,2),(3,4),(5,6),(7,8): the first four picks must cover four
- // distinct physical cores before any sibling repeats.
- candidates := []int{1, 2, 3, 4, 5, 6, 7, 8}
- topo := pairTopo(candidates)
- for key := range uint64(16) {
- got := arrange(candidates, topo, 4, splitmix64(key))
- seen := map[int]bool{}
- for _, c := range got[:4] {
- g := topo.coreOf[c]
- if seen[g] {
- t.Fatalf("key %d: sibling before all cores covered: %v", key, got)
- }
- seen[g] = true
- }
- }
- }
- func TestArrangeNUMAConfinesToOneNode(t *testing.T) {
- // Two nodes of four; both fit routines=3, so the result must sit
- // entirely inside one of them, and the hash must pick both across keys.
- candidates := []int{1, 2, 3, 4, 10, 11, 12, 13}
- topo := flatTopology(candidates)
- for _, c := range []int{10, 11, 12, 13} {
- topo.nodeOf[c] = 1
- }
- nodesSeen := map[int]bool{}
- for key := range uint64(32) {
- got := arrange(candidates, topo, 3, splitmix64(key))
- if len(got) != 4 {
- t.Fatalf("key %d: not confined to one node: %v", key, got)
- }
- n := topo.nodeOf[got[0]]
- for _, c := range got {
- if topo.nodeOf[c] != n {
- t.Fatalf("key %d: spans nodes: %v", key, got)
- }
- }
- nodesSeen[n] = true
- }
- if len(nodesSeen) != 2 {
- t.Errorf("hash never spread instances across nodes: %v", nodesSeen)
- }
- }
- func TestArrangeNUMASpansWhenNoNodeFits(t *testing.T) {
- candidates := []int{1, 2, 3, 4, 10, 11, 12, 13}
- topo := flatTopology(candidates)
- for _, c := range []int{10, 11, 12, 13} {
- topo.nodeOf[c] = 1
- }
- got := arrange(candidates, topo, 6, splitmix64(1))
- if len(got) != len(candidates) {
- t.Errorf("undersized nodes must span, got %v", got)
- }
- }
- func TestPickCandidates(t *testing.T) {
- allowed := []int{0, 1, 2, 3, 4, 5, 6, 7}
- perf := []int{4, 5}
- // Enough perf cores for every routine: only they are used.
- if got := pickCandidates(allowed, perf, 2); !slices.Equal(got, perf) {
- t.Errorf("perf filter not applied: %v", got)
- }
- // Perf filter too small for the routine count: discarded, everyone
- // gets their own core from the full allowed set.
- if got := pickCandidates(allowed, perf, 4); !slices.Equal(got, allowed) {
- t.Errorf("undersized perf filter not discarded: %v", got)
- }
- }
|