[tor-commits] [arm/master] Moving graph statistical information to a Stat class

atagar at torproject.org atagar at torproject.org
Sun Oct 19 01:28:44 UTC 2014


commit ea6fdd1afbbbe4ac6bf4fc5fbb762d172d53c711
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Oct 18 18:27:38 2014 -0700

    Moving graph statistical information to a Stat class
    
    Simple class to represent the statistical information on the graph. Yes, we had
    a previous GraphStat class but that confused the method that we'd use to fetch
    data with both a primary and secondary stat. This doubled the amount of code we
    needed. Oh, and the new Stat has a slightly nicer implementation.
---
 arm/graph_panel.py |  185 +++++++++++++++++++++++++---------------------------
 1 file changed, 88 insertions(+), 97 deletions(-)

diff --git a/arm/graph_panel.py b/arm/graph_panel.py
index ca91bb2..fab6603 100644
--- a/arm/graph_panel.py
+++ b/arm/graph_panel.py
@@ -86,6 +86,48 @@ CONFIG = conf.config_dict('arm', {
 COLLAPSE_WIDTH = 135
 
 
+class Stat(object):
+  """
+  Graphable statistical information.
+
+  :var int latest_value: last value we recorded
+  :var int total: sum of all values we've recorded
+  :var dict values: mapping of intervals to an array of samplings from newest to oldest
+  :var dict max_value: mapping of intervals to the maximum value it has had
+  """
+
+  def __init__(self, clone = None):
+    if clone:
+      self.latest_value = clone.latest_value
+      self.total = clone.total
+      self.tick = clone.tick
+      self.values = copy.deepcopy(clone.values)
+      self.max_value = dict(clone.max_value)
+      self._in_process_value = dict(clone._in_process_value)
+    else:
+      self.latest_value = 0
+      self.total = 0
+      self.tick = 0
+      self.values = dict([(i, CONFIG['features.graph.max_width'] * [0]) for i in CONFIG['attr.graph.intervals']])
+      self.max_value = dict([(i, 0) for i in CONFIG['attr.graph.intervals']])
+      self._in_process_value = dict([(i, 0) for i in CONFIG['attr.graph.intervals']])
+
+  def update(self, new_value):
+    self.latest_value = new_value
+    self.total += new_value
+    self.tick += 1
+
+    for interval in CONFIG['attr.graph.intervals']:
+      interval_seconds = int(CONFIG['attr.graph.intervals'][interval])
+      self._in_process_value[interval] += new_value
+
+      if self.tick % interval_seconds == 0:
+        new_entry = self._in_process_value[interval] / interval_seconds
+        self.values[interval] = [new_entry] + self.values[interval][:-1]
+        self.max_value[interval] = max(self.max_value[interval], new_entry)
+        self._in_process_value[interval] = 0
+
+
 class GraphStats:
   """
   Module that's expected to update dynamically and provide attributes to be
@@ -104,29 +146,8 @@ class GraphStats:
     self.is_selected = False
     self.is_pause_buffer = False
 
-    # tracked stats
-
-    self.tick = 0                                    # number of processed events
-    self.last_primary, self.last_secondary = 0, 0    # most recent registered stats
-    self.primary_total, self.secondary_total = 0, 0  # sum of all stats seen
-
-    # timescale dependent stats
-
-    self.max_column = CONFIG['features.graph.max_width']
-    self.max_primary, self.max_secondary = {}, {}
-    self.primary_counts, self.secondary_counts = {}, {}
-
-    for i in range(len(CONFIG['attr.graph.intervals'])):
-      # recent rates for graph
-
-      self.max_primary[i] = 0
-      self.max_secondary[i] = 0
-
-      # historic stats for graph, first is accumulator
-      # iterative insert needed to avoid making shallow copies (nasty, nasty gotcha)
-
-      self.primary_counts[i] = (self.max_column + 1) * [0]
-      self.secondary_counts[i] = (self.max_column + 1) * [0]
+    self.primary = Stat()
+    self.secondary = Stat()
 
     # tracks BW events
 
@@ -143,15 +164,9 @@ class GraphStats:
     if not new_copy:
       new_copy = GraphStats()
 
-    new_copy.tick = self.tick
-    new_copy.last_primary = self.last_primary
-    new_copy.last_secondary = self.last_secondary
-    new_copy.primary_total = self.primary_total
-    new_copy.secondary_total = self.secondary_total
-    new_copy.max_primary = dict(self.max_primary)
-    new_copy.max_secondary = dict(self.max_secondary)
-    new_copy.primary_counts = copy.deepcopy(self.primary_counts)
-    new_copy.secondary_counts = copy.deepcopy(self.secondary_counts)
+    new_copy.primary = Stat(self.primary)
+    new_copy.secondary = Stat(self.secondary)
+
     new_copy.is_pause_buffer = True
     return new_copy
 
@@ -172,7 +187,7 @@ class GraphStats:
     if self._graph_panel and self.is_selected and not self._graph_panel.is_paused():
       # use the minimum of the current refresh rate and the panel's
       update_rate = int(CONFIG['attr.graph.intervals'].values()[self._graph_panel.update_interval])
-      return (self.tick + 1) % update_rate == 0
+      return (self.primary.tick + 1) % update_rate == 0
     else:
       return False
 
@@ -200,31 +215,8 @@ class GraphStats:
 
     is_redraw = self.is_next_tick_redraw()
 
-    self.last_primary, self.last_secondary = primary, secondary
-    self.primary_total += primary
-    self.secondary_total += secondary
-
-    # updates for all time intervals
-
-    self.tick += 1
-
-    for i in range(len(CONFIG['attr.graph.intervals'])):
-      lable, timescale = CONFIG['attr.graph.intervals'].items()[i]
-      timescale = int(timescale)
-
-      self.primary_counts[i][0] += primary
-      self.secondary_counts[i][0] += secondary
-
-      if self.tick % timescale == 0:
-        self.max_primary[i] = max(self.max_primary[i], self.primary_counts[i][0] / timescale)
-        self.primary_counts[i][0] /= timescale
-        self.primary_counts[i].insert(0, 0)
-        del self.primary_counts[i][self.max_column + 1:]
-
-        self.max_secondary[i] = max(self.max_secondary[i], self.secondary_counts[i][0] / timescale)
-        self.secondary_counts[i][0] /= timescale
-        self.secondary_counts[i].insert(0, 0)
-        del self.secondary_counts[i][self.max_column + 1:]
+    self.primary.update(primary)
+    self.secondary.update(secondary)
 
     if is_redraw and self._graph_panel:
       self._graph_panel.redraw(True)
@@ -261,8 +253,8 @@ class BandwidthStats(GraphStats):
     start_time = system.start_time(controller.get_pid(None))
 
     if read_total and write_total and start_time:
-      self.primary_total = int(read_total) / 1024  # Bytes -> KB
-      self.secondary_total = int(write_total) / 1024  # Bytes -> KB
+      self.primary.total = int(read_total) / 1024  # Bytes -> KB
+      self.secondary.total = int(write_total) / 1024  # Bytes -> KB
       self.start_time = start_time
     else:
       self.start_time = time.time()
@@ -297,7 +289,7 @@ class BandwidthStats(GraphStats):
 
     # crops starting entries so they're the same size
 
-    entry_count = min(len(bw_read_entries), len(bw_write_entries), self.max_column)
+    entry_count = min(len(bw_read_entries), len(bw_write_entries), CONFIG['features.graph.max_width'])
     bw_read_entries = bw_read_entries[len(bw_read_entries) - entry_count:]
     bw_write_entries = bw_write_entries[len(bw_write_entries) - entry_count:]
 
@@ -316,16 +308,13 @@ class BandwidthStats(GraphStats):
     for i in range(entry_count):
       read_value, write_value = bw_read_entries[i], bw_write_entries[i]
 
-      self.last_primary, self.last_secondary = read_value, write_value
-
-      self.primary_counts[interval_index].insert(0, read_value)
-      self.secondary_counts[interval_index].insert(0, write_value)
+      self.primary.latest_value, self.secondary.latest_value = read_value / 900, write_value / 900
 
-    self.max_primary[interval_index] = max(self.primary_counts)
-    self.max_secondary[interval_index] = max(self.secondary_counts)
+      self.primary.values['15 minute'] = [read_value] + self.primary.values['15 minute'][:-1]
+      self.secondary.values['15 minute'] = [write_value] + self.secondary.values['15 minute'][:-1]
 
-    del self.primary_counts[interval_index][self.max_column + 1:]
-    del self.secondary_counts[interval_index][self.max_column + 1:]
+    self.primary.max_value['15 minute'] = max(self.primary.values)
+    self.secondary.max_value['15 minute'] = max(self.secondary.values)
 
     return time.time() - min(stats.last_read_time, stats.last_write_time)
 
@@ -339,14 +328,14 @@ class BandwidthStats(GraphStats):
     return 'Bandwidth (%s):' % stats_label if stats_label else 'Bandwidth:'
 
   def primary_header(self, width):
-    stats = ['%-14s' % ('%s/sec' % _size_label(self.last_primary * 1024))]
+    stats = ['%-14s' % ('%s/sec' % _size_label(self.primary.latest_value * 1024))]
 
     # if wide then avg and total are part of the header, otherwise they're on
     # the x-axis
 
     if width * 2 > COLLAPSE_WIDTH:
-      stats.append('- avg: %s/sec' % _size_label(self.primary_total / (time.time() - self.start_time) * 1024))
-      stats.append(', total: %s' % _size_label(self.primary_total * 1024))
+      stats.append('- avg: %s/sec' % _size_label(self.primary.total / (time.time() - self.start_time) * 1024))
+      stats.append(', total: %s' % _size_label(self.primary.total * 1024))
 
     stats_label = str_tools.join(stats, '', width - 12)
 
@@ -356,14 +345,14 @@ class BandwidthStats(GraphStats):
       return 'Download:'
 
   def secondary_header(self, width):
-    stats = ['%-14s' % ('%s/sec' % _size_label(self.last_secondary * 1024))]
+    stats = ['%-14s' % ('%s/sec' % _size_label(self.secondary.latest_value * 1024))]
 
     # if wide then avg and total are part of the header, otherwise they're on
     # the x-axis
 
     if width * 2 > COLLAPSE_WIDTH:
-      stats.append('- avg: %s/sec' % _size_label(self.secondary_total / (time.time() - self.start_time) * 1024))
-      stats.append(', total: %s' % _size_label(self.secondary_total * 1024))
+      stats.append('- avg: %s/sec' % _size_label(self.secondary.total / (time.time() - self.start_time) * 1024))
+      stats.append(', total: %s' % _size_label(self.secondary.total * 1024))
 
     stats_label = str_tools.join(stats, '', width - 10)
 
@@ -455,12 +444,12 @@ class ConnStats(GraphStats):
     return 'Connection Count:'
 
   def primary_header(self, width):
-    avg = self.primary_total / max(1, self.tick)
-    return 'Inbound (%s, avg: %s):' % (self.last_primary, avg)
+    avg = self.primary.total / max(1, self.primary.tick)
+    return 'Inbound (%s, avg: %s):' % (self.primary.latest_value, avg)
 
   def secondary_header(self, width):
-    avg = self.secondary_total / max(1, self.tick)
-    return 'Outbound (%s, avg: %s):' % (self.last_secondary, avg)
+    avg = self.secondary.total / max(1, self.secondary.tick)
+    return 'Outbound (%s, avg: %s):' % (self.secondary.latest_value, avg)
 
 
 class ResourceStats(GraphStats):
@@ -482,15 +471,15 @@ class ResourceStats(GraphStats):
     return 'System Resources:'
 
   def primary_header(self, width):
-    avg = self.primary_total / max(1, self.tick)
-    return 'CPU (%0.1f%%, avg: %0.1f%%):' % (self.last_primary, avg)
+    avg = self.primary.total / max(1, self.primary.tick)
+    return 'CPU (%0.1f%%, avg: %0.1f%%):' % (self.primary.latest_value, avg)
 
   def secondary_header(self, width):
     # memory sizes are converted from MB to B before generating labels
 
-    usage_label = str_tools.size_label(self.last_secondary * 1048576, 1)
+    usage_label = str_tools.size_label(self.secondary.latest_value * 1048576, 1)
 
-    avg = self.secondary_total / max(1, self.tick)
+    avg = self.secondary.total / max(1, self.secondary.tick)
     avg_label = str_tools.size_label(avg * 1048576, 1)
 
     return 'Memory (%s, avg: %s):' % (usage_label, avg_label)
@@ -741,7 +730,7 @@ class GraphPanel(panel.Panel):
       return
 
     param = self.get_attr('stats')[self.current_display]
-    graph_column = min((width - 10) / 2, param.max_column)
+    graph_column = min((width - 10) / 2, CONFIG['features.graph.max_width'])
 
     if self.is_title_visible():
       self.addstr(0, 0, param.get_title(width), curses.A_STANDOUT)
@@ -758,23 +747,25 @@ class GraphPanel(panel.Panel):
 
     # determines max/min value on the graph
 
+    interval = CONFIG['attr.graph.intervals'].keys()[self.update_interval]
+
     if self.bounds == Bounds.GLOBAL_MAX:
-      primary_max_bound = int(param.max_primary[self.update_interval])
-      secondary_max_bound = int(param.max_secondary[self.update_interval])
+      primary_max_bound = param.primary.max_value[interval]
+      secondary_max_bound = param.secondary.max_value[interval]
     else:
       # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima
       if graph_column < 2:
         # nothing being displayed
         primary_max_bound, secondary_max_bound = 0, 0
       else:
-        primary_max_bound = int(max(param.primary_counts[self.update_interval][1:graph_column + 1]))
-        secondary_max_bound = int(max(param.secondary_counts[self.update_interval][1:graph_column + 1]))
+        primary_max_bound = max(param.primary.values[interval][:graph_column])
+        secondary_max_bound = max(param.secondary.values[interval][:graph_column])
 
     primary_min_bound = secondary_min_bound = 0
 
     if self.bounds == Bounds.TIGHT:
-      primary_min_bound = int(min(param.primary_counts[self.update_interval][1:graph_column + 1]))
-      secondary_min_bound = int(min(param.secondary_counts[self.update_interval][1:graph_column + 1]))
+      primary_min_bound = min(param.primary.values[interval][:graph_column])
+      secondary_min_bound = min(param.secondary.values[interval][:graph_column])
 
       # if the max = min (ie, all values are the same) then use zero lower
       # bound so a graph is still displayed
@@ -819,14 +810,14 @@ class GraphPanel(panel.Panel):
     # creates bar graph (both primary and secondary)
 
     for col in range(graph_column):
-      column_count = int(param.primary_counts[self.update_interval][col + 1]) - primary_min_bound
-      column_height = min(self.graph_height, self.graph_height * column_count / (max(1, primary_max_bound) - primary_min_bound))
+      column_count = int(param.primary.values[interval][col]) - primary_min_bound
+      column_height = int(min(self.graph_height, self.graph_height * column_count / (max(1, primary_max_bound) - primary_min_bound)))
 
       for row in range(column_height):
         self.addstr(self.graph_height + 1 - row, col + 5, ' ', curses.A_STANDOUT, PRIMARY_COLOR)
 
-      column_count = int(param.secondary_counts[self.update_interval][col + 1]) - secondary_min_bound
-      column_height = min(self.graph_height, self.graph_height * column_count / (max(1, secondary_max_bound) - secondary_min_bound))
+      column_count = int(param.secondary.values[interval][col]) - secondary_min_bound
+      column_height = int(min(self.graph_height, self.graph_height * column_count / (max(1, secondary_max_bound) - secondary_min_bound)))
 
       for row in range(column_height):
         self.addstr(self.graph_height + 1 - row, col + graph_column + 10, ' ', curses.A_STANDOUT, SECONDARY_COLOR)
@@ -863,11 +854,11 @@ class GraphPanel(panel.Panel):
       # clears line
 
       self.addstr(labeling_line, 0, ' ' * width)
-      graph_column = min((width - 10) / 2, param.max_column)
+      graph_column = min((width - 10) / 2, CONFIG['features.graph.max_width'])
 
       runtime = time.time() - param.start_time
-      primary_footer = 'total: %s, avg: %s/sec' % (_size_label(param.primary_total * 1024), _size_label(param.primary_total / runtime * 1024))
-      secondary_footer = 'total: %s, avg: %s/sec' % (_size_label(param.secondary_total * 1024), _size_label(param.secondary_total / runtime * 1024))
+      primary_footer = 'total: %s, avg: %s/sec' % (_size_label(param.primary.total * 1024), _size_label(param.primary.total / runtime * 1024))
+      secondary_footer = 'total: %s, avg: %s/sec' % (_size_label(param.secondary.total * 1024), _size_label(param.secondary.total / runtime * 1024))
 
       self.addstr(labeling_line, 1, primary_footer, PRIMARY_COLOR)
       self.addstr(labeling_line, graph_column + 6, secondary_footer, SECONDARY_COLOR)



More information about the tor-commits mailing list