Member of The Internet Defense League Últimos cambios
Últimos Cambios
Blog personal: El hilo del laberinto Geocaching

Optimización de rendimiento en la visualización de datos de "launchmany"

Última Actualización: 15 de enero de 2009 - Jueves

En la actualidad utilizo la aplicación "btlaunchmanycurses.py" de BitTornado, con buenos resultados (y unos cuantos parches propios, muchos de los cuales ya han sido documentados en esta web). No obstante hay cosas que mejorar.

Una de las cosas mejorables es que, incluso en ausencia completa de tráfico, el consumo de CPU es proporcional al número de torrents en servicio. ¿Por qué?. Tras investigar el asunto compruebo que el problema es la visualización de los torrents activos y sus estadísticas.

El programa "btlaunchmanycurses.py" de BitTornado visualiza la información de los torrents en servicio en la ventana del terminal, utilizando la librería "curses". Como es muy habitual tener más torrents en el sistema que la capacidad de la ventana, la información se va mostrando en un lento "scroll", recorriendo todos los torrents poco a poco.

El problema es que calcular las estadísticas de cada torrent es una tarea lenta (entre otros motivos, porque puede requerir acceso al sistema de persistencia), y es algo que se hace dos veces por segundo. Peor aún, recalculamos las estadísticas de todos los torrents del sistema, cuando en realidad sólo se van a visualizar unos pocos en pantalla, a medida que se va moviendo el "scroll".

El cambio obvio, pues, es retrasar el cálculo de las estadísticas todo lo posible, y solo realizarlo para los torrents que se están visualizando en pantalla en este momento.

Este enfoque tiene un problema, y es que el total de tráfico de subida y bajada se calcula sumando todos los tráficos de todos los torrents. Si no vamos a realizar el cálculo estadístico completo, no tendremos información sobre el tráfico total. ¿O si?.

Algo tenemos.

Con fines de limitar el tráfico total de subida (parámetro "max_upload_rate"), BitTornado lleva una contabilidad interna del tráfico total. No necesitamos realizar una suma torrent a torrent. ¡Bien!.

Lamentablemente, aunque BitTornado permite limitar el tráfico de descarga (parámetro "max_download_rate"), en realidad dicha limitación se realiza torrent a torrent. No existe un contador global. Ésto es, obviamente, un bug de BitTornado, heredado de su padre, el cliente BitTorrent oficial original. Se trata de un problema grave que "alguien" debería solucionar. No seré yo, porque soy una persona ocupada y el problema no me afecta (controlo el tráfico por QoS, no en la aplicación).

Una de las consecuencias de ello es que no tenemos un contador de tráfico global que podamos visualizar, sin calcular todas las estadísticas y sumarlas.

Pero siempre podemos crear uno propio. Creamos un nuevo contador global, y lo actualizamos cada vez que se actualiza cualquier contador de descarga de cualquier torrent. ¡Problema solucionado!.

Tal vez sea posible utilizar este contador "global" para limitar el tráfico de descarga de forma global, y no por torrent, pero los algoritmos en juego son complejos y delicados, y el bug no me afecta directamente. Queda como ejercicio para el lector :-).

El parche es el siguiente:

Index: BitTornado/BT1/Downloader.py
===================================================================
--- BitTornado/BT1/Downloader.py        (revision 11)
+++ BitTornado/BT1/Downloader.py        (revision 14)
@@ -13,6 +13,8 @@

 EXPIRE_TIME = 60 * 60

+DownloadMeasureGlobal = None
+
 class PerIPStats:
     def __init__(self, ip):
         self.numgood = 0
@@ -67,6 +69,10 @@
         self.ip = connection.get_ip()
         self.guard = BadDataGuard(self)

+        global DownloadMeasureGlobal
+        if DownloadMeasureGlobal == None :
+            DownloadMeasureGlobal = Measure(downloader.max_rate_period)
+
     def _backlog(self, just_unchoked):
         self.backlog = min(
             2+int(4*self.measure.get_rate()/self.downloader.chunksize),
@@ -158,6 +164,9 @@
         self.last2 = clock()
         self.measure.update_rate(length)
         self.downloader.measurefunc(length)
+
+        DownloadMeasureGlobal.update_rate(length)
+
         if not self.downloader.storage.piece_came_in(index, begin, piece, self.guard):
             self.downloader.piece_flunked(index)
             return False
Index: BitTornado/launchmanycore.py
===================================================================
--- BitTornado/launchmanycore.py        (revision 11)
+++ BitTornado/launchmanycore.py        (revision 14)
@@ -229,8 +229,15 @@

     def stats(self):
         self.rawserver.add_task(self.stats, self.stats_period)
+
+        from BT1.Downloader import DownloadMeasureGlobal
+        if DownloadMeasureGlobal == None :
+            download_rate = 0
+        else :
+            download_rate = DownloadMeasureGlobal.get_rate()
         data = []
-        for hash in self.torrent_list:
+
+        def request_stats(hash) :
             cache = self.torrent_cache[hash]
             if self.config['display_path']:
                 name = cache['path']
@@ -287,9 +294,11 @@
             else:
                 msg = ''

-            data.append(( name, status, progress, peers, seeds, seedsmsg, dist,
-                          uprate, dnrate, upamt, dnamt, size, t, msg ))
-        stop = self.Output.display(data)
+            return (name, status, progress, peers, seeds, seedsmsg, dist,
+                          uprate, dnrate, upamt, dnamt, size, t, msg)
+
+        stop = self.Output.display(self.ratelimiter.get_upload_rate(),
+                download_rate, request_stats, self.torrent_list)
         if stop:
             self.doneflag.set()

Index: BitTornado/RateLimiter.py
===================================================================
--- BitTornado/RateLimiter.py   (revision 11)
+++ BitTornado/RateLimiter.py   (revision 14)
@@ -46,6 +46,9 @@
         self.upload_rate = MAX_RATE * 1000
         self.slots = SLOTS_STARTING    # garbage if not automatic

+    def get_upload_rate(self) :
+        return self.measure.get_rate()
+
     def set_upload_rate(self, rate):
         # rate = -1 # test automatic
         if rate < 0:
Index: btlaunchmanycurses.py
===================================================================
--- btlaunchmanycurses.py       (revision 11)
+++ btlaunchmanycurses.py       (revision 14)
@@ -173,7 +173,7 @@
             self.disp_end = True
         return self.disp_end

-    def _display_data(self, data):
+    def _display_data(self, request_stats, data):
         if 3*len(data) <= self.mainwinh:
             self.scroll_pos = 0
             self.scrolling = False
@@ -197,7 +197,7 @@
                 if self._display_line(''):
                     break
             ( name, status, progress, peers, seeds, seedsmsg, dist,
-              uprate, dnrate, upamt, dnamt, size, t, msg ) = data[ii]
+              uprate, dnrate, upamt, dnamt, size, t, msg ) = request_stats(data[ii])
             t = fmttime(t)
             if t:
                 status = t
@@ -217,7 +217,7 @@
             self._display_line('    '+ljust(msg,self.mainwinw-4))
             i += 1

-    def display(self, data):
+    def display(self, totalup, totaldn, request_stats, data):
         if self.changeflag.isSet():
             return

@@ -227,17 +227,10 @@

         self.mainwin.erase()
         if data:
-            self._display_data(data)
+            self._display_data(request_stats, data)
         else:
             self.mainwin.addnstr( 1, int(self.mainwinw/2)-5,
                                   'no torrents', 12, curses.A_BOLD )
-        totalup = 0
-        totaldn = 0
-        for ( name, status, progress, peers, seeds, seedsmsg, dist,
-              uprate, dnrate, upamt, dnamt, size, t, msg ) in data:
-            totalup += uprate
-            totaldn += dnrate
-
         totalup = '%s/s' % fmtsize(totalup)
         totaldn = '%s/s' % fmtsize(totaldn)

Este parche sólo calcula las estadísticas de los torrents actualmente en pantalla. También crea un contador global de descarga, para poder acumular el tráfico de descarga de todos los torrents.

El parche no es muy limpio (por ejemplo, usa objetos globales), pero funciona y mi tiempo es limitado. Una interesante consecuencia de utilizar los contadores globales de tráfico, y no el tráfico sumado de todos los torrents es que los números pueden no coincidir exactamente. Esto es normal, dado el algoritmo usado, y no lo veo problemático.

Con este parche, el consumo de CPU cuando no hay tráfico se ha reducido al 20-30%. Es decir, la CPU consumida pasa a ser de entre un tercio y una quinta parte. Es una mejora muy importante, cuando tenemos muchos torrent en servicio pero poco tráfico en los mismos.

De todas formas siguen existiendo consumos de CPU proporcionales al número de torrents en servicio, aunque de menor importancia. Por ejemplo, periódicamente se revisa el directorio de torrents para añadir/quitar torrents en el cliente BitTornado. Esa operación es O(número de torrents), y solo habría que realizarla cuando realmente hay cambios en el directorio. Por ejemplo, utilizando tecnologías "notify" del sistema operativo, si están disponibles, o grabar los torrents en el sistema de persistencia junto a un "timestamp" de la última modificación al directorio "virtual" bajo persistencia.

Otro día. Tanto por hacer, y tan poco tiempo...


Historia

  • 15/ene/09: Primera versión de esta página.



Python Zope ©2009 jcea@jcea.es

Más información sobre los OpenBadges

Donación BitCoin: 19niBN42ac2pqDQFx6GJZxry2JQSFvwAfS