[or-cvs] r20233: {arm} Rewrote graph panel so it can handle any real time statistic (arm/trunk/interface)
atagar at seul.org
atagar at seul.org
Sat Aug 8 07:02:42 UTC 2009
Author: atagar
Date: 2009-08-08 03:02:41 -0400 (Sat, 08 Aug 2009)
New Revision: 20233
Added:
arm/trunk/interface/bandwidthMonitor.py
arm/trunk/interface/connCountMonitor.py
arm/trunk/interface/graphPanel.py
Removed:
arm/trunk/interface/bandwidthPanel.py
Modified:
arm/trunk/interface/connPanel.py
arm/trunk/interface/controller.py
Log:
Rewrote graph panel so it can handle any real time statistics.
added: option to graph connection counts (feature request by phobos)
added: custom graph bounds (global or local maxima)
Added: arm/trunk/interface/bandwidthMonitor.py
===================================================================
--- arm/trunk/interface/bandwidthMonitor.py (rev 0)
+++ arm/trunk/interface/bandwidthMonitor.py 2009-08-08 07:02:41 UTC (rev 20233)
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# bandwidthMonitor.py -- Tracks stats concerning bandwidth usage.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import time
+from TorCtl import TorCtl
+
+import graphPanel
+import util
+
+DL_COLOR = "green" # download section color
+UL_COLOR = "cyan" # upload section color
+
+class BandwidthMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
+ """
+ Tor event listener, taking bandwidth sampling to draw a bar graph. This is
+ updated every second by the BW events.
+ """
+
+ def __init__(self, conn):
+ graphPanel.GraphStats.__init__(self)
+ TorCtl.PostEventListener.__init__(self)
+ self.conn = conn # Tor control port connection
+ self.accountingInfo = None # accounting data (set by _updateAccountingInfo method)
+
+ if conn:
+ self.isAccounting = conn.get_info('accounting/enabled')['accounting/enabled'] == '1'
+
+ # static limit stats for label
+ bwStats = conn.get_option(['BandwidthRate', 'BandwidthBurst'])
+ self.bwRate = util.getSizeLabel(int(bwStats[0][1]))
+ self.bwBurst = util.getSizeLabel(int(bwStats[1][1]))
+ else:
+ self.isAccounting = False
+ self.bwRate, self.bwBurst = -1, -1
+
+ # this doesn't track accounting stats when paused so doesn't need a custom pauseBuffer
+ contentHeight = 13 if self.isAccounting else 10
+ graphPanel.GraphStats.initialize(self, DL_COLOR, UL_COLOR, contentHeight)
+
+ def bandwidth_event(self, event):
+ self._processEvent(event.read / 1024.0, event.written / 1024.0)
+
+ def redraw(self, panel):
+ # provides accounting stats if enabled
+ if self.isAccounting:
+ if not self.isPaused: self._updateAccountingInfo()
+
+ if self.accountingInfo:
+ status = self.accountingInfo["status"]
+ hibernateColor = "green"
+ if status == "soft": hibernateColor = "yellow"
+ elif status == "hard": hibernateColor = "red"
+
+ panel.addfstr(10, 0, "<b>Accounting (<%s>%s</%s>)" % (hibernateColor, status, hibernateColor))
+ panel.addstr(10, 35, "Time to reset: %s" % self.accountingInfo["resetTime"])
+ panel.addstr(11, 2, "%s / %s" % (self.accountingInfo["read"], self.accountingInfo["readLimit"]), util.getColor(self.primaryColor))
+ panel.addstr(11, 37, "%s / %s" % (self.accountingInfo["written"], self.accountingInfo["writtenLimit"]), util.getColor(self.secondaryColor))
+ else:
+ panel.addfstr(10, 0, "<b>Accounting:</b> Shutting Down...")
+
+ def getTitle(self, width):
+ # provides label, dropping stats if there's not enough room
+ labelContents = "Bandwidth (cap: %s, burst: %s):" % (self.bwRate, self.bwBurst)
+ if width < len(labelContents):
+ labelContents = "%s):" % labelContents[:labelContents.find(",")] # removes burst measure
+ if width < len(labelContents): labelContents = "Bandwidth:" # removes both
+
+ return labelContents
+
+ def getHeaderLabel(self, isPrimary):
+ if isPrimary: return "Downloaded (%s/sec):" % util.getSizeLabel(self.lastPrimary * 1024)
+ else: return "Uploaded (%s/sec):" % util.getSizeLabel(self.lastSecondary * 1024)
+
+ def getFooterLabel(self, isPrimary):
+ avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+ return "avg: %s/sec" % util.getSizeLabel(avg * 1024)
+
+ def _updateAccountingInfo(self):
+ """
+ Updates mapping used for accounting info. This includes the following keys:
+ status, resetTime, read, written, readLimit, writtenLimit
+
+ Sets mapping to None if the Tor connection is closed.
+ """
+
+ try:
+ self.accountingInfo = {}
+
+ accountingParams = self.conn.get_info(["accounting/hibernating", "accounting/bytes", "accounting/bytes-left", "accounting/interval-end"])
+ self.accountingInfo["status"] = accountingParams["accounting/hibernating"]
+
+ # altzone subtraction converts from gmt to local with respect to DST
+ sec = time.mktime(time.strptime(accountingParams["accounting/interval-end"], "%Y-%m-%d %H:%M:%S")) - time.time() - time.altzone
+ resetHours = sec / 3600
+ sec %= 3600
+ resetMin = sec / 60
+ sec %= 60
+ self.accountingInfo["resetTime"] = "%i:%02i:%02i" % (resetHours, resetMin, sec)
+
+ read = int(accountingParams["accounting/bytes"].split(" ")[0])
+ written = int(accountingParams["accounting/bytes"].split(" ")[1])
+ readLeft = int(accountingParams["accounting/bytes-left"].split(" ")[0])
+ writtenLeft = int(accountingParams["accounting/bytes-left"].split(" ")[1])
+
+ self.accountingInfo["read"] = util.getSizeLabel(read)
+ self.accountingInfo["written"] = util.getSizeLabel(written)
+ self.accountingInfo["readLimit"] = util.getSizeLabel(read + readLeft)
+ self.accountingInfo["writtenLimit"] = util.getSizeLabel(written + writtenLeft)
+ except TorCtl.TorCtlClosed:
+ self.accountingInfo = None
+
Deleted: arm/trunk/interface/bandwidthPanel.py
===================================================================
--- arm/trunk/interface/bandwidthPanel.py 2009-08-08 04:28:00 UTC (rev 20232)
+++ arm/trunk/interface/bandwidthPanel.py 2009-08-08 07:02:41 UTC (rev 20233)
@@ -1,262 +0,0 @@
-#!/usr/bin/env python
-# bandwidthPanel.py -- Resources related to monitoring Tor bandwidth usage.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import time
-import copy
-import curses
-from TorCtl import TorCtl
-
-import util
-
-BANDWIDTH_GRAPH_COL = 30 # columns of data in graph
-BANDWIDTH_GRAPH_COLOR_DL = "green" # download section color
-BANDWIDTH_GRAPH_COLOR_UL = "cyan" # upload section color
-
-# time intervals at which graphs can be updated
-DEFAULT_INTERVAL_INDEX = 1 # defaults to using five seconds of data per bar in the graph
-UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5), ("30 seconds", 30), ("minutely", 60),
- ("half hour", 1800), ("hourly", 3600), ("daily", 86400)]
-
-class BandwidthMonitor(TorCtl.PostEventListener, util.Panel):
- """
- Tor event listener, taking bandwidth sampling and drawing bar graph. This is
- updated every second by the BW events and graph samples are spaced at
- a timescale determined by the updateIntervalIndex.
- """
-
- def __init__(self, lock, conn):
- TorCtl.PostEventListener.__init__(self)
- if conn: self.isAccounting = conn.get_info('accounting/enabled')['accounting/enabled'] == '1'
- else: self.isAccounting = False
-
- self.contentHeight = 13 if self.isAccounting else 10
- util.Panel.__init__(self, lock, self.contentHeight)
-
- self.conn = conn # Tor control port connection
- self.tick = 0 # number of updates performed
- self.lastDownloadRate = 0 # most recently sampled rates
- self.lastUploadRate = 0
- self.accountingInfo = None # accounting data (set by _updateAccountingInfo method)
- self.isPaused = False
- self.isVisible = True
- self.showLabel = True # shows top label if true, hides otherwise
- self.pauseBuffer = None # mirror instance used to track updates when paused
- self.updateIntervalIndex = DEFAULT_INTERVAL_INDEX
-
- # graphed download (read) and upload (write) rates - first index accumulator
- # iterative insert is to avoid issue with shallow copies (nasty, nasty gotcha)
- self.downloadRates, self.uploadRates = [], []
- for i in range(len(UPDATE_INTERVALS)):
- self.downloadRates.insert(0, (BANDWIDTH_GRAPH_COL + 1) * [0])
- self.uploadRates.insert(0, (BANDWIDTH_GRAPH_COL + 1) * [0])
-
- # max rates seen, used to determine graph bounds
- self.maxDownloadRate = len(UPDATE_INTERVALS) * [1]
- self.maxUploadRate = len(UPDATE_INTERVALS) * [1]
-
- # used to calculate averages, uses tick for time
- self.totalDownload = 0
- self.totalUpload = 0
-
- # retrieves static stats for label
- if conn:
- bwStats = conn.get_option(['BandwidthRate', 'BandwidthBurst'])
- self.bwRate = util.getSizeLabel(int(bwStats[0][1]))
- self.bwBurst = util.getSizeLabel(int(bwStats[1][1]))
- else: self.bwRate, self.bwBurst = -1, -1
-
- def bandwidth_event(self, event):
- if self.isPaused or not self.isVisible: self.pauseBuffer.bandwidth_event(event)
- else:
- self.lastDownloadRate = event.read
- self.lastUploadRate = event.written
-
- self.totalDownload += event.read
- self.totalUpload += event.written
-
- # updates graphs for all time intervals
- self.tick += 1
- for i in range(len(UPDATE_INTERVALS)):
- self.downloadRates[i][0] += event.read
- self.uploadRates[i][0] += event.written
- interval = UPDATE_INTERVALS[i][1]
-
- if self.tick % interval == 0:
- self.maxDownloadRate[i] = max(self.maxDownloadRate[i], self.downloadRates[i][0] / interval)
- self.downloadRates[i].insert(0, 0)
- del self.downloadRates[i][BANDWIDTH_GRAPH_COL + 1:]
-
- self.maxUploadRate[i] = max(self.maxUploadRate[i], self.uploadRates[i][0] / interval)
- self.uploadRates[i].insert(0, 0)
- del self.uploadRates[i][BANDWIDTH_GRAPH_COL + 1:]
-
- self.redraw()
-
- def redraw(self):
- """ Redraws bandwidth panel. """
- # doesn't draw if headless (indicating that the instance is for a pause buffer)
- if self.win:
- if not self.lock.acquire(False): return
- try:
- self.clear()
- dlColor = util.getColor(BANDWIDTH_GRAPH_COLOR_DL)
- ulColor = util.getColor(BANDWIDTH_GRAPH_COLOR_UL)
-
- # draws label, dropping stats if there's not enough room
- labelContents = "Bandwidth (cap: %s, burst: %s):" % (self.bwRate, self.bwBurst)
- if self.maxX < len(labelContents):
- labelContents = "%s):" % labelContents[:labelContents.find(",")] # removes burst measure
- if self.maxX < len(labelContents): labelContents = "Bandwidth:" # removes both
-
- if self.showLabel: self.addstr(0, 0, labelContents, util.LABEL_ATTR)
-
- # current numeric measures
- self.addstr(1, 0, "Downloaded (%s/sec):" % util.getSizeLabel(self.lastDownloadRate), curses.A_BOLD | dlColor)
- self.addstr(1, 35, "Uploaded (%s/sec):" % util.getSizeLabel(self.lastUploadRate), curses.A_BOLD | ulColor)
-
- # graph bounds in KB (uses highest recorded value as max)
- self.addstr(2, 0, "%4s" % str(self.maxDownloadRate[self.updateIntervalIndex] / 1024), dlColor)
- self.addstr(7, 0, " 0", dlColor)
-
- self.addstr(2, 35, "%4s" % str(self.maxUploadRate[self.updateIntervalIndex] / 1024), ulColor)
- self.addstr(7, 35, " 0", ulColor)
-
- # creates bar graph of bandwidth usage over time
- for col in range(BANDWIDTH_GRAPH_COL):
- bytesDownloaded = self.downloadRates[self.updateIntervalIndex][col + 1] / UPDATE_INTERVALS[self.updateIntervalIndex][1]
- colHeight = min(5, 5 * bytesDownloaded / self.maxDownloadRate[self.updateIntervalIndex])
- for row in range(colHeight):
- self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | dlColor)
-
- for col in range(BANDWIDTH_GRAPH_COL):
- bytesUploaded = self.uploadRates[self.updateIntervalIndex][col + 1] / UPDATE_INTERVALS[self.updateIntervalIndex][1]
- colHeight = min(5, 5 * bytesUploaded / self.maxUploadRate[self.updateIntervalIndex])
- for row in range(colHeight):
- self.addstr(7 - row, col + 40, " ", curses.A_STANDOUT | ulColor)
-
- # provides average dl/ul rates
- if self.tick > 0:
- avgDownload = self.totalDownload / self.tick
- avgUpload = self.totalUpload / self.tick
- else: avgDownload, avgUpload = 0, 0
- self.addstr(8, 1, "avg: %s/sec" % util.getSizeLabel(avgDownload), dlColor)
- self.addstr(8, 36, "avg: %s/sec" % util.getSizeLabel(avgUpload), ulColor)
-
- # accounting stats if enabled
- if self.isAccounting:
- if not self.isPaused and self.isVisible: self._updateAccountingInfo()
-
- if self.accountingInfo:
- status = self.accountingInfo["status"]
- hibernateColor = "green"
- if status == "soft": hibernateColor = "yellow"
- elif status == "hard": hibernateColor = "red"
-
- self.addfstr(10, 0, "<b>Accounting (<%s>%s</%s>)" % (hibernateColor, status, hibernateColor))
- self.addstr(10, 35, "Time to reset: %s" % self.accountingInfo["resetTime"])
- self.addstr(11, 2, "%s / %s" % (self.accountingInfo["read"], self.accountingInfo["readLimit"]), dlColor)
- self.addstr(11, 37, "%s / %s" % (self.accountingInfo["written"], self.accountingInfo["writtenLimit"]), ulColor)
- else:
- self.addfstr(10, 0, "<b>Accounting:</b> Shutting Down...")
-
- self.refresh()
- finally:
- self.lock.release()
-
- def setUpdateInterval(self, intervalIndex):
- """
- Sets the timeframe at which the graph is updated. This throws a ValueError
- if the index isn't within UPDATE_INTERVALS.
- """
-
- if intervalIndex >= 0 and intervalIndex < len(UPDATE_INTERVALS):
- self.updateIntervalIndex = intervalIndex
- else: raise ValueError("%i out of bounds of UPDATE_INTERVALS" % intervalIndex)
-
- def setPaused(self, isPause):
- """
- If true, prevents bandwidth updates from being presented.
- """
-
- if isPause == self.isPaused: return
- self.isPaused = isPause
- if self.isVisible: self._parameterSwap()
-
- def setVisible(self, isVisible):
- """
- Toggles panel visability, hiding if false.
- """
-
- if isVisible == self.isVisible: return
- self.isVisible = isVisible
-
- if self.isVisible: self.height = self.contentHeight
- else: self.height = 0
-
- if not self.isPaused: self._parameterSwap()
-
- def _parameterSwap(self):
- if self.isPaused or not self.isVisible:
- if self.pauseBuffer == None: self.pauseBuffer = BandwidthMonitor(None, None)
-
- # TODO: use a more clever swap using 'active' and 'inactive' instances
- self.pauseBuffer.tick = self.tick
- self.pauseBuffer.lastDownloadRate = self.lastDownloadRate
- self.pauseBuffer.lastUploadRate = self.lastUploadRate
- self.pauseBuffer.maxDownloadRate = list(self.maxDownloadRate)
- self.pauseBuffer.maxUploadRate = list(self.maxUploadRate)
- self.pauseBuffer.downloadRates = copy.deepcopy(self.downloadRates)
- self.pauseBuffer.uploadRates = copy.deepcopy(self.uploadRates)
- self.pauseBuffer.totalDownload = self.totalDownload
- self.pauseBuffer.totalUpload = self.totalUpload
- self.pauseBuffer.bwRate = self.bwRate
- self.pauseBuffer.bwBurst = self.bwBurst
- else:
- self.tick = self.pauseBuffer.tick
- self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
- self.lastUploadRate = self.pauseBuffer.lastUploadRate
- self.maxDownloadRate = self.pauseBuffer.maxDownloadRate
- self.maxUploadRate = self.pauseBuffer.maxUploadRate
- self.downloadRates = self.pauseBuffer.downloadRates
- self.uploadRates = self.pauseBuffer.uploadRates
- self.totalDownload = self.pauseBuffer.totalDownload
- self.totalUpload = self.pauseBuffer.totalUpload
- self.bwRate = self.pauseBuffer.bwRate
- self.bwBurst = self.pauseBuffer.bwBurst
- self.redraw()
-
- def _updateAccountingInfo(self):
- """
- Updates mapping used for accounting info. This includes the following keys:
- status, resetTime, read, written, readLimit, writtenLimit
-
- Sets mapping to None if the Tor connection is closed.
- """
-
- try:
- self.accountingInfo = {}
-
- accountingParams = self.conn.get_info(["accounting/hibernating", "accounting/bytes", "accounting/bytes-left", "accounting/interval-end"])
- self.accountingInfo["status"] = accountingParams["accounting/hibernating"]
-
- # altzone subtraction converts from gmt to local with respect to DST
- sec = time.mktime(time.strptime(accountingParams["accounting/interval-end"], "%Y-%m-%d %H:%M:%S")) - time.time() - time.altzone
- resetHours = sec / 3600
- sec %= 3600
- resetMin = sec / 60
- sec %= 60
- self.accountingInfo["resetTime"] = "%i:%02i:%02i" % (resetHours, resetMin, sec)
-
- read = int(accountingParams["accounting/bytes"].split(" ")[0])
- written = int(accountingParams["accounting/bytes"].split(" ")[1])
- readLeft = int(accountingParams["accounting/bytes-left"].split(" ")[0])
- writtenLeft = int(accountingParams["accounting/bytes-left"].split(" ")[1])
-
- self.accountingInfo["read"] = util.getSizeLabel(read)
- self.accountingInfo["written"] = util.getSizeLabel(written)
- self.accountingInfo["readLimit"] = util.getSizeLabel(read + readLeft)
- self.accountingInfo["writtenLimit"] = util.getSizeLabel(written + writtenLeft)
- except TorCtl.TorCtlClosed:
- self.accountingInfo = None
-
Added: arm/trunk/interface/connCountMonitor.py
===================================================================
--- arm/trunk/interface/connCountMonitor.py (rev 0)
+++ arm/trunk/interface/connCountMonitor.py 2009-08-08 07:02:41 UTC (rev 20233)
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# connCountMonitor.py -- Tracks the number of connections made by Tor.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import time
+from threading import Thread
+from TorCtl import TorCtl
+
+import connPanel
+import graphPanel
+import util
+
+class ConnCountMonitor(graphPanel.GraphStats, Thread):
+ """
+ Tracks number of connections, using cached values in connPanel if recent
+ enough (otherwise retrieved independently).
+ """
+
+ def __init__(self, connectionPanel):
+ graphPanel.GraphStats.__init__(self)
+ Thread.__init__(self)
+ graphPanel.GraphStats.initialize(self, connPanel.TYPE_COLORS["inbound"], connPanel.TYPE_COLORS["outbound"], 10)
+
+ self.lastUpdate = -1 # time last stats was retrived
+ self.connectionPanel = connectionPanel # connection panel, used to limit netstat calls
+
+ self.setDaemon(True)
+ self.start()
+
+ def run(self):
+ while True:
+ while self.lastUpdate + 1 > time.time(): time.sleep(0.5)
+
+ if self.connectionPanel.lastUpdate + 1 >= time.time():
+ # reuses netstat results if recent enough
+ counts = self.connectionPanel.connectionCount
+ self._processEvent(counts[0], counts[1])
+ else:
+ # cached results stale - requery netstat
+ inbound, outbound, control = 0, 0, 0
+ netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.connectionPanel.pid)
+ try:
+ results = netstatCall.readlines()
+
+ for line in results:
+ if not line.startswith("tcp"): continue
+ param = line.split()
+ localPort = param[3][param[3].find(":") + 1:]
+
+ if localPort in (self.connectionPanel.orPort, self.connectionPanel.dirPort): inbound += 1
+ elif localPort == self.connectionPanel.controlPort: control += 1
+ else: outbound += 1
+ except IOError:
+ # netstat call failed
+ self.connectionPanel.monitor_event("WARN", "Unable to query netstat for connection counts")
+
+ netstatCall.close()
+ self._processEvent(inbound, outbound)
+
+ self.lastUpdate = time.time()
+
+ def getTitle(self, width):
+ return "Connection Count:"
+
+ def getHeaderLabel(self, isPrimary):
+ avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+ if isPrimary: return "Inbound (%s, avg: %s):" % (self.lastPrimary, avg)
+ else: return "Outbound (%s, avg: %s):" % (self.lastSecondary, avg)
+
Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py 2009-08-08 04:28:00 UTC (rev 20232)
+++ arm/trunk/interface/connPanel.py 2009-08-08 07:02:41 UTC (rev 20233)
@@ -3,6 +3,7 @@
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
import os
+import time
import socket
import curses
from TorCtl import TorCtl
@@ -92,6 +93,7 @@
self.allowDNS = True # permits hostname resolutions if true
self.showLabel = True # shows top label if true, hides otherwise
self.showingDetails = False # augments display to accomidate details window if true
+ self.lastUpdate = -1 # time last stats was retrived
self.sortOrdering = [ORD_TYPE, ORD_FOREIGN_LISTING, ORD_FOREIGN_PORT]
self.isPaused = False
self.resolver = hostnameResolver.HostnameResolver()
@@ -172,7 +174,7 @@
# looks at netstat for tor with stderr redirected to /dev/null, options are:
# n = prevents dns lookups, p = include process (say if it's tor), t = tcp only
- netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor" % self.pid)
+ netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.pid)
try:
results = netstatCall.readlines()
@@ -201,6 +203,8 @@
if not self.providedGeoipWarning:
self.logger.monitor_event("WARN", "Tor geoip database is unavailable.")
self.providedGeoipWarning = True
+ except error:
+ countryCode = "??"
self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode))
except IOError:
@@ -208,6 +212,7 @@
self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
netstatCall.close()
+ self.lastUpdate = time.time()
# hostnames are sorted at redraw - otherwise now's a good time
if self.listingType != LIST_HOSTNAME: self.sortConnections()
Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py 2009-08-08 04:28:00 UTC (rev 20232)
+++ arm/trunk/interface/controller.py 2009-08-08 07:02:41 UTC (rev 20233)
@@ -12,13 +12,16 @@
from threading import RLock
from TorCtl import TorCtl
-import util
import headerPanel
-import bandwidthPanel
+import graphPanel
import logPanel
import connPanel
import confPanel
+import util
+import bandwidthMonitor
+import connCountMonitor
+
REFRESH_RATE = 5 # seconds between redrawing screen
cursesLock = RLock() # global curses lock (curses isn't thread safe and
# concurrency bugs produce especially sinister glitches)
@@ -29,10 +32,10 @@
# panel order per page
PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
PAGES = [
- ["bandwidth", "log"],
+ ["graph", "log"],
["conn"],
["torrc"]]
-PAUSEABLE = ["header", "bandwidth", "log", "conn"]
+PAUSEABLE = ["header", "graph", "log", "conn"]
PAGE_COUNT = 3 # all page numbering is internally represented as 0-indexed
# events needed for panels other than the event log
@@ -180,7 +183,7 @@
if eventType in loggedEvents: loggedEvents.remove(eventType)
if eventType in REQ_EVENTS:
- if eventType == "BW": msg = "(bandwidth panel won't function)"
+ if eventType == "BW": msg = "(bandwidth graph won't function)"
elif eventType in ("NEWDESC", "NEWCONSENSUS"): msg = "(connections listing can't register consensus changes)"
else: msg = ""
logListener.monitor_event("ERR", "Unsupported event type: %s %s" % (eventType, msg))
@@ -237,7 +240,7 @@
panels = {
"header": headerPanel.HeaderPanel(cursesLock, conn, torPid),
"popup": util.Panel(cursesLock, 9),
- "bandwidth": bandwidthPanel.BandwidthMonitor(cursesLock, conn),
+ "graph": graphPanel.GraphPanel(cursesLock),
"log": logPanel.LogMonitor(cursesLock, loggedEvents),
"torrc": confPanel.ConfPanel(cursesLock, conn.get_info("config-file")["config-file"])}
panels["conn"] = connPanel.ConnPanel(cursesLock, conn, torPid, panels["log"])
@@ -246,9 +249,14 @@
# provides error if pid coulnd't be determined (hopefully shouldn't happen...)
if not torPid: panels["log"].monitor_event("WARN", "Unable to resolve tor pid, abandoning connection listing")
+ # statistical monitors for graph
+ panels["graph"].addStats("bandwidth", bandwidthMonitor.BandwidthMonitor(conn))
+ panels["graph"].addStats("connection count", connCountMonitor.ConnCountMonitor(panels["conn"]))
+ panels["graph"].setStats("bandwidth")
+
# listeners that update bandwidth and log panels with Tor status
conn.add_event_listener(panels["log"])
- conn.add_event_listener(panels["bandwidth"])
+ conn.add_event_listener(panels["graph"].stats["bandwidth"])
conn.add_event_listener(panels["conn"])
# tells Tor to listen to the events we're interested
@@ -342,13 +350,12 @@
popup.addstr(0, 0, "Page %i Commands:" % (page + 1), util.LABEL_ATTR)
if page == 0:
- bwVisibleLabel = "visible" if panels["bandwidth"].isVisible else "hidden"
- popup.addfstr(1, 2, "b: toggle bandwidth panel (<b>%s</b>)" % bwVisibleLabel)
-
- # matches timescale used by bandwith panel to recognized labeling
- intervalLabel = bandwidthPanel.UPDATE_INTERVALS[panels["bandwidth"].updateIntervalIndex][0]
- popup.addfstr(1, 41, "i: graph update interval (<b>%s</b>)" % intervalLabel)
- popup.addstr(2, 2, "e: change logged events")
+ graphedStats = panels["graph"].currentDisplay
+ if not graphedStats: graphedStats = "none"
+ popup.addfstr(1, 2, "s: graphed stats (<b>%s</b>)" % graphedStats)
+ popup.addfstr(1, 41, "i: graph update interval (<b>%s</b>)" % panels["graph"].updateInterval)
+ popup.addfstr(2, 2, "b: graph bounds (<b>%s</b>)" % graphPanel.BOUND_LABELS[panels["graph"].bounds])
+ popup.addstr(2, 41, "e: change logged events")
if page == 1:
popup.addstr(1, 2, "up arrow: scroll up a line")
popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -387,29 +394,62 @@
setPauseState(panels, isPaused, page)
finally:
cursesLock.release()
- elif page == 0 and (key == ord('b') or key == ord('B')):
- # toggles bandwidth panel visability
- panels["bandwidth"].setVisible(not panels["bandwidth"].isVisible)
- oldY = -1 # force resize event
+ elif page == 0 and (key == ord('s') or key == ord('S')):
+ # provides menu to pick stats to be graphed
+ #options = ["None"] + [label for label in panels["graph"].stats.keys()]
+ options = ["None"]
+
+ # appends stats labels with first letters of each word capitalized
+ initialSelection, i = -1, 1
+ if not panels["graph"].currentDisplay: initialSelection = 0
+ for label in panels["graph"].stats.keys():
+ if label == panels["graph"].currentDisplay: initialSelection = i
+ words = label.split()
+ options.append(" ".join(word[0].upper() + word[1:] for word in words))
+ i += 1
+
+ # hides top label of the graph panel and pauses panels
+ if panels["graph"].currentDisplay:
+ panels["graph"].showLabel = False
+ panels["graph"].redraw()
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
+
+ # reverts changes made for popup
+ panels["graph"].showLabel = True
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1 and selection != initialSelection:
+ if selection == 0: panels["graph"].setStats(None)
+ else: panels["graph"].setStats(options[selection].lower())
+ oldY = -1 # force resize event
elif page == 0 and (key == ord('i') or key == ord('I')):
- # provides menu to pick bandwidth graph update interval
- options = [label for (label, intervalTime) in bandwidthPanel.UPDATE_INTERVALS]
- initialSelection = panels["bandwidth"].updateIntervalIndex
+ # provides menu to pick graph panel update interval
+ options = [label for (label, intervalTime) in graphPanel.UPDATE_INTERVALS]
- # hides top label of bandwidth panel and pauses panels
- panels["bandwidth"].showLabel = False
- panels["bandwidth"].redraw()
+ initialSelection = -1
+ for i in range(len(options)):
+ if options[i] == panels["graph"].updateInterval: initialSelection = i
+
+ # hides top label of the graph panel and pauses panels
+ if panels["graph"].currentDisplay:
+ panels["graph"].showLabel = False
+ panels["graph"].redraw()
setPauseState(panels, isPaused, page, True)
selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
# reverts changes made for popup
- panels["bandwidth"].showLabel = True
+ panels["graph"].showLabel = True
setPauseState(panels, isPaused, page)
# applies new setting
- if selection != -1:
- panels["bandwidth"].setUpdateInterval(selection)
+ if selection != -1: panels["graph"].updateInterval = options[selection]
+ elif page == 0 and (key == ord('b') or key == ord('B')):
+ # uses the next boundary type for graph
+ panels["graph"].bounds = (panels["graph"].bounds + 1) % 2
elif page == 0 and (key == ord('e') or key == ord('E')):
# allow user to enter new types of events to log - unchanged if left blank
cursesLock.acquire()
Added: arm/trunk/interface/graphPanel.py
===================================================================
--- arm/trunk/interface/graphPanel.py (rev 0)
+++ arm/trunk/interface/graphPanel.py 2009-08-08 07:02:41 UTC (rev 20233)
@@ -0,0 +1,281 @@
+#!/usr/bin/env python
+# graphPanel.py -- Graph providing a variety of statistics.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import copy
+import curses
+
+import util
+
+GRAPH_COL = 30 # columns of data in graph
+
+# enums for graph bounds:
+# BOUNDS_MAX - global maximum (highest value ever seen)
+# BOUNDS_TIGHT - local maximum (highest value currently on the graph)
+BOUNDS_MAX, BOUNDS_TIGHT = range(2)
+BOUND_LABELS = {BOUNDS_MAX: "max", BOUNDS_TIGHT: "tight"}
+
+# time intervals at which graphs can be updated
+DEFAULT_UPDATE_INTERVAL = "5 seconds"
+UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5), ("30 seconds", 30), ("minutely", 60),
+ ("half hour", 1800), ("hourly", 3600), ("daily", 86400)]
+
+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 UPDATE_INTERVALS.
+ """
+
+ def __init__(self):
+ """
+ Initializes all parameters to dummy values.
+ """
+
+ self.primaryColor = None # colors used to draw stats/graphs
+ self.secondaryColor = None
+ self.height = None # vertical size of content
+ self.graphPanel = None # panel where stats are drawn (set when added to GraphPanel)
+
+ self.isPaused = False
+ self.pauseBuffer = None # mirror instance used to track updates when pauses -
+ # this is a pauseBuffer instance itself if None
+
+ # tracked stats
+ self.tick = 0 # number of events processed
+ self.lastPrimary = 0 # most recent registered stats
+ self.lastSecondary = 0
+ self.primaryTotal = 0 # sum of all stats seen
+ self.secondaryTotal = 0
+
+ # timescale dependent stats
+ self.maxPrimary, self.maxSecondary = {}, {}
+ self.primaryCounts, self.secondaryCounts = {}, {}
+ for (label, timescale) in UPDATE_INTERVALS:
+ # recent rates for graph
+ self.maxPrimary[label] = 1
+ self.maxSecondary[label] = 1
+
+ # historic stats for graph, first is accumulator
+ # iterative insert needed to avoid making shallow copies (nasty, nasty gotcha)
+ self.primaryCounts[label] = (GRAPH_COL + 1) * [0]
+ self.secondaryCounts[label] = (GRAPH_COL + 1) * [0]
+
+ def initialize(self, primaryColor, secondaryColor, height, pauseBuffer=None):
+ """
+ Initializes newly constructed GraphPanel instance.
+ """
+
+ # used because of python's inability to have overloaded constructors
+ self.primaryColor = primaryColor # colors used to draw stats/graphs
+ self.secondaryColor = secondaryColor
+ self.height = height # vertical size of content
+
+ # mirror instance used to track updates when paused
+ if not pauseBuffer: self.pauseBuffer = GraphStats()
+ else: self.pauseBuffer = pauseBuffer
+
+ def getTitle(self, width):
+ """
+ Provides top label.
+ """
+
+ return ""
+
+ def getHeaderLabel(self, isPrimary):
+ """
+ Provides labeling presented at the top of the graph.
+ """
+
+ return ""
+
+ def getFooterLabel(self, isPrimary):
+ """
+ Provides labeling present at the bottom of the graph.
+ """
+
+ return ""
+
+ def redraw(self, panel):
+ """
+ Allows for any custom redrawing monitor wishes to append.
+ """
+
+ pass
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents bandwidth updates from being presented. This is a no-op
+ if a pause buffer.
+ """
+
+ if isPause == self.isPaused or not self.pauseBuffer: return
+ self.isPaused = isPause
+
+ if self.isPaused: active, inactive = self.pauseBuffer, self
+ else: active, inactive = self, self.pauseBuffer
+ self._parameterSwap(active, inactive)
+
+ def _parameterSwap(self, active, inactive):
+ """
+ Either overwrites parameters of pauseBuffer or with the current values or
+ vice versa. This is a helper method for setPaused and should be overwritten
+ to append with additional parameters that need to be preserved when paused.
+ """
+
+ active.tick = inactive.tick
+ active.lastPrimary = inactive.lastPrimary
+ active.lastSecondary = inactive.lastSecondary
+ active.primaryTotal = inactive.primaryTotal
+ active.secondaryTotal = inactive.secondaryTotal
+ active.maxPrimary = dict(inactive.maxPrimary)
+ active.maxSecondary = dict(inactive.maxSecondary)
+ active.primaryCounts = copy.deepcopy(inactive.primaryCounts)
+ active.secondaryCounts = copy.deepcopy(inactive.secondaryCounts)
+
+ def _processEvent(self, primary, secondary):
+ """
+ Includes new stats in graphs and notifies GraphPanel of changes.
+ """
+
+ if self.isPaused: self.pauseBuffer._processEvent(primary, secondary)
+ else:
+ self.lastPrimary, self.lastSecondary = primary, secondary
+ self.primaryTotal += primary
+ self.secondaryTotal += secondary
+
+ # updates for all time intervals
+ self.tick += 1
+ for (label, timescale) in UPDATE_INTERVALS:
+ self.primaryCounts[label][0] += primary
+ self.secondaryCounts[label][0] += secondary
+
+ if self.tick % timescale == 0:
+ self.maxPrimary[label] = max(self.maxPrimary[label], self.primaryCounts[label][0] / timescale)
+ self.primaryCounts[label][0] /= timescale
+ self.primaryCounts[label].insert(0, 0)
+ del self.primaryCounts[label][GRAPH_COL + 1:]
+
+ self.maxSecondary[label] = max(self.maxSecondary[label], self.secondaryCounts[label][0] / timescale)
+ self.secondaryCounts[label][0] /= timescale
+ self.secondaryCounts[label].insert(0, 0)
+ del self.secondaryCounts[label][GRAPH_COL + 1:]
+
+ if self.graphPanel: self.graphPanel.redraw()
+
+class GraphPanel(util.Panel):
+ """
+ Panel displaying a graph, drawing statistics from custom GraphStats
+ implementations.
+ """
+
+ def __init__(self, lock):
+ util.Panel.__init__(self, lock, 0) # height is overwritten with current module
+ self.updateInterval = DEFAULT_UPDATE_INTERVAL
+ self.isPaused = False
+ self.showLabel = True # shows top label if true, hides otherwise
+ self.bounds = BOUNDS_MAX # determines bounds on graph
+ self.currentDisplay = None # label of the stats currently being displayed
+ self.stats = {} # available stats (mappings of label -> instance)
+
+ def redraw(self):
+ """ Redraws graph panel """
+ if self.win:
+ if not self.lock.acquire(False): return
+ try:
+ self.clear()
+
+ if self.currentDisplay:
+ param = self.stats[self.currentDisplay]
+ primaryColor = util.getColor(param.primaryColor)
+ secondaryColor = util.getColor(param.secondaryColor)
+
+ if self.showLabel: self.addstr(0, 0, param.getTitle(self.maxX), util.LABEL_ATTR)
+
+ # top labels
+ left, right = param.getHeaderLabel(True), param.getHeaderLabel(False)
+ if left: self.addstr(1, 0, left, curses.A_BOLD | primaryColor)
+ if right: self.addstr(1, 35, right, curses.A_BOLD | secondaryColor)
+
+ # determines max value on the graph
+ primaryBound, secondaryBound = -1, -1
+
+ if self.bounds == BOUNDS_MAX:
+ primaryBound = param.maxPrimary[self.updateInterval]
+ secondaryBound = param.maxSecondary[self.updateInterval]
+ elif self.bounds == BOUNDS_TIGHT:
+ for value in param.primaryCounts[self.updateInterval][1:]: primaryBound = max(value, primaryBound)
+ for value in param.secondaryCounts[self.updateInterval][1:]: secondaryBound = max(value, secondaryBound)
+
+ # displays bound
+ self.addstr(2, 0, "%4s" % str(int(primaryBound)), primaryColor)
+ self.addstr(7, 0, " 0", primaryColor)
+
+ self.addstr(2, 35, "%4s" % str(int(secondaryBound)), secondaryColor)
+ self.addstr(7, 35, " 0", secondaryColor)
+
+ # creates bar graph of bandwidth usage over time
+ for col in range(GRAPH_COL):
+ colHeight = min(5, 5 * param.primaryCounts[self.updateInterval][col + 1] / primaryBound)
+ for row in range(colHeight): self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
+
+ for col in range(GRAPH_COL):
+ colHeight = min(5, 5 * param.secondaryCounts[self.updateInterval][col + 1] / secondaryBound)
+ for row in range(colHeight): self.addstr(7 - row, col + 40, " ", curses.A_STANDOUT | secondaryColor)
+
+ # bottom labels
+ left, right = param.getFooterLabel(True), param.getFooterLabel(False)
+ if left: self.addstr(8, 1, left, primaryColor)
+ if right: self.addstr(8, 36, right, secondaryColor)
+
+ # allows for finishing touches by monitor
+ param.redraw(self)
+
+ self.refresh()
+ finally:
+ self.lock.release()
+
+ def addStats(self, label, stats):
+ """
+ Makes GraphStats instance available in the panel.
+ """
+
+ stats.graphPanel = self
+ self.stats[label] = stats
+ stats.isPaused = True
+
+ def setStats(self, label):
+ """
+ Sets the current stats instance, hiding panel if None.
+ """
+
+ if label != self.currentDisplay:
+ if self.currentDisplay: self.stats[self.currentDisplay].setPaused(True)
+
+ if not label:
+ self.currentDisplay = None
+ self.height = 0
+ elif label in self.stats.keys():
+ self.currentDisplay = label
+
+ newStats = self.stats[label]
+
+ # TODO: BUG - log panel's partly overwritten if showing a smaller panel
+ # (simple workaround is to use max size, but fix would be preferable)
+ #self.height = newStats.height
+ maxHeight = 0
+ for panel in self.stats.values(): maxHeight = max(panel.height, maxHeight)
+ self.height = maxHeight
+
+ newStats.setPaused(self.isPaused)
+ else: raise ValueError("Unrecognized stats label: %s" % label)
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents bandwidth updates from being presented.
+ """
+
+ if isPause == self.isPaused: return
+ self.isPaused = isPause
+ if self.currentDisplay: self.stats[self.currentDisplay].setPaused(self.isPaused)
+
More information about the tor-commits
mailing list