Serialização de dados com FlatBuffers

Na computação, serializar dados sempre foi uma necessidade – seja para persistir os dados que estão em memória para um simples arquivo, seja para intercambiar informações entre aplicações através da rede. Hoje qualquer pessoa pode “estar conectada” à internet através de smartphones, o que na grande maioria das vezes significa a troca de dados entre aplicações e serviços rodando em clouds.

Normalmente essa troca é feita utilizando dados no formato JSON (JavaScript Object Notation), que ao longo dos anos, ganhou a guerra contra o formato XML por ser mais compacto. Mas por mais compacto que seja, é necessário ler todo seu conteúdo para então ter acesso ao modelo de dados – o que implica em processamento e uso de memória.

Por exemplo, o Facebook começou a ter sérios problemas com o parsing do conteúdo JSON do feed de notícias de sua aplicação para Android, tarefa esta que consumia muita memória e tempo de processamento e tornava a aplicação lenta (rolagem do feed de notícias não fluida, por exemplo). Foi então que a empresa teve a ideia de utilizar FlatBuffers para serializar os dados dos serviços consumidos pela aplicação Android – e o resultado foi o seguinte:

  • Tempo de carregamento de uma história armazenada em cache caiu de 35 ms para 4 ms.
  • Alocação de memória transiente reduziu em 75%.
  • Tempo do primeiro início da aplicação reduzido entre 10% e 15%.
  • Armazenamento de dados temporários reduzido em 15%.

Todos estes números são impressionantes, sem dúvida. Porém o interessante é que FlatBuffers não foram pensados para serializar dados na comunicação entre aplicações. O Google desenvolveu a biblioteca com o intuito de serializar dados de jogos em dispositivos móveis, onde performance é um requisito obrigatório!

Antes de pôr a mão na massa

Para serializar dados no formato de FlatBuffers sem a necessidade de escrever uma aplicação, precisamos de um compilador que, combinado com um arquivo de esquema, transformará arquivos JSON em arquivos no formato de FlatBuffers. Na demonstração inicial utilizarei este compilador de FlatBuffers e posteriormente em outros posts, as bibliotecas para C# e Android.

O Google disponibiliza em um repositório no GitHub o código-fonte do compilador de FlatBuffers (em projetos para Visual Studio 2010 e Xcode, além de Makefiles para os diversos “sabores” de Unix), bem como bibliotecas para gerar e ler conteúdo para Android (JNI), C#, C++, Go, Java, JavaScript e Python.

Será necessário gerar o arquivo executável do compilador de FlatBuffers, pois não há no repositório sua distribuição binária. Utilizarei o Visual Studio Community Edition para tal tarefa.

Acesse a página e faça o download do arquivo ZIP que contém todo o código do repositório, clicando no botão “Download ZIP”.

serializacao_de_dados_com_flatbuffers_1

Após baixar o arquivo, descompacte-o e abra o projeto que está em flatbuffers-master\build\VS2010. Troque a configuração do projeto para Release e a plataforma para Win32 e então compile-o.

serializacao_de_dados_com_flatbuffers_2

Importante!!! Em algumas versões do Visual Studio durante a compilação do projeto podem ocorrer erros do tipo: “C2220: warning treated as error – no ‘object’ file generated”. Para resover este problema, compile em um nível de aviso inferior. Por exemplo, use /W3 em vez de /W4. Esta opção está disponível clicando com o botão direito em cima do projeto flatc -> Properties ->  C/C++ -> General -> Warning Level.

Crie uma pasta para armazenarmos os arquivos da demonstração e copie para lá o compilador de FlatBuffers (flatc) gerado pelo Visual Studio.

serializacao_de_dados_com_flatbuffers_3

Arquivo de Esquema

Diferente de arquivos JSON, arquivos FlatBuffers não são textuais, ou seja, não é possível ler seu conteúdo a partir de um editor de textos comum e interpretá-lo facilmente. Além disso, não podemos definir a estrutura destes arquivos de forma tão simples como é feita em arquivos JSON. É necessário seguir um “contrato” para gerar estes arquivos, que é representado pelo arquivo de esquema.

O arquivo de esquema é feito utilizando IDL (Interface Description Language), que é um modo genérico para definição de interfaces de comunicação entre sistemas. Vamos tomar como exemplo o seguinte arquivo de esquema e em seguida explicarei cada uma das partes:

file_identifier "IMV3";
file_extension "fb";

namespace imobiliaria;

enum Estado : byte { AC, AL, AP, AM, BA, CE, DF, ES, GO, MA, MT, MS, MG, PA, PB, PR, PE, PI, RJ, RN, RS, RO, RR, SC, SP, SE, TO }

enum Tipo: byte { compra, aluguel }

struct Area {
	largura: float;
	comprimento: float;
}

union Vendedor { Corretor, Imobiliaria }

table Imoveis {
	casas: [Casa];
	aptos: [Apartamento];
} 

table Casa {
	endereco: string;
	numero: ushort;
	bairro: string;
	cidade: string;
	estado: Estado;
	areaTerreno: Area (required);
	areaConstruida: float;
	quartos: ubyte;
	tipo: Tipo;
	valor: float;
	aceitaFGTS: bool = true;
	foto: [byte] (deprecated);
	urlFoto: string;
	vendedor: Vendedor (required);
}

table Apartamento {
	endereco: string;
	numero: ushort;
	numApto: ubyte;
	bairro: string;
	cidade: string;
	estado: Estado;
	area: float;
	quartos: ubyte;
	valor: float;
	valorCondominio: float;
	tipo: Tipo;
	aceitaFGTS: bool = true;
	foto: [byte] (deprecated);
	urlFoto: string;
	vendedor: Vendedor (required);
}

table Corretor {
	nome: string;
	telefone: string;
	creci: string;
}

table Imobiliaria {
	nome: string;
	endereco: string;
	numero: ushort;
	bairro: string;
	cidade: string;
	estado: Estado;
	telefone: string;
}

root_type Imoveis;

Tables

As tables são os elementos mais importantes na definição de esquemas. Fazendo um paralelo com orientação a objetos, elas seriam análogas às classes. Estas são compostas por campos (assim como as classes) que podem assumir tipos escalares ou não-escalares.

São considerados tipos escalares:

  • 8 bits: byte, ubyte e bool.
  • 16 bits: short e ushort.
  • 32 bits: int, uint e float.
  • 64 bits: long, ulong e double.

Os tipos não-escalares são:

  • Vetores, que são denotados por tipos entre colchetes.
  • Strings.
  • Referências para outras tables, unions, structs ou enums.

Caso o desenvolvedor não defina nenhum valor para os campos da tabela, estes são gravados com valor 0. É possível definir valores padrão para campos com tipos escalares, como foi feito no campo aceitaFGTS das tabelas Casa e Apartamento:

aceitaFGTS: bool = true;

Atributos

Os campos também admitem atributos que são utilizados para ajudar na criação de classes para a manipulação de FlatBuffers. No exemplo dado, utilizamos somente dois deles: deprecated e required:

foto: [byte] (deprecated);
vendedor: Vendedor (required);

O atributo deprecated indica que o campo não é mais utilizado na tabela. Ao executar o compilador de FlatBuffers, as classes de manipulação geradas não terão métodos para tratar os campos que estão marcados como deprecated. Atenção: não remova nem troque a ordem de campos já existentes, pois isso modifica a estrutura dos registros e causará problemas na leitura de dados gravados por versões antigas de seu modelo. Quando um campo não for mais necessário, marque-o como deprecated. Ao adicionar um campo, sempre o adicione ao fim da tabela.

foto: [byte] (deprecated);
urlFoto: string;

No trecho de esquema acima é possível notar que a foto do imóvel era guardada na tabela, mas que em algum momento deixou de ser utilizada em favor da url da foto (que ocupa bem menos espaço).

O atributo required indica que é obrigatório preencher o campo ao gravar o registro e essa verificação fica a cargo das classes de manipulação geradas pelo compilador. Note que só é possível utilizar este atributo em campos não-escalares.

Existem ainda outros atributos que podem ser utilizados em conjunto com os campos de uma tabela, mas como a ideia deste post é apenas introduzir o assunto, não tratarei dos demais – caso tenha curiosidade, eles estão documentados no site do projeto.

Enumeradores

Assim como na maioria das linguagens de programação, os enumeradores são uma sequência de constantes, sendo que a primeira constante sempre recebe o valor 0.

enum Tipo: byte { compra, aluguel }

Neste exemplo, o enumerador Tipo será do tipo byte e terá duas constantes, compra e aluguel, sendo que compra terá o valor 0 e aluguel terá o valor 1.

Existe a possibilidade de utilizar outros valores para as constantes de um enumerador:

enum Tipo: byte { compra = 10, aluguel }

Agora, compra terá o valor 10 e aluguel, 11.

Ao executar o compilador de FlatBuffers com a opção de geração de código, classes com as constantes dos enumeradores serão criadas, incluindo métodos para a transformação de strings com o nome das constantes para seu valor numérico correspondente.

Structs

As structs de FlatBuffers são similares às structs da linguagem C e parecidas com as tables, porém são mais simples. Os tipos dos campos das structs só podem ser de tipos escalares ou referências para outras structs e uma vez tendo suas composições definidas, não é possível modificá-las, ou seja, não se pode adicionar, remover ou alterar tipos de campos. Também não é possível definir valores padrão para os campos:

struct Area {
	largura: float;
	comprimento: float;
}

Por serem mais simples que tables, structs ocupam menos memória e também são mais rápidas de serem manipuladas.

Unions

Unions funcionam como enumeradores de tables, o que significa que um campo declarado como Union pode fazer referência a tipos diferentes de tabelas em cada registro:

union Vendedor { Corretor, Imobiliaria }

Neste exemplo, a Union Vendedor pode conter referência para um registro de Corretor ou Imobiliaria.

Outras definições do esquema

Identificação do arquivo

É possível definir um identificador para um FlatBuffer com o intuito de tornar a leitura do arquivo mais segura. Isto é feito através da propriedade file_identifier:

file_identifier "IMV3";

Este identificador é gravado no começo do arquivo binário e assim que a leitura do arquivo é iniciada, a verificação do identificador pode ser feita. Se esta for diferente do esperado, significa que o arquivo que está sendo lido pode não conter o tipo de dado esperado. Neste caso, não seria necessário ler o restante do arquivo.

O identificador do FlatBuffer deve ser uma string de exatamente 4 caracteres.

Por padrão, os arquivos gerados pelo compilador de FlatBuffers possuem a extensão “bin”, mas é possível alterá-la através do arquivo de esquema utilizando a propriedade file_extension:

file_extension "fb";

Namespace

Como foi dito, o compilador consegue gerar as classes de manipulação de FlatBuffers baseado no arquivo de esquema. Estas classes já podem ser geradas com seus namespaces (no caso de C++ e C#) ou pacotes (no caso de Java) corretos. Para isso, utilize a propriedade namespace:

namespace imobiliaria;

Tipo raiz

É necessário definir no arquivo de esquema qual o tipo de dados que será encontrado no arquivo de FlatBuffer assim que este começar a ser lido. Isso é feito através da propriedade root_type:

root_type Imoveis;

O tipo raiz pode ser uma table ou uma struct (unions não são aceitas).

Criando um FlatBuffer a partir de um arquivo JSON

Utilizando o arquivo de esquema dado anteriormente, vamos criar um arquivo de FlatBuffer a partir de um arquivo JSON. Todos os arquivos envolvidos neste post estão disponíveis no GitHub do talkitbr e podem ser baixados de lá.

O arquivo imoveis.json contém algumas informações de imóveis que estão descritas de forma aderente ao arquivo de esquemas. Para fazer a conversão, executaremos o compilador de FlatBuffers passando como parâmetros o arquivo de esquema (imoveis.fbs) e o arquivo JSON (imoveis.json):

flatc -b imoveis.fbs imoveis.json

Após a execução do compilador foi gerado o arquivo imoveis.fb. O arquivo JSON, legível, se transformou neste arquivo binário:

serializacao_de_dados_com_flatbuffers_4

O tamanho do arquivo original era de 1562 bytes (versão compacta, sem espaços ou quebras de linha). Ao ser convertido, seu tamanho foi reduzido a 1108 bytes – redução de aproximadamente 30%. A redução é proporcional ao tamanho do arquivo JSON, pois FlatBuffers não declaram os nomes dos campos a cada registro como é feito nos arquivos JSON.

Até agora fizemos a comparação do tamanho da mesma massa de dados serializada tanto para JSON quanto para FlatBuffers. Vimos que FlatBuffers ocupam menos espaço e isso pode ser importante na economia de banda durante a transferência de dados. E em relação à performance, será que a vantagem se confirma? Veremos isso em posts futuros! Continuem nos acompanhando!

Fontes:
Anúncios