# 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 = {}