Chromium Code Reviews

Side by Side Diff: tools/py_event_log_analyzer/rtp_analyzer.py

Issue 1999113002: New rtc dump analyzing tool in Python (Closed) Base URL: https://chromium.googlesource.com/external/webrtc.git@master
Patch Set: pylint issues Created 4 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff |
OLDNEW
(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()
OLDNEW
« tools/py_event_log_analyzer/pb_parse.py ('K') | « tools/py_event_log_analyzer/pb_parse.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine