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 |