Análisis detallado de RenderingNG: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

Soy Ian Kilpatrick líder de ingeniería del equipo de diseño Blink, junto con Koji Ishii. Antes de trabajar en el equipo de Blink, Fui ingeniero frontend (antes de que Google tuviera el rol de "ingeniero frontend"), creando funciones en Documentos de Google, Drive y Gmail. Después de 5 años en ese cargo, tomé la apuesta y cambié al equipo de Blink aprendiendo C++ de forma efectiva en el trabajo, y trataba de aumentar la compleja base de código Blink. Incluso hoy en día, solo entiendo una porción relativamente pequeña. Agradezco por el tiempo que se me ha dado durante este período. Me convenció el hecho de que muchas de "recuperar ingenieros front-end" se convirtió en "ingeniero de navegadores" antes que yo.

Mi experiencia previa me guió personalmente cuando formo parte del equipo de Blink. Como ingeniero front-end, constantemente me encontré con inconsistencias del navegador, problemas de rendimiento, errores de renderización y funciones faltantes. LayoutNG me brindó la oportunidad de ayudar a solucionar sistemáticamente estos problemas en el sistema de diseño de Blink. y representa la suma de los ingenieros a lo largo de los años.

En esta publicación, explicaré cómo un gran cambio en la arquitectura como este puede reducir y mitigar varios tipos de errores y problemas de rendimiento.

Vista de 9,000 metros de las arquitecturas de los motores de diseño

Antes, el árbol de diseño de Blink era lo que llamaré "árbol mutable".

Muestra el árbol como se describe en el siguiente texto.

Cada objeto del árbol de diseño contenía información de input. como el tamaño disponible que impone un padre, la posición de los números de punto flotante y la información de resultado, por ejemplo, el ancho y la altura finales del objeto o su posición en "x" y "y".

Estos objetos se mantuvieron entre las renderizaciones. Cuando ocurría un cambio en el estilo, marcamos ese objeto como sucio y también todos sus elementos superiores en el árbol. Cuando se ejecutó la fase de diseño de la canalización de renderización, limpiamos el árbol, recorremos los objetos sucios y ejecutamos un diseño para que queden limpios.

Descubrimos que esta arquitectura generó muchas clases de problemas que describiremos a continuación. Pero, primero, repasemos y consideremos cuáles son las entradas y salidas del diseño.

La ejecución del diseño en un nodo de este árbol toma conceptualmente los atributos "Style más DOM", y cualquier restricción superior del sistema de diseño principal (cuadrícula, bloque o flexible) ejecuta el algoritmo de restricción de diseño y produce un resultado.

El modelo conceptual descrito anteriormente.

Nuestra nueva arquitectura formaliza este modelo conceptual. Seguimos conservando el árbol de diseño, pero lo usamos principalmente para retener las entradas y salidas del diseño. Para el resultado, generamos un objeto immutable completamente nuevo llamado immutable.

El árbol de fragmentos

Ya analizamos las árbol de fragmentos inmutable del tipo que describe cómo está diseñado para reutilizar grandes porciones del árbol anterior para diseños incrementales.

Además, se almacena el objeto de restricciones superior que generó ese fragmento. Usamos esto como una clave de caché que analizaremos más adelante.

El algoritmo de diseño intercalado (texto) también se reescribe para que coincida con la nueva arquitectura inmutable. No solo produce representación inmutable de lista plana para un diseño intercalado, pero también ofrece almacenamiento en caché a nivel de párrafo para un rediseño más rápido. forma por párrafo para aplicar características de fuente en elementos y palabras, un nuevo algoritmo bidireccional de Unicode que usa ICU, muchas correcciones y mucho más.

Tipos de errores de diseño

En términos generales, los errores de diseño se dividen en cuatro categorías diferentes: cada uno con diferentes causas raíz.

Precisión

Cuando pensamos en errores en el sistema de renderización, pensamos en la precisión por ejemplo: "El navegador A tiene un comportamiento X, mientras que el navegador B tiene el comportamiento Y". o “Los navegadores A y B están rotos”. Antes, a esto invertimos mucho tiempo y, en el proceso, luchamos constantemente con el sistema. Un modo de falla común era aplicar una corrección específica para un error pero, semanas después, hemos causado una regresión en otra parte (aparentemente no relacionada) del sistema.

Como se describió en las publicaciones anteriores, esto es una señal de un sistema muy frágil. Específicamente, para el diseño, no teníamos un contrato limpio entre ninguna clase, lo que provoca que los ingenieros de navegadores dependan del estado en el que no deberían o malinterprete algún valor de otra parte del sistema.

Por ejemplo, en un momento, tuvimos una cadena de aproximadamente 10 errores en el transcurso de más de un año relacionadas con el diseño flexible. Cada corrección provocó un problema de corrección o rendimiento en una parte del sistema lo que lleva a otro error.

Ahora que LayoutNG define claramente el contrato entre todos los componentes del sistema de diseño, hemos descubierto que podemos aplicar los cambios con mucha más confianza. También nos beneficiamos enormemente del excelente proyecto de Pruebas de plataforma web (WPT), lo que permite que varias partes contribuyan a un paquete de pruebas web común.

Hoy descubrimos que si lanzamos una regresión real en nuestro canal estable, no suele tener pruebas asociadas en el repositorio de WPT y no sea el resultado de un malentendido de los contratos de componentes. Además, como parte de nuestra política de corrección de errores, siempre agregamos una nueva prueba WPT, lo que ayuda a garantizar que ningún navegador vuelva a cometer el mismo error.

Invalidación inferior

Si alguna vez tuviste un error misterioso en el que cambiar el tamaño de la ventana del navegador o activar o desactivar una propiedad de CSS, por arte de magia, hace que el error desaparezca. has encontrado un problema de invalidación insuficiente. De hecho, una parte del árbol mutable se consideró limpia, pero, debido a algún cambio en las restricciones superiores, no representaba el resultado correcto.

Esto es muy común en el modelo de dos pasos, (recorrer el árbol de diseño dos veces para determinar el estado final del diseño) que se describen a continuación. Antes, nuestro código se veía así:

if (/* some very complicated statement */) {
  child->ForceLayout();
}

Por lo general, una solución para este tipo de error sería la siguiente:

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

Una solución para este tipo de problema podría causar una regresión de rendimiento grave (consulta sobre invalidación a continuación) y fue muy delicado para corregir la situación.

En la actualidad (como se describió anteriormente), tenemos un objeto de restricciones superior inmutable que describe todas las entradas desde el diseño de nivel superior hasta el secundario. Almacenamos esto con el fragmento inmutable resultante. Debido a esto, tenemos un lugar centralizado en el que diferenciamos estas dos entradas para determinar si el elemento secundario necesita que se realice otro pase de diseño. Esta lógica de diffing es complicada, pero está bien contenida. La depuración de esta clase de problemas de invalidación insuficiente suele dar como resultado una inspección manual de las dos entradas y decidir qué cambió en la entrada de modo que se requiera otro pase de diseño.

Las correcciones de este código de diffing suelen ser simples y es fácil realizar pruebas de unidades debido a lo fácil que es crear estos objetos independientes.

Comparar una imagen de ancho fijo y un porcentaje de ancho
A un elemento de ancho o altura fijos no le importa si el tamaño disponible asignado aumenta. Sin embargo, a un elemento de ancho o altura basado en porcentajes sí lo hace. El valor de available-size se representa en el objeto Parent Constraints y, como parte del algoritmo de diffing, se realizará esta optimización.

El código de diffing para el ejemplo anterior es el siguiente:

if (width.IsPercent()) {
  if (old_constraints.WidthPercentageSize() 
    != new_constraints.WidthPercentageSize())
   return kNeedsLayout;
}
if (height.IsPercent()) {
  if (old_constraints.HeightPercentageSize() 
    != new_constraints.HeightPercentageSize())
   return kNeedsLayout;
}

Histéesis

Esta clase de errores es similar a la invalidación insuficiente. Básicamente, en el sistema anterior era increíblemente difícil garantizar que el diseño fuera idempotente, es decir, volver a ejecutar el diseño con las mismas entradas y obtener el mismo resultado.

En el siguiente ejemplo, simplemente cambiamos una propiedad de CSS entre dos valores. Sin embargo, esto da como resultado un "crecimiento infinito" rectángulo.

. En el video y la demostración, se muestra un error de histésis en Chrome 92 y versiones anteriores. Se corrigió en Chrome 93.

Con nuestro árbol mutable anterior, fue muy fácil introducir errores como este. Si el código cometió el error de leer el tamaño o la posición de un objeto en el momento o la etapa incorrectos (por ejemplo, porque no "borramos" el tamaño o la posición anteriores), agregamos inmediatamente un error de histésis sutil. Por lo general, estos errores no aparecen en las pruebas, ya que la mayoría de ellas se enfocan en un solo diseño y renderización. Lo más preocupante es que sabíamos que parte de esta histésis era necesaria para que algunos modos de diseño funcionaran correctamente. Teníamos errores en los que debíamos realizar una optimización para quitar un pase de diseño pero introducir un "error" ya que el modo de diseño requería dos pases para obtener el resultado correcto.

Un árbol que muestra los problemas descritos en el texto anterior.
Según la información del resultado de diseño anterior, los resultados son diseños no idempotentes.

Con LayoutNG, dado que tenemos estructuras explícitas de datos de entrada y salida, y no está permitido acceder al estado anterior, mitigamos ampliamente esta clase de error desde el sistema de diseño.

Invalidación excesiva y rendimiento

Esto es lo opuesto directo a la clase de errores de invalidación insuficiente. A menudo, cuando se corrige un error de invalidación insuficiente, se activa un límite de rendimiento.

A menudo, tuvimos que tomar decisiones difíciles para favorecer la precisión sobre el rendimiento. En la siguiente sección, profundizaremos en cómo mitigamos estos tipos de problemas de rendimiento.

Incremento de los diseños de dos pasos y los acantilados de rendimiento

El diseño de flexibilidad y cuadrícula representaron un cambio en la expresividad de los diseños en la Web. Sin embargo, estos algoritmos eran fundamentalmente diferentes del algoritmo de diseño de bloques anterior.

El diseño de bloques (en casi todos los casos) solo requiere que el motor realice el diseño en todos sus elementos secundarios exactamente una vez. Esto es excelente para el rendimiento, pero termina por no ser tan expresivo como quieren los desarrolladores web.

Por ejemplo: Es común que quieras que el tamaño de todos los elementos secundarios se expanda al más grande. Para ello, se usa el diseño de nivel superior (flex o cuadrícula) realizará un pase de medición para determinar el tamaño de cada uno de los elementos secundarios, y, luego, un pase de diseño para estirar todos los elementos secundarios a este tamaño. Este comportamiento es el predeterminado para el diseño flexible y de cuadrícula.

Dos conjuntos de cuadros: el primero muestra el tamaño intrínseco de los cuadros en el pase de medición, el segundo en el diseño con la misma altura.

Estos diseños de dos pases fueron aceptables en cuanto al rendimiento ya que, por lo general, las personas no los anidaban profundamente. Sin embargo, comenzamos a observar problemas de rendimiento significativos a medida que surgía contenido más complejo. Si no almacenas en caché el resultado de la fase de medición el árbol de diseño hará un hiperpaginación entre su estado measure y su estado layout final.

Los diseños de uno, dos y tres pases explicados en la leyenda.
En la imagen de arriba, tenemos tres elementos <div>. Un diseño simple de un solo paso (como el diseño de bloques) visitará tres nodos de diseño (complejidad O(n)). Sin embargo, para un diseño de dos pases (como flex o cuadrícula), lo que podría dar como resultado la complejidad de las visitas de O(2n) en este ejemplo.
Gráfico que muestra el aumento exponencial en el tiempo de diseño.
Esta imagen y demostración muestran un diseño exponencial con diseño de cuadrícula. Esto se corrigió en Chrome 93 como resultado de trasladar Grid a la nueva arquitectura

Antes, intentábamos agregar cachés muy específicas al diseño flexible y de cuadrícula para combatir este tipo de acantilados de rendimiento. Esto funcionó (y llegamos muy lejos con Flex) pero luchaban constantemente con errores de invalidación insuficientes y excesivos.

LayoutNG nos permite crear estructuras de datos explícitas tanto para la entrada como para la salida del diseño. y, además de eso, creamos cachés de los pases de medición y diseño. Esto devuelve la complejidad a O(n), lo que da como resultado un rendimiento lineal predecible para desarrolladores web. Si existe un caso en el que un diseño tenga un diseño de tres pases, también almacenaremos en caché ese pase. Esto puede generar oportunidades para introducir de forma segura modos de diseño más avanzados en el futuro, un ejemplo de cómo RenderingNG fundamentalmente genera extensibilidad en todos los ámbitos. En algunos casos, el diseño de cuadrícula puede requerir diseños de tres pases, pero es muy raro en este momento.

Descubrimos que, cuando los desarrolladores tienen problemas de rendimiento, Por lo general, se debe a un error de tiempo de diseño exponencial en lugar de a la capacidad de procesamiento sin procesar de la etapa de diseño de la canalización. Si un pequeño cambio incremental (un elemento que modifica una sola propiedad CSS) da como resultado un diseño de 50-100 ms, es probable que este sea un error de diseño exponencial.

Resumen

El diseño es un área muy compleja, y no cubrimos todo tipo de detalles interesantes, como las optimizaciones de diseño intercalado (realmente, cómo funciona todo el subsistema intercalado y el subsistema de texto) e incluso los conceptos que aquí se mencionan aquí solo arrancaron y pasamos por alto muchos detalles. Sin embargo, esperamos haber demostrado cómo la mejora sistemática de la arquitectura de un sistema puede generar ganancias extraordinarias a largo plazo.

Sin embargo, sabemos que todavía nos queda mucho trabajo por delante. Sabemos que hay clases de problemas (de rendimiento y de corrección) que estamos trabajando para resolver. y te entusiasma la llegada de nuevas funciones de diseño a CSS. Creemos que la arquitectura de LayoutNG permite resolver estos problemas de forma segura y fácil de resolver.

Una imagen (¡ya sabes cuál!) de Una Kravets.