| # Copyright 2020 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. |
| """Client test for logging system performance metrics.""" |
| |
| from collections import namedtuple |
| import logging |
| import os |
| import time |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.cros.power import power_test |
| |
| ROOT_DIR = '/tmp/graphics_Power/' |
| DEFAULT_SIGNAL_RUNNING_FILE = os.path.join(ROOT_DIR, 'signal_running') |
| DEFAULT_SIGNAL_CHECKPOINT_FILE = os.path.join(ROOT_DIR, 'signal_checkpoint') |
| |
| |
| def remove_file_if_exists(f): |
| """Attempt to delete the file only if it exists.""" |
| if os.path.exists(f): |
| os.remove(f) |
| |
| |
| class MonitoredFile(): |
| """Watches a file and supports querying changes to its status. |
| |
| Tracks a file's current and previous status based on its modified time and |
| existence. Provides convenience functions that test for the occurrence of |
| various changes, such as file creation, deletion, and modification. |
| """ |
| |
| MonitoredFileStatus = namedtuple('monitored_file_status', |
| ('exists', 'mtime')) |
| |
| def __init__(self, filename): |
| self.filename = filename |
| self._prev_status = self._get_file_status() |
| self._curr_status = self._prev_status |
| |
| def _get_file_status(self): |
| exists = os.path.exists(self.filename) |
| if exists: |
| mtime = os.path.getmtime(self.filename) |
| else: |
| mtime = None |
| |
| return self.MonitoredFileStatus(exists=exists, mtime=mtime) |
| |
| def update(self): |
| """Check file to update its status. |
| |
| This should be called once per an event loop iteration. |
| """ |
| self._prev_status = self._curr_status |
| self._curr_status = self._get_file_status() |
| |
| def exists(self): |
| """Tests if the file exists.""" |
| return self._curr_status.exists |
| |
| def deleted(self): |
| """Tests that the file was just deleted""" |
| return not self._curr_status.exists and self._prev_status.exists |
| |
| def created(self): |
| """Tests that the file was just created""" |
| return self._curr_status.exists and not self._prev_status.exists |
| |
| def modified(self): |
| """Tests that the file was just modified""" |
| return (self.deleted() or self.created() or |
| self._prev_status.mtime != self._curr_status.mtime) |
| |
| |
| class graphics_Power(power_test.power_Test): |
| """Wrapper around power_Test client test for use in server tests. |
| |
| Wraps the client power_Test for acquiring system metrics related to graphics |
| rendering performance (temperature, clock freqs, power states). |
| |
| This class should only be instantiated from a server test. For background |
| logging, see |
| <autotest_lib.server.cros.graphics.graphics_power.GraphicsPowerThread()> |
| """ |
| version = 1 |
| |
| def __init__(self, *args, **kwargs): |
| super(graphics_Power, self).__init__(*args, **kwargs) |
| self._last_checkpoint_time = None |
| |
| def initialize(self, sample_rate_seconds=1, pdash_note=''): |
| """Setup power_Test base class. |
| |
| Args: |
| sample_rate_seconds: Optional; Number defining seconds between data |
| point acquisition. |
| pdash_note: Optional; A tag that is included as a filter field on |
| the ChromeOS power-dashboard. |
| """ |
| super(graphics_Power, self).initialize( |
| seconds_period=sample_rate_seconds, |
| pdash_note=pdash_note, |
| force_discharge=False) |
| |
| @staticmethod |
| def _read_checkpoint_file(filename): |
| """Parses checkpoint signal file and returns name and start_time. |
| |
| Args: |
| filename: String path to the checkpoint file to be read. |
| |
| Returns: |
| A 2-tuple: (name, start_time) containing a checkpoint name (string) |
| and the checkpoint's start time (float; seconds since the epoch). |
| |
| If the start time is not provided in the checkpoint file, start_time |
| is equal to None. |
| """ |
| with open(filename, 'r') as f: |
| name = f.readline().rstrip('\n') |
| if not name: |
| name = None |
| |
| start_time = f.readline().rstrip('\n') |
| if start_time: |
| start_time = float(start_time) |
| else: |
| start_time = None |
| return name, start_time |
| |
| def checkpoint_measurements(self, name, start_time=None): |
| """Save a power_Test measurement checkpoint. |
| |
| Wraps power_Test.checkpoint_measurements to change behavior of default |
| start_time to continue from the end of the previous checkpoint, rather |
| than from the test start time. |
| |
| Args: |
| name: String defining the saved checkpoint's name. |
| start_time: Optional; Float indicating the time (in seconds since |
| the epoch) at which this checkpoint should actually start. This |
| functionally discards data from the beginning of the logged |
| duration until start_time. |
| """ |
| # The default start_time is the test start time, but we want checkpoints |
| # to start from the end of the previous one. |
| if not start_time: |
| start_time = self._last_checkpoint_time |
| logging.debug('Saving measurements checkpoint "%s" with start time %f', |
| name, start_time) |
| super(graphics_Power, self).checkpoint_measurements(name, start_time) |
| self._last_checkpoint_time = time.time() |
| |
| def run_once(self, |
| max_duration_minutes, |
| result_dir=None, |
| signal_running_file=DEFAULT_SIGNAL_RUNNING_FILE, |
| signal_checkpoint_file=DEFAULT_SIGNAL_CHECKPOINT_FILE): |
| """Run system performance loggers until stopped or timeout occurs. |
| |
| Temporal data logs are written to |
| <test_results>/{power,cpu,temp,fan_rpm}_results_<timestamp>_raw.txt |
| |
| If result_dir points to a valid filesystem path, post-processing of logs |
| will be performed and a more convenient temporal format will be saved in |
| the result_dir. |
| |
| Args: |
| max_duration_minutes: Number defining the maximum running time of |
| the managed sub-test. |
| result_dir: Optional; String defining the location on the test |
| target where post-processed results from this sub-test should be |
| saved for retrieval by the managing test process. Set to None if |
| results output is not be created. |
| signal_running_file: Optional; String defining the location of the |
| 'running' RPC flag file on the test target. Removal of this file |
| triggers the subtest to finish logging and stop gracefully. |
| signal_checkpoint_file: Optional; String defining the location of |
| the 'checkpoint' RPC flag file on the test target. Modifying |
| this file triggers the subtest to create a checkpoint with name |
| equal to the utf-8-encoded contents of the first-line and |
| optional alternative start time (in seconds since the epoch) |
| equal to the second line of the file. |
| """ |
| # Initiailize test state |
| for f in (signal_running_file, signal_checkpoint_file): |
| remove_file_if_exists(f) |
| |
| # Indicate 'running' state by touching a mutually-monitored file |
| try: |
| open(signal_running_file, 'w').close() |
| if not os.path.exists(signal_running_file): |
| raise RuntimeError( |
| 'Signal "running" file %s was not properly initiailized' % |
| signal_running_file) |
| except: |
| logging.exception('Failed to set "running" state.') |
| raise |
| |
| signal_running = MonitoredFile(signal_running_file) |
| logging.info('Monitoring "running" signal file: %s', |
| signal_running_file) |
| signal_checkpoint = MonitoredFile(signal_checkpoint_file) |
| logging.info('Monitoring "checkpoint" signal file: %s', |
| signal_checkpoint_file) |
| |
| self.start_measurements() # provided by power_Test class |
| time_start = time.time() |
| time_end = time_start + max_duration_minutes * 60.0 |
| self._last_checkpoint_time = time_start |
| monitored_files = [signal_running, signal_checkpoint] |
| while time.time() < time_end: |
| for f in monitored_files: |
| f.update() |
| |
| if signal_checkpoint.exists() and signal_checkpoint.modified(): |
| try: |
| checkpoint_name, checkpoint_start_time = \ |
| self._read_checkpoint_file(signal_checkpoint_file) |
| except ValueError as err: |
| logging.exception(err) |
| raise error.TestFail( |
| 'Error while converting the checkpoint start time ' |
| 'string to a float.' % signal_checkpoint_file) |
| self.checkpoint_measurements(checkpoint_name, |
| checkpoint_start_time) |
| |
| if signal_running.deleted(): |
| logging.info('Signaled to stop by the managing test process') |
| break |
| |
| time.sleep(1) |
| |
| self.checkpoint_measurements('default') |
| |
| # Rely on managing test to create/cleanup result_dir |
| if result_dir: |
| # TODO(ryanneph): Implement structured log output for raw power_Test |
| # log files |
| with open( |
| os.path.join(result_dir, 'graphics_Power_test_output.txt'), |
| 'w') as f: |
| f.write('{}\n') |
| |
| # Cleanup our test state from the filesystem. |
| for f in (signal_running_file, signal_checkpoint_file): |
| remove_file_if_exists(f) |