Los desarrolladores web esperan un impacto mínimo o nulo en el rendimiento cuando depuran su código. Sin embargo, esta expectativa no es universal. Un desarrollador de C++ nunca esperaría que una compilación de depuración de su aplicación alcanzara el rendimiento de producción y, en los primeros años de Chrome, el simple hecho de abrir Herramientas para desarrolladores tuvo un impacto significativo en el rendimiento de la página.
El hecho de que ya no se sienta esta degradación del rendimiento es el resultado de años de inversión en las capacidades de depuración de DevTools y V8. Sin embargo, nunca podremos reducir a cero la sobrecarga de rendimiento de DevTools. Establecer puntos de interrupción, recorrer el código, recopilar seguimientos de pila, capturar un seguimiento de rendimiento, etc., afectan la velocidad de ejecución en diversos grados. Después de todo, observar algo lo cambia.
Pero, por supuesto, la sobrecarga de DevTools, como cualquier depurador, debe ser razonable. Recientemente, notamos un aumento significativo en la cantidad de informes que indicaban que, en algunos casos, DevTools ralentizaba la aplicación hasta el punto de que ya no se podía usar. A continuación, puedes ver una comparación en paralelo del informe chromium:1069425, que ilustra la sobrecarga de rendimiento de tener abiertas las Herramientas para desarrolladores.
Como puedes ver en el video, la ralentización es de alrededor de 5 a 10 veces, lo que es claramente inaceptable. El primer paso fue comprender adónde iba todo el tiempo y qué causaba esta gran ralentización cuando DevTools estaba abierto. El uso de Linux perf en el proceso del renderizador de Chrome reveló la siguiente distribución del tiempo de ejecución general del renderizador:
Si bien esperábamos ver algo relacionado con la recopilación de seguimientos de pila, no esperábamos que alrededor del 90% del tiempo de ejecución general se destinara a simbolizar los marcos de pila. Aquí, la simbolización se refiere al acto de resolver los nombres de las funciones y las posiciones de origen concretas (números de línea y columna en las secuencias de comandos) a partir de marcos de pila sin procesar.
Inferencia de nombres de métodos
Lo más sorprendente es el hecho de que casi todo el tiempo se utiliza en la función JSStackFrame::GetMethodName()
de V8, aunque sabíamos por investigaciones anteriores que JSStackFrame::GetMethodName()
no es ajeno en lo que respecta a problemas de rendimiento. Esta función intenta calcular el nombre del método para los marcos que se consideran invocaciones del método (marcos que representan invocaciones de funciones con el formato obj.func()
en lugar de func()
). Un vistazo rápido al código reveló que funciona realizando un recorrido completo del objeto y su cadena de prototipo, y buscando
- propiedades de datos cuyo
value
es el cierre defunc
- propiedades de acceso en las que
get
oset
sean iguales al cierrefunc
Si bien esto no suena especialmente barato, tampoco parece explicar esta horrible demora. Por lo tanto, comenzamos a analizar el ejemplo informado en chromium:1069425 y descubrimos que los seguimientos de pila se recopilaron para tareas asíncronas y para mensajes de registro que provienen de classes.js
, un archivo JavaScript de 10 MiB. Una mirada más detallada reveló que se trataba de un entorno de ejecución Java más un código de aplicación compilado en JavaScript. Los seguimientos de pila contenían varios fotogramas con métodos que se invocaban en un objeto A
, por lo que pensamos que podría ser conveniente comprender con qué tipo de objeto estamos tratando.
Al parecer, el compilador de Java a JavaScript generó un solo objeto con 82,203 funciones, lo que claramente comenzaba a ser interesante. Luego, volvimos a JSStackFrame::GetMethodName()
de V8 para comprender si hay alguna fruta que pudiéramos elegir allí.
- Para ello, primero busca el
"name"
de la función como una propiedad en el objeto y, si se encuentra, verifica que el valor de la propiedad coincida con la función. - Si la función no tiene nombre o el objeto no tiene una propiedad coincidente, recurre a una búsqueda inversa a través de todas las propiedades del objeto y sus prototipos.
En nuestro ejemplo, todas las funciones son anónimas y tienen propiedades "name"
vacías.
A.SDV = function() {
// ...
};
El primer hallazgo fue que la búsqueda inversa se dividió en dos pasos (se realizó para el objeto en sí y para cada objeto en su cadena de prototipos):
- Extrae los nombres de todas las propiedades enumerables.
- Realiza una búsqueda de propiedades genéricas para cada nombre y prueba si el valor de la propiedad resultante coincide con el cierre que buscábamos.
Eso parecía una solución bastante sencilla, ya que extraer los nombres requiere recorrer todas las propiedades. En lugar de hacer los dos pases (O(N) para la extracción del nombre y O(N log(N)) para las pruebas, podríamos hacer todo en un solo pase y verificar directamente los valores de las propiedades. Eso hizo que toda la función fuera entre 2 y 10 veces más rápida.
El segundo hallazgo fue aún más interesante. Si bien, técnicamente, las funciones eran funciones anónimas, el motor V8 había registrado lo que llamamos un nombre inferido para ellas. En el caso de los literales de función que aparecen en el lado derecho de las asignaciones en el formato obj.foo = function() {...}
, el analizador V8 memoriza "obj.foo"
como nombre inferido para el literal de función. En nuestro caso, eso significa que, si bien no teníamos el nombre correcto que podíamos buscar, teníamos algo lo suficientemente cercano: en el ejemplo de A.SDV = function() {...}
anterior, teníamos "A.SDV"
como nombre inferido y podíamos derivar el nombre de la propiedad del nombre inferido buscando el último punto y, luego, buscando la propiedad "SDV"
en el objeto. Eso hizo el truco en casi todos los casos, reemplazando un costoso recorrido completo con una sola búsqueda de propiedad. Estas dos mejoras se implementaron como parte de esta lista de cambios y redujeron significativamente la demora para el ejemplo informado en chromium:1069425.
Error.stack
Podríamos haber terminado aquí. Pero había algo sospechoso, ya que Herramientas para desarrolladores nunca usa el nombre del método para los marcos de pila. De hecho, la clase v8::StackFrame
en la API de C++ ni siquiera expone una forma de acceder al nombre del método. Por lo tanto, parecía incorrecto que terminaríamos llamando a JSStackFrame::GetMethodName()
en primer lugar. En cambio, el único lugar en el que usamos (y exponemos) el nombre del método es en la API de JavaScript stack trace. Para comprender este uso, considera el siguiente ejemplo simple de error-methodname.js
:
function foo() {
console.log((new Error).stack);
}
var object = {bar: foo};
object.bar();
Aquí tenemos una función foo
que se instaló con el nombre "bar"
en object
. Cuando se ejecuta este fragmento en Chromium, se obtiene el siguiente resultado:
Error
at Object.foo [as bar] (error-methodname.js:2)
at error-methodname.js:6
Aquí vemos la búsqueda de nombres de métodos en juego: se muestra la parte superior de la pila para llamar a la función foo
en una instancia de Object
a través del método llamado bar
. Por lo tanto, la propiedad error.stack
no estándar hace un uso intensivo de JSStackFrame::GetMethodName()
y, de hecho, nuestras pruebas de rendimiento también indican que nuestros cambios hicieron que todo fuera mucho más rápido.
Pero volviendo al tema de las Herramientas para desarrolladores de Chrome, el hecho de que el nombre del método se calcule aunque no se use error.stack
no parece correcto. Aquí hay algunos antecedentes que nos ayudan: Tradicionalmente, V8 tenía dos mecanismos distintos para recopilar y representar un seguimiento de pila para las dos APIs diferentes descritas anteriormente (la API de v8::StackFrame
de C++ y la API de seguimiento de pila de JavaScript). Tener dos maneras diferentes de hacer (más o menos) lo mismo era propenso a errores y, a menudo, generaba inconsistencias y errores, por lo que, a fines de 2018, iniciamos un proyecto para establecer un único cuello de botella para la captura de seguimientos de pila.
Ese proyecto fue un gran éxito y redujo drásticamente la cantidad de problemas relacionados con la recopilación de seguimiento de pila. La mayor parte de la información proporcionada a través de la propiedad error.stack
no estándar también se había calculado de forma diferida y solo cuando realmente era necesario, pero como parte de la refactorización, aplicamos el mismo truco a los objetos v8::StackFrame
. Toda la información acerca del marco de pila se calcula la primera vez que se invoca un método en él.
Por lo general, esto mejora el rendimiento, pero, lamentablemente, resultó ser algo contrario a la forma en que se usan estos objetos de API de C++ en Chromium y DevTools. En particular, como presentamos una nueva clase v8::internal::StackFrameInfo
, que contenía toda la información sobre un marco de pila que se exponía a través de v8::StackFrame
o error.stack
, siempre calculábamos el superconjunto de la información proporcionada por ambas APIs, lo que significaba que, para los usos de v8::StackFrame
(y, en particular, para DevTools), también calculábamos el nombre del método, en cuanto se solicitaba información sobre un marco de pila. Resulta que DevTools siempre solicita información de la fuente y la secuencia de comandos de inmediato.
En función de esa información, pudimos refactorizar y simplificar drásticamente la representación de marcos de pila y hacerla aún más diferida, de modo que los usos en V8 y Chromium ahora solo pagan el costo de procesar la información que solicitan. Esto proporcionó un gran aumento del rendimiento de DevTools y otros casos de uso de Chromium, que solo necesitan una fracción de la información sobre los marcos de pila (esencialmente, solo el nombre de la secuencia de comandos y la ubicación de la fuente en forma de desplazamiento de línea y columna) y abrió la puerta a más mejoras de rendimiento.
Nombres de funciones
Con las refactorizaciones mencionadas anteriormente, la sobrecarga de simbolización (el tiempo empleado en v8_inspector::V8Debugger::symbolize
) se redujo a alrededor del 15% del tiempo de ejecución general, y pudimos ver con mayor claridad dónde V8 pasaba tiempo cuando (recopilaba y) simbolizaba marcos de pila para el consumo en Herramientas para desarrolladores.
Lo primero que destacó fue el costo acumulado para calcular el número de línea y columna. La parte costosa aquí es calcular el desplazamiento de caracteres dentro de la secuencia de comandos (según el desplazamiento de código de bytes que obtenemos de V8) y, debido a nuestra refactorización anterior, lo hicimos dos veces, una cuando calculamos el número de línea y otra vez cuando calculamos el número de columna. Almacenar en caché la posición de origen en instancias de v8::internal::StackFrameInfo
ayudó a resolver este problema rápidamente y eliminó por completo v8::internal::StackFrameInfo::GetColumnNumber
de cualquier perfil.
El hallazgo más interesante para nosotros fue que v8::StackFrame::GetFunctionName
era sorprendentemente alto en todos los perfiles que analizamos. Al profundizar en esto, nos dimos cuenta de que era costoso de forma innecesaria calcular el nombre que mostraríamos para la función en el marco de pila de DevTools.
- primero buscaríamos la propiedad
"displayName"
no estándar y, si eso generara una propiedad de datos con un valor de cadena, la usaríamos. - de lo contrario, busca la propiedad
"name"
estándar y vuelve a verificar si genera una propiedad de datos cuyo valor es una cadena. - y, finalmente, recurre a un nombre de depuración interno que infiere el analizador de V8 y se almacena en el literal de la función.
La propiedad "displayName"
se agregó como solución alternativa para la propiedad "name"
en instancias de Function
que son de solo lectura y no se pueden configurar en JavaScript, pero nunca se estandarizó y no se usó de forma generalizada, ya que las herramientas para desarrolladores del navegador agregaron una inferencia de nombre de función que hace el trabajo en el 99.9% de los casos. Además, ES2015 hizo que la propiedad "name"
en instancias Function
fuera configurable, lo que eliminó por completo la necesidad de una propiedad "displayName"
especial. Dado que la búsqueda negativa de "displayName"
es bastante costosa y no es realmente necesaria (ES2015 se lanzó hace más de cinco años), decidimos quitar la compatibilidad con la propiedad fn.displayName
no estándar de V8 (y DevTools).
Tras la búsqueda negativa de "displayName"
, se quitó la mitad del costo de v8::StackFrame::GetFunctionName
. La otra mitad se dirige a la búsqueda de propiedades "name"
genérica. Por fortuna, ya teníamos cierta lógica para evitar búsquedas costosas de la propiedad "name"
en instancias de Function
(sin tocar), que presentamos en V8 hace un tiempo para que Function.prototype.bind()
fuera más rápido. Portamos las verificaciones necesarias, lo que nos permite omitir la búsqueda genérica costosa en primer lugar, con el resultado de que v8::StackFrame::GetFunctionName
ya no aparece en ningún perfil que hayamos considerado.
Conclusión
Con las mejoras anteriores, hemos reducido significativamente la sobrecarga de Herramientas para desarrolladores en términos de seguimientos de pila.
Sabemos que aún hay varias mejoras posibles, por ejemplo, la sobrecarga cuando se usan objetos MutationObserver
sigue siendo notable, como se informa en chromium:1077657, pero, por el momento, solucionamos los principales problemas y es posible que regresemos en el futuro para optimizar aún más el rendimiento de la depuración.
Descarga los canales de vista previa
Considera usar Chrome Canary, Dev o Beta como tu navegador de desarrollo predeterminado. Estos canales de versión preliminar te brindan acceso a las funciones más recientes de DevTools, te permiten probar las APIs de plataformas web de vanguardia y te ayudan a encontrar problemas en tu sitio antes que tus usuarios.
Comunícate con el equipo de Chrome DevTools
Usa las siguientes opciones para hablar sobre las funciones nuevas, las actualizaciones o cualquier otro tema relacionado con DevTools.
- Envíanos tus comentarios y solicitudes de funciones a crbug.com.
- Para informar un problema de Herramientas para desarrolladores, usa Más opciones > Ayuda > Informar un problema de Herramientas para desarrolladores en Herramientas para desarrolladores.
- Twittea a @ChromeDevTools.
- Deja comentarios en los videos de YouTube sobre las novedades de DevTools o en los videos de YouTube sobre sugerencias de DevTools.