Browse Source

Added graphing and processing files.

Steven Engler 5 years ago
parent
commit
ee0a3e2764
3 changed files with 692 additions and 0 deletions
  1. 73 0
      src/cpu_graph.py
  2. 412 0
      src/parse_measureme_logs.py
  3. 207 0
      src/plot_streams.py

+ 73 - 0
src/cpu_graph.py

@@ -0,0 +1,73 @@
+import sys
+import json
+import gzip
+import numpy as np
+import matplotlib.pylab as plt
+#
+def load_cpu_stats(path):
+    with gzip.GzipFile(path, 'r') as f:
+        return json.load(f)
+    #
+#
+def calculate_cpu_usage(initial, current):
+    """
+    Calculation adapted from: https://stackoverflow.com/questions/23367857/accurate-calculation-of-cpu-usage-given-in-percentage-in-linux/
+    """
+    #
+    initial_idle = initial['idle'] + initial['iowait']
+    current_idle = current['idle'] + current['iowait']
+    #
+    initial_non_idle = initial['user'] + initial['nice'] + initial['system'] + initial['irq'] + initial['softirq'] + initial['steal']
+    current_non_idle = current['user'] + current['nice'] + current['system'] + current['irq'] + current['softirq'] + current['steal']
+    #
+    initial_total = initial_idle + initial_non_idle
+    current_total = current_idle + current_non_idle
+    #
+    return (current_non_idle-initial_non_idle)/(current_total-initial_total)
+#
+def calculate_cpu_usage_continuous(stats):
+	cpu_usages = []
+	for i in range(len(stats)-1):
+		cpu_usages.append(calculate_cpu_usage(stats[i], stats[i+1]))
+	#
+	return cpu_usages
+#
+def parse_range_list(range_list_str):
+        '''
+        Take an input like '1-3,5,7-10' and return a list like [1,2,3,5,7,8,9,10].
+        '''
+        #
+        range_strings = range_list_str.split(',')
+        all_items = []
+        for range_str in range_strings:
+                if '-' in range_str:
+                        # in the form '12-34'
+                        range_ends = [int(x) for x in range_str.split('-')]
+                        assert(len(range_ends) == 2)
+                        all_items.extend(range(range_ends[0], range_ends[1]+1))
+                else:
+                        # just a number
+                        all_items.append(int(range_str))
+                #
+        #
+        return all_items
+#
+cpu_data = load_cpu_stats(sys.argv[1])
+#
+unused_cpus = [str(x) for x in parse_range_list('128-129,138-139,18-19,148-149,28-29,158-159,38-39,48-49,58-59,68-69,78-79,98-99,108-109,118-119')]
+numa_sets = [[0, 80, 1, 81], [10, 90, 11, 91], [20, 100, 21, 101], [30, 110, 31, 111], [40, 120, 41, 121], [50, 130, 51, 131], [60, 140, 61, 141], [70, 150, 71, 151], [2, 82, 3, 83], [12, 92, 13, 93], [22, 102, 23, 103], [32, 112, 33, 113], [42, 122, 43, 123], [52, 132, 53, 133], [62, 142, 63, 143], [72, 152, 73, 153], [4, 84, 5, 85], [14, 94, 15, 95], [24, 104, 25, 105], [34, 114, 35, 115], [44, 124, 45, 125], [54, 134, 55, 135], [64, 144, 65, 145], [74, 154, 75, 155], [6, 86, 7, 87], [16, 96, 17, 97], [26, 106, 27, 107], [36, 116, 37, 117], [46, 126, 47, 127], [56, 136, 57, 137], [66, 146, 67, 147], [76, 156, 77, 157], [8, 88, 9, 89]]
+#
+timestamps = (np.array(cpu_data['timestamps'][1:]) + np.array(cpu_data['timestamps'][:-1])) / 2
+cpu_usages = {int(cpu): np.array(calculate_cpu_usage_continuous(cpu_data['stats']['cpus'][cpu])) for cpu in cpu_data['stats']['cpus'] if cpu not in unused_cpus}
+tor_usages = [np.sum([cpu_usages[cpu] for cpu in x], axis=0)/4 for x in numa_sets]
+print(len(cpu_usages))
+print(len(tor_usages))
+#
+plt.figure()
+#
+for cpu in tor_usages:
+	plt.plot(timestamps, cpu*100)
+#
+plt.xlabel('Time (s)')
+plt.ylabel('CPU Usage (Average over 4 cores)')
+plt.show()

+ 412 - 0
src/parse_measureme_logs.py

@@ -0,0 +1,412 @@
+#!/usr/bin/python3
+#
+import itertools
+#
+def read_log(f):
+	events = {}
+	events['recv_edge_data'] = ('timestamp', 'cell_id', 'length')
+	events['send_edge_data'] = ('timestamp', 'cell_id', 'length')
+	events['recv_relay_cell'] = ('timestamp', 'cell_id', 'fingerprint', 'payload')
+	events['send_relay_cell'] = ('timestamp', 'cell_id', 'fingerprint', 'payload')
+	events['recv_sendme'] = ('timestamp', 'window_size')
+	events['send_sendme'] = ('timestamp', 'window_size')
+	#
+	log = {}
+	log['measureme_id'] = {}
+	log['fingerprint'] = None
+	#
+	def new_measureme_id(measureme_id):
+		log['measureme_id'][measureme_id] = {}
+		log['measureme_id'][measureme_id]['stream_id'] = {}
+		log['measureme_id'][measureme_id]['circuit'] = {}
+		log['measureme_id'][measureme_id]['circuit']['event'] = {}
+	#
+	def new_stream_id(measureme_id, stream_id):
+		log['measureme_id'][measureme_id]['stream_id'][stream_id] = {}
+		log['measureme_id'][measureme_id]['stream_id'][stream_id]['event'] = {}
+	#
+	def new_event(where, event):
+		where['event'][event] = {}
+		where['event'][event]['forward'] = {}
+		where['event'][event]['backward'] = {}
+		#
+		for value in events[event]:
+			where['event'][event]['forward'][value] = []
+			where['event'][event]['backward'][value] = []
+		#
+	#
+	for line in f:
+		line_values = [x.strip() for x in line.split(',')]
+		timestamp = float(line_values[0])
+		event = line_values[1].lower()
+		event_values = line_values[2:]
+		#
+		if event == 'fingerprint':
+			try:
+				(fingerprint,) = event_values
+			except Exception as e:
+				raise Exception('Trying to parse: {}'.format(event_values)) from e
+			#
+			try:
+				if int(fingerprint) == 0:
+					# if the fingerprint is printed as all zeroes, then it is an OP
+					fingerprint = None
+				#
+			except:
+				pass
+			#
+			log['fingerprint'] = fingerprint
+		elif event == 'recv_edge_data' or event == 'send_edge_data':
+			try:
+				(direction, measureme_id, stream_id, cell_id, length) = event_values
+				#
+				direction = direction.lower()
+				measureme_id = int(measureme_id)
+				stream_id = int(stream_id)
+				cell_id = int(cell_id)
+				length = int(length)
+			except Exception as e:
+				raise Exception('Trying to parse: {}'.format(event_values)) from e
+			#
+			if measureme_id not in log['measureme_id']:
+				new_measureme_id(measureme_id)
+			#
+			if stream_id not in log['measureme_id'][measureme_id]['stream_id']:
+				new_stream_id(measureme_id, stream_id)
+			#
+			where = log['measureme_id'][measureme_id]['stream_id'][stream_id]
+			#
+			if event not in where['event']:
+				new_event(where, event)
+			#
+			where = where['event'][event][direction]
+			#
+			where['timestamp'].append(timestamp)
+			where['cell_id'].append(cell_id)
+			where['length'].append(length)
+		elif event == 'recv_relay_cell' or event == 'send_relay_cell':
+			try:
+				(direction, measureme_id, cell_id, fingerprint, payload) = event_values
+				#
+				direction = direction.lower()
+				measureme_id = int(measureme_id)
+				cell_id = int(cell_id)
+				payload = int(payload)
+			except Exception as e:
+				raise Exception('Trying to parse: {}'.format(event_values)) from e
+			#
+			if measureme_id not in log['measureme_id']:
+				new_measureme_id(measureme_id)
+			#
+			where = log['measureme_id'][measureme_id]['circuit']
+			#
+			if event not in where['event']:
+				new_event(where, event)
+			#
+			where = where['event'][event][direction]
+			#
+			where['timestamp'].append(timestamp)
+			where['cell_id'].append(cell_id)
+			where['fingerprint'].append(fingerprint)
+			where['payload'].append(payload)
+		elif event == 'recv_sendme' or event == 'send_sendme':
+			try:
+				(direction, measureme_id, stream_id, window_size) = event_values
+				#
+				direction = direction.lower()
+				measureme_id = int(measureme_id)
+				stream_id = int(stream_id)
+				window_size = int(window_size)
+			except Exception as e:
+				raise Exception('Trying to parse: {}'.format(event_values)) from e
+			#
+			if measureme_id not in log['measureme_id']:
+				new_measureme_id(measureme_id)
+			#
+			if stream_id not in log['measureme_id'][measureme_id]['stream_id']:
+				new_stream_id(measureme_id, stream_id)
+			#
+			where = log['measureme_id'][measureme_id]['stream_id'][stream_id]
+			#
+			if event not in where['event']:
+				new_event(where, event)
+			#
+			where = where['event'][event][direction]
+			#
+			where['timestamp'].append(timestamp)
+			where['window_size'].append(window_size)
+		#
+	#
+	return log
+#
+def follow_stream(this_relay, cell_ids_forward, cell_ids_backward, measureme_id, stream_id):
+	this_hop = {}
+	#
+	for direction in ('forward', 'backward'):
+		this_hop[direction] = {}
+		for x in ('received', 'sent'):
+			this_hop[direction][x] = {}
+			this_hop[direction][x]['timestamp'] = []
+			this_hop[direction][x]['length'] = []
+		#
+	#
+	for stream_id in this_relay['measureme_id'][measureme_id]['stream_id']:
+		for cell_id in cell_ids:
+			try:
+				cell_index = this_relay['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['recv_edge_data'][direction]['cell_id'].index(cell_id)
+			except ValueError:
+				continue
+			#
+			this_hop[direction]['received']['timestamp'].append(
+			        this_relay['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['recv_edge_data'][direction]['timestamp'][cell_index])
+			this_hop[direction]['received']['length'].append(
+			        this_relay['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['recv_edge_data'][direction]['length'][cell_index])
+		#
+	#
+#
+def link_relay_cells(relay_1, relay_2):
+	common_measureme_ids = list(set(relay_1['measureme_id'].keys()) & set(relay_2['measureme_id'].keys()))
+	results = {}
+	for measureme_id in common_measureme_ids:
+		for direction in ('forward', 'backward'):
+			if direction == 'forward':
+				relay_1_cells = relay_1['measureme_id'][measureme_id]['circuit']['event']['send_relay_cell'][direction]
+				relay_2_cells = relay_2['measureme_id'][measureme_id]['circuit']['event']['recv_relay_cell'][direction]
+			else:
+				relay_1_cells = relay_1['measureme_id'][measureme_id]['circuit']['event']['recv_relay_cell'][direction]
+				relay_2_cells = relay_2['measureme_id'][measureme_id]['circuit']['event']['send_relay_cell'][direction]
+			#
+			#print(relay_1['fingerprint'])
+			#print(len(relay_1_cells['fingerprint']))
+			#print(relay_2['fingerprint'])
+			#print(len(relay_2_cells['fingerprint']))
+			if len(relay_1_cells['fingerprint']) == 0 or len(relay_2_cells['fingerprint']) == 0:
+				continue
+			#
+			assert all([x==relay_1_cells['fingerprint'][0] for x in relay_1_cells['fingerprint']])
+			assert all([x==relay_2_cells['fingerprint'][0] for x in relay_2_cells['fingerprint']])
+			# make sure all the fingerprints are the same for each relay cell
+			#
+			#if relay_1_cells['fingerprint'][0] != relay_2['fingerprint'] or relay_2_cells['fingerprint'][0] != relay_1['fingerprint']:
+			if relay_1_cells['fingerprint'][0] != relay_2['fingerprint'][:8]:
+				#print(relay_1_cells['fingerprint'][0])
+				#print(relay_2['fingerprint'])
+				continue
+			#
+			lookahead = 10
+			relay_1_start = None
+			relay_2_start = None
+			#
+			for (x,y) in itertools.product(range(lookahead),range(lookahead)):
+			#for (x,y) in zip(range(lookahead),range(lookahead)):
+				if relay_1_cells['payload'][x] == relay_2_cells['payload'][y]:
+					relay_1_start = x
+					relay_2_start = y
+					break
+				#
+			#
+			assert relay_1_start is not None and relay_2_start is not None
+			#
+			assert len(relay_1_cells['cell_id'][relay_1_start:]) == len(relay_2_cells['cell_id'][relay_2_start:]), \
+			       '{} /= {} for {} to {}'.format(len(relay_1_cells['cell_id'][relay_1_start:]), len(relay_2_cells['cell_id'][relay_2_start:]), relay_1['fingerprint'], relay_2['fingerprint'])
+#			print('{} /= {} for {} to {}'.format(len(relay_1_cells['cell_id'][relay_1_start:]), len(relay_2_cells['cell_id'][relay_2_start:]), relay_1['fingerprint'], relay_2['fingerprint']))
+			#
+			#results[measureme_id][direction] = list(zip(relay_1_cells['cell_id'], relay_2_cells['cell_id']))
+			if measureme_id not in results:
+				results[measureme_id] = {}
+			#
+			results[measureme_id][direction] = {x[0]:x[1] for x in zip(relay_1_cells['cell_id'][relay_1_start:], relay_2_cells['cell_id'][relay_2_start:])}
+		#
+	#
+	return results
+#
+def reverse_links(links):
+	results = {}
+	for measureme_id in links:
+		results[measureme_id] = {}
+		for direction in links[measureme_id]:
+			results[measureme_id][direction] = {links[measureme_id][direction][x]: x for x in links[measureme_id][direction]}
+		#
+	#
+	return results
+#
+def get_info_for_cell_ids(where, cell_ids, keys):
+	output = {}
+	for key in keys:
+		output[key] = []
+	#
+	for cell_id in cell_ids:
+		cell_index = where['cell_id'].index(cell_id)
+		for key in keys:
+			output[key].append(where[key][cell_index])
+		#
+	#
+	return output
+#
+def get_streams_from_logs(logs):
+	proxies = [log for log in logs if log['fingerprint'] is None]
+	relays = [log for log in logs if log['fingerprint'] is not None]
+	#relays_by_fingerprint = {log['fingerprint']: log for log in logs if log['fingerprint'] is not None}
+	#
+	linked_relay_cells = {}
+	#
+	for proxy in proxies:
+		for relay in relays:
+			#
+			links = link_relay_cells(proxy, relay)
+			linked_relay_cells[(logs.index(proxy), logs.index(relay))] = links
+			#linked_relay_cells[(logs.index(relay), logs.index(proxy))] = reverse_links(links)
+		#
+	#
+	#print(linked_relay_cells)
+	#exit(1)
+	print('Done proxies')
+	for x in range(len(relays)):
+		for y in range(x+1, len(relays)):
+			relay_x = relays[x]
+			relay_y = relays[y]
+			#
+			links = link_relay_cells(relay_x, relay_y)
+			#linked_relay_cells[(logs.index(relay_x), logs.index(relay_y))] = links
+			#linked_relay_cells[(logs.index(relay_y), logs.index(relay_x))] = reverse_links(links)
+			linked_relay_cells[(logs.index(relay_x), logs.index(relay_y))] = link_relay_cells(relay_x, relay_y)
+			linked_relay_cells[(logs.index(relay_y), logs.index(relay_x))] = link_relay_cells(relay_y, relay_x)
+		#
+		print('x: {}'.format(x))
+	#
+	print('Started')
+	streams = {}
+	streams_completed = 0
+	for proxy in proxies:
+		for measureme_id in proxy['measureme_id']:
+			streams[measureme_id] = {}
+			for stream_id in proxy['measureme_id'][measureme_id]['stream_id']:
+				streams[measureme_id][stream_id] = {}
+				for direction in ('forward', 'backward'):
+					streams[measureme_id][stream_id][direction] = []
+					if direction == 'forward':
+						where = proxy['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['recv_edge_data'][direction]
+						#cell_ids = proxy['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['recv_edge_data']['forward']['cell_id']
+						#lengths = proxy['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['recv_edge_data']['forward']['length']
+					else:
+						where = proxy['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['send_edge_data'][direction]
+						#cell_ids = proxy['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['send_edge_data']['backward']['cell_id']
+						#lengths = proxy['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['send_edge_data']['backward']['length']
+					#
+					lengths = where['length']
+					cell_ids = where['cell_id']
+					current_relay = proxy
+					#
+					while current_relay is not None:
+						this_hop = {}
+						this_hop['fingerprint'] = current_relay['fingerprint']
+						#
+						new_cell_ids = []
+						next_relay = None
+						#
+						for x in ('received', 'sent'):
+							this_hop[x] = {}
+							this_hop[x]['timestamp'] = []
+							this_hop[x]['length'] = []
+						#
+						if current_relay == proxy:
+							if direction == 'forward':
+								where = current_relay['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['recv_edge_data'][direction]
+								this_hop['received']['timestamp'] = where['timestamp']
+								this_hop['received']['length'] = lengths
+							else:
+								where = current_relay['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['send_edge_data'][direction]
+								this_hop['sent']['timestamp'] = where['timestamp']
+								this_hop['sent']['length'] = lengths
+							#
+						else:
+							if direction == 'forward':
+								where = current_relay['measureme_id'][measureme_id]['circuit']['event']['recv_relay_cell'][direction]
+								info = get_info_for_cell_ids(where, cell_ids, ['timestamp'])
+								this_hop['received']['timestamp'] = info['timestamp']
+								this_hop['received']['length'] = lengths
+							else:
+								where = current_relay['measureme_id'][measureme_id]['circuit']['event']['send_relay_cell'][direction]
+								info = get_info_for_cell_ids(where, cell_ids, ['timestamp'])
+								this_hop['sent']['timestamp'] = info['timestamp']
+								this_hop['sent']['length'] = lengths
+							#
+						#
+						for x in linked_relay_cells:
+							if x[0] == logs.index(current_relay) and measureme_id in linked_relay_cells[x] and \
+							                       direction in linked_relay_cells[x][measureme_id]:
+								# if the current relay has a linked relay, and they both have the current measureme_id
+								for cell_id in cell_ids:
+									paired_cell_id = linked_relay_cells[x][measureme_id][direction][cell_id]
+									new_cell_ids.append(paired_cell_id)
+								#
+								next_relay = logs[x[1]]
+								break
+							#
+						#
+						if next_relay is None:
+							# exiting
+							if direction == 'forward':
+								where = current_relay['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['send_edge_data'][direction]
+								this_hop['sent']['timestamp'] = where['timestamp']
+								this_hop['sent']['length'] = lengths
+							else:
+								where = current_relay['measureme_id'][measureme_id]['stream_id'][stream_id]['event']['recv_edge_data'][direction]
+								this_hop['received']['timestamp'] = where['timestamp']
+								this_hop['received']['length'] = lengths
+							#
+						else:
+							# passing to next hop
+							assert(len(cell_ids) == len(new_cell_ids))
+							if direction == 'forward':
+								where = current_relay['measureme_id'][measureme_id]['circuit']['event']['send_relay_cell'][direction]
+								info = get_info_for_cell_ids(where, cell_ids, ['timestamp'])
+								this_hop['sent']['timestamp'] = info['timestamp']
+								this_hop['sent']['length'] = lengths
+							else:
+								where = current_relay['measureme_id'][measureme_id]['circuit']['event']['recv_relay_cell'][direction]
+								info = get_info_for_cell_ids(where, cell_ids, ['timestamp'])
+								this_hop['received']['timestamp'] = info['timestamp']
+								this_hop['received']['length'] = lengths
+							#
+						#
+						current_relay = next_relay
+						cell_ids = new_cell_ids
+						streams[measureme_id][stream_id][direction].append(this_hop)
+					#
+				#
+				streams_completed += 1
+				print('Completed: {}'.format(streams_completed))
+			#
+		#
+	#
+	return streams
+#
+if __name__ == '__main__':
+	import sys
+	import os
+	import pickle
+	import gzip
+	#
+	logs = []
+	#
+	save_to = 'processed-data.pickle.gz'
+	if os.path.isfile(save_to):
+		okay_to_overwrite = input('Output file \'{}\' already exists. Would you like to overwrite it? [y/n]: '.format(save_to)).strip()
+		okay_to_overwrite = (okay_to_overwrite.lower() == 'y')
+		if not okay_to_overwrite:
+			print('Exiting')
+			exit()
+		#
+	#
+	for arg in sys.argv[1:]:
+		with open(arg, 'r') as f:
+			logs.append(read_log(f))
+		#
+	#
+	streams = get_streams_from_logs(logs)
+	#
+	with gzip.GzipFile(save_to, 'wb') as f:
+		pickle.dump(streams, f, protocol=4)
+	#
+#

+ 207 - 0
src/plot_streams.py

@@ -0,0 +1,207 @@
+import gzip
+import pickle
+import matplotlib.pylab as plt
+import numpy as np
+#
+def plot_cells(stream, plot_widget, time_offset=None, clickable=False, label=None, color=None):
+	num_bytes = np.cumsum(stream['length'])
+	timestamps = np.asarray(stream['timestamp'])
+	#
+	if time_offset is not None:
+		timestamps = timestamps-time_offset
+	#
+	return ax.step(timestamps, num_bytes/(1024**2), where='post', label=label, color=color, picker=(5 if clickable else None))[0]
+#
+def onresize(event):
+	# matplotlib axes size only scales based on fractions (even with tight_layout), so we manually calculate a fixed padding size
+	w = event.width/event.canvas.figure.dpi
+	h = event.height/event.canvas.figure.dpi
+	w_padding = 0.8 # in inches
+	h_padding = 0.6 # in inches
+	#
+	for ax in event.canvas.figure.axes:
+		for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] + ax.get_xticklabels() + ax.get_yticklabels()):
+			item.set_fontsize(11+0.5*(w*h/(5**2)))
+		#
+	#
+	event.canvas.figure.subplots_adjust(left=w_padding/w+0.01, right=1-((w_padding/2)/w), top=1-(h_padding/h), bottom=h_padding/h+0.01)
+#
+def onscroll(event):
+	scale = 2**(-event.step)
+	ax = event.inaxes
+	mouse_x = event.xdata
+	mouse_y = event.ydata
+	old_xlim = ax.get_xlim()
+	old_ylim = ax.get_ylim()
+	#
+	ax.set_xlim([old_xlim[0]*scale+mouse_x*(1-scale), old_xlim[1]*scale+mouse_x*(1-scale)])
+	ax.set_ylim([old_ylim[0]*scale+mouse_y*(1-scale), old_ylim[1]*scale+mouse_y*(1-scale)])
+	#
+	event.canvas.draw()
+#
+def onpick(event, lines):
+	this_line = event.artist
+	if event.mouseevent.button == 1 and not event.mouseevent.dblclick and event.mouseevent.key is None:
+		# if the mouse is single-clicked and no keyboard key is held down
+		if this_line.get_visible() and this_line in lines.keys():
+			this_info = [x[1] for x in lines.items() if x[0] == this_line][0]
+			#
+			for_legend = []
+			for (line, info) in lines.items():
+				if info['measureme_id'] != this_info['measureme_id']:
+					line.set_visible(False)
+				else:
+					line.set_visible(True)
+					for_legend.append(line)
+				#
+			#
+			event.mouseevent.inaxes.legend(for_legend, [x.get_label() for x in for_legend], loc='upper right')
+			event.canvas.draw_idle()
+		#
+	#
+#
+def onclick(event, lines):
+	if event.button == 1 and event.dblclick and event.key is None:
+		# if the mouse is double-clicked and no keyboard key is held down
+		for (line, info) in lines.items():
+			if info['is_main_plot']:
+				line.set_visible(True)
+			else:
+				line.set_visible(False)
+			#
+		#
+		event.inaxes.get_legend().remove()
+		event.canvas.draw_idle()
+	#
+#
+def onmotion(event, pan_settings):
+	if event.inaxes is not None and event.key == 'control' and event.button == 1:
+		ax = event.inaxes
+		pixel_to_data = ax.transData.inverted()
+		current_pos = pixel_to_data.transform_point((event.x, event.y))
+		last_pos = pixel_to_data.transform_point((pan_settings['start_x'], pan_settings['start_y']))
+		#
+		old_xlim = ax.get_xlim()
+		old_ylim = ax.get_ylim()
+		#
+		ax.set_xlim([old_xlim[0]+(last_pos[0]-current_pos[0]), old_xlim[1]+(last_pos[0]-current_pos[0])])
+		ax.set_ylim([old_ylim[0]+(last_pos[1]-current_pos[1]), old_ylim[1]+(last_pos[1]-current_pos[1])])
+		#
+		event.canvas.draw_idle()
+	#
+	pan_settings['start_x'] = event.x
+	pan_settings['start_y'] = event.y
+#
+def onkeypress(event, lines):
+	if event.key == 'control':
+		num_points = 0
+		range_x = event.inaxes.xaxis.get_view_interval()
+		range_y = event.inaxes.yaxis.get_view_interval()
+		#
+		for line in [l for l in lines if l.get_visible()]:
+			data_x = line.get_xdata(orig=True)
+			data_y = line.get_ydata(orig=True)
+			#
+			num_points += ((data_x>=range_x[0]) & (data_x<=range_x[1]) & (data_y>=range_y[0]) & (data_y<=range_y[1])).sum()
+			# how many points are being rendered
+		#
+		for (line, info) in lines.items():
+			data_x = line.get_xdata(orig=True)
+			data_y = line.get_ydata(orig=True)
+			line.orig_x = data_x
+			line.orig_y = data_y
+			line.orig_drawstyle = line.get_drawstyle()
+			#
+			subsample_spacing = max(1, int(num_points/10000))
+			# the constant can be decreased to speed up plotting on slower computers
+			#
+			mask = np.ones(len(data_x))
+			mask[::subsample_spacing] = 0
+			line.set_xdata(data_x[mask==0])
+			line.set_ydata(data_y[mask==0])
+			line.set_drawstyle('default')
+		#
+		event.canvas.draw_idle()
+	#
+#
+def onkeyrelease(event, lines):
+	if event.key == 'control':
+		for (line, info) in lines.items():
+			line.set_xdata(line.orig_x)
+			line.set_ydata(line.orig_y)
+			line.set_drawstyle(line.orig_drawstyle)
+		#
+		event.canvas.draw_idle()
+	#
+#
+def get_complimentary_color(color_index):
+	return (color_index+1 if color_index%2==0 else color_index-1)
+#
+if __name__ == '__main__':
+	with gzip.GzipFile('processed-data.pickle.gz', 'rb') as f:
+		streams = pickle.load(f)
+	#
+	fig, ax = plt.subplots()#constrained_layout=True
+	#
+	start_time = min([hop[t]['timestamp'][0] for m in streams for s in streams[m] for d in streams[m][s]
+	                  for hop in streams[m][s][d] for t in ('received','sent')])
+	#
+	lines = {}
+	assigned_colors = []
+	colormap = plt.get_cmap('tab20') #'tab10'
+	direction_shortforms = {'forward':'fwd', 'backward':'bwd'}
+	transmission_shortforms = {'received':'recv', 'sent':'sent'}
+	#
+	for measureme_id in streams:
+		# for each circuit
+		for stream_id in streams[measureme_id]:
+			# for each stream
+			direction = 'forward'
+			for hop_index in range(len(streams[measureme_id][stream_id][direction])):
+				# for each hop in the circuit (including the OP)
+				for transmission in ('received', 'sent'):
+					data = streams[measureme_id][stream_id][direction][hop_index][transmission]
+					#
+					if hop_index == len(streams[measureme_id][stream_id][direction])-1:
+						guard_fingerprint = streams[measureme_id][stream_id][direction][1]['fingerprint']
+						if guard_fingerprint not in assigned_colors:
+							assigned_colors.append(guard_fingerprint)
+						#
+						if transmission == 'sent':
+							color_index = assigned_colors.index(guard_fingerprint)
+						else:
+							color_index = get_complimentary_color(assigned_colors.index(guard_fingerprint))
+						#
+					else:
+						color_index = hop_index*2 + ('received', 'sent').index(transmission)
+					#
+					is_main_plot = (hop_index == len(streams[measureme_id][stream_id][direction])-1 and transmission == 'sent')
+					direction_label = direction_shortforms[direction]
+					transmission_label = transmission_shortforms[transmission]
+					label = 'hop={}, {:.4}, {:.4}, mid={}, sid={}'.format(hop_index, direction_label, transmission_label, measureme_id, stream_id)
+					line = plot_cells(data, ax, time_offset=start_time, clickable=is_main_plot, label=label, color=colormap(color_index))
+					if not is_main_plot:
+						line.set_visible(False)
+					#
+					lines[line] = {'measureme_id':measureme_id, 'stream_id':stream_id, 'direction':direction,
+					               'hop_index':hop_index, 'transmission':transmission, 'is_main_plot':is_main_plot}
+				#
+			#
+		#
+	#
+	ax.set_xlabel('Time (s)')
+	ax.set_ylabel('Data (MiB)')
+	ax.set_title('Test')
+	fig.tight_layout(pad=0)
+	#ax.set_ylim(0, None)
+	#
+	fig.canvas.mpl_connect('resize_event', onresize)
+	fig.canvas.mpl_connect('scroll_event', onscroll)
+	fig.canvas.mpl_connect('pick_event', lambda event,lines=lines: onpick(event, lines))
+	fig.canvas.mpl_connect('button_press_event', lambda event,lines=lines: onclick(event, lines))
+	fig.canvas.mpl_connect('motion_notify_event', lambda event,pan_settings={'start_x':0,'start_y':0}: onmotion(event, pan_settings))
+	fig.canvas.mpl_connect('key_press_event', lambda event,lines=lines: onkeypress(event, lines))
+	fig.canvas.mpl_connect('key_release_event', lambda event,lines=lines: onkeyrelease(event, lines))
+	#
+	plt.show(fig)
+#