• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
11from __future__ import division
12from __future__ import print_function
13
14import collections
15import optparse
16import os
17import sys
18
19import matplotlib.pyplot as plt
20import numpy
21
22import misc
23import pb_parse
24
25
26class RTPStatistics(object):
27  """Has methods for calculating and plotting RTP stream statistics."""
28
29  BANDWIDTH_SMOOTHING_WINDOW_SIZE = 10
30  PLOT_RESOLUTION_MS = 50
31
32  def __init__(self, data_points):
33    """Initializes object with data_points and computes simple statistics.
34
35    Computes percentages of number of packets and packet sizes by
36    SSRC.
37
38    Args:
39        data_points: list of pb_parse.DataPoints on which statistics are
40            calculated.
41
42    """
43
44    self.data_points = data_points
45    self.ssrc_frequencies = misc.NormalizeCounter(
46        collections.Counter([pt.ssrc for pt in self.data_points]))
47    self.ssrc_size_table = misc.SsrcNormalizedSizeTable(self.data_points)
48    self.bandwidth_kbps = None
49    self.smooth_bw_kbps = None
50
51  def PrintHeaderStatistics(self):
52    print("{:>6}{:>14}{:>14}{:>6}{:>6}{:>3}{:>11}".format(
53        "SeqNo", "TimeStamp", "SendTime", "Size", "PT", "M", "SSRC"))
54    for point in self.data_points:
55      print("{:>6}{:>14}{:>14}{:>6}{:>6}{:>3}{:>11}".format(
56          point.sequence_number, point.timestamp,
57          int(point.arrival_timestamp_ms), point.size, point.payload_type,
58          point.marker_bit, "0x{:x}".format(point.ssrc)))
59
60  def PrintSsrcInfo(self, ssrc_id, ssrc):
61    """Prints packet and size statistics for a given SSRC.
62
63    Args:
64        ssrc_id: textual identifier of SSRC printed beside statistics for it.
65        ssrc: SSRC by which to filter data and display statistics
66    """
67    filtered_ssrc = [point for point in self.data_points if point.ssrc
68                     == ssrc]
69    payloads = misc.NormalizeCounter(
70        collections.Counter([point.payload_type for point in
71                             filtered_ssrc]))
72
73    payload_info = "payload type(s): {}".format(
74        ", ".join(str(payload) for payload in  payloads))
75    print("{} 0x{:x} {}, {:.2f}% packets, {:.2f}% data".format(
76        ssrc_id, ssrc, payload_info, self.ssrc_frequencies[ssrc] * 100,
77        self.ssrc_size_table[ssrc] * 100))
78    print("  packet sizes:")
79    (bin_counts, bin_bounds) = numpy.histogram([point.size for point in
80                                                filtered_ssrc], bins=5,
81                                               density=False)
82    bin_proportions = bin_counts / sum(bin_counts)
83    print("\n".join([
84        " {:.1f} - {:.1f}: {:.2f}%".format(bin_bounds[i], bin_bounds[i + 1],
85                                           bin_proportions[i] * 100)
86        for i in range(len(bin_proportions))
87    ]))
88
89  def ChooseSsrc(self):
90    """Queries user for SSRC."""
91
92    if len(self.ssrc_frequencies) == 1:
93      chosen_ssrc = self.ssrc_frequencies.keys()[0]
94      self.PrintSsrcInfo("", chosen_ssrc)
95      return chosen_ssrc
96
97    ssrc_is_incoming = misc.SsrcDirections(self.data_points)
98    incoming = [ssrc for ssrc in ssrc_is_incoming if ssrc_is_incoming[ssrc]]
99    outgoing = [ssrc for ssrc in ssrc_is_incoming if not ssrc_is_incoming[ssrc]]
100
101    print("\nIncoming:\n")
102    for (i, ssrc) in enumerate(incoming):
103      self.PrintSsrcInfo(i, ssrc)
104
105    print("\nOutgoing:\n")
106    for (i, ssrc) in enumerate(outgoing):
107      self.PrintSsrcInfo(i + len(incoming), ssrc)
108
109    while True:
110      chosen_index = int(misc.get_input("choose one> "))
111      if 0 <= chosen_index < len(self.ssrc_frequencies):
112        return (incoming + outgoing)[chosen_index]
113      else:
114        print("Invalid index!")
115
116  def FilterSsrc(self, chosen_ssrc):
117    """Filters and wraps data points.
118
119    Removes data points with `ssrc != chosen_ssrc`. Unwraps sequence
120    numbers and timestamps for the chosen selection.
121    """
122    self.data_points = [point for point in self.data_points if
123                        point.ssrc == chosen_ssrc]
124    unwrapped_sequence_numbers = misc.Unwrap(
125        [point.sequence_number for point in self.data_points], 2**16 - 1)
126    for (data_point, sequence_number) in zip(self.data_points,
127                                             unwrapped_sequence_numbers):
128      data_point.sequence_number = sequence_number
129
130    unwrapped_timestamps = misc.Unwrap([point.timestamp for point in
131                                        self.data_points], 2**32 - 1)
132
133    for (data_point, timestamp) in zip(self.data_points,
134                                       unwrapped_timestamps):
135      data_point.timestamp = timestamp
136
137  def PrintSequenceNumberStatistics(self):
138    seq_no_set = set(point.sequence_number for point in
139                     self.data_points)
140    missing_sequence_numbers = max(seq_no_set) - min(seq_no_set) + (
141        1 - len(seq_no_set))
142    print("Missing sequence numbers: {} out of {}  ({:.2f}%)".format(
143        missing_sequence_numbers,
144        len(seq_no_set),
145        100 * missing_sequence_numbers / len(seq_no_set)
146    ))
147    print("Duplicated packets: {}".format(len(self.data_points) -
148                                          len(seq_no_set)))
149    print("Reordered packets: {}".format(
150        misc.CountReordered([point.sequence_number for point in
151                             self.data_points])))
152
153  def EstimateFrequency(self, always_query_sample_rate):
154    """Estimates frequency and updates data.
155
156    Guesses the most probable frequency by looking at changes in
157    timestamps (RFC 3550 section 5.1), calculates clock drifts and
158    sending time of packets. Updates `self.data_points` with changes
159    in delay and send time.
160    """
161    delta_timestamp = (self.data_points[-1].timestamp -
162                       self.data_points[0].timestamp)
163    delta_arr_timestamp = float((self.data_points[-1].arrival_timestamp_ms -
164                                 self.data_points[0].arrival_timestamp_ms))
165    freq_est = delta_timestamp / delta_arr_timestamp
166
167    freq_vec = [8, 16, 32, 48, 90]
168    freq = None
169    for f in freq_vec:
170      if abs((freq_est - f) / f) < 0.05:
171        freq = f
172
173    print("Estimated frequency: {:.3f}kHz".format(freq_est))
174    if freq is None or always_query_sample_rate:
175      if not always_query_sample_rate:
176        print ("Frequency could not be guessed.", end=" ")
177      freq = int(misc.get_input("Input frequency (in kHz)> "))
178    else:
179      print("Guessed frequency: {}kHz".format(freq))
180
181    for point in self.data_points:
182      point.real_send_time_ms = (point.timestamp -
183                                 self.data_points[0].timestamp) / freq
184      point.delay = point.arrival_timestamp_ms - point.real_send_time_ms
185
186  def PrintDurationStatistics(self):
187    """Prints delay, clock drift and bitrate statistics."""
188
189    min_delay = min(point.delay for point in self.data_points)
190
191    for point in self.data_points:
192      point.absdelay = point.delay - min_delay
193
194    stream_duration_sender = self.data_points[-1].real_send_time_ms / 1000
195    print("Stream duration at sender: {:.1f} seconds".format(
196        stream_duration_sender
197    ))
198
199    arrival_timestamps_ms = [point.arrival_timestamp_ms for point in
200                             self.data_points]
201    stream_duration_receiver = (max(arrival_timestamps_ms) -
202                                min(arrival_timestamps_ms)) / 1000
203    print("Stream duration at receiver: {:.1f} seconds".format(
204        stream_duration_receiver
205    ))
206
207    print("Clock drift: {:.2f}%".format(
208        100 * (stream_duration_receiver / stream_duration_sender - 1)
209    ))
210
211    total_size = sum(point.size for point in self.data_points) * 8 / 1000
212    print("Send average bitrate: {:.2f} kbps".format(
213        total_size / stream_duration_sender))
214
215    print("Receive average bitrate: {:.2f} kbps".format(
216        total_size / stream_duration_receiver))
217
218  def RemoveReordered(self):
219    last = self.data_points[0]
220    data_points_ordered = [last]
221    for point in self.data_points[1:]:
222      if point.sequence_number > last.sequence_number and (
223          point.real_send_time_ms > last.real_send_time_ms):
224        data_points_ordered.append(point)
225        last = point
226    self.data_points = data_points_ordered
227
228  def ComputeBandwidth(self):
229    """Computes bandwidth averaged over several consecutive packets.
230
231    The number of consecutive packets used in the average is
232    BANDWIDTH_SMOOTHING_WINDOW_SIZE. Averaging is done with
233    numpy.correlate.
234    """
235    start_ms = self.data_points[0].real_send_time_ms
236    stop_ms = self.data_points[-1].real_send_time_ms
237    (self.bandwidth_kbps, _) = numpy.histogram(
238        [point.real_send_time_ms for point in self.data_points],
239        bins=numpy.arange(start_ms, stop_ms,
240                          RTPStatistics.PLOT_RESOLUTION_MS),
241        weights=[point.size * 8 / RTPStatistics.PLOT_RESOLUTION_MS
242                 for point in self.data_points]
243    )
244    correlate_filter = (numpy.ones(
245        RTPStatistics.BANDWIDTH_SMOOTHING_WINDOW_SIZE) /
246                        RTPStatistics.BANDWIDTH_SMOOTHING_WINDOW_SIZE)
247    self.smooth_bw_kbps = numpy.correlate(self.bandwidth_kbps, correlate_filter)
248
249  def PlotStatistics(self):
250    """Plots changes in delay and average bandwidth."""
251
252    start_ms = self.data_points[0].real_send_time_ms
253    stop_ms = self.data_points[-1].real_send_time_ms
254    time_axis = numpy.arange(start_ms / 1000, stop_ms / 1000,
255                             RTPStatistics.PLOT_RESOLUTION_MS / 1000)
256
257    delay = CalculateDelay(start_ms, stop_ms,
258                            RTPStatistics.PLOT_RESOLUTION_MS,
259                            self.data_points)
260
261    plt.figure(1)
262    plt.plot(time_axis, delay[:len(time_axis)])
263    plt.xlabel("Send time [s]")
264    plt.ylabel("Relative transport delay [ms]")
265
266    plt.figure(2)
267    plt.plot(time_axis[:len(self.smooth_bw_kbps)], self.smooth_bw_kbps)
268    plt.xlabel("Send time [s]")
269    plt.ylabel("Bandwidth [kbps]")
270
271    plt.show()
272
273
274def CalculateDelay(start, stop, step, points):
275  """Quantizes the time coordinates for the delay.
276
277  Quantizes points by rounding the timestamps downwards to the nearest
278  point in the time sequence start, start+step, start+2*step... Takes
279  the average of the delays of points rounded to the same. Returns
280  masked array, in which time points with no value are masked.
281
282  """
283  grouped_delays = [[] for _ in numpy.arange(start, stop + step, step)]
284  rounded_value_index = lambda x: int((x - start) / step)
285  for point in points:
286    grouped_delays[rounded_value_index(point.real_send_time_ms)
287                  ].append(point.absdelay)
288  regularized_delays = [numpy.average(arr) if arr else -1 for arr in
289                        grouped_delays]
290  return numpy.ma.masked_values(regularized_delays, -1)
291
292
293def main():
294  usage = "Usage: %prog [options] <filename of rtc event log>"
295  parser = optparse.OptionParser(usage=usage)
296  parser.add_option("--dump_header_to_stdout",
297                    default=False, action="store_true",
298                    help="print header info to stdout; similar to rtp_analyze")
299  parser.add_option("--query_sample_rate",
300                    default=False, action="store_true",
301                    help="always query user for real sample rate")
302
303  parser.add_option("--working_directory",
304                    default=None, action="store",
305                    help="directory in which to search for relative paths")
306
307  (options, args) = parser.parse_args()
308
309  if len(args) < 1:
310    parser.print_help()
311    sys.exit(0)
312
313  input_file = args[0]
314
315  if options.working_directory and not os.path.isabs(input_file):
316    input_file = os.path.join(options.working_directory, input_file)
317
318  data_points = pb_parse.ParseProtobuf(input_file)
319  rtp_stats = RTPStatistics(data_points)
320
321  if options.dump_header_to_stdout:
322    print("Printing header info to stdout.", file=sys.stderr)
323    rtp_stats.PrintHeaderStatistics()
324    sys.exit(0)
325
326  chosen_ssrc = rtp_stats.ChooseSsrc()
327  print("Chosen SSRC: 0X{:X}".format(chosen_ssrc))
328
329  rtp_stats.FilterSsrc(chosen_ssrc)
330
331  print("Statistics:")
332  rtp_stats.PrintSequenceNumberStatistics()
333  rtp_stats.EstimateFrequency(options.query_sample_rate)
334  rtp_stats.PrintDurationStatistics()
335  rtp_stats.RemoveReordered()
336  rtp_stats.ComputeBandwidth()
337  rtp_stats.PlotStatistics()
338
339if __name__ == "__main__":
340  main()
341