# 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. """Provides a management class for using graphics_Power in server tests.""" import logging import os import tempfile import threading import time from autotest_lib.client.common_lib import error from autotest_lib.server import autotest 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') class GraphicsPowerThread(threading.Thread): """Thread for running the graphics_Power client test. Provides a threaded management interface for the graphics_Power subtest. This class can be used from an autotest server test to log system performance metrics in the background on the test host. """ class Error(Exception): """Base error that can be inherited to define more specific errors.""" pass class ThreadNotInitializedError(Error): """An error indicating that the thread was not properly initialized.""" pass class InitTimeoutError(Error): """An error indicating that a timeout occurred during a blocking call.""" pass def __init__(self, host, max_duration_minutes, sample_rate_seconds=1, test_tag=None, pdash_note=None, result_dir=None, signal_running_file=DEFAULT_SIGNAL_RUNNING_FILE, signal_checkpoint_file=DEFAULT_SIGNAL_CHECKPOINT_FILE): """Initializes the thread. Args: host: An autotest host instance. max_duration_minutes: Float defining the maximum running time of the managed sub-test. sample_rate_seconds: Optional; Number defining seconds between data point acquisition. test_tag: Optional; String describing the test that initiated this monitoring process; appended to the true test name. pdash_note: Optional; A tag that is included as a filter field on the ChromeOS power-dashboard. 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. """ super(GraphicsPowerThread, self).__init__(name=__name__) self._running = False self._autotest_client = autotest.Autotest(host) self._host = host self._test_thread = None self.max_duration_minutes = max_duration_minutes self.sample_rate_seconds = sample_rate_seconds self.test_tag = test_tag self.pdash_note = pdash_note self.result_dir = result_dir self.signal_running_file = signal_running_file self.signal_checkpoint_file = signal_checkpoint_file def is_running(self): """Return a bool indicating the 'running' state of the subtest. This check can be used to ensure system logging is initialized and running before beginning other subtests. """ try: self._host.run('test -f %s' % self.signal_running_file) return True except (error.AutotestHostRunCmdError, error.AutoservRunError): return False def wait_until_running(self, timeout=120): """Block execution until the subtest reports it is logging properly. Args: timeout: Optional; Float that defines how long to block before timeout occurs. If timeout=None, then block forever Raises: RuntimeError: The subtest ended unexpectedly before initialization finished. GraphicsPowerThread.ThreadNotInitializedError: The thread hasn't been started by the managing server test yet. GraphicsPowerThread.InitTimeoutError: A timeout occurred while waiting for subtest to report itself as running. """ if timeout: time_start = time.time() time_end = time_start + timeout while True: if timeout and time.time() >= time_end: self.stop() raise self.InitTimeoutError( 'The graphics_Power subtest initialization timed out') if not self.is_alive(): raise RuntimeError( 'The graphics_Power subtest failed to initialize') if self.is_running(): break time.sleep(1) if not self._test_thread: raise self.ThreadNotInitializedError def stop(self, timeout=None): """Gracefully stop the subtest on the test host. If timeout is None, then this is a blocking call that waits forever. If timeout is a positive number, then it waits for 'timeout' seconds. If timeout is 0, then it returns immediately. Args: timeout: Time (seconds) before giving up on joining the thread. Returns: A bool indicating if thread was stopped. """ self._running = False self.join(timeout) return not self.is_alive() def checkpoint_measurements(self, name, start_time=None): """Save the current log buffers with an associated name. The power-dashboard displays time series data in one or more checkpoints that can be used to annotate different phases of a test. By saving a checkpoint, the time series data collected since the end of the most recently committed checkpoint (or the test start if no checkpoints are saved yet) is annotated on the power-dashboard with the specified name. The checkpoint start time can be adjusted with the optional 'start_time' argument. 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. """ with tempfile.NamedTemporaryFile('w') as tf: tf.write(str(name) + '\n') if start_time: tf.write(str(start_time)) tf.flush() self._host.send_file(tf.name, self.signal_checkpoint_file) def _run_test_async(self): self._autotest_client.run_test( 'graphics_Power', tag=self.test_tag, max_duration_minutes=self.max_duration_minutes, sample_rate_seconds=self.sample_rate_seconds, pdash_note=self.pdash_note, result_dir=self.result_dir, signal_running_file=self.signal_running_file, signal_checkpoint_file=self.signal_checkpoint_file) def run(self): self._running = True self._test_thread = threading.Thread(target=self._run_test_async) self._test_thread.start() logging.info('Started thread: %s', self.__class__.__name__) def send_stop_signal_and_join(): """Emits a stop signal to the test host and joins the thread. Deletes a monitored file on the test host over ssh and waits for the graphics_Power sub-test to end gracefully as a consequence. """ while True: self._host.run('rm %s 2>/dev/null || true' % self.signal_running_file) self._test_thread.join(5) if not self._test_thread.is_alive(): break while True: time.sleep(1) if not self._test_thread.is_alive(): logging.debug('The graphics_Power subtest ended') break elif not self._running: logging.debug( 'Sending stop signal to the graphics_Power subtest') send_stop_signal_and_join() break