Hasta ahora, el consumo de memoria de librsvg no ha sido un problema para GNOME, pues ahí se usa casi sólo para renderear iconitos. Pero para SVGs con miles de elementos, podría comportarse mucho mejor.
El consumo de memoria en el DOM
Librsvg y los navegadores de web atacan más o menos el mismo problema:
tienen que construir un árbol DOM en memoria con los elementos del
SVG, y mantener un montón de información para cada nodo del árbol.
Por ejemplo, cada elemento de un SVG puede tener un atributo id
, o
class
; cada elemento tiene una matriz de transformación; etc.
Además de los metadatos de los nodos del árbol (punteros a los nodos hermanos y padre), cada nodo tiene lo siguiente:
/// Contenido de un nodo del árbol
pub struct NodeData {
node_type: NodeType,
element_name: QualName,
id: Option<String>, // atributo "id" del elemento XML
class: Option<String>, // atributo "class" del elemento XML
specified_values: SpecifiedValues,
important_styles: HashSet<QualName>,
result: NodeResult,
transform: Transform,
values: ComputedValues,
cond: bool,
style_attr: String,
node_impl: Box<dyn NodeTrait>, // estructura concreta para los tipos de nodos
}
En una compu de 64 bits, ese struct NodeData
ocupa 1808 bytes. Los
campos más grandes son los de tipo SpecifiedValues
(824 bytes) y
ComputedValues
(704 bytes).
Librsvg representa todos los nodos del árbol con ese struct. Por ejemplo, veamos un SVG como éste:
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<rect x="10" y="20"/>
<path d="..."/>
<text x="10" y="20">Hola</text>
<!-- etc -->
</svg>
Ahí hay 4 elementos. Sin embargo, también hay nodos del árbol para
los nodos de texto del XML, es decir, el espacio en blanco entre las
etiquetas y el "Hola
" adentro del elemento <text>
.
El contenido de cada uno de esos nodos de texto es pequeño (un
caracter de nueva línea y un par de espacios), pero cada nodo sigue
ocupando por lo menos 1808 bytes del struct NodeData
, además del
tamaño de la cadena de texto.
Vamos a refactorizar esto para hacer más fácil quitar el espacio sobrante.
Primer paso: separar los nodos de texto de los nodos para elementos
En las entrañas de librsvg, los nodos de texto del XML se representan
con un struct NodeChars
, que es una cadena de texto con
un par de cosillas más. Todos los structs que se usan para
implementar tipos concretos de nodos en el árbol deben implementar el
trait NodeTrait
, y NodeChars
no es la excepción:
pub struct NodeChars {
// una cadena con el contenido del nodo de texto
}
impl NodeTrait for NodeChars {
// una impl más bien vacía porque el texto no hace nada
}
Aunque no se ve en la definición de NodeData
en la sección anterior,
un nodo de texto pondría el NodeChars
en el campo
NodeData.node_impl
. El NodeChars
quedaría en un bloque de memoria
aparte, en el heap, gracias al Box
— puede hacer eso porque como
NodeChars
implementa el trait NodeTrait
, entonces puede ir dentro
del campo node_impl: Box<dyn NodeTrait>
.
Lo que hice primero fue convertir el struct NodeData
en un enum con
dos variantes, y mover todos sus campos anteriores a un struct
Element
nuevo:
// Éste es nuevo
pub enum NodeData {
Element(Element),
Text(NodeChars),
}
// Éste es el struct viejo, pero con un nombre nuevo
pub enum Element {
node_type: NodeType,
element_name: QualName,
id: Option<String>,
class: Option<String>,
specified_values: SpecifiedValues,
important_styles: HashSet<QualName>,
result: NodeResult,
transform: Transform,
values: ComputedValues,
cond: bool,
style_attr: String,
node_impl: Box<dyn NodeTrait>,
}
El tamaño de un enum en Rust es el máximo de los tamaños de sus variantes, más un entero extra para el discriminante del enum (puedes pensar en un struct en C con un int para el discriminante, y una unión de variantes).
El código requirió varios
cambios
para separar el NodeData
de esta manera; me sirvió ponerle funciones
de ayuda para accesar las variantes Element
o Text
sin que el
código que usa el enum supiera de su representación interna. Éste es
el tipo de refactorización en el que cambias una declaración, y te
puedes ayudar de los errores del compilador para cambiar cada uno de
los casos necesarios en el resto del código.
Segundo paso: mover la variante Element a un bloque de memoria separado
Ahora, convertimos el NodeData
en
esto:
pub enum NodeData {
Element(Box<Element>), // Esto va dentro de un Box
Text(NodeChars),
}
Así, la variante Element
acaba siendo sólo del tamaño de un puntero
(i.e. un puntero al Box que vive en el heap), y la variante Text
se
mantiene del tamaño de un NodeChars
igual que antes.
Esto significa que los nodos para un Element
son igual de grandes en
la memoria que como eran antes, más un puntero adicional, más un
bloque extra en el heap.
¡Sin embargo, los nodos Text
son mucho más pequeños!
- Antes:
sizeof::<NodeData>() = 1808
- Después:
sizeof::<NodeData>() = 72
Al hacer la variante Element
mucho más pequeña (con el tamaño de un
Box
, que es sólo un puntero), no excede de tamaño a la variante Text
.
Esto quiere decir que para el archivo SVG, todo el espacio en blanco entre las etiquetas de XML ahora ocupa mucho menos memoria.
Mediciones de un archivo patológico
El bug 42 se
refiere a un archivo SVG que sólo tiene un elemento use
repetido
muchísimas veces, uno por línea:
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<symbol id="glyph0-0">
<!-- algunos elementos aquí -->
</symbol>
</defs>
<use xlink:href="#glyph0-0" x="1" y="10"/>
<use xlink:href="#glyph0-0" x="1" y="10"/>
<use xlink:href="#glyph0-0" x="1" y="10"/>
<!-- unas 196,000 líneas de lo mismo -->
</svg>
Entonces tenemos unos 196,000 elementos. De acuerdo a la herramienta
Massif de Valgrind, esto hace que rsvg-convert
pida
800,501,568 bytes de memoria en la versión vieja, a comparación de
463,412,720 bytes en la versión nueva, o 60% del espacio original.
Qué sigue por hacer
Los nodos de texto de un SVG promedio son muy repetitivos. Por ejemplo, en el archivo patológico de la sección anterior, casi todo el espacio en blanco es idéntico: entre cada elemento hay un caracter de nueva línea y dos espacios. En vez de tener miles de bloquecitos de memoria con cadenas de texto idénticas, podría haber un conjunto de cadenas compartidas. Los archivos SVG con indentación "de deveras" podrían beneficiarse de compartir los nodos de indentación similar.
Los motores de web de deveras son muy cuidadosos y comparten las estructuras de estilos entre varios elementos del SVG o del HTML. Busca "style struct sharing" en "Inside a super fast CSS engine: Quantum CSS". Esto implica una buena cantidad de trabajo para librsvg, pero podemos llegar a ello poco a poco.
Referencias
- Commits para la refactorización completa
- Convertir el struct NodeData en un enum con variantes para Element y Text
- Poner la variante Element dentro de un Box para hacer los nodos de Texto más pequeños. El mensaje del commit tiene partes de la salida de massif, con todos los números interesantes.
- Bug para monitorear el consumo de memoria