Análisis de rendimiento en Python

Análisis de rendimiento en Python

El análisis de rendimiento de código, comúnmente conocido como profiling, consiste en la caracterización del tiempo que un determinado programa emplea en cada una de sus funciones y métodos, y es una herramienta fundamental a la hora de estudiar y planificar la refactorización de código cuando es necesario mejorar el rendimiento de nuestras aplicaciones. Además, en el caso de Python, cuyo mayor (y probablemente único) punto débil es precisamente el rendimiento, cobra mayor importancia si cabe.

En este post explicaremos e ilustraremos algunas de las principales herramientas de profiling disponibles para Python, lo que nos servirá también para recordar ciertos aspectos teóricos y prácticos sobre complejidad computacional, lo que nunca viene mal ;). Todo el código está escrito para Python 2.7, pero su adaptación a Python 3 es trivial. Comencemos.

Tipos y estructuras de datos

En estos experimentos utilizaremos estructuras de datos lineales (listas y conjuntos, implementados por los tipos list y set), a los que añadiremos como tipo de dato tuplas de cadenas de caracteres (tipos tuple y str). Como dato básico utilizaremos tuplas de 100 cadenas de caracteres, cada una de las cuales estará formada por 10 letras minúsculas aleatorias. Estos datos se generarán a partir de la función random_tuple(), cuya definición mostramos a continuación:

from string import ascii_lowercase as low
from random import choice

def random_tuple():
    """
    Generates a random tuple of size 100 with strings of size 10 formed by
    only lowercase letters
    """
    return tuple(''.join(choice(low) for _ in xrange(10)) for _ in xrange(100))

Profiling básico: El módulo timeit

En ocasiones no resulta necesario disponer de complejas herramientas que analicen en detalle todo el flujo de control de la aplicación y midan el tiempo empleado en cada nivel de la jerarquía de llamadas a funciones y métodos. En estos casos, puede ser más que suficiente una simple comparativa o medición del tiempo que tarda en ejecutarse una determinada función. Para ello está especialmente diseñado el módulo timeit, que podemos emplear a través de su interfaz por línea de comandos, como una librería, o de la forma más cómoda y recomendable, como un magic de IPython.

It's Magic!

Para demostrar de forma simple el funcionamiento de timeit, comprobaremos experimentalmente la popular recomendación de que, si deseamos crear una lista en Python, siempre es preferible hacerlo a través de una list comprehension que mediante la adición de elementos de forma iterativa. Definimos por tanto dos funciones:

def slow_list_creation(n):
    lst = []
    for _ in xrange(n):
        lst.append(random_tuple())
    return lst

def fast_list_creation(n):
    return [random_tuple() for _ in xrange(n)]

Y obtenemos sus tiempos de ejecución para listas de tamaño 100. Para ello, en un terminal de IPython, es suficiente con ejecutar las siguientes sentencias:

In [1]: %timeit fast_list_creation(100)
10 loops, best of 3: 45.9 ms per loop

In [2]: %timeit slow_list_creation(100)
10 loops, best of 3: 45.2 ms per loop

Como vemos, %timeit se encarga automáticamente de seleccionar un número de veces adecuado para ejecutar la función y que la llamada no tarde demasiado, y a continuación imprime el mejor tiempo obtenido. En este caso, los resultados contradicen la hipótesis inicial, pues el tiempo empleado en la creación de la lista mediante adición es incluso inferior a la list comprehension, aunque la diferencia sea prácticamente inapreciable.

¿Qué ha pasado entonces? Pues una primera pista nos la puede dar la medición del tiempo empleado en generar los datos que introducimos en las listas:

In [3]: %timeit random_tuple()
1000 loops, best of 3: 449 µs per loop

¡Ya hemos identificado el problema! Si multiplicamos el tiempo de generación de cada tupla aleatoria por el número de elementos que generamos (100), prácticamente tenemos los 45ms que tarda la creación de cada una de las listas. Probamos entonces a redefinir las funciones de creación para evitar que la generación de los elementos nos introduzca tanto overhead.

def slow_list_creation(n):
    lst = []
    for _ in xrange(n):
        lst.append(tuple())
    return lst

def fast_list_creation(n):
    return [tuple() for _ in xrange(n)]

En este caso, simplemente añadimos tuplas vacías a nuestra lista. Repetimos el experimento:

In [5]: %timeit fast_list_creation(100)
100000 loops, best of 3: 7.9 µs per loop

In [4]: %timeit slow_list_creation(100)
100000 loops, best of 3: 11.7 µs per loop

Con estos nuevos resultados, vemos que efectivamente las list comprehension son aproximadamente un 35% más rápidas en la creación de listas que la clásica aproximación de añadir elementos iterativamente.

Ahora probamos a aumentar significativamente el tamaño de las listas creadas, introduciendo 10000 elementos:

In [5]: %timeit fast_list_creation(10000)
1000 loops, best of 3: 703 µs per loop

In [6]: %timeit slow_list_creation(10000)
1000 loops, best of 3: 1.09 ms per loop

Como vemos, en ambos casos el tiempo ha escalado linealmente, por lo que podemos concluir que, tal y como sería de esperar, la creación de listas en Python tiene un coste de orden O(n), y que las list comprehension tardan un 35% menos tiempo en generar una lista que el método iterativo clásico.

Profiling avanzado: El módulo cProfile

El módulo timeit sin duda es una herramienta muy útil a la hora de estudiar el comportamiento de nuestros programas, pero la información que puede proporcionarnos es en ocasiones muy limitada (como acabamos de ver, si el coste real se encuentra en una función de uso interno los resultados lo ocultarán, y no nos mostrarán lo que realmente pretendemos medir), y de hecho no puede considerarse una herramienta de profiling propiamente dicha. Para realizar un análisis detallado del rendimiento de una aplicación necesitamos algo más que una simple medida del tiempo de ejecución de funciones, y para ello disponemos del excelente módulo cProfile, incluido también en la librería estándar de Python.

Este módulo nos permite realizar un análisis pormenorizado del rendimiento dinámico de cualquier programa, analizando todo el grafo de llamadas a funciones y métodos, y mostrando el tiempo empleado en cada una de ellas, tanto en cada invocación individual como de forma acumulada a lo largo de toda la ejecución del programa.

La forma más cómoda de utilizar este módulo es a través de la línea de comandos, pasando como argumento el script que deseamos ejecutar. Como primer ejemplo, intentaremos repetir el primer experimento, comparando el rendimiento de crear una lista mediante list comprehension o añadiendo elementos de forma iterativa. El script a ejectuar es el siguiente, que nombraremos profiling_test.py:

from string import ascii_lowercase as low
from random import choice

def random_tuple():
    return tuple(''.join(choice(low) for _ in xrange(10)) for _ in xrange(100))

def slow_list_creation(n):
    lst = []
    for _ in xrange(n):
        lst.append(random_tuple())
    return lst

def fast_list_creation(n):
    return [random_tuple() for _ in xrange(n)]

for _ in xrange(100):
    slow_list_creation(100)
    fast_list_creation(100)

Algo importante a tener en cuenta es que, a diferencia de timeit, el módulo cProfile simplemente ejecuta una vez el script de entrada y mide el tiempo empleado en cada una de las funciones y métodos, sin intentar obtener de forma automática una medida estadística del tiempo empleado. Por lo tanto, hemos programado explícitamente en nuestro código la repetición de la tarea de creación de listas 100 veces. Ejecutamos el profiling con la siguiente orden:

:~$ python -m cProfile profiling_test.py 
         64050249 function calls in 14.232 seconds
   Ordered by: standard name
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    20000    0.204    0.000   18.747    0.001 profiling_test.py:12(random_tuple)
  2020000    0.906    0.000   18.543    0.000 profiling_test.py:17(<genexpr>)
      100    0.005    0.000    9.373    0.094 profiling_test.py:19(slow_list_creation)
      100    0.003    0.000    9.382    0.094 profiling_test.py:25(fast_list_creation)
        1    0.019    0.019   18.775   18.775 profiling_test.py:7(<module>)
        1    0.000    0.000    0.000    0.000 random.py:100(seed)
 20000000    8.434    0.000   10.395    0.000 random.py:271(choice)
        1    0.000    0.000    0.001    0.001 random.py:40(<module>)
        1    0.000    0.000    0.000    0.000 random.py:650(WichmannHill)
        1    0.000    0.000    0.000    0.000 random.py:72(Random)
        1    0.000    0.000    0.000    0.000 random.py:800(SystemRandom)
        1    0.000    0.000    0.000    0.000 random.py:91(__init__)
    10000    0.001    0.000    0.001    0.000 {method 'append' of 'list' objects}
  2000000    2.698    0.000   17.637    0.000 {method 'join' of 'str' objects}
 20000000    0.992    0.000    0.992    0.000 {method 'random' of '_random.Random' objects}

Ante estos resultados, podemos ver inmediatamente que prácticamente todo el tiempo de ejecución se concentra en la función random_tuple(), mientras que la diferencia entre las funciones slow_list_creation() y fast_list_creation() es casi indistinguible.

Estos resultados nos muestran un nivel de detalle muy superior al de la simple medición de tiempos de ejecución, pero su análisis todavía resulta tedioso, y es extremadamente complicado analizar el flujo de llamadas que se lleva a cabo en el programa. Para ayudarnos en esta tarea, utilizaremos una excelente herramienta compatible con cProfile, denominada RunSnakeRun. Se trata de un visualizador gráfico de resultados de cProfile, que hace infinitamente más sencillo el análisis de rendimiento de una aplicación.

Para demostrar el uso de RunSnakeRun, emplearemos un script ligeramente distinto, y que nos permitirá comprobar el rendimiento de distintos métodos de búsqueda en colecciones ordenadas, con diferentes órdenes de complejidad. El script utilizado a partir de ahora será el siguiente:

from string import ascii_lowercase as low
from random import choice
import bisect

def random_tuple():
    return tuple(''.join(choice(low) for _ in xrange(10)) for _ in xrange(100))

N = 5
STRL = sorted(random_tuple() for _ in xrange(N))
STRS = set(STRL)
istr = choice(STRL)

for _ in xrange(100000):
    STRL.index(istr)
    bisect.bisect_left(STRL, istr)
    STRS.__contains__(istr)

Este programa simplemente genera una lista de tuplas aleatorias de tamaño N (en este primer ejemplo N=5), crea un conjunto con los mismos elementos, y selecciona aleatoriamente una tupla de entre todas las generadas. A continuación, se realiza una búsqueda de la tupla seleccionada mediante tres métodos distintos:

  1. Búsqueda lineal en la lista, cuya complejidad es O(n). Esta búsqueda se realiza mediante el método index() del tipo list, que busca el índice de un determinado elemento recorriendo la lista por orden.
  2. Búsqueda binaria en la lista, con una complejidad O(log(n)). Para esta búsqueda utilizamos la función bisect_left() del módulo bisect.
  3. Búsqueda en el conjunto, con una complejidad O(1). Al tratarse de un tipo de dato no ordenado, simplemente comprobaremos si el elemento pertenece al conjunto, a través de la función __contains__().

Como todas son operaciones extremadamente rápidas, repetiremos las búsquedas 100000 veces para evitar que la generación del conjunto de datos eclipse el tiempo de cómputo que nos interesa medir.

Ejecutamos el script con cProfile almacenando la salida en un archivo de nombre prof_results, y a continuación abrimos este archivo con RunSnakeRun a través del comando runsnake.

:~$ python -m cProfile -o prof_results profiling_test.py
:~$ runsnake prof_results

Profiling con N=5

En la parte izquierda de la interfaz encontramos la misma información que se imprimía por consola al ejecutar directamente el script con cProfile, con la ventaja de que podemos reordenar la lista en función de distintos criterios como el número de llamadas, el tiempo de ejcución por cada llamada, el tiempo de ejecución acumulado, etc. En la parte derecha se muestra de forma muy intuitiva la jerarquía de llamadas de nuestro programa junto con la influencia de cada función en el tiempo total de ejecución. Si nos fijamos en este diagrama, vemos que sorprendentemente el método de búsqueda más lento es la búsqueda en un conjunto, cuya complejidad teórica es O(1), mientras que el método más rápido es la búsqueda lineal, que incluso funciona mejor que la búsqueda binaria.

Lo que intentamos ilustrar con este ejemplo es que a la hora de seleccionar estructuras de datos no debemos considerar solamente la complejidad teórica de cada una de las operaciones que vayamos a realizar, sino también el tamaño de la entrada que previsiblemente vamos a tener para cada una de ellas. El motivo de que la búsqueda en un conjunto sea tan lenta en este caso es que el tipo de dato que estamos utilizando (tuplas de 100 elementos) es relativamente complejo, y el cálculo de la función hash para determinar su presencia en el conjunto es costoso.

Repetimos el experimento, pero en este caso estableciendo la variable N=50.

Profiling con N=50

En este caso, vemos como el método más costoso ya es con diferencia la búsqueda lineal, mientras que también destaca el aumento proporcional del tiempo dedicado a la generación de la lista de datos. Sin embargo, en este ejemplo la búsqueda binaria sigue siendo más rápida que la búsqueda en el conjunto, que como era de esperar tarda exactamente lo mismo que en el caso anterior.

Por último, repetimos la prueba con N=1000.

Profiling con N=1000

Vemos que a partir de este tamaño de entrada el comportamiento ya es el teóricamente esperado. El método más rápido de búsqueda es la búsqueda en un conjunto, que sigue manteniendo su comportamiento constante, mientras que la práctica totalidad del tiempo de ejecución pasa a situarse en la búsqueda lineal y en la generación de los datos de entrada, que efectivamente escalan linealmente.

Y hasta aquí esta introducción sobre métodos y herramientas de profiling en Python. Cualquier duda o aportación que puedas hacernos en los comentarios por supuesto será más que bienvenida.

Spark sobre Elastic MapReduce

Amazon Elastic MapReduce (Amazon EMR) es un servicio web para la configuración y depliegue de un cluster basado en instancias de máquinas en el servicio Amazon Elastic Compute Cloud (Amazon EC2) y que es gestionado mediante Hadoop. También se puede ejecutar en Amazon EMR otros marcos de trabajo distribuídos como Spark, e interactuar con los datos en otros almacenes de datos como Amazon S3.

Para poder utilizar Spark sobre EMR es necesario seguir una serie de pasos adicionales al despliegue típico de un cluster EMR. En primer lugar es necesario realizar la instalación de Spark sobre el cluster mediante una acción de lanzamiento (bootstrap action). Además, es necesario modificar la forma en que se lanzan los trabajos, ya que deben ser lanzados sobre Spark y no sobre Hadoop.

Instalar Spark

Para instalar Spark se añade una nueva bootstrap action en el momento de configurar el cluster antes de su despliegue:
emr_bootstrap_new
donde el script a ejecutar es el siguiente (sin argumentos):

s3://support.elasticmapreduce/spark/install-spark

Este script ya lo proporciona el prop Amazon, por lo que es fiable su funcionalidad sobre EMR. Solo añadiendo esta acción ya estará Spark plenamente funcional en el cluster EMR desplegado.

Ejecutar un trabajo

Por defecto, los steps de EMR son ejecutados mediante hadoop. Para evitar esto, amazon proporciona un programa en java para ejecutar scripts fuera de hadoop. De esta forma, para ejecutar un trabajo sobre Spark, se debe añadir un step a modo de Custom Jar:
emr_steps_new
donde las opciones son las siguientes (sustituir los argumentos entre <> por sus valores reales):

  • Step type: Custom JAR
  • JAR Location:

    s3://<CLUSTER_REGION>.elasticmapreduce/libs/script-runner/script-runner.jar
  • Arguments:

    /home/hadoop/spark/bin/spark-submit --deploy-mode cluster --master yarn-cluster --class <MAIN_CLASS> s3://<BUCKET>/<FILE_JAR> <JAR_OPTIONS>

El significado de los argumentos es:

  • /home/hadoop/spark/bin/spark-submit — es el script de ejecución de trabajos sobre spark.
  • –deploy-mode cluster indica el despliegue de spark en modo cluster, aprovechando todos los nodos configurados en hadoop.
  • –master yarn-cluster lanza spark sobre Apache Hadoop NextGen MapReduce.
  • –class <MAIN_CLASS> indica cual es la clase main del programa java. Esto es necesario, ya que un jar almacenado en S3 no hace disponible conocer su clase main.
  • s3://<BUCKET>/<FILE_JAR> es la localización del programa java que realizará el trabajo.
  • <JAR_OPTIONS> son los argumentos necesarios del programa java.

La ejecución del trabajo generará unos logs en el directorio S3 configurado a la hora de desplegar el cluster, conteniendo la salida estandar y la salida estandar de error.

Java 8

La última versión AMI disponible (3.7.0) contiene como versión java 7. Para aquellos que usan java 8 (algo común en este tipo de entorno, por el uso de funciones lambda), pueden instalarlo en el cluster mediante un script ejecutado como bootstrap action. Para ello, solo es necesario almacenar en el contenedor S3 el siguiente script para luego añadirlo como bootstrap action a la hora de lanzar un nuevo cluster.

Referencias

Compilar Hadoop para 64 bits

A estas aĺturas creo que todos sabemos lo que es Hadoop: el framework principal para el almacenamiento y procesamiento del Big Data en clusters de commodity hardware. Su versión 2 incorpora YARN (Yet Another Resource Negociator), que extiende las funcionalidades de Hadoop permitiéndolo ir más allá de la ejecución de trabajos MapReduce. YARN actúa como planificador del cluster gestionando la ejecución cualquier tipo de aplicaciones (MapReduce, MPI, servidores, etc.) y proporcionando las ventajas de Hadoop: distribución de carga, minimización del tráfico de red (la aplicación va a los datos, no al revés), replicación de datos, tolerancia a fallos,  etc.

Hacer una instalación básica de Hadoop es muy simple. Hadoop es una aplicación Java, y basta con descargar la release que queramos de http://hadoop.apache.org/releases.html (en estos momentos, la última es la 2.4.0) en formato tar.gz, descomprimirla, y empezar a trabajar. Alternativamente, se pueden obtener paquetes que incluyen Hadoop y otras aplicaciones de su ecosistema, con todo instalado como las que ofrecen Cloudera (última versión CDH5), Hortonworks (última versión HDP 2.1) o MapR (última versión MapR 4.0).  También aplicaciones como Apache Ambari permiten desplegar un cluster Hadoop en cuestión de minutos (incluyendo herramientas como Nagios y Ganglia para monitorizarlo).

En cualquier casi, si queremos un mayor control sobre el software instalado, lo mejor es descargar la release desde hadoop.apache.org y seguir los pasos indicados en la documentación (http://hadoop.apache.org/docs/r2.4.0/ para la última versión), ya sea para instalar un único nodo de prueba o un cluster completo.

Si lo hacemos así, un problema que nos puede surgir es que, al probar nuestra reluciente instalación nos encontremos con el siguiente warning:

$ hadoop fs -ls /
OpenJDK 64-Bit Server VM warning: You have loaded library /opt/yarn/hadoop-2.4.0/lib/native/libhadoop.so.1.0.0-32b which might have disabled stack guard. The VM will try to fix the stack guard now.
It’s highly recommended that you fix the library with ‘execstack -c <libfile>’, or link it with ‘-z noexecstack’.
14/06/02 18:32:18 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform… using builtin-java classes where applicable

¿Es preocupante este warning? ¿a qué se debe? En principio el warning no impide que todo funcione sin problemas, pero puede suponer una perdida de rendimiento importante. Se debe a que, a pesar de ser una aplicación Java, algunas funcionalidades de Hadoop tienen implementaciones nativas por razones de rendimiento (ver http://hadoop.apache.org/docs/r2.4.0/hadoop-project-dist/hadoop-common/NativeLibraries.html). Una de las funcionalidades afectadas es el manejo de ficheros comprimidos, de gran importancia cuando trabajamos con datos realmente Big (ver esta presentación para un análisis de los mecanismos de compresión usados en Hadoop).

Estas funcionalidades nativas se encuentran disponibles en una única librería, libhadoop.so, localizada en $HADOOP_HOME/lib/native. El warning anterior es debido a que en la release de Hadoop que descargamos desde la página oficial, esta librería está compilada para 32 bits, por lo que no funciona en nuestro sistema de 64 bits. La solución pasa por obtener una versión en 64 bits de esa librería, o compilarla en nuestro sistema.

Lo que vamos a hacer, entonces, es compilar Hadoop desde los ficheros fuente en un sistema CentOS 6.5 de 64 bits.  Es un proceso relativamente simple, pero hay que tener en cuenta las dependencias. También necesitamos bastante espacio de disco (el código fuente compilado ocupa más de 5GB) y tiempo, pues lleva un rato largo.

Pasos:

1. Obtener el código fuente de http://hadoop.apache.org/releases.html (la última versión a 5 de junio es la 2.4.0, fichero hadoop-2.4.0-src.tar.gz) y descomprimirlo (tar xvzf hadoop-2.4.0-src.tar.gz).

2. Instalar el software necesario para la compilación. Necesitamos:

a. JDK 1.6+. Sirve el OpenJDK. Instalamos con yum la versión 1.7

$ su # recordad que sudo es para cobardes!
# yum -y install java-1.7.0-openjdk-devel

b. Compilador de C, C++ para compilar el código nativo (en caso de que no las tengamos ya instaladas) y librerías adicionales (no indicadas en la documentación pero necesarias).

# yum -y install gcc gcc-c++ gawk
# yum -y install zlib-devel openssl-devel

c. CMake 2.6+. Se pueden instalar directamente con yum la versión 2.6.4

# yum -y install cmake

d. Maven 3.0+, para compilar el proyecto. No está disponible como paquete en los repositorios principales, pero es facilmente instalable:

# cd /usr/local/
# wget http://ftp.cixug.es/apache/maven/maven-3/3.2.1/binaries/apache-maven-3.2.1-bin.tar.gz
# tar xvzf apache-maven-3.2.1-bin.tar.gz
# ln -s apache-maven-3.2.1 apache-maven
# cd bin
# ln -s ../apache-maven/bin/mvn
# echo ‘export M2_HOME=/usr/local/apache-maven’ > /etc/profile.d/apache-maven.sh
# echo ‘export M2=$M2_HOME/bin’ >> /etc/profile.d/apache-maven.sh
# echo ‘setenv M2_HOME /usr/local/apache-maven’ > /etc/profile.d/apache-maven.csh
# echo ‘setenv M2 $M2_HOME/bin’ >> /etc/profile.d/apache-maven.csh
# exit
$ . /etc/profile.d/apache-maven.sh # si usamos bash
$ mvn -version #probamos que funciona

e. ProtocolBuffer 2.5.0. Protocol Buffers (o protobuf) es un formato de serialización de datos de Google, similar a Apache Thrift o Apache Avro. Google usa protobuf en casi todos sus protocolos RPC internos y formatos de fichero. Tenemos que compilarlo desde las fuentes:

$ cd; wget https://protobuf.googlecode.com/files/protobuf-2.5.0.tar.bz2
$ tar xjf protobuf-2.5.0.tar.bz2; cd protobuf-2.5.0
$ ./configure
$ make
$ su -c ‘make install’

f. Snappy (opcional). Snappy es una librería de compresión/decompresión rápida de Google. Su instalación es opcional, pero por completitud es interesante tenerla. La versión disponible en los repositorios nos sirve

# yum -y install snappy-devel

g. Findbugs 1.3.9 (opcional?), un programa para encontrar bugs en Java. En teoría, su instalación es opcional, pero a mi no me funcionó la compilación de Hadoop sin ese programa. Podemos bajar el código ya compilado (Java).

$ cd /usr/local; su
# wget http://prdownloads.sourceforge.net/findbugs/findbugs-2.0.3.tar.gz
# tar xzf findbugs-2.0.3.tar.gz
# echo ‘export FINDBUGS_HOME=/usr/local/findbugs-2.0.3’ > /etc/profile.d/findbugs.sh
# echo ‘setenv FINDBUGS_HOME /usr/local/findbugs-2.0.3’ > /etc/profile.d/findbugs.csh
# exit
$ cd; . /etc/profile.d/findbugs.sh

4. Ya podemos pasar a crear la distribución de Hadoop para nustro sistema (saltándonos los tests, que llevan mucho tiempo, e incluyendo la documentación). En cualquier caso, la lleva su buen rato (unos 40 minutos).

$ cd; cd hadoop-2.4.0-src
$ mvn package -Pdist,native,docs -DskipTests -Dtar -Drequire.snappy

Una vez terminado, en el directorio hadoop-dist/target tendremos el fichero hadoop-2.4.0.tar.gz con las librerías nativas compiladas para 64 bits y también la documentación en hadoop-dist-2.4.0-javadoc.jar.

Gestión de dependencias de terceros con Maven

Uno de los problemas recurrentes al pasar de una herramienta de desarrollo para Java como Ant a Maven es la de cómo lidiar con librerías de terceros (3rd party libs) que no se encuentran en ningún repositorio Maven. Para los que no estéis familiarizados, Maven es una alternativa a Ant como herramienta de automatización de desarrollo que se configura mediante la definición de una serie de archivos POM (Project Object Model) en XML que le explican a Maven cómo está el proyecto estructurado, qué dependencias tiene, datos sobre la organización, etc. Una de las características más potente es que, a diferencia de Ant, Maven es capaz de resolver las dependencias del proyecto analizando el POM y descargarlas automáticamente de repositorios especiales de dependencias (artifact repositories).

En general existen tres tipos distintos de repositorios:

  • Repositorio local. Es el repositorio Maven instalado en la máquina de desarrollo y ubicado por defecto en el directorio “.m2/” del home. El repositorio local se sincroniza con uno o varios repositorios remotos.
  • Repositorio remoto interno. Servidor web Maven privado usado generalmente a nivel interno de organización.
  • Repositorio central. Es el repositorio remoto público usado por defecto para buscar las dependencias que no se encuentran en el repositorio local. Por defecto se usa Maven Central (http://search.maven.org/). Por ejemplo, si buscamos “Jetty” nos sale toda la información sobre la librería publicada en el repositorio.

Cuando un proyecto necesita una dependencia, Maven se encarga de buscarla en el repositorio central, bajarla e instalarla en el repositorio local del equipo de desarrollo. Sin embargo, el problema viene cuando estas dependencias no se encuentran en ningún repositorio, bien porque no se han publicado, o porque son librerías propias que no tiene sentido publicar en Maven Central. Para estos casos, Maven cuenta con dos métodos para resolver el problema:

  1. Crear un repositorio remoto (como Maven Central), donde se ubicarán las dependencias.
  2. Instalar las librerías manualmente en el repositorio local usando Maven.

La primera solución es más complicada pero la más útil si contamos con muchas dependencias de distintos proyectos. Este sería el típico escenario de una organización que cuenta con múltiples proyectos y librerías propias no publicadas en Maven Central. La segunda solución es más sencilla y la recomendada para aquellos casos en que solo necesitemos alguna librería puntualmente.

A continuación veremos tanto el método 1 como el 2 en base a un pequeño ejemplo que consistirá en crear un proyecto que use una dependencia no publicada en el repositorio central de Maven.

1) Cómo instalar un repositorio remoto interno de Maven y publicar dependencias.

Un repositorio Maven es básicamente un servidor web (por ejemplo tomcat o jetty) que aloja un conjunto de librerías y que se consulta a la hora de resolver dependencias. Existen distintas herramientas para la creación de servidores Maven. Las más conocidas son Artifactory y Nexus.

Para este ejemplo, vamos a levantar un servidor Nexus OSS 2.7.0 (el ofrecido por los creadores de Maven) en la misma máquina de desarrollo. Para ello hay que descargar la versión Nexus OSS en http://www.sonatype.org/nexus/go y seguir unos pasos muy sencillos:

  1. Una vez descargado el zip o el tar, descomprimir donde tengamos permisos de lectura/escritura/ejecución. Si usamos algún sistema tipo Unix (Linux, MacOSX, Solaris…) podemos descomprimirlo con:
    1. unzip nexus-2.7.0-04-bundle.zip (para el zip)
    2. tar xvzf nexus-2.7.0-04-bundle.tar.gz (para el tar.gz)
  2. Dentro del directorio descomprimido nexus-2.7.0-04/bin/ simplemente lanzamos el servidor mediante “./nexus start”. Si usamos Windows hay un script bat (nexus.bat) para lanzarlo. El servidor se despliega por defecto en localhost:8081/nexus.
  3. Una vez iniciado, comprobamos que podemos acceder al servidor en http://localhost:8081/nexus. Ten en cuenta que desde que se lanza hasta que la web es accesible puede llevar por lo menos 30 segundos. Si no está accesible inmediatamente inténtalo unos segundos después.
  4. Si la web carga correctamente, ya tenemos el servidor interno preparado. Puedes comprobar el estado de nexus con “./nexus status”. En caso de tener cualquier problema en este punto o necesitar una configuración avanzada del servidor, hay una guía completa aquí: http://books.sonatype.com/nexus-book/reference/install.html

Una vez tenemos Nexus levantado, vamos a publicar una dependencia y a usarla desde un proyecto. Para el ejemplo he creado una pequeña librería con Maven y la he subido en github. Esta librería será la dependencia externa que necesitamos en nuestro proyecto pero que desgraciadamente no está en Maven Central. El jar compilado se puede descargar del repositorio Nexus del CITIUS

Pantallazo del Nexus con los datos para hacer login

Una vez descargado, procedemos a publicarlo en nuestro repositorio. Para ello vamos a la página principal de Nexus http://localhost:8081/nexus (que debemos tener iniciado) y seguimos los siguientes pasos:

  1. En la esquina superior derecha, hacemos click en login (captura) y metemos los datos de administrador por defecto (usuario “admin”, password “admin123”).
  2. Una vez hecho login, vamos al apartado “repositories” en el panel lateral izquierdo. Veremos que aparece una lista de repositorios disponibles, entre ellos uno llamado 3rd party, que es el que nos interesa para este caso. Hacemos click en éste, y en el panel inferior vamos a la pestaña “Upload artifact”.
  3. Ahora metemos la información que se usará para localizar la dependencia. Hay dos modos de hacerlo: manual (Gav definition: Gav Parameters) o automática usando el pom (Gav definition: from POM). Lo habitual en dependencias de terceros es no contar con el POM, así que vamos a introducirlos manualmente seleccionando Gav Parameters (para este caso tenemos el POM de la dependencia https://github.com/pablormier/parallel-loops/blob/master/pom.xml en caso de preferir el método automático).
  4. A continuación rellenamos el formulario con los siguientes datos (nota: estos datos de no disponerlos podemos inventarlos, pero han de ser únicos, no puede haber 2 dependencias distintas con los mismos datos):

    Group: es.usc.citius.common
    Artifact: parallel-loops
    Version: 1.0
    Packaging: jar

  5. Finalmente, sólo nos queda seleccionar el jar en el formulario de abajo, pulsando en “Select Artifact(s) to upload”. Seleccionamos el jar y pulsamos el botón “add artifact”, y finalmente “Upload Artifact(s)”. Una vez hecho esto ya tenemos la dependencia subida a nexus y lista para ser usada desde otros proyectos.

Ya sólo nos queda configurar el POM de nuestro proyecto indicándole a Maven dónde está el repositorio que queremos usar para que busque las dependencias. El snippet en el caso del repositorio nexus de pruebas del CITIUS es el siguiente:

<repositories>
<repository>
<id>citius-thirdparty</id>
<url>http://tec.citius.usc.es/nexus/content/repositories/thirdparty</url>
</repository>
</repositories>

Para añadir la dependencia pegamos el siguiente snippet dentro de la sección <dependencies> del POM:

<dependency>
<groupId>es.usc.citius.common</groupId>
<artifactId>parallel-loops</artifactId>
<version>1.0</version>
</dependency>

Aquí tienes un ejemplo de cómo debería quedar el POM configurado: https://github.com/pablormier/maven-3rd-party-example/blob/master/pom.xml

Ya solo queda probar que todo funciona creando un nuevo proyecto Maven que use esta dependencia. Para el tutorial he creado uno de ejemplo que hace un uso sencillo de la dependencia. Dicho proyecto se puede bajar de https://github.com/pablormier/maven-3rd-party-example (lo interesante es ver cómo está definido el POM.xml para usar el repositorio nexus y la dependencia). Para probarlo, simplemente lanzar el comando maven “mvn test”.

2) Instalar dependencia localmente con Maven

Este método es mucho más sencillo que el anterior y el preferido cuando no necesitamos un servidor de dependencias (que es el caso de la mayoría de proyectos personales). Para poder instalar cualquier jar en nuestro repositorio local Maven, simplemente hay que ejecutar el comando:

mvn install:install-file -Dfile= -DgroupId= -DartifactId=[artifact-id] -Dversion=[version] -Dpackaging=[packaging]

Para instalar la dependencia parallel-loops del ejemplo, nos la descargamos y ejecutamos:

mvn install:install-file -Dfile=/ruta/al/archivo/parallel-loops.jar -DgroupId=es.usc.citius.common -DartifactId=parallel-loops -Dversion=1.0 -Dpackaging=jar

Con este comando, Maven lo que hace es copiar la dependencia en el repositorio local (generalmente localizado en el directorio ~/.m2/) con los metadatos necesarios. Podemos comprobar que efectivamente se ha instalado correctamente viendo si existe el directorio “home/usuario/.m2/repository/es/usc/citius/common/parallel-loops/1.0/” y que contiene la librería parallel-loops-1.0.jar.

Una vez instalado podemos usar el mismo proyecto de ejemplo usado para el caso 1 pero modificando el POM para que no se busque la dependencia en el repositorio Nexus remoto y se haga en el local. Para ello simplemente borramos del POM del proyecto el repositorio del CITIUS para obligar a que sean buscadas en el local:

<!– Ruta a nuestro repositorio Nexus –>
<repositories>
<repository>
<id>citius-nexus</id>
<url>http://tec.citius.usc.es/nexus/content/repositories/thirdparty</url>
</repository>
</repositories>

y probamos que todo funciona lanzando “mvn test” en el directorio de nuestro proyecto.

Referencias:

http://www.javaworld.com/javaworld/jw-05-2006/jw-0529-maven.html
http://www.theserverside.com/news/1364121/Setting-Up-a-Maven-Repository
http://maven.apache.org/guides/introduction/introduction-to-repositories.html
http://stackoverflow.com/questions/364775/should-we-use-nexus-or-artifactory-for-a-maven-repo
http://books.sonatype.com/nexus-book/reference/install-sect-as-a-war.html
http://en.wikipedia.org/wiki/Apache_Maven
http://www.theserverside.com/news/1364121/Setting-Up-a-Maven-Repository
http://books.sonatype.com/nexus-book/reference/install-sect-service.html
https://support.sonatype.com/entries/21283268-Configure-Maven-to-Deploy-to-Nexus
http://maven.apache.org/guides/mini/guide-3rd-party-jars-local.html

Disponible GitHub para el CiTIUS

Git es un sistema distribuído de control de versiones gratuito y open source diseñado para trabajar tanto con pequeños proyectos como con grandes y complejos desarrollos de manera rápida y eficiente. Las posibilidades que ofrece Git son inmensas y eso se refleja en la cantidad de atención que obtiene con respecto a otros sistemas.

collabocats

Gran parte de la culpa de su éxito lo tiene GitHub, el servicio de alojamiento de repositorios de software por excelencia (actualmente). En él están alojados proyectos de software libre tan importante como jQuery, reddit, Sparkle, curl, Ruby on Rails, node.js… Además, algunas de las grandes empresas de Internet, como Facebook, también lo utilizan para publicar sus proyectos públicos como librerías o SDK’s.

Desde hoy, el CiTIUS hace disponible una página propia dentro de GitHub para aquellos que quieran publicar allí sus repositorios. El único requisito es que el proyecto debe ser de acceso público. Para aquellos que no quieran que su repositorio sea de acceso público, GitHub ofrece cuentas académicas validables con el correo de la USC. Con esta modalidad es posible disponer de hasta 5 repositorios privados.

Además de alojar repositorios Git, GitHub también tiene unas características añadidas muy interesantes:

  • Seguimiento de tareas integrado más orientado al seguimiento de errores o incidencias. Se integra perfectamente con los comentarios asociados a los commits del código.
  • Revisión colaborativa del código que facilita la inclusión de comentarios en el propio código, para indicar errores o señalar que se ha cambiado para arreglar un problema.
  • Wiki sencilla complementaria al código.
  • GitHub Pages para publicar los proyectos de GitHub de forma agradable y vistosa.

Finalmente, os dejo algunos enlaces útiles para los que estéis interesados:

Pequeño llamamiento a la Refactorización

Este post estará centrado en dar a conocer brevemente las ventajas y necesidad de realizar refactorización de código en proyectos de software en general, pero particularmente en software desarrollado para investigación.

En breves palabras, la refactorización consiste en la modificación de la estructura interna de un programa sin cambiar su comportamiento. El objetivo principal de estos cambios estructurales es hacer el código más fácil de comprender y modificar, en contraposición con optimizar el código, lo cual muchas veces lleva a que el código sea ininteligible (aunque algunas veces claridad y optimización van de la mano, pero eso da para otro post). Es por este motivo por el que muchas veces a este tipo de técnicas se les denominan mantenimiento preventivo, ya que ayudan a que los futuros cambios se hagan de un modo más sencillo y mejor.

Creo que es posible comprender fácilmente la utilidad de esta técnica en el mundo de la investigación con un sencillo ejemplo. Todos hemos vivido épocas de mucha presión por la cercanía de algún tipo de deadline. El congreso es el mejor escaparate para nuestro trabajo y hay que enviar algo como sea. El día se acerca, los resultados no son tan prometedores como se esperaban y las ideas de última hora surgen sin mesura. El código tan bonito al que le teníamos cariño, en menos de 1 semana se ha convertido en un monstruo con métodos de más de 1000 líneas que hacen doscientas cosas diferentes y que falla de vez en cuando sin razón alguna. Gracias al incansable trabajo de todo el equipo de investigación, todos acaban felices y comiendo perdices, el artículo se ha enviado con resultados favorables. Todo ha salido bien.

O no. Después de pasado el deadline, es posible que el encargado del código haya decidido avanzar en otros temas atrasados, o darse un día de descanso después de las horas extra anteriores. Pero tarde o temprano hay que seguir avanzado con ese proyecto. Y cuando ya se ha realizado una hipótesis de por donde seguir, hay que incorporarlo al código antiguo. El horror nos atraviesa como una descarga eléctrica por la espalda. Nuestro monstruoso código no se deja coger por ningún lado. Podría intentarse hacer una capa de abstracción o continuar con el propio código, pero para aquellos atrevidos les animo que hablen de su experiencia con cualquiera que haya trabajado con software heredado. Otra alternativa (en mi opinión, mejor) es la refactorización.

No es poner el código bonito, no es adornarlo para que cuando otros lo vean piensen en lo buenos programadores que somos. Simplemente es hacer el código lo más fácilmente adaptable a futuros requisitos/cambios. Es tenerlo preparado para la siguiente tormenta. Y en mi experiencia se consigue ahorrar muchísimo tiempo (hasta semanas) con solo dedicarle un tiempo exclusivamente a la estructura del código.

Así que, para terminar, un par de consejos. Doctorandos (o ya doctores que les guste programar), haced refactorización y estad orgullosos de ello, animad a los demás e informad a vuestros directores. Y para los directores, sed conscientes de lo favorable que será en un futuro gastar tiempo en este tipo de prácticas y favoreced que así se pueda hacer. El único proposito es que todos salgamos ganando.

Enlaces para más información:

Introducción a Scrum en menos de 10 minutos

Os dejo un vídeo muy interesante que introduce las ideas básicas de Scrum. Está elaborado por la empresa Axosoft, creadora del software de gestión ágil OnTime. El vídeo contiene un poco de publicadad al final (como es normal), pero la idea que intenta transmitir es bastante buena y sencilla, para aquellos que quieran empezar a aprender un poco del desarrollo Ágil y en concreto de Scrum.

 

Nova plantilla de presentacións beamer do CiTIUS

Xa está dispoñible na wiki do centro a nova plantilla de presentacións beamer do CiTIUS, e que sustitue á antiga plantilla asociada co vello logotipo. De xeito oficioso, recoméndase a todos os usuarios de beamer que empreguen esta nova plantilla nas súas presentacións relacionadas co CiTIUS, xa que está deseñada de acordo coa nova imaxe corporativa do centro.

Como sabedes, beamer é un tipo de documento LaTeX que permite xerar PDF optimizados para a súa visualización en proxectores. Obviamente, este tipo de presentacións teñen todas as vantaxes (e, dependendo do punto de vista, tamén algunha desvantaxe) dos documentos LaTeX. En calquera caso, un coñecemento básico/medio de LaTeX é máis que suficiente para comezar a empregar esta plantilla de beamer. Na propia wiki do centro hai un exemplo, tanto o código fonte como o PDF resultante, no que se amosan algunhas funcionalidades e características tanto de beamer como da propia plantilla. Máis información sobre beamer na Guía de usuario de beamer.

Se queredes migrar as vosas presentacións beamer feitas coa antiga plantilla, tede en conta o seguinte:

  • Os paquetes babel, inputenc e fontenc xa non están no ficheiro de definicións, polo que se deben poñer explícitamente, e coas opcións axeitadas, no documento TEX principal.
  • Xa non existe o comando \finishslides, xa que agora é un entorno, como se pode comprobar na propia plantilla.
  • Se utilizades unha tabla de contidos para as seccións da vosa presentación, é preciso definir un novo comando na cabeceira do documento TEX principal:
    \def\cabeceiraTOC{__Cabeceira da tabla de contidos}

Por último, salientar que o nome dos autores non están en letra bold (negrita), xa que se reserva este tipo de letra para destacar algún autor sobre os demáis (por exemplo, na presentación dunha contribución de varios autores). En calquera caso, para favorecer as cualidades visuais da presentación, recoméndase empregar a letra negrita (\textbf{}) no autor ou autores principais do documento.

Para máis información, por favor poñédevos en contacto coa sección LaTeX da Unidade de Xestión de Infraestructuras TIC (ext. 16411).

Cosas que todo programador debería saber

En este post se recopila una breve lista de “principios y recetas” sobre buenas prácticas en el mundo del desarrollo de software para las que hay un consenso bastante generalizado en el sector.

Esta lista no pretende ser completa ni exhaustiva sino generar debate entorno a los diferentes puntos para que entre todos la completemos y discutamos.

  • Programa para humanos, no para máquinas. Como regla general un programador pasa más tiempo revisando código propio y de terceros que escribiéndolo. Esto debería tenerse en cuenta a la hora de codificar para facilitar la labor al programador. 
    • Selecciona nombres adecuados y descriptivos para los distintos elementos: clases, métodos, atributos, …
    • Comenta sólo lo que el código no diga. No le contemos a otro programador lo que puede saber leyendo el código.
    • Métodos de 1000 líneas y clases de 10000 no son legibles para humanos. Segmenta el código.
  • Haz uso de herramientas de control de versiones; con ello tendrás una serie de ventajas:
    • Copias de seguridad de tu código.
    • Capacidad de trabajo en paralelo con otros “colegas”.
    • Seguimiento de los cambios a lo largo del desarrollo del proyecto.
  • Refactoriza. Mejora la calidad de tu código sin cambiar el comportamiento del sistema. Haz que sea más fácil de entender, mantener y extender.
  • Automatiza. Los humanos no somos demasiado buenos en la ejecución de tareas repetitivas pero las máquinas sí; por ello:
    • Define atajos para llevar a cabo tareas repetitivas.
    • Haz uso de un entorno de integración contínua.
    • Define procesos de despliegue automático de aplicaciones.
  • Usa el principio DRY (Don’t Repeat Yourself). Evita el uso del “copy/paste” a no ser que sea imprescindible. Si duplicas código tienes el doble de código para mantener.
  • Usa el principio KISS (Keep It Simple, Stupid!): desarrolla empleando partes sencillas y comprensibles, rechazando lo enrevesado e innecesario.
  • Conoce tu IDE de desarrollo. Aprender los atajos y las características que tiene un IDE nos permite usarlo con todo su potencial y nos puede ahorrar incontables horas de trabajo poco productivo.
  • Entiende el dominio. El cualquier desarrollo y sobre todo en los vinculados a I+D el conocimiento del dominio es fundamental.

Si se te ocurre algún punto más o no estás de acuerdo en alguno de los puntos mencionados déjalo en los comentarios de este post.

Referencias:

Gestión visual Kanban

Kanban es una técnica de control visual. Es un sistema de señalización para desencadenar acciones sólo cuando son necesarias, reduciendo al mismo tiempo la sobreproducción y el almacenamiento innecesario.

El origen de este sistema de control visual se remonta a finales de los cuarenta o principios de los cincuenta, cuando su autor, Taiichi Onho desarrollaba señales para implementar con gestión visual métodos de producción “Just In Time” (JIT) en los centros de producción de Toyota de Japón.

En el ámbito del desarrollo ágil de software las prácticas de gestión visual es una parte importante  por ser las que mejor sirven a los principios de comunicación directa y simplicidad en la documentación y gestión. Kanban es una de las prácticas más ampliamente utilizada por su sencillez y eficacia. Sin embargo, no se trata de un modelo ágil en si, sino de una herramienta o técnica para comunicar información relativa y necesaria en la ejecución o control de un trabajo. Entre sus características más destacables se encuentran:

  • Favorece la comunicación directa
  • Genera un flujo de trabajo que lleva los problemas a la superficie
  • Produce un desarrollo incremental
  • Evita la ley de Parkinson

Veamos ahora como se utiliza esta técnica.

Tablero Kanban

El elemento más fundamental de Kanban es el tablero. Puede ser cualquier superficie donde puedan colocarse objetos movibles (una pizarra, una pared con posits, un tablero electrónico…). Dentro del tablero hay una serie de columnas, donde cada una de ellas consiste en una de las fases a realizar para todos los procesos. Un ejemplo típico (Figura 1) consiste en 3 columnas:

  • To Do: tareas pendientes de realizar
  • In Process: tareas que se están haciendo ahora mismo
  • Done: tareas que ya han sido terminadas
kanban1

Figura 1. Tablero Kanban Simple

Tarjetas Kanban

El contenido de un tablero kanban son las tarjetas kanban. Cada una de las tarjetas se corresponde con una única tarea. Cada una de estas tarjetas irán pasando de columna en columna según vaya progresando su estado, realizando el flujo de trabajo definido en el tablero kanban. En la mayoría de modelos, cada tarjeta tiene un responsable que es el que decide cuando pasa de una columna a la siguiente. Además, mediante una simple barra horizontal es posible monitorizar el progreso de la tarea.

Fig 2. Tarjeta Kanban

Fig 2. Tarjeta Kanban

Tanto en el caso del tablero como de las tarjetas, no existe un formato cerrado. Kanban es solo una técnica de visualización, pero la forma de ser implementada dependerá del propio equipo de desarrollo. Muchos tableros kanban tienen más etapas que las vistas (Backlog, To Validate…) e incluso en vez de columnas, la disposición es en filas.

Por otro lado, las tarjetas kanban es algo muy personal. En un equipo con una gran cooperación es posible prescindir de responsables, o es posible que se quiera añadir una pequeña descripción por tarea. En otro modelos de desarrollo existen unas meta-tarjetas llamadas historias que agrupan a un conjunto de tareas con un mismo fin.

Sea cual sea la aproximación elegida, lo importante de kanban es que sea visualmente identificable el estado en el que se encuentran todas las tareas involucradas en el desarrollo.

WIP: Work In Process

Para lograr un flujo continuo de funcionalidades que, una a una van aportando incrementos de forma sostenida, es necesario limitar la cantidad de trabajo que hay en proceso. Esto es, limitar el número de tareas que pueden estar de forma simultánea en los diferentes estados de desarrollo. De esta forma se evita que se amontonen tareas obligando a los miembros del equipo a trabajar en varias a la vez, perdiendo el foco, el ritmo y la eficiencia.

Al parámetro que indica el número máximo de tareas en un área del tablero kanban se le denomina WIP: Work In Process. Un valor WIP demasiado bajo produce tiempos muertos, y demasiado alto, cuellos de botella. La experiencia ayuda al equipo a ir ajustándolo para lograr un flujo lo más continuo posible.

En la figura 3 se muestra un caso práctico usando WIP. Este es el caso de un tablero kanban personal de un único desarrollador. Este desarrollador ha decidido limitar el número de tareas en espera de ser realizadas a 5. También ha introducido un WIP de 2 para las tareas en proceso, ya que más tareas a la vez le harían ser poco eficiente. Hasta que no termine una de las dos tareas en progreso, no podrá empezar otra tarea nueva.

Fig 3. Ejemplo WIP

Fig 3. Ejemplo WIP

Para profundizar