Less.js: La versión 3.10.x usa mucha más memoria y es significativamente más lenta que la 3.9.0

Creado en 12 sept. 2019  ·  95Comentarios  ·  Fuente: less/less.js

Nuestras compilaciones comenzaron a fallar recientemente porque ejecutamos alrededor de 80 compilaciones Less en paralelo durante el proceso de compilación de nuestro proyecto y la nueva versión de Less.js usa tanta memoria que Node falla. Rastreamos el bloqueo hasta la actualización de Less.js de 3.9.0 a 3.10.3.

Cambié nuestro script Less para compilar los archivos secuencialmente (construyendo 2 archivos a la vez) y muestreé el uso de memoria de Node durante el proceso y obtuve los siguientes resultados:

less graph

Less.js parece usar un 130% más de memoria ahora y tarda aproximadamente un 100% más en compilarse para nosotros.

Me pregunto si ha evaluado Less.js y si ve resultados similares

Comentario más útil

¡Bien equipo! Voy a publicar la compilación 3.xy en algún momento más tarde, tal vez la semana que viene, publicaré la 4.0. Ambos deberían tener ahora las correcciones de rendimiento. ¡Gracias a todos los que ayudaron a depurar!

Todos 95 comentarios

Eso es extraño. ¿Cuál es tu versión de Node?

@ PatSmuk360 ¿Podría probar y obtener un perfil de memoria de 3.10.0 y ver si difiere?

Estamos ejecutando la última versión de 10 (10.16.3).

Instantánea del montón antes:

image

Instantánea del montón después de:

image

También probé el Nodo 12.10.0 y parece mucho peor, alcanzando 587 MB de uso de memoria en un punto de la compilación secuencial.

Perfil de CPU antes:
CPU-20190916T133934.cpuprofile.zip

image

Perfil de CPU después:
CPU-20190916T134917.cpuprofile.zip

image

@ PatSmuk360 Entonces, lo largo y lo corto es que la diferencia entre esas versiones es que el código base se transformó a la sintaxis ES6. Lo que técnicamente no es un cambio importante, por lo que no fue una versión principal.

PERO ... mi sospecha es que algunas de las conversiones de Babel para cosas como la sintaxis de extensión de objeto / matriz son menos eficientes que las versiones más detalladas de ES5. Es por eso que preguntaba si podía probar 3.10.0, porque originalmente estaba exportando un paquete transpilado que era compatible con el Nodo 6 en adelante, pero rompió una integración con una biblioteca en particular que no podía manejar la sintaxis de la clase. Así que bajé al Nodo 4 sin construcciones de clase.

Si podemos averiguar exactamente qué transformación de ES5 no está funcionando bien, teóricamente podríamos establecer de manera más granular esas configuraciones de exportación de Babel en una exportación de mayor rendimiento.

@ PatSmuk360 Por cierto, ¿qué es esa función split que está ganando tanto tiempo?

@ matthew-dean Ese parece ser el String.prototype.split . Si abre el perfil en sus herramientas de desarrollo de Chrome, puede ver todos los datos codificados por colores. Estoy tratando de modificar el perfil para vincularlo a https://cdn.jsdelivr.net/npm/[email protected]/dist/less.cjs.js como fuente para que sea más fácil inspeccionar los cuellos de botella. ¿Hay un mapa fuente disponible para el archivo *.cjs.js a *.js ? El archivo https://cdn.jsdelivr.net/npm/[email protected]/dist/less.min.js.map parece asignar el archivo .min a la fuente ES6. Tal vez podamos alimentar el mapa de origen a las herramientas de desarrollo para que podamos averiguar dónde la transpilación causa cuellos de botella.

Esta línea en específico https://github.com/less/less.js/blob/cae5021358a5fca932c32ed071f652403d07def8/lib/less/source-map-output.js#L78 parece tener una gran cantidad de tiempo de CPU. Pero teniendo en cuenta la operación que realiza, no me parece fuera de lugar.

No tengo mucha experiencia con perfiles de montón, pero lo que me llama la atención es el aumento en la cantidad de (closures), (system), (array), system / Context . Intentando vincular eso con el perfil de la CPU, parece que el aumento de estos objetos da como resultado un aumento masivo del tiempo dedicado a la recolección de basura.

@kevinramharak En general, convertir un AST como Less con _n_ depth en un árbol de salida serializado y plano como CSS implica mucha creación de objetos temporales. Entonces, lo que podría estar sucediendo es que con ciertas transformaciones, está agregando una cantidad adicional _x_ de objetos. Incluso temporalmente, puede obtener un efecto exponencial en el que cada nodo crea incluso solo 2-3 veces los objetos, multiplicado por el número de nodos multiplicado por el número de veces que tiene que aplanar las reglas ... pude ver que se suma. Probablemente fuimos ingenuos en general para pensar esencialmente en la sintaxis de ES6 como un azúcar esencialmente sintáctico de la sintaxis de ES5. (Los desarrolladores de JavaScript son probablemente culpables de esto en general). En realidad, la transpilación de una sintaxis más nueva puede crear patrones de ES5 que no son muy eficaces. Para el 99% de los desarrolladores, esto no es un gran problema porque no están iterando a través de ese código cientos o miles de veces por segundo. Pero esta es mi suposición sobre lo que está sucediendo, porque no hubo otros cambios importantes.

@kevinramharak Re: las líneas de origen: esto se debe a que el analizador Less original no rastrea líneas / columnas de entrada, por lo que cuando se agregó el mapeo de origen, era necesario dividir la entrada en líneas para averiguar cómo debería mapear al original fuente. Esto no será un problema en 4.x +, pero tiene sentido por qué pasaría mucho tiempo allí ahora.

Estoy usando less.min.js en un navegador y 3.10.3 es dos veces más lento que 2.7.3 que estaba usando antes. Tanto en Chrome como en Firefox.

@ PatSmuk360 ¿Puedes ver esta rama? https://github.com/matthew-dean/less.js/tree/3.11.0

En resumen, la transpilación de Babel a ES5 es un poco horrible y usa toneladas de llamadas Object.defineProperty para transpilar clases. Cambié la transpilación a TypeScript, que tiene una salida mucho más sana de prototipos de funciones.

Todo esto está bien y es excelente, pero después de hacer esto, las pruebas del navegador de Less no se ejecutarán porque usa el PhantomJS muy antiguo y desactualizado, y hasta ahora, nadie (incluido yo) ha podido migrar con éxito las pruebas fuera de PhantomJS en Headless Chrome.

Pero, un problema a la vez: si la fuente dist en esa rama no tiene los problemas de memoria, entonces tal vez podamos abordar el lío de las pruebas del navegador.

Me las arreglé para migrar menos pruebas de navegador a Headless Chrome, pero en términos de rendimiento / estabilidad, necesito muchos comentarios de los usuarios antes de que sea seguro fusionar, debido a que se cambió la canalización de transpilación por completo de Babel a TypeScript.

Las sucursales actuales se pueden encontrar aquí: https://github.com/less/less.js/pull/3442

Sigue siendo 2 veces más lento que el 2,7.

@alecpl Esa es una buena información, pero realmente quiero saber si 3.11.0 es una mejora con respecto a 3.10.

Lo extraño de esto es que, en teoría, el código Less original se convirtió con Lebab, que debería ser lo opuesto a Babel. Lo que significa que ES5 -> ES6 -> ES5 debería ser casi idéntico, pero obviamente ese no es el caso. Así que tendré que investigar (a menos que alguien más tenga tiempo, que es un apoyo bienvenido) en qué se diferencia el código ES6-> ES5 del código ES5 original.

@alecpl

Entonces, realicé una variedad de pruebas y pasé el tiempo haciendo evaluaciones comparativas de 3.11.0 vs 3.10.3 vs 3.9.0 vs 2.7.3 en Headless Chrome

No he encontrado evidencia de que el compilador Less sea más lento para ninguna versión, y mucho menos 2 veces más lento. Aún podría ser cierto que 3.10.0 tiene más sobrecarga de memoria debido a la configuración de transpilación, y tal vez si un sistema tuviera limitaciones de espacio y realizara más intercambios de memoria o más GC como resultado, ¿podría resultar en una desaceleración? Pero no puedo verificarlo.

Puede probarse ejecutando grunt benchmark en la rama 3.11.0. Pueden informar números diferentes en una ejecución individual, pero si ejecuta suficientes veces, debería ver que los tiempos son aproximadamente iguales. Entonces no sé de dónde obtiene sus datos.

@ PatSmuk360 ¿Ha podido probar la sobrecarga de memoria de 3.11.0?

Yo no construyo su código por mi cuenta. No uso nodejs. Solo tomo el archivo less.min.js de la carpeta dist para dos versiones diferentes que mencioné y las uso en mi página. Luego, en la consola, veo los tiempos impresos con el código Less. Mi código menos usa varios archivos y el archivo de salida es de aproximadamente 100 kB de css minificado. Ese es un punto de referencia de la "vida real". Estoy hablando de código Roundcube .

Chrome es mucho más rápido que Firefox, pero en ambos la diferencia entre versiones es similar.

@alecpl Hmm ... tal vez sea una diferencia particular para ese código Less en particular. Es cierto que el punto de referencia es arbitrario. Si tengo tiempo, agregaré esos archivos Less a la evaluación comparativa.

Este problema se ha marcado automáticamente como obsoleto porque no ha tenido actividad reciente. Se cerrará si no se produce más actividad. Gracias por sus aportaciones.

Solo quería publicar una actualización rápida: probé 3.11.1 y fue tan lento como 3.10.3. Veré si puedo preparar un punto de referencia representativo para probar / perfilar esto.

Me actualicé a 3.11.1 y ahora tengo los mismos problemas de consumo de memoria.
Un proyecto de tamaño modesto requiere ~ 600 MB de RAM para construir a través del paquete web y less-loader .

He tomado un cronograma de asignación de montón que resultó en esto;

heap-timeline

Algo está causando enormes asignaciones que se mantienen vivas por Ruleset .


[EDITAR]
Estoy bajando a 3.9 y tomando una línea de tiempo de asignación de montón de eso. Voy a ver si veo algo radicalmente diferente.

@ matthew-dean
Encontré una pista para ti.

En 3.11, uno de los retenedores enumerados para RuleSet es ImportManager.
En 3.9 este _no es el caso_.

Si todo se mantiene vivo mediante ImportManager e ImportManager es un singleton en todo el proceso de compilación. Bueno, sí; eso inflaría significativamente el uso de la memoria porque no se puede recolectar basura. Ni siquiera conjuntos de reglas intermediarios.

@rjgotten Hmm ... si un objeto tiene una referencia a otro objeto, ¿por qué evitaría eso GC? Quiero decir, técnicamente, todos los objetos conservan referencias a los nodos API públicos a través de la cadena de prototipos. Solo evitaría GC si lo contrario es cierto, es decir, algún objeto que retiene referencias a cada instancia de conjunto de reglas.

Parece haber entendido mal.

Cuando "A es un retenedor de B", significa que A está reteniendo referencias a B que impiden la recolección de basura de B. Entonces, cuando escribí ImportManager aparece como un retenedor de Ruleset, expresé exactamente lo que también concluye: ImportManager contiene referencias a instancias de Ruleset, lo que evita la GC de esas instancias de Ruleset.

@rjgotten Oh, ¿sí? Me pregunto cómo llegó ese cambio. Hay una gran posibilidad de que tenga la culpa, o hay algo en el refactor de ES6 que lo hizo. Pero sí, ¡definitivamente podría hacerlo! ¡Gracias por investigar!

Bien, ¿qué pasa con esta idea radical?

Creé una rama con cherry picking todo EXCEPTO la conversión ES6. Esto no fue simple, y esto destruiría todo ese trabajo, pero si un archivo Babelified / Typecript'd no puede superar al JS nativo existente, entonces simplemente no vale la pena.

No tengo idea de cómo conciliaremos el historial de git, pero aquí está la rama -> https://github.com/less/less.js/tree/release_v3.12.0-RC1. Nuking la conversión es un gran problema, por lo que creo que esta rama tendría una evaluación comparativa realmente sólida para comparar.

Me pregunto cómo llegó ese cambio. Hay una gran posibilidad de que tenga la culpa, o hay algo en el refactor de ES6 que lo hizo. Pero sí, ¡definitivamente podría hacerlo! ¡Gracias por investigar!

Ciertamente es extraño. Por lo que pude descifrar, parece que ImportManager de alguna manera termina siendo un retenedor para los conjuntos de reglas a través del código de pegamento / polyfill agregado por la conversión de ES6.

Me pregunto si hay una solución aquí que termina a mitad de camino. Es decir, conserve la compilación de ES6 para Node.js, pero también tiene un objetivo de navegador transpilado. Sería una pérdida enorme eliminar la claridad del código que agrega la conversión ES6. 😢

@rjgotten

Sería una pérdida enorme eliminar la claridad del código que agrega la conversión ES6.

Puede que no sea tan malo como parece, por dos razones:

  1. La configuración acumulada todavía existe y podría extraerse de una confirmación si alguien quiere investigarla.
  2. He estado trabajando activamente en un Less 4.0 basado en TypeScript durante algún tiempo. Prefiero dedicar mi tiempo a eso que rastrear esta regresión de rendimiento. Es un refactor básico, por lo que no está cerca de ninguna manera, pero por la naturaleza de TS, es poco probable que tenga estas mutaciones de objetos únicos que se aferran a las referencias y previenen la GC. Y el código es mucho más claro de seguir . Así que ahí está.

Parece un _bit_ torpe revertir todo a ES5 solo para solucionar este problema, pero es comprensible dado que se está reescribiendo. Aún así, debemos considerar cuánto tiempo tendremos que vivir con esta decisión. Como mencionó @rjgotten ,

¿Hay alguien que haya experimentado esto que esté dispuesto a compartir su código? Sería genial si pudiéramos comparar utilizando un proyecto del mundo real para estar seguros de si los cambios están ayudando o no. Si pudiéramos conseguirlo, estaría dispuesto a ayudar a tratar de reducir dónde podría estar el cuello de botella.

@ matthew-dean, en una nota al margen, ¿intentaste compilar con Babel en modo suelto para ver si producía algún código más sano?

@seanCodes Estoy totalmente a favor de no tomar la opción nuclear, si alguien tiene tiempo para investigar cómo / por qué la salida de Rollup / Typescript funciona peor, y tiene estos efectos extra de memoria. En parte, sería una cuestión de mirar el código original, compararlo con el resultado y buscar pistas.

Una suposición que tengo es que parte de la lógica de desestructuración tiene mucha repetición de creación de objetos.

Básicamente, alguien tendría que ofrecerse como voluntario para ser dueño de este problema.

@ matthew-dean, en una nota al margen, ¿intentaste compilar con Babel en modo suelto para ver si producía algún código más sano?

@seanCodes El código se compila utilizando TypeScript, no Babel. Mi idea era agregar gradualmente tipos JSDoc para hacer que la verificación de tipos sea más sólida, pero el TS en v4 es suficiente para reescribirlo, no estoy seguro de si es necesario. Entonces, alguien podría experimentar con Babel v. TypeScript (y diferentes configuraciones para cada uno) para ver si Babel produce un código más eficaz. Solo tenga en cuenta este problema, que fue causado por producir inicialmente una compilación que no es de ES5 para Node.js: https://github.com/less/less.js/issues/3414

@seanCodes Además, todavía me resulta difícil VERIFICAR la diferencia de rendimiento. Nadie ha elaborado un PR / pasos para DEMOSTRAR, definitivamente, la diferencia de desempeño. Hay varias anécdotas en este hilo, pero sin un código o pasos reproducibles, no estoy seguro de cómo alguien investiga esto. Idealmente, habría un RP que lance una herramienta de creación de perfiles (a través del depurador de Chrome o de otro modo) para generar un número en un sistema, promediando una serie de pruebas. Entonces, debido a que esto, hasta ahora, no es 100% reproducible, en la medida en que alguien ha podido ofrecer pasos de reproducción, esa es en parte la razón por la que personalmente no quería ir a esa madriguera del conejo. (En otras palabras, los comentarios de "Usé el depurador de Chrome" no son un conjunto de pasos de reproducción. Es útil conocerlos, pero no ayuda a nadie a investigar. Necesitamos saber qué estamos rastreando y por qué / qué resultado es esperado.)

Por lo tanto, si la gente quiere mejorar el rendimiento de Less utilizando la base de código actual, probablemente se necesitarán varios voluntarios y una persona para que se haga cargo de este problema.

Personalmente, no he visto mucha diferencia de rendimiento como en _speed_, pero la diferencia en el consumo de memoria es asombrosa y parece correlacionarse con la cantidad de importaciones, lo que mi análisis superficial parece sugerir que también está relacionado con el problema.

Si eso es correcto, ~ usted ~ cualquiera debería poder armar un proyecto de prueba viable siempre que consista en muchos archivos importados donde cada uno contiene algunos conjuntos de reglas, para ver la diferencia también.

@rjgotten

Si eso es correcto, debería poder armar un proyecto de prueba con muchos archivos importados, cada uno con algunos conjuntos de reglas y ver la diferencia también.

El "tú" es lo importante aquí. 😉

El "tú" es lo importante aquí. 😉

Desafortunada elección de palabras. Lo digo en el sentido general, es decir, "cualquiera".
Yo mismo indagaría más, pero ya tengo demasiados platos girando actualmente.

@rjgotten ¿Puede darme una mejor idea de lo que podrían ser “muchos archivos importados”? ¿Lo harían 100 archivos separados o estamos hablando de 1000?

Además, ¿cómo notó el problema por primera vez? ¿Falló una compilación debido al consumo de memoria o estaba mirando el uso de la memoria? ¿O notó que su máquina se ralentizaba?

No he intentado replicar esto todavía, pero todavía espero saber qué buscar exactamente para no tener que recurrir a adivinar y verificar.

100 o más lo harían. Eso es aproximadamente el tamaño del proyecto de la vida real en el que noté el problema.

Lo noté por primera vez cuando una compilación que se ejecutaba en nuestro entorno corporativo de CI comenzó a fallar. Los ejecutamos dentro de los contenedores de Docker con restricciones de memoria configuradas.

@rjgotten ¿Todavía tenemos el problema de la memoria en 3.11.3? Eliminé cualquier almacenamiento en caché (referencia) del AST dentro de las importaciones en una versión anterior, por lo que si las importaciones se retenían y los árboles AST se retenían dentro de eso, eso aumentaría el uso de la memoria, pero si dejamos de retener los árboles, ¿Eso lo resuelve?

@ matthew-dean Sí, el problema sigue presente en 3.11.3.

Intentaré crear una prueba de concepto, pero tengo mucho en mi plato. Puse esto en mi lista de tareas pendientes una vez que tenga algo de tiempo extra.

@ matthew-dean Quiero probar esto en un proyecto relativamente grande, que anteriormente estaba fallando. ¿Algo que necesite saber? ¿Debo usar esta rama, https://github.com/less/less.js/tree/release_v3.12.0-RC1 ?

@nfq Puede intentarlo, pero la sabiduría predominante en este hilo es no hacer estallar la conversión de ES6, que es lo que hace esa rama, y ​​en su lugar obtener más información para diagnosticar correctamente el problema.

Por cierto, intenté inspeccionar el objeto less durante la compilación en un intento de encontrar objetos persistentes y no pude encontrar ninguno. 🤷‍♂️

También estoy experimentando problemas de rendimiento. Para un mismo conjunto de pruebas:

| Versión | Tiempo |
| - | - |
| v3.9.0 | ~ 1,6 s |
| v3.10.0 ~ v3.11.1 | ~ 3,6 s |
| v3.11.2 + | ~ 12s |

Aparte de 3.9.0 → 3.10.0 discutido aquí, parece haber una degradación significativa del rendimiento en v3.11.2 . Este cambio en el registro de cambios parece sospechoso:

3498 Eliminar el almacenamiento en caché del árbol en el administrador de importación (# 3498)

Lo mismo para mi.

Tiempos para compilaciones idénticas (todas en el nodo 10.19.0):

  • v3.9: 29.9 segundos
  • v3.10: 76.0 segundos
  • v3.12: 89.3 segundos

@ jrnail23

¿Tiene un repositorio que pueda demostrar esos resultados?

@ matthew-dean Tengo uno para https://github.com/less/less.js/issues/3434#issuecomment -672580467: https://github.com/ecomfe/dls-tooling/tree/master/packages/ less-plugin-dls (es un monorepo, que incluye un complemento Less).

Lo siento, @ matthew-dean, no lo hago. Esos resultados son del producto de mi empleador.

Esos resultados son del producto de mi empleador.

La misma razón por la que tampoco puedo proporcionar ningún archivo. 😞

@rjgotten @ jrnail23 @Justineo - Solo por curiosidad, ¿tiene varios casos en los que la misma importación se importa varias veces en diferentes archivos?

En mi caso, tenemos un complemento que inyecta una declaración @import y proporciona un montón de funciones personalizadas. El archivo importado importa otras partes del proyecto que finalmente producirán más de 1000 variables Menos.

@ matthew-dean, tenemos un archivo base.less que importa cosas comunes (por ejemplo, variables de color, tipografía, etc.).
Un poco de grepping rápido de nuestra aplicación muestra que una referencia base.less (es decir, <strong i="8">@import</strong> (reference) "../../../less/base.less"; ) es importada por otros 66 archivos less específicos de componente / función, y algunos de esos archivos pueden también importe otros archivos menos específicos de componentes / funciones (que pueden hacer referencia a base.less ).
Curiosamente, aparentemente también tenemos otro archivo less que (¿probablemente accidentalmente?) Se importa a sí mismo como referencia.

@rjgotten @ jrnail23 @Justineo - Solo por curiosidad, ¿tiene varios casos en los que la misma importación se importa varias veces en diferentes archivos?

Para mí, eso es un 'sí'. El proyecto en el que experimenté problemas por primera vez es una solución que se puede personalizar y que hace uso de un archivo de variables centralizado que se incluye en todos los demás. Además, utilizamos un diseño basado en componentes en el que tenemos fábricas de mezclas que producen, por ejemplo, ciertos tipos de botones, iconos de fuentes, etc. y partes de ellos también se importan en varios archivos.

Básicamente; cada dependiente está configurado para importar estrictamente sus dependencias y confiamos en el compilador Less para eliminar y secuenciar las importaciones en el orden correcto y garantizar la salida CSS correcta.

@rjgotten

Lo publiqué y lo eliminé (porque pensé que lo había descubierto, pero aún no pude replicarlo), todavía no hay pasos para reproducir lo que la gente está informando, incluido el proceso.

Incluso algo tan simple como esto:

En 3.11, uno de los retenedores enumerados para RuleSet es ImportManager.

No puedo encontrar evidencia de eso, pero tampoco sé cómo pudiste determinarlo. No estoy lo suficientemente familiarizado con Chrome DevTools como para saber cómo se podría determinar qué es lo que retiene qué, y ese es un tema lo suficientemente vago como para que Google no me haya ayudado. Por ejemplo, ¿las personas se están uniendo al proceso de Node? ¿Ejecutarlo en el navegador y establecer puntos de interrupción? Cuales son los pasos?

En resumen, en el año en que se abrió este número, ninguno de los informes tiene pasos para reproducir. Estoy seguro de que todas estas anécdotas significan ALGO, pero ¿cómo determinaste TÚ lo que encontraste? ¿O puede pensar en una configuración que demuestre esto? Me gustaría ayudar a resolverlo, pero no sé cómo reproducirlo.

@Justineo En su repositorio, ¿qué pasos tomó para determinar el tamaño de la memoria o el tiempo de compilación? ¿Qué estabas usando para medir?

@Justineo Ya que señaló la eliminación de la caché (que tal vez fue una mala idea), ¿puede probar esta rama y ver si ayuda a su velocidad de compilación? https://github.com/less/less.js/tree/cache-restored

Solo para dar algunas actualizaciones sobre la experimentación / prueba de hoy:

Grunt's shell:test veces

  • Menos 3.9 - 1.8s
  • Menos 3.12 (Babel-transpilado a ES5) - 9.2s
  • Menos 3.12 (transpilado de Babel a ES6) - 3.1s
  • Menos 3.12 (transpilado de TypeScript a ES5) - 3.2s

Entonces, creo que lo largo y lo corto es que el código transpilado siempre es más lento, y fuimos ingenuos al pensar lo contrario. Estoy un poco sorprendido porque la transpilación es ahora tan "estándar" en el mundo de JavaScript. ¿Hay alguna otra investigación que respalde esto?

@Justineo En su repositorio, ¿qué pasos tomó para determinar el tamaño de la memoria o el tiempo de compilación? ¿Qué estabas usando para medir?

npm run test producirá el tiempo total transcurrido. Y en otros proyectos estamos experimentando OOM cuando cambiamos a 3.12.

Si miramos el código generado por TypeScript, obtenemos algo como:

Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var node_1 = tslib_1.__importDefault(require("./node"));
var variable_1 = tslib_1.__importDefault(require("./variable"));
var property_1 = tslib_1.__importDefault(require("./property"));
var Quoted = /** <strong i="6">@class</strong> */ (function (_super) {
    tslib_1.__extends(Quoted, _super);
    function Quoted(str, content, escaped, index, currentFileInfo) {
        var _this = _super.call(this) || this;
        _this.escaped = (escaped == null) ? true : escaped;
        _this.value = content || '';
        _this.quote = str.charAt(0);
        _this._index = index;
        _this._fileInfo = currentFileInfo;
        _this.variableRegex = /@\{([\w-]+)\}/g;
        _this.propRegex = /\$\{([\w-]+)\}/g;
        _this.allowRoot = escaped;
        return _this;
    }

vs:

var Node = require('./node'),
    Variable = require('./variable'),
    Property = require('./property');

var Quoted = function (str, content, escaped, index, currentFileInfo) {
    this.escaped = (escaped == null) ? true : escaped;
    this.value = content || '';
    this.quote = str.charAt(0);
    this._index = index;
    this._fileInfo = currentFileInfo;
    this.variableRegex = /@\{([\w-]+)\}/g;
    this.propRegex = /\$\{([\w-]+)\}/g;
};

Entonces, mi suposición es que todas esas definiciones y llamadas de funciones adicionales se suman, con el tiempo. A menos que esto sea una pista falsa, pero no sé a qué más atribuir esto excepto a la transpilación. Lo que no entiendo es por qué TS no puede producir un código más cercano al original.

ACTUALIZAR:

Si trato de poner las pruebas 3.9 a la par con lo que hay en 3.12, entonces básicamente obtengo 1.2s frente a 1.3s. Así que ya no estoy seguro de esta diferencia porque las pruebas han cambiado. Tendría que ejecutarse exactamente con los mismos archivos menos.

@Justineo @rjgotten He impulsado algunos ajustes de TypeScript para tal vez hacer una compilación más eficiente. ¿Quieres construir y probar esa rama? https://github.com/less/less.js/tree/cache-restored

@ matthew-dean 👍 ¡Gracias! Lo probaré hoy más tarde.

Probé la rama cache-restored y es mucho más rápida que la v3.11.2 +, aproximadamente a la misma velocidad que la v.3.1.0 ~ 3.11.1.

@Justineo

Probé la rama de restauración de caché y es mucho más rápida que la v3.11.2 +, aproximadamente a la misma velocidad que la v.3.1.0 ~ 3.11.1.

Bueno, eso es prometedor. Obtengamos algunos comentarios de @ jrnail23 y otros en este hilo. Esa eliminación de caché fue después de que se publicara este hilo (3.11.2); de hecho, fue un intento de eliminar parte de la sobrecarga de memoria, pero en los casos en que está importando el mismo archivo varias veces, definitivamente podría haber empeorado las cosas.

Entonces, en resumen, todavía no estoy seguro sobre el problema original o su causa (aparte de ALGO que sucedió en la conversión del código), y tendría curiosidad por saber si esta rama todavía tiene esos problemas, pero como mencioné antes , no hay pasos de reproducción claros, así que no estoy seguro.

@ matthew-dean, estoy teniendo algunos problemas al intentar usar la rama cache-restored (no tengo muy claro qué se debe hacer para usarla localmente, ya que npm link falla me).
¿Pueden publicar una versión canary / prelanzamiento que pueda probar?

@ jrnail23

Creo que esto funcionó. Intente eliminar Less e instalar con npm i [email protected]+84d40222

@ matthew-dean, acabo de probar esa versión y todavía es mala para mí.
Para compilaciones de paquetes web nuevos (sin almacenamiento en caché), se necesitan 62.4 segundos para la versión 3.9, pero 121 segundos para la versión 3.13.1.
Para las compilaciones en caché, la v3.9 tarda 30 segundos y la v3.13.1 tarda de 83 a 87 segundos.

@ jrnail23 ¿Puedes intentar eliminar todos los módulos de nodo e instalar [email protected]+b2049010

Menos 3.9 Benchmark:
Screen Shot 2020-12-05 at 2 36 09 PM

Menos 4.0.1-alpha.0:
Screen Shot 2020-12-05 at 1 12 26 PM

Menos 4.0.1-alpha.2:
Screen Shot 2020-12-05 at 2 35 20 PM

@Justineo @rjgotten ¿Puedes probar esto también?

@ jrnail23 @Justineo @rjgotten

Tenga en cuenta que esta es una compilación 4.0, por lo que puede encontrar errores si su código tiene estos cambios importantes:

  • Se requieren paréntesis para mezclar llamadas (por ejemplo, .mixin; no está permitido)
  • El modo matemático predeterminado de Less ahora es la división por pares, por lo que las barras inclinadas (pensadas como matemáticas) deben estar entre paréntesis

Entonces, aunque ahora soy mucho más optimista, he solucionado el problema, según los últimos puntos de referencia, todavía tengo que averiguar por qué ha estado sucediendo este problema. Si el rendimiento se mantiene para todos los demás, puedo dar un resumen de cómo finalmente reduje el problema y lo que encontré. En otras palabras, creo que encontré _dónde_ está / estaba el problema, pero no necesariamente por qué.

El resultado del mismo conjunto de pruebas que https://github.com/less/less.js/issues/3434#issuecomment -672580467:

| Versión | Tiempo |
| - | - |
| v3.9.0 | ~ 1,6 s |
| v3.10.0 ~ v3.11.1 | ~ 3,6 s |
| v3.11.2 + | ~ 12s |
| 4.0.1-alpha.2 + b2049010 | ~ 1,6 s |

Puedo confirmar que para mi conjunto de pruebas específico, el nivel de rendimiento ha mejorado tan rápido como v3.9.0. ¡Muy apreciado Matt! Aunque no estoy seguro del cambio de pausa en el modo matemático. Cambiar esto puede provocar la rotura de muchas de nuestras aplicaciones, por lo que es posible que nos quedemos atascados en la v3.9.0 incluso si se resuelve el problema de rendimiento.

@Justineo

Aunque no estoy seguro del cambio de pausa en el modo matemático.

Puede compilar explícitamente en el modo math=always para obtener el comportamiento matemático anterior. Es solo un valor predeterminado diferente.

Desglose del problema

TL; DR - Cuidado con el patrón de clases

El problema estaba en la transpilación de clases tanto en Babel como en TypeScript. (En otras palabras, ambos tenían los mismos problemas de rendimiento con el código transpilado, y Babel era un poco peor). Ahora, hace años, cuando se introdujo el patrón de clase, me dijeron, y hasta este problema, creí, que la clase La herencia en JavaScript es un azúcar sintáctico para la herencia funcional.

En resumen, no lo es. _ (Editar: bueno ... lo es y no lo es. Depende de lo que quieras decir con "herencia" y de cómo la hayas definido, como verás en breve ... es decir, hay varios patrones para crean "herencia" en la cadena de prototipos en JavaScript, y las clases representan solo un patrón, pero ese patrón es algo diferente de todos los demás, de ahí la necesidad de TS / Babel de un código auxiliar para "imitar" este patrón usando funciones especializadas.) _

Menos código se veía así:

var Node = function() {
  this.foo = 'bar';
}

var Inherited = function() {
  this.value = 1;
}
Inherited.prototype = new Node();

var myNode = new Inherited();

Ahora digamos que quería reescribir esto usando JS "moderno". De hecho, puede automatizar este proceso, lo cual hice, pero de cualquier manera lo habría escrito de la misma manera, que fue:

class Node {
  constructor() {
    this.foo = 'bar';
  }
}
class Inherited extends Node {
  constructor() {
    super();
    this.value = 1;
  }
}
var myNode = new Inherited();

Lo mismo, ¿verdad? En realidad no. El primero creará un objeto con una propiedad de { value: 1 } , y en su cadena prototipo, tiene un objeto de { foo: 'bar' } .

El segundo, debido a que llamará al constructor tanto en Inherited como en Node , creará un objeto con una estructura como { value: 1, foo: 'bar' } .

Ahora, para el _usuario_, esto realmente no importa, porque puede acceder tanto a value como a foo desde myNode en cualquier caso. _Funcionalmente_, parecen comportarse igual.

Aquí es donde entro en pura especulación

Por lo que recuerdo en artículos sobre motores JIT como V8, las estructuras de objetos son realmente muy importantes. Si creo un montón de estructuras como { value: 1 } , { value: 2 } , { value: 3 } , { value: 4 } , V8 crea una representación estática interna de esa estructura. Básicamente, está almacenando la estructura + los datos una vez, y luego los datos 3 veces más.

PERO si agrego diferentes propiedades a esto cada vez, como: { a: 'a', value: 1 } , { b: 'b', value: 2 } , { c: 'c', value: 3 } , { d: 'd', value: 4 } , entonces esas son 4 estructuras diferentes con 4 conjuntos de datos , incluso si se crearon a partir del mismo conjunto de clases originales. Cada mutación de un objeto JS des-optimiza las búsquedas de datos, y un patrón de clase que se transpila en funciones causa (tal vez) mutaciones más únicas. (Honestamente, no tengo idea de si esto es cierto para el soporte de clases nativas en los navegadores).

AFAIK, lo que también es cierto es que cuantas más propiedades tenga en un objeto, más tiempo llevará una búsqueda de propiedad individual.

Una vez más, para los usuarios finales, esto rara vez importa porque los motores JS son muy rápidos. Buuuuut dice que está manteniendo un motor que crea y busca propiedades / métodos en MUCHOS objetos lo más rápido posible. (Ding ding ding.) De repente, estas pequeñas diferencias entre la forma en que TypeScript / Babel "extienden" los objetos y la herencia prototípica funcional nativa se suman muy rápidamente.

Escribí una implementación básica de un Nodo y una función heredada, usando la antigua sintaxis Less y el patrón de clase transpilado con TS. Desde el principio, el nodo heredado consume un 25% más de memoria / recursos, y eso ANTES de que se hayan creado las instancias del nodo heredado.

Ahora considere que algunos nodos heredan de otros nodos. Eso significa que la representación en memoria de las clases comienza a multiplicarse, al igual que sus instancias heredadas.

De nuevo, esto es especulación.

Debo enfatizar tomar todo esto con un grano de sal, porque nunca había oído hablar de una conversión a clases que fuera tan desastrosa para el rendimiento. Lo que creo que está sucediendo es que hay una combinación especial de búsquedas de objetos / instancias que está usando Less que está desoptimizando seriamente el JIT. _ (Si algún experto en motores JavaScript sabe por qué las clases transpiladas funcionan mucho peor que los métodos de herencia JS nativos en este caso, me encantaría saberlo). _ Intenté crear una medida de rendimiento para crear miles de objetos heredados utilizando ambos métodos, y Nunca pude ver una diferencia constante en el rendimiento.

Por lo tanto, antes de irse con "las clases en JavaScript son malas", podría ser algún otro patrón realmente desafortunado en la base de código Less junto con la diferencia en las clases transpiladas frente a JS nativo que creó esta caída en el rendimiento.

Cómo finalmente lo descubrí

Honestamente, nunca sospeché el patrón de clase. Sabía que el código ES5 original se ejecutaba más rápido que el código Babelificado inverso, pero sospechaba algo relacionado con las funciones de flecha o la sintaxis extendida en alguna parte. Todavía tenía la rama ES5 actualizada, así que un día decidí ejecutar lebab nuevamente y solo usar estas transformaciones: let,class,commonjs,template . Fue entonces cuando descubrí que nuevamente tenía problemas de rendimiento. Sabía que no se trataba de una cadena de plantillas o de un let-to-var; Pensé que tal vez los requisitos para las importaciones hicieron algo, así que jugué con eso por un tiempo. Eso dejó clases. Entonces, por una corazonada, reescribí todas las clases extendidas de nuevo a la herencia funcional. Bam, el rendimiento estaba de vuelta.

Un cuento con moraleja

¡Entonces! Lección aprendida. Si su proyecto está en el antiguo código ES5, y anhela esa bondad "moderna" de Babel-ified o TypeScripted, recuerde que lo que sea que transpile, no lo escribió.

Todavía pude reescribir las clases en algo más "similar a una clase", con un patrón un poco más fácil de mantener , pero más o menos conservando el patrón de herencia funcional original del nodo Less, y dejaré una nota fuerte en el proyecto para un futuro mantenedor para no volver a hacer esto nunca más.

Puede compilar explícitamente en modo math = always para obtener el comportamiento matemático anterior. Es solo un valor predeterminado diferente.

Soy consciente de esto. Tenemos muchas bases de código Less en muchos equipos diferentes, por lo que romper los cambios incluso en las opciones de compilación predeterminadas aumentaría los costos de comunicación. No estoy seguro de si los beneficios valen la pena.

¡Gracias por el desglose detallado!

Vi el uso de Object.assign en la salida compilada, lo que significa que solo funciona en navegadores que admiten la sintaxis nativa de ES class menos que ahora se requieran polyfills. Entonces, ¿podemos usar la sintaxis nativa sin pasar a ES5 si pretendemos eliminar el soporte para entornos más antiguos (por ejemplo, IE11, Nodo 4, ...)?

Al mismo tiempo, creo que es mejor si podemos separar la corrección de la degradación del rendimiento y los cambios importantes, lo que significa conseguir la corrección del rendimiento en la v3 e incluir los cambios importantes solo en la v4.

@ matthew-dean
El hecho de que las clases de ES sean las culpables es una locura .
Gracias por el elaborado desglose.

Va a mostrar: _composición sobre herencia_ 😛

Aunque para su información; si desea una cadena de herencia protypal más limpia, debería hacerlo un poco diferente a su ejemplo.
Si usa Object.create así:

var Node = function() {
  this.foo = 'bar';
}
Node.prototype = Object.create();
Node.prototype.constructor = Node;

var Inherited = function() {
  Node.prototype.constructor.call( this );
  this.value = 1;
}
Inherited.prototype = Object.create( Node.prototype );
Inherited.prototype.constructor = Inherited;

var myNode = new Inherited();

luego aplana las propiedades en las propias instancias que luego comparten la misma forma; los prototipos comparten forma; _y_ evita tener que arrastrar las cadenas de prototipos para cada acceso a la propiedad. 😉

@Justineo

Vi el uso de Object.assign en la salida compilada, lo que significa que solo funciona en navegadores que admiten la sintaxis de clase ES nativa a menos que ahora se requieran polyfills. Entonces, ¿podemos usar la sintaxis nativa sin pasar a ES5 si pretendemos eliminar el soporte para entornos más antiguos (por ejemplo, IE11, Nodo 4, ...)?

No creo que sea del todo correcto, es decir, Object.assign aterrizó poco antes de la implementación de las clases, pero su punto está tomado. Solo esperaba evitar escribir [Something].prototype.property una y otra vez: /

Técnicamente, todo sigue sucediendo, así que no lo sé. El objetivo original era una base de código más fácil de mantener / legible. Si necesita un polyfill Object.assign en algún entorno, que así sea. Sería una versión de algo que Less no admite.

Al mismo tiempo, creo que es mejor si podemos separar la corrección de la degradación del rendimiento y los cambios importantes, lo que significa conseguir la corrección del rendimiento en la v3 e incluir los cambios importantes solo en la v4.

He estado pensando en eso y supongo que también es un buen punto. Solo estoy tratando de hacer que mi cabeza no explote construyendo / manteniendo 3 versiones principales de Less.

@rjgotten AFAIK eso es lo que se supone que debe hacer un patrón de clase, exactamente como lo ha definido. A menos que no vea una diferencia crítica. Entonces 🤷‍♂️. No voy a intentar alterar el patrón de herencia de Less's Node nuevamente si está funcionando bien (aparte de que puedo eliminar esta llamada Object.assign como sugirió @Justineo ).

@Justineo @rjgotten ¿Puedes probar esto? [email protected]+b1390a54

Solo lo probé:

| Versión | Tiempo |
| - | - |
| v3.9.0 | ~ 1,6 s |
| v3.10.0 ~ v3.11.1 | ~ 3,6 s |
| v3.11.2 + | ~ 12s |
| 4.0.1-alpha.2 + b2049010 | ~ 1,6 s |
| 3.13.0-alpha.10 + b1390a54 | ~ 4,7 s |

PD. Probado con Node.js v12.13.1.

Soy que.

No tuve tiempo para establecer puntos de referencia separados.
Pero desde una perspectiva de prueba de integración, aquí hay algunos números del mundo real: los resultados acumulativos de un proyecto de Webpack de nivel de producción que usa Less.

Versión | Tiempo | Memoria pico
: ------------------------ | --------: | ------------:
3,9 | 35376ms | 950 MB
3.11.3 | 37878ms | 920 MB
3.13.0-alpha.10 + b1390a54 | 34801ms | 740 MB
3.13.1-alpha.1 + 84d40222 | 37367ms | 990 MB
4.0.1-alpha.2 + b2049010 | 35857ms | 770 MB

Para mí, el 3.13.0 da los mejores resultados ... 🙈

3.11.3 también parece no tener el uso de memoria descontrolado que vi antes con la versión 3.11.1.
Pero las versiones beta 4.0.1 y 3.13.0 son aún mejores.

El 3.13.1 que restauró la caché no ayuda a mejorar mis tiempos de compilación en el mundo real y solo aumenta el uso de la memoria.

@rjgotten Sí, creo que el problema es que, en este hilo, la gente está midiendo cosas diferentes, y hasta ahora todos tienen esencialmente datos privados que son imposibles de replicar (sin someterse a menos evaluaciones comparativas o traducir eso en un PR). Less tiene una prueba de referencia, que podría usar contra diferentes clones del repositorio, para verificar por mí mismo las diferencias en el tiempo de análisis / evaluación, pero donde el almacenamiento en caché no se aplica (actualmente).

Aquí hay una compilación publicada con los cambios de herencia en el árbol y con el caché del árbol de análisis restaurado: [email protected]+e8d05c61

Caso de prueba

https://github.com/ecomfe/dls-tooling/tree/master/packages/less-plugin-dls

Resultados

| Versión | Tiempo |
| - | - |
| v3.9.0 | ~ 1,6 s |
| v3.10.0 ~ v3.11.1 | ~ 3,6 s |
| v3.11.2 + | ~ 12s |
| 4.0.1-alpha.2 | ~ 1,6 s |
| 3.13.0-alpha.10 | ~ 4,7 s |
| 3.13.0-alpha.12 | ~ 1,6 s |

Versión de Node.js

v12.13.1


A mí me parece que esta versión funciona a la perfección. Les pediré a mis colegas que prueben y verifiquen.

Funciona un poco peor que el alpha.10 para mí, pero también podría ser una variación:

Versión | Tiempo | Memoria pico
: ------------------------ | --------: | ------------:
3,9 | 35376ms | 950 MB
3.11.3 | 37878ms | 920 MB
3.13.0-alpha.10 + b1390a54 | 34801ms | 740 MB
3.13.0-alpha.12 + e8d05c61 | 36263ms | 760 MB
3.13.1-alpha.1 + 84d40222 | 37367ms | 990 MB
4.0.1-alpha.2 + b2049010 | 35857ms | 770 MB

@ matthew-dean, parece que mi código aún no es compatible con los cambios 4.0.1-alpha.2+b2049010 . Veré si puedo resolver mis problemas y probarlo.

@rjgotten Sí, la diferencia parece menor para su caso de prueba.

Creo que el resultado de esto es que podría preparar una versión 3.xy una versión 4.0. No tenía ganas de impulsar 4.0 con este problema sin resolver. Afortunadamente, parece que lo ha sido.

El otro elemento que haré es abrir un problema para que cualquiera lo tome, que es poner en una tubería de CI que falla Menos si cae por debajo de un cierto umbral de referencia.

@ jrnail23 Sería genial si pudieras modificar para ejecutar el alfa 4.0, pero mientras tanto, ¿puedes probar [email protected]+e8d05c61 ?

@ matthew-dean, obtengo lo siguiente para mi compilación:

  • v3.9: 98 segundos
  • v3.10.3: 161 segundos
  • v3.13.0-alpha.12: 93-96 segundos

¡Parece que has vuelto a donde quieres estar! ¡Buen trabajo!

¡Buen trabajo!

Sí, voy a segundo; tercera; y cuarto eso.
Esta fue una regresión de rendimiento desagradable y desagradable que realmente requirió mucho esfuerzo para limpiar.

Trabajo muy bien hecho.

¡Bien equipo! Voy a publicar la compilación 3.xy en algún momento más tarde, tal vez la semana que viene, publicaré la 4.0. Ambos deberían tener ahora las correcciones de rendimiento. ¡Gracias a todos los que ayudaron a depurar!

Por cierto, actualmente estoy en el proceso de convertir el código base de Less.js a TypeScript, y planeo aprovechar la oportunidad para hacer algunos ajustes / refactorizaciones de rendimiento. ¡Estoy dispuesto a ayudar si alguien está interesado! https://github.com/matthew-dean/less.js/compare/master...matthew-dean : siguiente

¿Algo específico en el que prefieras unos ojos extra? El PR es muy grande, así que realmente no sé por dónde empezar.

@kevinramharak Esa es una pregunta justa. Para ser honesto, me encontré con obstáculos inesperados con la conversión a TypeScript (errores que se volvieron difíciles de resolver), y ahora estoy reconsiderando hacerlo. Menos funciona bien tal como está, y muchas de las razones por las que quería convertir (para facilitar la refactorización / agregar nuevas funciones de lenguaje) ahora son irrelevantes, ya que he decidido trasladar mis ideas para nuevas funciones de lenguaje a una nueva versión previa. lenguaje de procesamiento. No quiero promocionarme demasiado, así que envíame un mensaje de correo electrónico a Twitter o Gitter si quieres más detalles.

¿Fue útil esta página
0 / 5 - 0 calificaciones