Composer reduce su consumo de memoria hasta un 30% gracias a un programador español

Composer es el gestor de paquetes/dependencias utilizado por las aplicaciones PHP. Gracias a Composer, las aplicaciones sólo tienen que indicar las librerías que quieren usar, y Composer se encarga del resto, como por ejemplo averiguar la versión exacta que hay que instalar de cada librería para que todas sean compatibles entre sí.

La única pega de Composer es que consume mucha memoria. En aplicaciones muy complejas puede llegar a necesitar más de 2GB de RAM para resolver todas esas dependencias. Por eso es común ejecutarlo como php -d memory_limit=-1 composer ... para permitirle usar memoria ilimitada.

Los responsables de Composer son conscientes de este problema, pero las mejoras para reducir el consumo de memoria no han sido muy significativas en los últimos años. Sin embargo, el pasado 21 de febrero, un programador español llamado Rubén González creaba este pull request prometiendo reducir el consumo de memoria de Composer hasta un 30%.

Tras el correspondiente proceso de revisión y verificación, el pull request ha sido aceptado y la próxima versión de Composer ya incluirá esta mejora. Las comparaciones de rendimiento definitivas son:

Proyecto Memoria Antes Memoria Después
Sylius 983 MB 539 MB
Symfony Demo 699 MB 383 MB
Proyecto Symfony complejo 1.000 MB 536 MB

Nos hemos puesto en contacto con Rubén para conocer más detalles sobre su gran trabajo y el ha accedido amablemente a responder a nuestras preguntas:

Para empezar, cuéntanos un poco sobre ti

Me llamó Rubén González (@rubenrua) y soy un teleco de Vigo que actualmente trabaja en Teltek Video Research, empresa tecnológica dedicada a la innovación en soluciones abiertas de video educativo.

Si digo que hace un par de años estaba loco con Scala y ahora intento aprender todo lo que encuentro sobre Machine Learning os podéis imaginar un poco como soy. Llevo trabajando con Symfony desde la versión 1.0 y tengo un cariño especial por este framework y su comunidad.

Para los que no conocemos los detalles internos de Composer, ¿nos podrías explicar las claves de tu solución para reducir el consumo de memoria?

Para entender las claves, hay que unir los puntos que me llevaron a este resultado:

  1. He usado blackfire.io varias veces para conseguir resultados increíbles en pequeños proyectos locales.
  2. Le prometí a la gente de PHPVigo que iba a dar una charla de blackfire.io para una de sus reuniones.
  3. Un día, el responsable de sistema de Teltek, mi amigo Pablo, se quejó de que Composer usaba mucha memoria en nuestros entornos de staging on premise, y que era difícil justificar al cliente el añadir más memoria. Lo solucionamos usando swap.

Así que, cuando me puse a preparar dicha charla, pensé que estaría bien hacer una pequeña contribución a Composer y contarlo. Pero cuando empezé a realizar profilers con Blackfire y a leer el código fuente de Composer, me di cuenta que no iba a ser fácil. Composer ya está muy optimizado, sobre todo en lo referente a tiempo de respuesta (más info).

Composer usa una clase llamada Rule para resolver las dependencias, con ella indica los paquetes a instalar, borrar, dependencias, conflictos, alias, etc. Para un proyecto de tamaño grande, dicha clase se instancia muchas veces ( sobre 1 millon de veces en Sylius). Es fácil pensar que si optimizas un poco dicha clase, el resultado final será grande, pero la clase ya estaba muy optimizada para no usar memoria; por ejemplo usando máscara de bits para minimizar el tamaño de su estado. Para explicar mi optimizacion la clase Rule se puede resumir en:

class Rule
{
    const BITFIELD_TYPE = 0;
    const BITFIELD_REASON = 8;
    const BITFIELD_DISABLED = 16;
 
    public $literals;
    public function __construct(array $literals, $reason, $reasonData)
    {
        $this->literals = $literals; // un array de integers
        $this->reasonData = $reasonData; // referencia al objeto origen de la regla
 
        // máscara de bits para el estado
        $this->bitfield = (0 << self::BITFIELD_DISABLED) |
            ($reason << self::BITFIELD_REASON) |
            (255 << self::BITFIELD_TYPE);
    }
}

¿Cómo llegaste a descubrir esa solución?

Llevando a mi hija a la guardería, después de estar toda la noche poniéndome al día en un curso de ML, tuve una idea feliz: ver la distribución del tamaño de literals. Y ¡bingo! La mayoría de las reglas eran conflictos (80%), cuyo array solo tenía dos elementos. Si un array ocupa sobre 200 bytes, con una pequeña refactorización podía reducir 1.000.000 * 0,8 * 200 bytes (sobre 160MB) (más info a bajo nivel),

class ConflictRule
{
    public $literal1;
    public $literal2;
    public function __construct($literal1, $literal2)
    {
        $this->literal1 = $literal1;
        $this->literal2 = $literal2;
 
        // ...
    }
}

Esta primera mejora se puede ver en mi pimer commit

Después de hacer un pull request con está mejora, encontré otra mejora decrementando el uso de arrays en el proceso de eliminación de las reglas duplicadas. Las reglas se agrupan en otra clase llamada RuleSet donde se eliminan los duplicados de forma muy óptima en tiempo, pero no tanto en memoria.

Para eliminar los duplicados se genera un hash de cada regla y para eliminarlos sólo se comparan la reglas que tienen el mismo hash (denominados duplicados potenciales). Con el diseño realizado, la probabilidad de que dos reglas tengan el mismo hash y sean distintas es muy pequeña, y de esta probabilidad obtuve la segunda mejora. Sólo tuve que postponer la creación del array donde se almacenan los duplicados potenciales hasta que aparezca el primer duplicado potencial.

Esta segunda mejora se puede ver en mi segundo commit

¿Crees que todavía hay un margen de mejora significativo para seguir reduciendo el consumo de memoria de Composer, o estamos acercándonos al límite?

Composer es una herramienta que ya está muy optimizada y será difícil mejorarla. Dicho esto, tampoco creo que sea imposible hacer pequeñas mejoras.

Tras las pruebas realizadas con Blackfire, la otra parte del código que más memoria consume es procesar los datos del servidor (ver json_decode). A lo mejor se pueden arañar unos pocos MB en esa parte.

Por otro lado, no me cuesta nada pensar en una extensión de PHP que mejore exponencialmente el performance de composer, o incluso una implementación de composer en otro lenguaje más óptimo para este tipo de herramientas (como Golang, lenguaje que empieza a usar públicamente Fabien Potencier)

¿Cómo ha sido el proceso de contribución a un proyecto tan importante como Composer?

En mi día a día toco muchas tecnologías diferentes y aunque critico mucho a PHP como lenguaje de programación, tengo que reconocer que mi experiencia realizando contribuciones a Symfony siempre han sido muy positivas. Recuerdo con alegría mi primera contribución, y eso que sólo eran la traducciones a Gallego de los mensajes de validación. En este caso, con Composer, mi experiencia contribuyendo también fue muy buena, sólo que tuvo mucha más repercusión.

Una de las ventajas que te aporta contribuir a estos proyectos es el feedback que recibes. Aprendiendo siempre cosas nuevas.

Muchas veces veo a programadores que tienen respeto por hacer contribuciones a proyectos libres y no lo entiendo. Siempre que se realiza una contribución hay que ponerse en el papel del que la recibe, e intentar hacer su vida lo más fácil posible. Pero la comunidad te devuelve con creces los que les das.

Y lo que digo siempre a los jóvenes programadores, a día de hoy no hay mejor CV para un programador que GitHub.

¿Cuál ha sido el papel de la comunidad PHPVigo en tu contribución?

Como ya dije PHPVigo fue el detonante de esta contribución. Felix y Sergio están haciendo un gran trabajo y es difícil no aprender algo nuevo en cada meetup. Y lo más importante, como dice David Bonilla, "La gente no acude a una charla para aprender, sino para encontrar inspiración y motivación."

Vigo se está moviendo mucho a nivel TIC y a día de hoy tenemos comunidades de todo: PHPVigo, PythonVigo, VigoLabs, JUGVigo, GDGVigo, AgileVigo, SysAdsminGalicias, etc. Es algo muy bueno para la ciudad.

Me gustan mucho las citas y me gustaría cerrar la entrevista con una que describe muy bien mi experiencia con el mundo del software y la optimización:

You can optimise for execution speed. You can optimise for space. But the most precious thing you should optimise for is understandability

Comentarios

Publicada el

27 de febrero de 2017

Proyectos Symfony destacados

La forma más sencilla de generar el backend de tus aplicaciones Symfony. Ver más

Síguenos en @symfony_es para acceder a las últimas noticias.