#!/usr/bin/env python # Plot the changes in /proc/interrupts over time # Date: 2014-06-17 # Author: Peter Wu # Wishlist: # Thicker legend lines # Nicier smoothing import matplotlib.pyplot as plt import matplotlib from collections import deque, OrderedDict import numpy as np from scipy.interpolate import spline, interp1d import threading # Number of seconds to show in the graph XRANGE = 60 # Delay between updating the graph INTERVAL = .5 # Log scale base or 0 to disable logarithmic y scaling LOG_SCALE_BASE = 10 # Whether to enable smooth curves or not SMOOTH_CURVES = True MARKER_DEFAULT = 'o' MARKER_SELECTED = 'v' # 26 colors from http://graphicdesign.stackexchange.com/a/3815 # "A Colour Alphabet and the Limits of Colour Coding" COLORS = ['#F0A3FF', '#0075DC', '#993F00', '#4C005C', '#191919', '#005C31', '#2BCE48', '#FFCC99', '#808080', '#94FFB5', '#8F7C00', '#9DCC00', '#C20088', '#003380', '#FFA405', '#FFA8BB', '#426600', '#FF0010', '#5EF1F2', '#00998F', '#E0FF66', '#740AFF', '#990000', '#FFFF80', '#FFFF00', '#FF5005'] # From alex440's comment (currently not used) ALT_COLORS = ['#023FA5', '#7D87B9', '#BEC1D4', '#D6BCC0', '#BB7784', '#FFFFFF', '#4A6FE3', '#8595E1', '#B5BBE3', '#E6AFB9', '#E07B91', '#D33F6A', '#11C638', '#8DD593', '#C6DEC7', '#EAD3C6', '#F0B98D', '#EF9708', '#0FCFC0', '#9CDED6', '#D5EAE7', '#F3E1EB', '#F6C4E1', '#F79CD4'] def is_line_ok(name, yvalues): """Returns True if a line should be displayed for this name.""" if max(yvalues) < 5: return False names_ok = ['hci', 'timer'] for name_ok_part in names_ok: if name_ok_part in name: return True # Accept all return True # Fix Unicode font matplotlib.rc('font', family='DejaVu Sans') # From http://stackoverflow.com/a/490090 def synchronized(lock): """Synchronization decorator.""" def wrap(f): def newFunction(*args, **kw): with lock: return f(*args, **kw) return newFunction return wrap def get_numbers(): # TODO: may break the graph if a line disappears with open('/proc/interrupts') as pi: ncpus = len(pi.readline().split()) for line in pi: name, values = line.split(':', 1) name = name.strip() values = values.strip().split(None, ncpus) if len(values) >= ncpus: # Name is ID + description for uniqueness name += ':' + values[-1]; yield name, sum(int(values[i]) for i in range(0, ncpus)) prev = OrderedDict() def get_diffs(): for name, n in get_numbers(): if name in prev: yield name, n - prev[name] else: yield name, 0 prev[name] = n plt.ylabel(u'\u0394interrupts') plt.xlabel(u'time (sec)') plt.grid('on') #plt.ion() # Not necessary if show() does not block. plt.show(block=False) if LOG_SCALE_BASE > 0: plt.yscale('log', nonposy='clip', basey=LOG_SCALE_BASE) # Used when picking a new line or in update() update_legend = False ### BEGIN EVENTS # After pressing ^W, stop the main loop running = True def on_close(event): global running running = False plt.connect('close_event', on_close) # Space toggles updating paused = False def on_keypress(event): global paused if event.key == ' ': paused = not paused update_title() plt.connect('key_press_event', on_keypress) last_selected = None def select_line(line): global last_selected line = lines[line.get_label()] if last_selected: last_selected.set_marker(MARKER_DEFAULT) if last_selected == line: last_selected = None else: line.set_marker(MARKER_SELECTED) last_selected = line def on_pick(event): global update_legend artist = event.artist if isinstance(artist, matplotlib.lines.Line2D): select_line(artist) update_legend = True do_draw() plt.connect('pick_event', on_pick) ### END EVENTS names = [name for name, _ in get_diffs()] yvalues = {} x_count = int(XRANGE / INTERVAL) # Initialize y values for each name for name in names: ydata = deque([0] * x_count, x_count) yvalues[name] = ydata lines = {} def update_title(): title = 'Figure' if paused: title += ' (paused - press Space to resume)' plt.gcf().canvas.set_window_title(title) # Lock to avoid updating the UI while the data is being refreshed data_lock = threading.Lock() # Since yvalues gets filled on the tail, substract from start time start_time = -XRANGE @synchronized(data_lock) def refresh_data(): global start_time # Update data for name, n in get_diffs(): yvalues[name].append(n) start_time += INTERVAL smooth = {} @synchronized(data_lock) def update(): """Reads new data and updates the line values.""" global update_legend # Update lines for name, n in get_diffs(): ys = yvalues[name] # Consider only strictly positive values ydata = [y for y in ys if y > 0] xdata = [start_time + i * INTERVAL for i, y in enumerate(ys) if y > 0] if ydata and is_line_ok(name, ys): # Data is significant, show it if name in lines: lines[name].set_data(xdata, ydata) else: color = COLORS[names.index(name) % len(COLORS)] lines[name], = plt.plot(xdata, ydata, MARKER_DEFAULT + '-', label=name, color=color) lines[name].set_picker(5) # Make selectable update_legend = True # Smooth curve ydata_len = len(ydata) if ydata_len > 3 and SMOOTH_CURVES: min_x = min(xdata) max_x = max(xdata) xnew = np.linspace(min_x, max_x, (1 + max_x - min_x) * 8) #ynew = spline(xdata, ydata, xnew) # quadratic and cubic splines give too much deviations ynew = interp1d(xdata, ydata, kind='slinear')(xnew) if not name in smooth: smooth[name], = plt.plot(xnew, ynew, color=lines[name].get_color()) # Smooth line is shown, hide straight lines lines[name].set_linestyle('') else: smooth[name].set_data(xnew, ynew) elif name in smooth: smooth[name].remove() del smooth[name] # No smooth line is shown, fallback to straight lines lines[name].set_linestyle('-') elif name in lines: # Data is insignificant, remove previous line lines[name].remove() del lines[name] update_legend = True largest = 10 for name in yvalues: ydata = yvalues[name] if is_line_ok(name, ydata): largest = max(largest, max(ydata)) # Update iff graph becomes too large #ymin, ymax = plt.ylim() #if ymax - ymin < largest: # plt.ylim(ymin, ymin + largest) plt.ylim(0, largest) min_x = max(start_time, 0) plt.xlim(min_x, min_x + XRANGE) def do_draw(): """Actually draw the graph, updating the legend if necessary.""" global update_legend # update legend if a line gets added, changed or removed if update_legend: old_legend = plt.axes().get_legend() if old_legend: # Undocumented API, use it to remember legend position old_loc = old_legend._get_loc() legend = plt.legend(loc='upper left', framealpha=.5, fontsize='small') legend.draggable() if old_legend: legend._set_loc(old_loc) # Enable selecting a line by clicking in the legend for line in legend.get_lines(): line.set_picker(5) update_legend = False plt.draw() update_title() # Separate worker for fetching data def refresh_data_timer(): global t t = threading.Timer(INTERVAL, refresh_data_timer) t.start() refresh_data() refresh_data_timer() while running: if not paused: update() do_draw() plt.pause(INTERVAL) # Cancel any scheduled timer t.cancel()