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
|