jueves, 8 de noviembre de 2007

Enumerable#iterate

¿Quién no se ha encontrado alguna vez escribiendo un bucle donde necesita saber cuándo está en el último elemento? ¿O cuál es el próximo elemento?

  coleccion.each {|item|
     if estoy_en_el_ultimo?
       ....

La solución típica en este caso sería usar each_with_index y dentro del bloque coleccion.size == index + 1, pero, siendo Ruby como es, molaría algo más “objetista”

Seguramente habrán muchas implementaciones de algo así, pero me apetecía experimentar con una, así que antes de buscar le di un poco de caña al Enumerable

Partiendo de que en Ruby se puede modificar cualquier cosa (entendiendo por cosa... lo que sea), bastaría con añadir un método iterate al módulo Enumerable, de manera que todos los objetos que actúan como secuencias se beneficiarían del método

Por ejemplo, podríamos tener


output = ''
(0..10).map { (rand * 10).to_i }.iterate {|iterator|
    output << "<b>" if iterator.first? || iterator.last?
    output << iterator.object.to_s

    if iterator.object % 2 == 0
        output << " " << iterator.next.to_s
        iterator.skip_next!
    end

    output << "</b>" if iterator.first? || iterator.last?

    output << " <br />\n" unless iterator.last?
}

puts output


Que nos daría una salida como

<b>6 3</b> <br />
6 2 <br />
7 <br />
3 <br />
2 1 <br />
8 3 <br />
<b>5</b>

O también (un poco más rebuscado)


[1, "uno", 2, "dos", "tres", 3, "cuatro", "cinco", "seis", 0].iterate {|i|
    puts "<div>" if i.first?
    i.skip_next! i.object
    i.object.times {|n| print i.next(n + 1) + " " }
    puts i.last? ? "</div>" : "<br />"
}


Que nos daría

<div>
uno <br />
dos tres <br />
cuatro cinco seis <br />
</div>

¿Y cómo conseguir todo eso?


module Enumerable

    class Iterator
        attr_accessor :object
        attr_accessor :index
        attr_reader :parent

        def initialize(parent)
            @parent = parent
            @skip_items = 0
        end

        def last?
            index == parent.size - 1
        end

        def first?
            index == 0
        end

        def next(offset = 1)
            (offset + index >= parent.size) ? nil : parent[index + offset]
        end

        def previous(offset = 1)
            (index - offset < 0) ? nil : parent[index - offset]
        end

        def skip_next!(items = 1)
            @skip_items += items.to_i
        end

        def skip?(keep_value = false)
            if @skip_items > 0
                @skip_items -= 1 unless keep_value
                true
            end
        end
    end

    def iterate(&block)
        iterator = Iterator.new self
        each_with_index {|object, index|
            next if iterator.skip?
            iterator.object = object
            iterator.index = index
            block.call iterator
        }
    end

end



Después de terminar este pequeño experimento hice una búsqueda rápida a ver qué más había por ahí. Lo único que vi, sin buscar demasiado, fue un mensaje en la ruby-tak del 2002, donde hay una implementación parecida a ésta.

domingo, 21 de octubre de 2007

12 de octubre: Día del comienzo de la masacre

El pasado 12 de octubre (hace algo más de una semana ya) se celebró el Día de la Hispanidad. Nada en especial para mí. Sin embargo, después ver varios posts en la blogosfera me decidí a escribir lo que me parece esa fiesta.

El «Día de la Hispanidad» celebra la conquista de América después de ser encontrada por casualidad por Colón, el 12 de octubre de 1492. En el artículo de la Wikipedia sobre la conquista de América hay información muy interesante que, seguramente, no todos conocen. Destacaría, por ejemplo, esta parte:

La llegada de Cristóbal Colón a América abriría la Conquista del mal llamado Nuevo Mundo por parte de algunos imperios europeos, justificando la dominación en tres grandes principios:

  1. Era "Tierra de Nadie" (res nullius), principio que supuso de hecho y derecho el desconocimiento de la presencia de sus habitantes como personas con derechos.
  2. Eran "tierras para la cristianidad", principio que llevó a su vez a la decisión de imponer el cristianismo a los habitantes de América, habilitando su exterminio en caso de negarse a aceptar esa religión. En ciertos casos los europeos sostuvieron que "los indios no tenían alma" negando la condición humana de los pueblos originarios.
  3. Como América era "Tierra de Nadie", las civilizaciones que allí se habían desarrollado, las riquezas acumuladas y naturales, así como el derecho a utilizar a los habitantes como mano de obra forzada, correspondía a quien se impusiera primero. La obsesión por el oro, simbolizada en la búsqueda de El Dorado, caracterizó la Conquista de América así como la migración de miles de tropas mercenarias.

En otro artículo de la Wikipedia, Catástrofe demográfica en América tras la llegada de los europeos, hablan acerca del extermio de los habitantes del continente invadido. Destacaría algunos párrafos, como por ejemplo

Las causas del colapso demográfico son ampliamente aceptadas entre historiadores y demógrafos: por una parte, las enfermedades infecciosas portadas por los europeos, para las cuales la población indígena americana no poseía defensas. Por otra parte, la brutalidad de la conquista y las condiciones del régimen de explotación a que fueron sujetos durante la mayor parte de la época colonial. Existen discrepancias acerca de en qué grado contribuyeron unas y otras a la debacle demográfica, coincidiéndose en la realimentación entre ambas.

Y también

El investigador norteamericano H. F. Dobyns ha calculado que un 95% de la población total de América murió en los primeros 130 años después de la llegada de Colón. Por su parte, Cook y Borah, de la Universidad de Berkeley, establecieron luego de décadas de investigación, que la población en México disminuyó de 25,2 millones en 1518 a 700 mil personas en 1623, menos del 3% de la población original

El año pasado, el programa Caiga Quien Caiga en Argentina estuvo la celebración de Madrid de ese día. El vídeo, sencillamente genial:

Afortunadamente, la versión española de Caiga Quien Caiga ha vuelto a la carga, y este año tenemos un video tan genial como éste:

Este vídeo es más claro aún. Muestra la clase de españoles que tenemos en ésta, “nuestra España

Días antes, Rajoy mostraba su orgullo de ser español, y nos decía, más o menos, que hiciéramos lo mismo. A mí, personalmente, me parece lamentable que alguien esté “orgulloso” de ser de un país, de una religión o lo que sea. Como decía Montesquieu, «soy necesariamente hombre mientras que no soy francés [ponga aquí nacionalidad favorita] más que por casualidad». Me encantaría que esas personas que se muestran tan orgullosas de ser españoles (empezando por Rajoy) explicaran exáctamente qué les hace sentir tan orgullosos de haber nacido en España.

Yo, por el momento, secundo a JRMora.

domingo, 19 de agosto de 2007

Optimización de la carga de imágenes pequeñas

El problema

El tiempo de carga de las páginas es algo muy importante para que los usuarios (nosotros mismos) se sientan cómodos usando las aplicaciones o navegando por la web. En algunas páginas como http://jimmac.musichall.cz/openoffice-icons.php o http://www.google.com/language_tools hay muchísimos iconos. Cada uno de estos iconos está en un fichero separado, por lo que el navegador los tendrá que descargar uno a uno. Este problema también lo podremos observar en cualquier aplicación que tenga muchos iconos.

Mirando http://www.google.com/language_tools con el Firebug, en la pestaña Net, podemos ver algo como esto:

Firebug: Net

Las 158 imágenes de la página se van cargando una detrás de otra. Usando el Full page test de Pingdom vemos un resultado similar de la carga de la página.

Las 157 imágenes que corresponden a las banderas pesan (todas juntas) algo menos de 100KiB. Con una conexión decente no debería llegar a los 2 segundos. Sin embargo está cargando casi 9 segundos. ¿Qué pasa?.

Para empezar, tenemos las cabeceras HTTP. Para cada imagen se lanza una petición como ésta:

GET /images/flags/XX_flag.gif HTTP/1.1
Host: www.google.com
User-Agent: Mozilla/5.0
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: es-es,es;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: [Cookie de google. No se puede decir que sea corta...]

El tamaño de la petición está por encima de 1KiB. Puede ser insignificante para una sola petición, pero no lo es para 157. La cabecera de la respuesta es bastante más pequeña. Aparte del overhead que añaden las cabeceras hay que tener el de TCP/IP. El uso de keep alive mejora los tiempos, ya que no hace falta volver a abrir una nueva conexión, pero sigue siendo necesario enviar una petición para cada archivo.

La solución

La solución es simplemente combinar todas las imágenes en una sola. Con esto nos ahorramos las cabeceras necesarias para las peticiones HTTP. Además, al comprimir todas las imágenes juntas el fichero resultante tendrá un peso bastante menor.

Esta técnica se ha comentado varias veces en la blogosfera. En A List Apart hay un artículo titulado CSS Sprites: Image Slicing’s Kiss of Death. Incluso hay algunas implementaciones, como el ImageBundle de GWT o este snippet para Django.

No encontré nada para Rails, así que decidí implementar uno.

Funcionamiento

El funcionamiento es muy sencillo

  1. Se empaquetan las imágenes, y se guarda la posición de cada una dentro del paquete.
  2. Se cambia el image_tag para que pueda generar imágenes hacia paquete.

Para generar el paquete hay que tener en cuenta algunos puntos:

  • Primero, es preferible agrupar las imágenes según el formato. Es decir, las imágenes PNG en un paquete guardado como PNG. Las JPEG en otro como JPEG, etc. Cada imagen vendrá en un formato según las ventajas de éste (como que PNG no pierde calidad o que JPEG comprime más, por poner algunos ejemplos), así que al convertirlas a otro formato perderemos ventajas.
  • Segundo, sólo deberían empaquetarse imágenes pequeñas. Las que son muy grandes se suelen usar para fondos o algún logotipo, y el overhead no suele ser un problema. No hay que olvidar que en las pequeñas el peso de las cabeceras puede llegar a ser mayor que la propia imagen.
  • Tercero, las animaciones (generalmente ficheros GIF con más de un fotograma) no pueden ser empaquetadas.

El image_tag tiene que poder detectar si la imagen que se está pidiendo está empaquetada. De ser así, el <img> generado apuntará hacia el paquete.

Implementación

Para poder disponer de algo así fácilmente en cualquier aplicación lo más cómodo es tenerlo como un plugin. El código lo subiré a RubyForge desde que me acepten el proyecto (avisaré por aquí).

El plugin añade una tarea en el rake para actualizar los paquetes generados. De esta manera, tan sólo necesitamos poner

$ rake images:bundle:update

Cuando la tarea empieza comprueba si existen los ficheros config/rails-image-bundle.yml y public/images/fulltransparent.gif. Si no existe alguno de ellos los copia del directorio templates que viene dentro del propio plugin. El primer fichero se usa para cambiar algunos parámetros del plugin. El segundo lo veremos más abajo.

Los parámetros que se pueden configurar del plugin son

  • file-bundle, que indica la base del nombre donde se generarán los paquetes. Inicialmente tiene el valor generated-image-bundle. De esta manera, los paquetes se generarán en public/images/generated-image-bundle.png (o .jpeg, .gif, etc)
  • max-width, que indica el ancho máximo que puede tener una imagen para ser empaquetada.
  • max-height, lo mismo que max-width, pero para el alto.

En la mayoría de los casos no hará falta tocar el fichero.

Para generar los paquetes el plugin buscará todos los ficheros por debajo de public/images de la aplicación. Cada uno los cargará con RMagick y comprobará si:

  • Se cargó correctamente
  • Las dimensiones de la imagen no pasan de max-width y max-height
  • Sólo tiene un fotograma (no es una animación)

Si se cumplen todas las condiciones añadirá al paquete de su formato (public/images/generated-image-bundle.{png,jpeg,...}) y guarda la información necesaria para saber que el fichero está empaquetado. Para esto se necesita guardar:

  • El nombre del fichero que se va a empaquetar (por ejemplo, edit_button.png)
  • La posición (x,y) dentro del paquete
  • El nombre del paquete donde está
  • Las dimensiones de la imagen empaquetada

Después de empaquetar todas las imágenes se vuelca el fichero con toda la información sobre los paquetes en tmp/rails-image-bundle.marshal. Puesto que no es necesario que este fichero sea legible para personas humanas (ni para las personas no humanas) se usa Marshal.

Con esto ya tenemos nuestros paquetes de imágenes generados. Ahora nos queda que sean accesibles al navegador. Cambiaremos el image_tag para detectar cuándo queremos usar alguna imagen que está empaquetada.

La técnica para poder utilizar los paquetes se basa en CSS. Consiste en:

  • Generamos una etiqueta img con una imagen completamente transparente (de ahí el fulltransparent.gif)
  • Ponemos el tamaño exacto de la imagen que queremos mostrar. Con esto además evitamos el efecto (no muy agradable) de que el contenido de la página se mueva mientras se cargan las imágenes
  • Añadimos como fondo de la etiqueta la imagen del paquete, y lo desplazamos para que sólo quede visible la parte que corresponde a la imagen que queremos ver,

Para que se hagan una idea, vean cómo quedó implementado el image_tag

module ImageBundle
    module ViewMethods

        def self.included(base)
            base.alias_method_chain :image_tag, :bundle
        end

        def image_tag_with_bundle(source, options = {})
            @__bundle_information ||= (Marshal.load(File.read(ImageBundle::INFO_FILE)) rescue nil) || {}
            if info = @__bundle_information[source.to_s]
                source = "fulltransparent.gif"
                options[:style] = "background: url(#{image_path info[:bundle]}) -#{info[:x]}px -#{info[:y]}px; #{options[:style]}"
                options[:width] = info[:width]
                options[:height] = info[:height]
            end

            image_tag_without_bundle(source, options)
        end

    end
end

Posibles mejoras

Hay muchas mejoras que se pueden hacer. La implementación actual tiene 142 líneas, escritas en algo más de hora y media. El objetivo principal (optimizar la carga de imágenes pequeñas) está conseguido. ¿Qué podemos hacer ahora?

Una mejora interesante es poder restringir qué archivos son empaquetados. Ahora mismo se cogen todos los que soporta el backend que use RMagick (que será ImageMagick o GraphicsMagick) hasta cierto tamaño. En algún caso nos puede interesar que sólo se empaqueten ficheros PNG y GIF, o que no se empaqueten los que estén bajo public/images/donotuse.

Otra posible mejora (aunque de mucha menos utilidad) es distribuir las imágenes para optimizar el espacio. Ahora mismo se ponen todas en vertical. Habría que hacer pruebas, pero en el caso más general no creo que haya diferencias significativas de tamaño.

Seguro que hay muchas más mejoras, que se irán conociendo con el tiempo.

miércoles, 4 de julio de 2007

iPhone por aquí, iPhone por allá

Estos días no han parado de salir noticias y posts en blogs sobre el iPhone. Como decía Ricardo Galli:

Por último, ¿cómo la gente puede sacar tanto para escribir de un teléfono? ¿cómo se puede estar tan contento de conseguir uno después de horas de largas colas? ¿cómo no se puede sentir un mínimo de pudor de hablar tanto y tanto sobre un cacharrito? Alucino.

Pues... he aquí la noticia que más me ha gustado de todas :-P: iPhone Root Password Hacked in Three Days

martes, 26 de junio de 2007

Viaje al interior de... un DVD

Hoy me pasaron un DVD con un video. Al llegar a mis manos me comentaron que el DVD no estaba cerrado («sin finalizar», que supongo que será el término que usen los programas como Nero). En principio no debería haber ningún problema, ya que mi máquina es nueva, ya han pasado por aquí varios CDs sin cerrar, estoy con un 2.6.17 (el de Ubuntu Edgy... y ya... no es que esté recién sacado del horno, pero tampoco se puede decir que sea viejo), etc etc

Cuando el DVD entra en la máquina aparece un mensaje como que acabo de meter un disco virgen, y me pregunta si lo quiero desvirgar ¿?¿?. Antes de que cunda el caos, miro en el syslog a ver qué se cuenta. Cada vez que entra el DVD aparece.

...] cdrom: This disc doesn't have any tracks I recognize!

Buscando por Google con ese mensaje no veo nada de utilidad, así que intento atacar el disco directamente. Aunque no tenía muchas esperanzas probé con un dd, pero salía nada más empezar, y en el syslog podía ver

...] end_request: I/O error, dev sr0, sector 0
...] printk: 17 messages suppressed.
...] Buffer I/O error on device sr0, logical block 0
...] end_request: I/O error, dev sr0, sector 0

Ouch!

Buscando herramientas relacionadas con los DVD (aún no he perdido la esperanza) veo un dvd+rw-mediainfo, que forma parte del paquete dvd+rw-tools. Al ejecutarlo sobre el DVD veo cosas bastante prometedoras:

# dvd+rw-mediainfo /dev/dvd
INQUIRY:                [PHILIPS ][DVD+-RW SDVD8820][AD15]
GET [CURRENT] CONFIGURATION:
 Mounted Media:         11h, DVD-R Sequential
 Media ID:              CMC MAG. AE1
 Current Write Speed:   8.0x1385=11080KB/s
 Write Speed #0:        8.0x1385=11080KB/s
 Write Speed #1:        6.0x1385=8310KB/s
 Write Speed #2:        4.0x1385=5540KB/s
 Write Speed #3:        2.0x1385=2770KB/s
 Speed Descriptor#0:    00/2297887 R@8.0x1385=11080KB/s W@8.0x1385=11080KB/s
 Speed Descriptor#1:    00/2297887 R@8.0x1385=11080KB/s W@6.0x1385=8310KB/s
 Speed Descriptor#2:    00/2297887 R@8.0x1385=11080KB/s W@4.0x1385=5540KB/s
 Speed Descriptor#3:    00/2297887 R@8.0x1385=11080KB/s W@2.0x1385=2770KB/s
READ DVD STRUCTURE[#10h]:
 Media Book Type:       25h, DVD-R book [revision 5]
 Legacy lead-out at:    2298496*2KB=4707319808
READ DVD STRUCTURE[#0h]:
 Media Book Type:       25h, DVD-R book [revision 5]
 Last border-out at:    0*2KB=0
READ DISC INFORMATION:
 Disc status:           appendable
 Number of Sessions:    1
 State of Last Session: incomplete
 "Next" Track:          1
 Number of Tracks:      4
READ TRACK INFORMATION[#1]:
 Track State:           reserved incremental
 Track Start Address:   0*2KB
 Next Writable Address: 0*2KB
 Free Blocks:           1520*2KB
 Track Size:            1520*2KB
READ TRACK INFORMATION[#2]:
 Track State:           complete incremental
 Track Start Address:   1536*2KB
 Free Blocks:           0*2KB
 Track Size:            176*2KB
 Last Recorded Address: 1551*2KB
READ TRACK INFORMATION[#3]:
 Track State:           complete incremental
 Track Start Address:   1728*2KB
 Free Blocks:           0*2KB
 Track Size:            668544*2KB
 Last Recorded Address: 670271*2KB
READ TRACK INFORMATION[#4]:
 Track State:           invisible incremental
 Track Start Address:   670288*2KB
 Next Writable Address: 670288*2KB
 Free Blocks:           1627600*2KB
 Track Size:            1627600*2KB
READ CAPACITY:          0*2048=0

Apenas he trabajado con DVDs multisesión, pero supongo que esa salida será más que normal. Lo que importa es que el DVD está ahí, y, de algún modo, dvd+rw-mediainfo puede acceder a él.

Me pongo a buscar entre las herramientas sobre DVDs y CDs y veo que cdrecord tiene un readcd, que parece ser lo que necesito. Los primeros resultados no son muy positivos

# readcd dev=/dev/scd0 f=cdimage.raw
Read  speed: 11080 kB/s (CD  62x, DVD  8x).
Write speed: 11080 kB/s (CD  62x, DVD  8x).
Capacity: 1 Blocks = 2 kBytes = 0 MBytes = 0 prMB
Sectorsize: 2048 Bytes
Copy from SCSI (1,0,0) disk to file 'cdimage.raw'
end:         1
readcd: Input/output error. read_g1: scsi sendcmd: no error
CDB:  28 00 00 00 00 00 00 00 01 00
status: 0x2 (CHECK CONDITION)
Sense Bytes: 70 00 05 00 00 00 00 0A 00 00 00 00 21 00 00 00
Sense Key: 0x5 Illegal Request, Segment 0
Sense Code: 0x21 Qual 0x00 (logical block address out of range) Fru 0x0
Sense flags: Blk 0 (not valid) 
resid: 2048
cmd finished after 0.009s timeout 40s
readcd: Input/output error. Cannot read source disk
readcd: Retrying from sector 0.
.~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~+~~~-~~~
readcd: Input/output error. Error on sector 0 not corrected. Total of 1 errors.

Time total: 1.982sec
Read 0.00 kB at 0.0 kB/sec.
Max corected retry count was 0 (limited to 128).
The following 1 sector(s) could not be read correctly:
0

Tiene el mismo problema que el dd. Mirando en el man para ver si hay manera de que ignore esos errores y siga leyendo veo que hay opciones como -noerror o -nocorr. Con estas opciones lee un poco más, pero no sale de la primera pista

Mirando un poco más veo que hay una opción para indicar qué sectores sacar del DVD. Fíjandome en la salida del dvd+rw-mediainfo veo que la segunda y la tercera pista están desde el sector 1536 al 670448 (el final se saca sumando el tamaño de las dos)... Así que añado sectors=1536-670448 a las opciones del readcd... y ¡Voilà!

Los datos que empiezan a leer corresponden al video que quería ver. Después de unos minutos tengo un fichero de 1,3Gib con un video de unos 20 minutos

Según file el fichero es un Video title set, v11, aunque el mplayer lo abre usando el codec mpegpes, que (según su propia descripción) corresponde a MPEG-PES output (.mpg or DXR3/DVB card)

Quizás con algún programa de esos llenos de dibujos y colores por todas partes no hubiera tenido que pensar en cómo solucionar este tema, pero seguro que no me hubiera sentido tan bien con la solución.