1#!/usr/bin/env python3

2# Copyright 2019 Canonical Ltd.

3# Written by:

4# Maciej Kisielewski <maciej.kisielewski@canonical.com>

5#

6# This is free software: you can redistribute it and/or modify

7# it under the terms of the GNU General Public License version 3,

8# as published by the Free Software Foundation.

9#

10# This file is distributed in the hope that it will be useful,

11# but WITHOUT ANY WARRANTY; without even the implied warranty of

12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

13# GNU General Public License for more details.

14#

15# You should have received a copy of the GNU General Public License

16# along with this file. If not, see <http://www.gnu.org/licenses/>.

17"""

18 This program checks if the system's fans react to the CPU load applied.

19"""

20

21 import glob

22 import hashlib

23 import multiprocessing

24 import os

25 import random

26 import time

27

28

29 class FanMonitor:

30 """Device that reports fan RPM or something correlating to that."""

31

32 def __init__(self):

33 """Use heuristics to find something that we can read."""

34 self.hwmons = []

35 self._fan_paths = glob.glob("/sys/class/hwmon/hwmon*/fan*_input")

36 # All entries (except name) under /sys/class/hwmon/hwmon/* are optional

37 # and should only be created in a given driver if the chip has

38 # the feature.

39 # Use fan*_input is because the "thinkpad_hwmon" driver is report

40 # fan_input only. If there is any driver has different implementation

41 # then may need to check other entries in the future.

42 for i in self._fan_paths:

43 device = os.path.join(os.path.dirname(i), "device")

44 device_path = os.path.realpath(device)

45 # Get the class of pci device of hwmon whether is GPU.

46 if "pci" in device_path:

47 pci_class_path = os.path.join(device, "class")

48 try:

49 with open(pci_class_path, "r") as _file:

50 pci_class = _file.read().splitlines()

51 pci_device_class = (

52 int(pci_class[0], base=16) >> 16

53 ) & 0xFF

54 """Make sure the fan is not on graphic card"""

55 if pci_device_class == 3: 55 ↛ 60 line 55 didn't jump to line 60

56 continue

57 except OSError:

58 print("Not able to access {}".format(pci_class_path))

59 continue

60 self.hwmons.append(i)

61 if not self.hwmons:

62 print("Fan monitoring interface not found in SysFS")

63 raise SystemExit(0)

64

65 def get_rpm(self):

66 result = {}

67 for p in self.hwmons:

68 try:

69 with open(p, "rt") as f:

70 fan_mon_name = os.path.relpath(p, "/sys/class/hwmon")

71 result[fan_mon_name] = int(f.read())

72 except OSError:

73 print("Fan SysFS node disappeared ({})".format(p))

74 return result

75

76 def get_average_rpm(self, period):

77 acc = self.get_rpm()

78 for i in range(period):

79 time.sleep(1)

80 rpms = self.get_rpm()

81 for k, v in acc.items():

82 acc[k] += rpms[k]

83 for k, v in acc.items():

84 acc[k] /= period + 1

85 return acc

86

87

88 class Stressor:

89 def __init__(self, thread_count=None):

90 """Prepare the stressor."""

91 if thread_count is None:

92 thread_count = multiprocessing.cpu_count()

93 print("Found {} CPU(s) in the system".format(thread_count))

94 print("Will use #{} thread(s)".format(thread_count))

95 self._thread_count = thread_count

96 self._procs = []

97

98 def start(self):

99 """Start stress processes in the background."""

100 for n in range(self._thread_count):

101 self._procs.append(

102 multiprocessing.Process(target=self._stress_fun)

103 )

104 self._procs[-1].start()

105

106 def stop(self):

107 """Stop all running stress processes."""

108 for proc in self._procs:

109 proc.terminate()

110 proc.join()

111 self._procs = []

112

113 def _stress_fun(self):

114 """Actual stress function."""

115 # generate some random data

116 data = bytes(random.getrandbits(8) for _ in range(1024))

117 hasher = hashlib.sha256()

118 hasher.update(data)

119 while True:

120 new_digest = hasher.digest()

121 # use the newly obtained digest as the new data to the hasher

122 hasher.update(new_digest)

123

124

125 def main():

126 """Entry point."""

127

128 fan_mon = FanMonitor()

129 stressor = Stressor()

130

131 print("Precooling for 10s - no stress load")

132 time.sleep(10)

133 print("Measuring baseline fan speed")

134 baseline_rpm = fan_mon.get_average_rpm(5)

135 print("Launching stressor for 120s")

136 stressor.start()

137 for cycle in range(120):

138 print("Cycle #{}, RPM={}".format(cycle, fan_mon.get_rpm()))

139 time.sleep(1)

140 print("Measuring an average fan speed over 5s")

141 stress_rpm = fan_mon.get_average_rpm(5)

142 print("Stopping stressor, waiting for 60s for system to cool off")

143 stressor.stop()

144 for cycle in range(60):

145 print("Cycle #{}, RPM={}".format(cycle, fan_mon.get_rpm()))

146 time.sleep(1)

147 print("Measuring an average fan speed over 5s")

148 end_rpm = fan_mon.get_average_rpm(5)

149

150 had_a_fan_spinning = any((rpm > 0 for rpm in stress_rpm.values()))

151 rpm_rose_during_stress = False

152 rpm_dropped_during_cooling = False

153 for fan_mon in sorted(baseline_rpm.keys()):

154 if baseline_rpm[fan_mon]:

155 stress_delta = stress_rpm[fan_mon] / baseline_rpm[fan_mon]

156 else:

157 stress_delta = 999 if stress_rpm[fan_mon] > 0 else 0

158 # if any of the fans raised rpms - similar to any()

159 if not rpm_rose_during_stress:

160 # XXX: this checks only if the rpms rose, not by how much

161 # we may want to introduce a threshold later on

162 rpm_rose_during_stress = stress_delta > 0

163 if not rpm_dropped_during_cooling:

164 rpm_dropped_during_cooling = end_rpm[fan_mon] < stress_rpm[fan_mon]

165

166 print("{} RPM:".format(fan_mon))

167 print(" baseline : {:.2f}".format(baseline_rpm[fan_mon]))

168 print(

169 " during stress : {:.2f} ({:.2f}% of baseline)".format(

170 stress_rpm[fan_mon], stress_delta * 100

171 )

172 )

173 print(" after stress : {:.2f}".format(end_rpm[fan_mon]))

174 if not had_a_fan_spinning:

175 print("The system had no fans spinning during the test")

176 return 0

177 if rpm_rose_during_stress:

178 print("RPM rose during the stress.")

179 else:

180 print("RPM did not rise during the stress!")

181 if rpm_dropped_during_cooling:

182 print("RPM dropped after the stress.")

183 else:

184 print("RPM did not drop after the stress!")

185 # inverse logic, returning True would mean return code of 1

186 if not (rpm_rose_during_stress and rpm_dropped_during_cooling):

187 raise SystemExit("Fans did not react to stress expectedly")

188

189

190 if __name__ == "__main__":

191 main()