FlatBuffers em apps universais

Como prometido no post inicial sobre FlatBuffers, vamos verificar se além da economia de banda, o uso de FlatBuffers também nos traz ganho de performance ao deserializar dados e transformá-los em um modelo de classes carregado em memória. E começaremos a fazer isso utilizando uma aplicação Universal Windows Platform. Todo o código da demonstração feita neste post está disponível no GitHub do talkitbr.

Caso opte por seguir passo a passo o tutorial, baixe os seguintes arquivos criados no post anterior: o arquivo de esquema de FlatBuffer (imoveis.fbs), o arquivo JSON que contém os dados que serão importados pela aplicação (imoveis.json) e o compilador de FlatBuffers (flatc.exe).

Criação do projeto

Abra o Visual Studio e crie um projeto C# do tipo Blank App (Universal Windows) e nomeie-o como FlatBuffersWindows:

flatbuffers_universal_windows_pps (1)

Após a criação do projeto, adicione a dependência Json.NET. Utilizaremos este parser de JSON para fazer a comparação com a leitura do FlatBuffer. Para isso, clique com o botão direito em References no painel Solution Explorer e depois clique em “Manage NuGet Packages…”. Selecione a aba Browse e digite “JSON.net” (sem as aspas) no campo de busca. Selecione o item Newtonsoft.Json e então clique no botão Install.

flatbuffers_universal_windows_pps (2)

Agora crie um novo projeto C# do tipo Class Library (Portable) na solução atual. Utilizaremos este projeto para guardar as classes de suporte para a leitura do FlatBuffer. Existe um projeto semelhante no GitHub do FlatBuffers, porém o projeto foi gerado em uma versão antiga do Visual Studio e não pode ser convertido para uma biblioteca para ser utilizado em uma Universal Windows App.

Para isso, clique no menu File, selecione New e então “Project…“. Selecione Class Library (Portable) como tipo de projeto. No campo Solution, selecione a opção Add to solution e nomeie o projeto como FlatBuffers.

flatbuffers_universal_windows_pps (3)

No diálogo seguinte, mantenha as opções .NET Framework 4.6, Windows Universal 10.0 e ASP.NET Core 5.0 selecionadas. Ao término da criação, apague a classe Class1 do projeto FlatBuffers.

Baixe o repositório do FlatBuffers (assim como foi feito no post anterior) e copie as classes de suporte para .Net localizadas na pasta net/FlatBuffers para o projeto recém criado. São elas: ByteBuffer.cs, FlatBufferBuilder.cs, FlatBufferConstants.cs, Offset.cs, Struct.cs e Table.cs:

flatbuffers_universal_windows_pps (4)

Por fim, adicione o projeto FlatBuffers como dependência do projeto principal. Clique com o botão direito sobre o item References do projeto FlatBuffersWindows no painel Project Explorer e depois em “Add Reference…“. Marque o item FlatBuffers na subseção Solution da seção Projects. Clique em OK para finalizar:

flatbuffers_universal_windows_pps (5)

Geração das classes de modelo para leitura do FlatBuffer

Antes de seguirmos com a geração das classes, altere o namespace que será utilizado nas classes no arquivo de esquema:

namespace FlatBuffersWindows.model.vo.fb;

Foi dito que o compilador de FlatBuffers gera automaticamente as classes para a manipulação de dados para diferentes linguagens, incluindo C#. Abra um prompt de comando, acesse a pasta onde está o compilador de FlatBuffers e execute-o deste modo:

flatc –help

Serão exibidas as opções de execução do compilador:

usage: flatc [OPTION]... FILE... [-- FILE...]
 -b Generate wire format binaries for any data definitions.
 -t Generate text output for any data definitions.
 -c Generate C++ headers for tables/structs.
 -g Generate Go files for tables/structs.
 -j Generate Java classes for tables/structs.
 -s Generate JavaScript code for tables/structs.
 -n Generate C# classes for tables/structs.
 -p Generate Python files for tables/structs.
 -o PATH Prefix PATH to all generated files.
 -I PATH Search for includes in the specified path.
 -M Print make rules for generated files.
 --strict-json Strict JSON: field names must be / will be quoted,
 no trailing commas in tables/vectors.
 --defaults-json Output fields whose value is the default when
 writing JSON
 --no-prefix Don't prefix enum values with the enum type in C++.
 --scoped-enums Use C++11 style scoped and strongly typed enums.
 also implies --no-prefix.
 --gen-includes (deprecated), this is the default behavior.
 If the original behavior is required (no include
 statements) use --no-includes.
 --no-includes Don't generate include statements for included
 schemas the generated file depends on (C++).
 --gen-mutable Generate accessors that can mutate buffers in-place.
 --gen-onefile Generate single output file for C#
 --raw-binary Allow binaries without file_indentifier to be read.
 This may crash flatc given a mismatched schema.
 --proto Input is a .proto, translate to .fbs.
 --schema Serialize schemas instead of JSON (use with -b)
FILEs may depend on declarations in earlier files.
FILEs after the -- must be binary flatbuffer format files.
Output files are named using the base file name of the input,
and written to the current directory or the path given by -o.
example: flatc -c -b schema1.fbs schema2.fbs data.json

Nos interessam as opções -b (para gerar o arquivo de FlatBuffer a partir de um arquivo JSON) e -n (para gerar as classes de manipulação em C#). Vamos executar o compilador de FlatBuffers utilizando estas opções:

flatc -b -n imoveis.fbs imoveis.json

Note que o arquivo de FlatBuffer foi criado no mesmo nível do arquivo de esquema e as classes foram geradas em uma estrutura de diretórios:

flatbuffers_universal_windows_pps (6)

Copie a pasta model para dentro do projeto FlatBuffersWindows:

flatbuffers_universal_windows_pps (7)

Criação das classes de modelo para parsing do arquivo JSON

Para fazer o parsing de um arquivo JSON utilizando o framework JSON.net precisamos ter um modelo de dados que seja correspondente à estrutura do arquivo em questão. Poderíamos tentar modificar as classes geradas pelo compilador de FlatBuffers para ganhar tempo, porém, como são classes geradas automaticamente, não seria uma boa ideia visto que poderíamos mudar o esquema do FlatBuffer e isso descartaria todo o trabalho de alteração.

Vamos então criar o seguinte esquema de classes:

flatbuffers_universal_windows_pps (8)

Note que todas as classes estão com o sufixo “JSON”. Isso é proposital – desta forma conseguiremos identificar de forma mais fácil a utilização das classes relativas ao FlatBuffer e ao arquivo JSON. Ao criar as classes, coloque-as na pasta model\vo\json e utilize o namespace FlatBuffersWindows.model.vo.json.

Enumeradores

Temos dois enumeradores em nosso modelo de dados: um para os estados da federação e outro para identificar o tipo de negociação do imóvel (compra ou aluguel). Ambas informações são dadas como strings no arquivo JSON e o parser precisa saber converter esse texto para um dos valores do enumerador. Para isso, utilizaremos duas anotações – uma para indicar para o parser que a conversão deve ser feita a partir de strings e outra para fazer a correspondência entre um texto específico e um valor do enumerador.

A anotação JSONConverter deve ser utilizada na declaração da classe e como parâmetro devemos passar a classe StringEnumConverter, que é classe de manipulação que efetivamente converte um texto em um valor do enumerador:

[JsonConverter(typeof(StringEnumConverter))]
public enum EstadoJSON

Depois, em cada um dos valores do enumerador, utilizamos a anotação EnumMember e definimos qual texto corresponde ao valor:

[EnumMember(Value = "SP")]
 SP,

EstadoJSON.cs

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Runtime.Serialization;

namespace FlatBuffersWindows.model.vo.json
{
    [JsonConverter(typeof(StringEnumConverter))]
    public enum EstadoJSON
    {
        [EnumMember(Value = "AC")]
        AC,

        [EnumMember(Value = "AL")]
        AL,

        [EnumMember(Value = "AP")]
        AP,

        [EnumMember(Value = "AM")]
        AM,

        [EnumMember(Value = "BA")]
        BA,

        [EnumMember(Value = "CE")]
        CE,

        [EnumMember(Value = "DF")]
        DF,

        [EnumMember(Value = "ES")]
        ES,

        [EnumMember(Value = "GO")]
        GO,

        [EnumMember(Value = "MA")]
        MA,

        [EnumMember(Value = "MT")]
        MT,

        [EnumMember(Value = "MS")]
        MS,

        [EnumMember(Value = "MG")]
        MG,

        [EnumMember(Value = "PA")]
        PA,

        [EnumMember(Value = "PB")]
        PB,

        [EnumMember(Value = "PR")]
        PR,

        [EnumMember(Value = "PE")]
        PE,

        [EnumMember(Value = "PI")]
        PI,

        [EnumMember(Value = "RJ")]
        RJ,

        [EnumMember(Value = "RN")]
        RN,

        [EnumMember(Value = "RS")]
        RS,

        [EnumMember(Value = "RO")]
        RO,

        [EnumMember(Value = "RR")]
        RR,

        [EnumMember(Value = "SC")]
        SC,

        [EnumMember(Value = "SP")]
        SP,

        [EnumMember(Value = "SE")]
        SE,

        [EnumMember(Value = "TO")]
        TO
    };
}

TipoJSON.cs

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Runtime.Serialization;

namespace FlatBuffersWindows.model.vo.json
{
    [JsonConverter(typeof(StringEnumConverter))]
    public enum TipoJSON
    {
        [EnumMember(Value = "compra")]
        Compra,

        [EnumMember(Value = "aluguel")]
        Aluguel
    };
}

Classes

Felizmente não é necessário o uso de anotações nas classes. Basta apenas definir o nome das propriedades de modo que coincidam com o nome das chaves no arquivo JSON.

Area.JSON.cs

namespace FlatBuffersWindows.model.vo.json
{
    class AreaJSON
    {
        public float largura { get; set; }
        public float comprimento { get; set; }
    }
}

VendedorJSON.cs

namespace FlatBuffersWindows.model.vo.json
{
    abstract class VendedorJSON
    {
        public string nome { get; set; }
        public string telefone { get; set; }
    }
}

ImobiliariaJSON.cs

namespace FlatBuffersWindows.model.vo.json
{
    class ImobiliariaJSON : VendedorJSON
    {
        public string endereco { get; set; }
        public int numero { get; set; }
        public string bairro { get; set; }
        public string cidade { get; set; }
        public EstadoJSON estado { get; set; }
    }
}

CorretorJSON.cs

namespace FlatBuffersWindows.model.vo.json
{
    class CorretorJSON : VendedorJSON
    {
        public string creci { get; set; }
    }
}

ImovelJSON.cs

namespace FlatBuffersWindows.model.vo.json
{
    abstract class ImovelJSON
    {
        public string endereco { get; set; }
        public int numero { get; set; }
        public string bairro { get; set; }
        public string cidade { get; set; }
        public EstadoJSON estado { get; set; }
        public int quartos { get; set; }
        public TipoJSON tipo { get; set; }
        public float valor { get; set; }
        public bool aceitaFGTS { get; set; } = true;
        public VendedorJSON vendedor { get; set; }
        public string urlFoto { get; set; }
    }
}

ApartamentoJSON.cs

namespace FlatBuffersWindows.model.vo.json
{
    class ApartamentoJSON : ImovelJSON
    {
        public int numApto { get; set; }
        public float area { get; set; }
    }
}

CasaJSON.cs

namespace FlatBuffersWindows.model.vo.json
{
    class CasaJSON : ImovelJSON
    {
        public AreaJSON areaTerreno { get; set; }
        public float areaConstruida { get; set; }
    }
}

ImoveisJSON.cs

namespace FlatBuffersWindows.model.vo.json
{
    class ImoveisJSON
    {
       public List<ApartamentoJSON> aptos { get; set; }
       public List<CasaJSON> casas { get; set; }
    }
}

Após a criação destas classes, teremos todas as classes necessárias para carregar os dados:

flatbuffers_universal_windows_pps (9)

Arquivos de dados

O intuito deste post é medir a performance da conversão de dados serializados em um modelo de classes carregado na memória. Portanto, iremos embarcar os arquivos de dados na própria aplicação. Crie uma pasta chamada data no projeto FlatBuffersWindows e copie para dentro dela os arquivos imoveis.fb (criado durante a geração das classes pelo compilador de FlatBuffers) e imoveis.json (criado no post passado).

Depois de copiar os arquivos é necessário informar ao Visual Studio que estes arquivos precisam ser embarcados junto da aplicação. Repita, para cada um dos arquivos de dados, o seguinte:

  • Selecione o arquivo no painel Solution Explorer.
  • No painel Properties, selecione o valor Content na propriedade Build Action.
  • Ainda no painel Properties, selecione o valor Copy always na propriedade Copy to Output Directory

flatbuffers_universal_windows_pps (10)

Por fim, será necessário modificar o arquivo JSON para indicar para o parser quais classes serão utilizadas em determinados blocos do arquivo. Isso é necessário quando o bloco JSON corresponde à uma classe polimorfa – em outras palavras, uma classe abstrata.  Exemplo:

{
    "aptos": [
        {
            "endereco": "Rua 1",
            "numero": 150,
            "numApto": 12,
            "bairro": "Centro",
            "cidade": "São Paulo",
            "estado": "SP",
            "area": 72,
            "quartos": 3,
            "valor": 730000,
            "tipo": "compra",
            "vendedor": {
                "$type": "FlatBuffersWindows.model.vo.json.CorretorJSON, FlatBuffersWindows",
                "nome": "João das Couves",
                "telefone": "99999-9999",
                "creci": "abcdef"
            },
            "urlFoto": "http://www.fotosdeimoves.com/apto1.jpg"
        }
    ]
}

O vendedor de um imóvel pode ser um corretor de imóvel autônomo ou uma imobiliária. Neste exemplo, o vendedor é um corretor autônomo.

A primeira propriedade do bloco JSON deve ser nomeada como $type e seu valor deve ser o namespace completo da classe concreta que representa aquele bloco, seguido do Assembly Name do projeto que contém a classe separados por vírgula.

Remova também as chaves nomeadas como vendedor_type, pois elas eram utilizadas para serializar os dados como FlatBuffers e não serão mais necessárias.

Tela para comparação dos tempos de deserialização

Vamos construir uma tela simples para nos mostrar os tempos de deserialização tanto do FlatBuffer quando do arquivo JSON:

flatbuffers_universal_windows_pps (11)

Neste caso, criei um RelativePanel e dentro dele coloquei um TextBlock para identificar a seção de dados que estava tratando, um botão para carregar os dados e outro TextBlock para exibir o tempo de carregamento. Foi utilizado um conjunto para o FlatBuffer e outro para o arquivo JSON. Ao final, o arquivo XAML ficou assim:

<Page x:Class="FlatBuffersWindows.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:FlatBuffersWindows" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <RelativePanel HorizontalAlignment="Center" Height="620" Margin="10,10,0,0" VerticalAlignment="Top" Width="340">
            <TextBlock x:Name="lblFlatBufferTitle" HorizontalAlignment="Center" Height="20" TextWrapping="Wrap" Text="FlatBuffer" VerticalAlignment="Center" Margin="0" FontWeight="Bold" RelativePanel.AlignHorizontalCenterWithPanel="True"/>
            <Button x:Name="btnLoadFlatBuffer" Content="Carregar FlatBuffer" HorizontalAlignment="Left" Height="32" VerticalAlignment="Top" RelativePanel.Below="lblFlatBufferTitle" RelativePanel.AlignHorizontalCenterWithPanel="True" Margin="0,10,0,0"/>
            <TextBlock x:Name="lblFlatBufferLoadingTime" Height="20" TextWrapping="Wrap" Text="Tempo de carregamento:" VerticalAlignment="Center" RelativePanel.Below="btnLoadFlatBuffer" RelativePanel.AlignHorizontalCenterWithPanel="True" Margin="-77,10,-77.333,0" Width="320" TextAlignment="Center"/>
            <TextBlock x:Name="lblJSONTitle" HorizontalAlignment="Left" Height="20" TextWrapping="Wrap" Text="JSON" VerticalAlignment="Top" RelativePanel.Below="lblFlatBufferLoadingTime" FontWeight="Bold" Margin="0,40,0,0" RelativePanel.AlignHorizontalCenterWithPanel="True"/>
            <Button x:Name="btnLoadJSON" Content="Carregar JSON" HorizontalAlignment="Left" Height="32" VerticalAlignment="Top" RelativePanel.Below="lblJSONTitle" RelativePanel.AlignHorizontalCenterWithPanel="True" Margin="0,10,0,0"/>
            <TextBlock x:Name="lblJSONLoadingTime" HorizontalAlignment="Center" Height="20" TextWrapping="Wrap" Text="Tempo de carregamento:" VerticalAlignment="Center" RelativePanel.Below="btnLoadJSON" RelativePanel.AlignHorizontalCenterWithPanel="True" Margin="-77,10,-77.333,0" Width="320" TextAlignment="Center"/>
        </RelativePanel>
    </Grid>
</Page>

Código para deserialização dos dados

Insira o seguinte código no clique do botão de carregamento do FlatBuffer:

Uri appUri = new Uri((@"ms-appx:///data/imoveis.fb"));
StorageFile fbFile = StorageFile.GetFileFromApplicationUriAsync(appUri).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
IBuffer buffer = FileIO.ReadBufferAsync(fbFile).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();

byte[] content = new byte[buffer.Length];

using (DataReader reader = DataReader.FromBuffer(buffer))
{
    reader.ReadBytes(content);
}

ByteBuffer bb = new ByteBuffer(content);

long start = System.DateTime.Now.Ticks;

Imoveis imoveis = Imoveis.GetRootAsImoveis(bb);

long end = System.DateTime.Now.Ticks;

lblFlatBufferLoadingTime.Text = "Tempo de carregamento: " + ((end - start) / 10000) + " milissegundos";

Debug.WriteLine("Apartamentos carregados: " + imoveis.AptosLength + ". Casas carregadas: " + imoveis.CasasLength);

E este é o código para carregamento do arquivo JSON:

Uri appUri = new Uri((@"ms-appx:///data/imoveis.json"));
StorageFile jsonFile = StorageFile.GetFileFromApplicationUriAsync(appUri).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
string jsonText = FileIO.ReadTextAsync(jsonFile).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();

JsonSerializerSettings settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };

long start = System.DateTime.Now.Ticks;

ImoveisJSON imoveis = JsonConvert.DeserializeObject<ImoveisJSON>(jsonText, settings);

long end = System.DateTime.Now.Ticks;

lblJSONLoadingTime.Text = "Tempo de carregamento: " + ((end - start) / 10000) + " milissegundos";

Debug.WriteLine("Apartamentos carregados: " + imoveis.aptos.Count + ". Casas carregadas: " + imoveis.casas.Count);

Em ambos os casos não considerei o tempo de leitura do arquivo. A cronometragem será feita apenas na deserialização dos dados.

E agora, FlatBuffer?

O projeto está pronto para execução. Será que FlatBuffers, além de ocupar menos espaço, tem performance melhor no processamento? Execute a aplicação e veja o resultado!

flatbuffers_universal_windows_pps (12)

Nesta execução utilizei um telefone rodando Windows 10. O tempo de deserialização do FlatBuffer foi cerca de 28 vezes menor que a deserialização do arquivo JSON.

Ao rodar o mesmo aplicativo em um computador de mesa com Windows 10, obtive o seguinte resultado:

flatbuffers_universal_windows_pps (13)

Agora o tempo de deserialização do FlatBuffer foi cerca de 34 vezes menor que a deserialização do arquivo JSON.

Provamos que além de ocupar menos espaço, um FlatBuffer também é extremamente mais rápido para ser deserializado que um arquivo JSON. Acredito que, da próxima vez em que você for desenvolver um aplicativo, irá considerar o uso de FlatBuffers.

Continue nos acompanhando! No próximo post irei abordar o uso de FlatBuffers em Android!

 

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s