tor-commits
Threads by month
- ----- 2025 -----
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
October 2014
- 26 participants
- 1552 discussions
commit f3d55aa694bffdbc1ce95046fea4d01e7e1917b1
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Oct 12 19:43:17 2014 -0700
Merging graph module into single file
Devious scheme is to rip this module a new one, simplifying it to the point
that it can comfortably live in a single file. It's presently way on the long
end of uncomfortable, but merging it will help push us along with this
overhaul.
---
arm/controller.py | 11 +-
arm/graph_panel.py | 947 +++++++++++++++++++++++++++++++++++++++
arm/graphing/__init__.py | 10 -
arm/graphing/bandwidth_stats.py | 285 ------------
arm/graphing/conn_stats.py | 59 ---
arm/graphing/graph_panel.py | 581 ------------------------
arm/graphing/resource_stats.py | 57 ---
arm/menu/actions.py | 4 +-
8 files changed, 953 insertions(+), 1001 deletions(-)
diff --git a/arm/controller.py b/arm/controller.py
index 42d0531..1525412 100644
--- a/arm/controller.py
+++ b/arm/controller.py
@@ -15,10 +15,7 @@ import arm.header_panel
import arm.log_panel
import arm.config_panel
import arm.torrc_panel
-import arm.graphing.graph_panel
-import arm.graphing.bandwidth_stats
-import arm.graphing.conn_stats
-import arm.graphing.resource_stats
+import arm.graph_panel
import arm.connections.conn_panel
import arm.util.tracker
@@ -26,9 +23,9 @@ import stem
from stem.control import State
-from arm.util import msg, panel, tor_config, tor_controller, ui_tools
+from arm.util import panel, tor_config, tor_controller, ui_tools
-from stem.util import conf, enum, log, str_tools, system
+from stem.util import conf, log, system
ARM_CONTROLLER = None
@@ -105,7 +102,7 @@ def init_controller(stdscr, start_time):
# first page: graph and log
if CONFIG["features.panels.show.graph"]:
- first_page_panels.append(arm.graphing.graph_panel.GraphPanel(stdscr))
+ first_page_panels.append(arm.graph_panel.GraphPanel(stdscr))
if CONFIG["features.panels.show.log"]:
expanded_events = arm.arguments.expand_events(CONFIG["startup.events"])
diff --git a/arm/graph_panel.py b/arm/graph_panel.py
new file mode 100644
index 0000000..8f08fb3
--- /dev/null
+++ b/arm/graph_panel.py
@@ -0,0 +1,947 @@
+"""
+Flexible panel for presenting bar graphs for a variety of stats. This panel is
+just concerned with the rendering of information, which is actually collected
+and stored by implementations of the GraphStats interface. Panels are made up
+of a title, followed by headers and graphs for two sets of stats. For
+instance...
+
+Bandwidth (cap: 5 MB, burst: 10 MB):
+Downloaded (0.0 B/sec): Uploaded (0.0 B/sec):
+ 34 30
+ * *
+ ** * * * **
+ * * * ** ** ** *** ** ** ** **
+ ********* ****** ****** ********* ****** ******
+ 0 ************ **************** 0 ************ ****************
+ 25s 50 1m 1.6 2.0 25s 50 1m 1.6 2.0
+"""
+
+import copy
+import curses
+import time
+
+import arm.popups
+import arm.controller
+import arm.util.tracker
+
+import stem.control
+
+from arm.util import bandwidth_from_state, msg, panel, tor_controller
+
+from stem.control import Listener, State
+from stem.util import conf, enum, log, str_tools, system
+
+GraphStat = enum.Enum('BANDWIDTH', 'CONNECTIONS', 'SYSTEM_RESOURCES')
+
+# maps 'features.graph.type' config values to the initial types
+
+GRAPH_INIT_STATS = {1: GraphStat.BANDWIDTH, 2: GraphStat.CONNECTIONS, 3: GraphStat.SYSTEM_RESOURCES}
+
+DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
+PRIMARY_COLOR, SECONDARY_COLOR = 'green', 'cyan'
+MIN_GRAPH_HEIGHT = 1
+
+# enums for graph bounds:
+# Bounds.GLOBAL_MAX - global maximum (highest value ever seen)
+# Bounds.LOCAL_MAX - local maximum (highest value currently on the graph)
+# Bounds.TIGHT - local maximum and minimum
+
+Bounds = enum.Enum('GLOBAL_MAX', 'LOCAL_MAX', 'TIGHT')
+
+WIDE_LABELING_GRAPH_COL = 50 # minimum graph columns to use wide spacing for x-axis labels
+
+ACCOUNTING_RATE = 5
+
+
+def conf_handler(key, value):
+ if key == 'features.graph.height':
+ return max(MIN_GRAPH_HEIGHT, value)
+ elif key == 'features.graph.max_width':
+ return max(1, value)
+ elif key == 'features.graph.bound':
+ return max(0, min(2, value))
+
+
+# used for setting defaults when initializing GraphStats and GraphPanel instances
+
+CONFIG = conf.config_dict('arm', {
+ 'attr.hibernate_color': {},
+ 'attr.graph.intervals': {},
+ 'features.graph.height': 7,
+ 'features.graph.interval': 0,
+ 'features.graph.bound': 1,
+ 'features.graph.max_width': 150,
+ 'features.graph.showIntermediateBounds': True,
+ 'features.graph.type': 1,
+ 'features.panels.show.connection': True,
+ 'features.graph.bw.prepopulate': True,
+ 'features.graph.bw.transferInBytes': False,
+ 'features.graph.bw.accounting.show': True,
+ 'tor.chroot': '',
+}, conf_handler)
+
+# width at which panel abandons placing optional stats (avg and total) with
+# header in favor of replacing the x-axis label
+
+COLLAPSE_WIDTH = 135
+
+
+class GraphStats:
+ """
+ Module that's expected to update dynamically and provide attributes to be
+ graphed. Up to two graphs (a 'primary' and 'secondary') can be displayed at a
+ time and timescale parameters use the labels defined in CONFIG['attr.graph.intervals'].
+ """
+
+ def __init__(self):
+ """
+ Initializes parameters needed to present a graph.
+ """
+
+ # panel to be redrawn when updated (set when added to GraphPanel)
+
+ self._graph_panel = None
+ 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]
+
+ # tracks BW events
+
+ tor_controller().add_event_listener(self.bandwidth_event, stem.control.EventType.BW)
+
+ def clone(self, new_copy=None):
+ """
+ Provides a deep copy of this instance.
+
+ Arguments:
+ new_copy - base instance to build copy off of
+ """
+
+ 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.is_pause_buffer = True
+ return new_copy
+
+ def event_tick(self):
+ """
+ Called when it's time to process another event. All graphs use tor BW
+ events to keep in sync with each other (this happens once a second).
+ """
+
+ pass
+
+ def is_next_tick_redraw(self):
+ """
+ Provides true if the following tick (call to _process_event) will result in
+ being redrawn.
+ """
+
+ 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
+ else:
+ return False
+
+ def get_title(self, width):
+ """
+ Provides top label.
+ """
+
+ return ''
+
+ def primary_header(self, width):
+ return ''
+
+ def secondary_header(self, width):
+ return ''
+
+ def get_content_height(self):
+ """
+ Provides the height content should take up (not including the graph).
+ """
+
+ return DEFAULT_CONTENT_HEIGHT
+
+ def draw(self, panel, width, height):
+ """
+ Allows for any custom drawing monitor wishes to append.
+ """
+
+ pass
+
+ def bandwidth_event(self, event):
+ if not self.is_pause_buffer:
+ self.event_tick()
+
+ def _process_event(self, primary, secondary):
+ """
+ Includes new stats in graphs and notifies associated GraphPanel of changes.
+ """
+
+ 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:]
+
+ if is_redraw and self._graph_panel:
+ self._graph_panel.redraw(True)
+
+
+class BandwidthStats(GraphStats):
+ """
+ Uses tor BW events to generate bandwidth usage graph.
+ """
+
+ def __init__(self, is_pause_buffer = False):
+ GraphStats.__init__(self)
+
+ # listens for tor reload (sighup) events which can reset the bandwidth
+ # rate/burst and if tor's using accounting
+
+ controller = tor_controller()
+ self._title_stats = []
+ self._accounting_stats = None
+
+ if not is_pause_buffer:
+ self.reset_listener(controller, State.INIT, None) # initializes values
+
+ controller.add_status_listener(self.reset_listener)
+ self.new_desc_event(None) # updates title params
+
+ # We both show our 'total' attributes and use it to determine our average.
+ #
+ # If we can get *both* our start time and the totals from tor (via 'GETINFO
+ # traffic/*') then that's ideal, but if not then just track the total for
+ # the time arm is run.
+
+ read_total = controller.get_info('traffic/read', None)
+ write_total = controller.get_info('traffic/written', None)
+ 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.start_time = start_time
+ else:
+ self.start_time = time.time()
+
+ def clone(self, new_copy = None):
+ if not new_copy:
+ new_copy = BandwidthStats(True)
+
+ new_copy._accounting_stats = self._accounting_stats
+ new_copy._title_stats = self._title_stats
+
+ return GraphStats.clone(self, new_copy)
+
+ def reset_listener(self, controller, event_type, _):
+ # updates title parameters and accounting status if they changed
+
+ self.new_desc_event(None) # updates title params
+
+ if event_type in (State.INIT, State.RESET) and CONFIG['features.graph.bw.accounting.show']:
+ is_accounting_enabled = controller.get_info('accounting/enabled', None) == '1'
+
+ if is_accounting_enabled != bool(self._accounting_stats):
+ self._accounting_stats = tor_controller().get_accounting_stats(None)
+
+ # redraws the whole screen since our height changed
+
+ arm.controller.get_controller().redraw()
+
+ # redraws to reflect changes (this especially noticeable when we have
+ # accounting and shut down since it then gives notice of the shutdown)
+
+ if self._graph_panel and self.is_selected:
+ self._graph_panel.redraw(True)
+
+ def prepopulate_from_state(self):
+ """
+ Attempts to use tor's state file to prepopulate values for the 15 minute
+ interval via the BWHistoryReadValues/BWHistoryWriteValues values. This
+ returns True if successful and False otherwise.
+ """
+
+ stats = bandwidth_from_state()
+
+ missing_read_entries = int((time.time() - stats.last_read_time) / 900)
+ missing_write_entries = int((time.time() - stats.last_write_time) / 900)
+
+ # fills missing entries with the last value
+
+ bw_read_entries = stats.read_entries + [stats.read_entries[-1]] * missing_read_entries
+ bw_write_entries = stats.write_entries + [stats.write_entries[-1]] * missing_write_entries
+
+ # crops starting entries so they're the same size
+
+ entry_count = min(len(bw_read_entries), len(bw_write_entries), self.max_column)
+ bw_read_entries = bw_read_entries[len(bw_read_entries) - entry_count:]
+ bw_write_entries = bw_write_entries[len(bw_write_entries) - entry_count:]
+
+ # gets index for 15-minute interval
+
+ interval_index = 0
+
+ for interval_rate in CONFIG['attr.graph.intervals'].values():
+ if int(interval_rate) == 900:
+ break
+ else:
+ interval_index += 1
+
+ # fills the graphing parameters with state information
+
+ 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.max_primary[interval_index] = max(self.primary_counts)
+ self.max_secondary[interval_index] = max(self.secondary_counts)
+
+ del self.primary_counts[interval_index][self.max_column + 1:]
+ del self.secondary_counts[interval_index][self.max_column + 1:]
+
+ return time.time() - min(stats.last_read_time, stats.last_write_time)
+
+ def bandwidth_event(self, event):
+ if self._accounting_stats and self.is_next_tick_redraw():
+ if time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE:
+ self._accounting_stats = tor_controller().get_accounting_stats(None)
+
+ # scales units from B to KB for graphing
+
+ self._process_event(event.read / 1024.0, event.written / 1024.0)
+
+ def draw(self, panel, width, height):
+ # line of the graph's x-axis labeling
+
+ labeling_line = GraphStats.get_content_height(self) + panel.graph_height - 2
+
+ # if display is narrow, overwrites x-axis labels with avg / total stats
+
+ if width <= COLLAPSE_WIDTH:
+ # clears line
+
+ panel.addstr(labeling_line, 0, ' ' * width)
+ graph_column = min((width - 10) / 2, self.max_column)
+
+ runtime = time.time() - self.start_time
+ primary_footer = 'total: %s, avg: %s/sec' % (_size_label(self.primary_total * 1024), _size_label(self.primary_total / runtime * 1024))
+ secondary_footer = 'total: %s, avg: %s/sec' % (_size_label(self.secondary_total * 1024), _size_label(self.secondary_total / runtime * 1024))
+
+ panel.addstr(labeling_line, 1, primary_footer, PRIMARY_COLOR)
+ panel.addstr(labeling_line, graph_column + 6, secondary_footer, SECONDARY_COLOR)
+
+ # provides accounting stats if enabled
+
+ if self._accounting_stats:
+ if tor_controller().is_alive():
+ hibernate_color = CONFIG['attr.hibernate_color'].get(self._accounting_stats.status, 'red')
+
+ x, y = 0, labeling_line + 2
+ x = panel.addstr(y, x, 'Accounting (', curses.A_BOLD)
+ x = panel.addstr(y, x, self._accounting_stats.status, curses.A_BOLD, hibernate_color)
+ x = panel.addstr(y, x, ')', curses.A_BOLD)
+
+ panel.addstr(y, 35, 'Time to reset: %s' % str_tools.short_time_label(self._accounting_stats.time_until_reset))
+
+ panel.addstr(y + 1, 2, '%s / %s' % (self._accounting_stats.read_bytes, self._accounting_stats.read_limit), PRIMARY_COLOR)
+ panel.addstr(y + 1, 37, '%s / %s' % (self._accounting_stats.written_bytes, self._accounting_stats.write_limit), SECONDARY_COLOR)
+ else:
+ panel.addstr(labeling_line + 2, 0, 'Accounting:', curses.A_BOLD)
+ panel.addstr(labeling_line + 2, 12, 'Connection Closed...')
+
+ def get_title(self, width):
+ stats_label = str_tools.join(self._title_stats, ', ', width - 13)
+ 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))]
+
+ # 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_label = str_tools.join(stats, '', width - 12)
+
+ if stats_label:
+ return 'Download (%s):' % stats_label
+ else:
+ return 'Download:'
+
+ def secondary_header(self, width):
+ stats = ['%-14s' % ('%s/sec' % _size_label(self.last_secondary * 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_label = str_tools.join(stats, '', width - 10)
+
+ if stats_label:
+ return 'Upload (%s):' % stats_label
+ else:
+ return 'Upload:'
+
+ def get_content_height(self):
+ base_height = GraphStats.get_content_height(self)
+ return base_height + 3 if self._accounting_stats else base_height
+
+ def new_desc_event(self, event):
+ controller = tor_controller()
+
+ if not controller.is_alive():
+ return # keep old values
+
+ my_fingerprint = controller.get_info('fingerprint', None)
+
+ if not event or (my_fingerprint and my_fingerprint in [fp for fp, _ in event.relays]):
+ stats = []
+
+ bw_rate = controller.get_effective_rate(None)
+ bw_burst = controller.get_effective_rate(None, burst = True)
+
+ if bw_rate and bw_burst:
+ bw_rate_label = _size_label(bw_rate)
+ bw_burst_label = _size_label(bw_burst)
+
+ # if both are using rounded values then strip off the '.0' decimal
+
+ if '.0' in bw_rate_label and '.0' in bw_burst_label:
+ bw_rate_label = bw_rate_label.split('.', 1)[0]
+ bw_burst_label = bw_burst_label.split('.', 1)[0]
+
+ stats.append('limit: %s/s' % bw_rate_label)
+ stats.append('burst: %s/s' % bw_burst_label)
+
+ my_router_status_entry = controller.get_network_status(default = None)
+ measured_bw = getattr(my_router_status_entry, 'bandwidth', None)
+
+ if measured_bw:
+ stats.append('measured: %s/s' % _size_label(measured_bw))
+ else:
+ my_server_descriptor = controller.get_server_descriptor(default = None)
+ observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None)
+
+ if observed_bw:
+ stats.append('observed: %s/s' % _size_label(observed_bw))
+
+ self._title_stats = stats
+
+
+class ConnStats(GraphStats):
+ """
+ Tracks number of connections, counting client and directory connections as
+ outbound. Control connections are excluded from counts.
+ """
+
+ def clone(self, new_copy=None):
+ if not new_copy:
+ new_copy = ConnStats()
+
+ return GraphStats.clone(self, new_copy)
+
+ def event_tick(self):
+ """
+ Fetches connection stats from cached information.
+ """
+
+ inbound_count, outbound_count = 0, 0
+
+ controller = tor_controller()
+
+ or_ports = controller.get_ports(Listener.OR)
+ dir_ports = controller.get_ports(Listener.DIR)
+ control_ports = controller.get_ports(Listener.CONTROL)
+
+ for entry in arm.util.tracker.get_connection_tracker().get_value():
+ local_port = entry.local_port
+
+ if local_port in or_ports or local_port in dir_ports:
+ inbound_count += 1
+ elif local_port in control_ports:
+ pass # control connection
+ else:
+ outbound_count += 1
+
+ self._process_event(inbound_count, outbound_count)
+
+ def get_title(self, width):
+ 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)
+
+ def secondary_header(self, width):
+ avg = self.secondary_total / max(1, self.tick)
+ return 'Outbound (%s, avg: %s):' % (self.last_secondary, avg)
+
+
+class ResourceStats(GraphStats):
+ """
+ System resource usage tracker.
+ """
+
+ def __init__(self):
+ GraphStats.__init__(self)
+ self._last_counter = None
+
+ def clone(self, new_copy=None):
+ if not new_copy:
+ new_copy = ResourceStats()
+
+ return GraphStats.clone(self, new_copy)
+
+ def get_title(self, width):
+ 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)
+
+ 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)
+
+ avg = self.secondary_total / max(1, self.tick)
+ avg_label = str_tools.size_label(avg * 1048576, 1)
+
+ return 'Memory (%s, avg: %s):' % (usage_label, avg_label)
+
+ def event_tick(self):
+ """
+ Fetch the cached measurement of resource usage from the ResourceTracker.
+ """
+
+ resource_tracker = arm.util.tracker.get_resource_tracker()
+
+ if resource_tracker and resource_tracker.run_counter() != self._last_counter:
+ resources = resource_tracker.get_value()
+ primary = resources.cpu_sample * 100 # decimal percentage to whole numbers
+ secondary = resources.memory_bytes / 1048576 # translate size to MB so axis labels are short
+
+ self._last_counter = resource_tracker.run_counter()
+ self._process_event(primary, secondary)
+
+
+class GraphPanel(panel.Panel):
+ """
+ Panel displaying a graph, drawing statistics from custom GraphStats
+ implementations.
+ """
+
+ def __init__(self, stdscr):
+ panel.Panel.__init__(self, stdscr, 'graph', 0)
+ self.update_interval = CONFIG['features.graph.interval']
+
+ if self.update_interval < 0 or self.update_interval > len(CONFIG['attr.graph.intervals']) - 1:
+ self.update_interval = 0 # user configured it with a value that's out of bounds
+
+ self.bounds = list(Bounds)[CONFIG['features.graph.bound']]
+ self.graph_height = CONFIG['features.graph.height']
+ self.current_display = None # label of the stats currently being displayed
+
+ self.stats = {
+ GraphStat.BANDWIDTH: BandwidthStats(),
+ GraphStat.SYSTEM_RESOURCES: ResourceStats(),
+ }
+
+ if CONFIG['features.panels.show.connection']:
+ self.stats[GraphStat.CONNECTIONS] = ConnStats()
+
+ for stat in self.stats.values():
+ stat._graph_panel = self
+
+ self.set_pause_attr('stats')
+
+ try:
+ initial_stats = GRAPH_INIT_STATS.get(CONFIG['features.graph.type'])
+ self.set_stats(initial_stats)
+ except ValueError:
+ pass # invalid stats, maybe connections when lookups are disabled
+
+ # prepopulates bandwidth values from state file
+
+ if CONFIG["features.graph.bw.prepopulate"] and tor_controller().is_alive():
+ try:
+ missing_seconds = self.stats[GraphStat.BANDWIDTH].prepopulate_from_state()
+
+ if missing_seconds:
+ log.notice(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(missing_seconds, 0, True)))
+ else:
+ log.notice(msg('panel.graphing.prepopulation_all_successful'))
+
+ self.update_interval = 4
+ except ValueError as exc:
+ log.info(msg('panel.graphing.prepopulation_failure', error = str(exc)))
+
+ def get_update_interval(self):
+ """
+ Provides the rate that we update the graph at.
+ """
+
+ return self.update_interval
+
+ def set_update_interval(self, update_interval):
+ """
+ Sets the rate that we update the graph at.
+
+ Arguments:
+ update_interval - update time enum
+ """
+
+ self.update_interval = update_interval
+
+ def get_bounds_type(self):
+ """
+ Provides the type of graph bounds used.
+ """
+
+ return self.bounds
+
+ def set_bounds_type(self, bounds_type):
+ """
+ Sets the type of graph boundaries we use.
+
+ Arguments:
+ bounds_type - graph bounds enum
+ """
+
+ self.bounds = bounds_type
+
+ def get_height(self):
+ """
+ Provides the height requested by the currently displayed GraphStats (zero
+ if hidden).
+ """
+
+ if self.current_display:
+ return self.stats[self.current_display].get_content_height() + self.graph_height
+ else:
+ return 0
+
+ def set_graph_height(self, new_graph_height):
+ """
+ Sets the preferred height used for the graph (restricted to the
+ MIN_GRAPH_HEIGHT minimum).
+
+ Arguments:
+ new_graph_height - new height for the graph
+ """
+
+ self.graph_height = max(MIN_GRAPH_HEIGHT, new_graph_height)
+
+ def resize_graph(self):
+ """
+ Prompts for user input to resize the graph panel. Options include...
+ down arrow - grow graph
+ up arrow - shrink graph
+ enter / space - set size
+ """
+
+ control = arm.controller.get_controller()
+
+ with panel.CURSES_LOCK:
+ try:
+ while True:
+ msg = 'press the down/up to resize the graph, and enter when done'
+ control.set_msg(msg, curses.A_BOLD, True)
+ curses.cbreak()
+ key = control.key_input()
+
+ if key.match('down'):
+ # don't grow the graph if it's already consuming the whole display
+ # (plus an extra line for the graph/log gap)
+
+ max_height = self.parent.getmaxyx()[0] - self.top
+ current_height = self.get_height()
+
+ if current_height < max_height + 1:
+ self.set_graph_height(self.graph_height + 1)
+ elif key.match('up'):
+ self.set_graph_height(self.graph_height - 1)
+ elif key.is_selection():
+ break
+
+ control.redraw()
+ finally:
+ control.set_msg()
+
+ def handle_key(self, key):
+ if key.match('r'):
+ self.resize_graph()
+ elif key.match('b'):
+ # uses the next boundary type
+ self.bounds = Bounds.next(self.bounds)
+ self.redraw(True)
+ elif key.match('s'):
+ # provides a menu to pick the graphed stats
+
+ available_stats = self.stats.keys()
+ available_stats.sort()
+
+ # uses sorted, camel cased labels for the options
+
+ options = ['None']
+
+ for label in available_stats:
+ words = label.split()
+ options.append(' '.join(word[0].upper() + word[1:] for word in words))
+
+ if self.current_display:
+ initial_selection = available_stats.index(self.current_display) + 1
+ else:
+ initial_selection = 0
+
+ selection = arm.popups.show_menu('Graphed Stats:', options, initial_selection)
+
+ # applies new setting
+
+ if selection == 0:
+ self.set_stats(None)
+ elif selection != -1:
+ self.set_stats(available_stats[selection - 1])
+ elif key.match('i'):
+ # provides menu to pick graph panel update interval
+
+ options = CONFIG['attr.graph.intervals'].keys()
+ selection = arm.popups.show_menu('Update Interval:', options, self.update_interval)
+
+ if selection != -1:
+ self.update_interval = selection
+ else:
+ return False
+
+ return True
+
+ def get_help(self):
+ return [
+ ('r', 'resize graph', None),
+ ('s', 'graphed stats', self.current_display if self.current_display else 'none'),
+ ('b', 'graph bounds', self.bounds.lower()),
+ ('i', 'graph update interval', CONFIG['attr.graph.intervals'].keys()[self.update_interval]),
+ ]
+
+ def draw(self, width, height):
+ if not self.current_display:
+ return
+
+ param = self.get_attr('stats')[self.current_display]
+ graph_column = min((width - 10) / 2, param.max_column)
+
+ if self.is_title_visible():
+ self.addstr(0, 0, param.get_title(width), curses.A_STANDOUT)
+
+ # top labels
+
+ left, right = param.primary_header(width / 2), param.secondary_header(width / 2)
+
+ if left:
+ self.addstr(1, 0, left, curses.A_BOLD, PRIMARY_COLOR)
+
+ if right:
+ self.addstr(1, graph_column + 5, right, curses.A_BOLD, SECONDARY_COLOR)
+
+ # determines max/min value on the graph
+
+ 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])
+ 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_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]))
+
+ # if the max = min (ie, all values are the same) then use zero lower
+ # bound so a graph is still displayed
+
+ if primary_min_bound == primary_max_bound:
+ primary_min_bound = 0
+
+ if secondary_min_bound == secondary_max_bound:
+ secondary_min_bound = 0
+
+ # displays upper and lower bounds
+
+ self.addstr(2, 0, '%4i' % primary_max_bound, PRIMARY_COLOR)
+ self.addstr(self.graph_height + 1, 0, '%4i' % primary_min_bound, PRIMARY_COLOR)
+
+ self.addstr(2, graph_column + 5, '%4i' % secondary_max_bound, SECONDARY_COLOR)
+ self.addstr(self.graph_height + 1, graph_column + 5, '%4i' % secondary_min_bound, SECONDARY_COLOR)
+
+ # displays intermediate bounds on every other row
+
+ if CONFIG['features.graph.showIntermediateBounds']:
+ ticks = (self.graph_height - 3) / 2
+
+ for i in range(ticks):
+ row = self.graph_height - (2 * i) - 3
+
+ if self.graph_height % 2 == 0 and i >= (ticks / 2):
+ row -= 1
+
+ if primary_min_bound != primary_max_bound:
+ primary_val = (primary_max_bound - primary_min_bound) * (self.graph_height - row - 1) / (self.graph_height - 1)
+
+ if primary_val not in (primary_min_bound, primary_max_bound):
+ self.addstr(row + 2, 0, '%4i' % primary_val, PRIMARY_COLOR)
+
+ if secondary_min_bound != secondary_max_bound:
+ secondary_val = (secondary_max_bound - secondary_min_bound) * (self.graph_height - row - 1) / (self.graph_height - 1)
+
+ if secondary_val not in (secondary_min_bound, secondary_max_bound):
+ self.addstr(row + 2, graph_column + 5, '%4i' % secondary_val, SECONDARY_COLOR)
+
+ # 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))
+
+ 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))
+
+ for row in range(column_height):
+ self.addstr(self.graph_height + 1 - row, col + graph_column + 10, ' ', curses.A_STANDOUT, SECONDARY_COLOR)
+
+ # bottom labeling of x-axis
+
+ interval_sec = int(CONFIG['attr.graph.intervals'].values()[self.update_interval]) # seconds per labeling
+
+ interval_spacing = 10 if graph_column >= WIDE_LABELING_GRAPH_COL else 5
+ units_label, decimal_precision = None, 0
+
+ for i in range((graph_column - 4) / interval_spacing):
+ loc = (i + 1) * interval_spacing
+ time_label = str_tools.time_label(loc * interval_sec, decimal_precision)
+
+ if not units_label:
+ units_label = time_label[-1]
+ elif units_label != time_label[-1]:
+ # upped scale so also up precision of future measurements
+ units_label = time_label[-1]
+ decimal_precision += 1
+ else:
+ # if constrained on space then strips labeling since already provided
+ time_label = time_label[:-1]
+
+ self.addstr(self.graph_height + 2, 4 + loc, time_label, PRIMARY_COLOR)
+ self.addstr(self.graph_height + 2, graph_column + 10 + loc, time_label, SECONDARY_COLOR)
+
+ param.draw(self, width, height) # allows current stats to modify the display
+
+ def get_stats(self):
+ """
+ Provides the currently selected stats label.
+ """
+
+ return self.current_display
+
+ def set_stats(self, label):
+ """
+ Sets the currently displayed stats instance, hiding panel if None.
+ """
+
+ if label != self.current_display:
+ if self.current_display:
+ self.stats[self.current_display].is_selected = False
+
+ if not label:
+ self.current_display = None
+ elif label in self.stats.keys():
+ self.current_display = label
+ self.stats[self.current_display].is_selected = True
+ else:
+ raise ValueError('Unrecognized stats label: %s' % label)
+
+ def copy_attr(self, attr):
+ if attr == 'stats':
+ # uses custom clone method to copy GraphStats instances
+ return dict([(key, self.stats[key].clone()) for key in self.stats])
+ else:
+ return panel.Panel.copy_attr(self, attr)
+
+
+def _size_label(byte_count):
+ return str_tools.size_label(byte_count, 1, is_bytes = CONFIG['features.graph.bw.transferInBytes'])
diff --git a/arm/graphing/__init__.py b/arm/graphing/__init__.py
deleted file mode 100644
index 74f7e07..0000000
--- a/arm/graphing/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-Graphing panel resources.
-"""
-
-__all__ = [
- 'bandwidth_stats',
- 'conn_stats',
- 'graph_panel',
- 'resource_stats',
-]
diff --git a/arm/graphing/bandwidth_stats.py b/arm/graphing/bandwidth_stats.py
deleted file mode 100644
index 4d52aae..0000000
--- a/arm/graphing/bandwidth_stats.py
+++ /dev/null
@@ -1,285 +0,0 @@
-"""
-Tracks bandwidth usage of the tor process, expanding to include accounting
-stats if they're set.
-"""
-
-import time
-import curses
-
-import arm.controller
-
-from arm.graphing import graph_panel
-from arm.util import bandwidth_from_state, tor_controller
-
-from stem.control import State
-from stem.util import conf, str_tools, system
-
-ACCOUNTING_RATE = 5
-
-CONFIG = conf.config_dict('arm', {
- 'attr.hibernate_color': {},
- 'attr.graph.intervals': {},
- 'features.graph.bw.transferInBytes': False,
- 'features.graph.bw.accounting.show': True,
- 'tor.chroot': '',
-})
-
-# width at which panel abandons placing optional stats (avg and total) with
-# header in favor of replacing the x-axis label
-
-COLLAPSE_WIDTH = 135
-
-
-class BandwidthStats(graph_panel.GraphStats):
- """
- Uses tor BW events to generate bandwidth usage graph.
- """
-
- def __init__(self, is_pause_buffer = False):
- graph_panel.GraphStats.__init__(self)
-
- # listens for tor reload (sighup) events which can reset the bandwidth
- # rate/burst and if tor's using accounting
-
- controller = tor_controller()
- self._title_stats = []
- self._accounting_stats = None
-
- if not is_pause_buffer:
- self.reset_listener(controller, State.INIT, None) # initializes values
-
- controller.add_status_listener(self.reset_listener)
- self.new_desc_event(None) # updates title params
-
- # We both show our 'total' attributes and use it to determine our average.
- #
- # If we can get *both* our start time and the totals from tor (via 'GETINFO
- # traffic/*') then that's ideal, but if not then just track the total for
- # the time arm is run.
-
- read_total = controller.get_info('traffic/read', None)
- write_total = controller.get_info('traffic/written', None)
- 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.start_time = start_time
- else:
- self.start_time = time.time()
-
- def clone(self, new_copy = None):
- if not new_copy:
- new_copy = BandwidthStats(True)
-
- new_copy._accounting_stats = self._accounting_stats
- new_copy._title_stats = self._title_stats
-
- return graph_panel.GraphStats.clone(self, new_copy)
-
- def reset_listener(self, controller, event_type, _):
- # updates title parameters and accounting status if they changed
-
- self.new_desc_event(None) # updates title params
-
- if event_type in (State.INIT, State.RESET) and CONFIG['features.graph.bw.accounting.show']:
- is_accounting_enabled = controller.get_info('accounting/enabled', None) == '1'
-
- if is_accounting_enabled != bool(self._accounting_stats):
- self._accounting_stats = tor_controller().get_accounting_stats(None)
-
- # redraws the whole screen since our height changed
-
- arm.controller.get_controller().redraw()
-
- # redraws to reflect changes (this especially noticeable when we have
- # accounting and shut down since it then gives notice of the shutdown)
-
- if self._graph_panel and self.is_selected:
- self._graph_panel.redraw(True)
-
- def prepopulate_from_state(self):
- """
- Attempts to use tor's state file to prepopulate values for the 15 minute
- interval via the BWHistoryReadValues/BWHistoryWriteValues values. This
- returns True if successful and False otherwise.
- """
-
- stats = bandwidth_from_state()
-
- missing_read_entries = int((time.time() - stats.last_read_time) / 900)
- missing_write_entries = int((time.time() - stats.last_write_time) / 900)
-
- # fills missing entries with the last value
-
- bw_read_entries = stats.read_entries + [stats.read_entries[-1]] * missing_read_entries
- bw_write_entries = stats.write_entries + [stats.write_entries[-1]] * missing_write_entries
-
- # crops starting entries so they're the same size
-
- entry_count = min(len(bw_read_entries), len(bw_write_entries), self.max_column)
- bw_read_entries = bw_read_entries[len(bw_read_entries) - entry_count:]
- bw_write_entries = bw_write_entries[len(bw_write_entries) - entry_count:]
-
- # gets index for 15-minute interval
-
- interval_index = 0
-
- for interval_rate in CONFIG['attr.graph.intervals'].values():
- if int(interval_rate) == 900:
- break
- else:
- interval_index += 1
-
- # fills the graphing parameters with state information
-
- 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.max_primary[interval_index] = max(self.primary_counts)
- self.max_secondary[interval_index] = max(self.secondary_counts)
-
- del self.primary_counts[interval_index][self.max_column + 1:]
- del self.secondary_counts[interval_index][self.max_column + 1:]
-
- return time.time() - min(stats.last_read_time, stats.last_write_time)
-
- def bandwidth_event(self, event):
- if self._accounting_stats and self.is_next_tick_redraw():
- if time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE:
- self._accounting_stats = tor_controller().get_accounting_stats(None)
-
- # scales units from B to KB for graphing
-
- self._process_event(event.read / 1024.0, event.written / 1024.0)
-
- def draw(self, panel, width, height):
- # line of the graph's x-axis labeling
-
- labeling_line = graph_panel.GraphStats.get_content_height(self) + panel.graph_height - 2
-
- # if display is narrow, overwrites x-axis labels with avg / total stats
-
- if width <= COLLAPSE_WIDTH:
- # clears line
-
- panel.addstr(labeling_line, 0, ' ' * width)
- graph_column = min((width - 10) / 2, self.max_column)
-
- runtime = time.time() - self.start_time
- primary_footer = 'total: %s, avg: %s/sec' % (_size_label(self.primary_total * 1024), _size_label(self.primary_total / runtime * 1024))
- secondary_footer = 'total: %s, avg: %s/sec' % (_size_label(self.secondary_total * 1024), _size_label(self.secondary_total / runtime * 1024))
-
- panel.addstr(labeling_line, 1, primary_footer, graph_panel.PRIMARY_COLOR)
- panel.addstr(labeling_line, graph_column + 6, secondary_footer, graph_panel.SECONDARY_COLOR)
-
- # provides accounting stats if enabled
-
- if self._accounting_stats:
- if tor_controller().is_alive():
- hibernate_color = CONFIG['attr.hibernate_color'].get(self._accounting_stats.status, 'red')
-
- x, y = 0, labeling_line + 2
- x = panel.addstr(y, x, 'Accounting (', curses.A_BOLD)
- x = panel.addstr(y, x, self._accounting_stats.status, curses.A_BOLD, hibernate_color)
- x = panel.addstr(y, x, ')', curses.A_BOLD)
-
- panel.addstr(y, 35, 'Time to reset: %s' % str_tools.short_time_label(self._accounting_stats.time_until_reset))
-
- panel.addstr(y + 1, 2, '%s / %s' % (self._accounting_stats.read_bytes, self._accounting_stats.read_limit), graph_panel.PRIMARY_COLOR)
- panel.addstr(y + 1, 37, '%s / %s' % (self._accounting_stats.written_bytes, self._accounting_stats.write_limit), graph_panel.SECONDARY_COLOR)
- else:
- panel.addstr(labeling_line + 2, 0, 'Accounting:', curses.A_BOLD)
- panel.addstr(labeling_line + 2, 12, 'Connection Closed...')
-
- def get_title(self, width):
- stats_label = str_tools.join(self._title_stats, ', ', width - 13)
- 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))]
-
- # 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_label = str_tools.join(stats, '', width - 12)
-
- if stats_label:
- return 'Download (%s):' % stats_label
- else:
- return 'Download:'
-
- def secondary_header(self, width):
- stats = ['%-14s' % ('%s/sec' % _size_label(self.last_secondary * 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_label = str_tools.join(stats, '', width - 10)
-
- if stats_label:
- return 'Upload (%s):' % stats_label
- else:
- return 'Upload:'
-
- def get_content_height(self):
- base_height = graph_panel.GraphStats.get_content_height(self)
- return base_height + 3 if self._accounting_stats else base_height
-
- def new_desc_event(self, event):
- controller = tor_controller()
-
- if not controller.is_alive():
- return # keep old values
-
- my_fingerprint = controller.get_info('fingerprint', None)
-
- if not event or (my_fingerprint and my_fingerprint in [fp for fp, _ in event.relays]):
- stats = []
-
- bw_rate = controller.get_effective_rate(None)
- bw_burst = controller.get_effective_rate(None, burst = True)
-
- if bw_rate and bw_burst:
- bw_rate_label = _size_label(bw_rate)
- bw_burst_label = _size_label(bw_burst)
-
- # if both are using rounded values then strip off the '.0' decimal
-
- if '.0' in bw_rate_label and '.0' in bw_burst_label:
- bw_rate_label = bw_rate_label.split('.', 1)[0]
- bw_burst_label = bw_burst_label.split('.', 1)[0]
-
- stats.append('limit: %s/s' % bw_rate_label)
- stats.append('burst: %s/s' % bw_burst_label)
-
- my_router_status_entry = controller.get_network_status(default = None)
- measured_bw = getattr(my_router_status_entry, 'bandwidth', None)
-
- if measured_bw:
- stats.append('measured: %s/s' % _size_label(measured_bw))
- else:
- my_server_descriptor = controller.get_server_descriptor(default = None)
- observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None)
-
- if observed_bw:
- stats.append('observed: %s/s' % _size_label(observed_bw))
-
- self._title_stats = stats
-
-
-def _size_label(byte_count):
- return str_tools.size_label(byte_count, 1, is_bytes = CONFIG['features.graph.bw.transferInBytes'])
diff --git a/arm/graphing/conn_stats.py b/arm/graphing/conn_stats.py
deleted file mode 100644
index c5b1c83..0000000
--- a/arm/graphing/conn_stats.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""
-Tracks stats concerning tor's current connections.
-"""
-
-import arm.util.tracker
-
-from arm.graphing import graph_panel
-from arm.util import tor_controller
-
-from stem.control import Listener
-
-
-class ConnStats(graph_panel.GraphStats):
- """
- Tracks number of connections, counting client and directory connections as
- outbound. Control connections are excluded from counts.
- """
-
- def clone(self, new_copy=None):
- if not new_copy:
- new_copy = ConnStats()
-
- return graph_panel.GraphStats.clone(self, new_copy)
-
- def event_tick(self):
- """
- Fetches connection stats from cached information.
- """
-
- inbound_count, outbound_count = 0, 0
-
- controller = tor_controller()
-
- or_ports = controller.get_ports(Listener.OR)
- dir_ports = controller.get_ports(Listener.DIR)
- control_ports = controller.get_ports(Listener.CONTROL)
-
- for entry in arm.util.tracker.get_connection_tracker().get_value():
- local_port = entry.local_port
-
- if local_port in or_ports or local_port in dir_ports:
- inbound_count += 1
- elif local_port in control_ports:
- pass # control connection
- else:
- outbound_count += 1
-
- self._process_event(inbound_count, outbound_count)
-
- def get_title(self, width):
- 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)
-
- def secondary_header(self, width):
- avg = self.secondary_total / max(1, self.tick)
- return 'Outbound (%s, avg: %s):' % (self.last_secondary, avg)
diff --git a/arm/graphing/graph_panel.py b/arm/graphing/graph_panel.py
deleted file mode 100644
index 77f9404..0000000
--- a/arm/graphing/graph_panel.py
+++ /dev/null
@@ -1,581 +0,0 @@
-"""
-Flexible panel for presenting bar graphs for a variety of stats. This panel is
-just concerned with the rendering of information, which is actually collected
-and stored by implementations of the GraphStats interface. Panels are made up
-of a title, followed by headers and graphs for two sets of stats. For
-instance...
-
-Bandwidth (cap: 5 MB, burst: 10 MB):
-Downloaded (0.0 B/sec): Uploaded (0.0 B/sec):
- 34 30
- * *
- ** * * * **
- * * * ** ** ** *** ** ** ** **
- ********* ****** ****** ********* ****** ******
- 0 ************ **************** 0 ************ ****************
- 25s 50 1m 1.6 2.0 25s 50 1m 1.6 2.0
-"""
-
-import copy
-import curses
-
-import arm.popups
-import arm.controller
-
-import stem.control
-
-from arm.util import msg, panel, tor_controller
-
-from stem.util import conf, enum, log, str_tools
-
-GraphStat = enum.Enum('BANDWIDTH', 'CONNECTIONS', 'SYSTEM_RESOURCES')
-
-# maps 'features.graph.type' config values to the initial types
-
-GRAPH_INIT_STATS = {1: GraphStat.BANDWIDTH, 2: GraphStat.CONNECTIONS, 3: GraphStat.SYSTEM_RESOURCES}
-
-DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
-PRIMARY_COLOR, SECONDARY_COLOR = 'green', 'cyan'
-MIN_GRAPH_HEIGHT = 1
-
-# enums for graph bounds:
-# Bounds.GLOBAL_MAX - global maximum (highest value ever seen)
-# Bounds.LOCAL_MAX - local maximum (highest value currently on the graph)
-# Bounds.TIGHT - local maximum and minimum
-
-Bounds = enum.Enum('GLOBAL_MAX', 'LOCAL_MAX', 'TIGHT')
-
-WIDE_LABELING_GRAPH_COL = 50 # minimum graph columns to use wide spacing for x-axis labels
-
-
-def conf_handler(key, value):
- if key == 'features.graph.height':
- return max(MIN_GRAPH_HEIGHT, value)
- elif key == 'features.graph.max_width':
- return max(1, value)
- elif key == 'features.graph.bound':
- return max(0, min(2, value))
-
-
-# used for setting defaults when initializing GraphStats and GraphPanel instances
-
-CONFIG = conf.config_dict('arm', {
- 'attr.graph.intervals': {},
- 'features.graph.height': 7,
- 'features.graph.interval': 0,
- 'features.graph.bound': 1,
- 'features.graph.max_width': 150,
- 'features.graph.showIntermediateBounds': True,
- 'features.graph.type': 1,
- 'features.panels.show.connection': True,
- 'features.graph.bw.prepopulate': True,
-}, conf_handler)
-
-
-class GraphStats:
- """
- Module that's expected to update dynamically and provide attributes to be
- graphed. Up to two graphs (a 'primary' and 'secondary') can be displayed at a
- time and timescale parameters use the labels defined in CONFIG['attr.graph.intervals'].
- """
-
- def __init__(self):
- """
- Initializes parameters needed to present a graph.
- """
-
- # panel to be redrawn when updated (set when added to GraphPanel)
-
- self._graph_panel = None
- 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]
-
- # tracks BW events
-
- tor_controller().add_event_listener(self.bandwidth_event, stem.control.EventType.BW)
-
- def clone(self, new_copy=None):
- """
- Provides a deep copy of this instance.
-
- Arguments:
- new_copy - base instance to build copy off of
- """
-
- 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.is_pause_buffer = True
- return new_copy
-
- def event_tick(self):
- """
- Called when it's time to process another event. All graphs use tor BW
- events to keep in sync with each other (this happens once a second).
- """
-
- pass
-
- def is_next_tick_redraw(self):
- """
- Provides true if the following tick (call to _process_event) will result in
- being redrawn.
- """
-
- 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
- else:
- return False
-
- def get_title(self, width):
- """
- Provides top label.
- """
-
- return ''
-
- def primary_header(self, width):
- return ''
-
- def secondary_header(self, width):
- return ''
-
- def get_content_height(self):
- """
- Provides the height content should take up (not including the graph).
- """
-
- return DEFAULT_CONTENT_HEIGHT
-
- def draw(self, panel, width, height):
- """
- Allows for any custom drawing monitor wishes to append.
- """
-
- pass
-
- def bandwidth_event(self, event):
- if not self.is_pause_buffer:
- self.event_tick()
-
- def _process_event(self, primary, secondary):
- """
- Includes new stats in graphs and notifies associated GraphPanel of changes.
- """
-
- 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:]
-
- if is_redraw and self._graph_panel:
- self._graph_panel.redraw(True)
-
-
-class GraphPanel(panel.Panel):
- """
- Panel displaying a graph, drawing statistics from custom GraphStats
- implementations.
- """
-
- def __init__(self, stdscr):
- panel.Panel.__init__(self, stdscr, 'graph', 0)
- self.update_interval = CONFIG['features.graph.interval']
-
- if self.update_interval < 0 or self.update_interval > len(CONFIG['attr.graph.intervals']) - 1:
- self.update_interval = 0 # user configured it with a value that's out of bounds
-
- self.bounds = list(Bounds)[CONFIG['features.graph.bound']]
- self.graph_height = CONFIG['features.graph.height']
- self.current_display = None # label of the stats currently being displayed
-
- self.stats = {
- GraphStat.BANDWIDTH: arm.graphing.bandwidth_stats.BandwidthStats(),
- GraphStat.SYSTEM_RESOURCES: arm.graphing.resource_stats.ResourceStats(),
- }
-
- if CONFIG['features.panels.show.connection']:
- self.stats[GraphStat.CONNECTIONS] = arm.graphing.conn_stats.ConnStats()
-
- for stat in self.stats.values():
- stat._graph_panel = self
-
- self.set_pause_attr('stats')
-
- try:
- initial_stats = GRAPH_INIT_STATS.get(CONFIG['features.graph.type'])
- self.set_stats(initial_stats)
- except ValueError:
- pass # invalid stats, maybe connections when lookups are disabled
-
- # prepopulates bandwidth values from state file
-
- if CONFIG["features.graph.bw.prepopulate"] and tor_controller().is_alive():
- try:
- missing_seconds = self.stats[GraphStat.BANDWIDTH].prepopulate_from_state()
-
- if missing_seconds:
- log.notice(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(missing_seconds, 0, True)))
- else:
- log.notice(msg('panel.graphing.prepopulation_all_successful'))
-
- self.update_interval = 4
- except ValueError as exc:
- log.info(msg('panel.graphing.prepopulation_failure', error = str(exc)))
-
- def get_update_interval(self):
- """
- Provides the rate that we update the graph at.
- """
-
- return self.update_interval
-
- def set_update_interval(self, update_interval):
- """
- Sets the rate that we update the graph at.
-
- Arguments:
- update_interval - update time enum
- """
-
- self.update_interval = update_interval
-
- def get_bounds_type(self):
- """
- Provides the type of graph bounds used.
- """
-
- return self.bounds
-
- def set_bounds_type(self, bounds_type):
- """
- Sets the type of graph boundaries we use.
-
- Arguments:
- bounds_type - graph bounds enum
- """
-
- self.bounds = bounds_type
-
- def get_height(self):
- """
- Provides the height requested by the currently displayed GraphStats (zero
- if hidden).
- """
-
- if self.current_display:
- return self.stats[self.current_display].get_content_height() + self.graph_height
- else:
- return 0
-
- def set_graph_height(self, new_graph_height):
- """
- Sets the preferred height used for the graph (restricted to the
- MIN_GRAPH_HEIGHT minimum).
-
- Arguments:
- new_graph_height - new height for the graph
- """
-
- self.graph_height = max(MIN_GRAPH_HEIGHT, new_graph_height)
-
- def resize_graph(self):
- """
- Prompts for user input to resize the graph panel. Options include...
- down arrow - grow graph
- up arrow - shrink graph
- enter / space - set size
- """
-
- control = arm.controller.get_controller()
-
- with panel.CURSES_LOCK:
- try:
- while True:
- msg = 'press the down/up to resize the graph, and enter when done'
- control.set_msg(msg, curses.A_BOLD, True)
- curses.cbreak()
- key = control.key_input()
-
- if key.match('down'):
- # don't grow the graph if it's already consuming the whole display
- # (plus an extra line for the graph/log gap)
-
- max_height = self.parent.getmaxyx()[0] - self.top
- current_height = self.get_height()
-
- if current_height < max_height + 1:
- self.set_graph_height(self.graph_height + 1)
- elif key.match('up'):
- self.set_graph_height(self.graph_height - 1)
- elif key.is_selection():
- break
-
- control.redraw()
- finally:
- control.set_msg()
-
- def handle_key(self, key):
- if key.match('r'):
- self.resize_graph()
- elif key.match('b'):
- # uses the next boundary type
- self.bounds = Bounds.next(self.bounds)
- self.redraw(True)
- elif key.match('s'):
- # provides a menu to pick the graphed stats
-
- available_stats = self.stats.keys()
- available_stats.sort()
-
- # uses sorted, camel cased labels for the options
-
- options = ['None']
-
- for label in available_stats:
- words = label.split()
- options.append(' '.join(word[0].upper() + word[1:] for word in words))
-
- if self.current_display:
- initial_selection = available_stats.index(self.current_display) + 1
- else:
- initial_selection = 0
-
- selection = arm.popups.show_menu('Graphed Stats:', options, initial_selection)
-
- # applies new setting
-
- if selection == 0:
- self.set_stats(None)
- elif selection != -1:
- self.set_stats(available_stats[selection - 1])
- elif key.match('i'):
- # provides menu to pick graph panel update interval
-
- options = CONFIG['attr.graph.intervals'].keys()
- selection = arm.popups.show_menu('Update Interval:', options, self.update_interval)
-
- if selection != -1:
- self.update_interval = selection
- else:
- return False
-
- return True
-
- def get_help(self):
- return [
- ('r', 'resize graph', None),
- ('s', 'graphed stats', self.current_display if self.current_display else 'none'),
- ('b', 'graph bounds', self.bounds.lower()),
- ('i', 'graph update interval', CONFIG['attr.graph.intervals'].keys()[self.update_interval]),
- ]
-
- def draw(self, width, height):
- if not self.current_display:
- return
-
- param = self.get_attr('stats')[self.current_display]
- graph_column = min((width - 10) / 2, param.max_column)
-
- if self.is_title_visible():
- self.addstr(0, 0, param.get_title(width), curses.A_STANDOUT)
-
- # top labels
-
- left, right = param.primary_header(width / 2), param.secondary_header(width / 2)
-
- if left:
- self.addstr(1, 0, left, curses.A_BOLD, PRIMARY_COLOR)
-
- if right:
- self.addstr(1, graph_column + 5, right, curses.A_BOLD, SECONDARY_COLOR)
-
- # determines max/min value on the graph
-
- 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])
- 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_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]))
-
- # if the max = min (ie, all values are the same) then use zero lower
- # bound so a graph is still displayed
-
- if primary_min_bound == primary_max_bound:
- primary_min_bound = 0
-
- if secondary_min_bound == secondary_max_bound:
- secondary_min_bound = 0
-
- # displays upper and lower bounds
-
- self.addstr(2, 0, '%4i' % primary_max_bound, PRIMARY_COLOR)
- self.addstr(self.graph_height + 1, 0, '%4i' % primary_min_bound, PRIMARY_COLOR)
-
- self.addstr(2, graph_column + 5, '%4i' % secondary_max_bound, SECONDARY_COLOR)
- self.addstr(self.graph_height + 1, graph_column + 5, '%4i' % secondary_min_bound, SECONDARY_COLOR)
-
- # displays intermediate bounds on every other row
-
- if CONFIG['features.graph.showIntermediateBounds']:
- ticks = (self.graph_height - 3) / 2
-
- for i in range(ticks):
- row = self.graph_height - (2 * i) - 3
-
- if self.graph_height % 2 == 0 and i >= (ticks / 2):
- row -= 1
-
- if primary_min_bound != primary_max_bound:
- primary_val = (primary_max_bound - primary_min_bound) * (self.graph_height - row - 1) / (self.graph_height - 1)
-
- if primary_val not in (primary_min_bound, primary_max_bound):
- self.addstr(row + 2, 0, '%4i' % primary_val, PRIMARY_COLOR)
-
- if secondary_min_bound != secondary_max_bound:
- secondary_val = (secondary_max_bound - secondary_min_bound) * (self.graph_height - row - 1) / (self.graph_height - 1)
-
- if secondary_val not in (secondary_min_bound, secondary_max_bound):
- self.addstr(row + 2, graph_column + 5, '%4i' % secondary_val, SECONDARY_COLOR)
-
- # 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))
-
- 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))
-
- for row in range(column_height):
- self.addstr(self.graph_height + 1 - row, col + graph_column + 10, ' ', curses.A_STANDOUT, SECONDARY_COLOR)
-
- # bottom labeling of x-axis
-
- interval_sec = int(CONFIG['attr.graph.intervals'].values()[self.update_interval]) # seconds per labeling
-
- interval_spacing = 10 if graph_column >= WIDE_LABELING_GRAPH_COL else 5
- units_label, decimal_precision = None, 0
-
- for i in range((graph_column - 4) / interval_spacing):
- loc = (i + 1) * interval_spacing
- time_label = str_tools.time_label(loc * interval_sec, decimal_precision)
-
- if not units_label:
- units_label = time_label[-1]
- elif units_label != time_label[-1]:
- # upped scale so also up precision of future measurements
- units_label = time_label[-1]
- decimal_precision += 1
- else:
- # if constrained on space then strips labeling since already provided
- time_label = time_label[:-1]
-
- self.addstr(self.graph_height + 2, 4 + loc, time_label, PRIMARY_COLOR)
- self.addstr(self.graph_height + 2, graph_column + 10 + loc, time_label, SECONDARY_COLOR)
-
- param.draw(self, width, height) # allows current stats to modify the display
-
- def get_stats(self):
- """
- Provides the currently selected stats label.
- """
-
- return self.current_display
-
- def set_stats(self, label):
- """
- Sets the currently displayed stats instance, hiding panel if None.
- """
-
- if label != self.current_display:
- if self.current_display:
- self.stats[self.current_display].is_selected = False
-
- if not label:
- self.current_display = None
- elif label in self.stats.keys():
- self.current_display = label
- self.stats[self.current_display].is_selected = True
- else:
- raise ValueError('Unrecognized stats label: %s' % label)
-
- def copy_attr(self, attr):
- if attr == 'stats':
- # uses custom clone method to copy GraphStats instances
- return dict([(key, self.stats[key].clone()) for key in self.stats])
- else:
- return panel.Panel.copy_attr(self, attr)
diff --git a/arm/graphing/resource_stats.py b/arm/graphing/resource_stats.py
deleted file mode 100644
index d38803a..0000000
--- a/arm/graphing/resource_stats.py
+++ /dev/null
@@ -1,57 +0,0 @@
-"""
-Tracks the system resource usage (cpu and memory) of the tor process.
-"""
-
-import arm.util.tracker
-
-from arm.graphing import graph_panel
-
-from stem.util import str_tools
-
-
-class ResourceStats(graph_panel.GraphStats):
- """
- System resource usage tracker.
- """
-
- def __init__(self):
- graph_panel.GraphStats.__init__(self)
- self._last_counter = None
-
- def clone(self, new_copy=None):
- if not new_copy:
- new_copy = ResourceStats()
-
- return graph_panel.GraphStats.clone(self, new_copy)
-
- def get_title(self, width):
- 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)
-
- 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)
-
- avg = self.secondary_total / max(1, self.tick)
- avg_label = str_tools.size_label(avg * 1048576, 1)
-
- return 'Memory (%s, avg: %s):' % (usage_label, avg_label)
-
- def event_tick(self):
- """
- Fetch the cached measurement of resource usage from the ResourceTracker.
- """
-
- resource_tracker = arm.util.tracker.get_resource_tracker()
-
- if resource_tracker and resource_tracker.run_counter() != self._last_counter:
- resources = resource_tracker.get_value()
- primary = resources.cpu_sample * 100 # decimal percentage to whole numbers
- secondary = resources.memory_bytes / 1048576 # translate size to MB so axis labels are short
-
- self._last_counter = resource_tracker.run_counter()
- self._process_event(primary, secondary)
diff --git a/arm/menu/actions.py b/arm/menu/actions.py
index b95f1fc..d4323b3 100644
--- a/arm/menu/actions.py
+++ b/arm/menu/actions.py
@@ -7,7 +7,7 @@ import functools
import arm.popups
import arm.controller
import arm.menu.item
-import arm.graphing.graph_panel
+import arm.graph_panel
import arm.util.tracker
from arm.util import tor_controller, ui_tools
@@ -182,7 +182,7 @@ def make_graph_menu(graph_panel):
bounds_menu = arm.menu.item.Submenu("Bounds")
bounds_group = arm.menu.item.SelectionGroup(graph_panel.set_bounds_type, graph_panel.get_bounds_type())
- for bounds_type in arm.graphing.graph_panel.Bounds:
+ for bounds_type in arm.graph_panel.Bounds:
bounds_menu.add(arm.menu.item.SelectionMenuItem(bounds_type, bounds_group, bounds_type))
graph_menu.add(bounds_menu)
1
0
19 Oct '14
commit 197640fe1e939570c60f34ddd054a0df0b1ebaf7
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Oct 18 14:14:33 2014 -0700
Moving accounting info into the graph panel
Shifting handling for accounting info from the bandwidth stats to the graph
panel. This doesn't make things much better or worse at present, but it'll
allow us to greatly simplify the stats.
---
arm/graph_panel.py | 111 +++++++++++++++++++++++++---------------------------
1 file changed, 53 insertions(+), 58 deletions(-)
diff --git a/arm/graph_panel.py b/arm/graph_panel.py
index 8f08fb3..7072f38 100644
--- a/arm/graph_panel.py
+++ b/arm/graph_panel.py
@@ -189,13 +189,6 @@ class GraphStats:
def secondary_header(self, width):
return ''
- def get_content_height(self):
- """
- Provides the height content should take up (not including the graph).
- """
-
- return DEFAULT_CONTENT_HEIGHT
-
def draw(self, panel, width, height):
"""
Allows for any custom drawing monitor wishes to append.
@@ -253,11 +246,10 @@ class BandwidthStats(GraphStats):
GraphStats.__init__(self)
# listens for tor reload (sighup) events which can reset the bandwidth
- # rate/burst and if tor's using accounting
+ # rate/burst
controller = tor_controller()
self._title_stats = []
- self._accounting_stats = None
if not is_pause_buffer:
self.reset_listener(controller, State.INIT, None) # initializes values
@@ -286,32 +278,13 @@ class BandwidthStats(GraphStats):
if not new_copy:
new_copy = BandwidthStats(True)
- new_copy._accounting_stats = self._accounting_stats
new_copy._title_stats = self._title_stats
return GraphStats.clone(self, new_copy)
def reset_listener(self, controller, event_type, _):
- # updates title parameters and accounting status if they changed
-
self.new_desc_event(None) # updates title params
- if event_type in (State.INIT, State.RESET) and CONFIG['features.graph.bw.accounting.show']:
- is_accounting_enabled = controller.get_info('accounting/enabled', None) == '1'
-
- if is_accounting_enabled != bool(self._accounting_stats):
- self._accounting_stats = tor_controller().get_accounting_stats(None)
-
- # redraws the whole screen since our height changed
-
- arm.controller.get_controller().redraw()
-
- # redraws to reflect changes (this especially noticeable when we have
- # accounting and shut down since it then gives notice of the shutdown)
-
- if self._graph_panel and self.is_selected:
- self._graph_panel.redraw(True)
-
def prepopulate_from_state(self):
"""
Attempts to use tor's state file to prepopulate values for the 15 minute
@@ -364,10 +337,6 @@ class BandwidthStats(GraphStats):
return time.time() - min(stats.last_read_time, stats.last_write_time)
def bandwidth_event(self, event):
- if self._accounting_stats and self.is_next_tick_redraw():
- if time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE:
- self._accounting_stats = tor_controller().get_accounting_stats(None)
-
# scales units from B to KB for graphing
self._process_event(event.read / 1024.0, event.written / 1024.0)
@@ -375,7 +344,7 @@ class BandwidthStats(GraphStats):
def draw(self, panel, width, height):
# line of the graph's x-axis labeling
- labeling_line = GraphStats.get_content_height(self) + panel.graph_height - 2
+ labeling_line = DEFAULT_CONTENT_HEIGHT + panel.graph_height - 2
# if display is narrow, overwrites x-axis labels with avg / total stats
@@ -392,25 +361,6 @@ class BandwidthStats(GraphStats):
panel.addstr(labeling_line, 1, primary_footer, PRIMARY_COLOR)
panel.addstr(labeling_line, graph_column + 6, secondary_footer, SECONDARY_COLOR)
- # provides accounting stats if enabled
-
- if self._accounting_stats:
- if tor_controller().is_alive():
- hibernate_color = CONFIG['attr.hibernate_color'].get(self._accounting_stats.status, 'red')
-
- x, y = 0, labeling_line + 2
- x = panel.addstr(y, x, 'Accounting (', curses.A_BOLD)
- x = panel.addstr(y, x, self._accounting_stats.status, curses.A_BOLD, hibernate_color)
- x = panel.addstr(y, x, ')', curses.A_BOLD)
-
- panel.addstr(y, 35, 'Time to reset: %s' % str_tools.short_time_label(self._accounting_stats.time_until_reset))
-
- panel.addstr(y + 1, 2, '%s / %s' % (self._accounting_stats.read_bytes, self._accounting_stats.read_limit), PRIMARY_COLOR)
- panel.addstr(y + 1, 37, '%s / %s' % (self._accounting_stats.written_bytes, self._accounting_stats.write_limit), SECONDARY_COLOR)
- else:
- panel.addstr(labeling_line + 2, 0, 'Accounting:', curses.A_BOLD)
- panel.addstr(labeling_line + 2, 12, 'Connection Closed...')
-
def get_title(self, width):
stats_label = str_tools.join(self._title_stats, ', ', width - 13)
return 'Bandwidth (%s):' % stats_label if stats_label else 'Bandwidth:'
@@ -449,10 +399,6 @@ class BandwidthStats(GraphStats):
else:
return 'Upload:'
- def get_content_height(self):
- base_height = GraphStats.get_content_height(self)
- return base_height + 3 if self._accounting_stats else base_height
-
def new_desc_event(self, event):
controller = tor_controller()
@@ -608,6 +554,7 @@ class GraphPanel(panel.Panel):
self.bounds = list(Bounds)[CONFIG['features.graph.bound']]
self.graph_height = CONFIG['features.graph.height']
self.current_display = None # label of the stats currently being displayed
+ self._accounting_stats = None
self.stats = {
GraphStat.BANDWIDTH: BandwidthStats(),
@@ -621,6 +568,7 @@ class GraphPanel(panel.Panel):
stat._graph_panel = self
self.set_pause_attr('stats')
+ self.set_pause_attr('_accounting_stats')
try:
initial_stats = GRAPH_INIT_STATS.get(CONFIG['features.graph.type'])
@@ -643,6 +591,26 @@ class GraphPanel(panel.Panel):
except ValueError as exc:
log.info(msg('panel.graphing.prepopulation_failure', error = str(exc)))
+ tor_controller().add_event_listener(self.bandwidth_event, stem.control.EventType.BW)
+
+ def bandwidth_event(self, event):
+ if not CONFIG['features.graph.bw.accounting.show']:
+ self._accounting_stats = None
+ elif not self._accounting_stats or time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE:
+ old_accounting_stats = self._accounting_stats
+ self._accounting_stats = tor_controller().get_accounting_stats(None)
+
+ if bool(old_accounting_stats) != bool(self._accounting_stats):
+ # we either added or removed accounting info, redraw the whole screen since this changes our height
+
+ arm.controller.get_controller().redraw()
+
+ # redraws to reflect changes (this especially noticeable when we have
+ # accounting and shut down since it then gives notice of the shutdown)
+
+ if self.current_display == GraphStat.BANDWIDTH:
+ self.redraw(True)
+
def get_update_interval(self):
"""
Provides the rate that we update the graph at.
@@ -684,9 +652,14 @@ class GraphPanel(panel.Panel):
"""
if self.current_display:
- return self.stats[self.current_display].get_content_height() + self.graph_height
+ height = DEFAULT_CONTENT_HEIGHT + self.graph_height
else:
- return 0
+ height = 0
+
+ if self.current_display == GraphStat.BANDWIDTH and self._accounting_stats:
+ height += 3
+
+ return height
def set_graph_height(self, new_graph_height):
"""
@@ -911,6 +884,28 @@ class GraphPanel(panel.Panel):
param.draw(self, width, height) # allows current stats to modify the display
+ # provides accounting stats if enabled
+
+ accounting_stats = self.get_attr('_accounting_stats')
+
+ if self.current_display == GraphStat.BANDWIDTH and accounting_stats:
+ if tor_controller().is_alive():
+ hibernate_color = CONFIG['attr.hibernate_color'].get(accounting_stats.status, 'red')
+
+ labeling_line = DEFAULT_CONTENT_HEIGHT + self.graph_height - 2
+ x, y = 0, labeling_line + 2
+ x = self.addstr(y, x, 'Accounting (', curses.A_BOLD)
+ x = self.addstr(y, x, accounting_stats.status, curses.A_BOLD, hibernate_color)
+ x = self.addstr(y, x, ')', curses.A_BOLD)
+
+ self.addstr(y, 35, 'Time to reset: %s' % str_tools.short_time_label(accounting_stats.time_until_reset))
+
+ self.addstr(y + 1, 2, '%s / %s' % (accounting_stats.read_bytes, accounting_stats.read_limit), PRIMARY_COLOR)
+ self.addstr(y + 1, 37, '%s / %s' % (accounting_stats.written_bytes, accounting_stats.write_limit), SECONDARY_COLOR)
+ else:
+ self.addstr(labeling_line + 2, 0, 'Accounting:', curses.A_BOLD)
+ self.addstr(labeling_line + 2, 12, 'Connection Closed...')
+
def get_stats(self):
"""
Provides the currently selected stats label.
1
0
commit cb5c895333092f051d15d363a74219e08e65b502
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Oct 12 15:37:17 2014 -0700
Draw block is a no-op without any stats
Our graph panel's whole draw method was wrapped in a conditional. Minor tweak
so we can unindent the whole method.
---
arm/graphing/graph_panel.py | 176 +++++++++++++++++++++----------------------
1 file changed, 88 insertions(+), 88 deletions(-)
diff --git a/arm/graphing/graph_panel.py b/arm/graphing/graph_panel.py
index 4450c6c..99c1d3b 100644
--- a/arm/graphing/graph_panel.py
+++ b/arm/graphing/graph_panel.py
@@ -438,129 +438,129 @@ class GraphPanel(panel.Panel):
]
def draw(self, width, height):
- """ Redraws graph panel """
+ if not self.current_display:
+ return
- if self.current_display:
- param = self.get_attr('stats')[self.current_display]
- graph_column = min((width - 10) / 2, param.max_column)
+ param = self.get_attr('stats')[self.current_display]
+ graph_column = min((width - 10) / 2, param.max_column)
- if self.is_title_visible():
- self.addstr(0, 0, param.get_title(width), curses.A_STANDOUT)
+ if self.is_title_visible():
+ self.addstr(0, 0, param.get_title(width), curses.A_STANDOUT)
- # top labels
+ # top labels
- left, right = param.primary_header(width / 2), param.secondary_header(width / 2)
+ left, right = param.primary_header(width / 2), param.secondary_header(width / 2)
- if left:
- self.addstr(1, 0, left, curses.A_BOLD, PRIMARY_COLOR)
+ if left:
+ self.addstr(1, 0, left, curses.A_BOLD, PRIMARY_COLOR)
- if right:
- self.addstr(1, graph_column + 5, right, curses.A_BOLD, SECONDARY_COLOR)
+ if right:
+ self.addstr(1, graph_column + 5, right, curses.A_BOLD, SECONDARY_COLOR)
- # determines max/min value on the graph
+ # determines max/min value on the graph
- 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])
+ 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])
+ 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:
- # 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_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_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]))
- # if the max = min (ie, all values are the same) then use zero lower
- # bound so a graph is still displayed
+ primary_min_bound = secondary_min_bound = 0
- if primary_min_bound == primary_max_bound:
- primary_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]))
- if secondary_min_bound == secondary_max_bound:
- secondary_min_bound = 0
+ # if the max = min (ie, all values are the same) then use zero lower
+ # bound so a graph is still displayed
- # displays upper and lower bounds
+ if primary_min_bound == primary_max_bound:
+ primary_min_bound = 0
- self.addstr(2, 0, '%4i' % primary_max_bound, PRIMARY_COLOR)
- self.addstr(self.graph_height + 1, 0, '%4i' % primary_min_bound, PRIMARY_COLOR)
+ if secondary_min_bound == secondary_max_bound:
+ secondary_min_bound = 0
- self.addstr(2, graph_column + 5, '%4i' % secondary_max_bound, SECONDARY_COLOR)
- self.addstr(self.graph_height + 1, graph_column + 5, '%4i' % secondary_min_bound, SECONDARY_COLOR)
+ # displays upper and lower bounds
- # displays intermediate bounds on every other row
+ self.addstr(2, 0, '%4i' % primary_max_bound, PRIMARY_COLOR)
+ self.addstr(self.graph_height + 1, 0, '%4i' % primary_min_bound, PRIMARY_COLOR)
- if CONFIG['features.graph.showIntermediateBounds']:
- ticks = (self.graph_height - 3) / 2
+ self.addstr(2, graph_column + 5, '%4i' % secondary_max_bound, SECONDARY_COLOR)
+ self.addstr(self.graph_height + 1, graph_column + 5, '%4i' % secondary_min_bound, SECONDARY_COLOR)
- for i in range(ticks):
- row = self.graph_height - (2 * i) - 3
+ # displays intermediate bounds on every other row
- if self.graph_height % 2 == 0 and i >= (ticks / 2):
- row -= 1
+ if CONFIG['features.graph.showIntermediateBounds']:
+ ticks = (self.graph_height - 3) / 2
- if primary_min_bound != primary_max_bound:
- primary_val = (primary_max_bound - primary_min_bound) * (self.graph_height - row - 1) / (self.graph_height - 1)
+ for i in range(ticks):
+ row = self.graph_height - (2 * i) - 3
- if primary_val not in (primary_min_bound, primary_max_bound):
- self.addstr(row + 2, 0, '%4i' % primary_val, PRIMARY_COLOR)
+ if self.graph_height % 2 == 0 and i >= (ticks / 2):
+ row -= 1
- if secondary_min_bound != secondary_max_bound:
- secondary_val = (secondary_max_bound - secondary_min_bound) * (self.graph_height - row - 1) / (self.graph_height - 1)
+ if primary_min_bound != primary_max_bound:
+ primary_val = (primary_max_bound - primary_min_bound) * (self.graph_height - row - 1) / (self.graph_height - 1)
- if secondary_val not in (secondary_min_bound, secondary_max_bound):
- self.addstr(row + 2, graph_column + 5, '%4i' % secondary_val, SECONDARY_COLOR)
+ if primary_val not in (primary_min_bound, primary_max_bound):
+ self.addstr(row + 2, 0, '%4i' % primary_val, PRIMARY_COLOR)
- # creates bar graph (both primary and secondary)
+ if secondary_min_bound != secondary_max_bound:
+ secondary_val = (secondary_max_bound - secondary_min_bound) * (self.graph_height - row - 1) / (self.graph_height - 1)
- 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))
+ if secondary_val not in (secondary_min_bound, secondary_max_bound):
+ self.addstr(row + 2, graph_column + 5, '%4i' % secondary_val, SECONDARY_COLOR)
- for row in range(column_height):
- self.addstr(self.graph_height + 1 - row, col + 5, ' ', curses.A_STANDOUT, PRIMARY_COLOR)
+ # creates bar graph (both primary and secondary)
- 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))
+ 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))
- for row in range(column_height):
- self.addstr(self.graph_height + 1 - row, col + graph_column + 10, ' ', curses.A_STANDOUT, SECONDARY_COLOR)
+ for row in range(column_height):
+ self.addstr(self.graph_height + 1 - row, col + 5, ' ', curses.A_STANDOUT, PRIMARY_COLOR)
- # bottom labeling of x-axis
+ 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))
- interval_sec = 1 # seconds per labeling
+ for row in range(column_height):
+ self.addstr(self.graph_height + 1 - row, col + graph_column + 10, ' ', curses.A_STANDOUT, SECONDARY_COLOR)
- for i in range(len(UPDATE_INTERVALS)):
- if i == self.update_interval:
- interval_sec = UPDATE_INTERVALS[i][1]
+ # bottom labeling of x-axis
- interval_spacing = 10 if graph_column >= WIDE_LABELING_GRAPH_COL else 5
- units_label, decimal_precision = None, 0
+ interval_sec = 1 # seconds per labeling
- for i in range((graph_column - 4) / interval_spacing):
- loc = (i + 1) * interval_spacing
- time_label = str_tools.time_label(loc * interval_sec, decimal_precision)
-
- if not units_label:
- units_label = time_label[-1]
- elif units_label != time_label[-1]:
- # upped scale so also up precision of future measurements
- units_label = time_label[-1]
- decimal_precision += 1
- else:
- # if constrained on space then strips labeling since already provided
- time_label = time_label[:-1]
+ for i in range(len(UPDATE_INTERVALS)):
+ if i == self.update_interval:
+ interval_sec = UPDATE_INTERVALS[i][1]
+
+ interval_spacing = 10 if graph_column >= WIDE_LABELING_GRAPH_COL else 5
+ units_label, decimal_precision = None, 0
+
+ for i in range((graph_column - 4) / interval_spacing):
+ loc = (i + 1) * interval_spacing
+ time_label = str_tools.time_label(loc * interval_sec, decimal_precision)
+
+ if not units_label:
+ units_label = time_label[-1]
+ elif units_label != time_label[-1]:
+ # upped scale so also up precision of future measurements
+ units_label = time_label[-1]
+ decimal_precision += 1
+ else:
+ # if constrained on space then strips labeling since already provided
+ time_label = time_label[:-1]
- self.addstr(self.graph_height + 2, 4 + loc, time_label, PRIMARY_COLOR)
- self.addstr(self.graph_height + 2, graph_column + 10 + loc, time_label, SECONDARY_COLOR)
+ self.addstr(self.graph_height + 2, 4 + loc, time_label, PRIMARY_COLOR)
+ self.addstr(self.graph_height + 2, graph_column + 10 + loc, time_label, SECONDARY_COLOR)
- param.draw(self, width, height) # allows current stats to modify the display
+ param.draw(self, width, height) # allows current stats to modify the display
def get_stats(self):
"""
1
0
[arm/master] Moving graph stat inititalization into the graph panel
by atagar@torproject.org 19 Oct '14
by atagar@torproject.org 19 Oct '14
19 Oct '14
commit 6ea5b4a6acd94bb8151ad67d92820d69c0341fe2
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Oct 12 15:20:41 2014 -0700
Moving graph stat inititalization into the graph panel
Hmmmm, not sure why we did this. Our controller initialized graph stats then
added them to the graph panel. Maybe this made sense with a past feature or
pausing implementation.
... or maybe it still makes sense and I'm not spotting why. It's a lot simpler
for the panel to be responsible for its own stats so going with this for now.
Guess we'll see if this bites me down the line.
---
arm/controller.py | 45 -----------------------------------
arm/graphing/graph_panel.py | 55 ++++++++++++++++++++++++++++++++++---------
2 files changed, 44 insertions(+), 56 deletions(-)
diff --git a/arm/controller.py b/arm/controller.py
index 48034e9..42d0531 100644
--- a/arm/controller.py
+++ b/arm/controller.py
@@ -52,17 +52,9 @@ CONFIG = conf.config_dict("arm", {
"features.redrawRate": 5,
"features.refreshRate": 5,
"features.confirmQuit": True,
- "features.graph.type": 1,
- "features.graph.bw.prepopulate": True,
"start_time": 0,
}, conf_handler)
-GraphStat = enum.Enum("BANDWIDTH", "CONNECTIONS", "SYSTEM_RESOURCES")
-
-# maps 'features.graph.type' config values to the initial types
-
-GRAPH_INIT_STATS = {1: GraphStat.BANDWIDTH, 2: GraphStat.CONNECTIONS, 3: GraphStat.SYSTEM_RESOURCES}
-
def get_controller():
"""
@@ -173,43 +165,6 @@ def init_controller(stdscr, start_time):
ARM_CONTROLLER = Controller(stdscr, sticky_panels, page_panels)
- # additional configuration for the graph panel
-
- graph_panel = ARM_CONTROLLER.get_panel("graph")
-
- if graph_panel:
- # statistical monitors for graph
-
- bw_stats = arm.graphing.bandwidth_stats.BandwidthStats()
- graph_panel.add_stats(GraphStat.BANDWIDTH, bw_stats)
- graph_panel.add_stats(GraphStat.SYSTEM_RESOURCES, arm.graphing.resource_stats.ResourceStats())
-
- if CONFIG["features.panels.show.connection"]:
- graph_panel.add_stats(GraphStat.CONNECTIONS, arm.graphing.conn_stats.ConnStats())
-
- # sets graph based on config parameter
-
- try:
- initial_stats = GRAPH_INIT_STATS.get(CONFIG["features.graph.type"])
- graph_panel.set_stats(initial_stats)
- except ValueError:
- pass # invalid stats, maybe connections when lookups are disabled
-
- # prepopulates bandwidth values from state file
-
- if CONFIG["features.graph.bw.prepopulate"] and tor_controller().is_alive():
- try:
- missing_seconds = bw_stats.prepopulate_from_state()
-
- if missing_seconds:
- log.notice(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(missing_seconds, 0, True)))
- else:
- log.notice(msg('panel.graphing.prepopulation_all_successful'))
-
- graph_panel.update_interval = 4
- except ValueError as exc:
- log.info(msg('panel.graphing.prepopulation_failure', error = str(exc)))
-
class LabelPanel(panel.Panel):
"""
diff --git a/arm/graphing/graph_panel.py b/arm/graphing/graph_panel.py
index a034e56..4450c6c 100644
--- a/arm/graphing/graph_panel.py
+++ b/arm/graphing/graph_panel.py
@@ -24,9 +24,9 @@ import arm.controller
import stem.control
-from arm.util import panel, tor_controller
+from arm.util import msg, panel, tor_controller
-from stem.util import conf, enum, str_tools
+from stem.util import conf, enum, log, str_tools
# time intervals at which graphs can be updated
@@ -41,6 +41,12 @@ UPDATE_INTERVALS = [
('daily', 86400),
]
+GraphStat = enum.Enum("BANDWIDTH", "CONNECTIONS", "SYSTEM_RESOURCES")
+
+# maps 'features.graph.type' config values to the initial types
+
+GRAPH_INIT_STATS = {1: GraphStat.BANDWIDTH, 2: GraphStat.CONNECTIONS, 3: GraphStat.SYSTEM_RESOURCES}
+
DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
PRIMARY_COLOR, SECONDARY_COLOR = 'green', 'cyan'
MIN_GRAPH_HEIGHT = 1
@@ -74,6 +80,9 @@ CONFIG = conf.config_dict('arm', {
'features.graph.bound': 1,
'features.graph.max_width': 150,
'features.graph.showIntermediateBounds': True,
+ 'features.graph.type': 1,
+ 'features.panels.show.connection': True,
+ 'features.graph.bw.prepopulate': True,
}, conf_handler)
@@ -246,9 +255,41 @@ class GraphPanel(panel.Panel):
self.bounds = list(Bounds)[CONFIG['features.graph.bound']]
self.graph_height = CONFIG['features.graph.height']
self.current_display = None # label of the stats currently being displayed
- self.stats = {} # available stats (mappings of label -> instance)
+
+ self.stats = {
+ GraphStat.BANDWIDTH: arm.graphing.bandwidth_stats.BandwidthStats(),
+ GraphStat.SYSTEM_RESOURCES: arm.graphing.resource_stats.ResourceStats(),
+ }
+
+ if CONFIG['features.panels.show.connection']:
+ self.stats[GraphStat.CONNECTIONS] = arm.graphing.conn_stats.ConnStats()
+
+ for stat in self.stats.values():
+ stat._graph_panel = self
+
self.set_pause_attr('stats')
+ try:
+ initial_stats = GRAPH_INIT_STATS.get(CONFIG['features.graph.type'])
+ self.set_stats(initial_stats)
+ except ValueError:
+ pass # invalid stats, maybe connections when lookups are disabled
+
+ # prepopulates bandwidth values from state file
+
+ if CONFIG["features.graph.bw.prepopulate"] and tor_controller().is_alive():
+ try:
+ missing_seconds = self.stats[GraphStat.BANDWIDTH].prepopulate_from_state()
+
+ if missing_seconds:
+ log.notice(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(missing_seconds, 0, True)))
+ else:
+ log.notice(msg('panel.graphing.prepopulation_all_successful'))
+
+ self.update_interval = 4
+ except ValueError as exc:
+ log.info(msg('panel.graphing.prepopulation_failure', error = str(exc)))
+
def get_update_interval(self):
"""
Provides the rate that we update the graph at.
@@ -521,14 +562,6 @@ class GraphPanel(panel.Panel):
param.draw(self, width, height) # allows current stats to modify the display
- def add_stats(self, label, stats):
- """
- Makes GraphStats instance available in the panel.
- """
-
- stats._graph_panel = self
- self.stats[label] = stats
-
def get_stats(self):
"""
Provides the currently selected stats label.
1
0
18 Oct '14
commit 11469cf3b5f012b3f5e152bd40e6dcce485787c2
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Oct 18 14:10:25 2014 -0700
Accounting info's time_until_reset incorrect
Oops. We inverted attributes always accounting a negative time_until_reset.
---
stem/control.py | 2 +-
test/unit/control/controller.py | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/stem/control.py b/stem/control.py
index 4688bd7..ed83a00 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -1246,7 +1246,7 @@ class Controller(BaseController):
retrieved = retrieved,
status = status,
interval_end = interval_end,
- time_until_reset = int(retrieved) - calendar.timegm(interval_end.timetuple()),
+ time_until_reset = calendar.timegm(interval_end.timetuple()) - int(retrieved),
read_bytes = used_read,
read_bytes_left = left_read,
read_limit = used_read + left_read,
diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py
index 948068f..5e0058f 100644
--- a/test/unit/control/controller.py
+++ b/test/unit/control/controller.py
@@ -281,7 +281,7 @@ class TestControl(unittest.TestCase):
self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners)
@patch('stem.control.Controller.get_info')
- @patch('time.time', Mock(return_value = 1410723698.276578))
+ @patch('time.time', Mock(return_value = 1410723598.276578))
def test_get_accounting_stats(self, get_info_mock):
"""
Exercises the get_accounting_stats() method.
@@ -296,10 +296,10 @@ class TestControl(unittest.TestCase):
}[param]
expected = stem.control.AccountingStats(
- 1410723698.276578,
+ 1410723598.276578,
'awake',
datetime.datetime(2014, 9, 14, 19, 41),
- 38,
+ 62,
4837, 102944, 107781,
2050, 7440, 9490,
)
1
0
[translation/torbutton-torbuttonproperties] Update translations for torbutton-torbuttonproperties
by translation@torproject.org 18 Oct '14
by translation@torproject.org 18 Oct '14
18 Oct '14
commit e103d413dc0b4e9011bb4ba03a1efdf4f509f781
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 18 20:45:54 2014 +0000
Update translations for torbutton-torbuttonproperties
---
af/torbutton.properties | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/af/torbutton.properties b/af/torbutton.properties
index 1548b52..1913165 100644
--- a/af/torbutton.properties
+++ b/af/torbutton.properties
@@ -1,7 +1,7 @@
-torbutton.button.tooltip.disabled = Enable Tor
-torbutton.button.tooltip.enabled = Disable Tor
-torbutton.panel.tooltip.disabled = Click to enable Tor
-torbutton.panel.tooltip.enabled = Click to disable Tor
+torbutton.button.tooltip.disabled = Sit Tor aan
+torbutton.button.tooltip.enabled = Sit Tor Af
+torbutton.panel.tooltip.disabled = Klil om Tor aan te sit
+torbutton.panel.tooltip.enabled = Klik om Tor af te sit
torbutton.panel.plugins.disabled = Click to enable plugins
torbutton.panel.plugins.enabled = Click to disable plugins
torbutton.panel.label.disabled = Tor Disabled
@@ -16,9 +16,9 @@ torbutton.popup.test.success = Tor proxy test successful!
torbutton.popup.test.failure = Tor proxy test FAILED! Check your proxy and Polipo settings.
torbutton.popup.test.confirm_toggle = The most recent Tor proxy test failed to use Tor.\n\nAre you sure you want to enable anyway?\n\nNote: If you have fixed the problem, you can rerun the test in the Torbutton Proxy Preferences window to eliminate this warning.
torbutton.popup.test.ff3_notice = Click OK to test Tor proxy settings. This test will happen in the background. Please be patient.
-torbutton.panel.label.verified = Tor Verified
+torbutton.panel.label.verified = Tor Geverifieer
torbutton.popup.test.auto_failed = The automatic Tor proxy test failed to use Tor.\n\nAre you sure you want to enable anyway?
-torbutton.prefs.recommended = (recommended)
+torbutton.prefs.recommended = (aanbeveel)
torbutton.prefs.optional = (opsionele)
torbutton.prefs.crucial = (noodsaaklik)
torbutton.popup.external.title = Download an external file type?
1
0
[translation/tor-launcher-properties] Update translations for tor-launcher-properties
by translation@torproject.org 18 Oct '14
by translation@torproject.org 18 Oct '14
18 Oct '14
commit e9f78db03d36d60278351ceec6df750c7bc7e338
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 18 20:45:33 2014 +0000
Update translations for tor-launcher-properties
---
af/torlauncher.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/af/torlauncher.properties b/af/torlauncher.properties
index 02be756..c347389 100644
--- a/af/torlauncher.properties
+++ b/af/torlauncher.properties
@@ -27,7 +27,7 @@ torlauncher.error_bridges_missing=You must specify one or more bridges.
torlauncher.error_default_bridges_type_missing=You must select a transport type for the provided bridges.
torlauncher.error_bridge_bad_default_type=No provided bridges that have the transport type %S are available. Please adjust your settings.
-torlauncher.recommended_bridge=(recommended)
+torlauncher.recommended_bridge=(aanbeveel)
torlauncher.connect=Connect
torlauncher.restart_tor=Restart Tor
1
0
[translation/torbutton-torbuttonproperties] Update translations for torbutton-torbuttonproperties
by translation@torproject.org 18 Oct '14
by translation@torproject.org 18 Oct '14
18 Oct '14
commit 2ebea073ba238d2d05f2edd0ddff775bf9f91812
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 18 20:16:08 2014 +0000
Update translations for torbutton-torbuttonproperties
---
af/torbutton.properties | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/af/torbutton.properties b/af/torbutton.properties
index 0154401..1548b52 100644
--- a/af/torbutton.properties
+++ b/af/torbutton.properties
@@ -19,19 +19,19 @@ torbutton.popup.test.ff3_notice = Click OK to test Tor proxy settings. This test
torbutton.panel.label.verified = Tor Verified
torbutton.popup.test.auto_failed = The automatic Tor proxy test failed to use Tor.\n\nAre you sure you want to enable anyway?
torbutton.prefs.recommended = (recommended)
-torbutton.prefs.optional = (optional)
-torbutton.prefs.crucial = (crucial)
+torbutton.prefs.optional = (opsionele)
+torbutton.prefs.crucial = (noodsaaklik)
torbutton.popup.external.title = Download an external file type?
-torbutton.popup.external.app = Tor Browser cannot display this file. You will need to open it with another application.\n\n
+torbutton.popup.external.app = Tor Browser kan hierdie lêer nie vertoon nie. Jy sal n ander program moet gebruik om dit oop te maak.\n\n
torbutton.popup.external.note = Some types of files can cause applications to connect to the Internet without using Tor.\n\n
torbutton.popup.external.suggest = To be safe, you should only open downloaded files while offline, or use a Tor Live CD such as Tails.\n
torbutton.popup.launch = Download file
-torbutton.popup.cancel = Cancel
+torbutton.popup.cancel = Kanselleer
torbutton.popup.dontask = Automatically download files from now on
torbutton.popup.test.no_http_proxy = Tor proxy test: Local HTTP Proxy is unreachable. Is Polipo running properly?
-torbutton.popup.captcha.title = Avoid Google Captchas?
+torbutton.popup.captcha.title = Vermy Google captcha ?
torbutton.popup.captcha.ask = Torbutton detected a Google Captcha. Would you like to be redirected to another search engine for this query?
-torbutton.popup.captcha.always = Always perform this action from now on
+torbutton.popup.captcha.always = Voer altyd hierdie aksie uit van nou af
torbutton.popup.redirect = Redirect
torbutton.popup.no_redirect = Don't Redirect
torbutton.popup.prompted_language = Om jou meer privaatheid te gee, kan Torbutton die Engelse weergawe van die web bladsye versoek . Dit kan veroorsaak dat web bladsye wat jy verkies om in jou moedertaal te lees, in Engels vertoon sal word.\n\nWil jy die Engelse web bladsye versoek vir beter privaatheid?
@@ -39,19 +39,19 @@ torbutton.popup.no_newnym = Torbutton cannot safely give you a new identity. It
torbutton.popup.use_tbb = Dit lyk of jy Torbutton met Firefox gebruik, wat nie meer 'n veilige opstel is nie.\n\nIn plaas daarvan, beveel ons aan dat jy die nuutste Tor Browser Bundel kry deur n e-pos aan gettor(a)torproject.org te stuur of om dit by die volgende URL af te laai :
torbutton.popup.pref_error = Torbutton cannot update preferences in the Tor Browser profile directory.
torbutton.popup.permission_denied = Please either reset the permissions of the Tor Browser directory or copy it to a new location.
-torbutton.popup.device_full = The disk appears to be full. Please free up space or move Tor Browser to a new device.
-torbutton.title.prompt_torbrowser = Important Torbutton Information
+torbutton.popup.device_full = Die hardeskyf lyk vol . Maak asseblief ruimte of beweeg Tor Browser na 'n nuwe toestel.
+torbutton.title.prompt_torbrowser = Belangrike Torbutton Inligting
torbutton.popup.prompt_torbrowser = Torbutton works differently now: you can't turn it off any more.\n\nWe made this change because it isn't safe to use Torbutton in a browser that's also used for non-Tor browsing. There were too many bugs there that we couldn't fix any other way.\n\nIf you want to keep using Firefox normally, you should uninstall Torbutton and download Tor Browser Bundle. The privacy properties of Tor Browser are also superior to those of normal Firefox, even when Firefox is used with Torbutton.\n\nTo remove Torbutton, go to Tools->Addons->Extensions and then click the Remove button next to Torbutton.
-torbutton.popup.short_torbrowser = Important Torbutton Information!\n\nTorbutton is now always enabled.\n\nClick on the Torbutton for more information.
+torbutton.popup.short_torbrowser = Belangrike Torbutton inligting!\n\nTorbutton is nou altyd aan.\n\nKlik op die Torbutton vir meer inligting.
torbutton.popup.confirm_plugins = Plugins such as Flash can harm your privacy and anonymity.\n\nThey can also bypass Tor to reveal your current location and IP address.\n\nAre you sure you want to enable plugins?\n\n
-torbutton.popup.never_ask_again = Never ask me again
+torbutton.popup.never_ask_again = Moet my nooit weer vra nie.
# Canvas permission prompt. Strings are kept here for ease of translation.
canvas.siteprompt=This website (%S) attempted to extract HTML5 canvas image data, which may be used to uniquely identify your computer.\n\nShould Tor Browser allow this website to extract HTML5 canvas image data?
-canvas.notNow=Not Now
+canvas.notNow=Nie nou nie
canvas.notNowAccessKey=N
-canvas.allow=Allow in the future
+canvas.allow=Laat toe in die toekoms
canvas.allowAccessKey=A
-canvas.never=Never for this site (recommended)
+canvas.never=Nie vir hierdie webwerf nie (aanbeveel)
canvas.neverAccessKey=e
1
0
[translation/tails-misc] Update translations for tails-misc
by translation@torproject.org 18 Oct '14
by translation@torproject.org 18 Oct '14
18 Oct '14
commit 3475d41703427b22ae4b897003fec6d48c70f890
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 18 20:15:53 2014 +0000
Update translations for tails-misc
---
af.po | 34 +++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/af.po b/af.po
index a249139..5772078 100644
--- a/af.po
+++ b/af.po
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-10-10 13:00+0200\n"
-"PO-Revision-Date: 2014-10-11 08:52+0000\n"
+"POT-Creation-Date: 2014-10-15 17:57+0200\n"
+"PO-Revision-Date: 2014-10-18 20:10+0000\n"
"Last-Translator: runasand <runa.sandvik(a)gmail.com>\n"
"Language-Team: Afrikaans (http://www.transifex.com/projects/p/torproject/language/af/)\n"
"MIME-Version: 1.0\n"
@@ -301,12 +301,12 @@ msgid ""
msgstr ""
#: config/chroot_local-includes/usr/local/bin/tails-upgrade-frontend-wrapper:19
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:60
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:62
msgid "error:"
msgstr ""
#: config/chroot_local-includes/usr/local/bin/tails-upgrade-frontend-wrapper:20
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:61
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:63
msgid "Error"
msgstr ""
@@ -356,63 +356,63 @@ msgstr ""
msgid "Cancel"
msgstr "Kanselleer"
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:71
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:73
msgid "Do you really want to launch the Unsafe Browser?"
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:73
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:75
msgid ""
"Network activity within the Unsafe Browser is <b>not anonymous</b>. Only use"
" the Unsafe Browser if necessary, for example if you have to login or "
"register to activate your Internet connection."
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:74
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:76
msgid "_Launch"
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:75
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:77
msgid "_Exit"
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:85
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:87
msgid "Starting the Unsafe Browser..."
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:86
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:88
msgid "This may take a while, so please be patient."
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:104
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:106
msgid "Failed to setup chroot."
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:184
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:180
#: ../config/chroot_local-includes/usr/share/applications/unsafe-browser.desktop.in.h:1
msgid "Unsafe Browser"
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:240
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:236
msgid "Shutting down the Unsafe Browser..."
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:241
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:237
msgid ""
"This may take a while, and you may not restart the Unsafe Browser until it "
"is properly shut down."
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:253
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:249
msgid "Failed to restart Tor."
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:261
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:257
msgid ""
"Another Unsafe Browser is currently running, or being cleaned up. Please "
"retry in a while."
msgstr ""
-#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:274
+#: config/chroot_local-includes/usr/local/sbin/unsafe-browser:270
msgid ""
"No DNS server was obtained through DHCP or manually configured in "
"NetworkManager."
1
0
[translation/torbutton-torbuttonproperties] Update translations for torbutton-torbuttonproperties
by translation@torproject.org 18 Oct '14
by translation@torproject.org 18 Oct '14
18 Oct '14
commit 6b3ae0511fb4e55f5613640b668a720b931b9ac6
Author: Translation commit bot <translation(a)torproject.org>
Date: Sat Oct 18 19:45:52 2014 +0000
Update translations for torbutton-torbuttonproperties
---
af/torbutton.properties | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/af/torbutton.properties b/af/torbutton.properties
index d23c67d..0154401 100644
--- a/af/torbutton.properties
+++ b/af/torbutton.properties
@@ -34,9 +34,9 @@ torbutton.popup.captcha.ask = Torbutton detected a Google Captcha. Would you lik
torbutton.popup.captcha.always = Always perform this action from now on
torbutton.popup.redirect = Redirect
torbutton.popup.no_redirect = Don't Redirect
-torbutton.popup.prompted_language = To give you more privacy, Torbutton can request the English language version of web pages. This may cause web pages that you prefer to read in your native language to display in English instead.\n\nWould you like to request English language web pages for better privacy?
+torbutton.popup.prompted_language = Om jou meer privaatheid te gee, kan Torbutton die Engelse weergawe van die web bladsye versoek . Dit kan veroorsaak dat web bladsye wat jy verkies om in jou moedertaal te lees, in Engels vertoon sal word.\n\nWil jy die Engelse web bladsye versoek vir beter privaatheid?
torbutton.popup.no_newnym = Torbutton cannot safely give you a new identity. It does not have access to the Tor Control Port.\n\nAre you running Tor Browser Bundle?
-torbutton.popup.use_tbb = It appears that you are using Torbutton with Firefox, which is no longer a recommended safe configuration.\n\nInstead, we recommend that you obtain the latest Tor Browser Bundle by sending email to gettor(a)torproject.org or by downloading it at the following URL:
+torbutton.popup.use_tbb = Dit lyk of jy Torbutton met Firefox gebruik, wat nie meer 'n veilige opstel is nie.\n\nIn plaas daarvan, beveel ons aan dat jy die nuutste Tor Browser Bundel kry deur n e-pos aan gettor(a)torproject.org te stuur of om dit by die volgende URL af te laai :
torbutton.popup.pref_error = Torbutton cannot update preferences in the Tor Browser profile directory.
torbutton.popup.permission_denied = Please either reset the permissions of the Tor Browser directory or copy it to a new location.
torbutton.popup.device_full = The disk appears to be full. Please free up space or move Tor Browser to a new device.
1
0