Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. | |
| 2 # | |
| 3 # Use of this source code is governed by a BSD-style license | |
| 4 # that can be found in the LICENSE file in the root of the source | |
| 5 # tree. An additional intellectual property rights grant can be found | |
| 6 # in the file PATENTS. All contributing project authors may | |
| 7 # be found in the AUTHORS file in the root of the source tree. | |
| 8 | |
| 9 """Displays statistics and plots graphs from RTC protobuf dump.""" | |
| 10 | |
| 11 from __future__ import division | |
| 12 from __future__ import print_function | |
| 13 | |
| 14 import sys | |
| 15 import builtins | |
| 16 import matplotlib.pyplot as plt | |
| 17 import misc | |
| 18 import numpy | |
| 19 import pb_parse | |
| 20 | |
| 21 | |
| 22 class RTPStatistics(object): | |
| 23 """Acts as namespace for RTP statistics. | |
|
kwiberg-webrtc
2016/05/24 12:09:51
Modules and packages are Python's closest analogs
aleloi
2016/05/24 15:54:42
Changed. Still not really satisfied with the comme
| |
| 24 """ | |
| 25 | |
| 26 BANDWIDTH_SMOOTHING_WINDOW_SIZE = 10 | |
| 27 | |
| 28 def __init__(self, data_points): | |
| 29 """Initializes data_points and does computations. | |
|
ivoc
2016/05/24 11:50:18
I suggest merging this with the comment below, so:
kwiberg-webrtc
2016/05/24 12:09:51
This sounds misleading. __init__ doesn't do anythi
aleloi
2016/05/24 15:54:41
Hopefully a little better now.
aleloi
2016/05/24 15:54:41
Done.
| |
| 30 | |
| 31 Calculates statistics for number of packages and size of packages | |
|
ivoc
2016/05/24 11:50:18
package -> packet
aleloi
2016/05/24 15:54:41
Done.
| |
| 32 by SSRC. | |
| 33 | |
| 34 Args: | |
| 35 data_points: list of pb_parse.DataPoint:s on which statistics are | |
|
ivoc
2016/05/24 11:50:18
Remove the : in DataPoint:s please.
aleloi
2016/05/24 15:54:42
Done.
| |
| 36 calculated. | |
| 37 | |
| 38 """ | |
| 39 | |
| 40 # currently does nothing, because parse_protobuf() only returns RTP packages | |
|
ivoc
2016/05/24 11:50:18
Please start comment with a capital and end with a
aleloi
2016/05/24 15:54:41
Removed completely.
| |
| 41 no_rtcp_packages = [x for x in data_points if not 72 <= x.pt <= 76] | |
|
ivoc
2016/05/24 11:50:18
package -> packet, aren't these just the RTP packe
kwiberg-webrtc
2016/05/24 12:09:51
"packages" -> "packets" (twice)
Also, I don't und
aleloi
2016/05/24 15:54:41
removed, because RTCP were filtered in protobuf al
| |
| 42 num_rtcp = len(data_points) - len(no_rtcp_packages) | |
| 43 if num_rtcp > 0: | |
| 44 print("Removing {} RTCP packets".format(num_rtcp)) | |
| 45 else: | |
| 46 print("No RTCP packets present") | |
| 47 data_points = no_rtcp_packages | |
|
kwiberg-webrtc
2016/05/24 12:09:52
Please don't reuse variable names lightly. It make
| |
| 48 | |
| 49 self.data_points = data_points | |
| 50 self.ssrc_frequencies = misc.percent_table([x.ssrc for x in | |
| 51 self.data_points]) | |
| 52 self.ssrc_size_table = misc.ssrc_size_table(self.data_points) | |
| 53 self.bandwidth_kbps = None | |
| 54 self.smooth_bw_kbps = None | |
| 55 | |
| 56 def print_ssrc_info(self, ssrc_id, ssrc): | |
| 57 """Prints packet and size statistics for given SSRC. | |
|
kwiberg-webrtc
2016/05/24 12:09:51
Explain the other argument.
aleloi
2016/05/24 15:54:41
Done.
| |
| 58 | |
| 59 Raises: | |
| 60 Exception: when different payload types are present in data | |
| 61 for same SSRC | |
| 62 """ | |
| 63 filtered_ssrc = [x for x in self.data_points if x.ssrc == ssrc] | |
| 64 payloads = misc.percent_table([x.pt for x in filtered_ssrc]) | |
| 65 sizes = misc.percent_table([x.size for x in filtered_ssrc]) | |
| 66 | |
| 67 if len(payloads) == 1: | |
| 68 payload_info = "payload type {}".format(*list(payloads)) | |
| 69 else: | |
| 70 raise Exception( | |
| 71 "This tool cannot yet handle changes in codec sample rate") | |
| 72 print("{} 0X{:X} {}, {:.2f}% packets, {:.2f}% data".format( | |
| 73 ssrc_id, ssrc, payload_info, self.ssrc_frequencies[ssrc]*100, | |
| 74 self.ssrc_size_table[ssrc]*100)) | |
| 75 print(" package sizes:") | |
| 76 size_hists = misc.hists(sizes, 5) | |
| 77 print("\n".join([ | |
| 78 " {} - {}: {:.2f}%".format(size_interval[0], size_interval[1], | |
| 79 size_hists[size_interval]*100) | |
| 80 for size_interval in sorted(size_hists) | |
| 81 ])) | |
| 82 | |
| 83 def choose_ssrc(self): | |
| 84 """Queries user for SSRC.""" | |
| 85 ssrc_frequencies_lst = list(enumerate(self.ssrc_frequencies)) | |
| 86 | |
| 87 assert self.ssrc_frequencies | |
|
ivoc
2016/05/24 11:50:18
This should be at the top of the function.
aleloi
2016/05/24 15:54:41
Was not really needed, because constructor initial
| |
| 88 if len(self.ssrc_frequencies) == 1: | |
| 89 chosen_ssrc = self.ssrc_frequencies[0][-1] | |
| 90 self.print_ssrc_info("", chosen_ssrc) | |
| 91 return chosen_ssrc | |
| 92 | |
| 93 for i, ssrc in enumerate(self.ssrc_frequencies): | |
| 94 self.print_ssrc_info(i, ssrc) | |
| 95 chosen_index = None | |
| 96 while chosen_index is None: | |
| 97 chosen_index = int(builtins.input("choose one> ")) | |
| 98 if 0 <= chosen_index < len(ssrc_frequencies_lst): | |
| 99 chosen_ssrc = ssrc_frequencies_lst[chosen_index][-1] | |
| 100 else: | |
| 101 print("Invalid index!") | |
| 102 chosen_index = None | |
| 103 return chosen_ssrc | |
|
kwiberg-webrtc
2016/05/24 12:09:52
Hmm. Wouldn't it be simpler to do something like
aleloi
2016/05/24 15:54:41
Done.
| |
| 104 | |
| 105 def filter_ssrc(self, chosen_ssrc): | |
| 106 """Filters and wraps data points. | |
| 107 | |
| 108 Removes data points with `ssrc != chosen_ssrc`. Unwraps sequence | |
| 109 numbers and time stamps for the chosen selection. | |
| 110 """ | |
| 111 self.data_points = [x for x in self.data_points if x.ssrc == | |
| 112 chosen_ssrc] | |
| 113 data_points_seq_no_unwrap = misc.unwrap([x.seq_no for x in | |
| 114 self.data_points], | |
| 115 2**16-1) # 65535 | |
|
kwiberg-webrtc
2016/05/24 12:09:51
This comment is probably not that useful. You've p
aleloi
2016/05/24 15:54:41
Done.
| |
| 116 for i, seq_no_unwrap_value in enumerate(data_points_seq_no_unwrap): | |
| 117 self.data_points[i].seq_no = seq_no_unwrap_value | |
| 118 | |
| 119 data_points_time_stamp_unwrap = enumerate( | |
| 120 misc.unwrap([x.timestamp for x in self.data_points], | |
| 121 2**32-1)) # 4294967295 | |
|
kwiberg-webrtc
2016/05/24 12:09:52
Remove this comment too.
aleloi
2016/05/24 15:54:41
Done.
| |
| 122 for i, timestamp_unwrap_value in data_points_time_stamp_unwrap: | |
| 123 self.data_points[i].timestamp = timestamp_unwrap_value | |
|
kwiberg-webrtc
2016/05/24 12:09:51
You've placed enumerate outside the loop expressio
aleloi
2016/05/24 15:54:41
Done.
| |
| 124 | |
| 125 def print_seq_no_statistics(self): | |
|
ivoc
2016/05/24 11:50:18
rename to print_sequence_number_statistics
aleloi
2016/05/24 15:54:41
Done.
| |
| 126 sortseq_no = sorted(x.seq_no for x in self.data_points) | |
| 127 print("Missing sequence numbers: {} out of {}".format( | |
| 128 sortseq_no[-1] - sortseq_no[0] + 1 - len(set(sortseq_no)), | |
| 129 len(set(sortseq_no)) | |
|
kwiberg-webrtc
2016/05/24 12:09:51
You can get the min and max elements without sorti
aleloi
2016/05/24 15:54:41
Done.
| |
| 130 )) | |
| 131 print("Duplicated packets: {}".format(sortseq_no.count(0))) | |
|
kwiberg-webrtc
2016/05/24 12:09:51
How does this work? Doesn't this just count the nu
aleloi
2016/05/24 15:54:42
Yes, that was wrong. Fixed now!
| |
| 132 print("Reordered packets: {}".format( | |
| 133 misc.count_reordered([x.seq_no for x in self.data_points]))) | |
| 134 | |
| 135 def print_frequency_duration_statistics(self): | |
| 136 """Estimates frequency and prints related statistics. | |
| 137 | |
| 138 Guesses the most probable frequency by looking at changes in | |
| 139 timestamps (RFC 3550 section 5.1), calculates clock drifts and | |
| 140 sending time of packets. Updates `self.data_points` with changes | |
| 141 in delay and send time. | |
| 142 | |
| 143 """ | |
| 144 delta_timestamp = (self.data_points[-1].timestamp - | |
| 145 self.data_points[0].timestamp) | |
| 146 delta_arr_timestamp = float((self.data_points[-1].arrival_timestamp_ms - | |
| 147 self.data_points[0].arrival_timestamp_ms)) | |
| 148 fs_est = delta_timestamp / delta_arr_timestamp | |
| 149 | |
| 150 fs_vec = [8, 16, 32, 48, 90] # TODO(aleloi) 90 is a hack for video | |
| 151 fs = None | |
| 152 for f in fs_vec: | |
| 153 if abs((fs_est - f)/float(f)) < 0.05: | |
|
ivoc
2016/05/24 11:50:18
Why not just use the closest one to the estimated
aleloi
2016/05/24 15:54:41
To notify the user that something is odd when the
| |
| 154 fs = f | |
| 155 | |
| 156 print("Estimated frequency: {}".format(fs_est)) | |
| 157 print("Guessed frequency: {}".format(fs)) | |
| 158 | |
| 159 for f in self.data_points: | |
| 160 f.real_send_time_ms = (f.timestamp - | |
| 161 self.data_points[0].timestamp) / fs | |
| 162 f.delay = f.arrival_timestamp_ms - f.real_send_time_ms | |
| 163 | |
| 164 min_delay = min(f.delay for f in self.data_points) | |
| 165 | |
| 166 for f in self.data_points: | |
| 167 f.absdelay = f.delay - min_delay | |
| 168 | |
| 169 stream_duration_sender = self.data_points[-1].real_send_time_ms / 1000 | |
| 170 print("Stream duration at sender: {:.1f} seconds".format( | |
| 171 stream_duration_sender | |
| 172 )) | |
| 173 | |
| 174 stream_duration_receiver = (self.data_points[-1].arrival_timestamp_ms - | |
|
ivoc
2016/05/24 11:50:18
Packet reordering could make this incorrect, max/m
aleloi
2016/05/24 15:54:41
Done.
| |
| 175 self.data_points[0].arrival_timestamp_ms) / 1000 | |
| 176 print("Stream duration at receiver: {:.1f} seconds".format( | |
| 177 stream_duration_receiver | |
| 178 )) | |
| 179 | |
| 180 print("Clock drift: {:.2f}%".format( | |
| 181 100* (stream_duration_receiver / stream_duration_sender - 1) | |
| 182 )) | |
| 183 | |
| 184 print("Send average bitrate: {:.2f} kbps".format( | |
| 185 sum(x.size for x | |
| 186 in self.data_points) * 8 / stream_duration_sender / 1000)) | |
| 187 | |
| 188 print("Receive average bitrate: {:.2f} kbps".format( | |
| 189 sum(x.size | |
| 190 for x in self.data_points) * 8 / stream_duration_receiver / | |
| 191 1000)) | |
| 192 | |
| 193 def remove_reordered(self): | |
| 194 last = self.data_points[0] | |
| 195 data_points_ordered = [last] | |
| 196 for x in self.data_points[1:]: | |
| 197 if x.seq_no > last.seq_no and (x.real_send_time_ms > | |
| 198 last.real_send_time_ms): | |
| 199 data_points_ordered.append(x) | |
| 200 last = x | |
| 201 self.data_points = data_points_ordered | |
| 202 | |
| 203 def compute_bandwidth(self): | |
| 204 """Computes bandwidth averaged over several consecutive packets. | |
| 205 | |
| 206 The number of consecutive packets used in the average is | |
| 207 BANDWIDTH_SMOOTHING_WINDOW_SIZE. Averaging is done in numpy by a | |
| 208 FFT convolution. | |
|
ivoc
2016/05/24 11:50:18
I don't think numpy actually uses an FFT implement
aleloi
2016/05/24 15:54:41
Done.
| |
| 209 """ | |
| 210 self.bandwidth_kbps = [] | |
| 211 for i in range(len(self.data_points)-1): | |
| 212 self.bandwidth_kbps.append( | |
| 213 self.data_points[i].size*8 / (self.data_points[i+1].real_send_time_ms | |
| 214 - self.data_points[i].real_send_time_ms) | |
| 215 ) | |
| 216 convolve_filter = (numpy.ones( | |
| 217 RTPStatistics.BANDWIDTH_SMOOTHING_WINDOW_SIZE) / | |
| 218 RTPStatistics.BANDWIDTH_SMOOTHING_WINDOW_SIZE) | |
| 219 self.smooth_bw_kbps = numpy.convolve(self.bandwidth_kbps, convolve_filter) | |
|
ivoc
2016/05/24 11:50:18
Please use numpy.correlate here (and update commen
aleloi
2016/05/24 15:54:41
Done.
| |
| 220 | |
| 221 def plot_statistics(self): | |
| 222 """Plots changes in delay and average bandwidth.""" | |
| 223 plt.figure(1) | |
| 224 plt.plot([f.real_send_time_ms/1000 for f in self.data_points], | |
| 225 [f.absdelay for f in self.data_points]) | |
| 226 plt.xlabel("Send time [s]") | |
| 227 plt.ylabel("Relative transport delay [ms]") | |
| 228 | |
| 229 plt.figure(2) | |
| 230 plt.plot([f.real_send_time_ms / 1000 for f in | |
| 231 self.data_points][:len(self.smooth_bw_kbps)], | |
|
ivoc
2016/05/24 11:50:18
Formatting seems off here, please check.
aleloi
2016/05/24 15:54:41
No, it's right. pylint and the presubmit test woul
| |
| 232 self.smooth_bw_kbps[:len(self.data_points)]) | |
| 233 plt.xlabel("Send time [s]") | |
| 234 plt.ylabel("Bandwidth [kbps]") | |
| 235 | |
| 236 plt.show() | |
| 237 | |
| 238 | |
| 239 def main(): | |
| 240 | |
| 241 if len(sys.argv) < 2: | |
| 242 print("Usage: python rtp_analyzer.py <filename of rtc event log>") | |
| 243 sys.exit(0) | |
| 244 | |
| 245 data_points = pb_parse.parse_protobuf(sys.argv[1]) | |
| 246 rtp_stats = RTPStatistics(data_points) | |
| 247 chosen_ssrc = rtp_stats.choose_ssrc() | |
| 248 print("Chosen SSRC: 0X{:X}".format(chosen_ssrc)) | |
| 249 | |
| 250 rtp_stats.filter_ssrc(chosen_ssrc) | |
| 251 print("Statistics:") | |
| 252 rtp_stats.print_seq_no_statistics() | |
| 253 rtp_stats.print_frequency_duration_statistics() | |
| 254 rtp_stats.remove_reordered() | |
| 255 rtp_stats.compute_bandwidth() | |
| 256 rtp_stats.plot_statistics() | |
| 257 | |
| 258 if __name__ == "__main__": | |
| 259 main() | |
| OLD | NEW |