Less.js: A versão 3.10.x usa muito mais memória e é significativamente mais lenta que a 3.9.0

Criado em 12 set. 2019  ·  95Comentários  ·  Fonte: less/less.js

Nossas compilações começaram a falhar recentemente porque executamos cerca de 80 compilações Less em paralelo durante o processo de compilação do nosso projeto e a nova versão do Less.js usa tanta memória que o Node trava. Rastreamos a falha até a atualização de Less.js de 3.9.0 para 3.10.3.

Mudei nosso script Less para compilar os arquivos sequencialmente (construindo 2 arquivos por vez) e fiz uma amostra do uso de memória do Node durante o processo e obtive os seguintes resultados:

less graph

Less.js parece usar 130% mais memória agora e leva cerca de 100% mais tempo para compilar para nós.

Gostaria de saber se você comparou Less.js e se vê resultados semelhantes

Comentários muito úteis

Ok time! Vou publicar a compilação 3.x, e algum tempo depois, talvez na próxima semana, publicar 4.0. Ambos agora devem ter as correções de desempenho. Obrigado a todos que ajudaram a depurar!

Todos 95 comentários

Isso é estranho. Qual é a sua versão do Node?

@ PatSmuk360 Você poderia testar e obter um perfil de memória de 3.10.0 e ver se ele difere?

Estamos executando a versão mais recente do 10 (10.16.3).

Instantâneo de heap antes:

image

Instantâneo de heap após:

image

Também tentei no Node 12.10.0 e parece muito pior, atingindo 587 MB de uso de memória em um ponto da compilação sequencial.

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

image

Perfil da CPU após:
CPU-20190916T134917.cpuprofile.zip

image

@ PatSmuk360 Então, o longo e o curto é que a diferença entre essas versões é que a base de código foi transformada para a sintaxe ES6. O que não é tecnicamente uma mudança significativa, e é por isso que não era uma versão principal.

MAS ... minha suspeita é que algumas das conversões do Babel para coisas como sintaxe de propagação de objeto / array são menos eficientes do que as versões ES5 mais detalhadas. É por isso que eu estava perguntando se você poderia testar 3.10.0, porque originalmente eu estava exportando um pacote transpilado que era compatível com o Node 6 e superior, mas quebrou uma integração com uma biblioteca particular que não conseguia lidar com a sintaxe de classe. Então, desci para o Nó 4 sem nenhuma construção de classe.

Se pudermos descobrir exatamente qual transformação ES5 não está tendo um bom desempenho, poderíamos teoricamente definir de forma mais granular essas configurações de exportação do Babel para uma exportação de melhor desempenho.

@ PatSmuk360 Aliás, o que é aquela função split que está demorando tanto?

@ matthew-dean Esse parece ser o String.prototype.split . Se você abrir o perfil em seus devtools do Chrome, poderá ver todos os dados codificados por cores. Estou tentando alterar o perfil para vincular a https://cdn.jsdelivr.net/npm/[email protected]/dist/less.cjs.js como sua fonte para que seja mais fácil inspecionar os gargalos. Existe um mapa de origem disponível para o arquivo *.cjs.js a *.js ? O arquivo https://cdn.jsdelivr.net/npm/[email protected]/dist/less.min.js.map parece mapear o arquivo .min para a origem ES6. Talvez possamos alimentar o mapa de origem para as ferramentas de desenvolvimento para que possamos descobrir onde a transpilação causa gargalos.

Esta linha específica https://github.com/less/less.js/blob/cae5021358a5fca932c32ed071f652403d07def8/lib/less/source-map-output.js#L78 parece ter uma grande quantidade de tempo de CPU. Mas, considerando a operação que ele realiza, isso não parece fora do lugar para mim.

Não tenho muita experiência com perfis de heap, mas o que me chama a atenção é o aumento no valor de (closures), (system), (array), system / Context . Tentando vincular isso ao perfil da CPU, parece que o aumento desses objetos resulta em um aumento massivo no tempo gasto na coleta de lixo.

@kevinramharak Em geral, converter um AST como o de Less com profundidade _n_ em uma árvore de saída plana e serializada como o CSS envolve muita criação de objeto temporário. Então, o que pode estar acontecendo é que, com certas transformações, está adicionando uma quantidade adicional de _x_ de objetos. Mesmo temporariamente, você pode obter um efeito exponencial em que cada nó criando apenas 2 a 3x os objetos, multiplicado pelo número de nós multiplicado pelo número de vezes que precisa para nivelar as regras ... Eu posso ver que está somando. Provavelmente éramos ingênuos em geral em pensar essencialmente na sintaxe ES6 como um açúcar essencialmente sintático da sintaxe ES5. (Os desenvolvedores de JavaScript provavelmente são culpados disso em geral.) Na realidade, transpilar a sintaxe mais recente pode criar padrões ES5 que não têm muito desempenho. Para 99% dos desenvolvedores, isso não é grande coisa, porque eles não estão iterando por meio desse código centenas ou milhares de vezes por segundo. Mas este é o meu palpite sobre o que está acontecendo, porque não houve outras mudanças importantes.

@kevinramharak Re: as linhas de origem - isso ocorre porque o analisador Less original não rastreia linhas / colunas de entrada, então quando o mapeamento de origem foi adicionado, ele precisava essencialmente dividir a entrada em linhas para descobrir como deveria ser mapeado para o original fonte. Isso não será um problema no 4.x +, mas faz sentido porque ele passaria muito tempo lá agora.

Estou usando less.min.js em um navegador e 3.10.3 é duas vezes mais lento do que 2.7.3 que eu usava antes. Tanto no Chrome quanto no Firefox.

@ PatSmuk360 Você pode verificar este ramo? https://github.com/matthew-dean/less.js/tree/3.11.0

Resumindo, a transpilação de Babel para ES5 é meio horrível e usa toneladas de chamadas Object.defineProperty para transpilar classes. Troquei a transpilação para TypeScript, que tem uma saída muito mais sã de protótipos de funções.

Tudo isso é bom e elegante, mas depois de fazer isso, os testes de navegador de Less não serão executados porque ele usa o PhantomJS muito antigo e desatualizado e, até agora, ninguém (incluindo eu) foi capaz de migrar com sucesso os testes do PhantomJS no Headless Chrome.

Mas, um problema de cada vez: se a fonte dist naquele branch não tem problemas de memória, então talvez pudéssemos resolver a bagunça que é o teste do navegador.

Consegui migrar menos testes de navegador para Headless Chrome, mas em termos de desempenho / estabilidade, preciso de muitos comentários dos usuários antes que seja seguro mesclar, por causa da mudança do pipeline de transpilação inteiramente de Babel para TypeScript.

Branch (s) atual (is) podem ser encontrados aqui: https://github.com/less/less.js/pull/3442

Ainda 2x mais lento do que 2.7.

@alecpl Essa é uma boa informação, mas realmente quero saber se 3.11.0 é uma melhoria em relação ao 3.10.

O curioso sobre isso é que, _em teoria_, o código Less original foi convertido com Lebab, que deveria ser o oposto de Babel. O que significa que ES5 -> ES6 -> ES5 deve ser quase idêntico, mas obviamente não é o caso. Portanto, vou precisar investigar (a menos que alguém tenha tempo, o que é um suporte bem-vindo) como o código ES6-> ES5 difere do código ES5 original.

@alecpl

Então, eu executei uma variedade de testes e passei um tempo fazendo benchmarking de 3.11.0 vs. 3.10.3 vs. 3.9.0 vs. 2.7.3 no Headless Chrome

Não encontrei nenhuma evidência de que o compilador Less seja mais lento para qualquer versão, muito menos 2x mais lento. Ainda pode ser verdade que 3.10.0 tem mais sobrecarga de memória por causa das configurações de transpilação, e talvez se um sistema fosse limitado por espaço e fazendo mais trocas de memória ou mais GC como resultado, isso poderia resultar em uma desaceleração. Mas não posso verificar isso.

Você pode testar a si mesmo executando grunt benchmark no branch 3.11.0. Eles podem relatar números diferentes em uma corrida individual, mas se você correr muitas vezes, verá que os tempos são praticamente iguais. Portanto, não sei de onde você está obtendo seus dados.

@ PatSmuk360 Você conseguiu testar a sobrecarga de memória do 3.11.0?

Eu não construo seu código sozinho. Eu não uso nodejs. Eu apenas pego o arquivo less.min.js da pasta dist para duas versões diferentes que mencionei e as uso em minha página. Então, no console, vejo os tempos impressos pelo código Less. Meu menos código usa vários arquivos e o arquivo de saída tem cerca de 100kB de css minificado. Essa é uma referência da "vida real". Estou falando sobre o código Roundcube .

O Chrome é muito mais rápido que o Firefox, mas em ambos a diferença entre as versões é semelhante.

@alecpl Hmm .... talvez seja uma diferença particular para aquele código Less em particular. É verdade que o benchmark é arbitrário. Se eu tiver tempo, adicionarei esses arquivos Less ao benchmarking.

Este problema foi automaticamente marcado como obsoleto porque não teve atividades recentes. Ele será fechado se nenhuma outra atividade ocorrer. Obrigado por suas contribuições.

Só queria postar uma atualização rápida: tentei 3.11.1 e era tão lento quanto 3.10.3. Vou ver se consigo criar um benchmark representativo para testar / criar perfil disso.

Eu atualizei para 3.11.1 e tenho os mesmos problemas de consumo de memória agora.
Um projeto de tamanho modesto está consumindo ~ 600 MB de RAM para ser construído por meio do webpack e less-loader .

Peguei um cronograma de alocação de heap resultando nisso;

heap-timeline

Algo está causando alocações enormes, mantidas vivas por Ruleset .


[EDITAR]
Estou fazendo o downgrade para 3.9 e pegando um cronograma de alocação de heap a partir disso. Vou ver se encontro algo radicalmente diferente.

@ matthew-dean
Encontrei uma dica para você.

No 3.11, um dos retentores listados para RuleSet é o ImportManager.
No 3.9 este _não é o caso_.

Se tudo for mantido vivo por ImportManager e ImportManager é um singleton em todo o processo de compilação. Bem, sim; isso aumentaria significativamente o uso de memória, porque nada pode ser coletado como lixo. Nem mesmo conjuntos de regras intermediários.

@rjgotten Hmm ...... se um objeto tem uma referência a outro objeto, por que isso impediria o GC? Quero dizer, tecnicamente, todos os objetos retêm referências aos nós de API públicos por meio da cadeia de protótipo. Isso só impediria o GC se o inverso fosse verdadeiro, ou seja, algum objeto retendo referências a todas as instâncias do conjunto de regras.

Você parece ter entendido mal.

Quando "A é um retentor de B", significa que A está retendo referências a B que impedem a coleta de lixo de B. Portanto, quando escrevi ImportManager está listado como um retentor do conjunto de regras, expressei exatamente o que você também concluiu: ImportManager mantém referências a instâncias do conjunto de regras, o que evita GC dessas instâncias do conjunto de regras.

@rjgotten Oh, sim? Eu me pergunto como essa mudança entrou aí. É forte a possibilidade de eu ser o culpado, ou há algo no refatorador ES6 que fez isso. Mas sim, isso definitivamente poderia funcionar! Obrigado por investigar!

Ok, que tal essa ideia radical.

Eu criei uma filial com a escolha certa de tudo, EXCETO a conversão ES6. Isso não era simples e destruiria todo o trabalho, mas se um arquivo Babelified / Typescript não pode superar o JS nativo existente, então simplesmente não vale a pena.

Não tenho ideia de como vamos reconciliar o histórico do git, mas aqui está o branch -> https://github.com/less/less.js/tree/release_v3.12.0-RC1. Nuking a conversão é um grande negócio, então eu acho que este branch seria um benchmarking sólido real para comparar.

Eu me pergunto como essa mudança entrou aí. É forte a possibilidade de eu ser o culpado, ou há algo no refatorador ES6 que fez isso. Mas sim, isso definitivamente poderia funcionar! Obrigado por investigar!

Certamente é estranho. Pelo que consegui decifrar, parece que o ImportManager de alguma forma acaba sendo um retentor para conjuntos de regras por meio de gluecode / polyfill adicionado pela conversão ES6.

Eu me pergunto se há uma solução aqui que atenda os fins no meio do caminho. Ou seja, retém a compilação ES6 para Node.js, mas também tem um destino de navegador transpilado. Seria uma perda enorme acabar com a clareza de código que a conversão ES6 adiciona. 😢

@rjgotten

Seria uma perda enorme acabar com a clareza de código que a conversão ES6 adiciona.

Pode não ser tão ruim quanto parece, por dois motivos:

  1. A configuração de rollup ainda existe e pode ser retirada de um commit se alguém quiser pesquisá-la.
  2. Tenho trabalhado ativamente em um Less 4.0 baseado em TypeScript há algum tempo. Prefiro gastar meu tempo nisso do que rastrear essa regressão de desempenho. É um refatorador básico, então não é próximo de forma alguma, mas pela natureza do TS, é improvável que haja essas mutações únicas de objeto que mantêm referências e evitam GC. E o código é muito mais claro de seguir . Então é isso.

Parece um _bit_ desajeitado reverter tudo para o ES5 apenas para corrigir esse problema, mas é compreensível, visto que uma reescrita está em andamento. Ainda assim, devemos considerar por quanto tempo teremos que viver com essa decisão. Como @rjgotten mencionou, estaríamos perdendo muita clareza de código, e se a v4 ainda estiver muito distante, então eu diria que vamos tentar investigar um pouco mais antes de escolher a opção nuclear ES5 - especialmente desde esse problema (enquanto um ruim) não parece ser um obstáculo para a maioria dos usuários.

Há alguém que já passou por isso que gostaria de compartilhar seu código? Seria ótimo se pudéssemos avaliar o desempenho usando um projeto do mundo real para que possamos ter certeza se as mudanças estão ajudando ou não. Se pudéssemos conseguir isso, eu estaria disposto a ajudar a tentar identificar onde pode estar o gargalo.

@matthew-dean, em uma nota lateral, você tentou compilar com o Babel em modo solto para ver se produzia algum código de aparência mais sã?

@seanCodes Sou totalmente a favor de não tomar a opção nuclear, se alguém tiver tempo para investigar como / por que a saída de Rollup / Typescript tem um desempenho pior e tem esses efeitos de memória extras. Em parte, seria uma questão de examinar o código original, compará-lo com a saída e procurar pistas.

Uma suposição que eu tenho é que parte da lógica de desestruturação tem muitos clichês de criação de objetos.

Basicamente, alguém teria que se voluntariar para assumir esse problema.

@matthew-dean, em uma nota lateral, você tentou compilar com o Babel em modo solto para ver se produzia algum código de aparência mais sã?

@seanCodes O código é compilado usando TypeScript, não Babel. Minha ideia era adicionar gradualmente os tipos JSDoc para tornar a verificação de tipo mais forte, mas o TS na v4 é suficiente para reescrever, não tenho certeza se isso é necessário. Portanto, alguém pode experimentar o Babel v. TypeScript (e configurações diferentes para cada um) para ver se o Babel produz código com melhor desempenho. Esteja ciente desse problema, que foi causado inicialmente pela produção de uma compilação não ES5 para Node.js: https://github.com/less/less.js/issues/3414

@seanCodes Além disso, ainda acho difícil VERIFICAR a diferença de desempenho. Ninguém produziu um PR / passos para PROVAR, de forma definitiva, a diferença de desempenho. Há uma série de anedotas neste tópico, mas sem código ou etapas reproduzíveis, não tenho certeza de como alguém investiga isso. O ideal é que haja um PR que inicie uma ferramenta de criação de perfil (por meio do depurador do Chrome ou de outra forma) para gerar um número em um sistema, calculando a média de vários testes. Então, porque isso, até agora, não é 100% reproduzível, na medida em que alguém foi capaz de oferecer etapas de reprodução, é em parte por isso que eu pessoalmente não queria entrar naquela toca do coelho. (Em outras palavras, o feedback de "Eu usei o depurador do Chrome" não é um conjunto de etapas de reprodução. É útil saber sobre, mas não ajuda ninguém a investigar. Precisamos saber o que estamos rastreando e por que / qual resultado é esperado.)

Portanto, se as pessoas quiserem melhorar o desempenho Menos usando a base de código atual, provavelmente serão necessários vários voluntários e uma pessoa para assumir o problema.

Pessoalmente, não vi muita diferença de desempenho como na _velocidade_, mas a diferença no consumo de memória é surpreendente e parece estar correlacionada com a quantidade de importações, o que minha análise de heap superficial parece sugerir como ligada ao problema também.

Se estiver correto, ~ você ~ qualquer um deve ser capaz de montar um projeto de teste viável, desde que consista em muitos arquivos importados em que cada um contenha alguns conjuntos de regras, para ver a diferença também.

@rjgotten

Se estiver correto, você deve ser capaz de montar um projeto de teste com muitos arquivos importados, cada um contendo alguns conjuntos de regras, e também ver a diferença.

O "você" é a parte importante aqui. 😉

O "você" é a parte importante aqui. 😉

Infeliz escolha de palavras. Quero dizer isso no sentido geral - ou seja, "qualquer um".
Eu mesmo iria cavar mais fundo, mas já tenho muitos pratos girando atualmente.

@rjgotten Você pode me dar uma ideia melhor do que podem ser “muitos arquivos importados”? 100 arquivos separados fariam isso ou estamos falando de 1000?

Além disso, como você percebeu o problema pela primeira vez? Uma compilação falhou devido ao consumo de memória ou aconteceu de você estar observando o uso da memória? Ou você notou que sua máquina está desacelerando?

Não tentei replicar isso ainda, mas ainda espero aprender o que exatamente procurar, para não ter que recorrer a adivinhar e verificar.

100 ou mais fariam isso. É mais ou menos o tamanho do projeto da vida real em que percebi o problema.

Percebi isso pela primeira vez quando um build em execução em nosso ambiente de CI corporativo começou a falhar. Nós os executamos dentro de contêineres Docker com restrições de memória configuradas.

@rjgotten Ainda temos o problema de memória no 3.11.3? Eu removi qualquer cache (referência) do AST nas importações em uma versão anterior, então se as importações estivessem sendo mantidas e as árvores AST estivessem sendo mantidas dentro disso, então isso aumentaria o uso de memória, mas se impedirmos as árvores de serem retidas, isso resolve isso?

@ matthew-dean Sim, o problema ainda está presente no 3.11.3.

Vou tentar criar uma prova de conceito, mas tenho muito a fazer. Coloquei isso na minha lista de tarefas assim que conseguir algum tempo extra.

@ matthew-dean, quero testar isso em um projeto relativamente grande, que estava falhando. Algo que eu preciso saber? Devo usar este branch, https://github.com/less/less.js/tree/release_v3.12.0-RC1 ?

@nfq Você pode tentar, mas a sabedoria predominante neste tópico não é explodir a conversão ES6, que é o que esse branch faz, e em vez disso, obter mais informações para diagnosticar o problema corretamente.

A propósito, tentei inspecionar o objeto less durante a compilação na tentativa de encontrar qualquer objeto persistente e não consegui encontrar nenhum. 🤷‍♂️

Também estou tendo problemas de desempenho. Para um mesmo conjunto de testes:

| Versão | Tempo |
| - | - |
| v3.9.0 | ~ 1.6s |
| v3.10.0 ~ v3.11.1 | ~ 3.6s |
| v3.11.2 + | ~ 12s |

Além de 3.9.0 → 3.10.0 discutido aqui, parece haver uma degradação de desempenho significativa em v3.11.2 . Esta mudança no changelog parece ser suspeita:

3498 Remova o cache da árvore no gerenciador de importação (# 3498)

O mesmo para mim.

Tempos para compilações idênticas (todas no nó 10.19.0):

  • v3.9: 29,9 s
  • v3.10: 76,0 seg
  • v3.12: 89,3 seg

@ jrnail23

Você tem um repo que pode demonstrar esses resultados?

@ matthew-dean, tenho um para https://github.com/less/less.js/issues/3434#issuecomment -672580467: https://github.com/ecomfe/dls-tooling/tree/master/packages/ less-plugin-dls (é um monorepo, que inclui um plugin Less.)

Desculpe, @matthew-dean, não sei. Esses resultados são do produto do meu empregador.

Esses resultados são do produto do meu empregador.

Pelo mesmo motivo, não posso fornecer nenhum arquivo. 😞

@rjgotten @ jrnail23 @Justineo - Apenas curioso, você tem vários casos em que a mesma importação é importada várias vezes em arquivos diferentes?

No meu caso, temos um plugin que injeta uma instrução @import e fornece várias funções personalizadas. O arquivo importado importa outras partes do projeto que finalmente produzirão 1000+ Less variáveis.

@ matthew-dean, temos um arquivo base.less que importa coisas comuns (por exemplo, variáveis ​​de cor, tipografia, etc.).
Alguns grepping rápidos de nosso aplicativo mostram que uma referência base.less (ou seja, <strong i="8">@import</strong> (reference) "../../../less/base.less"; ) é importada por 66 outros arquivos específicos de componente / recurso less , e alguns desses arquivos podem também importe outros arquivos específicos de componentes / recursos menos (que podem fazer referência a base.less ).
Curiosamente, aparentemente também temos outro arquivo less que (provavelmente acidentalmente?) Se importa como referência.

@rjgotten @ jrnail23 @Justineo - Apenas curioso, você tem vários casos em que a mesma importação é importada várias vezes em arquivos diferentes?

Para mim, isso é um 'sim'. O projeto no qual tive problemas pela primeira vez é uma solução que pode ser alterada por skin que faz uso de um arquivo de variáveis ​​centralizadas que está incluído em todos os outros. Além disso, usamos um design baseado em componentes onde temos fábricas de mixins que produzem, por exemplo, certos tipos de botões, ícones de fonte, etc. e partes deles são importadas em vários arquivos também.

Basicamente; cada dependente é configurado para importar estritamente suas dependências e contamos com o compilador Less para desduplicar e sequenciar as importações na ordem correta e garantir a saída CSS correta.

@rjgotten

Postei e apaguei (porque pensei que tinha descoberto, mas ainda não consegui replicar), ainda não há etapas para reproduzir o que as pessoas estão relatando, inclusive em qual processo.

Mesmo algo tão simples como isto:

No 3.11, um dos retentores listados para RuleSet é o ImportManager.

Não consigo encontrar nenhuma evidência disso, mas também não sei como você foi capaz de determinar isso. Não estou familiarizado o suficiente com o Chrome DevTools para saber como alguém pode determinar algo como o que é retido por quê, e esse é um tópico vago o suficiente que o Google não me ajudou. Tipo, as pessoas estão se conectando ao processo do Node? Executando-o no navegador e definindo pontos de interrupção? Quais são as etapas?

Em suma, no ano em que esta edição foi aberta, nenhum dos relatórios teve etapas para reproduzir. Tenho certeza de que todas essas anedotas significam ALGUMA COISA, mas como VOCÊ determinou o que VOCÊ encontrou? Ou você pode pensar em uma configuração que demonstre isso? Gostaria de ajudar a descobrir, mas não sei como reproduzi-lo.

@Justineo Em seu repo, que medidas você tomou para determinar tanto o tamanho da memória ou tempo para compilar? O que você estava usando para medir?

@Justineo Já que você apontou a remoção do cache (o que talvez tenha sido uma má ideia), você pode testar este branch e ver se ele ajuda na velocidade de compilação? https://github.com/less/less.js/tree/cache-restored

Apenas para dar algumas atualizações sobre os experimentos / testes de hoje:

Grunt's shell:test vezes

  • Menos 3,9 - 1,8 s
  • Menos 3.12 (Babel-transpilado para ES5) - 9.2s
  • Menos 3.12 (transpilado de Babel para ES6) - 3.1s
  • Menos 3.12 (TypeScript-transpiled para ES5) - 3.2s

Então, eu acho que o longo e o curto é que o código transpilado é sempre mais lento, e éramos ingênuos em pensar o contrário. Estou um pouco surpreso, pois a transpilação agora é tão "padrão" no mundo do JavaScript. Existe alguma outra pesquisa que apóie isso?

@Justineo Em seu repo, quais etapas você executou para determinar o tamanho da memória ou o tempo de compilação? O que você estava usando para medir?

npm run test produzirá o tempo total decorrido. E em outros projetos, estamos experimentando OOM quando mudamos para 3.12.

Se olharmos para o código gerado pelo TypeScript, obteremos 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;
};

Então, meu palpite é que todas essas definições e chamadas de funções extras se somam com o tempo? A menos que isso seja uma pista falsa, mas não sei mais a que atribuir isso, exceto a transpilação. O que não entendo é porque o TS não consegue produzir código mais próximo do original.

ATUALIZAR:

Se eu tentar colocar os testes de 3.9 no mesmo nível do que está em 3.12, então obtenho essencialmente 1.2s vs. 1.3s. Então, não tenho mais certeza sobre essa diferença porque os testes mudaram. Teria que ser executado exatamente nos mesmos menos arquivos.

@Justineo @rjgotten Eu empurrei alguns ajustes do TypeScript para talvez fazer uma compilação mais eficiente. Você quer construir e experimentar esse ramo? https://github.com/less/less.js/tree/cache-restored

@ matthew-dean 👍 Obrigado! Vou experimentar mais tarde hoje.

Eu testei o branch cache-restored e ele é muito mais rápido do que v3.11.2 +, quase na mesma velocidade que v.3.1.0 ~ 3.11.1.

@Justineo

Eu testei o branch restaurado por cache e é muito mais rápido do que v3.11.2 +, mais ou menos na mesma velocidade que v.3.1.0 ~ 3.11.1.

Bem, isso é promissor. Vamos obter alguns comentários de @rjgotten @ jrnail23 e outros neste tópico. A remoção do cache ocorreu depois que este tópico foi postado (3.11.2); na verdade, foi uma tentativa de remover parte da sobrecarga da memória, mas em casos em que você está importando o mesmo arquivo várias vezes, isso definitivamente poderia ter piorado as coisas.

Então, em resumo, ainda não tenho certeza sobre o problema original ou sua causa (além de ALGO que aconteceu na conversão do código), e ficaria curioso para saber se este branch ainda tem esses problemas, mas como mencionei antes , não há etapas de reprodução claras, então não tenho certeza.

@ matthew-dean, estou tendo um pouco de dificuldade para tentar usar o branch cache-restored (não estou muito certo sobre o que precisa ser feito para usá-lo localmente, pois npm link falha mim).
Você pode publicar uma versão canário / de pré-lançamento que eu possa experimentar?

@ jrnail23

Acho que funcionou. Tente remover Less e instalar com npm i [email protected]+84d40222

@matthew-dean, acabei de experimentar essa versão e ainda é ruim para mim.
Para novas compilações de webpack (sem armazenamento em cache), leva 62,4 segundos para a v3.9, mas 121 segundos para a v3.13.1.
Para compilações em cache, a v3.9 leva 30 segundos e a v3.13.1 leva de 83 a 87 segundos.

@ jrnail23 Você pode tentar remover todos os módulos de nó e instalar [email protected]+b2049010

Menos 3.9 Referência:
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 Você

@ jrnail23 @Justineo @rjgotten

Observe que esta é uma versão 4.0, portanto, você pode encontrar erros se seu código tiver essas alterações importantes:

  • Parênteses são necessários para chamadas mixin (por exemplo, .mixin; não é permitido)
  • O modo matemático padrão de Less agora é divisão entre parênteses, então as barras (pretendidas como matemática) precisam estar entre parênteses

Portanto, embora esteja agora muito mais otimista por ter corrigido o problema, com base nos benchmarks mais recentes, ainda não descobri por que esse problema está acontecendo. Se o desempenho se mantiver para todos os outros, posso dar um resumo de como finalmente reduzi a questão e o que descobri. Em outras palavras, acho que encontrei _onde_ o problema é / estava, mas não necessariamente por quê.

O resultado do mesmo conjunto de testes de https://github.com/less/less.js/issues/3434#issuecomment -672580467:

| Versão | Tempo |
| - | - |
| v3.9.0 | ~ 1.6s |
| v3.10.0 ~ v3.11.1 | ~ 3.6s |
| v3.11.2 + | ~ 12s |
| 4.0.1-alpha.2 + b2049010 | ~ 1.6s |

Posso confirmar que, para meu conjunto de testes específico, o nível de desempenho melhorou para tão rápido quanto a v3.9.0. Muito apreciado Matt! Embora eu não tenha certeza sobre a mudança de intervalo no modo matemático. Alterar isso pode levar à quebra de muitos de nossos aplicativos, portanto, podemos ficar presos na v3.9.0 mesmo que o problema de desempenho seja resolvido.

@Justineo

Embora eu não tenha certeza sobre a mudança de intervalo no modo matemático.

Você pode compilar explicitamente no modo math=always para obter o comportamento matemático anterior. É apenas um padrão diferente.

Detalhamento do problema

TL; DR - Cuidado com o padrão de classe

O problema estava na transpilação de classes tanto no Babel quanto no TypeScript. (Em outras palavras, ambos tinham os mesmos problemas de desempenho com o código transpilado, com Babel sendo um pouco pior.) Agora, anos atrás, quando o padrão de classe foi introduzido, me disseram - e até esse problema, acreditei - que a classe herança em JavaScript é um açúcar sintático para herança funcional.

Resumindo, não é. _ (Editar: bem ... é e não é. Depende do que você entende por "herança" e de como você a definiu, como verá em breve ... ou seja, existem vários padrões para criar "herança" na cadeia de protótipo em JavaScript, e as classes representam apenas um padrão, mas esse padrão é um pouco diferente de todos os outros, portanto, a necessidade de TS / Babel de código auxiliar para "imitar" esse padrão usando funções especializadas.) _

Menos código se parecia com isto:

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

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

var myNode = new Inherited();

Agora, digamos que você queira reescrever isso usando JS "moderno". Na verdade, você pode automatizar esse processo, o que eu fiz, mas de qualquer forma, eu teria escrito da mesma forma, que era:

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

Mesma coisa, certo? Na verdade não. O primeiro criará um objeto com uma propriedade { value: 1 } , e em sua cadeia de protótipos, ele terá um objeto { foo: 'bar' } .

O segundo, porque chamará o construtor em Inherited e Node , criará um objeto com uma estrutura como { value: 1, foo: 'bar' } .

Agora, para o _usuário_, isso realmente não importa, porque você pode acessar value e foo de myNode em ambos os casos. _Funcionalmente_, eles parecem se comportar da mesma forma.

É aqui que entro em pura especulação

Pelo que me lembro em artigos sobre motores JIT como o V8, as estruturas de objetos são realmente muito importantes. Se eu criar um monte de estruturas como { value: 1 } , { value: 2 } , { value: 3 } , { value: 4 } , o V8 cria uma representação estática interna dessa estrutura. Você está basicamente armazenando a estrutura + dados uma vez e, em seguida, os dados mais 3 vezes.

MAS se eu adicionar propriedades diferentes a isso a cada vez, como: { a: 'a', value: 1 } , { b: 'b', value: 2 } , { c: 'c', value: 3 } , { d: 'd', value: 4 } , então essas são 4 estruturas diferentes com 4 conjuntos de dados , mesmo que tenham sido criados a partir do mesmo conjunto de classes originais. Cada mutação de um objeto JS desotimiza as pesquisas de dados, e um padrão de classe que é transpilado em funções causa (talvez) mutações mais exclusivas. (Sinceramente, não tenho ideia se isso é verdade para o suporte de classe nativa em navegadores.)

AFAIK, o que também é verdade é que quanto mais propriedades você tem em um objeto, mais tempo leva uma pesquisa de propriedade individual.

Novamente, para usuários finais, isso raramente importa porque os mecanismos JS são muito rápidos. Mas digamos que você está mantendo um mecanismo que cria e procura propriedades / métodos em MUITOS objetos o mais rápido possível. (Ding ding ding.) De repente, essas pequenas diferenças entre a maneira como TypeScript / Babel "estendem" objetos e a herança de protótipo funcional nativo aumentam muito rapidamente.

Eu escrevi uma implementação básica de um Node e uma função herdada, usando a antiga sintaxe Less e o padrão de classe transpilado com TS. Logo de cara, o nó herdado consome 25% mais memória / recursos, e isso ANTES de qualquer instância do nó herdado ter sido criada.

Agora, considere que alguns nós herdam de outros nós. Isso significa que a representação na memória das classes começa a se multiplicar, assim como suas instâncias herdadas.

Novamente, isso é especulação

Devo enfatizar que levo tudo isso com cautela, porque nunca ouvi falar de uma conversão para as aulas ser tão desastrosa para o desempenho. O que eu _penso_ que está acontecendo é que há alguma combinação especial de pesquisas de objeto / instância que Less está usando que está desotimizando seriamente o JIT. _ (Se algum especialista em mecanismo de JavaScript souber por que as classes transpiladas têm um desempenho muito pior do que os métodos de herança JS nativos neste caso, eu adoraria saber.) _ Tentei criar uma medida de desempenho para criar milhares de objetos herdados usando os dois métodos, e Eu nunca pude ver uma diferença consistente no desempenho.

Portanto, antes de você continuar com "classes em JavaScript são ruins", pode ser algum outro padrão realmente infeliz na base de código Less, juntamente com a diferença em classes transpiladas vs. JS nativo que criou essa queda no desempenho.

Como eu finalmente descobri

Honestamente, nunca suspeitei do padrão de classe. Eu sabia que o código ES5 original era executado mais rápido do que o código Babelificado reverso, mas suspeitei de algo em torno das funções de seta ou sintaxe de propagação em algum lugar. Eu ainda tinha o branch ES5 atualizado, então um dia decidi rodar lebab novamente e usar apenas estas transformações: let,class,commonjs,template . Foi quando descobri que novamente tinha problemas de desempenho. Eu sabia que não era modelagem de strings ou let-to-var; Eu pensei que talvez o requer-para-importações fizesse algo, então brinquei com isso por um tempo. Isso deixou as classes. Portanto, seguindo um palpite, reescrevi todas as classes estendidas de volta à herança funcional. Bam, o desempenho estava de volta.

Um conto preventivo

Então! Lição aprendida. Se o seu projeto está em um código ES5 antigo, e você deseja aquela bondade "moderna" de Babel-ified ou TypeScripted, lembre-se de que tudo o que você transpilar, você não escreveu.

Eu ainda fui capaz de reescrever as classes em algo mais "semelhante a uma classe", com um padrão um pouco mais sustentável , mas mais ou menos mantendo o padrão de herança funcional original do nó Less, e deixarei uma nota forte no projeto para um futuro mantenedor nunca mais fará isso.

Você pode compilar explicitamente no modo math = always para obter o comportamento matemático anterior. É apenas um padrão diferente.

Estou ciente disso. Temos muitas bases de código Less em muitas equipes diferentes, portanto, interromper as alterações, mesmo nas opções de compilação padrão, aumentaria os custos de comunicação. Não tenho certeza se os benefícios compensam o prejuízo.

Obrigado pela análise detalhada!

Eu vi o uso de Object.assign na saída compilada, o que significa que só funciona em navegadores que oferecem suporte à sintaxe nativa ES class menos que polyfills sejam necessários agora. Portanto, podemos apenas usar a sintaxe nativa sem transpilar para ES5 se pretendemos descartar o suporte para ambientes mais antigos (por exemplo, IE11, Nó 4, ...)?

Ao mesmo tempo, acho melhor se pudermos separar a correção de degradação de desempenho e as alterações de interrupção, o que significa acertar a correção de desempenho na v3 e incluir as alterações de interrupção apenas na v4.

@ matthew-dean
O fato de que as classes ES são o culpado é louco assustador.
Obrigado pela análise elaborada.

Mostra: _composição sobre herança_ 😛

Embora FYI; se você deseja uma cadeia de herança protypal mais limpa, você realmente deve fazê-lo um pouco diferente do seu exemplo.
Se você usar Object.create assim:

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();

em seguida, você aplaina as propriedades nas próprias instâncias, que compartilham a mesma forma; os protótipos compartilham forma; _e_ você evita ter que rastrear as cadeias de protótipos para cada acesso de propriedade. 😉

@Justineo

Eu vi o uso de Object.assign na saída compilada, o que significa que só funciona em navegadores que suportam sintaxe de classe ES nativa, a menos que polyfills sejam necessários agora. Portanto, podemos apenas usar a sintaxe nativa sem transpilar para ES5 se pretendemos descartar o suporte para ambientes mais antigos (por exemplo, IE11, Nó 4, ...)?

Eu não acho que isso esteja certo, ou seja, Object.assign pousou pouco antes da implementação das classes, mas seu ponto foi aceito. Eu só esperava evitar escrever [Something].prototype.property repetidamente: /

Tecnicamente, tudo ainda está transpilando, então eu não sei. O objetivo original era uma base de código mais fácil de manter / legível. Se você precisa de um polyfill Object.assign em algum ambiente, que seja. Seria em uma versão de algo que Less não suporta.

Ao mesmo tempo, acho melhor se pudermos separar a correção de degradação de desempenho e as alterações de interrupção, o que significa acertar a correção de desempenho na v3 e incluir as alterações de interrupção apenas na v4.

Tenho pensado nisso e acho que também é um ponto justo. Só estou tentando fazer minha cabeça não explodir construindo / mantendo 3 versões principais do Less.

@rjgotten AFAIK isso é o que um padrão de classe deve fazer, exatamente como você o definiu. A menos que eu não esteja vendo uma diferença crítica. Então 🤷‍♂️. Não vou tentar alterar o padrão de herança do Nó de Less novamente se estiver funcionando bem (além de poder remover esta chamada Object.assign como @Justineo sugeriu.)

@Justineo @rjgotten Você pode tentar isto: [email protected]+b1390a54

Apenas tentei:

| Versão | Tempo |
| - | - |
| v3.9.0 | ~ 1.6s |
| v3.10.0 ~ v3.11.1 | ~ 3.6s |
| v3.11.2 + | ~ 12s |
| 4.0.1-alpha.2 + b2049010 | ~ 1.6s |
| 3.13.0-alpha.10 + b1390a54 | ~ 4.7s |

Ps. Testado com Node.js v12.13.1.

Eu sou o que.

Não tive tempo para configurar benchmarks separados.
Mas de uma perspectiva de teste de integração, aqui estão alguns números do mundo real: os resultados cumulativos de um projeto Webpack de nível de produção que usa Less.

Versão | Tempo | Peak Memory
: ------------------------ | --------: | ------------:
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 mim, o 3.13.0 fornece os _melhores_ resultados ... 🙈

3.11.3 também parece não ter o uso de memória runaway que eu vi antes com a versão 3.11.1.
Mas os betas 4.0.1 e 3.13.0 são melhores ainda.

O 3.13.1 que restaurou o cache não ajuda a melhorar meus tempos de compilação do mundo real e apenas aumenta o uso de memória.

@rjgotten Sim, acho que o problema é que, neste tópico, as pessoas estão medindo coisas diferentes, e todos, até agora, têm essencialmente dados privados que são impossíveis de replicar (sem submeter ao Less benchmarking ou traduzir isso em um PR). O Less tem um teste de benchmark, que eu poderia usar contra diferentes clones do repo, para verificar por mim mesmo as diferenças no tempo de análise / avaliação, mas onde o cache (atualmente) não se aplica.

Aqui está uma versão publicada com as alterações de herança na árvore e com o cache da árvore de análise restaurado: [email protected]+e8d05c61

Caso de teste

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

Resultados

| Versão | Tempo |
| - | - |
| v3.9.0 | ~ 1.6s |
| v3.10.0 ~ v3.11.1 | ~ 3.6s |
| v3.11.2 + | ~ 12s |
| 4.0.1-alpha.2 | ~ 1.6s |
| 3.13.0-alpha.10 | ~ 4.7s |
| 3.13.0-alpha.12 | ~ 1.6s |

Versão Node.js

v12.13.1


Para mim, essa versão parece funcionar perfeitamente. Vou pedir aos meus colegas para experimentar e verificar.

Funciona um pouco pior do que alpha.10 para mim, mas também pode ser apenas variação:

Versão | Tempo | Peak Memory
: ------------------------ | --------: | ------------:
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 meu código ainda não é compatível com as alterações de 4.0.1-alpha.2+b2049010 . Vou ver se consigo resolver meus problemas e tentar.

@rjgotten Sim, a diferença parece pequena para o seu caso de teste.

Acho que o resultado disso é que posso preparar uma versão 3.xe uma versão 4.0. Eu não estava ansioso para empurrar o 4.0 com esse problema não resolvido. Felizmente, parece que sim.

O outro item que farei é abrir um problema para qualquer um pegar, que é colocar um pipeline de CI que falhe Menos se ficar abaixo de um determinado limite de referência.

@ jrnail23 Seria ótimo se você pudesse fazer modificações para executar o alpha 4.0, mas você pode tentar [email protected]+e8d05c61 enquanto isso?

@ matthew-dean, estou recebendo o seguinte para minha construção:

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

Portanto, parece que você voltou para onde queria estar! Bom trabalho!

Bom trabalho!

Sim, vou para o segundo; terceiro; e em quarto lugar.
Esta foi uma regressão de desempenho desagradável, desagradável que realmente exigiu _muito_ esforço para limpar.

Trabalho _muito bem_ executado.

Ok time! Vou publicar a compilação 3.x, e algum tempo depois, talvez na próxima semana, publicar 4.0. Ambos agora devem ter as correções de desempenho. Obrigado a todos que ajudaram a depurar!

A propósito, estou atualmente no processo de conversão da base de código Less.js para TypeScript e planejo usar a oportunidade para fazer algum ajuste / refatoração de desempenho. Estou aberto a ajudar se alguém estiver interessado! https://github.com/matthew-dean/less.js/compare/master...matthew-dean : próximo

Algo específico onde você prefere alguns olhos extras? O PR é muito grande, então não sei por onde começar

@kevinramharak Essa é uma pergunta justa. Para ser honesto, encontrei obstáculos inesperados ao converter para TypeScript (erros que se tornaram difíceis de resolver) e agora estou repensando em fazer isso. Menos está funcionando bem do jeito que está, e muitos dos motivos pelos quais eu queria converter (para tornar a refatoração / adição de novos recursos de linguagem mais fácil) agora são irrelevantes, pois decidi mover minhas ideias de novos recursos de linguagem para um novo pré- linguagem de processamento. Não quero se autopromover demais, então mande um DM para mim no Twitter ou no Gitter se quiser detalhes.

Esta página foi útil?
0 / 5 - 0 avaliações