|
| 1 | +defmodule Circuits.GPIO.Diagnostics.CPU do |
| 2 | + @moduledoc """ |
| 3 | + CPU |
| 4 | + """ |
| 5 | + |
| 6 | + @doc """ |
| 7 | + Force the CPU to its slowest setting |
| 8 | +
|
| 9 | + This requires the Linux kernel to have the powersave CPU scaling governor available. |
| 10 | + """ |
| 11 | + @spec force_slowest() :: :ok |
| 12 | + def force_slowest() do |
| 13 | + cpu_list() |
| 14 | + |> Enum.each(&set_governor(&1, "powersave")) |
| 15 | + end |
| 16 | + |
| 17 | + @doc """ |
| 18 | + Force the CPU to its fastest setting |
| 19 | +
|
| 20 | + This requires the Linux kernel to have the performance CPU scaling governor available. |
| 21 | + """ |
| 22 | + @spec force_fastest() :: :ok |
| 23 | + def force_fastest() do |
| 24 | + cpu_list() |
| 25 | + |> Enum.each(&set_governor(&1, "performance")) |
| 26 | + end |
| 27 | + |
| 28 | + @doc """ |
| 29 | + Set the CPU to the specified frequency |
| 30 | +
|
| 31 | + This requires the Linux kernel to have the userspace CPU scaling governor available. |
| 32 | + Not all frequencies are supported. The closest will be picked. |
| 33 | + """ |
| 34 | + @spec set_frequency(number()) :: :ok |
| 35 | + def set_frequency(frequency_mhz) do |
| 36 | + cpus = cpu_list() |
| 37 | + Enum.each(cpus, &set_governor(&1, "userspace")) |
| 38 | + Enum.each(cpus, &set_frequency(&1, frequency_mhz)) |
| 39 | + end |
| 40 | + |
| 41 | + defp set_governor(cpu, governor) do |
| 42 | + File.write!("/sys/bus/cpu/devices/#{cpu}/cpufreq/scaling_governor", governor) |
| 43 | + end |
| 44 | + |
| 45 | + defp set_frequency(cpu, frequency_mhz) do |
| 46 | + frequency_khz = round(frequency_mhz * 1000) |
| 47 | + File.write!("/sys/bus/cpu/devices/#{cpu}/cpufreq/scaling_setspeed", to_string(frequency_khz)) |
| 48 | + end |
| 49 | + |
| 50 | + @doc """ |
| 51 | + Return the string names for all CPUs |
| 52 | +
|
| 53 | + CPUs are named `"cpu0"`, `"cpu1"`, etc. |
| 54 | + """ |
| 55 | + @spec cpu_list() :: [String.t()] |
| 56 | + def cpu_list() do |
| 57 | + case File.ls("/sys/bus/cpu/devices") do |
| 58 | + {:ok, list} -> Enum.sort(list) |
| 59 | + _ -> [] |
| 60 | + end |
| 61 | + end |
| 62 | + |
| 63 | + @doc """ |
| 64 | + Check benchmark suitability and return CPU information |
| 65 | + """ |
| 66 | + @spec check_benchmark_suitability() :: %{ |
| 67 | + uname: String.t(), |
| 68 | + cpu_count: non_neg_integer(), |
| 69 | + speed_mhz: number(), |
| 70 | + warnings?: boolean() |
| 71 | + } |
| 72 | + def check_benchmark_suitability() do |
| 73 | + cpus = cpu_list() |
| 74 | + |
| 75 | + scheduler_warnings? = Enum.all?(cpus, &check_cpu_scheduler/1) |
| 76 | + {frequency_warnings?, mhz} = mean_cpu_frequency(cpus) |
| 77 | + |
| 78 | + %{ |
| 79 | + uname: uname(), |
| 80 | + cpu_count: length(cpus), |
| 81 | + speed_mhz: mhz, |
| 82 | + warnings?: scheduler_warnings? or frequency_warnings? |
| 83 | + } |
| 84 | + end |
| 85 | + |
| 86 | + defp uname() do |
| 87 | + case File.read("/proc/version") do |
| 88 | + {:ok, s} -> String.trim(s) |
| 89 | + {:error, _} -> "Unknown" |
| 90 | + end |
| 91 | + end |
| 92 | + |
| 93 | + defp check_cpu_scheduler(cpu) do |
| 94 | + case File.read("/sys/bus/cpu/devices/#{cpu}/cpufreq/scaling_governor") do |
| 95 | + {:error, _} -> |
| 96 | + io_warn("Could not check CPU frequency scaling for #{cpu}") |
| 97 | + true |
| 98 | + |
| 99 | + {:ok, text} -> |
| 100 | + governor = String.trim(text) |
| 101 | + |
| 102 | + if governor in ["powersave", "performance", "userspace"] do |
| 103 | + false |
| 104 | + else |
| 105 | + io_warn( |
| 106 | + "CPU #{cpu} is using a dynamic CPU frequency governor. Performance results may vary." |
| 107 | + ) |
| 108 | + |
| 109 | + true |
| 110 | + end |
| 111 | + end |
| 112 | + end |
| 113 | + |
| 114 | + defp cpu_frequency_mhz(cpu) do |
| 115 | + # Report the actual CPU frequency just in case something is throttling the governor (e.g., thermal throttling). |
| 116 | + # The governor's target frequency is in the "scaling_cur_freq" file. |
| 117 | + case File.read("/sys/bus/cpu/devices/#{cpu}/cpufreq/cpuinfo_cur_freq") do |
| 118 | + {:ok, string} -> string |> String.trim() |> String.to_integer() |> Kernel./(1000) |
| 119 | + {:error, _} -> 0.0 |
| 120 | + end |
| 121 | + end |
| 122 | + |
| 123 | + defp mean_cpu_frequency(cpu_list) do |
| 124 | + speeds = cpu_list |> Enum.map(&cpu_frequency_mhz/1) |
| 125 | + |
| 126 | + case speeds do |
| 127 | + [] -> |
| 128 | + {true, 0.0} |
| 129 | + |
| 130 | + [speed] -> |
| 131 | + {false, speed} |
| 132 | + |
| 133 | + [first | _rest] -> |
| 134 | + mean = Enum.sum(speeds) / length(speeds) |
| 135 | + |
| 136 | + if abs(mean - first) < 0.001 do |
| 137 | + {false, mean} |
| 138 | + else |
| 139 | + io_warn("CPU speeds don't all match: #{inspect(speeds)}") |
| 140 | + {true, mean} |
| 141 | + end |
| 142 | + end |
| 143 | + end |
| 144 | + |
| 145 | + defp io_warn(text) do |
| 146 | + [:yellow, "WARNING: ", text, :reset] |
| 147 | + |> IO.ANSI.format() |
| 148 | + |> IO.puts() |
| 149 | + end |
| 150 | +end |
0 commit comments