Olá, meu nome é Dmitry Karlovsky e eu ... desempregado. Portanto, tenho muito tempo livre para tocar música, esportes, criatividade, idiomas, conferências em JS e ciência da computação. Vou falar sobre as pesquisas mais recentes no campo da divisão semiautomática de cálculos longos em pequenos quanta de vários milissegundos, o que resultou em uma biblioteca em miniatura $mol_fiber
. Mas primeiro, vamos descrever os problemas que resolveremos ..

Esta é uma versão em texto da performance de mesmo nome no HolyJS 2018 Piter . Você pode lê-lo como um artigo ou abri-lo na interface de apresentação ou assistir a um vídeo .
Problema: Baixa capacidade de resposta
Se queremos ter 60 quadros por segundo estáveis, temos apenas 16 com uma ninharia de milissegundos para fazer todo o trabalho, incluindo o que o navegador faz para mostrar os resultados na tela.
Mas e se tomarmos o fluxo por mais tempo? Em seguida, o usuário observará uma interface atrasada, inibindo a animação e similares da degradação do UX.

Problema: Não há escapatória
Acontece que, enquanto realizamos os cálculos, o resultado não é mais interessante para nós. Por exemplo, temos um pergaminho virtual, o usuário o puxa ativamente, mas não podemos acompanhá-lo e não podemos renderizar a área real até que a renderização anterior retorne o controle para processar eventos do usuário.

Idealmente, não importa quanto tempo trabalhemos, devemos continuar processando eventos e poder cancelar a qualquer momento o trabalho que começamos, mas ainda não concluímos.
Eu sou rápido e sei disso
Mas e se o nosso trabalho não for um, mas vários, mas um fluxo? Imagine que você dirige com o lótus amarelo recém-adquirido e vai até a travessia ferroviária. Quando está livre, você pode colocá-lo em uma fração de segundo. Mas ..

Problema: sem simultaneidade
Quando a travessia é ocupada por um trem de um quilômetro, é preciso ficar de pé e esperar dez minutos até que ela passe. Não é por isso que você comprou um carro esportivo, certo?

E como seria legal se este trem fosse dividido em 10 trens de 100 metros cada e houvesse vários minutos entre eles para passar! Você não chegaria tão tarde então.
Então, quais são as soluções para esses problemas no mundo JS agora?
Solução: Trabalhadores
A primeira coisa que vem à mente: vamos colocar todos os cálculos complexos em um segmento separado? Para fazer isso, temos um mecanismo para WebWorkers.

Eventos do fluxo da interface do usuário são passados para o trabalhador. Lá eles são processados e as instruções sobre o que e como alterar na página já são passadas de volta. Assim, salvamos o fluxo da interface do usuário de uma grande camada de computação, mas nem todos os problemas são resolvidos dessa maneira e, além disso, novos são adicionados.
Trabalhadores: problemas: (des) serialização
A comunicação entre fluxos ocorre enviando mensagens serializadas em um fluxo de bytes, transferidas para outro fluxo e, em seguida, são analisadas em objetos. Tudo isso é muito mais lento que uma chamada de método direta em um único encadeamento.

Trabalhadores: problemas: somente assíncrono
As mensagens são transmitidas estritamente de forma assíncrona. E isso significa que alguns recursos solicitados não estão disponíveis. Por exemplo, você não pode parar a ascensão de um evento de interface do usuário de um trabalhador, pois quando o manipulador é iniciado, o evento no encadeamento da interface do usuário já completará seu ciclo de vida.

Trabalhadores: problemas: APIs limitadas
As seguintes APIs não estão disponíveis para nós nos trabalhadores.
- DOM, CSSOM
- Tela
- Localização geográfica
- História e Localização
- Sincronizar solicitações http
- XMLHttpRequest.responseXML
- Janela
Trabalhadores: Problemas: Não é possível cancelar
E, novamente, não temos como parar os cálculos no woker.

Sim, podemos parar o trabalhador inteiro, mas isso interromperá todas as tarefas nele.
Sim, você pode executar cada tarefa em um trabalhador separado, mas consome muitos recursos.
Solução: Reagir fibra
Certamente muitos ouviram o FaceBook reescrever heroicamente o React, dividindo todos os cálculos nele em um monte de pequenas funções lançadas por um agendador especial.

Não entrarei em detalhes de sua implementação, pois esse é um grande tópico separado. Anotarei apenas alguns recursos, pelos quais pode não ser adequado para você.
Reagir fibra: Reagir necessário
Obviamente, se você usar Angular, Vue ou outra estrutura diferente de React, o React Fiber será inútil para você.

React Fiber: Somente renderização
React - cobre apenas a camada de renderização. Todas as outras camadas do aplicativo são deixadas sem quantização.

O React Fiber não o salvará quando você precisar, por exemplo, filtrar um grande bloco de dados por condições difíceis.
React Fiber: A quantização está desativada
Apesar do suporte alegado para quantização, ele ainda está desativado por padrão, pois quebra a compatibilidade com versões anteriores.

A quantização no React ainda é uma coisa experimental. Cuidado!
Reagir fibra: depuração é dor
Quando você ativa a quantização, o callstack não corresponde mais ao seu código, o que complica bastante a depuração. Mas retornaremos a esta questão.

Solução: quantização
Vamos tentar generalizar a abordagem React Fiber para se livrar das desvantagens mencionadas. Queremos permanecer em um fluxo, mas dividimos cálculos longos em pequenas quanta, entre as quais o navegador pode processar as alterações já feitas na página e responderemos a eventos.

Acima, você vê um longo cálculo que parou o mundo inteiro em mais de 100ms. E de baixo - o mesmo cálculo, mas dividido em fatias de tempo de cerca de 16ms, o que deu uma média de 60 quadros por segundo. Como geralmente não sabemos quanto tempo os cálculos levarão, não podemos dividi-lo manualmente em 16ms de antecedência. portanto, precisamos de algum tipo de mecanismo de tempo de execução que mede o tempo necessário para concluir a tarefa e quando o quantum é excedido, o que interrompe a execução até o próximo quadro de animação. Vamos pensar em quais mecanismos temos para implementar tarefas suspensas aqui ..
Concorrência: fibras - stackfull coroutines
Em idiomas como Go e D, existe um idioma como uma "corotina com uma pilha", também é uma "fibra" ou "fibra".
import { Future } from 'node-fibers' const one = ()=> Future.wait( future => setTimeout( future.return ) ) const two = ()=> one() + 1 const three = ()=> two() + 1 const four = ()=> three() + 1 Future.task( four ).detach()
No exemplo de código, você vê a one
função, que pode pausar a fibra atual, mas possui uma interface completamente síncrona. As funções two
, three
e four
são funções síncronas regulares que não sabem nada sobre fibra. Neles, você pode usar todos os recursos do javascript na íntegra. E, finalmente, na última linha, simplesmente executamos as four
funções em uma fibra separada.
O uso de fibras é bastante conveniente, mas para suportá-las, você precisa de suporte em tempo de execução, que a maioria dos intérpretes JS não possui. No entanto, para o NodeJS, há uma extensão nativa node-fibers
que adiciona esse suporte. Infelizmente, nenhum navegador está disponível em nenhum navegador.
Simultaneidade: FSM - coroutines sem pilha
Em linguagens como C # e agora JS, há suporte para "coroutines sem pilha" ou "funções assíncronas". Tais funções são uma máquina de estado sob o capô e não sabem nada sobre a pilha; portanto, elas precisam ser marcadas com a palavra-chave especial "async", e os locais onde podem ser pausadas são "aguardam".
const one = ()=> new Promise( done => setTimeout( done ) ) const two = async ()=> ( await one() ) + 1 const three = async ()=> ( await two() ) + 1 const four = async ()=> ( await three() ) + 1 four()
Como podemos precisar adiar o cálculo a qualquer momento, acontece que quase todas as funções no aplicativo precisarão ser assíncronas. Isso não é apenas a complexidade do código, mas também afeta muito o desempenho. Além disso, muitas APIs de aceitação de retorno de chamada ainda não oferecem suporte a retornos de chamada assíncronos. Um exemplo impressionante é o método de reduce
de qualquer matriz.
Simultaneidade: semi-fibras - reinicia
Vamos tentar fazer algo semelhante à fibra, usando apenas os recursos disponíveis em qualquer navegador moderno.
import { $mol_fiber_async , $mol_fiber_start } from 'mol_fiber/web' const one = ()=> $mol_fiber_async( back => setTimeout( back ) ) const two = ()=> one() + 1 const three = ()=> two() + 1 const four = ()=> three() + 1 $mol_fiber_start( four )
Como você pode ver, as funções intermediárias não sabem nada sobre interrupção - isso é JS comum. Somente a one
função sabe da possibilidade de suspensão. Para abortar o cálculo, ela simplesmente lança Promise
como uma exceção. Na última linha, executamos a função four
em uma pseudo-fibra separada, que monitora as exceções lançadas no interior e, se o Promise
chegar, assina sua resolve
e reinicia a fibra.
Para mostrar como as pseudo-fibras funcionam, escreveremos um código complicado.

Vamos imaginar que a função step
aqui grave algo no console e faça algum outro trabalho duro por 20ms. E a função walk
chama o step
duas vezes, registrando todo o processo. No meio, ele mostrará o que agora é exibido no console. E à direita está o estado da árvore de pseudo-fibra.
$ mol_fiber: sem quantização
Vamos executar esse código e ver o que acontece ..

Até agora, tudo é simples e óbvio. A árvore de pseudo-fibra, é claro, não está envolvida. E tudo ficaria bem, mas esse código é executado por mais de 40 ms, o que é inútil.
$ mol_fiber: cache primeiro
Vamos agrupar as duas funções em um invólucro especial que o executa em uma pseudo-fibra e ver o que acontece.

Aqui vale a pena prestar atenção ao fato de que para cada local de chamada da one
função dentro da fibra de walk
, uma fibra separada foi criada. O resultado da primeira chamada foi armazenado em cache, mas, em vez da segunda, a Promise
foi lançada, pois tínhamos esgotado nosso tempo.
$ mol_fiber: cache segundo
Lançada no primeiro quadro, o Promise
será resolvido automaticamente no próximo, o que levará ao reinício da fibra de walk
.

Como você pode ver, devido ao reinício, produzimos novamente “start” e “first done” para o console, mas “first begin” não está mais lá, pois está na fibra com o cache preenchido anteriormente, razão pela qual seu manipulador é mais não chamado. Quando o cache da fibra de walk
é preenchido, todas as fibras incorporadas são destruídas, pois a execução nunca as alcançará.
Então, por que você first begin
imprimir uma vez e first done
duas? É tudo sobre idempotência. console.log
- operação não idempotente, quantas vezes você o chama, tantas vezes ele adiciona uma entrada ao console. Mas a fibra que está sendo executada em outra fibra é idempotente, apenas executa o identificador na primeira chamada e, nos retornos subsequentes, imediatamente o resultado do cache, sem levar a efeitos colaterais adicionais.
$ mol_fiber: idempotência primeiro
Vamos agrupar console.log
em uma fibra, tornando-a idempotente e ver como o programa se comporta.

Como você pode ver, agora na árvore de fibras temos entradas para cada chamada para a função de log
.
$ mol_fiber: idempotência em segundo
Na próxima reinicialização da fibra walk
, chamadas repetidas para a função log
não levam mais a chamadas para o console.log
real, mas assim que chegamos à execução das fibras com um cache vazio, as chamadas para console.log
retomadas.

Observe que no console agora não exibimos nada supérfluo - exatamente o que seria exibido no código síncrono sem fibra e quantificação.
$ mol_fiber: break
Como o cálculo é interrompido? No início do quantum, um prazo é definido. E antes de iniciar cada fibra, é verificado se a alcançamos. E se você chegar, então Promise
corre, o que é resolvido no próximo quadro e inicia um novo quantum.
if( Date.now() > $mol_fiber.deadline ) { throw new Promise( $mol_fiber.schedule ) }
$ mol_fiber: prazo
O prazo para o quantum é fácil de definir. 8 milissegundos são adicionados ao horário atual. Por que exatamente 8, porque existem até 16 para preparar a foto? O fato é que não sabemos antecipadamente quanto tempo o navegador precisará renderizar, portanto, precisamos deixar algum tempo para que ele funcione. Mas às vezes acontece que o navegador não precisa renderizar nada e, com 8 ms de quanta, podemos inserir outro quantum no mesmo quadro, o que fornecerá um pacote denso de quanta com tempo de inatividade mínimo do processador.
const now = Date.now() const quant = 8 const elapsed = Math.max( 0 , now - $mol_fiber.deadline ) const resistance = Math.min( elapsed , 1000 ) / 10
Mas se lançarmos uma exceção a cada 8ms, a depuração com a exceção ativada se transformará em um pequeno ramo do inferno. Precisamos de algum mecanismo para detectar esse modo de depurador. Infelizmente, isso só pode ser entendido indiretamente: uma pessoa leva cerca de um segundo para entender se deve continuar executando ou não. E isso significa que, se o controle não retornar ao script por muito tempo, o depurador será interrompido ou haverá um cálculo pesado. Para sentar em ambas as cadeiras, adicionamos ao quantum 10% do tempo decorrido, mas não mais que 100 ms. Isso não afeta muito o FPS, mas reduz a frequência de parada do depurador em uma ordem de magnitude devido à quantização.
Depuração: try / catch
Como estamos falando sobre depuração, o que você acha, em que lugar desse código o depurador para?
function foo() { throw new Error( 'Something wrong' ) // [1] } try { foo() } catch( error ) { handle( error ) throw error // [2] }
Como regra, ele precisa parar onde a exceção é lançada pela primeira vez, mas a realidade é que ele para apenas onde foi lançada pela última vez, o que geralmente é muito longe de onde ocorreu. Portanto, para não complicar a depuração, as exceções nunca devem ser capturadas através do try-catch. Mas mesmo sem manipulação de exceção é impossível.
Depuração: eventos não manipulados
Normalmente, um tempo de execução fornece um evento global que ocorre para cada exceção não capturada.
function foo() { throw new Error( 'Something wrong' ) } window.addEventListener( 'error' , event => handle( event.error ) ) foo()
Além da inconveniência, essa solução tem uma desvantagem que todas as exceções se enquadram aqui e é bastante difícil entender de qual fibra e fibra se o evento ocorreu.
Depuração: Promessa
Promessas são a melhor maneira de lidar com exceções.
function foo() { throw new Error( 'Something wrong' ) } new Promise( ()=> { foo() } ).catch( error => handle( error ) )
A função passada para o Promise é chamada imediatamente, de forma síncrona, mas a exceção não é capturada e para com segurança o depurador no lugar de sua ocorrência. Um pouco mais tarde, de forma assíncrona, ele já chama o manipulador de erros, no qual sabemos exatamente qual fibra causou a falha e qual falha. Este é precisamente o mecanismo usado no $ mol_fiber.
Rastreamento de pilha: reagir fibra
Vamos dar uma olhada no rastreamento de pilha que você obtém no React Fiber ..

Como você pode ver, temos muita reação no estômago. Do útil aqui, apenas o ponto de ocorrência da exceção e os nomes dos componentes são mais altos na hierarquia. Não muito.
Rastreamento de pilha: $ mol_fiber
No $ mol_fiber, obtemos um rastreamento de pilha muito mais útil: sem tripas, apenas pontos específicos no código do aplicativo através do qual houve uma exceção.

Isto é conseguido através do uso da pilha nativa, promessas e remoção automática dos intestinos. Se desejar, você pode expandir o erro no console, como na captura de tela, e ver as entranhas, mas não há nada de interessante.
$ mol_fiber: handle
Então, para interromper um quantum, Promise é lançado.
limit() { if( Date.now() > $mol_fiber.deadline ) { throw new Promise( $mol_fiber.schedule ) }
Mas, como você pode imaginar, Promise pode ser absolutamente qualquer coisa - para uma fibra, de um modo geral, não importa o que esperar: o próximo quadro, a conclusão do carregamento de dados ou algo mais.
fail( error : Error ) { if( error instanceof Promise ) { const listener = ()=> self.start() return error.then( listener , listener ) }
O Fiber simplesmente se inscreve para resolver promessas e reinicializações. Mas não é necessário lançar e receber promessas manualmente, porque o pacote inclui vários invólucros úteis.
$ mol_fiber: funções
Para transformar qualquer função síncrona em uma fibra idempotente, envolva-a em $mol_fiber_func
..
import { $mol_fiber_func as fiberize } from 'mol_fiber/web' const log = fiberize( console.log ) export const main = fiberize( ()=> { log( getData( 'goo.gl' ).data ) } )
Aqui, tornamos o console.log
idempotente e o main
ensinou a interromper enquanto aguardava o download.
$ mol_fiber: tratamento de erros
Mas como responder a exceções se não queremos usar o try-catch
? Em seguida, podemos registrar o manipulador de erros com $mol_fiber_catch
...
import { $mol_fiber_func as fiberize , $mol_fiber_catch as onError } from 'mol_fiber' const getConfig = fiberize( ()=> { onError( error => ({ user : 'Anonymous' }) ) return getData( '/config' ).data } )
Se retornarmos algo diferente do erro nele, será o resultado da fibra atual. Neste exemplo, se não for possível fazer o download da configuração do servidor, a função getConfig
retornará a configuração por padrão.
$ mol_fiber: métodos
Obviamente, você pode agrupar não apenas funções, mas também métodos usando um decorador.
import { $mol_fiber_method as action } from 'mol_fiber/web' export class Mover { @action move() { sendData( 'ya.ru' , getData( 'goo.gl' ) ) } }
Aqui, por exemplo, carregamos dados do Google e no Yandex.
$ mol_fiber: promessas
Para baixar dados do servidor, basta levar, por exemplo, a fetch
assíncrona da função e, com um movimento do pulso, transformá-la em síncrona.
import { $mol_fiber_sync as sync } from 'mol_fiber/web' export const getData = sync( fetch )
Essa implementação é boa para todos, mas não suporta o cancelamento de uma solicitação quando uma árvore de fibras é destruída; portanto, precisamos usar uma API
mais confusa.
$ mol_fiber: solicitação de cancelamento
import { $mol_fiber_async as async } from 'mol_fiber/web' function getData( uri : string ) : Response { return async( back => { var controller = new AbortController(); fetch( uri , { signal : controller.signal } ).then( back( res => res ) , back( error => { throw error } ) , ) return ()=> controller.abort() } ) }
A função passada para o wrapper async
é chamada apenas uma vez e o wrapper de back
é passado para ele, no qual é necessário agrupar os retornos de chamada. Portanto, nesses retornos de chamada, você deve retornar o valor ou lançar uma exceção. Qualquer que seja o resultado do retorno de chamada, ele também será o resultado da fibra. Observe que, no final, retornamos uma função que será chamada em caso de destruição prematura da fibra.
$ mol_fiber: cancelar resposta
No lado do servidor, também pode ser útil cancelar o cálculo quando o cliente cair. Vamos implementar um wrapper sobre o midleware
que criará uma fibra na qual o midleware
original será executado. , , , .
import { $mol_fiber_make as Fiber } from 'mol_fiber' const middle_fiber = middleware => ( req , res ) => { const fiber = Fiber( ()=> middleware( req , res ) ) req.on( 'close' , ()=> fiber.destructor() ) fiber.start() } app.get( '/foo' , middle_fiber( ( req , res ) => { // do something } ) )
$mol_fiber: concurrency
, . , 3 : , , - ..

: , . . , , .
$mol_fiber: properties
, ..
Pros:
- Runtime support isn't required
- Can be cancelled at any time
- High FPS
- Concurrent execution
- Debug friendly
- ~ 3KB gzipped
Cons:
- Instrumentation is required
- All code should be idempotent
- Longer total execution
$mol_fiber — , . — , . , , . , , , , . , . .
Links
Call back

: , , )
: , .
: . , .
: . , . , .
: , . , )
: , .
: - . , , .
: . , , .
: , . 16ms, ? 16 8 , 8, . , . , «».
: — . Obrigada
: . , . !
: , . .
: , , , , , / , .
: , .
: .
: , . mol.
: , , . , , , .
: .
: , . , $mol, , .
: , , . — . .
: - , .
: $mol , . (pdf, ) , .
: , . , .
: , ) .
: . .
: In some places I missed what the reporter was saying. The conversation was about how to use the "Mola" library and "why?". But how it works remains a mystery for me.To smoke an source code is for the overhead.
: , .
: . , . . .
: : . - (, ). , : 16?
Mais ou menos : eu não trabalhei com fibra. No relatório, ouvi a teoria do trabalho da fibra. Mas eu absolutamente não descobri como usar o mol_fiber em casa ... Pequenos exemplos são ótimos, mas como isso pode ser aplicado em um aplicativo grande com 30fps para acelerar para 60fps - não havia entendimento. Agora, se o autor prestasse mais atenção a este e menos design do módulo interno - a classificação seria maior.