Leben – Day 36

Voltando ao projeto

Estive trabalhando no meu “joguinho”

ainda há muito trabalho, aproveitei minhas férias pra descansar e agora que estou um pouco menos estressado e consegui um cadin de energia

acabei voltando pro Leben.

O que fiz de novo?

acabei implementando o backbone de estruturas no mapa, algo simples onde toda entidade do mundo tenha algum tipo de input e output, por enquanto pensei pequeno com um poço, como havia citado no Leben – 25-2, eu estou implementando tudo via BDD, quero continuar neste formato

ainda não tem tantos detalhes, primeiro estou criando um poço, então indicando que há um ator interagindo com o poço e forço a conclusão da tarefa do ator, então uma referência de “balde de água” é adicionada no inventário do poço.
mas está funcional

eu gosto muito de usar o BDDFy

somente por isso:

futuramente caso necessário eu terei uma comportamento esperado em vários pontos do jogo, evitando que eu quebre :sweat_smile: alguma coisa.

Falando em quebrar…

Estava todo feliz pensando que iria usar a configuração de noise do mapa que havia feito e começar implementar as entidades no mapa.

O resultado é que agora todos os pontos que eu separei nos biomas (desert, mountain, deepforest, forest, beach, nearwater, deepSea, nearwater, beach) estavam quebrados, a única mudança foi que troquei de PC, provávelmente.

mas não tem problema, eu já queria criar um sistema separado pra gerar mapas e poder “pintar” um mapa.
na minha cabeça irei usar a técnica de geração de mapa procedural para fazer maior parte do trabalho, indicando biomas, setando onde é possível ter determinado tipo de recurso e tal, depois dele gerar o mapa eu dô os “tapas” para fazer a finalização.

Em breve mostro uma ferramenta que acabei dazendo para visualizar o mapa de maneira rápida.

ate mais 😁

Async não é paralelismo e não… Async não é mesma coisa que sync.- Parte 1

Intro

Calma, leia até o final :sweat_smile: juro que fará sentido o título, bom… pra mim fez…..

Não sei o quanto vocês conhecem de Async e a famosa maquina de estado no contexto de C#

particularmente eu já tive vários problemas e cheguei odiar porque sempre tive problema e sempre culpei o compilador….

bom, depois de criar vergonha na cara e meter a fuça em como as coisas funcionam aprendi um pouquinho, o suficiente pra não cair em algumas armadilhas.

tenho alguns Links de referência como meu book of reference, tudo que achei útil quando tive problemas eu acabei adicionando nele pra estudar aos poucos.

A importância de saber sobre async e sua máquina de estado.

Recentemente fui convidado pra fazer um Code Review em um código, particularmente devido o projeto que toco é complicado fazer code-review e quando acontece é algo bem pontual onde preciso separar na agenda.

e encontrei alguns itens que me chamaram atenção, foi removido paralelismo, remoção de async e foi adicionado um tal de GC.Collect().

Não me entenda mal, mas se você faz isso, talvez seja importante ler o livro: Pro .Net Memory Management: For Better Code, Performance, and Scalability by Kokosa ele dá um background ótimo sobre com funciona GC e convenhamos… A linguagem já tem mais de 20 anos, com engenheiros super competentes que pensaram durante anos da forma mais eficiênte deste processo, vamos ter humildade :sweat_smile:, existem pouquíssimos casos que você realmente precisa fazer isso, pelo que me recordo em debugging ou fazendo benchmark, com excessão desses pontos, você vai apenas piorar a heurística e se ele está sendo o problema… novamente, leia o livro do Kokosa e comece usar o Stack.

bom pulando pra parte que eu queria falar :sleeping:

Async at last!

não sei se vocês tem o costume de ler o que o código vai gerar ao escrever o código, não é sempre que você precisa disso, na verdade você ignora essa parte porque são poucos cenários que você precisará entender o que acontece por trás do código gerado ou IL pra determinar uma nova forma de escrever código

um código simples :

using System;
using System.Threading.Tasks;
public class TestSubject {
    public void DoSomething() {
    }
    public async Task DoSomethingAsync() {
    }
}

e o compilador irá traduzir isso para:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using System.Threading.Tasks;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class TestSubject
{
    [CompilerGenerated]
    private sealed class <DoSomethingAsync>d__1 : IAsyncStateMachine
    {
        public int <>1__state;

        public AsyncTaskMethodBuilder <>t__builder;

        public TestSubject <>4__this;

        private void MoveNext()
        {
            int num = <>1__state;
            try
            {
            }
            catch (Exception exception)
            {
                <>1__state = -2;
                <>t__builder.SetException(exception);
                return;
            }
            <>1__state = -2;
            <>t__builder.SetResult();
        }

        void IAsyncStateMachine.MoveNext()
        {
            //ILSpy generated this explicit interface implementation from .override directive in MoveNext
            this.MoveNext();
        }

        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine stateMachine)
        {
        }

        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
        {
            //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
            this.SetStateMachine(stateMachine);
        }
    }

    public void DoSomething()
    {
    }

    [AsyncStateMachine(typeof(<DoSomethingAsync>d__1))]
    [DebuggerStepThrough]
    public Task DoSomethingAsync()
    {
        <DoSomethingAsync>d__1 stateMachine = new <DoSomethingAsync>d__1();
        stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
        stateMachine.<>4__this = this;
        stateMachine.<>1__state = -1;
        stateMachine.<>t__builder.Start(ref stateMachine);
        return stateMachine.<>t__builder.Task;
    }
}

é possível perceber que toda vez que você deixa uma assinatura de “async” no método ele gera uma máquina de estado, é até um dos motivos que eu tento não criar nested async, afinal invariávelmente será alocado em memória, então em cenários críticos eu penso bem… Continuando….

Precisava dar esse contexto caso alguém não tivesse ainda a oportunidade de saber o que acontece por baixo dos panos, então foi um resumo pra continuar

em minha memória do código de algum tempo atrás era o seguinte:

public async Task SomeMainTask(){

    var id = 0;
    var processoUm = Task.Run(()=>{ id = DoSomething();});
    var processoDois = DoOtherThing();

    await processoUm;

    (... outros processos sincronos e assíncronos)

    var processoDois = DoOtherThing(id);

    Task.WaitAll(
        processoDois,
        (...)
    );
}

public async Task<id> DoSomething(){
    (... alguma chamada pro banco)
    ///código apenas pra contexto
    return Task.FromResult(id);

}

e quando eu li o código:

public void  SomeMainTask(){

    var id = 0;
    var processoUm = DoSomething();
    var processoDois = DoOtherThing();

    (... outros processos sincronos e assíncronos)

    var processoDois = DoOtherThing(id);    
}

public id DoSomething(){
    (... alguma chamada pro banco)
    ///código apenas pra contexto
    return id;

}

eu fiquei meio intrigado, eu queria entender a motivação… particularmente eu faço tudo sincrono mesmo e eventualmente quando estou fazendo meu próprio code-review eu penso se faz ou não sentido colocar um async.

quando questionei, a resposta foi que async e sincrono seriam as mesmas coisas no final das contas ou seja :

isso

var id = DoSomething();

public int DoSomething(){
    (...chamada pro banco)
    return someid;
}

é igual isso:

var id = await DoSomething();

public Task<int> DoSomething(){
    (...chamada pro banco)
    using(var someconn = new SqlConnection(connectionstring)){
        return someconn.FirstOrDefaultAsync<int>("select 1");   
    }   
}

será?

tentei explicar que não era… afinal na própria documentação da microsoft (inclusive… que maravilha de doc ) explica exatamente o que eu disse. Apesar do meu esforço de mostrar que ele vai bloquear uma thread até ter resposta…

achei mais simples criar um código muito simples pra apresentar o problema:

Apenas pra dar contexto, eu sou um usuário assíduo do LinqPad, logo quando estiver escrito “someText”.Dump(); no código abaixo, entenda como Console.WriteLine(“sometext”);

async System.Threading.Tasks.Task Main()
{
    int nWorkers; 
    int nCompletions; 

    ///aqui estou forçando que apenas exista uma única Thread no meu sistema pra apresentar um cenário que dá pra observar o comportamento
    ThreadPool.SetMinThreads(1, 1).Dump();
    ThreadPool.SetMaxThreads(1, 1).Dump();

    /// apenas me assegurando que é apenas uma thread..... vai que... 
    ThreadPool.GetMaxThreads(out nWorkers, out nCompletions);
        "workers: {nWorkers}".Dump();"nCompletions: {nWorkers}".Dump();

    ///estou deixando uma thread executando sem parar, com um intervalo de 2 segundos, como se fosse um heart-beat.
    System.Threading.Tasks.Task.Run(async ()=>{
                while(true){
                    "Heartbeat".Dump();
                     await System.Threading.Tasks.Task.Delay(2000);
                }
    });


    for(var i = 0; i<=4; i++){
        /// aqui estou criando algumas Tasks assíncronas para executar alguma ação representando um insert no banco.
        await System.Threading.Tasks.Task.Run(async ()=>{
                await System.Threading.Tasks.Task.Delay(5000);"{Thread.CurrentThread.ManagedThreadId}-{DateTime.Now.ToString("HH:MM:ss")}".Dump();
        });
    }

    "-----------------------Async Done-----------------------".Dump();
    for(var i = 0; i<=4; i++){
        /// aqui estou criando as mesmas Tasks mas desta vez estou forçando a execução síncrona para executar a mesma ação no banco.
            System.Threading.Tasks.Task.Run(()=>{
                System.Threading.Tasks.Task.Delay(5000).Wait();
                $"{i} - {Thread.CurrentThread.ManagedThreadId}-{DateTime.Now.ToString("HH:MM:ss")}".Dump();
            });
    }
}

O que você acha que aconteceu?

eu tive que gravar um vídeo…:sweat_smile:

como esperado a thread que está resposável pelo heart-beat continua a execução, é liberada e então a task Async de “persistência no banco” consegue trabalhar, termina e assim o fluxo continua..

agora quando chega no síncrono… nenhum funciona, infelizmente não consegui compreender o motivo, a thread acabou ficando em lock no Task.Delay, mas isso eu não vou tentar entender hoje.

mas espero que esse post traga a luz para alguém que assim como eu também já sofreu com async.

p.s. eu sei que acontecer um thread starvation em alguns cenários é difícil dependendo do seu cenário, mas no cenário que eu verifiquei isso pode ser a diferênça entre aumentar a capacidade do servidor pra dar vazão no processamento, sendo que o custo pode ser evitado com um uso inteligênte dos recursos.

p.s² Pegadinha! eu não falei sobre paralelismo ainda :laughing:, mas o que eu falaria não escapa da gloriosa doc da mãecrosoft:

e depois continuo :p

Esteja Curioso!

Otimizando o KMM – Parte 2

Onde paramos mesmo?

Em nosso último Snapshot tivemos esse resultado

nosso TodoList é pra tentar diminuir o tempo e agora carregar os dados on-demand.

O commit até o momento Commit
Removi algumas propriedades e processamento inútil, o snapshot ficou:

Ainda não descobri o motivo que deu ~12segs de diferença quando eu removi as coleções inúteis, sei que melhorou o gen0 e gen 1, não vou focar nisso agora, porque estive fazendo vários experimentos, fazendo buffering a asyncwrite mas acabou dando na média de 60seg.

Voltarei no tempo de leitura, quando estiver refatorando a leitura de arquivo especificamente

Encontrei um novo problema.

Apesar de ter indexado todos os valores e poder acessar rapidamente, conforme o print.

Ainda tenho o desafio de ter que pesquisar no sqlite a informação com o Tipo, Key e section
o que se resume em :

select * from modchange mc
join (
    select Key, Type,Section from mod m
    join modchange mc on mc.modId = m.id
    where hash = 'HPrv0uf3qAfqvypEDYYHCgC8EQQsmLoBGGNhjOYS5nM='
) mcd on mcd.key = mc.key and mcd.Type = mc.type and mcd.section = mc.section

Porém ao executar temos 17Segs de execução.

o que acaba sendo inviável.

Uma solução que pensei foi transformar key,type e section em um identificador único, e então usar como chave, tornando o processo muito mais rápido na pesquisa.

NOTA: enquanto estava fazendo o hash das 3 string, o tempo foi pra 2 minutos!,

então removi o Type, já que ele já é um Enum, não precisa participar da criação do hash

Eis o resultado em disco e tempo:

O tempo médio pra processamento está em cerca de 1 minuto e 130MB salvo em disco, mas eu acho que está ruim, apesar de ser a primeira execução, eu quero que fique em torno de 30 segundos.

São aproximadamente 1kk de alterações. mais ou menos 43k de changes por segundo.

Depois de descansar, voltei ao problema.

Fiquei um tempo tentando usar hash, tirando daqui, mudando ali pra ter um tempo menor… então me lembrei que eu tinha esquecido de fazer algo BEM mais simples que resolveria o problema, eis o código em sql:




CREATE INDEX idx_mod_change ON ModChange (Key,Type,Section);
o resultado, como esperado: 

pois é, as vezes você está tão focado em um cenário que acaba deixando passar os detalhes mais simples, no meu caso eu parei pra descansar, assistir filme e tirar o dia pra desansar, voltei e imitei o panda ao perceber o que tinha deixado pra trás.

era isso por hoje.

Esteja Curioso!

Otimizando o KMM – Parte 1

Onde paramos mesmo?

Em nosso último Snapshot tivemos esse resultado

Gen 0: 187
Gen 1: 72 
Gen 3: 11 
Tempo: 13179ms 
Memória: 590mb 

Meu objetivo agora era parar de ficar perdendo tempo re-processando todos os mods, não fazia sentido.

Há várias soluções que podemos aplicar, a mais simples “pra mim”, seria utilizar um Helper que fiz pra SqlLite sendo assim eu iria facilitar e muito a quantidade de código que eu escreveria.

Primeiro problema

Fui percebendo que estava fazendo uma iteração desnecessária, pois eu já estava carregando os dados do Mod na memória, depois de terminar de ler tudo eu iterava de novo só pra identificar as modificações… mas isso é redundante e o resultado era apenas ter o valor “Old”.

Porém como os mods podem ser re-ordenados o “Old” se mudará conforme o contexto, logo… é uma informação inútil.

Resultado: Remoção desses código inúteis.

Segundo problema

Eu estava usando minha Library então, o passo mais simples seria abrir uma connection com o banco e ir salvando no DB, algo como:

    var db  = new DataService();
    db.Insert(query, parametros);

sim… é só isso

o único problema é que não estou inserindo 10 itens, e sim mais de 500K, então meio que ele ficou processando por mais de 1 hora e não havia terminado (eu dei uma pausa pra descansar, ele ficou processando enquanto isso) logo precisava efetuar otimizações… (que novidade :x)

Mas eu já esperava isso, eu havia encontrado uma forma eficiente em SqlLite que itera e insere os dados, um dia eu posto mais detalhes (talvez)

o código no final ficou:

        static Dictionary[] List = new Dictionary[5000];
        static int lastIndex = 0;
        public static void AddToList(int id, ItemType type, string key, string name, string empty, string flag2, State state)
        {

            List[lastIndex] = new Dictionary(7)
            {   { "ModId", id },
                {"Type" , type },
                {"Section" , key },
                {"Key" , name },
                {"OldVal" , string.Empty },
                {"NewVal" , flag2 },
                {"State" , state },
            };
            lastIndex++;
            if (lastIndex > 5000)
            {
                var db = new DataService();

                   db.InsertBatch(@"INSERT INTO ModChange (
                                            ModId,
                                            Type,
                                            Section,
                                            [Key],
                                            OldVal,
                                            NewVal,
                                            State
                                        )
                                        VALUES (
                                            :ModId,
                                            :Type,
                                            :Section,
                                            :Key,
                                            :OldVal,
                                            :NewVal,
                                            :State
                                        )", List.Take(lastIndex));
            List = new Dictionary[5000];
            lastIndex = 0;


            }
        }
Bom um pouquinho grande, mas dá pra entender, eu coloquei cada batch pra 5k de registro, me parecia ser suficiente, mas vamos ver como ficou o resultado no GC?


É… mais uma vez aumentamos a quantidade de varreduras do GC, mas o que podemos fazer pra resolver isso?

Nós estamos criando uma lista de dictionary, que depois será criado transformado em uma lista de parameters conforme o código aqui

vamos resolver isso criando uma lista de parameters.

  static SQLiteParameter[][] List = new SQLiteParameter[12000][];
        static int lastIndex = 0;
        public static void AddToList(int id, ItemType type, string key, string name, string empty, string flag2, State state)
        {
            if (lastIndex == 12000)
            {
                UpdateDatabase();
            }


            List[lastIndex] = new SQLiteParameter[7] {
                new SQLiteParameter("ModId", id),
                    new SQLiteParameter("Type", type),
                    new SQLiteParameter("Section", key),
                    new SQLiteParameter("Key", name),
                    new SQLiteParameter("OldVal", empty),
                    new SQLiteParameter("NewVal", flag2),
                    new SQLiteParameter("State", state)
            };

            lastIndex++;



        }

        public static void UpdateDatabase()
        {
            var db = new DataService();
            var query = @"(..query..)";

            using (var connection = new SQLiteConnection($"Data Source={DataService.DbLocation}; Version=3;"))
            {
                connection.Open();
                using (var transaction = connection.BeginTransaction())
                using (var command = connection.CreateCommand())
                {
                    command.Prepare();
                    command.CommandText = query;

                    foreach (var item in List.Take(lastIndex))
                    {
                        command.Parameters.AddRange(item);
                        command.ExecuteNonQuery();
                    }

                    transaction.Commit();
                }
            }

            lastIndex = 0;
        }

resultado:

o resultado pra mim é simbólicamente o mesmo, mas por que será que não houve melhora?

existem duas coisas que pioram a vida:
– GC sendo executado com frequência porque tem objeto que não está sendo usado.
– Alocar algo na memória tem seu custo..

mas e se não criarmos mais objetos que precisamos usar?

bom, fiz a adequação no código

static SQLiteParameter[][] List = new SQLiteParameter[5000][];
        static int lastIndex = 0;
        public static void AddToList(int id, ItemType type, string key, string name, string empty, string flag2, State state)
        {
            if (lastIndex == 5000)
            {
                UpdateDatabase();
            }


            if (List[lastIndex] == null)
            {
                List[lastIndex] = new SQLiteParameter[7] {
                new SQLiteParameter("ModId", id),
                    new SQLiteParameter("Type", type),
                    new SQLiteParameter("Section", key),
                    new SQLiteParameter("Key", name),
                    new SQLiteParameter("OldVal", empty),
                    new SQLiteParameter("NewVal", flag2),
                    new SQLiteParameter("State", state)
                };
            }
            else
            {
                List[lastIndex][0].Value = id;
                List[lastIndex][1].Value = type;
                List[lastIndex][2].Value = key;
                List[lastIndex][3].Value = name;
                List[lastIndex][4].Value = empty;
                List[lastIndex][5].Value = flag2;
                List[lastIndex][6].Value = state;
            }

            lastIndex++;



        }

        public static void UpdateDatabase()
        {
            var db = new DataService();
            var query = @"INSERT INTO ModChange (
                                            ModId,
                                            Type,
                                            Section,
                                            [Key],
                                            OldVal,
                                            NewVal,
                                            State
                                        )
                                        VALUES (
                                            :ModId,
                                            :Type,
                                            :Section,
                                            :Key,
                                            :OldVal,
                                            :NewVal,
                                            :State
                                        )";

            using (var connection = new SQLiteConnection($"Data Source={DataService.DbLocation}; Version=3;"))
            {
                connection.Open();
                using (var transaction = connection.BeginTransaction())
                using (var command = connection.CreateCommand())
                {
                    command.Prepare();
                    command.CommandText = query;

                    foreach (var item in List.Take(lastIndex))
                    {
                        command.Parameters.AddRange(item);
                        command.ExecuteNonQuery();
                    }

                    transaction.Commit();
                }
            }

            lastIndex = 0;
        }

resultado:

Vou encerrar por aqui,
mas para nossa TODO list temos:
– Processar somente os arquivos novos.
– implementar o carregamento de conflitos no frontend on-demand
– remover mais campos que não usamos pra ter mais eficiência.
– revalidar partes do código que só são sujeira.

Bom é isso, até o próximo post

Esteja curioso!

Sobre otimização em aplicações .Net – Parte 3 (final?)

Continuando essa saga, espero que seja útil pra você jovem aprendiz que anseia por casos reais de otimização em um código aberto.

Cenário anterior:

Memory: 937mb
Gen 0:499
Gen 1:103
Gen 3:10

Enquanto estive removendo bugs do meu mod, e estudando o Livro do Konrad, Pro .NET Memory Management, se você não conhece, tente assistir a explicação em uma conferência sobre performance.

Crédito: Amazon.

Acabei exercitando nesse código do KMM, e não documentei de maneira prática as alterações, mas vou apresentá-las com a ausência do meu pensamento na hora.

Utilizando a técnica que apresentei na parte 2 fui iterando nos itens que foram apresentando maior impacto no GC e memória.
lista de alterações:

  • Remoção de uma lista de tags que não fazia mais sentido pro código.
  • Alteração de alguns setters para private afim de identificar quais classes eram passíveis de ser transformadas em structs.
  • Simplificação de algumas strings que estavam sendo concatenadas em apenas uma string (Agora estou lembrando do Crocco), mas já já eu uso a Span
  • dupliquei a parte que carrega algumas alterações dos mods, porém removi os pontos que não eram pertinentes para identificar os conflitos.
  • transformei mais algumas classes em Structs e adequei o código para inicializar os valores direto na struct.
  • simplificação para setar o valor de TripleInt
  • remoção de uma lista sem capacity que não estava sendo usada para nada, mas mesmo assim era populada.
  • alteração para setar o valor do filename em uma propriedade invés de sempre ficar chamando Path.GetFileName((…));

Bora pra parte de praxe o commit e o resultado:

O que achou?

não sei você, mas eu estou aqui:

Clap Applause GIF

sumarizando os resultados:

Gen 0: 187 redução de 84% comparando com 1217 do primeiro post
Gen 1: 72 redução de 79% comparando com 351 do primeiro post
Gen 3: 11 redução de 54% comparando com 24 do primeiro post
Tempo: 13179ms redução de 83% comparando com 77309ms do primeiro post.
Memória: 590mb redução de 37% comparando com 944 do primeiro post.

Apesar desses resultados muito bons…

Não me leve a mal, não é que acho o resultado insatisfatório, mas o problema aqui não é mais eficiência de código, apesar de ter espaço pra otimização.

Consideremos a frase de Lowell Arthur descrito em “Software Engineering: A Practitioner’s Approach” by Roger Pressman:
20 percent of the code has 80 percent of the errors. Find them; fix them!

O que quero dizer é que os 20% do código, até mais que isso foi otimizado, porém eu preciso resolver o armazenamento de informação e indexação ineficiente BY DESIGN

vamos compreender o cenário:

  • São mais de 300 mods
  • cada mod pode conter milhares de alterações.
  • usamos os arquivos base do jogo para identificar quais alterações foram feitas.
  • carregamos todos os mods, indexamos os conflitos e enviamos pra memória

Somente com esses pontos é explícito o próximo passo, precisamos indexar essas alterações quando não existirem em uma base de dados, caso exista e o arquivo foi alterado indexamos, do contrário apenas usamos o que está disponível desta forma utilizaremos o consumo de CPU de forma eficiente, já que uma vez que ele trabalhar pra processar os mods, o trabalho será recuperável.

Também ganharemos mais memória, já que poderemos então carregar os conflitos somente para o mod que for selecionado na tela.

Próximos passos

Meu processo de otimização neste cenário se mostra suficiente para o problema abordado,

continuarei trabalhando na otimização e indexação desses arquivos de forma eficiente.

acabei corrigindo o valores de ganho, porque eu acabei invertendo as imagens…. :facepalm:

continuarei documentando as decisões que fui tomando, tentarei trazer o brainstorm que posso ter tido.

Esteja curioso!

Sobre otimização em aplicações .Net – Parte 2

Bom, continuando o post anterior: Sobre Otimização em aplicações .Net – Parte 1, se você não leu, vai lá,
porque eu estou tentando ser o mais detalhista possível.

Nós paramos no seguinte cenário:

eu dei uma lida no código inteiro e não particularmente não vi nada gritante muito explícito, sendo assim, já posso começar fazer o que acho mais conveniente.

Quebrando o problema em partes.

Bom, o código é grande o suficiente para que minha eficiência seja baixa, então vamos separar em 5 pontos.

  • Carregar os mods.
  • Identificar os conflitos
  • Atualizar a lista de conflitos
  • Atualização da GUI
  • Feedback via progress Bar

Destes pontos eu preciso identificar aonde está o maior tempo

O que se resume em :

https://gist.github.com/millerscout/6f2db01d0e304f997ec3c61efdc9b5c8
Sim… um código muito grande não é mesmo? eu também acho que é necessário dividir pra conquistar.

Coletando Métricas

Primeiro passo foi separar todo o código que eu tinha em um ambiente mais controlado, afinal neste mesmo projeto outros itens utilizam o mesmo recurso de Feedback pro usuário e atualizam a GUI sem impacto, logo nosso foco é mais específico.

Não sei se costumam usar o profiler do VS, se não tem o costume, comecem, ele é mais barato que quebrar a cabeça, chutar o problema ou esperar que mágicamente o problema suma.

Eu tenho uma lista de Profilers que já usei, o do VS é okay, meu favorito? nProfiler, como a ferramenta é cerca de R$ 3000, vou demorar um pouco pra adquirir e apresentar a diferença em um post futuro.

Voltando ao assunto…

Coloquei o breakpoint em 5 pontos:

  • Na primeira linha do main.
  • Antes de carregar os mods pra memória.
  • Antes de carregar as alterações do jogo base.
  • Carragando as mudanças do Mod.
  • Última linha.
Bom perceberam? quando clica nos eventos na parte superior dá pra identificar a quantidade de GC, Memória e CPU está sendo consumido, as setas amarelas são execuções de limpeza do GC 😉

essa é uma forma, fácil de saber qual dos itens você quer começar a otimização, quer uma mais fácil ainda sem breakpoints?

Alt+F2, selecione CPU Usage e rode o projeto.

só faltou o VS programar pra você agora né?

Vamos nos focar nesse caso aqui

faz sentido demorar eim..

Várias iterações que podem ser simplificadas.

Dados coletados, bora pro código

Vamos dar um tapa nessa parte, e mensurar os ganhos.

O Commit da alteração e o resultado:

de certa forma melhorou, reduzimos um pouco o GC, aumentou o tempo para processamento mas já esperávamos, já que não processamos mais paralelamente as alterações, Estamos dependendo do IO, dá pra fazer mais nada… será?

O Inimigo agora é outro:

Mais uma iteração no commit mudanças:

  • Mudei para acessar o dictionary diretamente e atualizar o objeto
  • Remoção do concurrentBag.
  • Alteração de classes para structs de ModListChanges e DetailChanges

Gen 0:499
Gen 1:103
Gen 3:10

estamos no caminho certo, começando respeitar o GC, executamos mesmo processo e em alguns segundos mais rápido, está bom, mas vamos buscar a excelência, ou até onde minha capacidade der, mas vocês podem me mandar msg pra melhorarmos ainda mais 🙂

Notas:

  • Percebeu como eu ignorei o fato que estou usando 3 objetos estáticos em uma classe nada haver “Helpers”?
    pois é, o foco está em resolver o problema depois pensamos em uma forma elegante de organizar o código.
  • Tenho quase certeza que estava usando o updateFactory e addFactory errado naquele dictionary :x, nem vou olhar porque deu vergonha
  • paramos de atualizar um objeto em memória sem necessidade no DetailChanges.
  • Simplifiquei a forma que era gerado o Hash.

Vou encerrar mais esse post, acredito que temos grandes lições aprendidas.
E lembrem-se para tomar uma ação precisamos testar, mensurar, mudar e repetir.

Próximo post.. quando eu tiver tempo :p

Sobre otimização em aplicações .Net – Parte 1

Bom, acredito que nunca tenha postado algo específico do meu trabalho.

Um dos motivos para isso seria que parte das coisas que faço são muito sensíveis para compartilhar.

Mas esse final de semana comecei um projeto do zero para identificar e otimizar o Kenshi Mod Manager (KMM) do jogo Kenshi, porque isso vai me deixar exercitar as tarefas que preciso em meu trabalho e o risco é mais controlado.

Resumidamente o gerenciador tem vários recursos, mas quando desenvolvi há cerca de um ano, haviam problemas sérios de performance, bom… não pro meu pc, mas pra quem utiliza.

Tenho cerca de 300+ mods, e indexar os conflitos nele causam ~1GBs e um tempo considerável pra apresentar as falhas

Como que comecei a análise

O KMM é um WPF em .Net Core 3.1 e eu acho realmente muito trabalhoso analisar cada parte do código sem ter uma ferramenta externa, já que o projeto já passou de 5K de linhas..

Então já que eu precisava estudar gRPC iniciei primeiro criando dois projetos, um server e um client.

sendo que o server está no KMM e o client em um console no MMDHelpers.

A alteração no KMM foi muito simples, bom … já que ele não era uma aplicação web, eu apenas subi o kestrel.

Uma observação, neste webserver que eu estou subindo, estou ignorando a parte de segurança completamente, já que irei identificar os problemas, remover o pacote e disponibilizar a nova versão.

Com o Client eu pude enviar comando para iniciar ou terminar as métricas no KMM

https://gist.github.com/millerscout/e9228ffe5f61edf9f151cab3aa61dbf7
Simples não?

o resultado foi este:

Eu consigo observar quantas coletas em Gen0-2, a quantidade de memória e o tempo que levou.

o tempo aqui não é meu foco ainda, mais para abaixo explico uma otimização para tempo (mas é tiro de canhão pra matar mosca)

Primeiro Passo

Particularmente eu tento quebrar os problemas mais complexos para o mais simples possível, e existe um item que não tinha nenhuma coleta no GC mas era o primeiro item que eu queria otimizar

Temos aqui o cenário:

https://gist.github.com/millerscout/a628f32fb4edd9d710c0fbb6c2b775ff

O código nem causa problema, afinal só estou usando o IO.Path.GetExtension e comparando se o arquivo está naquela extensão.

então tinha duas opções:

  • Usar String.EndsWith
  • Criar o meu EndsWith

Ainda estou encontrando formas de pegar o código base do c# pra remover alguns recursos que não preciso validar, ganhando tempo de processamento.

Em minha solução eu achei mais fácil iterar uma string do final pro começo, levando em conta que se qualquer char for diferente ele retorna false,

o código ficou desse jeito:

https://gist.github.com/millerscout/879956f6a0038330992827d09fedd429

A única coisa que faltou foi um benchmark certo?

então tá ai:

https://gist.github.com/millerscout/52f6e56a64faa3658e7e6958c1261b29

não apenas descartei o EndsWith de string, como percebi que minha versão é mais eficiente pro problema um ganho de 69% de performance, se este problema estivesse em um cenário de 4M de iterações o ganho seria de 195.16ms para 60.44ms, Bom depende muito do cenário né? mas… é algo á se pensar quando for otimizar, aqui nem usei o Princípio de Pareto, só foi pra brincar mesmo.


Pontos de atenção:
no meu cenário não preciso verificar se a string é case-sensitive ou possui acentos.

Segundo Passo


Fiz uma dúzia de alterações simples:

  • Parei de usar List para um array pequeno onde eu não precisava fazer nada apenas conter a lista.
  • Alterei algumas classes para structs pois as classes eram imutáveis e também não há risco de cópia em memória delas.
  • adicionei o capacity em algumas até o momento onde revisei

O que seria dar essa lista sem o commit não é mesmo? segue ai:

https://github.com/millerscout/Kenshi-Mod-Manager/commit/7f9ef386bdcb902ed3a168afac8eaaa23babf4c1

o resultado foi bem promissor:

Gen 0: 990 redução de 18% comparando com 1217 anteriormente
Gen 1: 284 redução de 31% comparando com 351 anteriormente
Gen 3: 28 aumento de 16% comparando com 24 anteriormente

Primeira parte encerrada aqui.

Você pode continuar lendo a Parte 2

Conclusão

Ainda há um longo caminho de aprendizado e técnicas para aprimorar minha capacidade de fine-tuning em .Net, mas acredito que o resultado mostra que não é rocket science, apenas precisamos nos policiar em coisas simples para trazer um ganho maior.

Créditos

Acredito que essa minha vontade de otimização não seriam possíveis caso as conversas com o @crocco @Massato não tivessem acontecido.

Eu tive também alguns materiais para me inspirar e aprender, bem como algumas referências, sendo elas:

o tio Scott, Elemar Jr. e Konrad Kokosa

Estive consumindo vários artigos da Eximia.Co, documentações da própria microsoft em momentos específicos os seguintes itens:

Sneak Peek

Próximo post gostaria de experimentar mais sobre sobre warmup em .Net, comecei ler algumas coisas, assim que tiver algo conclusivo trago para compartilhar 🙂

Enfim 30!

Estou aproveitando que amanhã é feriado (pra mim)

Bom, eu tentei dormir, mas estou há dois dias estive pensando nesse post, hoje eu consegui pegar o fio da meada enquanto estava olhando pra minha esposa toda linda dormindo.

eu não durmo mesmo, não tenho tanta necessidade. acredito que daqui uma hora (agora:00:25) eu vou ter sono, até lá tenho tempo pra escrever 😀

Pra mim é estranho, ontem eu estava brincando com meus brinquedos, pensando no futuro, imaginava como seria ter 30 anos, ser sábio ou algo assim.

Mais estranho ainda quando paro pra pensar quanta “sabedoria” eu já ouvi na vida, acho que deve ocorrer com todo mundo,
algumas dessas “sabedorias” me feriram, outras eu guardei na gaveta do coração,só pra poder fazer a pessoa engolir o que disse, outras eu simplesmente ignorei.

Alguns exemplos:

  • Você é fogo de palha, não foca em nada.
  • Você precisa tomar alguma ação, você só está esperando as coisas acontecerem.
  • Você não é programador, só altera variáveis.
  • Se eu te der R$ 10k, você não vai conseguir pagar um carro.
  • Tens de mudar de emprego, você tá perdendo seu tempo.
  • Deixar a faculdade foi o maior erro da sua vida.

Eu lembro que começei esboçar um planejamento lá pros meus 11-12 anos, quando eu começei brincar de “programar”,
eu tinha objetivos bem claros.

sempre que dava uma “merda” na vida eu me replanejava como podia, triste os adultos não conversarem disso com as crianças, meus pais não tiveram culpa, casaram muito jovens, iam ensinar o quê? nem eles sabiam o que estavam fazendo.

Como uma ótima pessoa introspectiva eu preferia não compartilhar meus objetivos… afinal eu já tinha ouvido tanto “você é fogo de palha”, que eu também achava que esse planejamento iria pras cucuias.

eu me planejei com poucas coisas até meus 30:

  • fazer faculdade.
    • eu iniciei, mas não tive dinheiro terminar, essa tem um post aparte…
  • ser um profissional único, pra poder ter e manter-me em um bom emprego ( acredito que ainda não escrevi o motivo, mas meus pais passaram muito aperto da vida, e eu não queria que minha família/filhos sofressem)
    • eu acho que não sou o melhor, mas percebo que mais resolvo problemas que causo, então tá bom..
  • conhecer alguém e morar junto com essa pessoa antes dos meus 30.
    • eu não conheci alguém, eu encontrei minha alma gêmea, é bem legal que eu encontrei alguém que não chega nem perto do que eu queria, mas é perfeita pra mim, uma pessoa amável, calma, sincera, emotiva, dedicada, empática e a lista não para…
  • ser independência, independência é muito abstrato, mas na minha visão era independência emotiva
    • essa eu aprendi cedo, desde quando minha mãe se divorciou eu comecei mais e mais calado, com objetivo em mente, eu já não precisava de “guia”
    • a vida me levou pra independência financeira também (não sou rico, mas se acabar ficando desempregado, dá tempo de procurar trabalho)
  • saber dirigir com meus 18
    • se eu não tinha dinheiro pra faculdade… se acha que eu tinha pra tirar habilitação?
  • sair da casa de meus pais assim que ter um emprego. (eu era muito inocente)…
    • eu conheci bem mais rápido que esperava, claro que tive alguns atalhos, mas consegui.

Por fim eu consegui cumprir com maior parte desse planejamento, parece que planejei minuciosamente tudo, na verdade não, mas fui me adaptando e errando menos conforme fui aprendendo mais da vida.

hoje, mais velho acho consigo responder cada uma das frases que ouvi, e a melhor parte?
eu não devo nada pra ninguém, conquistei aos poucos, sem pressa, e com total apoio de minha mãe,
tinha decisão que foi mais loucura que tudo, mas ela sempre deu apoio e dizendo: “vai, eu confio em você, não liga pros outros, de fome não morremos, mas se morrer estamos salvos pela graça, vamos pro céu”, essa frase dela pra mim é morbidamente boa,

Eis minhas respostas:

  • Você é fogo de palha, não foca em nada.
    • ainda bem… Eu aprendi tanta coisa e experimentei tanto, que aprender novas coisas e tomar decisões se tornaram triviais pra mim
  • Você precisa tomar alguma ação, você só está esperando as coisas acontecerem.
    • não, eu gosto de estar preparado pra quase todas as oportunidades que aparecem, enquanto elas não aparecem eu fico me preparando mais,
      eu não me frusto por estagnar por um tempo pra me preparar, mas eu fico muito mal caso apareça uma oportunidade e eu não estiver pronto.
  • Você não é programador, só altera variáveis.
    • essa eu nem respondo, só trás o problema, e vamos ver em quanto tempo eu resolvo :p
  • Se eu te der R$ 10k, você não vai conseguir pagar um carro.
    • (essa me doeu), o problema pra mim nunca foi a entrada do carro, sempre foi o “pós-venda”, comprar um carro não é só comprar um carro, é um filho, tem manutenção, revisão, seguro, combustível,
      eu aprendi o preço da falta de planejamento quando tive de deixar a faculdade, pra mim eu sou sozinho, não tenho ninguém pra me dar backup, na verdade eu sou o porto-seguro de algumas pessoas, por isso eu sou sozinho e tenho que me garantir,
      já que não tenho mamãe e papai, familiar pra me dar uma ajuda e nem fodendo eu pego empréstimo na minha vida exceto por falta de opção.
  • Tens de mudar de emprego, você tá perdendo seu tempo.
    • só porque passei 6 anos e 6 meses em uma empresa onde eu não recebia o merecido, não significa que estou perdendo meu tempo,
      na verdade eu considerei que estive fazendo faculdade nesse processo e ainda ganhava pelas merdas que eu fazia, o aprendizado foi tanto que eu sou um profissional fora da curva (segundo feedbacks)
  • Deixar a faculdade foi o maior erro da sua vida.
    • pode ter sido, mas faculdade é só um dos meios de abrir portas e adquirir conhecimento, e eu aprendi tanto com o que a faculdade ensina como também planejamento financeiro.

acho que está de bom tamanho essa foi a parte 1 O.o

vou acabar pegando esses meus posts e migrar pra um blog de capsula do tempo, eu gosto muito de ler eles depois de um tempo e perceber quanto minha forma de sintetizar um texto ou a forma que eu pensava mudou.

eu quase acertei (agora: 02:20) me deu sono, então depois eu reviso…

Leben – Day 36

Estive descansando um pouco do Leben, mas hoje tive vontade de voltar desenvolver.

Hoje estive adicionando um processo de propagação de arbustos, encontrei vários erros no Core do Leben que estavam ignorando o timer dos eventos, depois de corrigir todos, agora sei que é mais difícil ter um memory-leak tão rápido, mas ainda há muito trabalho nessa parte.

Primeiro porque um objeto está sendo Spawnado em cima de outro, e não existe um limite de Arbustos na região, então eles se propagam indefinidamente e isso é um problema, ainda não pensei em como limitar, talvez por nutrientes no solo… mas ainda não sei.

Como sempre um gif pra mostrar o comportamento.

Só pra manter o Blog ativo.

Bom, eu gosto de escrever pro meu blog,

Estou sem desenvolver no Leben, dá um pouco de preguiça e existe muitos projetos que quero fazer e ainda preciso me distrair pra manter a sanidade.

estive jogando Raft, mindustry, this war of mine e autonauts.

Mas principalmente estou dando suporte á minha ferramenta para gerenciar mods pro jogo Kenshi

Conforme ela está crescendo percebo como é legal ajudar outras pessoas gerenciar seus mods e melhorar a experiência de um jogo tão legal, por isso vou continuar melhorando ela.

bom era isso, só queria postar algo mesmo

:p