1
0

affinity_linux.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. //go:build linux
  2. package cpu
  3. import (
  4. "os"
  5. "runtime"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "golang.org/x/sys/unix"
  10. )
  11. var (
  12. affinityEnabled bool
  13. affinitySet unix.CPUSet
  14. )
  15. func init() {
  16. // Optional: enable via env to keep default behavior unchanged.
  17. //
  18. // - MAKARNA_CPUSET: e.g. "0-15,32-47"
  19. // - MAKARNA_NUMA_NODE: e.g. "0" (reads /sys/devices/system/node/node0/cpulist)
  20. if err := initAffinityFromEnv(); err != nil {
  21. affinityEnabled = false
  22. }
  23. }
  24. func initAffinityFromEnv() error {
  25. if cpus, ok := cpusFromEnv("MAKARNA_CPUSET"); ok {
  26. return setAffinityCPUs(cpus)
  27. }
  28. if nodeStr := strings.TrimSpace(os.Getenv("MAKARNA_NUMA_NODE")); nodeStr != "" {
  29. node, err := strconv.Atoi(nodeStr)
  30. if err != nil || node < 0 {
  31. return err
  32. }
  33. data, err := os.ReadFile("/sys/devices/system/node/node" + strconv.Itoa(node) + "/cpulist")
  34. if err != nil {
  35. return err
  36. }
  37. cpus, err := parseCPUList(strings.TrimSpace(string(data)))
  38. if err != nil {
  39. return err
  40. }
  41. return setAffinityCPUs(cpus)
  42. }
  43. return nil
  44. }
  45. func cpusFromEnv(key string) ([]int, bool) {
  46. raw := strings.TrimSpace(os.Getenv(key))
  47. if raw == "" {
  48. return nil, false
  49. }
  50. cpus, err := parseCPUList(raw)
  51. if err != nil {
  52. return nil, false
  53. }
  54. return cpus, true
  55. }
  56. func parseCPUList(s string) ([]int, error) {
  57. if s == "" {
  58. return nil, strconv.ErrSyntax
  59. }
  60. var cpus []int
  61. for _, part := range strings.Split(s, ",") {
  62. part = strings.TrimSpace(part)
  63. if part == "" {
  64. continue
  65. }
  66. if lo, hi, ok := strings.Cut(part, "-"); ok {
  67. start, err := strconv.Atoi(strings.TrimSpace(lo))
  68. if err != nil {
  69. return nil, err
  70. }
  71. end, err := strconv.Atoi(strings.TrimSpace(hi))
  72. if err != nil {
  73. return nil, err
  74. }
  75. if start < 0 || end < start {
  76. return nil, strconv.ErrSyntax
  77. }
  78. for c := start; c <= end; c++ {
  79. cpus = append(cpus, c)
  80. }
  81. continue
  82. }
  83. v, err := strconv.Atoi(part)
  84. if err != nil {
  85. return nil, err
  86. }
  87. if v < 0 {
  88. return nil, strconv.ErrSyntax
  89. }
  90. cpus = append(cpus, v)
  91. }
  92. if len(cpus) == 0 {
  93. return nil, strconv.ErrSyntax
  94. }
  95. sort.Ints(cpus)
  96. cpus = compactInts(cpus)
  97. return cpus, nil
  98. }
  99. func compactInts(xs []int) []int {
  100. if len(xs) < 2 {
  101. return xs
  102. }
  103. out := xs[:1]
  104. for _, v := range xs[1:] {
  105. if v == out[len(out)-1] {
  106. continue
  107. }
  108. out = append(out, v)
  109. }
  110. return out
  111. }
  112. func setAffinityCPUs(cpus []int) error {
  113. var set unix.CPUSet
  114. set.Zero()
  115. for _, c := range cpus {
  116. set.Set(c)
  117. }
  118. affinitySet = set
  119. affinityEnabled = true
  120. return nil
  121. }
  122. // WithPinnedThread runs fn on a locked OS thread with optional CPU affinity set
  123. // (enabled via env). If affinity is not configured, it behaves like fn().
  124. func WithPinnedThread(fn func()) {
  125. if !affinityEnabled {
  126. fn()
  127. return
  128. }
  129. runtime.LockOSThread()
  130. defer runtime.UnlockOSThread()
  131. _ = unix.SchedSetaffinity(0, &affinitySet)
  132. fn()
  133. }