Librería de logging para C++, boost::logging

En C++ tienes diversas librerías de logging, la mayoría son clones de log4j realmente, u otras con un estilo diferente bastante interesante; pero como suele ser costumbre en el mundo C++, tienes una alternativa relacionada con las librerías boost (en realidad boost::logging no es de boost oficialmente, pero es más que probable que en el futuro lo sea) que suele ser la ganadora por méritos propios.

Estoy hablando de la implementación de John Torjo. Muy flexible a la hora de configurarla unido a un uso trivial de la misma (como tiene que ser, tampoco es que, la labor de logging, sea algo muy complejo que digamos...). Para conocer todos los detalles puedes leerte la extensa y útil documentación, aunque de primeras puede ser un tanto compleja debido a nuevos conceptos que se usan a diestro y siniestro.

Principalmente debemos de conocer dos cosas:

  • Qué tipo de filtro queremos: el cual será el encargado decidir si un mensaje se escribe o no, dependiendo tanto de si el logging está activado, como si cumplimos la restricción de nivel (debug, warn, error, etc...)
  • Qué tipo de log queremos: ¿cómo debe de ser el formato de salida?, ¿cuál es la salida/s? ¿cómo será su comportamiento?.

Una vez definido esto, podemos escribirnos un par de ficheros en los cuales definiremos el log que podrá ser usado desde cualquier parte de la aplicación, incluyendo el header, y siempre y cuando se haya inicializado previamente.

 
#ifndef __LOGGING_HPP__
#define __LOGGING_HPP__
 
#include <boost/logging/format_fwd.hpp>
 
using namespace boost::logging;
using namespace boost::logging::scenario::usage;
 
typedef use<
    filter_::change::single_thread, // how often does the filter change?
    filter_::level::no_levels,      // does the filter use levels?
    logger_::change::single_thread, // how often does the logger change?
    logger_::favor::correctness     // what does the logger favor?
  > finder;
 
BOOST_DECLARE_LOG_FILTER( g_log_filter, finder::filter )
BOOST_DECLARE_LOG( g_log, finder::logger )
 
#define L_ BOOST_LOG_USE_LOG_IF_FILTER( g_log(), g_log_filter()->is_enabled() )
 
void initialize_logs();
 
#endif // __LOGGING_HPP__

Aquí acabamos de declarar un log, sin niveles, para una aplicación simple sin usar un thread separado para el logging. Para ver otras alternativas puedes echarle un ojo a los namespaces boost::logging::scenario::usage::filter_ y boost::logging::scenario::usage::logger_.

Finalmente solo nos quedaría inicializar el log (o logs, recuerda que nada te impide tener tantos como quieras):

 
#include "logging.hpp"
#include <boost/logging/format_ts.hpp>
#include <boost/thread/xtime.hpp>
 
BOOST_DEFINE_LOG_FILTER( g_log_filter, finder::filter )
 
BOOST_DEFINE_LOG( g_log, finder::logger )
 
void initialize_logs()
{
  g_log()->writer().add_formatter( formatter::idx(), "[%] "  );
  g_log()->writer().add_formatter( formatter::time("$hh:$mm.$ss ") );
  g_log()->writer().add_formatter( formatter::append_newline() );
 
  typedef detail::flag<destination::file_settings> flag;
  destination::file_settings file_settings;
  file_settings.initial_overwrite = flag::t<bool>( &file_settings, true );
  g_log()->writer().add_destination(
      destination::file( "app_debug.txt", file_settings )
    );
  g_log()->writer().add_destination(
      destination::cerr
    );
 
  g_log()->turn_cache_off(); // for showing output immediately
  g_log()->mark_as_initialized();
}

Aquí tenemos que hacer la definición del log y del filter de la misma forma que hemos hecho su declaración anteriormente. Definimos el formato de salida como más nos guste y finalmente solo nos queda indicar dónde se deben de escribir los mensajes. Se puede tener varios destinos, aunque básicamente podemos usar desde ficheros hasta cualquier stream, pasando por las salidas estándar (fíjate en los typedef de boost::logging:destination).

Una cosa importante que merece la pena mencionar es la llamada al método turn_cache_off(), especialmente importante si usas una salida a consola, pues por defecto el logging se cachea y solo se escribe al destino cada cierto tiempo, por lo que si monitorizas el log en tiempo real es crucial desactivar esta caché.

De esta forma, ya tendremos en cualquier parte de nuestro código donde incluyamos la declaración una macro L_ (o como la hayamos definido) lista para ser usada:

 
L_ << "Here we are";
 
unsigned int n = 42;
L_ << "The meaning of life is " << n;
 
L_ << boost::format( "We can even use the awesome %s library versión %d.%d.%d" )
    % "boost::format"
    % ( BOOST_VERSION / 100000 )
    % ( ( BOOST_VERSION / 100 ) % 1000 )
    % ( BOOST_VERSION % 100 );

Rails 2.2 & Inflector & Dependencies

Si has actualizado a rails 2.2 (o superior) desde una versión previa puedes encontrarte con un par de errores típicos con una solución muy simple:

`const_missing': uninitialized constant Rails::Plugin::Dependencies (NameError)

Es debido a que Dependencies ahora es ActiveSupport::Dependencies.

`load_missing_constant': uninitialized constant Inflector (NameError)

Es debido a que Inflector ahora es ActiveSupport::Inflector. Por lo que ahora puedes usar las inflections tal que así:

 
ActiveSupport::Inflector.inflections do |inflect|
  ...
end

Instalar módulos de CPAN como paquetes .deb automáticamente

En un sistema basado en paquetes .deb (Debian, ubuntu, etc...) es bastante común tener una gran cantidad de módulos perl ya empaquetados en los repositorios pertinentes. La traducción del nombre del módulo al nombre del paquete es inmediata, Nombre::Del::Paquete se convertiría a libnombre-del-paquete-perl. Si resulta que tienes la mala suerte de no tener el que buscas, no pasa nada, existe una maravillosa herramienta llamada dh-make-perl (# aptitude install dh-make-perl) que nos solucionará todo.

Tienes múltiples opciones de configuración, pero explicaré dos formas de usarla, la rápida, cómoda e instantánea (¡como el colacao!) y otra donde vas más paso a paso (es decir, que en vez de un comando, ejecutas dos, joooplis).

Forma 1: Directamente se descargará el código y generará un .deb listo para ser instalado.

Digamos que quieres instalarte el módulo Hola::Que::Tal, pues ejecutas:

$ dh-make-perl --build --cpan Hola::Que::Tal

Con eso estaremos descargando el código del paquete (si en cpan existe claro) y generando un .deb de forma automática. Si existen dependencias se te indicará qué módulos se requieren, en cuyo caso sería recomendable que mirases si los tienes en tus repositorios antes de generar un .deb para ellos también.

Por poner un ejemplo real:

 
$ dh-make-perl --build --cpan Gtk2::Sexy
(...)
$ ls
Gtk2-Sexy-0.05                     libgtk2-sexy-perl_0.05.orig.tar.gz
libgtk2-sexy-perl_0.05-1_i386.deb
$ sudo dpkg -i libgtk2-sexy-perl_0.05-1_i386.deb

Forma 2: Tenemos un .tar.gz descomprimido descargado de cpan, generaremos la estructura de ficheros necesaria para generar un .deb con las herramientas típicas de debian (con debuild, $ sudo aptitude install devscripts).

Por seguir el ejemplo anterior, si quisiéramos instalar el módulo Hola::Que::Tal versión 1.0, nos descargamos de cpan su tar.gz y lo descomprimimos, por lo que tendríamos un directorio con el nombre Hola-Que-Tal-1.0. Con dh-make-perl generaremos los ficheros necesarios para poder generar un paquete deb (es decir, se creará un directorio debian/ con ficheros varios, los cuales, si sabes para qué sirven y quieres complicarte la vida, puedes modificarlos a mano para personalizar el .deb generado). Después simplemente entramos en el directorio y generamos el .deb ejecutando debuild.

 
$ ls
Hola-Que-Tal-1.0.tar.gz
$ tar xfz Hola-Que-Tal-1.0.tar.gz
$ dh-make-perl Hola-Que-Tal-1.0/
(...)
$ cd Hola-Que-Tal-1.0
$ debuild
(...)
$ ls .. | grep .deb
libhola-que-tal-perl_1.0-1_i386.deb

Festival del humor en CSI New York

La que habla al final, dice algo así como:

I'll create a GUI interface in Visual Basic, see if I can track an IP address.

Crearé una GUI en Visual Basic, a ver si puedo rastrear una dirección IP.

Para la gente que no le haga gracia, que será mayoría, significará algo así como: blabla GUI blablabla IP blabla.

Vía reddit.

Paralelizando quicksort en Erlang

Haciendo pruebas con erlang, se me ocurrió que sería divertido probar a paralelizar algún algoritmo típico (sobretodo debido a que mi cpu es ahora un quadcore). Como mi imaginación no está muy avanzada, probé con el quicksort.

Su implementación en un lenguaje funcional suele ser bastante simple. Y en Erlang podría ser algo simplemente como:

 
qsort([]) ->
   [];
qsort([Pivot|T]) ->
   qsort([X || X <- T, X < Pivot])   ++
   [Pivot]                           ++
   qsort([X || X <- T, X >= Pivot]).

En un primer (y estúpido) intento por paralelizarlo, probé simplemente a que cada llamada recursiva fuese un nuevo proceso. El asunto terminó con el ordenador medio bloqueado al intentar crear millones de procesos (pues inicié erlang poniéndole explícitamente que no hubiese límite :P , por defecto tiene 216 como límite).

El asunto tiene simple solución. Si tenemos un problema por permitir un exceso de procesos, pues se pone un límite y arreglado, ¿no?. El esquema usado simplemente es que para cada llamada recursiva, se indicará el número máximo de procesos que puede crear el proceso en cuestión y todos sus hijos. Si las llamadas recursivas de un quicksort van a ser un árbol donde cada nodo tendrá dos hijos, simplemente vamos a podarlo para que finalmente las hojas de dicho árbol ejecuten el algoritmo quicksort normal y el resto, los nodos, realizarán el quicksort paralelo (p_qsort lo he llamado).

 
%%
%% Params:
%%   - List to be sorted
%%
p_qsort(L) ->
   S   = self(),
   Ref = erlang:make_ref(),
 
   spawn(fun() ->
            p_qsort(L, ?THREAD_LIMIT, S, Ref)
         end),
 
   gather(Ref).
%%
%% Params:
%%   - List to be sorted
%%   - Maximum number of processes can be created
%%   - Father process (the results will be sent to him)
%%   - Unique reference for knowing what position the results are in
p_qsort([], _, Father, Ref) ->
   Father ! {Ref, []};
p_qsort([Pivot|T], Limit, Father, Ref) when Limit > 2 ->
   S       = self(),
   R_right = erlang:make_ref(),
   R_left  = erlang:make_ref(),
 
   spawn(fun() ->
            p_qsort( [X || X <- T, X <   Pivot], Limit div 2, S, R_left)
         end),
   spawn(fun() ->
            p_qsort( [X || X <- T, X >=  Pivot], Limit div 2, S, R_right)
         end),
 
   Father ! {Ref, gather(R_left) ++ [Pivot] ++ gather(R_right)};
p_qsort(L, _, Father, Ref) ->
   Father ! {Ref, qsort(L)}.

La función con un único parámetro es la función pública de entrada. Ésta, crea un proceso con la función de quicksort paralelo indicando un número, como segundo parámetro, que significa el número máximo de procesos que puede crear. En las funciones siguientes tenemos primero el caso de lista vacía, seguido de la función que ejecutarán los nodos (los que todavía pueden crear procesos hijos) y finalmente cuando no podemos crear más procesos se resolverá simplemente llamando a qsort(L) (los procesos hoja del árbol de llamadas).

Algunas aclaraciones adicionales por si no estas muy puesto en Erlang:

  • Con spawn estamos creando un nuevo proceso. En este caso le pasamos una referencia al actual proceso, tercer parámetro, para que nos "mande" sus resultados
  • Con Variable ! Algo se está mandando el mensaje 'Algo' al proceso 'Variable'
  • Erlang llama a las funciones con un matching de los parámetros (similar a prolog), en este ejemplo es bastante importante la condición when Limit > 2 que condicionará si crearemos más procesos o no
  • La función gather es solo una función para "recolectar" un resultado que me he montado copié de Joe Amstrong:
     
    %%
    %% Purpose: Receive a response of a process which have sent {Ref, Something}
    %% Params :
    %%   - A erlang reference for matching the message
    %% Return : The second item inside the received message
    gather(Ref) ->
       receive
          {Ref, Ret} -> Ret
       end.

    Para hacer pruebas, me he creado un par de módulos erlang para facilitar la ejecución:

    • benchmark.erl: define do/1 y do_silent/1 a las cuales les pasamos una función y nos devolverá el tiempo en µs (e imprimiendo un mensaje por pantalla).
    • test_sorting.erl: define funciones para ejecutar algoritmos de ordenación generando listas de tamaño variable de forma aleatoria. Usa el modulo de benchmark anteriormente citado, para crear csv's con los resultados de las ejecuciones.

    Mientras se ejecutaban los algoritmos, me pareció que sería curioso capturar el consumo de cpu de ambos, te dejo que intentes adivinar cual se corresponde con el algoritmo "tradicional" y cual con el "paralelo" (no vale mirar el nombre :P ).

    Los resultados detallados, los he copiado en esta hoja de cálculo para OpenOffice, y un resumen de ellos los podemos ver en esta gráfica creada con gruff en ruby leyendo los csv generados desde erlang.

    ejecución de un quicksort paralelo en erlang

    El algoritmo paralelo es el doble de rápido que el tradicional, con valores medianos o grandes. Con valores menores a 1000 elementos es más costoso (al crear procesos, aunque erlang es mucho erlang, estamos incorporando un coste adicional, principalmente por el envío de mensajes, no por la creación per-se), pero en tales casos no sé para qué narices estamos mirando formas de mejorar el rendimiento, pues mil elementos los ordena hasta MySQL mi vecino.

    Lo que hay que destacar es la mejora sustancial conseguida, prácticamente sin cambiar nada (el código paralelo es trivial una vez se tiene la versión "normal").

    Algunas cosas interesantes de probar serían:

    • Intentar ésto mismo con un número de cpus mayor a 4, y encontrar la relación entre cpus y ganancia.
    • Crear algún "behaviour" para erlang para poder generalizar la paralelización de algoritmos de divide y vencerás (que son bastante comunes) y siguen este esquema con bastante homogeneidad.

    Si quieres obtener todos los ficheros para ojear o jugar por ti mismo, aquí lo tenemos.

    Have fun.