1
0

perf_linux.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. //go:build linux
  2. package cpupick
  3. import (
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. )
  10. // capacityKeepPct is the cpu_capacity admission threshold, relative to the
  11. // fastest allowed core. LITTLE cores are normalized to ~250-400 of the big
  12. // core's 1024 while mid cores sit at ~75%+, so half of max separates little
  13. // from the rest without splitting prime from mid on three-tier parts.
  14. const capacityKeepPct = 50
  15. // freqKeepPct is the cpuinfo_max_freq admission threshold. Favored-core
  16. // turbo skew is 2-4% and ARM mid-vs-prime ~12%, while E-cores, LITTLE
  17. // cores, and AMD compact cores all sit >= 20% below their siblings' max.
  18. const freqKeepPct = 85
  19. // perfCPUs partitions allowed into the subset that are "performance" cores,
  20. // consulting (in order of authority):
  21. //
  22. // 1. cpu_capacity — arch_topology's normalized per-CPU capacity, exposed on
  23. // arm/arm64/riscv; the scheduler's own view of big vs LITTLE.
  24. // 2. /sys/devices/cpu_core/cpus — the Intel hybrid P-core PMU mask, present
  25. // only on P/E parts (x86 has no cpu_capacity) and naming P cores outright.
  26. // 3. cpuinfo_max_freq — the cross-vendor fallback; catches AMD compact
  27. // cores, which neither of the above covers.
  28. //
  29. // Returns allowed unchanged (signal "") when nothing distinguishes the
  30. // cores: homogeneous parts, VMs without cpufreq, sysfs unavailable.
  31. func perfCPUs(allowed []int) ([]int, string) {
  32. return perfCPUsFrom("/sys/devices/system/cpu", "/sys/devices/cpu_core/cpus", allowed)
  33. }
  34. func perfCPUsFrom(cpuDir, intelCoreMask string, allowed []int) ([]int, string) {
  35. if cpus, ok := byPerCPUValue(cpuDir, "cpu_capacity", allowed, capacityKeepPct); ok {
  36. return cpus, "cpu_capacity"
  37. }
  38. if cpus, ok := byIntelCoreMask(intelCoreMask, allowed); ok {
  39. return cpus, "intel_core_pmu"
  40. }
  41. if cpus, ok := byPerCPUValue(cpuDir, "cpufreq/cpuinfo_max_freq", allowed, freqKeepPct); ok {
  42. return cpus, "max_freq"
  43. }
  44. return allowed, ""
  45. }
  46. // byPerCPUValue keeps the allowed CPUs whose per-CPU sysfs value is at least
  47. // keepPct percent of the maximum across allowed. Inconclusive (ok=false)
  48. // when any CPU is missing the file or when every value is equal.
  49. func byPerCPUValue(cpuDir, file string, allowed []int, keepPct int) ([]int, bool) {
  50. vals := make([]int, len(allowed))
  51. minV, maxV := 0, 0
  52. for i, cpu := range allowed {
  53. v, err := readIntFile(filepath.Join(cpuDir, fmt.Sprintf("cpu%d", cpu), file))
  54. if err != nil {
  55. return nil, false
  56. }
  57. vals[i] = v
  58. if i == 0 || v < minV {
  59. minV = v
  60. }
  61. if v > maxV {
  62. maxV = v
  63. }
  64. }
  65. if minV == maxV {
  66. return nil, false // homogeneous by this signal; try the next one
  67. }
  68. keep := make([]int, 0, len(allowed))
  69. for i, cpu := range allowed {
  70. if vals[i]*100 >= maxV*keepPct {
  71. keep = append(keep, cpu)
  72. }
  73. }
  74. return keep, true
  75. }
  76. // byIntelCoreMask keeps the allowed CPUs named by the hybrid P-core PMU
  77. // mask. Inconclusive when the file is absent (non-hybrid x86, other arches)
  78. // or no allowed CPU is in the mask (the process was deliberately confined
  79. // to E-cores; nothing useful to prefer within that).
  80. func byIntelCoreMask(maskPath string, allowed []int) ([]int, bool) {
  81. b, err := os.ReadFile(maskPath)
  82. if err != nil {
  83. return nil, false
  84. }
  85. set, err := parseCPUList(strings.TrimSpace(string(b)))
  86. if err != nil || len(set) == 0 {
  87. return nil, false
  88. }
  89. pcore := make(map[int]bool, len(set))
  90. for _, c := range set {
  91. pcore[c] = true
  92. }
  93. keep := make([]int, 0, len(allowed))
  94. for _, cpu := range allowed {
  95. if pcore[cpu] {
  96. keep = append(keep, cpu)
  97. }
  98. }
  99. if len(keep) == 0 {
  100. return nil, false
  101. }
  102. return keep, true
  103. }
  104. // parseCPUList decodes the kernel's cpulist format ("0-7,16-23", "3") into
  105. // individual CPU IDs. Empty input yields an empty list.
  106. func parseCPUList(s string) ([]int, error) {
  107. if s == "" {
  108. return nil, nil
  109. }
  110. var out []int
  111. for part := range strings.SplitSeq(s, ",") {
  112. part = strings.TrimSpace(part)
  113. if part == "" {
  114. continue
  115. }
  116. lo, hi, isRange := strings.Cut(part, "-")
  117. a, err := strconv.Atoi(lo)
  118. if err != nil {
  119. return nil, fmt.Errorf("bad cpulist entry %q: %w", part, err)
  120. }
  121. if !isRange {
  122. out = append(out, a)
  123. continue
  124. }
  125. b, err := strconv.Atoi(hi)
  126. if err != nil {
  127. return nil, fmt.Errorf("bad cpulist entry %q: %w", part, err)
  128. }
  129. if b < a || b-a > 8192 {
  130. return nil, fmt.Errorf("bad cpulist range %q", part)
  131. }
  132. for v := a; v <= b; v++ {
  133. out = append(out, v)
  134. }
  135. }
  136. return out, nil
  137. }
  138. func readIntFile(path string) (int, error) {
  139. b, err := os.ReadFile(path)
  140. if err != nil {
  141. return 0, err
  142. }
  143. return strconv.Atoi(strings.TrimSpace(string(b)))
  144. }