docker-compose.yml com podman

Contexto

Mesmo adorando usar docker, principalmente porque ele tem uma interface :heart_eyes:

acabei tendo que buscar alternativas, já que ele começou ser pago para empresas, sempre tentei compartilhar a liberdade que existe em usar um ambiente dentro de um container.

Pra mim é algo mágico poder simplesmente matar o container e fazer de novo, sem medo de perder algo, ou até mesmo pedir pra alguém testar algo.

enfim…

um dos meus projetos é apresentar alguns environments para outras pessoas, afinal é mais prático você apresentar algo á alguem usando o recurso do que pedindo pra pessoa estudar

em minhas conversas sempre é o mesmo papo, pergunto se a pessoa conhece docker, e no final a pessoa geralmente fala “já ouvi falar, mas nunca usei”, quando isso acontece eu tenho a oportunidade mais divertida, porque hoje eu compartilho minha tela e começo mostrando como é “dificil” subir alguma coisa, eu escolho o item mais simples que conheço: rabbitmq que por um acaso é um outro amor que tenho 😀

Exemplo de subir uma imagem

compartilho geralmente meu repositório de environments

então abro um console e digito:

git clone https://github.com/millerscout/environments.git
cd environments
cd rabbit
cd Default
docker-compose up


e Voilá! tá pronto, só abrir o localhost:15672

logo com user guest e senha guest e pronto

é tão simples que acaba sendo mais fácil ainda quando eu mostro como eu fiz, no docker-compose.yml

não é nada mais nada menos que só isso:

version: '3'

services:
  rabbitmq:
    image: "rabbitmq:3-management"
    hostname: "rabbit"
    ports:
      - "15672:15672"
      - "5672:5672"
    labels:
      NAME: "rabbitmq"

configurando o podman

bom primeiro eu tive que instalar o podman (duh?!)

tive que instalar um script em python para funcionar o podman-compose, tudo instalado, tranquilamente

mas quando executei: podman-compose up

sem sucesso acabei fazendo o que todo dev experiente faz, abri o google e pesquisei :scream:

e achei dois posts que são importantes:
https://github.com/containers/podman/issues/11530 que é o “problema” que eu tenho
https://www.redhat.com/sysadmin/container-image-short-names que é a explicação do motivo que acontece.

então acabei alterando meu yml pra funcionar tanto com podman quando docker apenas acrescentando a origem da imagem

version: '3'

services:
  rabbitmq:
    image: "docker.io/library/rabbitmq:3-management"
    hostname: "rabbit"
    ports:
      - "15672:15672"
      - "5672:5672"
    labels:
      NAME: "rabbitmq"

sim… somente essa mudança.

bom, agora que você sabe como rodar isso tanto em docker quanto em podman, vai lá e começa brincar e compartilhar com alguém do seu time que ainda não está usando 😀

até mais

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 🙂