1"""Extracts media info metrics from media info data points.""" 2 3import collections 4import enum 5 6 7# Direction and type numbers map to constants in the DataPoint group in 8# callstats.proto 9class Direction(enum.Enum): 10 """ 11 Possible directions for media entries of a data point. 12 """ 13 SENDER = 0 14 RECEIVER = 1 15 BANDWIDTH_ESTIMATION = 2 16 17 18class MediaType(enum.Enum): 19 """ 20 Possible media types for media entries of a data point. 21 """ 22 AUDIO = 1 23 VIDEO = 2 24 DATA = 3 25 26 27TimestampedValues = collections.namedtuple('TimestampedValues', 28 ['TimestampEpochSeconds', 29 'ValueList']) 30 31 32class MediaInfoMetricsExtractor(object): 33 """ 34 Extracts media metrics from a list of raw media info data points. 35 36 Media info datapoints are expected to be dictionaries in the format returned 37 by cfm_facade.get_media_info_data_points. 38 """ 39 40 def __init__(self, data_points): 41 """ 42 Initializes with a set of data points. 43 44 @param data_points Data points as a list of dictionaries. Dictionaries 45 should be in the format returned by 46 cfm_facade.get_media_info_data_points. I.e., as returned when 47 querying the Browser Window for datapoints when the ExportMediaInfo 48 mod is active. 49 """ 50 self._data_points = data_points 51 52 def get_top_level_metric(self, name): 53 """ 54 Gets top level metrics. 55 56 Gets metrics that are global for one datapoint. I.e., metrics that 57 are not in the media list, such as CPU usage. 58 59 @param name Name of the metric. Names map to the names in the DataPoint 60 group in callstats.proto. 61 62 @returns A list with TimestampedValues. The ValueList is guaranteed to 63 not contain any None values. 64 65 @raises KeyError If any datapoint is missing a metric with the 66 specified name. The KeyError will contain 2 args. The first is the 67 key, the second the entire data_point dictionary. 68 """ 69 metrics = [] 70 for data_point in self._data_points: 71 value = _get_value(data_point, name) 72 if value is not None: 73 timestamp = data_point['timestamp'] 74 metrics.append(TimestampedValues(timestamp, [value])) 75 return metrics 76 77 def get_media_metric(self, 78 name, 79 direction=None, 80 media_type=None, 81 post_process_func=lambda x: x): 82 """ 83 Gets media metrics. 84 85 Gets metrics that are in the media part of the datapoint. A DataPoint 86 often contains many media entries, why there are multiple values for a 87 specific name. 88 89 @param name Name of the metric. Names map to the names in the DataPoint 90 group in callstats.proto. 91 @param direction: Only include metrics with this direction in the media 92 stream. See the Direction constants in this module. If None, all 93 directions are included. 94 @param media_type: Only include metrics with this media type in the 95 media stream. See the MediaType constants in this module. If None, 96 all media types are included. 97 @param post_process_func: Function that takes a list of values and 98 optionally transforms it, returning the updated list or a scalar 99 value. Default is to return the unmodified list. This method is 100 called for the list of values in the same datapoint. Example usage 101 is to sum the received audio bytes for all streams for one logged 102 line. 103 @raises KeyError If any data point matching the direction and 104 media_type filter is missing metrics with the specified name. The 105 KeyError will contain 2 args. The first is the key, the second the 106 media dictionary that is missing the key. 107 108 @returns A list with TimestampedValues. The ValueList is guaranteed to 109 not contain any None values. 110 """ 111 metrics = [] 112 for data_point in self._data_points: 113 timestamp = data_point['timestamp'] 114 values = [ 115 _get_value(media, name) 116 for media in data_point['media'] 117 if _media_matches(media, direction, media_type) 118 ] 119 # Filter None values and only add the metric if there is at least 120 # one value left. 121 values = [x for x in values if x is not None] 122 if values: 123 values = post_process_func(values) 124 values = values if isinstance(values, list) else [values] 125 metrics.append(TimestampedValues(timestamp, values)) 126 return metrics 127 128 129def _get_value(dictionary, key): 130 """ 131 Returns the value in the dictionary for the specified key. 132 133 The only difference of using this method over dictionary[key] is that the 134 KeyError raised here contains both the key and the dictionary. 135 136 @param dictionary The dictionary to lookup the key in. 137 @param key The key to lookup. 138 139 @raises KeyError If the key does not exist. The KeyError will contain 2 140 args. The first is the key, the second the entire dictionary. 141 """ 142 try: 143 return dictionary[key] 144 except KeyError: 145 raise KeyError(key, dictionary) 146 147def _media_matches(media, direction, media_type): 148 direction_match = (True if direction is None 149 else media['direction'] == direction.value) 150 type_match = (True if media_type is None 151 else media['mediatype'] == media_type.value) 152 return direction_match and type_match 153 154