371 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			371 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
| # Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
 | |
| # Use of this source code is governed by a BSD-style license that can be
 | |
| # found in the LICENSE file.
 | |
| 
 | |
| import glob, logging, os, tempfile, threading, time
 | |
| from autotest_lib.client.bin import test
 | |
| from autotest_lib.client.common_lib import error, utils
 | |
| 
 | |
| class PlatformDescriptor(object):
 | |
|     '''
 | |
|     An object to keep platform specific information.
 | |
| 
 | |
|     @num_cores - number of CPU cores in this platform
 | |
|     @max_cpu_freq - maximum frequency the CPU can be running at
 | |
|     @min_cpu_freq - minimal frequency the CPU can be running at
 | |
|     '''
 | |
| 
 | |
|     def __init__(self, num_cores, max_cpu_freq, min_cpu_freq):
 | |
|         self.num_cores = num_cores
 | |
|         self.max_cpu_freq = max_cpu_freq
 | |
|         self.min_cpu_freq = min_cpu_freq
 | |
| 
 | |
| 
 | |
| # Base name of the sysfs file where CPU temperature is reported. The file is
 | |
| # exported by the temperature monitor driver and is located in the appropriate
 | |
| # device's subtree. We use the file name to locate the subtree, only one file
 | |
| # with this name is expected to exist in /sys. The ext_ prefix indicates that
 | |
| # this is a reading off a sensor located next to the CPU. This facility could
 | |
| # be not available on some platforms, the test would need to be updated to
 | |
| # accommodate those.
 | |
| #
 | |
| # The `standard' temperature reading available through
 | |
| # /sys/class/hwmon/hwmon0/device/temperature does not represent the actual CPU
 | |
| # temperature and when the CPU load changes, the 'standard' temperature
 | |
| # reading changes much slower and not to such a large extent than the value in
 | |
| # */ext_temperature.
 | |
| EXT_TEMP_SENSOR_FILE = 'ext_temperature'
 | |
| 
 | |
| # Base name of the file where the throttling temperature is set (if CPU temp
 | |
| # exceeds this value, clock throttling starts).
 | |
| THROTTLE_EXT_LIMIT_FILE = 'throttle_ext_limit'
 | |
| 
 | |
| # Root directory for all sysfs information about the CPU(s).
 | |
| CPU_INFO_ROOT = '/sys/devices/system/cpu'
 | |
| 
 | |
| # Template to get access to the directory/file containing current per core
 | |
| # information.
 | |
| PER_CORE_FREQ_TEMPLATE = CPU_INFO_ROOT + '/cpu%d/cpufreq/%s'
 | |
| 
 | |
| # Base name for the temporary files used by this test.
 | |
| TMP_FILE_TEMPLATE = '/tmp/thermal_'
 | |
| 
 | |
| # Temperature difference expected to be caused by increased CPU activity.
 | |
| DELTA = 3.0
 | |
| 
 | |
| # Name of the file controlling core's clocking discipline.
 | |
| GOVERNOR = 'scaling_governor'
 | |
| 
 | |
| # Name of the file providing space separated list of available clocking
 | |
| # disciplines.
 | |
| AVAILABLE_GOVERNORS = 'scaling_available_governors'
 | |
| 
 | |
| def clean_up(obj):
 | |
|     '''
 | |
|     A function to register with the autotest engine to ensure proper cleanup.
 | |
| 
 | |
|     It will be called after the test has run, either completing successfully
 | |
|     or throwing an exception.
 | |
|     '''
 | |
| 
 | |
|     obj.cleanup()
 | |
| 
 | |
| 
 | |
| class power_Thermal(test.test):
 | |
|     version = 1
 | |
| 
 | |
| 
 | |
|     def _cpu_heater(self):
 | |
|         '''
 | |
|         A function to execute some code to heat up the target.
 | |
| 
 | |
|         This function is run on a separate thread, all it does - opens a file
 | |
|         for writing, writes it with 100K characters, closes and removes the
 | |
|         file, it is running in a tight loop until the stop_all_workers flag
 | |
|         turns True.
 | |
| 
 | |
|         Multiple threads are spawn to cause maximum CPU activity.
 | |
|         '''
 | |
| 
 | |
|         (handle, fname) = tempfile.mkstemp(
 | |
|             prefix=os.path.basename(TMP_FILE_TEMPLATE),
 | |
|             dir=os.path.dirname(TMP_FILE_TEMPLATE))
 | |
|         os.close(handle)
 | |
|         os.remove(fname)
 | |
|         while not self.stop_all_workers:
 | |
|             f = open(fname, 'w')
 | |
|             f.write('x' * 100000)
 | |
|             f.close()
 | |
|             os.remove(fname)
 | |
| 
 | |
| 
 | |
|     def _add_heater_thread(self):
 | |
|         '''Add a thread to run another instance of _cpu_heater().'''
 | |
| 
 | |
|         thread_count = len(self.worker_threads)
 | |
|         logging.info('adding thread number %d' % thread_count)
 | |
|         new_thread = threading.Thread(target=self._cpu_heater)
 | |
|         self.worker_threads.append(new_thread)
 | |
|         new_thread.daemon = True
 | |
|         new_thread.start()
 | |
| 
 | |
| 
 | |
|     def _throttle_count(self):
 | |
|         '''
 | |
|         Return current throttling status of all cores.
 | |
| 
 | |
|         The return integer value is the sum of all cores' throttling status.
 | |
|         When the sum is equal the core number - all cores are throttling.
 | |
|         '''
 | |
| 
 | |
|         count = 0
 | |
|         for cpu in range(self.pl_desc.num_cores):
 | |
|             count += int(utils.read_file(
 | |
|                     PER_CORE_FREQ_TEMPLATE % (cpu, 'throttle')))
 | |
|         return count
 | |
| 
 | |
| 
 | |
|     def _cpu_freq(self, cpu):
 | |
|         '''Return current clock frequency of a CPU, integer in Kilohertz.'''
 | |
| 
 | |
|         return int(utils.read_file(
 | |
|                 PER_CORE_FREQ_TEMPLATE % (cpu, 'cpuinfo_cur_freq')))
 | |
| 
 | |
| 
 | |
|     def _cpu_temp(self):
 | |
|         '''Return current CPU temperature, a float value.'''
 | |
| 
 | |
|         return float(utils.read_file(
 | |
|                 os.path.join(self.temperature_data_path, EXT_TEMP_SENSOR_FILE)))
 | |
| 
 | |
| 
 | |
|     def _throttle_limit(self):
 | |
|         '''
 | |
|         Return current CPU throttling temperature threshold.
 | |
| 
 | |
|         If CPU temperature exceeds this value, clock throttling is activated,
 | |
|         causing CPU slowdown.
 | |
| 
 | |
|         Returns the limit as a float value.
 | |
|         '''
 | |
| 
 | |
|         return float(utils.read_file(
 | |
|                 os.path.join(self.temperature_data_path,
 | |
|                              THROTTLE_EXT_LIMIT_FILE)))
 | |
| 
 | |
| 
 | |
|     def _set_throttle_limit(self, new_limit):
 | |
|         '''
 | |
|         Set current CPU throttling temperature threshold.
 | |
| 
 | |
|         The passed in float value is rounded to the nearest integer.
 | |
|         '''
 | |
| 
 | |
|         utils.open_write_close(
 | |
|             os.path.join(
 | |
|                 self.temperature_data_path, THROTTLE_EXT_LIMIT_FILE),
 | |
|             '%d' % int(round(new_limit)))
 | |
| 
 | |
| 
 | |
|     def _check_freq(self):
 | |
|         '''Verify that all CPU clocks are in range for this target.'''
 | |
| 
 | |
|         for cpu in range(self.pl_desc.num_cores):
 | |
|             freq = self._cpu_freq(cpu)
 | |
|             if self.pl_desc.min_cpu_freq <= freq <= self.pl_desc.max_cpu_freq:
 | |
|                 return
 | |
|             raise error.TestError('Wrong cpu %d frequency reading %d' % (
 | |
|                     cpu, freq))
 | |
| 
 | |
| 
 | |
|     def _get_cpu_freq_raised(self):
 | |
|         '''
 | |
|         Bring all cores clock to max frequency.
 | |
| 
 | |
|         This function uses the scaling_governor mechanism to force the cores
 | |
|         to run at maximum frequency, writing the string 'performance' into
 | |
|         each core's governor file.
 | |
| 
 | |
|         The current value (if not 'performance') is preserved to be restored
 | |
|         in the end of the test.
 | |
| 
 | |
|         Returns a dictionary where keys are the core numbers and values are
 | |
|         the preserved governor setting.
 | |
| 
 | |
|         raises TestError in case 'performance' setting is not allowed on any
 | |
|                of the cores, or the clock frequency does not reach max on any
 | |
|                of the cores in 1 second.
 | |
|         '''
 | |
| 
 | |
|         rv = {}
 | |
|         for cpu in range(self.pl_desc.num_cores):
 | |
|             target = 'performance'
 | |
|             gov_file = PER_CORE_FREQ_TEMPLATE % (cpu, GOVERNOR)
 | |
|             current_gov = utils.read_file(gov_file).strip()
 | |
|             available_govs = utils.read_file(PER_CORE_FREQ_TEMPLATE % (
 | |
|                     cpu, AVAILABLE_GOVERNORS)).split()
 | |
| 
 | |
|             if current_gov != target:
 | |
|                 if not target in available_govs:
 | |
|                     raise error.TestError('core %d does not allow setting %s'
 | |
|                                           % (cpu, target))
 | |
|                 logging.info('changing core %d governor from %s to %s' % (
 | |
|                         cpu, current_gov, target))
 | |
|                 utils.open_write_close(gov_file, target)
 | |
|                 rv[cpu] = current_gov
 | |
| 
 | |
|         for _ in range(2):  # Wait for no more than 1 second
 | |
|             for cpu in range(self.pl_desc.num_cores):
 | |
|                 if self._cpu_freq(cpu) != self.pl_desc.max_cpu_freq:
 | |
|                     break
 | |
|             else:
 | |
|                 return rv
 | |
| 
 | |
|         freqs = []
 | |
|         for cpu in range(self.pl_desc.num_cores):
 | |
|             freqs.append('%d' % self._cpu_freq(cpu))
 | |
|         raise error.TestError('failed to speed up some CPU clocks: %s' %
 | |
|                               ', '.join(freqs))
 | |
| 
 | |
| 
 | |
|     def _get_cpu_temp_raised(self):
 | |
|         '''
 | |
|         Start more threads to increase CPU temperature.
 | |
| 
 | |
|         This function starts 10 threads and waits till either of the two
 | |
|         events happen:
 | |
| 
 | |
|         - the throttling is activated (the threshold is expected to be set at
 | |
|           DELTA/2 above the temperature when the test started). This is
 | |
|           considered a success, the function returns.
 | |
| 
 | |
|         - the temperature raises DELTA degrees above the original temperature
 | |
|           but throttling does not start. This is considered an overheating
 | |
|           failure, a test error is raised.
 | |
| 
 | |
|         If the temperature does not reach the DELTA and throttling does not
 | |
|         start in 30 seconds - a test error is also raised in this case.
 | |
|         '''
 | |
| 
 | |
|         base_temp = self._cpu_temp()
 | |
|         # Start 10 more cpu heater threads
 | |
|         for _ in range(10):
 | |
|             self._add_heater_thread()
 | |
| 
 | |
|         # Wait 30 seconds for the temp to raise DELTA degrees or throttling to
 | |
|         # start
 | |
|         for count in range(30):
 | |
|             new_temp = self._cpu_temp()
 | |
|             if new_temp - base_temp >= DELTA:
 | |
|                 raise error.TestError(
 | |
|                     'Reached temperature of %2.1fC in %d'
 | |
|                     ' seconds, no throttling.'
 | |
|                     % count)
 | |
|             if self._throttle_count() == self.pl_desc.num_cores:
 | |
|                 logging.info('full throttle after %d seconds' % count)
 | |
|                 return
 | |
|             time.sleep(1)
 | |
|         raise error.TestError(
 | |
|             'failed to raise CPU temperature from %s (reached %s), '
 | |
|             '%d cores throttled' % (
 | |
|                 str(base_temp), str(new_temp), self._throttle_count()))
 | |
| 
 | |
|     def _get_platform_descriptor(self):
 | |
|         '''Fill out the platform descriptor to be used by the test.'''
 | |
| 
 | |
|         present = utils.read_file(os.path.join(CPU_INFO_ROOT, 'present'))
 | |
|         if present.count('-') != 1:
 | |
|             raise error.TestError(
 | |
|                 "can't determine number of cores from %s" % present)
 | |
|         (min_core, max_core) = tuple(int(x) for x in present.split('-'))
 | |
|         min_freq = int(utils.read_file(
 | |
|             PER_CORE_FREQ_TEMPLATE % (0, 'cpuinfo_min_freq')))
 | |
|         max_freq = int(utils.read_file(
 | |
|             PER_CORE_FREQ_TEMPLATE % (0, 'cpuinfo_max_freq')))
 | |
| 
 | |
|         return PlatformDescriptor(max_core - min_core + 1, max_freq, min_freq)
 | |
| 
 | |
| 
 | |
|     def _prepare_test(self):
 | |
|         '''Prepare test: check initial conditions and set variables.'''
 | |
| 
 | |
|         ext_temp_path = utils.system_output(
 | |
|             'find /sys -name %s' % EXT_TEMP_SENSOR_FILE).splitlines()
 | |
|         if len(ext_temp_path) != 1:
 | |
|             raise error.TestError('found %d sensor files' % len(ext_temp_path))
 | |
| 
 | |
|         self.temperature_data_path = os.path.dirname(ext_temp_path[0])
 | |
| 
 | |
|         self.stop_all_workers = False
 | |
| 
 | |
|         self.pl_desc = self._get_platform_descriptor()
 | |
| 
 | |
|         # Verify CPU frequency is in range.
 | |
|         self._check_freq()
 | |
| 
 | |
|         # Make sure we are not yet throttling.
 | |
|         if self._throttle_count():
 | |
|             raise error.TestError('Throttling active before test started')
 | |
| 
 | |
|         # Remember throttling level setting before test started.
 | |
|         self.preserved_throttle_limit = self._throttle_limit()
 | |
| 
 | |
|         if self.preserved_throttle_limit - self._cpu_temp() < 4 * DELTA:
 | |
|             raise error.TestError('Target is too hot: %s C' % str(
 | |
|                     self._cpu_temp()))
 | |
| 
 | |
|         # list to keep track of threads started to heat up CPU.
 | |
|         self.worker_threads = []
 | |
| 
 | |
|         # Dictionary of saved cores' scaling governor settings.
 | |
|         self.saved_governors = {}
 | |
| 
 | |
|         self.register_after_iteration_hook(clean_up)
 | |
| 
 | |
| 
 | |
|     def run_once(self):
 | |
|         self._prepare_test()
 | |
|         logging.info('starting temperature is %s' % str(self._cpu_temp()))
 | |
|         logging.info('starting frequency is %s' % str(self._cpu_freq(0)))
 | |
| 
 | |
|         self.saved_governors = self._get_cpu_freq_raised()
 | |
|         self._set_throttle_limit(self._cpu_temp() + DELTA/2)
 | |
|         self._get_cpu_temp_raised()
 | |
|         self._set_throttle_limit(self.preserved_throttle_limit)
 | |
| 
 | |
|         # Half a second after restoring the throttling limit is plenty for
 | |
|         # throttling to stop.
 | |
|         time.sleep(.5)
 | |
|         if self._throttle_count():
 | |
|             raise error.TestError('Throttling did not stop')
 | |
| 
 | |
|         logging.info('ending temperature is %s' % str(self._cpu_temp()))
 | |
|         logging.info('ending frequency is %s' % str(self._cpu_freq(0)))
 | |
| 
 | |
| 
 | |
|     def cleanup(self):
 | |
|         self.stop_all_workers = True
 | |
|         self._set_throttle_limit(self.preserved_throttle_limit)
 | |
|         logging.info('stopping %d thread(s)' % len(self.worker_threads))
 | |
|         runaway_threads = 0
 | |
|         while self.worker_threads:
 | |
|             t = self.worker_threads.pop()
 | |
|             t.join(.5)
 | |
|             if t.isAlive():
 | |
|                 runaway_threads += 1
 | |
|         if runaway_threads:
 | |
|             for f in glob.glob('%s*' % TMP_FILE_TEMPLATE):
 | |
|                 logging.info('removing %s' % f)
 | |
|                 os.remove(f)
 | |
|             raise error.TestError(
 | |
|                 'Failed to join %d worker thread(s)' % runaway_threads)
 | |
| 
 | |
|         if not self.saved_governors:
 | |
|             return
 | |
| 
 | |
|         for (cpu, gov) in self.saved_governors.iteritems():
 | |
|             gov_file = PER_CORE_FREQ_TEMPLATE % (cpu, GOVERNOR)
 | |
|             logging.info('restoring core %d governor to %s' % (cpu, gov))
 | |
|             utils.open_write_close(gov_file, gov)
 | |
|         self.saved_governors = {}
 |