Skip to content

Commit 5effaf8

Browse files
committed
Add CPU and kernel checks to performance tests
Dynamic CPU frequency scaling and Linux kernel differences cause performance numbers to be quite different so include this in the report. This also provides warnings when dynamic CPU frequency governors are in use or when the CPU frequency is different between cores.
1 parent 04541ca commit 5effaf8

File tree

2 files changed

+186
-9
lines changed

2 files changed

+186
-9
lines changed

lib/gpio/diagnostics.ex

+36-9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule Circuits.GPIO.Diagnostics do
1212
to work on some devices.
1313
"""
1414
alias Circuits.GPIO
15+
alias Circuits.GPIO.Diagnostics.CPU
1516

1617
@doc """
1718
Reminder for how to use report/2
@@ -55,14 +56,23 @@ defmodule Circuits.GPIO.Diagnostics do
5556
Input ids: #{inspect(in_identifiers)}
5657
Backend: #{inspect(Circuits.GPIO.backend_info()[:name])}
5758
59+
== Functionality ==
60+
5861
""",
5962
Enum.map(results, &pass_text/1),
6063
"""
6164
62-
write/2: #{round(speed_results.write_cps)} calls/s
63-
read/1: #{round(speed_results.read_cps)} calls/s
64-
write_one/3: #{round(speed_results.write_one_cps)} calls/s
65-
read_one/2: #{round(speed_results.read_one_cps)} calls/s
65+
== Performance ==
66+
67+
Kernel: #{speed_results.uname}
68+
CPU count: #{speed_results.cpu_count}
69+
CPU speed: #{speed_results.speed_mhz} MHz
70+
Warnings?: #{speed_results.warnings?}
71+
72+
write/2: #{cps_to_us(speed_results.write_cps)} µs/call
73+
read/1: #{cps_to_us(speed_results.read_cps)} µs/call
74+
write_one/3: #{cps_to_us(speed_results.write_one_cps)} µs/call
75+
read_one/2: #{cps_to_us(speed_results.read_one_cps)} µs/call
6676
6777
""",
6878
if(check_connections?,
@@ -81,6 +91,9 @@ defmodule Circuits.GPIO.Diagnostics do
8191
passed
8292
end
8393

94+
# Truncate sub-nanosecond for readability
95+
defp cps_to_us(cps), do: round(1_000_000_000 / cps) / 1000
96+
8497
defp pass_text({name, :ok}), do: [name, ": ", :green, "PASSED", :reset, "\n"]
8598

8699
defp pass_text({name, {:error, reason}}),
@@ -108,17 +121,29 @@ defmodule Circuits.GPIO.Diagnostics do
108121
@doc """
109122
Run GPIO API performance tests
110123
111-
Disclaimer: There should be a better way than relying on the Circuits.GPIO
112-
write performance on nearly every device. Write performance shouldn't be
113-
terrible, though.
124+
If you get warnings about the CPU speed, run
125+
`Circuits.GPIO.Diagnostics.CPU.force_slowest/0` or
126+
`Circuits.GPIO.Diagnostics.CPU.set_speed/1` to make sure that the CPU doesn't
127+
change speeds during the test.
128+
129+
Disclaimer: This tests Circuits.GPIO write performance. Write performance
130+
should be reasonably good. However, if it's not acceptable, please
131+
investigate other options. Usually there's some hardware-assisted way to
132+
accomplish high speed GPIO tasks (PWM controllers, for example).
114133
"""
115134
@spec speed_test(GPIO.gpio_spec()) :: %{
116135
write_cps: float(),
117136
read_cps: float(),
118137
write_one_cps: float(),
119-
read_one_cps: float()
138+
read_one_cps: float(),
139+
uname: String.t(),
140+
cpu_count: non_neg_integer(),
141+
speed_mhz: number(),
142+
warnings?: boolean()
120143
}
121144
def speed_test(gpio_spec) do
145+
cpu_info = CPU.check_benchmark_suitability()
146+
122147
times = 1000
123148
one_times = ceil(times / 100)
124149

@@ -133,12 +158,14 @@ defmodule Circuits.GPIO.Diagnostics do
133158
write_one_cps = time_fun2(one_times, &write_one2/1, gpio_spec)
134159
read_one_cps = time_fun2(one_times, &read_one2/1, gpio_spec)
135160

136-
%{
161+
results = %{
137162
write_cps: write_cps,
138163
read_cps: read_cps,
139164
write_one_cps: write_one_cps,
140165
read_one_cps: read_one_cps
141166
}
167+
168+
Map.merge(results, cpu_info)
142169
end
143170

144171
defp time_fun2(times, fun, arg) do

lib/gpio/diagnostics/cpu.ex

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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

Comments
 (0)