Stats plugin update

This commit is contained in:
Calum Lind 2011-06-06 11:52:08 +01:00
parent 3397c2487b
commit 16fbf27b90
9 changed files with 872 additions and 283 deletions

View File

@ -36,5 +36,4 @@ import pkg_resources
import os.path
def get_resource(filename):
return pkg_resources.resource_filename("deluge.plugins.stats",
os.path.join("data", filename))
return pkg_resources.resource_filename("deluge.plugins.stats", os.path.join("data", filename))

View File

@ -22,18 +22,7 @@
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
@ -44,21 +33,19 @@
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
import logging
from twisted.internet.task import LoopingCall
import time
import deluge
from deluge.log import LOG as log
from deluge.plugins.pluginbase import CorePluginBase
from deluge import component
from deluge import configmanager
from deluge.core.rpcserver import export
log = logging.getLogger(__name__)
DEFAULT_PREFS = {
"test": "NiNiNi",
"update_interval": 2, #2 seconds.
"update_interval": 1, #2 seconds.
"length": 150, # 2 seconds * 150 --> 5 minutes.
}
@ -70,24 +57,55 @@ DEFAULT_TOTALS = {
"stats": {}
}
def get_key(config, key):
try:
return config[key]
except KeyError:
return None
def mean(items):
try:
return sum(items)/ len(items)
except Exception:
return 0
class Core(CorePluginBase):
totals = {} #class var to catch only updating this once per session in enable.
def enable(self):
log.debug("Stats plugin enabled")
self.core = component.get("Core")
self.stats ={}
self.count = {}
self.intervals = [1, 5, 30, 300]
self.last_update = {}
t = time.time()
for i in self.intervals:
self.stats[i] = {}
self.last_update[i] = t
self.count[i] = 0
self.config = configmanager.ConfigManager("stats.conf", DEFAULT_PREFS)
self.saved_stats = configmanager.ConfigManager("stats.totals", DEFAULT_TOTALS)
if self.totals == {}:
self.totals.update(self.saved_stats.config)
self.stats = self.saved_stats["stats"] or {}
self.length = self.config["length"]
#self.stats = get_key(self.saved_stats, "stats") or {}
self.stats_keys = []
self.add_stats(
'upload_rate',
'download_rate',
'num_connections',
'dht_nodes',
'dht_cache_nodes',
'dht_torrents',
'num_peers',
)
self.stats_keys = [
"payload_download_rate",
"payload_upload_rate"
]
self.update_stats()
self.update_timer = LoopingCall(self.update_stats)
@ -104,40 +122,94 @@ class Core(CorePluginBase):
except:
pass
def add_stats(self, *stats):
for stat in stats:
if stat not in self.stats_keys:
self.stats_keys.append(stat)
for i in self.intervals:
if stat not in self.stats[i]:
self.stats[i][stat] = []
def update_stats(self):
try:
status = self.core.get_session_status(self.stats_keys)
for key, value in status.items():
if key not in self.stats:
self.stats[key] = []
self.stats[key].insert(0, value)
#Get all possible stats!
stats = {}
for key in self.stats_keys:
#try all keys we have, very inefficient but saves having to
#work out where a key comes from...
try:
stats.update(self.core.get_session_status([key]))
except AttributeError:
pass
stats["num_connections"] = self.core.get_num_connections()
stats.update(self.core.get_config_values(["max_download",
"max_upload",
"max_num_connections"]))
# status = self.core.session.status()
# for stat in dir(status):
# if not stat.startswith('_') and stat not in stats:
# stats[stat] = getattr(status, stat, None)
for stat_list in self.stats.values():
if len(stat_list) > self.config["length"]:
update_time = time.time()
self.last_update[1] = update_time
#extract the ones we are interested in
#adding them to the 1s array
for stat, stat_list in self.stats[1].iteritems():
if stat in stats:
stat_list.insert(0, int(stats[stat]))
else:
stat_list.insert(0, 0)
if len(stat_list) > self.length:
stat_list.pop()
self.last_update = time.time()
def update_interval(interval, base, multiplier):
self.count[interval] = self.count[interval] + 1
if self.count[interval] >= interval:
self.last_update[interval] = update_time
self.count[interval] = 0
current_stats = self.stats[interval]
for stat, stat_list in self.stats[base].iteritems():
try:
avg = mean(stat_list[0:multiplier])
except ValueError:
avg = 0
current_stats[stat].insert(0, avg)
if len(current_stats[stat]) > self.length:
current_stats[stat].pop()
update_interval(5, 1, 5)
update_interval(30, 5, 6)
update_interval(300, 30, 10)
except Exception, e:
log.exception(e)
log.error("Stats update error %s" % e)
return True
def save_stats(self):
try:
self.saved_stats["stats"] = self.stats
self.saved_stats.config.update(self.get_totals())
self.saved_stats.save()
except Exception,e:
log.exception(e)
except Exception, e:
log.error("Stats save error", e)
return True
# export:
@export
def get_stats(self, keys):
def get_stats(self, keys, interval):
if interval not in self.intervals:
return None
stats_dict = {}
for key in keys:
if key in self.stats:
stats_dict[key] = self.stats[key]
stats_dict["_last_update"] = self.last_update
if key in self.stats[interval]:
stats_dict[key] = self.stats[interval][key]
stats_dict["_last_update"] = self.last_update[interval]
stats_dict["_length"] = self.config["length"]
stats_dict["_update_interval"] = interval
return stats_dict
@export
@ -169,3 +241,8 @@ class Core(CorePluginBase):
def get_config(self):
"returns the config dictionary"
return self.config.config
@export
def get_intervals(self):
"Returns the available resolutions"
return self.intervals

View File

@ -1,24 +1,263 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Fri Aug 8 23:34:44 2008 -->
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.16 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="window1">
<child>
<widget class="GtkHBox" id="prefs_box">
<widget class="GtkVBox" id="prefs_box">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label1">
<widget class="GtkFrame" id="frame1">
<property name="visible">True</property>
<property name="label" translatable="yes">Test config value:</property>
</widget>
</child>
<child>
<widget class="GtkEntry" id="txt_test">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment4">
<property name="visible">True</property>
<property name="left_padding">15</property>
<child>
<widget class="GtkTable" id="table2">
<property name="visible">True</property>
<property name="n_rows">10</property>
<property name="n_columns">2</property>
<property name="column_spacing">15</property>
<child>
<widget class="GtkColorButton" id="bandwidth_graph_download_rate_color">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="color">#000000000000</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Download color:</property>
</widget>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Upload color:</property>
</widget>
<packing>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing>
</child>
<child>
<widget class="GtkColorButton" id="bandwidth_graph_upload_rate_color">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="color">#000000000000</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label8">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&lt;b&gt;Connections Graph&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="right_attach">2</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label9">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&lt;b&gt;Bandwidth Graph&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="right_attach">2</property>
</packing>
</child>
<child>
<widget class="GtkColorButton" id="connections_graph_dht_nodes_color">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="color">#000000000000</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">DHT nodes:</property>
</widget>
<packing>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
</packing>
</child>
<child>
<widget class="GtkColorButton" id="connections_graph_dht_cache_nodes_color">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="color">#000000000000</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
<property name="x_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label11">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Cached DHT nodes:</property>
</widget>
<packing>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label12">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">DHT torrents:</property>
</widget>
<packing>
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label13">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Connections:</property>
</widget>
<packing>
<property name="top_attach">7</property>
<property name="bottom_attach">8</property>
</packing>
</child>
<child>
<widget class="GtkColorButton" id="connections_graph_dht_torrents_color">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="color">#000000000000</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
<property name="x_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<widget class="GtkColorButton" id="connections_graph_num_connections_color">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="color">#000000000000</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">7</property>
<property name="bottom_attach">8</property>
<property name="x_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label16">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&lt;b&gt;Seeds / Peers&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="right_attach">2</property>
<property name="top_attach">8</property>
<property name="bottom_attach">9</property>
</packing>
</child>
<child>
<widget class="GtkColorButton" id="seeds_graph_num_peers_color">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="color">#000000000000</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">9</property>
<property name="bottom_attach">10</property>
<property name="x_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label17">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Peers:</property>
</widget>
<packing>
<property name="top_attach">9</property>
<property name="bottom_attach">10</property>
</packing>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;b&gt;Graph Colors&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="type">label_item</property>
</packing>
</child>
</widget>
<packing>
<property name="position">1</property>
<property name="position">0</property>
</packing>
</child>
</widget>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Mon Oct 13 20:17:39 2008 -->
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="window1">
<child>
<widget class="GtkVBox" id="vbox1">
@ -14,6 +14,9 @@
<property name="visible">True</property>
<property name="stock">gtk-page-setup</property>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="graph_label_text">
@ -25,71 +28,121 @@
</packing>
</child>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="graph_tab">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<widget class="GtkNotebook" id="graph_notebook">
<widget class="GtkViewport" id="viewport1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tab_pos">GTK_POS_LEFT</property>
<property name="resize_mode">queue</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkDrawingArea" id="bandwidth_graph">
<widget class="GtkVBox" id="vbox2">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<child>
<widget class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="label" translatable="yes">Resolution</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkComboBox" id="combo_intervals">
<property name="visible">True</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkNotebook" id="graph_notebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tab_pos">left</property>
<child>
<widget class="GtkDrawingArea" id="bandwidth_graph">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkLabel" id="bandwidth_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Bandwidth</property>
</widget>
<packing>
<property name="tab_fill">False</property>
<property name="type">tab</property>
</packing>
</child>
<child>
<widget class="GtkDrawingArea" id="connections_graph">
<property name="visible">True</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="connections_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Connections</property>
</widget>
<packing>
<property name="position">1</property>
<property name="tab_fill">False</property>
<property name="type">tab</property>
</packing>
</child>
<child>
<widget class="GtkDrawingArea" id="seeds_graph">
<property name="visible">True</property>
</widget>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="seeds_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Seeds/Peers</property>
</widget>
<packing>
<property name="position">2</property>
<property name="tab_fill">False</property>
<property name="type">tab</property>
</packing>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="bandwidth_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Bandwidth</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkDrawingArea" id="connections_graph">
<property name="visible">True</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="connections_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Connections</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkDrawingArea" id="seeds_graph">
<property name="visible">True</property>
</widget>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="seeds_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Seeds/Peers</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">2</property>
<property name="tab_fill">False</property>
</packing>
</child>
</widget>
</child>
</widget>

View File

@ -1,6 +1,7 @@
#
# graph.py
#
# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net>
# Copyright (C) 2008 Damien Churchill <damoxc@gmail.com>
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) Marcos Pinto 2007 <markybob@gmail.com>
@ -21,7 +22,7 @@
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
@ -32,23 +33,14 @@
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
"""
port of old plugin by markybob.
"""
import time
import math
import cairo
import logging
from deluge.log import LOG as log
from deluge.ui.client import client
black = (0, 0, 0)
@ -60,13 +52,18 @@ green = (0, 1.0, 0)
blue = (0, 0, 1.0)
orange = (1.0, 0.74, 0)
log = logging.getLogger(__name__)
def default_formatter(value):
return str(value)
def size_formatter_scale(value):
scale = 1.0
for i in range(0,3):
scale = scale * 1024.0
if value / scale < 1024:
return scale
def change_opacity(color, opactiy):
"""A method to assist in changing the opacity of a color in order to draw the
"""A method to assist in changing the opactiy of a color inorder to draw the
fills.
"""
color = list(color)
@ -83,6 +80,7 @@ class Graph:
self.length = 150
self.stat_info = {}
self.line_size = 2
self.dash_length = [10]
self.mean_selected = True
self.legend_selected = True
self.max_selected = True
@ -105,125 +103,198 @@ class Graph:
def set_stats(self, stats):
self.last_update = stats["_last_update"]
log.debug("Last update: %s" % self.last_update)
del stats["_last_update"]
self.length = stats["_length"]
del stats["_length"]
self.interval = stats["_update_interval"]
del stats["_update_interval"]
self.stats = stats
return
def set_config(self, config):
self.length = config["length"]
self.interval = config["update_interval"]
# def set_config(self, config):
# self.length = config["length"]
# self.interval = config["update_interval"]
def set_interval(self, interval):
self.interval = interval
def draw_to_context(self, context, width, height):
self.ctx = context
self.width, self.height = width, height
try:
self.draw_rect(white, 0, 0, self.width, self.height)
self.draw_x_axis()
self.draw_left_axis()
if self.legend_selected:
self.draw_legend()
except cairo.Error, e:
log.exception(e)
self.draw_rect(white, 0, 0, self.width, self.height)
self.draw_graph()
return self.ctx
def draw(self, width, height):
self.width = width
self.height = height
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx = cairo.Context(surface)
self.draw_to_context(ctx, width, height)
return surface
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height)
self.ctx = cairo.Context(self.surface)
self.draw_rect(white, 0, 0, self.width, self.height)
self.draw_x_axis()
self.draw_left_axis()
if self.legend_selected:
self.draw_legend()
return self.surface
def draw_x_axis(self):
duration = float(self.length * self.interval)
def draw_x_axis(self, bounds):
(left, top, right, bottom) = bounds
duration = self.length * self.interval
start = self.last_update - duration
ratio = (self.width - 40) / duration
seconds_to_minute = 60 - time.localtime(start)[5]
ratio = (right - left) / float(duration)
if duration < 1800 * 10:
#try rounding to nearest 1min, 5mins, 10mins, 30mins
for step in [60, 300, 600, 1800]:
if duration / step < 10:
x_step = step
break
else:
# if there wasnt anything useful find a nice fitting hourly divisor
x_step = ((duration / 5) /3600 )* 3600
for i in xrange(0, 5):
text = time.strftime('%H:%M', time.localtime(start + seconds_to_minute + (60*i)))
x = int(ratio * (seconds_to_minute + (60*i)))
self.draw_text(text, x + 46, self.height - 20)
x = x + 59.5
self.draw_dotted_line(gray, x, 20, x, self.height - 20)
#this doesnt allow for dst and timezones...
seconds_to_step = math.ceil(start/float(x_step)) * x_step - start
y = self.height - 22.5
self.draw_dotted_line(gray, 60, y, int(self.width), y)
for i in xrange(0, duration/x_step + 1):
text = time.strftime('%H:%M', time.localtime(start + seconds_to_step + i*x_step))
# + 0.5 to allign x to nearest pixel
x = int(ratio * (seconds_to_step + i*x_step) + left) + 0.5
self.draw_x_text(text, x, bottom)
self.draw_dotted_line(gray, x, top-0.5, x, bottom+0.5)
def draw_left_axis(self):
self.draw_line(gray, left, bottom+0.5, right, bottom+0.5)
def draw_graph(self):
font_extents = self.ctx.font_extents()
x_axis_space = font_extents[2] + 2 + self.line_size / 2.0
plot_height = self.height - x_axis_space
#lets say we need 2n-1*font height pixels to plot the y ticks
tick_limit = (plot_height / font_extents[3] )# / 2.0
max_value = 0
for stat in self.stat_info:
if self.stat_info[stat]['axis'] == 'left':
try:
l_max = max(self.stats[stat])
except ValueError:
l_max = 0
if l_max > max_value:
max_value = l_max
if max_value < self.left_axis['min']:
max_value = self.left_axis['min']
y_ticks = self.intervalise(max_value, tick_limit)
max_value = y_ticks[-1]
#find the width of the y_ticks
y_tick_text = [self.left_axis['formatter'](tick) for tick in y_ticks]
def space_required(text):
te = self.ctx.text_extents(text)
return math.ceil(te[4] - te[0])
y_tick_width = max((space_required(text) for text in y_tick_text))
top = font_extents[2] / 2.0
#bounds(left, top, right, bottom)
bounds = (y_tick_width + 4, top + 2, self.width, self.height - x_axis_space)
self.draw_x_axis(bounds)
self.draw_left_axis(bounds, y_ticks, y_tick_text)
def intervalise(self, x, limit=None):
"""Given a value x create an array of tick points to got with the graph
The number of ticks returned can be constrained by limit, minimum of 3
"""
#Limit is the number of ticks which is 1 + the number of steps as we
#count the 0 tick in limit
if limit is not None:
if limit <3:
limit = 2
else:
limit = limit -1
scale = 1
if 'formatter_scale' in self.left_axis:
scale = self.left_axis['formatter_scale'](x)
x = x / float(scale)
#Find the largest power of 10 less than x
log = math.log10(x)
intbit = math.floor(log)
interval = math.pow(10, intbit)
steps = int(math.ceil(x / interval))
if steps <= 1 and (limit is None or limit >= 10*steps):
interval = interval * 0.1
steps = steps * 10
elif steps <= 2 and (limit is None or limit >= 5*steps):
interval = interval * 0.2
steps = steps * 5
elif steps <=5 and (limit is None or limit >= 2*steps):
interval = interval * 0.5
steps = steps * 2
if limit is not None and steps > limit:
multi = steps / float(limit)
if multi > 2:
interval = interval * 5
else:
interval = interval * 2
intervals = [i * interval * scale for i in xrange(1+int(math.ceil(x/ interval)))]
return intervals
def draw_left_axis(self, bounds, y_ticks, y_tick_text):
(left, top, right, bottom) = bounds
stats = {}
max_values = []
for stat in self.stat_info:
if self.stat_info[stat]['axis'] == 'left':
stats[stat] = self.stat_info[stat]
stats[stat]['values'] = self.stats[stat]
stats[stat]['fill_color'] = change_opacity(stats[stat]['color'], 0.5)
stats[stat]['color'] = change_opacity(stats[stat]['color'], 0.8)
stats[stat]['max_value'] = max(self.stats[stat])
max_values.append(stats[stat]['max_value'])
if len(max_values) > 1:
max_value = max(*max_values)
else:
max_value = max_values[0]
if max_value < self.left_axis['min']:
max_value = self.left_axis['min']
height = self.height - self.line_size - 22
#max_value = float(round(max_value, len(str(max_value)) * -1))
max_value = float(max_value)
height = bottom - top
max_value = y_ticks[-1]
ratio = height / max_value
for i in xrange(1, 6):
y = int(ratio * ((max_value / 5) * i)) - 0.5
if i < 5:
self.draw_dotted_line(gray, 60, y, self.width, y)
text = self.left_axis['formatter']((max_value / 5) * (5 - i))
self.draw_text(text, 0, y - 6)
self.draw_dotted_line(gray, 60.5, 20, 60.5, self.height - 20)
for i, y_val in enumerate(y_ticks):
y = int(bottom - y_val * ratio ) - 0.5
if i != 0:
self.draw_dotted_line(gray, left, y, right, y)
self.draw_y_text(y_tick_text[i], left, y)
self.draw_line(gray, left, top, left, bottom)
for stat, info in stats.iteritems():
self.draw_value_poly(info['values'], info['color'], max_value)
self.draw_value_poly(info['values'], info['fill_color'], max_value, info['fill'])
if len(info['values']) > 0:
self.draw_value_poly(info['values'], info['color'], max_value, bounds)
self.draw_value_poly(info['values'], info['fill_color'], max_value, bounds, info['fill'])
def draw_legend(self):
pass
def trace_path(self, values, max_value):
height = self.height - 24
width = self.width
def trace_path(self, values, max_value, bounds):
(left, top, right, bottom) = bounds
ratio = (bottom - top) / max_value
line_width = self.line_size
self.ctx.set_line_width(line_width)
self.ctx.move_to(width, height)
self.ctx.move_to(right, bottom)
self.ctx.line_to(width,
int(height - ((height - 28) * values[0] / max_value)))
self.ctx.line_to(right, int(bottom - values[0] * ratio ))
x = width
step = (width - 60) / float(self.length)
x = right
step = (right - left) / float(self.length -1)
for i, value in enumerate(values):
if i == self.length - 1:
x = 62
self.ctx.line_to(x,
int(height - 1 - ((height - 28) * value / max_value))
)
x = left
self.ctx.line_to(x, int(bottom - value * ratio))
x -= step
self.ctx.line_to(
int(width + 62 - (((len(values) - 1) * width) / (self.length - 1))),
height)
int(right - (len(values) - 1) * step),
bottom)
self.ctx.close_path()
def draw_value_poly(self, values, color, max_value, fill=False):
self.trace_path(values, max_value)
def draw_value_poly(self, values, color, max_value, bounds, fill=False):
self.trace_path(values, max_value, bounds)
self.ctx.set_source_rgba(*color)
if fill:
@ -231,9 +302,26 @@ class Graph:
else:
self.ctx.stroke()
def draw_text(self, text, x, y):
self.ctx.set_font_size(9)
self.ctx.move_to(x, y + 9)
def draw_x_text(self, text, x, y):
"""Draws text below and horizontally centered about x,y"""
fe = self.ctx.font_extents()
te = self.ctx.text_extents(text)
height = fe[2]
x_bearing = te[0]
width = te[2]
self.ctx.move_to(int(x - width/2.0 + x_bearing), int(y + height))
self.ctx.set_source_rgba(*self.black)
self.ctx.show_text(text)
def draw_y_text(self, text, x, y):
"""Draws text left of and vertically centered about x,y"""
fe = self.ctx.font_extents()
te = self.ctx.text_extents(text)
descent = fe[1]
ascent = fe[0]
x_bearing = te[0]
width = te[4]
self.ctx.move_to(int(x - width - x_bearing - 2), int(y + (ascent - descent)/2.0))
self.ctx.set_source_rgba(*self.black)
self.ctx.show_text(text)
@ -252,13 +340,12 @@ class Graph:
def draw_dotted_line(self, color, x1, y1, x2, y2):
self.ctx.set_source_rgba(*color)
self.ctx.set_line_width(1)
dash, offset = self.ctx.get_dash()
self.ctx.set_dash(self.dash_length, 0)
self.ctx.move_to(x1, y1)
self.ctx.line_to(x2, y2)
#self.ctx.stroke_preserve()
#self.ctx.set_source_rgba(*white)
#self.ctx.set_dash((1, 1), 4)
self.ctx.stroke()
#self.ctx.set_dash((1, 1), 0)
self.ctx.set_dash(dash, offset)
if __name__ == "__main__":
import test

View File

@ -1,6 +1,7 @@
#
# gtkui.py
#
# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net>
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
#
# Basic plugin template created by:
@ -23,18 +24,7 @@
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
@ -45,89 +35,211 @@
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
import os
import gtk
import logging
import gobject
from gtk.glade import XML
from twisted.internet import defer
# Relative imports
from . import common
from . import graph
import graph
import deluge
from deluge import component
from deluge.log import LOG as log
from deluge.common import fspeed
from deluge.ui.client import client
from deluge.ui.gtkui.torrentdetails import Tab
from deluge.plugins.pluginbase import GtkPluginBase
log = logging.getLogger(__name__)
import common
DEFAULT_CONF = { 'version': 1,
'colors' :{
'bandwidth_graph': {'upload_rate': str(gtk.gdk.Color("blue")),
'download_rate': str(gtk.gdk.Color("green")),
},
'connections_graph': { 'dht_nodes': str(gtk.gdk.Color("orange")),
'dht_cache_nodes': str(gtk.gdk.Color("blue")),
'dht_torrents': str(gtk.gdk.Color("green")),
'num_connections': str(gtk.gdk.Color("darkred")),
},
'seeds_graph': { 'num_peers': str(gtk.gdk.Color("blue")),
},
}
}
def neat_time(column, cell, model, iter):
"""Render seconds as seconds or minutes with label"""
seconds = model.get_value(iter, 0)
if seconds >60:
text = "%d %s" % (seconds / 60, _("minutes"))
elif seconds == 60:
text = _("1 minute")
elif seconds == 1:
text = _("1 second")
else:
text = "%d %s" % (seconds, _("seconds"))
cell.set_property('text', text)
return
def int_str(number):
return (str(int(number)))
def gtk_to_graph_color(color):
"""Turns a gtk.gdk.Color into a tuple with range 0-1 as used by the graph"""
MAX = float(65535)
gtk_color = gtk.gdk.Color(color)
red = gtk_color.red / MAX
green = gtk_color.green / MAX
blue = gtk_color.blue / MAX
return (red, green, blue)
class GraphsTab(Tab):
def __init__(self, glade):
def __init__(self, glade, colors):
Tab.__init__(self)
self._name = 'Graphs'
self.glade = glade
self.window = self.glade.get_widget('graph_tab')
self._child_widget = self.window
self.notebook = self.glade.get_widget('graph_notebook')
self.label = self.glade.get_widget('graph_label')
self._name = 'Graphs'
self._child_widget = self.window
self._tab_label = self.label
self.colors = colors
self.bandwidth_graph = self.glade.get_widget('bandwidth_graph')
self.bandwidth_graph.connect('expose_event', self.expose)
self.bandwidth_graph.connect('expose_event', self.graph_expose)
self.connections_graph = self.glade.get_widget('connections_graph')
self.connections_graph.connect('expose_event', self.graph_expose)
self.seeds_graph = self.glade.get_widget('seeds_graph')
self.seeds_graph.connect('expose_event', self.graph_expose)
self.notebook.connect('switch-page', self._on_notebook_switch_page)
self.selected_interval = 1 #should come from config or similar
self.select_bandwidth_graph()
self.window.unparent()
self.label.unparent()
self.graph_widget = self.bandwidth_graph
self.graph = graph.Graph()
self.graph.add_stat('payload_download_rate', label='Download Rate', color=graph.green)
self.graph.add_stat('payload_upload_rate', label='Upload Rate', color=graph.blue)
self.graph.set_left_axis(formatter=fspeed, min=10240)
self.intervals = None
self.intervals_combo = self.glade.get_widget('combo_intervals')
cell = gtk.CellRendererText()
self.intervals_combo.pack_start(cell, True)
self.intervals_combo.set_cell_data_func(cell, neat_time)
self.intervals_combo.connect("changed", self._on_selected_interval_changed)
self.update_intervals()
def expose(self, widget, event):
"""Redraw"""
def graph_expose(self, widget, event):
context = self.graph_widget.window.cairo_create()
# set a clip region
context.rectangle(event.area.x, event.area.y,
event.area.width, event.area.height)
context.clip()
width, height = self.graph_widget.allocation.width, self.graph_widget.allocation.height
self.graph.draw_to_context(context, width, height)
self.graph.draw_to_context(context,
self.graph_widget.allocation.width,
self.graph_widget.allocation.height)
#Do not propagate the event
return False
def update(self):
log.debug("getstat keys: %s", self.graph.stat_info.keys())
d1 = client.stats.get_stats(self.graph.stat_info.keys())
d1 = client.stats.get_stats(self.graph.stat_info.keys(), self.selected_interval)
d1.addCallback(self.graph.set_stats)
d2 = client.stats.get_config()
d2.addCallback(self.graph.set_config)
dl = defer.DeferredList([d1, d2])
def _on_update(result):
width, height = self.graph_widget.allocation.width, self.graph_widget.allocation.height
rect = gtk.gdk.Rectangle(0, 0, width, height)
self.graph_widget.window.invalidate_rect(rect, True)
dl.addCallback(_on_update)
def _update_complete(result):
self.graph_widget.queue_draw()
d1.addCallback(_update_complete)
return True
def clear(self):
pass
def update_intervals(self):
client.stats.get_intervals().addCallback(self._on_intervals_changed)
def select_bandwidth_graph(self):
log.debug("Selecting bandwidth graph")
self.graph_widget = self.bandwidth_graph
self.graph = graph.Graph()
colors = self.colors['bandwidth_graph']
self.graph.add_stat('download_rate', label='Download Rate',
color=gtk_to_graph_color(colors['download_rate']))
self.graph.add_stat('upload_rate', label='Upload Rate',
color=gtk_to_graph_color(colors['upload_rate']))
self.graph.set_left_axis(formatter=fspeed, min=10240,
formatter_scale=graph.size_formatter_scale)
def select_connections_graph(self):
log.debug("Selecting connections graph")
self.graph_widget = self.connections_graph
g = graph.Graph()
self.graph = g
colors = self.colors['connections_graph']
g.add_stat('dht_nodes', color=gtk_to_graph_color(colors['dht_nodes']))
g.add_stat('dht_cache_nodes', color=gtk_to_graph_color(colors['dht_cache_nodes']))
g.add_stat('dht_torrents', color=gtk_to_graph_color(colors['dht_torrents']))
g.add_stat('num_connections', color=gtk_to_graph_color(colors['num_connections']))
g.set_left_axis(formatter=int_str, min=10)
def select_seeds_graph(self):
log.debug("Selecting connections graph")
self.graph_widget = self.seeds_graph
self.graph = graph.Graph()
colors = self.colors['seeds_graph']
self.graph.add_stat('num_peers', color=gtk_to_graph_color(colors['num_peers']))
self.graph.set_left_axis(formatter=int_str, min=10)
def set_colors(self, colors):
self.colors = colors
# Fake switch page to update the graph colors (HACKY)
self._on_notebook_switch_page(self.notebook,
None, #This is unused
self.notebook.get_current_page())
def _on_intervals_changed(self, intervals):
liststore = gtk.ListStore(int)
for inter in intervals:
liststore.append([inter])
self.intervals_combo.set_model(liststore)
try:
current = intervals.index(self.selected_interval)
except:
current = 0
#should select the value saved in config
self.intervals_combo.set_active(current)
def _on_selected_interval_changed(self, combobox):
model = combobox.get_model()
iter = combobox.get_active_iter()
self.selected_interval = model.get_value(iter, 0)
self.update()
return True
def _on_notebook_switch_page(self, notebook, page, page_num):
p = notebook.get_nth_page(page_num)
if p is self.bandwidth_graph:
self.select_bandwidth_graph()
self.update()
elif p is self.connections_graph:
self.select_connections_graph()
self.update()
elif p is self.seeds_graph:
self.select_seeds_graph()
self.update()
return True
class GtkUI(GtkPluginBase):
def enable(self):
log.debug("Stats plugin enable called")
self.config = deluge.configmanager.ConfigManager("stats.gtkui.conf", DEFAULT_CONF)
self.glade = XML(common.get_resource("config.glade"))
component.get("Preferences").add_page("Stats", self.glade.get_widget("prefs_box"))
component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs)
component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs)
self.on_show_prefs()
self.graphs_tab = GraphsTab(XML(common.get_resource("tabs.glade")))
self.graphs_tab = GraphsTab(XML(common.get_resource("tabs.glade")), self.config['colors'])
self.torrent_details = component.get('TorrentDetails')
self.torrent_details.add_tab(self.graphs_tab)
@ -139,15 +251,31 @@ class GtkUI(GtkPluginBase):
def on_apply_prefs(self):
log.debug("applying prefs for Stats")
config = {
"test":self.glade.get_widget("txt_test").get_text()
}
gtkconf = {}
for graph, colors in self.config['colors'].items():
gtkconf[graph] = {}
for value, color in colors.items():
try:
color_btn = self.glade.get_widget("%s_%s_color" % (graph, value))
gtkconf[graph][value] = str(color_btn.get_color())
except:
gtkconf[graph][value] = DEFAULT_CONF['colors'][graph][value]
self.config['colors'] = gtkconf
self.graphs_tab.set_colors(self.config['colors'])
config = { }
client.stats.set_config(config)
def on_show_prefs(self):
for graph, colors in self.config['colors'].items():
for value, color in colors.items():
try:
color_btn = self.glade.get_widget("%s_%s_color" % (graph, value))
color_btn.set_color(gtk.gdk.Color(color))
except:
log.debug("Unable to set %s %s %s" % (graph, value, color))
client.stats.get_config().addCallback(self.cb_get_config)
def cb_get_config(self, config):
"callback for on show_prefs"
self.glade.get_widget("txt_test").set_text(config["test"])
pass

View File

@ -0,0 +1,12 @@
$:render.header(_("Network Graph"), 'graph')
$:render.admin_toolbar('graph')
<div style="padding-left:20px">
<img src="$base/graph/network.png?height=300&width=1000"><br \>
<img src="$base/graph/connections.png?height=300&width=1000"><br \>
</div>
$:render.footer()

View File

@ -1,7 +1,11 @@
#
# webui.py
#
# Copyright (C) 2009 Damien Churchill <mvoncken@gmail.com>
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
#
# Deluge is free software.
#
@ -33,20 +37,18 @@
#
#
import logging
from deluge.log import LOG as log
from deluge.ui.client import client
from deluge import component
from deluge.plugins.pluginbase import WebPluginBase
from common import get_resource
log = logging.getLogger(__name__)
class WebUI(WebPluginBase):
scripts = [get_resource("stats.js")]
# The enable and disable methods are not strictly required on the WebUI
# The enable and disable methods are not scrictly required on the WebUI
# plugins. They are only here if you need to register images/stylesheets
# with the webserver.
def enable(self):

View File

@ -1,6 +1,6 @@
#
# setup.py
#
# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net>
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
#
# Basic plugin template created by:
@ -23,18 +23,7 @@
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
@ -48,13 +37,16 @@
from setuptools import setup, find_packages
__plugin_name__ = "Stats"
__author__ = "Martijn Voncken"
__author_email__ = "mvoncken@gmail.com"
__version__ = "0.1"
__author__ = "Ian Martin"
__author_email__ = "ianmartin@cantab.net"
__version__ = "0.3.2"
__url__ = "http://deluge-torrent.org"
__license__ = "GPLv3"
__description__ = ""
__long_description__ = """"""
__description__ = "Display stats graphs"
__long_description__ = """
Records lots of extra stats
and produces time series
graphs"""
__pkg_data__ = {"deluge.plugins."+__plugin_name__.lower(): ["template/*", "data/*"]}
setup(