tcp: add cc stats plotting tools

Type: feature

cc_plot.py can plot cc stats collected via elogs from tcp connections.
To enable the collection of cc stats, tcp must be compiled with
TCP_DEBUG_CC_STAT turned on.

Once the tests have been carried out, and elogs have been saved with "ev
save log <file>", the logs can be converted to txt format with the
convert_evt script. The resulting file can be used as input to the
cc_plots script.

Change-Id: I16df2993fa09cab9ad35f91825eab7df4da6428b
Signed-off-by: Florin Coras <fcoras@cisco.com>
diff --git a/src/scripts/host-stack/cc_plots.py b/src/scripts/host-stack/cc_plots.py
new file mode 100755
index 0000000..ea2d4c9
--- /dev/null
+++ b/src/scripts/host-stack/cc_plots.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python
+
+import sys
+import re
+import argparse
+import matplotlib.pyplot as plt
+from matplotlib.lines import Line2D
+
+class Point():
+    "CC event"
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+
+def listx(points):
+  return list(map(lambda pt: pt.x, points))
+
+def listy(points):
+  return list(map(lambda pt: pt.y, points))
+
+def plot_data(d):
+
+  plt.figure(1)
+
+  cwndx = listx(d["cwnd"])
+  cwndy = listy(d["cwnd"])
+  congx = listx(d["congestion"])
+  congy = listy(d["congestion"])
+  rcvrdx = listx(d["recovered"])
+  rcvrdy = listy(d["recovered"])
+  rxttx = listx(d["rxtTimeout"])
+  rxtty = listy(d["rxtTimeout"])
+
+  # cwnd/ssthresh/cc events
+  plt.subplot(311)
+  plt.title("cwnd/ssthresh")
+  pcwnd = plt.plot(cwndx, cwndy, 'r')
+  psst = plt.plot(cwndx, d["ssthresh"], 'y-')
+  pcong = plt.plot(congx, congy,'yo')
+  precov = plt.plot(rcvrdx, rcvrdy,'co')
+  prxtt = plt.plot(rxttx, rxtty,'mo')
+
+  marker1 = Line2D(range(1), range(1), color="r")
+  marker2 = Line2D(range(1), range(1), color="y")
+  marker3 = Line2D(range(1), range(1), color="w", marker="o", markerfacecolor="y")
+  marker4 = Line2D(range(1), range(1), color="w", marker="o", markerfacecolor="c")
+  marker5 = Line2D(range(1), range(1), color="w", marker="o", markerfacecolor="m")
+  plt.legend((marker1, marker2, marker3, marker4, marker5),
+             ('cwnd', 'ssthresh', 'congestion', 'recovered', 'rxt-timeout'),
+             loc=4)
+  axes = plt.gca()
+  axes.set_ylim([-20e4, max(cwndy) + 20e4])
+
+  # snd variables
+  plt.subplot(312)
+  plt.title("cc variables")
+  plt.plot(cwndx, d["space"], 'g-', markersize=1)
+  plt.plot(cwndx, d["flight"], 'b-', markersize=1)
+  plt.plot(cwndx, d["sacked"], 'm:', markersize=1)
+  plt.plot(cwndx, d["lost"], 'y:', markersize=1)
+  plt.plot(cwndx, d["cc-space"], 'k:', markersize=1)
+  plt.plot(cwndx, cwndy, 'ro', markersize=2)
+
+  plt.plot(congx, congy, 'y^', markersize=10, markerfacecolor="y")
+  plt.plot(rcvrdx, rcvrdy, 'c^', markersize=10, markerfacecolor="c")
+  plt.plot(rxttx, rxtty, 'm^', markersize=10, markerfacecolor="m")
+
+  #plt.plot(cwndx, d["snd_wnd"], 'ko', markersize=1)
+  plt.legend(("snd-space", "flight", "sacked", "lost", "cc-space", "cwnd",
+              "congestion", "recovered", "rxt-timeout"),
+             loc=1)
+
+  # rto/srrt/rttvar
+  plt.subplot(313)
+  plt.title("rtt")
+  plt.plot(cwndx, d["srtt"], 'g-')
+  plt.plot(cwndx, [x/1000 for x in d["mrtt-us"]], 'r-')
+  plt.plot(cwndx, d["rttvar"], 'b-')
+  plt.legend(["srtt", "mrtt-us", "rttvar"])
+  axes = plt.gca()
+  #plt.plot(cwndx, rto, 'r-')
+  #axes.set_ylim([0, int(max(rto[2:len(rto)])) + 50])
+
+  # show
+  plt.show()
+
+def find_pattern(file_path,session_idx):
+    is_active_open = 1
+    listener_pattern = "l\[\d\]"
+    if (is_active_open):
+      initial_pattern = "\[\d\](\.\d+:\d+\->\.\d+:\d+)\s+open:\s"
+    else:
+      initial_pattern = "\[\d\](\.\d+:\d+\->\.\d+:\d+)\s"
+    idx = 0
+    f = open(file_path, 'r')
+    for line in f:
+      # skip listener lines (server)
+      if (re.search(listener_pattern, line) != None):
+        continue
+      match = re.search(initial_pattern, line)
+      if (match == None):
+        continue
+      if (idx < session_idx):
+        idx += 1
+        continue
+      filter_pattern = str(match.group(1)) + "\s+(.+)"
+      print ("pattern is %s" % filter_pattern)
+      f.close()
+      return filter_pattern
+    raise Exception ("Could not find initial pattern")
+
+def compute_time(min, sec, msec):
+  return int(min)*60 + int(sec) + int(msec)/1000.0
+
+def run(file_path, session_idx):
+    filter_sessions = 1
+    filter_pattern = ""
+
+    patterns = {
+      "time"      : "^\d+:(\d+):(\d+):(\d+):\d+",
+      "listener"  : "l\[\d\]",
+      "cc"        : "cwnd (\d+) flight (\d+) space (\d+) ssthresh (\d+) snd_wnd (\d+)",
+      "cc-snd"    : "cc_space (\d+) sacked (\d+) lost (\d+)",
+      "rtt"       : "rto (\d+) srtt (\d+) mrtt-us (\d+) rttvar (\d+)",
+      "rxtt"      : "rxt-timeout",
+      "congestion": "congestion",
+      "recovered" : "recovered",
+    }
+    d = {
+      "cwnd"        : [],
+      "space"       : [],
+      "flight"      : [],
+      "ssthresh"    : [],
+      "snd_wnd"     : [],
+      "cc-space"    : [],
+      "lost"        : [],
+      "sacked"      : [],
+      "rto"         : [],
+      "srtt"        : [],
+      "mrtt-us"     : [],
+      "rttvar"      : [],
+      "rxtTimeout"  : [],
+      "congestion"  : [],
+      "recovered"   : [],
+    }
+
+    if (filter_sessions):
+        filter_pattern = find_pattern(file_path, session_idx)
+    f = open(file_path, 'r')
+
+    stats_index = 0
+    start_time = 0
+
+    for line in f:
+        # skip listener lines (server)
+        if (re.search(patterns["listener"], line) != None):
+            continue
+        # filter sessions
+        if (filter_sessions):
+            match = re.search(filter_pattern, line)
+            if (match == None):
+                continue
+
+        original_line = line
+        line = match.group(1)
+        match = re.search (patterns["time"], original_line)
+        if (match == None):
+          print "something went wrong! no time!"
+          continue
+        time = compute_time (match.group(1), match.group(2), match.group(3))
+        if (start_time == 0):
+          start_time = time
+
+        time = time - start_time
+        match = re.search(patterns["cc"], line)
+        if (match != None):
+          d["cwnd"].append(Point(time, int(match.group(1))))
+          d["flight"].append(int(match.group(2)))
+          d["space"].append(int(match.group(3)))
+          d["ssthresh"].append(int(match.group(4)))
+          d["snd_wnd"].append(int(match.group(5)))
+          stats_index += 1
+          continue
+        match = re.search(patterns["cc-snd"], line)
+        if (match != None):
+          d["cc-space"].append(int(match.group(1)))
+          d["sacked"].append(int(match.group(2)))
+          d["lost"].append(int(match.group(3)))
+        match = re.search(patterns["rtt"], line)
+        if (match != None):
+           d["rto"].append(int(match.group(1)))
+           d["srtt"].append(int(match.group(2)))
+           d["mrtt-us"].append(int(match.group(3)))
+           d["rttvar"].append(int(match.group(4)))
+        if (stats_index == 0):
+           continue
+        match = re.search(patterns["rxtt"], line)
+        if (match != None):
+          d["rxtTimeout"].append(Point(time, d["cwnd"][stats_index - 1].y + 1e4))
+          continue
+        match = re.search(patterns["congestion"], line)
+        if (match != None):
+          d["congestion"].append(Point(time, d["cwnd"][stats_index - 1].y - 1e4))
+          continue
+        match = re.search(patterns["recovered"], line)
+        if (match != None):
+          d["recovered"].append(Point(time, d["cwnd"][stats_index - 1].y))
+          continue
+
+    plot_data(d)
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description="Plot tcp cc logs")
+    parser.add_argument('-f', action='store', dest='file', required=True,
+                        help="elog file in txt format")
+    parser.add_argument('-s', action='store', dest='session_index', default=0,
+                        help="session index for which to plot cc logs" )
+    results = parser.parse_args()
+    run(results.file, int(results.session_index))