xref: /aosp_15_r20/external/autotest/client/cros/power/power_test.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright 2018 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Liimport logging
6*9c5db199SXin Liimport re
7*9c5db199SXin Liimport time
8*9c5db199SXin Li
9*9c5db199SXin Lifrom autotest_lib.client.bin import test
10*9c5db199SXin Lifrom autotest_lib.client.bin import utils
11*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
12*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import arc_common
13*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import retry
14*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros.network import interface
15*9c5db199SXin Lifrom autotest_lib.client.cros import service_stopper
16*9c5db199SXin Lifrom autotest_lib.client.cros.camera import camera_utils
17*9c5db199SXin Lifrom autotest_lib.client.cros.power import force_discharge_utils
18*9c5db199SXin Lifrom autotest_lib.client.cros.power import power_dashboard
19*9c5db199SXin Lifrom autotest_lib.client.cros.power import power_status
20*9c5db199SXin Lifrom autotest_lib.client.cros.power import power_telemetry_utils
21*9c5db199SXin Lifrom autotest_lib.client.cros.power import power_utils
22*9c5db199SXin Lifrom autotest_lib.client.cros.video import histogram_verifier
23*9c5db199SXin Li
24*9c5db199SXin Liclass power_Test(test.test):
25*9c5db199SXin Li    """Optional base class power related tests."""
26*9c5db199SXin Li    version = 1
27*9c5db199SXin Li
28*9c5db199SXin Li    keypress_histogram = 'Event.Latency.EndToEnd.KeyPress'
29*9c5db199SXin Li    histogram_re = 'Histogram: %s recorded (\d+) samples, mean = (\d+\.\d+)'
30*9c5db199SXin Li    hist_percentile_re = '^(\d+).+\{(\d+)\.\d+\%\}'
31*9c5db199SXin Li
32*9c5db199SXin Li    def initialize(self,
33*9c5db199SXin Li                   seconds_period=20.,
34*9c5db199SXin Li                   pdash_note='',
35*9c5db199SXin Li                   force_discharge='false',
36*9c5db199SXin Li                   check_network=False,
37*9c5db199SXin Li                   run_arc=True):
38*9c5db199SXin Li        """Perform necessary initialization prior to power test run.
39*9c5db199SXin Li
40*9c5db199SXin Li        @param seconds_period: float of probing interval in seconds.
41*9c5db199SXin Li        @param pdash_note: note of the current run to send to power dashboard.
42*9c5db199SXin Li        @param force_discharge: string of whether to tell ec to discharge
43*9c5db199SXin Li                battery even when the charger is plugged in. 'false' means no
44*9c5db199SXin Li                forcing discharge; 'true' means forcing discharge and raising an
45*9c5db199SXin Li                error when it fails; 'optional' means forcing discharge when
46*9c5db199SXin Li                possible but not raising an error when it fails, which is more
47*9c5db199SXin Li                friendly to devices without a battery.
48*9c5db199SXin Li        @param check_network: check that Ethernet interface is not running.
49*9c5db199SXin Li        @param run_arc: bool, whether to run with ARC (if available)
50*9c5db199SXin Li
51*9c5db199SXin Li        @var backlight: power_utils.Backlight object.
52*9c5db199SXin Li        @var keyvals: dictionary of result keyvals.
53*9c5db199SXin Li        @var status: power_status.SysStat object.
54*9c5db199SXin Li
55*9c5db199SXin Li        @var _checkpoint_logger: power_status.CheckpointLogger to track
56*9c5db199SXin Li                                 checkpoint data.
57*9c5db199SXin Li        @var _psr: power_utils.DisplayPanelSelfRefresh object to monitor PSR.
58*9c5db199SXin Li        @var _services: service_stopper.ServiceStopper object.
59*9c5db199SXin Li        @var _start_time: float of time in seconds since Epoch test started.
60*9c5db199SXin Li        @var _stats: power_status.StatoMatic object.
61*9c5db199SXin Li        @var _meas_logs: list of power_status.MeasurementLoggers
62*9c5db199SXin Li        """
63*9c5db199SXin Li        super(power_Test, self).initialize()
64*9c5db199SXin Li        self.keyvals = dict()
65*9c5db199SXin Li        self.status = power_status.get_status()
66*9c5db199SXin Li
67*9c5db199SXin Li        self._checkpoint_logger = power_status.CheckpointLogger()
68*9c5db199SXin Li        self._seconds_period = seconds_period
69*9c5db199SXin Li
70*9c5db199SXin Li        self._force_discharge_success = force_discharge_utils.process(
71*9c5db199SXin Li                force_discharge, self.status)
72*9c5db199SXin Li        self.backlight = power_utils.Backlight(
73*9c5db199SXin Li                force_battery=self._force_discharge_success)
74*9c5db199SXin Li        self.backlight.set_default()
75*9c5db199SXin Li
76*9c5db199SXin Li        ifaces = [iface for iface in interface.get_interfaces()
77*9c5db199SXin Li                if (not iface.is_wifi_device() and
78*9c5db199SXin Li                iface.name.startswith('eth'))]
79*9c5db199SXin Li        logging.debug('Ethernet interfaces include: %s',
80*9c5db199SXin Li                      str([iface.name for iface in ifaces]))
81*9c5db199SXin Li        for iface in ifaces:
82*9c5db199SXin Li            if check_network and iface.is_lower_up:
83*9c5db199SXin Li                raise error.TestError('Ethernet interface is active. '
84*9c5db199SXin Li                                      'Please remove Ethernet cable.')
85*9c5db199SXin Li
86*9c5db199SXin Li        self._psr = power_utils.DisplayPanelSelfRefresh()
87*9c5db199SXin Li        self._services = service_stopper.ServiceStopper(
88*9c5db199SXin Li                service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
89*9c5db199SXin Li        self._services.stop_services()
90*9c5db199SXin Li        self._stats = power_status.StatoMatic()
91*9c5db199SXin Li
92*9c5db199SXin Li        self._meas_logs = power_status.create_measurement_loggers(
93*9c5db199SXin Li                seconds_period, self._checkpoint_logger)
94*9c5db199SXin Li        logging.debug('measurement loggers (%d):', len(self._meas_logs))
95*9c5db199SXin Li        for log in self._meas_logs:
96*9c5db199SXin Li            logging.debug('%s: %s', type(log).__name__, ", ".join(log.domains))
97*9c5db199SXin Li
98*9c5db199SXin Li        self._pdash_note = pdash_note
99*9c5db199SXin Li        self._failure_messages = []
100*9c5db199SXin Li
101*9c5db199SXin Li        self._arc_mode = arc_common.ARC_MODE_DISABLED
102*9c5db199SXin Li        if run_arc and utils.is_arc_available():
103*9c5db199SXin Li            self._arc_mode = arc_common.ARC_MODE_ENABLED
104*9c5db199SXin Li        self.keyvals['arc_mode'] = self._arc_mode
105*9c5db199SXin Li
106*9c5db199SXin Li    def get_extra_browser_args_for_camera_test(self):
107*9c5db199SXin Li        """Return Chrome args for camera power test."""
108*9c5db199SXin Li        ret = [
109*9c5db199SXin Li                # No pop up to ask permission to record video.
110*9c5db199SXin Li                '--use-fake-ui-for-media-stream',
111*9c5db199SXin Li                # Allow 2 windows side by side.
112*9c5db199SXin Li                '--force-tablet-mode=clamshell',
113*9c5db199SXin Li                # Prefer using constant frame rate for camera streaming.
114*9c5db199SXin Li                '--enable-features=PreferConstantFrameRate',
115*9c5db199SXin Li                # Bypass HID detection for Chromebox / Chromebase.
116*9c5db199SXin Li                '--disable-hid-detection-on-oobe',
117*9c5db199SXin Li                # Disable test account info sync, eg. Wi-Fi credentials,
118*9c5db199SXin Li                # so that each test run does not remember info from last test
119*9c5db199SXin Li                # run.
120*9c5db199SXin Li                '--disable-sync'
121*9c5db199SXin Li        ]
122*9c5db199SXin Li
123*9c5db199SXin Li        # Use fake camera for DUT without camera, e.g. chromebox.
124*9c5db199SXin Li        if not camera_utils.has_builtin_or_vivid_camera():
125*9c5db199SXin Li            ret.append('--use-fake-device-for-media-stream')
126*9c5db199SXin Li            self.keyvals['use_fake_camera'] = 1
127*9c5db199SXin Li        else:
128*9c5db199SXin Li            self.keyvals['use_fake_camera'] = 0
129*9c5db199SXin Li        return ret
130*9c5db199SXin Li
131*9c5db199SXin Li    def warmup(self, warmup_time=30):
132*9c5db199SXin Li        """Warm up.
133*9c5db199SXin Li
134*9c5db199SXin Li        Wait between initialization and run_once for new settings to stabilize.
135*9c5db199SXin Li
136*9c5db199SXin Li        @param warmup_time: integer of seconds to warmup.
137*9c5db199SXin Li        """
138*9c5db199SXin Li        time.sleep(warmup_time)
139*9c5db199SXin Li
140*9c5db199SXin Li    def start_measurements(self):
141*9c5db199SXin Li        """Start measurements."""
142*9c5db199SXin Li        for log in self._meas_logs:
143*9c5db199SXin Li            log.start()
144*9c5db199SXin Li        self._start_time = time.time()
145*9c5db199SXin Li        if self.status.battery:
146*9c5db199SXin Li            self._start_energy = self.status.battery.energy
147*9c5db199SXin Li        self._keyvallogger = power_dashboard.KeyvalLogger(self._start_time)
148*9c5db199SXin Li        power_telemetry_utils.start_measurement()
149*9c5db199SXin Li
150*9c5db199SXin Li    def loop_sleep(self, loop, sleep_secs):
151*9c5db199SXin Li        """Jitter free sleep.
152*9c5db199SXin Li
153*9c5db199SXin Li        @param loop: integer of loop (1st is zero).
154*9c5db199SXin Li        @param sleep_secs: integer of desired sleep seconds.
155*9c5db199SXin Li        """
156*9c5db199SXin Li        next_time = self._start_time + (loop + 1) * sleep_secs
157*9c5db199SXin Li        time.sleep(next_time - time.time())
158*9c5db199SXin Li
159*9c5db199SXin Li    def checkpoint_measurements(self, name, start_time=None):
160*9c5db199SXin Li        """Checkpoint measurements.
161*9c5db199SXin Li
162*9c5db199SXin Li        @param name: string name of measurement being checkpointed.
163*9c5db199SXin Li        @param start_time: float of time in seconds since Epoch that
164*9c5db199SXin Li                measurements being checkpointed began.
165*9c5db199SXin Li        """
166*9c5db199SXin Li        if not start_time:
167*9c5db199SXin Li            start_time = self._start_time
168*9c5db199SXin Li        self.status.refresh()
169*9c5db199SXin Li        self._checkpoint_logger.checkpoint(name, start_time)
170*9c5db199SXin Li        self._psr.refresh()
171*9c5db199SXin Li
172*9c5db199SXin Li    @retry.retry(Exception, timeout_min=1, delay_sec=2)
173*9c5db199SXin Li    def collect_keypress_latency(self, cr):
174*9c5db199SXin Li        """Collect keypress latency information from Histograms.
175*9c5db199SXin Li
176*9c5db199SXin Li        @param cr: object, the Chrome instance
177*9c5db199SXin Li        """
178*9c5db199SXin Li
179*9c5db199SXin Li        keypress_histogram_end = histogram_verifier.get_histogram(
180*9c5db199SXin Li            cr, self.keypress_histogram)
181*9c5db199SXin Li        matches = re.search((self.histogram_re % self.keypress_histogram),
182*9c5db199SXin Li                            keypress_histogram_end)
183*9c5db199SXin Li
184*9c5db199SXin Li        if matches:
185*9c5db199SXin Li            count = int(matches.group(1))
186*9c5db199SXin Li            mean_latency = float(matches.group(2))
187*9c5db199SXin Li            logging.info('latency count %d mean %f', count, mean_latency)
188*9c5db199SXin Li            self.keyvals['keypress_cnt'] = count
189*9c5db199SXin Li            self.keyvals['keypress_latency_us_avg'] = mean_latency
190*9c5db199SXin Li            self.output_perf_value(description='keypress_cnt', value=count,
191*9c5db199SXin Li                                   higher_is_better=True)
192*9c5db199SXin Li            self.output_perf_value(description='keypress_latency_us_avg',
193*9c5db199SXin Li                                   value=mean_latency,
194*9c5db199SXin Li                                   higher_is_better=False)
195*9c5db199SXin Li            self._keyvallogger.add_item('keypress_cnt', count, 'point',
196*9c5db199SXin Li                                        'keypress')
197*9c5db199SXin Li            self._keyvallogger.add_item('keypress_latency_us_avg',
198*9c5db199SXin Li                                        mean_latency, 'point', 'keypress')
199*9c5db199SXin Li
200*9c5db199SXin Li        # Capture the first bucket >= 90th percentile
201*9c5db199SXin Li        for s in keypress_histogram_end.splitlines():
202*9c5db199SXin Li            matches = re.search((self.hist_percentile_re), s)
203*9c5db199SXin Li            if matches:
204*9c5db199SXin Li                lat = int(matches.group(1))
205*9c5db199SXin Li                perc = int(matches.group(2))
206*9c5db199SXin Li                if perc >= 90:
207*9c5db199SXin Li                    self.keyvals['keypress_latency_us_high'] = lat
208*9c5db199SXin Li                    self.keyvals['keypress_high_percentile'] = perc
209*9c5db199SXin Li                    self.output_perf_value(
210*9c5db199SXin Li                        description='keypress_latency_us_high', value=lat,
211*9c5db199SXin Li                        higher_is_better=False)
212*9c5db199SXin Li                    self.output_perf_value(
213*9c5db199SXin Li                        description='keypress_high_percentile', value=perc,
214*9c5db199SXin Li                        higher_is_better=False)
215*9c5db199SXin Li                    self._keyvallogger.add_item('keypress_latency_us_high',
216*9c5db199SXin Li                                                lat, 'point', 'keypress')
217*9c5db199SXin Li                    self._keyvallogger.add_item('keypress_high_percentile',
218*9c5db199SXin Li                                                perc, 'point', 'keypress')
219*9c5db199SXin Li                    break
220*9c5db199SXin Li
221*9c5db199SXin Li    def publish_keyvals(self):
222*9c5db199SXin Li        """Publish power result keyvals."""
223*9c5db199SXin Li        keyvals = self._stats.publish()
224*9c5db199SXin Li        keyvals['level_backlight_max'] = self.backlight.get_max_level()
225*9c5db199SXin Li        keyvals['level_backlight_current'] = self.backlight.get_level()
226*9c5db199SXin Li
227*9c5db199SXin Li        # record battery stats if battery exists
228*9c5db199SXin Li        keyvals['b_on_ac'] = int(not self._force_discharge_success
229*9c5db199SXin Li                                 and self.status.on_ac())
230*9c5db199SXin Li        keyvals['force_discharge'] = int(self._force_discharge_success)
231*9c5db199SXin Li        for key in [
232*9c5db199SXin Li                'b_on_ac', 'force_discharge', 'percent_usb_suspended_time'
233*9c5db199SXin Li        ]:
234*9c5db199SXin Li            self._keyvallogger.add_item(key, keyvals[key], 'point', 'perf')
235*9c5db199SXin Li
236*9c5db199SXin Li        if self.status.battery:
237*9c5db199SXin Li            keyvals['ah_charge_full'] = self.status.battery.charge_full
238*9c5db199SXin Li            keyvals['ah_charge_full_design'] = \
239*9c5db199SXin Li                                self.status.battery.charge_full_design
240*9c5db199SXin Li            keyvals['ah_charge_now'] = self.status.battery.charge_now
241*9c5db199SXin Li            keyvals['a_current_now'] = self.status.battery.current_now
242*9c5db199SXin Li
243*9c5db199SXin Li            keyvals['wh_energy_full'] = self.status.battery.energy_full
244*9c5db199SXin Li            keyvals['wh_energy_full_design'] = \
245*9c5db199SXin Li                                self.status.battery.energy_full_design
246*9c5db199SXin Li            keyvals['wh_energy_now'] = self.status.battery.energy
247*9c5db199SXin Li            keyvals['wh_energy_start'] = self._start_energy
248*9c5db199SXin Li            energy_used = self._start_energy - self.status.battery.energy
249*9c5db199SXin Li            runtime_minutes = (time.time() - self._start_time) / 60.
250*9c5db199SXin Li            keyvals['wh_energy_used'] = energy_used
251*9c5db199SXin Li            keyvals['minutes_tested'] = runtime_minutes
252*9c5db199SXin Li            self._keyvallogger.add_item('minutes_tested',
253*9c5db199SXin Li                                        keyvals['minutes_tested'], 'point',
254*9c5db199SXin Li                                        'perf')
255*9c5db199SXin Li
256*9c5db199SXin Li            low_batt = power_utils.get_low_battery_shutdown_percent()
257*9c5db199SXin Li            keyvals['percent_sys_low_battery'] = low_batt
258*9c5db199SXin Li
259*9c5db199SXin Li            if energy_used > 0 and runtime_minutes > 1:
260*9c5db199SXin Li                keyvals['w_energy_rate'] = energy_used * 60. / runtime_minutes
261*9c5db199SXin Li                self._keyvallogger.add_item('w_energy_rate',
262*9c5db199SXin Li                                            keyvals['w_energy_rate'], 'point',
263*9c5db199SXin Li                                            'perf')
264*9c5db199SXin Li                energy_avail = self.status.battery.energy_full_design * \
265*9c5db199SXin Li                    ((100. - low_batt) / 100.)
266*9c5db199SXin Li                keyvals['minutes_battery_life'] = energy_avail / energy_used * \
267*9c5db199SXin Li                    runtime_minutes
268*9c5db199SXin Li                self._keyvallogger.add_item('minutes_battery_life',
269*9c5db199SXin Li                                            keyvals['minutes_battery_life'],
270*9c5db199SXin Li                                            'point', 'perf')
271*9c5db199SXin Li                keyvals['hours_battery_life'] = \
272*9c5db199SXin Li                    keyvals['minutes_battery_life'] / 60.
273*9c5db199SXin Li                self._keyvallogger.add_item('hours_battery_life',
274*9c5db199SXin Li                                            keyvals['hours_battery_life'],
275*9c5db199SXin Li                                            'point', 'perf')
276*9c5db199SXin Li
277*9c5db199SXin Li            keyvals['v_voltage_min_design'] = \
278*9c5db199SXin Li                                self.status.battery.voltage_min_design
279*9c5db199SXin Li            keyvals['v_voltage_now'] = self.status.battery.voltage_now
280*9c5db199SXin Li
281*9c5db199SXin Li        for log in self._meas_logs:
282*9c5db199SXin Li            keyvals.update(log.calc())
283*9c5db199SXin Li        keyvals.update(self._psr.get_keyvals())
284*9c5db199SXin Li
285*9c5db199SXin Li        self.keyvals.update(keyvals)
286*9c5db199SXin Li
287*9c5db199SXin Li        core_keyvals = power_utils.get_core_keyvals(self.keyvals)
288*9c5db199SXin Li        self.write_perf_keyval(core_keyvals)
289*9c5db199SXin Li
290*9c5db199SXin Li    def publish_dashboard(self):
291*9c5db199SXin Li        """Report results to chromeperf & power dashboard."""
292*9c5db199SXin Li
293*9c5db199SXin Li        self.publish_keyvals()
294*9c5db199SXin Li
295*9c5db199SXin Li        # publish power values
296*9c5db199SXin Li        for key, values in self.keyvals.items():
297*9c5db199SXin Li            if key.endswith('pwr_avg'):
298*9c5db199SXin Li                self.output_perf_value(description=key, value=values, units='W',
299*9c5db199SXin Li                        higher_is_better=False, graph='power')
300*9c5db199SXin Li
301*9c5db199SXin Li        # publish temperature values
302*9c5db199SXin Li        for key, values in self.keyvals.items():
303*9c5db199SXin Li            if key.endswith('temp_avg'):
304*9c5db199SXin Li                self.output_perf_value(description=key, value=values, units='C',
305*9c5db199SXin Li                        higher_is_better=False, graph='temperature')
306*9c5db199SXin Li
307*9c5db199SXin Li        # publish fps values
308*9c5db199SXin Li        for key, values in self.keyvals.items():
309*9c5db199SXin Li            if key.endswith('fps_avg'):
310*9c5db199SXin Li                self.output_perf_value(description=key, value=values,
311*9c5db199SXin Li                        units='fps', higher_is_better=True, graph='fps')
312*9c5db199SXin Li
313*9c5db199SXin Li        # include KeyvalLogger in dashboard
314*9c5db199SXin Li        self._meas_logs.append(self._keyvallogger)
315*9c5db199SXin Li
316*9c5db199SXin Li        # publish to power dashboard
317*9c5db199SXin Li        dashboard_factory = power_dashboard.get_dashboard_factory()
318*9c5db199SXin Li        for log in self._meas_logs:
319*9c5db199SXin Li            dashboard = dashboard_factory.createDashboard(log,
320*9c5db199SXin Li                self.tagged_testname, self.resultsdir, note=self._pdash_note)
321*9c5db199SXin Li            dashboard.upload()
322*9c5db199SXin Li
323*9c5db199SXin Li    def _save_results(self):
324*9c5db199SXin Li        """Save results of each logger in resultsdir."""
325*9c5db199SXin Li        for log in self._meas_logs:
326*9c5db199SXin Li            log.save_results(self.resultsdir)
327*9c5db199SXin Li        self._checkpoint_logger.save_checkpoint_data(self.resultsdir)
328*9c5db199SXin Li
329*9c5db199SXin Li    def postprocess_iteration(self):
330*9c5db199SXin Li        """Write keyval and send data to dashboard."""
331*9c5db199SXin Li        power_telemetry_utils.end_measurement()
332*9c5db199SXin Li        self.status.refresh()
333*9c5db199SXin Li        for log in self._meas_logs:
334*9c5db199SXin Li            log.done = True
335*9c5db199SXin Li        super(power_Test, self).postprocess_iteration()
336*9c5db199SXin Li        self.publish_dashboard()
337*9c5db199SXin Li        self._save_results()
338*9c5db199SXin Li        power_dashboard.generate_parallax_report(self.outputdir)
339*9c5db199SXin Li        if self._failure_messages:
340*9c5db199SXin Li            raise error.TestFail('Test has failed with messages: %s' %
341*9c5db199SXin Li                                 self._failure_messages)
342*9c5db199SXin Li
343*9c5db199SXin Li    def cleanup(self):
344*9c5db199SXin Li        """Reverse setting change in initialization."""
345*9c5db199SXin Li        force_discharge_utils.restore(self._force_discharge_success)
346*9c5db199SXin Li        if self.backlight:
347*9c5db199SXin Li            self.backlight.restore()
348*9c5db199SXin Li        self._services.restore_services()
349*9c5db199SXin Li        super(power_Test, self).cleanup()
350