En Pinterest desarrollamos compilaciones para iOS rápidas y confiables

Por Rahul Malik | Líder de tecnología de iOS Platform (Plataforma para iOS)

En Pinterest nos enfocamos en ayudar a las personas a descubrir ideas inspiradoras, desde recetas para una cena, hasta productos de estilo y para el hogar que comprar y lugares a los que viajar. Para ello, es fundamental crear los mejores productos para dispositivos móviles, ya que el 80% de los usuarios acceden a Pinterest a través de aplicaciones móviles. En el equipo de iOS en concreto, trabajamos constantemente para mejorar esa experiencia de la manera más eficiente y rápida posible, y ofrecer a nuestro equipo el mejor entorno de desarrollo y pruebas es un paso clave para ello.

Recientemente analizamos las formas de optimizar ese proceso y nos propusimos mejorar la velocidad y la confiabilidad de nuestras compilaciones para iOS en entornos de integración local y continua. Además, comenzamos a modularizar nuestra aplicación en marcos independientes y necesitábamos un sistema para respaldar dicha migración. Analizamos varias herramientas, como Xcode, Cocoapods, Buck y Bazel. Queríamos implementar una base más estable para el futuro, que es fundamental para que podamos iterar rápidamente y lanzar nuevas funciones para los usuarios.

Después de comparar Xcode, Cocoapods, Buck y Bazel, nos dimos cuenta de que Bazel era la mejor opción para nuestros objetivos: construir una base para una mejora significativa del rendimiento, eliminar la variabilidad en los entornos de compilación y adoptarlo de forma incremental. Como resultado, ahora enviamos todas nuestras versiones de iOS usando Bazel, lo que generó importantes logros como los siguientes:

Desarrollo local

● Compilaciones más rápidas: se redujo el tiempo de compilación limpia de 4 min 38 s a 3 min 38 s, lo que representa una mejora del 21%.

● La caché de disco local permite hacer recompilaciones instantáneas de cualquier cosa que hayas compilado antes (otras ramas, confirmaciones, etc.).

● Los entornos de integración continua y local son idénticos, por lo que los problemas de compilación son fáciles de reproducir.

● Mayor automatización: las tareas como la generación de código se incluyen como parte del gráfico de compilación.

Integración continua

● Cada compilación es una compilación incremental: como Bazel es reproducible, no realizamos ni una compilación limpia en integración continua en más de un año.

● Compilar una vez y reutilizar en todas partes: después de introducir el almacenamiento en caché con compilación remota, los tiempos de compilación disminuyeron a menos de un minuto e incluso a tan solo 30 segundos, ya que no necesitamos recompilar nada que se haya compilado en cualquier otra máquina.

● Reducción del tiempo para generar el código: el tiempo de compilación se redujo de 10 min 24 s a 7 min 34 s, lo que representa una mejora del 27%.

● Reducción del tiempo para que los cambios lleguen a los evaluadores de la versión beta: el tiempo de compilación de la versión beta se redujo de 14 min 32 s a 7 min 52 s, lo que representa una mejora del 45%.

● Ejecución de pruebas más rápida: las ejecuciones de pruebas son instantáneas si el código modificado no afecta la prueba.

● Mayor tasa de éxito de compilación: la tasa de éxito de las compilaciones mejoró de alrededor del 80% al 97%-100% al ejecutar tareas de compilación con Bazel.

Hacia un futuro de compilaciones rápidas y confiables

La velocidad de compilación es un cuello de botella constante para los desarrolladores, ya que usamos lenguajes compilados (Objective-C/C++). Pero es difícil cuantificar la velocidad de compilación. Incluye compilaciones en diferentes entornos, como la integración continua o el desarrollo local. También trabajamos con diferentes escenarios de flujo de trabajo, como compilaciones limpias, compilaciones incrementales, conmutación de ramas, reorganización, reversión de cambios y más. No puedes mejorar aquello que no mides, por lo que mejorar la velocidad de compilación requiere hacer un seguimiento de diferentes escenarios que nos permitan identificar las regresiones y enfocar nuestros esfuerzos de rendimiento.

Podemos hacer que las compilaciones sean más rápidas combinando menos trabajo o trabajo más eficiente. Esto puede implicar el uso de diferentes herramientas, la mejora de la paralelización o la actualización de la arquitectura del proyecto para que requiera menos archivos fuente. Contar con prácticas sólidas en torno al mantenimiento de una arquitectura modular y la limpieza de código muerto al que no se hace referencia o que está relacionado con experimentos completados ayudará a mantener o mejorar los tiempos de compilación. Usamos diferentes herramientas y scripts internos para identificar el código muerto. Para los experimentos, usamos una automatización que agrega anotaciones de clang para dejar en desuso los métodos y las constantes que están relacionados con el experimento, lo que permite que el compilador advierta a los desarrolladores que el experimento finalizó y se debe eliminar el código. Los desarrolladores identifican el código sin referencias según sea necesario mediante la ejecución periódica de herramientas que inspeccionan que el encabezado incluya el gráfico de nuestra compilación y buscan archivos que no tienen referencias recursivamente.

Nuestro proceso de compilación debe ser rápido y confiable. Las compilaciones confiables son aquellas que son reproducibles. Las compilaciones reproducibles son importantes no solo para reproducir los errores, sino también para garantizar que enviemos la versión exacta de la aplicación que desarrollamos y probamos. Solo podemos lograrlo si el entorno de compilación (las entradas y las salidas) son consistentes.

Los cambios en el entorno pueden afectar significativamente el producto final y generar variabilidad. Un entorno consistente garantiza que la aplicación se comporte de la misma manera independientemente de si se creó en la máquina de un desarrollador o mediante la integración continua, y elimina el tiempo destinado a averiguar por qué una compilación tiene éxito en un entorno pero fracasa en los demás.

Si bien las ideas y las exploraciones se centran en iOS, lograr compilaciones rápidas y reproducibles es un objetivo que todos compartimos y nos permitirá escalar la ingeniería del cliente.

Desafíos

La decisión de enfocarnos en mejorar nuestro proceso de compilación se basó en el impacto que estaba teniendo en la productividad de los desarrolladores. A medida que nuestro equipo y nuestro producto crecen, es fundamental que invirtamos en la capacidad de los desarrolladores para trabajar con un sistema de compilación consistente y rápido.

● Escala: a medida que escalamos la ingeniería del cliente, también aumenta la cantidad de tiempo destinada a apoyar a los desarrolladores, mantener o reducir los tiempos de compilación y mejorar la confiabilidad. La cantidad de ingenieros que ayudan a los desarrolladores no necesariamente aumenta de forma proporcional con la cantidad de desarrolladores, y Xcode no contiene herramientas para perfilar las compilaciones cuando el rendimiento se degrada.

● Arquitectura modular: comenzamos a refactorizar los marcos centrales que componen nuestra plataforma desde nuestra aplicación para mejorar nuestra arquitectura, documentación y calidad en general. Esto suma complejidad, ya que requiere un sistema de compilación que pueda administrar un gráfico de dependencia de los destinos de compilación que deben configurarse y compilarse en un orden específico. Si bien no es imposible lograrlo en Xcode, la configuración y el mantenimiento de dicho gráfico sería sumamente difícil de mantener a lo largo del tiempo debido a la falta de una API de configuración expresiva.

● Inestabilidad de compilación: por fuera de nuestra base de código, hay varias herramientas escritas en distintos lenguajes (Ruby, Python, Bash, etc.) que requieren versiones y cadenas de herramientas específicas que deben ser idénticas para crear compilaciones consistentes. Estas versiones pueden causar errores que son difíciles de reproducir de manera confiable. Para los desarrolladores, no era raro que una compilación fuera aprobada a nivel local, pero fallara en la integración continua, y viceversa. Solo ciertas máquinas cumplían los requisitos para crear una versión candidata para lanzamiento. El estado local puede corromperse, lo que requiere que se realicen compilaciones limpias. Eso es una pérdida de tiempo.

● Automatización de tareas y generación de código: confiamos en la generación de código para crear nuestros modelos inmutables (a través de Plank) e infraestructura de registro (mediante Thrift). Si bien Xcode admite la ejecución de fases de script, no puede introducir flujos de trabajo dinámicos como la generación de código o la automatización de tareas generales para que sean parte del proceso de compilación, y en su lugar requiere la integración manual de las fuentes generadas, lo que implica más trabajo para los desarrolladores y capacitación para la incorporación. También requiere agregar artefactos generados al control de versiones, lo que incrementa el tamaño de nuestro repositorio y el rendimiento de git clone.

● Recursos compartidos: la ruta de integración de los repositorios externos nunca ha sido clara e, históricamente, generó la copia periódica de recursos de otros repositorios. Analizamos opciones como git subtree o git submodule, pero esto requería una mayor inversión en la capacitación de los empleados y un cambio en los flujos de trabajo de los desarrolladores. Eso generó confusión y también significó una pérdida de tiempo. Xcode no permite declarar dependencias de compilación externas, por lo que tendríamos que depender de herramientas externas para ofrecer esta integración.

Soluciones

Buscábamos soluciones que nos permitieran superar estos desafíos con herramientas y automatización en lugar de aumentar la carga de capacitación y proceso de los desarrolladores, y perder menos tiempo. Optimizamos principalmente para lo siguiente:

● Iteración rápida: nuestra solución debe ofrecer funcionalidad para mejorar significativamente y mantener la velocidad de compilación y de los desarrolladores a lo largo del tiempo, que probablemente se logre a través de un mejor paralelismo y funciones avanzadas en las herramientas.

● Desarrollo en espacio aislado: un entorno consistente que nos permita tener compilaciones confiables y minimice la variabilidad y el impacto en la productividad de los desarrolladores.

● Desarrollo de tipo Monorepo: todas las fuentes deben permanecer en un repositorio. Esto minimiza la cantidad de trabajo y el cambio de contexto necesarios para hacer modificaciones en la aplicación.

● Perfilado, monitorización y análisis: necesitamos contar con herramientas que nos brinden información sobre nuestro sistema de compilación para identificar problemas. Nuestra solución debe permitirnos visualizar las acciones realizadas en toda la compilación y sus respectivas duraciones. Suponiendo que contamos con esto, podremos hacer un seguimiento frecuente de los cambios detallados.

● Compilación incremental: en cuanto compilamos el cliente una vez, deberíamos poder compilar de forma incremental y segura a través de todos los flujos de trabajo. Eso debería incluir la conmutación de ramas, la reversión de cambios u otras etapas del flujo de trabajo. Las compilaciones limpias son, por lejos, las compilaciones más caras y suelen realizarse cuando el estado local está dañado o el desarrollador intenta diagnosticar un problema de compilación desconocido.

Extensible para el futuro

A medida que nuestra aplicación se vuelve más compleja y nuestras necesidades evolucionan, debemos asegurarnos de contar con suficiente extensibilidad en nuestro sistema de compilación para permitir que se introduzcan cambios. Pero no debe ser demasiado específico como para obstaculizar una automatización más dinámica en el proceso de compilación. Esto puede abarcar desde poder automatizar tareas para integrar análisis estadísticos de terceros, cadenas de herramientas personalizadas y las herramientas internas que desarrollamos en Pinterest.

Modificar un sistema de compilación es un cambio significativo, y no podemos apostar por un enfoque que no pueda introducirse de forma incremental. Una solución de todo o nada podría requerir pausar el desarrollo o mantener una bifurcación duradera y realizar una migración arriesgada de forma atómica entre los entornos del desarrollador y los sistemas de integración continua.

Estamos construyendo el primer motor de descubrimiento visual del mundo. Más de 475 millones de personas de todo el mundo usan Pinterest para soñar, planear y preparar lo que quieren hacer en la vida. ¡Únete a nuestro equipo!


En Pinterest desarrollamos compilaciones para iOS rápidas y confiables was originally published in Pinterest Engineering Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Source: Pinterest

Leave a Reply

Your email address will not be published.


*