diff --git a/deluge/core/core.py b/deluge/core/core.py index 7142314fc..b46a57d18 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -42,6 +42,54 @@ from deluge.httpdownloader import download_file log = logging.getLogger(__name__) +OLD_SESSION_STATUS_KEYS = { + # 'active_requests': None, # In dht_stats_alert, if required. + 'allowed_upload_slots': 'ses.num_unchoke_slots', + # 'dht_global_nodes': None, + 'dht_node_cache': 'dht.dht_node_cache', + 'dht_nodes': 'dht.dht_nodes', + 'dht_torrents': 'dht.dht_torrents', + # 'dht_total_allocations': None, + 'down_bandwidth_bytes_queue': 'net.limiter_down_bytes', + 'down_bandwidth_queue': 'net.limiter_down_queue', + 'has_incoming_connections': 'net.has_incoming_connections', + 'num_peers': 'peer.num_peers_connected', + 'num_unchoked': 'peer.num_peers_up_unchoked', + # 'optimistic_unchoke_counter': None, # lt.settings_pack + 'total_dht_download': 'dht.dht_bytes_in', + 'total_dht_upload': 'dht.dht_bytes_out', + 'total_download': 'net.recv_bytes', + 'total_failed_bytes': 'net.recv_failed_bytes', + 'total_ip_overhead_download': 'net.recv_ip_overhead_bytes', + 'total_ip_overhead_upload': 'net.sent_ip_overhead_bytes', + 'total_payload_download': 'net.recv_payload_bytes', + 'total_payload_upload': 'net.sent_payload_bytes', + 'total_redundant_bytes': 'net.recv_redundant_bytes', + 'total_tracker_download': 'net.recv_tracker_bytes', + 'total_tracker_upload': 'net.sent_tracker_bytes', + 'total_upload': 'net.sent_bytes', + # 'unchoke_counter': None, # lt.settings_pack + 'up_bandwidth_bytes_queue': 'net.limiter_up_bytes', + 'up_bandwidth_queue': 'net.limiter_up_queue', + # 'utp_stats': None +} + +# TODO: replace with dynamic rate e.g. +# 'dht.dht_bytes_in'.replace('_bytes', '') + '_rate' +# would become 'dht.dht_in_rate' +SESSION_RATES_MAPPING = { + 'dht_download_rate': 'dht.dht_bytes_in', + 'dht_upload_rate': 'dht.dht_bytes_out', + 'ip_overhead_download_rate': 'net.recv_ip_overhead_bytes', + 'ip_overhead_upload_rate': 'net.sent_ip_overhead_bytes', + 'payload_download_rate': 'net.recv_payload_bytes', + 'payload_upload_rate': 'net.sent_payload_bytes', + 'tracker_download_rate': 'net.recv_tracker_bytes', + 'tracker_upload_rate': 'net.sent_tracker_bytes', + 'download_rate': 'net.recv_bytes', + 'upload_rate': 'net.sent_bytes', +} + class Core(component.Component): def __init__(self, listen_interface=None, read_only_config_keys=None): @@ -106,13 +154,29 @@ class Core(component.Component): # New release check information self.__new_release = None + # Session status timer + self.session_status = {} + self.session_status_timer_interval = 0.5 + self.session_status_timer = task.LoopingCall(self.session.post_session_stats) + self.alertmanager.register_handler('session_stats_alert', self._on_alert_session_stats) + self._session_rates = {(k_rate, k_bytes): 0 for k_rate, k_bytes in SESSION_RATES_MAPPING.items()} + self.session_rates_timer_interval = 2 + self.session_rates_timer = task.LoopingCall(self._update_session_rates) + def start(self): """Starts the core""" - pass + self.session_status_timer.start(self.session_status_timer_interval) + self.session_rates_timer.start(self.session_rates_timer_interval, now=False) def stop(self): log.debug('Core stopping...') + if self.session_status_timer.running: + self.session_status_timer.stop() + + if self.session_rates_timer.running: + self.session_rates_timer.stop() + # Save the libtorrent session state self.__save_session_state() @@ -187,6 +251,41 @@ class Core(component.Component): log.info('Successfully loaded %s: %s', filename, _filepath) self.session.load_state(state) + def _on_alert_session_stats(self, alert): + """The handler for libtorrent session stats alert""" + if not self.session_status: + # Empty dict on startup so needs populated with session rate keys and default value. + self.session_status.update({key: 0 for key in list(SESSION_RATES_MAPPING)}) + self.session_status.update(alert.values) + self._update_session_cache_hit_ratio() + + def _update_session_cache_hit_ratio(self): + """Calculates the cache read/write hit ratios and updates session_status""" + try: + self.session_status['write_hit_ratio'] = ((self.session_status['disk.num_blocks_written'] - + self.session_status['disk.num_write_ops']) / + self.session_status['disk.num_blocks_written']) + except ZeroDivisionError: + self.session_status['write_hit_ratio'] = 0.0 + + try: + self.session_status['read_hit_ratio'] = (self.session_status['disk.num_blocks_cache_hits'] / + self.session_status['disk.num_blocks_read']) + except ZeroDivisionError: + self.session_status['read_hit_ratio'] = 0.0 + + def _update_session_rates(self): + """Calculates status rates based on interval and value difference for session_status""" + if not self.session_status: + return + + for (rate_key, status_key), prev_bytes in list(self._session_rates.items()): + new_bytes = self.session_status[status_key] + byte_rate = (new_bytes - prev_bytes) / self.session_rates_timer_interval + self.session_status[rate_key] = byte_rate + # Store current value for next update. + self._session_rates[(rate_key, status_key)] = new_bytes + def get_new_release(self): log.debug('get_new_release') from urllib2 import urlopen, URLError @@ -381,8 +480,7 @@ class Core(component.Component): @export def get_session_status(self, keys): - """ - Gets the session status values for 'keys', these keys are taking + """Gets the session status values for 'keys', these keys are taking from libtorrent's session status. See: http://www.rasterbar.com/products/libtorrent/manual.html#status @@ -393,44 +491,26 @@ class Core(component.Component): :rtype: dict """ + + if not self.session_status: + return {key: 0 for key in keys} + + if not keys: + return self.session_status + status = {} - # TODO: libtorrent DEPRECATED for session_stats http://libtorrent.org/manual-ref.html#session-statistics - session_status = self.session.status() for key in keys: - status[key] = getattr(session_status, key) - + if key in OLD_SESSION_STATUS_KEYS: + new_key = OLD_SESSION_STATUS_KEYS[key] + log.warning('Using deprecated session status key %s, please use %s', key, new_key) + status[key] = self.session_status[new_key] + else: + try: + status[key] = self.session_status[key] + except KeyError: + log.warning('Session status key does not exist: %s', key) return status - @export - def get_cache_status(self): - """ - Returns a dictionary of the session's cache status. - - :returns: the cache status - :rtype: dict - - """ - # TODO: libtorrent DEPRECATED for session_stats: disk.num_blocks_cache_hits etc... - status = self.session.get_cache_status() - cache = {} - for attr in dir(status): - if attr.startswith('_'): - continue - cache[attr] = getattr(status, attr) - - # Add in a couple ratios - try: - cache['write_hit_ratio'] = (cache['blocks_written'] - cache['writes']) / cache['blocks_written'] - except ZeroDivisionError: - cache['write_hit_ratio'] = 0.0 - - try: - cache['read_hit_ratio'] = cache['blocks_read_hit'] / cache['blocks_read'] - except ZeroDivisionError: - cache['read_hit_ratio'] = 0.0 - - return cache - @export def force_reannounce(self, torrent_ids): log.debug('Forcing reannouncment to: %s', torrent_ids) diff --git a/deluge/plugins/Stats/deluge/plugins/stats/core.py b/deluge/plugins/Stats/deluge/plugins/stats/core.py index 94beade1a..08fe402ec 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/core.py +++ b/deluge/plugins/Stats/deluge/plugins/stats/core.py @@ -204,13 +204,8 @@ class Core(CorePluginBase): @export def get_session_totals(self): - status = self.core.session.status() - return { - 'total_upload': status.total_upload, - 'total_download': status.total_download, - 'total_payload_upload': status.total_payload_upload, - 'total_payload_download': status.total_payload_download - } + return self.core.get_session_status( + ['total_upload', 'total_download', 'total_payload_upload', 'total_payload_download']) @export def set_config(self, config): diff --git a/deluge/tests/test_core.py b/deluge/tests/test_core.py index 8e204db9b..97b5e5ab9 100644 --- a/deluge/tests/test_core.py +++ b/deluge/tests/test_core.py @@ -252,8 +252,8 @@ class CoreTestCase(BaseTestCase): self.assertEquals(type(status), dict) self.assertEquals(status['upload_rate'], 0.0) - def test_get_cache_status(self): - status = self.core.get_cache_status() + def test_get_session_status_ratio(self): + status = self.core.get_session_status(['write_hit_ratio', 'read_hit_ratio']) self.assertEquals(type(status), dict) self.assertEquals(status['write_hit_ratio'], 0.0) self.assertEquals(status['read_hit_ratio'], 0.0) diff --git a/deluge/ui/common.py b/deluge/ui/common.py index 7f33b7b20..56ac5dc23 100644 --- a/deluge/ui/common.py +++ b/deluge/ui/common.py @@ -120,6 +120,13 @@ DEFAULT_HOSTS = { 'hosts': [(sha(str(time.time())).hexdigest(), DEFAULT_HOST, DEFAULT_PORT, '', '')] } +# The keys from session statistics for cache status. +DISK_CACHE_KEYS = [ + 'disk.num_blocks_read', 'disk.num_blocks_written', 'disk.num_read_ops', 'disk.num_write_ops', + 'disk.num_blocks_cache_hits', 'read_hit_ratio', 'write_hit_ratio', 'disk.disk_blocks_in_use', + 'disk.read_cache_blocks' +] + class TorrentInfo(object): """ diff --git a/deluge/ui/console/cmdline/commands/cache.py b/deluge/ui/console/cmdline/commands/cache.py index abcce523c..8a8cc15c1 100644 --- a/deluge/ui/console/cmdline/commands/cache.py +++ b/deluge/ui/console/cmdline/commands/cache.py @@ -9,6 +9,7 @@ import deluge.component as component from deluge.ui.client import client +from deluge.ui.common import DISK_CACHE_KEYS from . import BaseCommand @@ -20,9 +21,7 @@ class Command(BaseCommand): self.console = component.get('ConsoleUI') def on_cache_status(status): - for key, value in status.items(): + for key, value in sorted(status.items()): self.console.write('{!info!}%s: {!input!}%s' % (key, value)) - d = client.core.get_cache_status() - d.addCallback(on_cache_status) - return d + return client.core.get_session_status(DISK_CACHE_KEYS).addCallback(on_cache_status) diff --git a/deluge/ui/console/modes/preferences/preference_panes.py b/deluge/ui/console/modes/preferences/preference_panes.py index 2f00ed284..6497a42c7 100644 --- a/deluge/ui/console/modes/preferences/preference_panes.py +++ b/deluge/ui/console/modes/preferences/preference_panes.py @@ -12,6 +12,7 @@ import logging from deluge.common import is_ip from deluge.decorators import overrides from deluge.ui.client import client +from deluge.ui.common import DISK_CACHE_KEYS from deluge.ui.console.widgets import BaseInputPane, BaseWindow from deluge.ui.console.widgets.fields import FloatSpinInput, TextInput from deluge.ui.console.widgets.popup import PopupsHandler @@ -412,29 +413,29 @@ class CachePane(BasePreferencePane): '%s:' % _('Cache Expiry (seconds)'), core_conf['cache_expiry'], min_val=1, max_val=32000) self.add_header(' %s' % _('Write'), space_above=True) - self.add_info_field('blocks_written', ' %s:' % _('Blocks Written'), status['blocks_written']) - self.add_info_field('writes', ' %s:' % _('Writes'), status['writes']) + self.add_info_field('blocks_written', ' %s:' % _('Blocks Written'), status['disk.num_blocks_written']) + self.add_info_field('writes', ' %s:' % _('Writes'), status['disk.num_write_ops']) self.add_info_field('write_hit_ratio', ' %s:' % _('Write Cache Hit Ratio'), '%.2f' % status['write_hit_ratio']) self.add_header(' %s' % _('Read')) self.add_info_field('blocks_read', - ' %s:' % _('Blocks Read'), status['blocks_read']) + ' %s:' % _('Blocks Read'), status['disk.num_blocks_read']) self.add_info_field('blocks_read_hit', - ' %s:' % _('Blocks Read hit'), status['blocks_read_hit']) + ' %s:' % _('Blocks Read hit'), status['disk.num_blocks_cache_hits']) self.add_info_field('reads', - ' %s:' % _('Reads'), status['reads']) + ' %s:' % _('Reads'), status['disk.num_read_ops']) self.add_info_field('read_hit_ratio', ' %s:' % _('Read Cache Hit Ratio'), '%.2f' % status['read_hit_ratio']) self.add_header(' %s' % _('Size')) self.add_info_field('cache_size_info', - ' %s:' % _('Cache Size'), status['cache_size']) + ' %s:' % _('Cache Size'), status['disk.disk_blocks_in_use']) self.add_info_field('read_cache_size', - ' %s:' % _('Read Cache Size'), status['read_cache_size']) + ' %s:' % _('Read Cache Size'), status['disk.read_cache_blocks']) @overrides(BasePreferencePane) def update(self, active): if active: - client.core.get_cache_status().addCallback(self.update_cache_status_fields) + client.core.get_session_status(DISK_CACHE_KEYS).addCallback(self.update_cache_status_fields) def update_cache_status_fields(self, status): if not self.created: diff --git a/deluge/ui/gtkui/glade/preferences_dialog.ui b/deluge/ui/gtkui/glade/preferences_dialog.ui index c8d732357..b88751521 100644 --- a/deluge/ui/gtkui/glade/preferences_dialog.ui +++ b/deluge/ui/gtkui/glade/preferences_dialog.ui @@ -4058,7 +4058,7 @@ the proxy instead of using the local DNS service - + True False 1 @@ -4070,7 +4070,7 @@ the proxy instead of using the local DNS service - + True False 1 @@ -4177,7 +4177,7 @@ the proxy instead of using the local DNS service - + True False 1 @@ -4189,7 +4189,7 @@ the proxy instead of using the local DNS service - + True False 1 @@ -4232,7 +4232,7 @@ the proxy instead of using the local DNS service - + True False @@ -4309,7 +4309,7 @@ the proxy instead of using the local DNS service - + True False 1 @@ -4321,7 +4321,7 @@ the proxy instead of using the local DNS service - + True False 1 diff --git a/deluge/ui/gtkui/preferences.py b/deluge/ui/gtkui/preferences.py index cdcc1716c..0d275d025 100644 --- a/deluge/ui/gtkui/preferences.py +++ b/deluge/ui/gtkui/preferences.py @@ -20,6 +20,7 @@ import deluge.component as component from deluge.configmanager import ConfigManager, get_config_dir from deluge.error import AuthManagerError, NotAuthorizedError from deluge.ui.client import client +from deluge.ui.common import DISK_CACHE_KEYS from deluge.ui.gtkui.common import associate_magnet_links, get_deluge_icon from deluge.ui.gtkui.dialogs import AccountDialog, ErrorDialog, InformationDialog, YesNoDialog from deluge.ui.gtkui.path_chooser import PathChooser @@ -292,9 +293,9 @@ class Preferences(component.Component): def _on_get_listen_port(port): self.active_port = port - client.core.get_cache_status().addCallback(_on_get_cache_status) + client.core.get_session_status(DISK_CACHE_KEYS).addCallback(_on_get_session_status) - def _on_get_cache_status(status): + def _on_get_session_status(status): self.cache_status = status self._show() @@ -719,12 +720,16 @@ class Preferences(component.Component): def __update_cache_status(self): # Updates the cache status labels with the info in the dict - for widget_name in ('label_cache_blocks_written', 'label_cache_writes', 'label_cache_write_hit_ratio', - 'label_cache_blocks_read', 'label_cache_blocks_read_hit', 'label_cache_read_hit_ratio', - 'label_cache_reads', 'label_cache_cache_size', 'label_cache_read_cache_size'): + cache_labels = ('label_cache_read_ops', 'label_cache_write_ops', + 'label_cache_num_blocks_read', 'label_cache_num_blocks_written', + 'label_cache_read_hit_ratio', 'label_cache_write_hit_ratio', + 'label_cache_num_blocks_cache_hits', 'label_cache_disk_blocks_in_use', + 'label_cache_read_cache_blocks') + + for widget_name in cache_labels: widget = self.builder.get_object(widget_name) - key = widget_name[len('label_cache_'):] - value = self.cache_status[key] + key = 'disk.' + widget_name[len('label_cache_'):] + value = self.cache_status.get(key, 0) if isinstance(value, float): value = '%.2f' % value else: @@ -733,11 +738,11 @@ class Preferences(component.Component): widget.set_text(value) def _on_button_cache_refresh_clicked(self, widget): - def on_get_cache_status(status): + def on_get_session_status(status): self.cache_status = status self.__update_cache_status() - client.core.get_cache_status().addCallback(on_get_cache_status) + client.core.get_session_status(DISK_CACHE_KEYS).addCallback(on_get_session_status) def on_pref_dialog_delete_event(self, widget, event): self.hide()