Home | History | Annotate | Download | only in cros
      1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Provides utility methods for controlling powerd in ChromiumOS."""
      6 
      7 import errno
      8 import logging
      9 import multiprocessing
     10 import os
     11 import rtc
     12 import time
     13 import upstart
     14 
     15 SYSFS_POWER_STATE = '/sys/power/state'
     16 SYSFS_WAKEUP_COUNT = '/sys/power/wakeup_count'
     17 
     18 
     19 class SuspendFailure(Exception):
     20     """Base class for a failure during a single suspend/resume cycle."""
     21     pass
     22 
     23 
     24 class SuspendTimeout(SuspendFailure):
     25     """Suspend took too long, got wakeup event (RTC tick) before it was done."""
     26     pass
     27 
     28 
     29 class KernelError(SuspendFailure):
     30     """Kernel problem encountered during suspend/resume.
     31 
     32     Whitelist is an array of a 2-entry tuples consisting of (source regexp, text
     33     regexp).  For example, kernel output might look like,
     34 
     35       [  597.079950] WARNING: CPU: 0 PID: 21415 at \
     36       <path>/v3.18/drivers/gpu/drm/i915/intel_pm.c:3687 \
     37       skl_update_other_pipe_wm+0x136/0x18a()
     38       [  597.079962] WARN_ON(!wm_changed)
     39 
     40     source regexp should match first line above while text regexp can match
     41     up to 2 lines below the source.  Note timestamps are stripped prior to
     42     comparing regexp.
     43     """
     44     WHITELIST = [
     45             # crosbug.com/37594: debug tracing clock desync we don't care about
     46             (r'kernel/trace/ring_buffer.c:\d+ rb_reserve_next_event',
     47              r'Delta way too big!'),
     48             # TODO(crosbug.com/p/52008): Remove from whitelist once watermark
     49             # implementation has landed.
     50             (r'v3.18/\S+/intel_pm.c:\d+ skl_update_other_pipe_wm',
     51             r'WARN_ON\(\!wm_changed\)')
     52         ]
     53 
     54 
     55 class FirmwareError(SuspendFailure):
     56     """String 'ERROR' found in firmware log after resume."""
     57     WHITELIST = [
     58             # crosbug.com/36762: no one knows, but it has always been there
     59             ('^stumpy', r'PNP: 002e\.4 70 irq size: 0x0000000001 not assigned'),
     60             # crbug.com/221538: no one knows what ME errors mean anyway
     61             ('^parrot', r'ME failed to respond'),
     62         ]
     63 
     64 
     65 class SpuriousWakeupError(SuspendFailure):
     66     """Received spurious wakeup while suspending or woke before schedule."""
     67     S3_WHITELIST = [  # (<board>, <eventlog wake source>, <syslog wake source>)
     68             # crbug.com/220014: spurious trackpad IRQs
     69             ('^link', 'Wake Source | GPIO | 12', ''),
     70             # crbug.com/345327: unknown, probably same as crbug.com/290923
     71             ('^x86-alex', '', ''),   # alex can report neither, blanket ignore
     72             # crbug.com/355106: unknown, possibly related to crbug.com/290923
     73             ('^lumpy|^parrot', '', 'PM1_STS: WAK PWRBTN'),
     74         ]
     75     S0_WHITELIST = [  # (<board>, <kernel wake source>)
     76             # crbug.com/290923: spurious keyboard IRQ, believed to be from Servo
     77             ('^x86-alex|^lumpy|^parrot|^butterfly', 'serio0'),
     78             # crosbug.com/p/46140: battery event caused by MKBP
     79             ('^elm|^oak', 'spi32766.0'),
     80         ]
     81 
     82 class MemoryError(SuspendFailure):
     83     """memory_suspend_test found memory corruption."""
     84     pass
     85 
     86 
     87 class SuspendNotAllowed(SuspendFailure):
     88     """Suspend was not allowed to be performed."""
     89     pass
     90 
     91 
     92 class S0ixResidencyNotChanged(SuspendFailure):
     93     """power_SuspendStress test found CPU/SoC is unable to idle properly
     94     when suspended to S0ix. """
     95     pass
     96 
     97 def prepare_wakeup(seconds):
     98     """Prepare the device to wake up from an upcoming suspend.
     99 
    100     @param seconds: The number of seconds to allow the device to suspend.
    101     """
    102     # May cause DUT not wake from sleep if the suspend time is 1 second.
    103     # It happens when the current clock (floating point) is close to the
    104     # next integer, as the RTC sysfs interface only accepts integers.
    105     # Make sure it is larger than or equal to 2.
    106     assert seconds >= 2
    107     wakeup_count = read_wakeup_count()
    108     alarm = int(rtc.get_seconds() + seconds)
    109     logging.debug('Suspend for %d seconds, wakealarm = %d', seconds, alarm)
    110     rtc.set_wake_alarm(alarm)
    111     return (alarm, wakeup_count)
    112 
    113 
    114 def check_wakeup(alarm):
    115     """Verify that the device did not wakeup early.
    116 
    117     @param alarm: The time at which the device was expected to wake up.
    118     """
    119     now = rtc.get_seconds()
    120     if now < alarm:
    121         logging.error('Woke up early at %d', now)
    122         raise SpuriousWakeupError('Woke from suspend early')
    123 
    124 
    125 def do_suspend(suspend_seconds, delay_seconds=0):
    126     """Do a suspend using the power manager.
    127 
    128     Wait for |delay_seconds|, suspend the system to RAM (S3), waking up again
    129     after having been suspended for |suspend_seconds|, using the
    130     powerd_dbus_suspend program. Function will block until suspend/resume
    131     has completed or failed. Returns the wake alarm time from the RTC as epoch.
    132 
    133     @param suspend_seconds: Number of seconds to suspend the DUT.
    134     @param delay_seconds: Number of seconds wait before suspending the DUT.
    135 
    136     """
    137     alarm, wakeup_count = prepare_wakeup(suspend_seconds)
    138     upstart.ensure_running('powerd')
    139     command = ('/usr/bin/powerd_dbus_suspend --delay=%d --timeout=30 '
    140                '--wakeup_count=%d') % (delay_seconds, wakeup_count)
    141     logging.info("Running '%s'", command)
    142     os.system(command)
    143     check_wakeup(alarm)
    144     return alarm
    145 
    146 
    147 def suspend_bg_for_dark_resume(delay_seconds=0):
    148     """Do a non-blocking indefinite suspend using power manager. ONLY USE THIS
    149     IF YOU ARE ABSOLUTELY CERTAIN YOU NEED TO.
    150 
    151     Wait for |delay_seconds|, then suspend to RAM (S3). This does not set an RTC
    152     alarm and does not pass an external wakeup count. It is meant to be used for
    153     dark resume testing, where the server-side API exposes it in such a fashion
    154     that the DUT will be woken by the server no matter how the test is exited.
    155 
    156     @param delay_seconds: Number of seconds wait before suspending the DUT.
    157 
    158     """
    159     upstart.ensure_running('powerd')
    160     command = ('/usr/bin/powerd_dbus_suspend --delay=%d '
    161                '--timeout=30') % delay_seconds
    162     logging.info("Running '%s'", command)
    163     process = multiprocessing.Process(target=os.system, args=(command,))
    164     process.start()
    165 
    166 
    167 def kernel_suspend(seconds, state='mem'):
    168     """Do a kernel suspend.
    169 
    170     Suspend the system to @state, waking up again after |seconds|, by directly
    171     writing to /sys/power/state. Function will block until suspend/resume has
    172     completed or failed.
    173 
    174     @param seconds: The number of seconds to suspend the device.
    175     @param state: power state to suspend to.  DEFAULT mem.
    176     """
    177     alarm, wakeup_count = prepare_wakeup(seconds)
    178     logging.debug('Saving wakeup count: %d', wakeup_count)
    179     write_wakeup_count(wakeup_count)
    180     try:
    181         logging.info('Suspending at %d', rtc.get_seconds())
    182         with open(SYSFS_POWER_STATE, 'w') as sysfs_file:
    183             sysfs_file.write(state)
    184     except IOError as e:
    185         logging.exception('Writing to %s failed', SYSFS_POWER_STATE)
    186         if e.errno == errno.EBUSY and rtc.get_seconds() >= alarm:
    187             # The kernel returns EBUSY if it has to abort because
    188             # another wakeup fires before we've reached suspend.
    189             raise SpuriousWakeupError('Received spurious wakeup in kernel.')
    190         else:
    191             # Some driver probably failed to suspend properly.
    192             # A hint as to what failed is in errno.
    193             raise KernelError('Suspend failed: %s' % e.strerror)
    194     else:
    195         logging.info('Woke from suspend at %d', rtc.get_seconds())
    196     logging.debug('New wakeup count: %d', read_wakeup_count())
    197     check_wakeup(alarm)
    198 
    199 
    200 def idle_suspend(seconds):
    201     """
    202     Wait for the system to suspend to RAM (S3), scheduling the RTC to wake up
    203     |seconds| after this function was called. Caller must ensure that the system
    204     will idle-suspend in time for this to happen. Returns the wake alarm time
    205     from the RTC as epoch.
    206 
    207     @param seconds: The number of seconds before wakeup.
    208     """
    209     alarm, _ = prepare_wakeup(seconds)
    210     while rtc.get_seconds() < alarm:
    211         time.sleep(0.2)
    212 
    213     # tell powerd something happened, or it will immediately idle-suspend again
    214     # TODO: switch to cros.power_utils#call_powerd_dbus_method once this
    215     # horrible mess with the WiFi tests and this file's imports is solved
    216     logging.debug('Simulating user activity after idle suspend...')
    217     os.system('dbus-send --type=method_call --system '
    218               '--dest=org.chromium.PowerManager /org/chromium/PowerManager '
    219               'org.chromium.PowerManager.HandleUserActivity')
    220 
    221     return alarm
    222 
    223 
    224 def memory_suspend(seconds, size=0):
    225     """Do a memory suspend.
    226 
    227     Suspend the system to RAM (S3), waking up again after |seconds|, using
    228     the memory_suspend_test tool. Function will block until suspend/resume has
    229     completed or failed. Returns the wake alarm time from the RTC as epoch.
    230 
    231     @param seconds: The number of seconds to suspend the device.
    232     @param size: Amount of memory to allocate, in bytes. 
    233                  Set to 0 to let memory_suspend_test determine amount of memory.   
    234     """
    235     # since we cannot have utils.system_output in here, we need a workaround
    236     output = '/tmp/memory_suspend_output'
    237     alarm, wakeup_count = prepare_wakeup(seconds)
    238     status = os.system('/usr/bin/memory_suspend_test --wakeup_count=%d '
    239                        '--size=%d > %s' % (wakeup_count, size, output))
    240     status = os.WEXITSTATUS(status)
    241     if status == 2:
    242         logging.error('memory_suspend_test found the following errors:')
    243         for line in open(output, 'r'):
    244             logging.error(line)
    245         raise MemoryError('Memory corruption found after resume')
    246     elif status == 1:
    247         raise SuspendFailure('Failure in powerd_suspend during memory test')
    248     elif status:
    249         raise SuspendFailure('Unknown failure in memory_suspend_test (crash?)')
    250     check_wakeup(alarm)
    251     return alarm
    252 
    253 
    254 def read_wakeup_count():
    255     """
    256     Retrieves the current value of /sys/power/wakeup_count.
    257     """
    258     with open(SYSFS_WAKEUP_COUNT) as sysfs_file:
    259         return int(sysfs_file.read().rstrip('\n'))
    260 
    261 
    262 def write_wakeup_count(wakeup_count):
    263     """Writes a value to /sys/power/wakeup_count.
    264 
    265     @param wakeup_count: The wakeup count value to write.
    266     """
    267     try:
    268         with open(SYSFS_WAKEUP_COUNT, 'w') as sysfs_file:
    269             sysfs_file.write(str(wakeup_count))
    270     except IOError as e:
    271         if (e.errno == errno.EINVAL and read_wakeup_count() != wakeup_count):
    272             raise SpuriousWakeupError('wakeup_count changed before suspend')
    273         else:
    274             raise
    275