xref: /aosp_15_r20/external/autotest/client/site_tests/graphics_Power/graphics_Power.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Client test for logging system performance metrics."""
6
7from collections import namedtuple
8import logging
9import os
10import time
11
12from autotest_lib.client.common_lib import error
13from autotest_lib.client.cros.power import power_test
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
20def remove_file_if_exists(f):
21    """Attempt to delete the file only if it exists."""
22    if os.path.exists(f):
23        os.remove(f)
24
25
26class MonitoredFile():
27    """Watches a file and supports querying changes to its status.
28
29    Tracks a file's current and previous status based on its modified time and
30    existence. Provides convenience functions that test for the occurrence of
31    various changes, such as file creation, deletion, and modification.
32    """
33
34    MonitoredFileStatus = namedtuple('monitored_file_status',
35                                     ('exists', 'mtime'))
36
37    def __init__(self, filename):
38        self.filename = filename
39        self._prev_status = self._get_file_status()
40        self._curr_status = self._prev_status
41
42    def _get_file_status(self):
43        exists = os.path.exists(self.filename)
44        if exists:
45            mtime = os.path.getmtime(self.filename)
46        else:
47            mtime = None
48
49        return self.MonitoredFileStatus(exists=exists, mtime=mtime)
50
51    def update(self):
52        """Check file to update its status.
53
54        This should be called once per an event loop iteration.
55        """
56        self._prev_status = self._curr_status
57        self._curr_status = self._get_file_status()
58
59    def exists(self):
60        """Tests if the file exists."""
61        return self._curr_status.exists
62
63    def deleted(self):
64        """Tests that the file was just deleted"""
65        return not self._curr_status.exists and self._prev_status.exists
66
67    def created(self):
68        """Tests that the file was just created"""
69        return self._curr_status.exists and not self._prev_status.exists
70
71    def modified(self):
72        """Tests that the file was just modified"""
73        return (self.deleted() or self.created() or
74                self._prev_status.mtime != self._curr_status.mtime)
75
76
77class graphics_Power(power_test.power_Test):
78    """Wrapper around power_Test client test for use in server tests.
79
80    Wraps the client power_Test for acquiring system metrics related to graphics
81    rendering performance (temperature, clock freqs, power states).
82
83    This class should only be instantiated from a server test. For background
84    logging, see
85    <autotest_lib.server.cros.graphics.graphics_power.GraphicsPowerThread()>
86    """
87    version = 1
88
89    def __init__(self, *args, **kwargs):
90        super(graphics_Power, self).__init__(*args, **kwargs)
91        self._last_checkpoint_time = None
92
93    def initialize(self, sample_rate_seconds=1, pdash_note=''):
94        """Setup power_Test base class.
95
96        Args:
97            sample_rate_seconds: Optional; Number defining seconds between data
98                point acquisition.
99            pdash_note: Optional; A tag that is included as a filter field on
100                the ChromeOS power-dashboard.
101        """
102        super(graphics_Power, self).initialize(
103            seconds_period=sample_rate_seconds,
104            pdash_note=pdash_note,
105            force_discharge=False)
106
107    @staticmethod
108    def _read_checkpoint_file(filename):
109        """Parses checkpoint signal file and returns name and start_time.
110
111        Args:
112            filename: String path to the checkpoint file to be read.
113
114        Returns:
115            A 2-tuple: (name, start_time) containing a checkpoint name (string)
116            and the checkpoint's start time (float; seconds since the epoch).
117
118            If the start time is not provided in the checkpoint file, start_time
119            is equal to None.
120        """
121        with open(filename, 'r') as f:
122            name = f.readline().rstrip('\n')
123            if not name:
124                name = None
125
126            start_time = f.readline().rstrip('\n')
127            if start_time:
128                start_time = float(start_time)
129            else:
130                start_time = None
131        return name, start_time
132
133    def checkpoint_measurements(self, name, start_time=None):
134        """Save a power_Test measurement checkpoint.
135
136        Wraps power_Test.checkpoint_measurements to change behavior of default
137        start_time to continue from the end of the previous checkpoint, rather
138        than from the test start time.
139
140        Args:
141            name: String defining the saved checkpoint's name.
142            start_time: Optional; Float indicating the time (in seconds since
143                the epoch) at which this checkpoint should actually start. This
144                functionally discards data from the beginning of the logged
145                duration until start_time.
146        """
147        # The default start_time is the test start time, but we want checkpoints
148        # to start from the end of the previous one.
149        if not start_time:
150            start_time = self._last_checkpoint_time
151        logging.debug('Saving measurements checkpoint "%s" with start time %f',
152                      name, start_time)
153        super(graphics_Power, self).checkpoint_measurements(name, start_time)
154        self._last_checkpoint_time = time.time()
155
156    def run_once(self,
157                 max_duration_minutes,
158                 result_dir=None,
159                 signal_running_file=DEFAULT_SIGNAL_RUNNING_FILE,
160                 signal_checkpoint_file=DEFAULT_SIGNAL_CHECKPOINT_FILE):
161        """Run system performance loggers until stopped or timeout occurs.
162
163        Temporal data logs are written to
164        <test_results>/{power,cpu,temp,fan_rpm}_results_<timestamp>_raw.txt
165
166        If result_dir points to a valid filesystem path, post-processing of logs
167        will be performed and a more convenient temporal format will be saved in
168        the result_dir.
169
170        Args:
171            max_duration_minutes: Number defining the maximum running time of
172                the managed sub-test.
173            result_dir: Optional; String defining the location on the test
174                target where post-processed results from this sub-test should be
175                saved for retrieval by the managing test process. Set to None if
176                results output is not be created.
177            signal_running_file: Optional; String defining the location of the
178                'running' RPC flag file on the test target. Removal of this file
179                triggers the subtest to finish logging and stop gracefully.
180            signal_checkpoint_file: Optional; String defining the location of
181                the 'checkpoint' RPC flag file on the test target. Modifying
182                this file triggers the subtest to create a checkpoint with name
183                equal to the utf-8-encoded contents of the first-line and
184                optional alternative start time (in seconds since the epoch)
185                equal to the second line of the file.
186        """
187        # Initiailize test state
188        for f in (signal_running_file, signal_checkpoint_file):
189            remove_file_if_exists(f)
190
191        # Indicate 'running' state by touching a mutually-monitored file
192        try:
193            open(signal_running_file, 'w').close()
194            if not os.path.exists(signal_running_file):
195                raise RuntimeError(
196                    'Signal "running" file %s was not properly initiailized' %
197                    signal_running_file)
198        except:
199            logging.exception('Failed to set "running" state.')
200            raise
201
202        signal_running = MonitoredFile(signal_running_file)
203        logging.info('Monitoring "running" signal file: %s',
204                     signal_running_file)
205        signal_checkpoint = MonitoredFile(signal_checkpoint_file)
206        logging.info('Monitoring "checkpoint" signal file: %s',
207                     signal_checkpoint_file)
208
209        self.start_measurements()  # provided by power_Test class
210        time_start = time.time()
211        time_end = time_start + max_duration_minutes * 60.0
212        self._last_checkpoint_time = time_start
213        monitored_files = [signal_running, signal_checkpoint]
214        while time.time() < time_end:
215            for f in monitored_files:
216                f.update()
217
218            if signal_checkpoint.exists() and signal_checkpoint.modified():
219                try:
220                    checkpoint_name, checkpoint_start_time = \
221                    self._read_checkpoint_file(signal_checkpoint_file)
222                except ValueError as err:
223                    logging.exception(err)
224                    raise error.TestFail(
225                        'Error while converting the checkpoint start time '
226                        'string to a float.' % signal_checkpoint_file)
227                self.checkpoint_measurements(checkpoint_name,
228                                             checkpoint_start_time)
229
230            if signal_running.deleted():
231                logging.info('Signaled to stop by the managing test process')
232                break
233
234            time.sleep(1)
235
236        self.checkpoint_measurements('default')
237
238        # Rely on managing test to create/cleanup result_dir
239        if result_dir:
240            # TODO(ryanneph): Implement structured log output for raw power_Test
241            # log files
242            with open(
243                    os.path.join(result_dir, 'graphics_Power_test_output.txt'),
244                    'w') as f:
245                f.write('{}\n')
246
247        # Cleanup our test state from the filesystem.
248        for f in (signal_running_file, signal_checkpoint_file):
249            remove_file_if_exists(f)
250