Node.js Por Baixo dos Panos #1 - Conhecendo nossas ferramentas

_staticvoid

Lucas Santos

Posted on September 3, 2019

Node.js Por Baixo dos Panos #1 - Conhecendo nossas ferramentas

Esta é uma tradução do meu artigo original

Recentemente eu fui convidado para fazer uma palestra em uma das maiores conferências brasileiras, a The Conf. E essa série de artigos foi criada para essa palestra em específico.

O objetivo da conferência é criar conteúdo em inglês, de forma que todas as pessoas do mundo possam se beneficiar deste conteúdo no futuro assistindo as palestras gravadas on-line. E não apenas nós, brasileiros que falamos Português.

Eu estava sentindo que o conteúdo que eu entregava nas minhas outras palestras não estavam tão avançados e profundos quanto eu gostaria que eles fossem. Então eu decidi fazer uma palestra sobre Node.js, JavaScript e como todo o ecossistema do Node.js, de fato, funciona. Isto porque a maioria dos programadores hoje em dia apenas usa as coisas, mas ninguém nunca para para pensar como elas funcionam ou o que elas fazem.

No mundo atual isto é até "aceitável", nós temos um monte de libs que removeram a necessidade que tínhamos de ler livros e mais livros sobre arquitetura de processadores só para que pudéssemos criar um relógio escrito em assembly. No entanto, isso nos tornou preguiçosos, usar coisas sem saber nada sobre elas criou uma atmosfera onde todos só sabem o suficiente e só leem o suficiente para criar o que precisam, e esquecem sobre todos os conceitos que vem junto com aquilo. Afinal de contas, copiar e colar código do Stack Overflow é muito mais simples.

Então, com isso em mente, eu decidi dar um mergulho nos internals do Node.js, pelo menos para mostrar como tudo se conecta e como a maioria dos nossos códigos realmente é executado no ecossistema Node.js.

Este é o primeiro de vários artigos sobre este tema em particular, o qual compilei e estudei para poder criar a minha palestra. Eu não vou postar todas as referências bibliográficas neste primeiro artigo, uma vez que são muitas mesmo. Ao invés disso, vou dividir todo este conteúdo em vários artigos, cada um deles contendo uma parte do estudo e, no último artigo, vou por as referências e os slides para a minha palestra.

Espero que gostem :D

Objetivos

O objetivo dessa série é tornar possível e mais tangível o entendimento de como o Node.js funciona internamente, isto é mais por conta de que o Node e o JavaScript são celebridades mundiais por conta de suas libs, mas ninguém sabe, de fato, como eles funcionam embaixo do capô. Para explicar tudo isso, vamos cobrir uma série de tópicos:

  1. O que é o Node.js
    1. Breve história
    2. Breve história do JS em si
    3. Elementos que são parte do Node.js
  2. Um exemplo com uma chamada de leitura de arquivos
  3. JavaScript
    1. Como funciona?
      1. Callstack
    2. Alocação de memória
  4. Libuv
    1. O que é a libuv?
    2. Para que precisamos dela?
    3. EventLoop
    4. Microtasks e Macrotasks
  5. V8
    1. O que é o v8
    2. Overview
      1. Abstract Syntax Tree usando Esprima
    3. Pipeline de compilação antiga
      1. O full codegen
      2. Crankshaft
        1. Hydrogen
        2. Lithium
    4. Nova pipeline de compilação
      1. Ignition
      2. TurboFan
        1. Hidden Classes e alocação de variáveis
    5. Garbage collection
  6. Otimizações de compilação
    1. Constant Folding
    2. Análise de indução de variáveis
    3. Rematerialização
    4. Removendo recursão
    5. Deflorestamento
    6. Otimizações Peephole
    7. Expansão inline
    8. Cache Inline
    9. Eliminação de código morto
    10. Reordenação de blocos de código
    11. Jump Threading
    12. Trampolines
    13. Eliminação de sub-expressões comuns

O que é o Node.js

O Node.js é definido por Ryan Dahl (o criador original) como um "conjunto de bibliotecas que são executadas no mecanismo V8, permitindo executar o código JavaScript no servidor", a Wikipedia define como "um runtime JavaScript open-source, multi-plataforma que executa código fora de um navegador ".

Essencialmente, o Node.js é um runtime que nos permite executar o JS fora do domínio do navegador. No entanto, essa não é a primeira implementação do Javascript no servidor. Em 1995, a Netscape implementou o chamado Netscape Enterprise Server, que permitia aos usuários executar o LiveScript (o jovem JavaScript) no servidor.

Breve história do Node.js

Node.js foi lançado pela primeira vez em 2009, escrito por Ryan Dahl, que mais tarde foi patrocinado pela Joyent. Toda a origem do runtime começa com as possibilidades limitadas do Apache HTTP Server - o servidor da web mais popular na época - para lidar com muitas conexões simultâneas. Além disso, Dahl criticou a maneira de escrever código, que era sequencial, isso poderia levar a um bloqueio inteiro do processo ou várias pilhas de execução no caso de várias conexões simultâneas.

O Node.js foi apresentado pela primeira vez na JSConf EU, em 8 de novembro de 2009. Combinou o V8, um event-loop fornecido pela - recentemente escrita - libuv e uma API de I/O de baixo nível.

Breve história do JavaScript

O Javascript é definido como uma "linguagem de script interpretada de alto nível" que está em conformidade com a especificação ECMAScript e é mantida pelo TC39. Criado em 1995 por Brendan Eich enquanto trabalhava em uma linguagem de script para o navegador Netscape. O JavaScript foi criado exclusivamente para atender à idéia de Marc Andreessen de ter uma "linguagem cola" entre HTML e web designers, que deve ser fácil de usar para montar componentes como imagens e plug-ins, de forma que o código fosse escrito diretamente no markup da página da web.

Brendan Eich foi recrutado para implementar a linguagem Scheme no Netscape, mas, devido a uma parceria entre a Sun Microsystems e a Netscape, a fim de incluir o Java no navegador Netscape, seu foco foi mudado para a criação de uma linguagem com uma sintaxe semelhante à Java. Para defender a idéia do JavaScript contra outras propostas, Eich escreveu, em 10 dias, um protótipo funcional.

A especificação da ECMA veio um ano depois, quando a Netscape enviou a linguagem JavaScript à ECMA International para criar uma especificação padrão, que outros fornecedores de navegadores poderiam implementar com base no trabalho realizado na Netscape. Isso levou ao primeiro padrão ECMA-262 em 1997. O ECMAScript-3 foi lançado em dezembro de 1999 e é a linha de base moderna da linguagem JavaScript. O ECMAScript 4 foi estagnado porque a Microsoft não tinha intenção de cooperar ou implementar o JavaScript de forma correta no IE, apesar de não terem nenhuma ideia para substituir o JS e terem uma implementação parcial, mas divergente, da linguagem .NET no lado do servidor.

Em 2005, as comunidades de código aberto e desenvolvedores começaram a trabalhar para revolucionar o que poderia ser feito com JavaScript. Primeiro, em 2005, Jesse James Garret publicou o rascunho do que seria chamado AJAX, o que resultou no renascimento do uso do JavaScript liderado por bibliotecas de código aberto como jQuery, Prototype e MooTools. Em 2008, depois que toda a comunidade começou a usar o JS novamente, o ECMAScript 5 foi anunciado e lançado em 2009.

Elementos que compõem o Node.js

O Node.js é composto de algumas dependências:

  • V8
  • Libuv
  • http-parser
  • c-ares
  • OpenSSL
  • zlib

A imagem abaixo tem a explicação perfeita:

Take from Samer Buna's Pluralsight course: Advanced Node.js

Tendo mostrado isso, podemos dividir o Node.js em duas partes: V8 e a libuv. O V8 é mais ou menos 70% em C++ e 30% em JavaScript, enquanto a libuv é completamente escrita em C.

Nosso exemplo - Uma chamada de leitura de dados

Para alcançar nosso objetivo (e ter um roteiro claro do que vamos fazer), começaremos escrevendo um programa simples que lê um arquivo e o imprime na tela. Você verá que esse código não será o código ideal que um programador pode escrever, mas cumprirá o propósito de ser um objeto de estudo para todas as partes pelas quais devemos passar.

Se você olhar mais de perto o fonte do Node.js., notará duas pastas principais: lib e src. A pasta lib é a que contém todas as definições de todas as funções e módulos que precisamos em nossos projetos, porém escritas em JavaScript. A pasta src é a implementação em C ++ que vem junto com elas, é aqui que reside a Libuv e o V8 e também onde todas as implementações para módulos como fs, http, crypto e outras acabam ficando.

Seja este programa simples:

const fs = require('fs')
const path = require('path')
const filePath = path.resolve(`../myDir/myFile.md`)

// Parseamos o buffer em string
function callback (data) {
  return data.toString()
}

// Transformamos a função em uma promise
const readFileAsync = (filePath) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, (err, data) => {
      if (err) return reject(err)
      return resolve(callback(data))
    })
  })
}

(() => {
  readFileAsync(filePath)
    .then(console.log)
    .catch(console.error)
})()
Enter fullscreen mode Exit fullscreen mode

Sim, eu sei que existe o util.promisify e o fs.promises, porém, eu queria converter manualmente o callback da função em uma promise de forma que possamos ter um melhor entendimento da parte interna das coisas.

Todos os exemplos que teremos neste artigo estarão relacionados a este programa. E isso se deve ao fato de que fs.readFile *não * faz parte da V8 ou do JavaScript. Esta função é implementada apenas pelo Node.js, como uma binding C++ para o sistema operacional local, no entanto, a API de alto nível que usamos como fs.readFile (path, cb) é totalmente implementada em JavaScript, que chama esses bindings. Aqui está o código fonte completo dessa função readFile especificamente (porque o arquivo inteiro tem 1850 linhas, mas está nas referências):

// https://github.com/nodejs/node/blob/0e03c449e35e4951e9e9c962ff279ec271e62010/lib/fs.js#L46
const binding = internalBinding('fs');
// https://github.com/nodejs/node/blob/0e03c449e35e4951e9e9c962ff279ec271e62010/lib/fs.js#L58
const { FSReqCallback, statValues } = binding;

// https://github.com/nodejs/node/blob/0e03c449e35e4951e9e9c962ff279ec271e62010/lib/fs.js#L283
function readFile(path, options, callback) {
  callback = maybeCallback(callback || options);
  options = getOptions(options, { flag: 'r' });
  if (!ReadFileContext)
    ReadFileContext = require('internal/fs/read_file_context');
  const context = new ReadFileContext(callback, options.encoding);
  context.isUserFd = isFd(path); // File descriptor ownership

  const req = new FSReqCallback();
  req.context = context;
  req.oncomplete = readFileAfterOpen;

  if (context.isUserFd) {
    process.nextTick(function tick() {
      req.oncomplete(null, path);
    });
    return;
  }

  path = getValidatedPath(path);
  binding.open(pathModule.toNamespacedPath(path),
               stringToFlags(options.flag || 'r'),
               0o666,
               req);
}
Enter fullscreen mode Exit fullscreen mode

Disclaimer: Estou colando as referências de código nos links de origem do Github a partir do commit 0e03c449e35e4951e9e9c962ff279ec271e62010, que é o mais recente no momento, desta forma, este documento sempre apontará para a implementação correta no momento em que o escrevi.

Vê a linha 5? Temos uma chamada require para read_file_context, outro arquivo JS (que também está nas referências). No final do código fonte para fs.readFile, temos uma chamada para binding.open, que é uma chamada C++ para abrir um file descriptor, passando o caminho, os sinalizadores fopen do C++, as permissões do modo de arquivo no formato octal (0o é o formato octal no ES6) e, por último, a variável req, que é o callback assíncrono que receberá a nossa resposta.

Junto com tudo isso, temos o internalBinding, que é um loader para um binding interno privado do C++, que não é acessível aos usuários finais (como nós) porque eles estão disponíveis no NativeModule.require, é isso que realmente carrega código C++. E é aqui que dependemos MUITO do V8.

Então, basicamente, no código acima, estamos dando um require em um binding fs com internalBinding('fs'), que chama e carrega o arquivo src/node_file.cc (porque todo esse arquivo está no namespace fs) que contém todas as implementações em C++ para as funções FSReqCallback e statValues.

A função FSReqCallback é o callback assíncrono que passamos quando chamamos fs.readFile (quando usamos fs.readFileSync, existe outra função chamada FSReqWrapSync que é definida aqui) e todos os seus métodos e implementações estão definidos aqui e expostos como bindings aqui:

// https://github.com/nodejs/node/blob/0e03c449e35e4951e9e9c962ff279ec271e62010/src/node_file.cc

FileHandleReadWrap::FileHandleReadWrap(FileHandle* handle, Local<Object> obj)
  : ReqWrap(handle->env(), obj, AsyncWrap::PROVIDER_FSREQCALLBACK),
    file_handle_(handle) {}

void FSReqCallback::Reject(Local<Value> reject) {
  MakeCallback(env()->oncomplete_string(), 1, &reject);
}

void FSReqCallback::ResolveStat(const uv_stat_t* stat) {
  Resolve(FillGlobalStatsArray(env(), use_bigint(), stat));
}

void FSReqCallback::Resolve(Local<Value> value) {
  Local<Value> argv[2] {
    Null(env()->isolate()),
    value
  };
  MakeCallback(env()->oncomplete_string(),
               value->IsUndefined() ? 1 : arraysize(argv),
               argv);
}

void FSReqCallback::SetReturnValue(const FunctionCallbackInfo<Value>& args) {
  args.GetReturnValue().SetUndefined();
}

void NewFSReqCallback(const FunctionCallbackInfo<Value>& args) {
  CHECK(args.IsConstructCall());
  Environment* env = Environment::GetCurrent(args);
  new FSReqCallback(env, args.This(), args[0]->IsTrue());
}

// Create FunctionTemplate for FSReqCallback
Local<FunctionTemplate> fst = env->NewFunctionTemplate(NewFSReqCallback);
fst->InstanceTemplate()->SetInternalFieldCount(1);
fst->Inherit(AsyncWrap::GetConstructorTemplate(env));
Local<String> wrapString =
    FIXED_ONE_BYTE_STRING(isolate, "FSReqCallback");
fst->SetClassName(wrapString);
target
    ->Set(context, wrapString,
          fst->GetFunction(env->context()).ToLocalChecked())
    .Check();
Enter fullscreen mode Exit fullscreen mode

Nesta última parte, há uma definição de um construtor: Local<FunctionTemplate> fst = env->NewFunctionTemplate(NewFSReqCallback). Isso basicamente diz que, quando chamamos new FSReqCallback (), a função NewFSReqCallback será chamada. Agora veja como a propriedade context aparece na parte target->Set(context, wrapString, fst->GetFunction) e também como o oncomplete também é definido e usado no ::Reject e ::Resolve.

Também é importante notar que a variável req é criada a partir do resultado da chamada new ReadFileContext, que é referenciada como context e definida como req.context. Isso significa que a variável req também é uma representação do binding do C++ de um callback criado com a função FSReqCallback() e configura seu contexto para nosso callback e ouve um evento oncomplete.

Conclusão

No momento, não vimos muito. No entanto, em artigos posteriores, iremos abordar cada vez mais como as coisas realmente funcionam e como podemos usar nossa função para entender melhor nossas ferramentas!

Não deixe de acompanhar mais do meu conteúdo no meu blog e se inscreva na newsletter para receber notícias semanais!

💖 💪 🙅 🚩
_staticvoid
Lucas Santos

Posted on September 3, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related