xref: /aosp_15_r20/external/autotest/server/cros/graphics/graphics_power.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Copyright 2020 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"""Provides a management class for using graphics_Power in server tests."""
5
6import logging
7import os
8import tempfile
9import threading
10import time
11
12from autotest_lib.client.common_lib import error
13from autotest_lib.server import autotest
14
15ROOT_DIR = '/tmp/graphics_Power/'
16DEFAULT_SIGNAL_RUNNING_FILE = os.path.join(ROOT_DIR, 'signal_running')
17DEFAULT_SIGNAL_CHECKPOINT_FILE = os.path.join(ROOT_DIR, 'signal_checkpoint')
18
19
20class GraphicsPowerThread(threading.Thread):
21    """Thread for running the graphics_Power client test.
22
23    Provides a threaded management interface for the graphics_Power subtest.
24    This class can be used from an autotest server test to log system
25    performance metrics in the background on the test host.
26    """
27
28    class Error(Exception):
29        """Base error that can be inherited to define more specific errors."""
30        pass
31
32    class ThreadNotInitializedError(Error):
33        """An error indicating that the thread was not properly initialized."""
34        pass
35
36    class InitTimeoutError(Error):
37        """An error indicating that a timeout occurred during a blocking call."""
38        pass
39
40    def __init__(self,
41                 host,
42                 max_duration_minutes,
43                 sample_rate_seconds=1,
44                 test_tag=None,
45                 pdash_note=None,
46                 result_dir=None,
47                 signal_running_file=DEFAULT_SIGNAL_RUNNING_FILE,
48                 signal_checkpoint_file=DEFAULT_SIGNAL_CHECKPOINT_FILE):
49        """Initializes the thread.
50
51        Args:
52            host: An autotest host instance.
53            max_duration_minutes: Float defining the maximum running time of the
54                managed sub-test.
55            sample_rate_seconds: Optional; Number defining seconds between data
56                point acquisition.
57            test_tag: Optional; String describing the test that initiated this
58                monitoring process; appended to the true test name.
59            pdash_note: Optional; A tag that is included as a filter field on
60                the ChromeOS power-dashboard.
61            result_dir: Optional; String defining the location on the test
62                target where post-processed results from this sub-test should be
63                saved for retrieval by the managing test process. Set to None if
64                results output is not be created.
65            signal_running_file: Optional; String defining the location of the
66                'running' RPC flag file on the test target. Removal of this file
67                triggers the subtest to finish logging and stop gracefully.
68            signal_checkpoint_file: Optional; String defining the location of
69                the 'checkpoint' RPC flag file on the test target. Modifying
70                this file triggers the subtest to create a checkpoint with name
71                equal to the utf-8-encoded contents of the first-line and
72                optional alternative start time (in seconds since the epoch)
73                equal to the second line of the file.
74        """
75        super(GraphicsPowerThread, self).__init__(name=__name__)
76        self._running = False
77        self._autotest_client = autotest.Autotest(host)
78        self._host = host
79        self._test_thread = None
80
81        self.max_duration_minutes = max_duration_minutes
82        self.sample_rate_seconds = sample_rate_seconds
83        self.test_tag = test_tag
84        self.pdash_note = pdash_note
85        self.result_dir = result_dir
86        self.signal_running_file = signal_running_file
87        self.signal_checkpoint_file = signal_checkpoint_file
88
89    def is_running(self):
90        """Return a bool indicating the 'running' state of the subtest.
91
92        This check can be used to ensure system logging is initialized and
93        running before beginning other subtests.
94        """
95        try:
96            self._host.run('test -f %s' % self.signal_running_file)
97            return True
98        except (error.AutotestHostRunCmdError, error.AutoservRunError):
99            return False
100
101    def wait_until_running(self, timeout=120):
102        """Block execution until the subtest reports it is logging properly.
103
104        Args:
105            timeout: Optional; Float that defines how long to block before
106                timeout occurs. If timeout=None, then block forever
107
108        Raises:
109            RuntimeError: The subtest ended unexpectedly before initialization
110                finished.
111            GraphicsPowerThread.ThreadNotInitializedError: The thread hasn't
112                been started by the managing server test yet.
113            GraphicsPowerThread.InitTimeoutError: A timeout occurred while
114                waiting for subtest to report itself as running.
115        """
116        if timeout:
117            time_start = time.time()
118            time_end = time_start + timeout
119        while True:
120            if timeout and time.time() >= time_end:
121                self.stop()
122                raise self.InitTimeoutError(
123                    'The graphics_Power subtest initialization timed out after'
124                    ' %d second(s).' % timeout)
125            if not self.is_alive():
126                raise RuntimeError(
127                    'The graphics_Power subtest failed to initialize')
128            if self.is_running():
129                break
130            time.sleep(1)
131
132        if not self._test_thread:
133            raise self.ThreadNotInitializedError
134
135    def stop(self, timeout=None):
136        """Gracefully stop the subtest on the test host.
137
138        If timeout is None, then this is a blocking call that waits forever.
139        If timeout is a positive number, then it waits for 'timeout' seconds.
140        If timeout is 0, then it returns immediately.
141
142        Args:
143            timeout: Time (seconds) before giving up on joining the thread.
144
145        Returns:
146            A bool indicating if thread was stopped.
147        """
148        self._running = False
149        self.join(timeout)
150        return not self.is_alive()
151
152    def checkpoint_measurements(self, name, start_time=None):
153        """Save the current log buffers with an associated name.
154
155        The power-dashboard displays time series data in one or more
156        checkpoints that can be used to annotate different phases of a test.
157
158        By saving a checkpoint, the time series data collected since the end of
159        the most recently committed checkpoint (or the test start if no
160        checkpoints are saved yet) is annotated on the power-dashboard with the
161        specified name. The checkpoint start time can be adjusted with the
162        optional 'start_time' argument.
163
164        Args:
165            name: String defining the saved checkpoint's name.
166            start_time: Optional; Float indicating the time (in seconds since
167                the epoch) at which this checkpoint should actually start. This
168                functionally discards data from the beginning of the logged
169                duration until start_time.
170        """
171        with tempfile.NamedTemporaryFile('w') as tf:
172            tf.write(str(name) + '\n')
173            if start_time:
174                tf.write(str(start_time))
175            tf.flush()
176            self._host.send_file(tf.name, self.signal_checkpoint_file)
177
178    def _run_test_async(self):
179        self._autotest_client.run_test(
180            'graphics_Power',
181            tag=self.test_tag,
182            max_duration_minutes=self.max_duration_minutes,
183            sample_rate_seconds=self.sample_rate_seconds,
184            pdash_note=self.pdash_note,
185            result_dir=self.result_dir,
186            signal_running_file=self.signal_running_file,
187            signal_checkpoint_file=self.signal_checkpoint_file)
188
189    def run(self):
190        self._running = True
191        self._test_thread = threading.Thread(target=self._run_test_async)
192        self._test_thread.start()
193        logging.info('Started thread: %s', self.__class__.__name__)
194
195        def send_stop_signal_and_join():
196            """Emits a stop signal to the test host and joins the thread.
197
198            Deletes a monitored file on the test host over ssh and waits for
199            the graphics_Power sub-test to end gracefully as a consequence.
200            """
201            while True:
202                self._host.run('rm %s 2>/dev/null || true' %
203                               self.signal_running_file)
204                self._test_thread.join(5)
205                if not self._test_thread.is_alive():
206                    break
207
208        while True:
209            time.sleep(1)
210            if not self._test_thread.is_alive():
211                logging.debug('The graphics_Power subtest ended')
212                break
213            elif not self._running:
214                logging.debug(
215                    'Sending stop signal to the graphics_Power subtest')
216                send_stop_signal_and_join()
217                break
218