1
0

cpupick_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. package cpupick
  2. import (
  3. "slices"
  4. "testing"
  5. )
  6. // pairTopo builds a topology where consecutive candidate pairs are SMT
  7. // siblings: (cpus[0],cpus[1]) share a core, (cpus[2],cpus[3]) the next, ...
  8. // All CPUs land on node 0.
  9. func pairTopo(cpus []int) topology {
  10. t := topology{
  11. nodeOf: make(map[int]int, len(cpus)),
  12. coreOf: make(map[int]int, len(cpus)),
  13. zeroCore: -1,
  14. }
  15. for i, c := range cpus {
  16. t.nodeOf[c] = 0
  17. t.coreOf[c] = i / 2
  18. if c == 0 {
  19. t.zeroCore = i / 2
  20. }
  21. }
  22. return t
  23. }
  24. func TestArrangeDemotesZeroForEveryKey(t *testing.T) {
  25. candidates := []int{0, 1, 2, 3, 4, 5, 6, 7}
  26. for key := range uint64(64) {
  27. got := arrange(candidates, flatTopology(candidates), 4, splitmix64(key))
  28. if len(got) != len(candidates) {
  29. t.Fatalf("key %d: len=%d want %d", key, len(got), len(candidates))
  30. }
  31. if got[0] == 0 {
  32. t.Errorf("key %d: CPU 0 at the front: %v", key, got)
  33. }
  34. if got[len(got)-1] != 0 {
  35. t.Errorf("key %d: CPU 0 not demoted to last: %v", key, got)
  36. }
  37. sorted := slices.Clone(got)
  38. slices.Sort(sorted)
  39. if !slices.Equal(sorted, candidates) {
  40. t.Errorf("key %d: not a permutation: %v", key, got)
  41. }
  42. }
  43. }
  44. func TestArrangeDemotesZeroSiblings(t *testing.T) {
  45. // Pairs (0,1),(2,3),(4,5),(6,7): CPU 0's core — 0 and its sibling 1 —
  46. // must tail the list, sibling ahead of 0 itself.
  47. candidates := []int{0, 1, 2, 3, 4, 5, 6, 7}
  48. for key := range uint64(64) {
  49. got := arrange(candidates, pairTopo(candidates), 2, splitmix64(key))
  50. n := len(got)
  51. if got[n-1] != 0 || got[n-2] != 1 {
  52. t.Fatalf("key %d: tail = %v, want [... 1 0]", key, got)
  53. }
  54. }
  55. }
  56. func TestArrangeZeroSiblingWithoutZero(t *testing.T) {
  57. // CPU 0 excluded (cpuset) but its sibling 1 remains: the sibling still
  58. // tails the list when the topology knows which core CPU 0 lives on.
  59. candidates := []int{1, 2, 3, 4, 5}
  60. topo := pairTopo([]int{0, 1, 2, 3, 4, 5})
  61. got := arrange(candidates, topo, 2, splitmix64(7))
  62. if got[len(got)-1] != 1 {
  63. t.Errorf("CPU 0's sibling not demoted: %v", got)
  64. }
  65. }
  66. func TestArrangeRotatesByKey(t *testing.T) {
  67. candidates := []int{1, 2, 3, 4, 5, 6, 7, 8}
  68. seen := map[int]bool{}
  69. for key := range uint64(64) {
  70. seen[arrange(candidates, flatTopology(candidates), 4, splitmix64(key))[0]] = true
  71. }
  72. // 64 hashed keys over 8 slots must hit more than one starting CPU, or
  73. // co-located instances would all stack again.
  74. if len(seen) < 2 {
  75. t.Errorf("rotation never varied across keys: %v", seen)
  76. }
  77. }
  78. func TestArrangeStableForSameKey(t *testing.T) {
  79. candidates := []int{0, 2, 4, 6}
  80. topo := flatTopology(candidates)
  81. a := arrange(candidates, topo, 2, splitmix64(4242))
  82. b := arrange(candidates, topo, 2, splitmix64(4242))
  83. if !slices.Equal(a, b) {
  84. t.Errorf("same key ordered differently: %v vs %v", a, b)
  85. }
  86. }
  87. func TestArrangeZeroOnly(t *testing.T) {
  88. if got := arrange([]int{0}, flatTopology([]int{0}), 1, splitmix64(7)); !slices.Equal(got, []int{0}) {
  89. t.Errorf("sole CPU 0 must survive: %v", got)
  90. }
  91. }
  92. func TestArrangeSMTSiblingsLast(t *testing.T) {
  93. // Pairs (1,2),(3,4),(5,6),(7,8): the first four picks must cover four
  94. // distinct physical cores before any sibling repeats.
  95. candidates := []int{1, 2, 3, 4, 5, 6, 7, 8}
  96. topo := pairTopo(candidates)
  97. for key := range uint64(16) {
  98. got := arrange(candidates, topo, 4, splitmix64(key))
  99. seen := map[int]bool{}
  100. for _, c := range got[:4] {
  101. g := topo.coreOf[c]
  102. if seen[g] {
  103. t.Fatalf("key %d: sibling before all cores covered: %v", key, got)
  104. }
  105. seen[g] = true
  106. }
  107. }
  108. }
  109. func TestArrangeNUMAConfinesToOneNode(t *testing.T) {
  110. // Two nodes of four; both fit routines=3, so the result must sit
  111. // entirely inside one of them, and the hash must pick both across keys.
  112. candidates := []int{1, 2, 3, 4, 10, 11, 12, 13}
  113. topo := flatTopology(candidates)
  114. for _, c := range []int{10, 11, 12, 13} {
  115. topo.nodeOf[c] = 1
  116. }
  117. nodesSeen := map[int]bool{}
  118. for key := range uint64(32) {
  119. got := arrange(candidates, topo, 3, splitmix64(key))
  120. if len(got) != 4 {
  121. t.Fatalf("key %d: not confined to one node: %v", key, got)
  122. }
  123. n := topo.nodeOf[got[0]]
  124. for _, c := range got {
  125. if topo.nodeOf[c] != n {
  126. t.Fatalf("key %d: spans nodes: %v", key, got)
  127. }
  128. }
  129. nodesSeen[n] = true
  130. }
  131. if len(nodesSeen) != 2 {
  132. t.Errorf("hash never spread instances across nodes: %v", nodesSeen)
  133. }
  134. }
  135. func TestArrangeNUMASpansWhenNoNodeFits(t *testing.T) {
  136. candidates := []int{1, 2, 3, 4, 10, 11, 12, 13}
  137. topo := flatTopology(candidates)
  138. for _, c := range []int{10, 11, 12, 13} {
  139. topo.nodeOf[c] = 1
  140. }
  141. got := arrange(candidates, topo, 6, splitmix64(1))
  142. if len(got) != len(candidates) {
  143. t.Errorf("undersized nodes must span, got %v", got)
  144. }
  145. }
  146. func TestPickCandidates(t *testing.T) {
  147. allowed := []int{0, 1, 2, 3, 4, 5, 6, 7}
  148. perf := []int{4, 5}
  149. // Enough perf cores for every routine: only they are used.
  150. if got := pickCandidates(allowed, perf, 2); !slices.Equal(got, perf) {
  151. t.Errorf("perf filter not applied: %v", got)
  152. }
  153. // Perf filter too small for the routine count: discarded, everyone
  154. // gets their own core from the full allowed set.
  155. if got := pickCandidates(allowed, perf, 4); !slices.Equal(got, allowed) {
  156. t.Errorf("undersized perf filter not discarded: %v", got)
  157. }
  158. }