284 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			284 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
| # 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.
 | |
| import collections
 | |
| import enum
 | |
| import json
 | |
| import os
 | |
| import logging
 | |
| import time
 | |
| 
 | |
| from autotest_lib.client.common_lib import error
 | |
| from autotest_lib.client.common_lib.cros import chrome
 | |
| from autotest_lib.client.common_lib.cros import power_load_util
 | |
| from autotest_lib.client.cros.input_playback import keyboard
 | |
| from autotest_lib.client.cros.power import power_dashboard
 | |
| from autotest_lib.client.cros.power import power_status
 | |
| from autotest_lib.client.cros.power import power_test
 | |
| 
 | |
| 
 | |
| class power_MeetClient(power_test.power_Test):
 | |
|     """class for power_MeetClient test.
 | |
| 
 | |
|     This test should be call from power_MeetCall server test only.
 | |
|     """
 | |
|     version = 1
 | |
| 
 | |
|     video_url = 'http://meet.google.com'
 | |
|     doc_url = 'http://doc.new'
 | |
| 
 | |
|     def initialize(self,
 | |
|                    seconds_period=5.,
 | |
|                    pdash_note='',
 | |
|                    force_discharge=False):
 | |
|         """initialize method."""
 | |
|         super(power_MeetClient, self).initialize(
 | |
|                 seconds_period=seconds_period,
 | |
|                 pdash_note=pdash_note,
 | |
|                 force_discharge=force_discharge)
 | |
| 
 | |
|     def run_once(self,
 | |
|                  meet_code,
 | |
|                  duration=180,
 | |
|                  layout='Tiled',
 | |
|                  username=None,
 | |
|                  password=None):
 | |
|         """run_once method.
 | |
| 
 | |
|         @param meet_code: Meet code generated in power_MeetCall.
 | |
|         @param duration: duration in seconds.
 | |
|         @param layout: string of meet layout to use.
 | |
|         @param username: Google account to use.
 | |
|         @param password: password for Google account.
 | |
|         """
 | |
|         if not username and not password:
 | |
|             username = power_load_util.get_meet_username()
 | |
|             password = power_load_util.get_meet_password()
 | |
|         if not username or not password:
 | |
|             raise error.TestFail('Need to supply both username and password.')
 | |
|         extra_browser_args = self.get_extra_browser_args_for_camera_test()
 | |
|         with keyboard.Keyboard() as keys,\
 | |
|              chrome.Chrome(init_network_controller=True,
 | |
|                            gaia_login=True,
 | |
|                            username=username,
 | |
|                            password=password,
 | |
|                            extra_browser_args=extra_browser_args,
 | |
|                            autotest_ext=True) as cr:
 | |
| 
 | |
|             # Move existing window to left half and open video page
 | |
|             tab = cr.browser.tabs[0]
 | |
|             tab.Activate()
 | |
| 
 | |
|             # Run in full-screen.
 | |
|             fullscreen = tab.EvaluateJavaScript('document.webkitIsFullScreen')
 | |
|             if not fullscreen:
 | |
|                 keys.press_key('f4')
 | |
| 
 | |
|             url = self.video_url + '/' + meet_code
 | |
|             logging.info('Navigating left window to %s', url)
 | |
|             tab.Navigate(url)
 | |
| 
 | |
|             # Workaround when camera isn't init for some unknown reason.
 | |
|             time.sleep(10)
 | |
|             tab.EvaluateJavaScript('location.reload()')
 | |
| 
 | |
|             tab.WaitForDocumentReadyStateToBeComplete()
 | |
|             logging.info(meet_code)
 | |
|             self.keyvals['meet_code'] = meet_code
 | |
| 
 | |
|             def wait_until(cond, error_msg):
 | |
|                 """Helper for javascript polling wait."""
 | |
|                 for _ in range(60):
 | |
|                     time.sleep(1)
 | |
|                     if tab.EvaluateJavaScript(cond):
 | |
|                         return
 | |
|                 raise error.TestFail(error_msg)
 | |
| 
 | |
|             wait_until('window.hasOwnProperty("hrTelemetryApi")',
 | |
|                        'Meet API does not existed.')
 | |
|             wait_until('hrTelemetryApi.isInMeeting()',
 | |
|                        'Can not join meeting.')
 | |
|             wait_until('hrTelemetryApi.getParticipantCount() > 1',
 | |
|                        'Meeting has no other participant.')
 | |
| 
 | |
|             # Make sure camera and mic are on.
 | |
|             tab.EvaluateJavaScript('hrTelemetryApi.setCameraMuted(false)')
 | |
|             tab.EvaluateJavaScript('hrTelemetryApi.setMicMuted(false)')
 | |
| 
 | |
|             if layout == 'Tiled':
 | |
|                 tab.EvaluateJavaScript('hrTelemetryApi.setTiledLayout()')
 | |
|             elif layout == 'Auto':
 | |
|                 tab.EvaluateJavaScript('hrTelemetryApi.setAutoLayout()')
 | |
|             elif layout == 'Sidebar':
 | |
|                 tab.EvaluateJavaScript('hrTelemetryApi.setSidebarLayout()')
 | |
|             elif layout == 'Spotlight':
 | |
|                 tab.EvaluateJavaScript('hrTelemetryApi.setSpotlightLayout()')
 | |
|             else:
 | |
|                 raise error.TestError('Unknown layout %s' % layout)
 | |
| 
 | |
|             self.keyvals['layout'] = layout
 | |
| 
 | |
|             self.start_measurements()
 | |
|             time.sleep(duration)
 | |
|             end_time = self._start_time + duration
 | |
| 
 | |
|             # Collect stat
 | |
|             if not tab.EvaluateJavaScript('window.hasOwnProperty("realtime")'):
 | |
|                 logging.info('Account %s is not in allowlist for MediaInfoAPI',
 | |
|                              username)
 | |
|                 return
 | |
| 
 | |
|             meet_data = tab.EvaluateJavaScript(
 | |
|                 'realtime.media.getMediaInfoDataPoints()')
 | |
| 
 | |
|             power_dashboard.get_dashboard_factory().registerDataType(
 | |
|                 MeetStatLogger, MeetStatDashboard)
 | |
| 
 | |
|             self._meas_logs.append(
 | |
|                     MeetStatLogger(self._start_time, end_time, meet_data))
 | |
| 
 | |
| 
 | |
| class MeetStatLogger(power_status.MeasurementLogger):
 | |
|     """Class for logging meet data point to power dashboard.
 | |
| 
 | |
|     Format of meet_data http://google3/logs/proto/buzz/callstats.proto
 | |
|     """
 | |
| 
 | |
|     def __init__(self, start_ts, end_ts, meet_data):
 | |
|         # Do not call parent constructor to avoid making a new thread.
 | |
|         self.times = [start_ts]
 | |
| 
 | |
|         # Meet epoch timestamp uses millisec unit.
 | |
|         self.meet_data = [data_point for data_point in meet_data
 | |
|             if start_ts * 1000 <= data_point['timestamp'] <= end_ts * 1000]
 | |
| 
 | |
|     def calc(self, mtype=None):
 | |
|         return {}
 | |
| 
 | |
|     def save_results(self, resultsdir, fname_prefix=None):
 | |
|         # Save raw dict from meet to file. Ignore fname_prefix.
 | |
|         with open(os.path.join(resultsdir, 'meet_powerlog.json'), 'w') as f:
 | |
|             json.dump(self.meet_data , f, indent=4, separators=(',', ': '),
 | |
|                       ensure_ascii=False)
 | |
| 
 | |
| 
 | |
| class MeetStatDashboard(power_dashboard.MeasurementLoggerDashboard):
 | |
|     """Dashboard class for MeetStatLogger class."""
 | |
| 
 | |
|     # Direction and type numbers map to constants in the proto
 | |
|     class Direction(enum.IntEnum):
 | |
|         """Possible directions for media entries of a data point."""
 | |
|         SENDER = 0
 | |
|         RECEIVER = 1
 | |
| 
 | |
|     class MediaType(enum.IntEnum):
 | |
|         """Possible media types for media entries of a data point."""
 | |
|         VIDEO = 2
 | |
| 
 | |
|     # Important metrics to collect.
 | |
|     MEET_KEYS = [
 | |
|         'encodeUsagePercent',
 | |
|         'fps',
 | |
|         'height',
 | |
|         'width',
 | |
|     ]
 | |
| 
 | |
|     def _get_ssrc_dict(self, meet_data):
 | |
|         """ Extract http://what/ssrc for all video stream and map to string.
 | |
| 
 | |
|         The format of the string would be sender_# / receiver_# where # denotes
 | |
|         index for the video counting from 0.
 | |
| 
 | |
|         Returns:
 | |
|             dict from ssrc to video stream string.
 | |
|         """
 | |
|         ret = {}
 | |
|         count = [0, 0]
 | |
| 
 | |
|         # We only care about video streams.
 | |
|         for media in meet_data[-1]['media']:
 | |
|             if media['mediatype'] != self.MediaType.VIDEO:
 | |
|                 continue
 | |
|             if (media['direction'] != self.Direction.SENDER and
 | |
|                 media['direction'] != self.Direction.RECEIVER):
 | |
|                 continue
 | |
|             name = [media['directionStr'], str(count[media['direction']])]
 | |
|             if media['direction'] == self.Direction.SENDER:
 | |
|                 name.append(media['sendercodecname'])
 | |
|             else:
 | |
|                 name.append(media['receiverCodecName'])
 | |
|             count[media['direction']] += 1
 | |
|             ret[media['ssrc']] = '_'.join(name)
 | |
| 
 | |
|         return ret
 | |
| 
 | |
|     def _get_meet_unit(self, key):
 | |
|         """Return unit from name of the key."""
 | |
|         if key.endswith('fps'):
 | |
|             return 'fps'
 | |
|         if key.endswith('Percent'):
 | |
|             return 'percent'
 | |
|         if key.endswith('width') or key.endswith('height') :
 | |
|             return 'point'
 | |
|         raise error.TestError('Unexpected key: %s' % key)
 | |
| 
 | |
|     def _get_meet_type(self, key):
 | |
|         """Return type from name of the key."""
 | |
|         if key.endswith('fps'):
 | |
|             return 'meet_fps'
 | |
|         if key.endswith('Percent'):
 | |
|             return 'meet_encoder_load'
 | |
|         if key.endswith('width'):
 | |
|             return 'meet_width'
 | |
|         if key.endswith('height'):
 | |
|             return 'meet_height'
 | |
|         raise error.TestError('Unexpected key: %s' % key)
 | |
| 
 | |
|     def _convert(self):
 | |
|         """Convert meet raw dict to data to power dict."""
 | |
| 
 | |
|         meet_data = self._logger.meet_data
 | |
|         ssrc_dict = self._get_ssrc_dict(meet_data)
 | |
| 
 | |
|         # Dict from timestamp to dict of meet_key to value
 | |
|         parse_dict = collections.defaultdict(
 | |
|                      lambda: collections.defaultdict(int))
 | |
| 
 | |
|         key_set = set()
 | |
|         testname='power_MeetCall'
 | |
| 
 | |
|         for data_point in meet_data:
 | |
|             timestamp = data_point['timestamp']
 | |
|             for media in data_point['media']:
 | |
|                 ssrc = media.get('ssrc', 0)
 | |
|                 if ssrc not in ssrc_dict:
 | |
|                     continue
 | |
|                 name = ssrc_dict[media['ssrc']]
 | |
|                 for meet_key in self.MEET_KEYS:
 | |
|                     if meet_key not in media:
 | |
|                         continue
 | |
|                     key = '%s_%s' % (name, meet_key)
 | |
|                     key_set.add(key)
 | |
|                     parse_dict[timestamp][key] = media[meet_key]
 | |
| 
 | |
|         timestamps = sorted(parse_dict.keys())
 | |
|         sample_count = len(timestamps)
 | |
| 
 | |
|         powerlog_data = collections.defaultdict(list)
 | |
|         for ts in sorted(parse_dict.keys()):
 | |
|             for key in key_set:
 | |
|                 powerlog_data[key].append(parse_dict[ts][key])
 | |
| 
 | |
|         powerlog_dict =  {
 | |
|             'sample_count': sample_count,
 | |
|             'sample_duration': 1,
 | |
|             'average': {k: 1.0 * sum(v) / sample_count
 | |
|                         for k, v in powerlog_data.iteritems()},
 | |
|             'data': powerlog_data,
 | |
|             'unit': {k: self._get_meet_unit(k) for k in key_set},
 | |
|             'type': {k: self._get_meet_type(k) for k in key_set},
 | |
|             'checkpoint': [[testname]] * sample_count,
 | |
|         }
 | |
| 
 | |
|         return powerlog_dict
 |