Performance e escalabilidade – Profiler AdvPL

Introdução

Qualquer rotina de um sistema, mais cedo ou mais tarde, e por inúmeras razões, pode apresentar em um ou mais instantes, uma perda de desempenho. E, geralmente quando isto acontece, principalmente em rotinas onde uma eventual demora impacta na produtividade ou na operação do cliente, problemas desta natureza saltam para o nível “crítico” muito rápido.

Mesmo lidando apenas com as causas prováveis, existem muitas variáveis envolvidas que atuam direta ou indiretamente nas causas. As mais comuns são: Chamada de um procedimento ou rotina com parâmetros de execução que consideram uma faixa muito grande de dados, quantidade de dados na base para o período de processamento muito acima da média (causas sazonais, picos), processos adicionais que podem ou não pertencer ao sistema levando o ambiente a picos de utilização de recursos ou esgotamento de recursos computacionais, defasagem do “sizing” de máquina devido a aumento de usuários ou módulos adicionais implantados, troca da versão do ERP sem reavaliar o “sizing” das máquinas, até razões ligadas ao desenvolvimento da rotina, como por exemplo um algoritmo projetado sem levar em conta o crescimento da base, e por aí vai.

Identificando as causas

Dada uma determinada situação de perda de desempenho, nomalmente avaliamos se algum dos recursos computacionais do ambiente está perto de seu limite, como utilização de disco, fragmentação de arquivos, consumo de banda de rede, cpu das máquinas de processamento e do banco de dados, além de levantar os detalhes de como a perda de desempenho foi percebida, qual era o tempo médio antes e agora, e se houve alguma alteração no ambiente ( infra, repositórios, binário) entre o cenário antes da lentidão ser percebida e a partir do momento que ela passou a ser reproduzida, além de avaliar se ela ocorre sempre a qualquer momento ou especificamente em momentos de muitos acessos (usuários) na aplicação.

Como eu já havia mencionado, normalmente as causas prováveis envolvem o excesso de consumo de um ou mais recursos, prejudicando o desempenho de algumas rotinas. Quando mais genérico e de uso comum o recurso em questão, mais rotinas ou até mesmo o ambiente inteiro pode ser afetado.

Como os algoritmos normalmente consomem não especificamente um recurso, mas múltiplos recursos simultaneamente, onde a quantidade e os recursos consumidos podem oscilar dentro da mesma aplicação, mesmo que você determine usando uma ferramenta de monitoramento de sistema operacional, que uma das causas da lentidão seja uma fila excessiva de espera por requisições de leitura ou gravação em disco, ainda assim existe a necessidade de se avaliar qual ou quais programas estão contribuindo para que este recurso seja esgotado.

Profiler AdvPL

Neste momento, existe um recurso que se faz muito útil, chamado Profiler (ou Profile) de exexcução de aplicações, que nada mais é do que um registro de log detalhado de quais recursos foram utilizados por um processo, e em qual quantidade, avaliando o prorcesso do início ao final.

Para auxiliar a avaliação de aplicações nestes momentos, ou mesmo para auxiliar o desenvolvedor para avaliar o impacto de alterar uma determinada rotina, foi criado já faz alguns anos o recurso de Profiler AdvPL. Este recurso inicialmente estava disponível no IDE do Protheus, onde a geração do log detalhado de execução era feito apenas para programas iniciados a partir de um processo de depuração. Este recurso também foi incorporado no novo IDE (TDS – Totvs Development Studio), inclusive está documentado no link “Desempenho de Programas (Profile)” — http://tdn.totvs.com/pages/viewpage.action?pageId=24347198

Posteriormente, foi criado uma parametrização de ambiente, para permitir a geração deste log detalhado para jobs, e sem a necessidade de iniciar o processo usando o TDS. Este recurso está documentado no link “LogProfiler – Profiler de execução de programas AdvPL” — http://tdn.totvs.com/pages/viewpage.action?pageId=6065063

Resumo da Ópera

Nos links acima, principalmente no último link, são detalhados não apenas como o recurso é habilitado, mas quais informações são geradas no log, e como elas devem ser interpretadas, inclusive são mencionados os casos mais comuns encontrados em análises de execuções já realizadas e identificadas durante o desenvolvimento e testes dos produtos.

A vantagem destes recursos de Profiler é a possibilidade de serem executados no mesmo ambiente em momentos distintos, onde o log de duas execuções completas que apresentaram diferenças gritantes de desempenho podem ser comparados, tornando mais claro qual ou quais pontos ou funções usadas na aplicação apresentaram diferenças de tempo de resposta. Esta análise, associada a um resultado de monitoramento de recursos do ambiente, pode gerar justamente a rastreabilidade entre as aplicações e os recursos que estão sendo consumidos (e esgotados) do ambiente.

Normalmente, após o código e o ambiente ser analisado, pode-se chegar a algumas conclusões: O algoritmo pode ser melhorado para usar menos recursos para obter o mesmo resultado esperado, ou o algoritmo já está optimizado segundo o paradigma original do processo, e precisa ser refatorado para atender aquela demanda, ou ainda devem ser revisadas e refeitas as distribuições das aplicações no ambiente, e em determinados pontos pode haver a necessidade de acrescentar mais recursos computacionais ( Mais servidores para distribuir a carga, ou escalar verticalmente alguma das máquinas envolvidas que não está dando conta da carga de requisições em questão.

Podem ser necessárias mais de uma ação ao mesmo tempo, dependendo de onde está (ou estão) o(s) gargalo(s) de desempenho da aplicação.

Conclusão

Avaliar um log de Profile de aplicação não é necessariamente complexo, porém o log de execução gerado é muito grande. Neste log, devem ser identificados os pontos realmente significantes, para então decidir onde e como atuar no ambiente ou na aplicação. Uma boa parte da teoria está nos documentos publicados na TDN, com exemplos práticos de análise e tudo mais. O que vai fazer a diferença na sua eficácia de análise é pegar um problema real, e analizá-lo com uma lupa. Quanto mais você meter a mão na massa e avaliar cenários diferentes, mais eficiente será a sua análise. Não têm mágica, você precisa sujar um pouco a mão de graxa e fritar alguns neurônios. Não se acanhe, não é um bicho de sete cabeças … esse aí têm no máximo duas !!!

E, para encerrar 2014, desejo a todos vocês um Feliz Natal atrasado, e um próspero Ano Novo adiantado !!!

Até o próximo post, pessoal 😉

Persistência de dados – ISAM

Introdução

Uma parte fundamental de qualquer sistema informatizado é a persistência dos dados. Hoje temos bancos de dados ISAM, relacionais ( SQL ), Bancos No-SQL — para dados não-tabulados, XML, entre outros. Cada um deles é fundamentado em um paradigma para o tipo, quantidade, formato e necessidade de armazenamento e recuperação de dados. Como a maioria dos recursos da tecnologia da informação, cada um destes bancos possui um diferencial ligado a casos de uso. Muitas aplicações comerciais podem utilizar mais de um tipo de banco de dados, de acordo com a necessidade de cada ponto da aplicação.

Neste tópico será abordado um pouco do conceito, características e utilização de mecanismos que usam a implementação ISAM para persistência e consulta de dados.

Conceito (um pouco de teoria é inevitável)

ISAM é um acrónimo de Indexed Sequential Access Method, ou método de acesso sequencial indexado. Trata-se de um método de indexação de dados para acesso rápido. Este mecanismo foi desenvolvido pela IBM para mainframes, antes da década de 70. O termo ISAM aplica-se atualmente a mecanismos e APIs de acesso a dados onde o desenvolvedor explicitamente faz a busca em um índice, para então recuperar a informação que o índice aponta (em contraste com um banco relacional, que possui um optimizador de queries que escolhe internamente o melhor índice a ser usado), ou ainda a um mecanismo de indexação que permita simultaneamente buscas sequenciais e por chave. Normalmente a API ISAM é composta por um grupo de funções, usadas para realizar operações individuais com tabelas, índices e registros.

Características e utilização

Os dados são organizados em registo de tamanho fixo, e salvos sequencialmente em um arquivo, onde a posição ordinal do dado no arquivo representa o seu número de registro. Um ou mais índices podem ser criados para este arquivo, com um ou mais campos nas chaves, e as operações básicas de recuperação de dados consiste basicamente em buscar uma informação diretamente em um índice a partir de uma chave de busca montada com os campos usados no índice, onde após o registro ser encontrado no índice, a informação na tabela de dados é posicionada (ponteiro de registro), e a partir deste instante você pode navegar para os próximos registros de dados ou aos registros anteriores, usando a ordem do índice atualmente aberto, ou usando a ordem natural de inserção (física) da tabela. Algumas APIs permitem campos de conteúdo variável, que normalmente são gravados em um arquivo auxiliar, onde um registro do arquivo ISAM de dados aponta para um offset do arquivo auxiliar para recuperar o(s) campo(s) de tamanho variável.

Na página dedicada ao ISAM pela Wikipedia (em inglês, vide referências no final do post), têm uma lista de aplicações que implementam a arquitetura ISAM. Entre elas, o Clipper, FoxPro, Microsoft Access, Faircom c-TreeACE, Dataflex, Paradox, entre outras. Existe uma relação mais próxima entre o dados fisicamente gravados e o desenho do algoritmo. Normalmente um sistema que utiliza uma engine ISAM para dados obtém grande desempenho desenhando bem a distribuição dos dados dentro de tabelas e os índices necessários para acessar rapidamente as informações.

Destas ferramentas, algumas também oferecem uma camada superior de abstração para implementar acesso relacional, com queries e tudo. A raiz dos dados no disco continua sendo ISAM, mas existem as tabelas de catálogo do banco (systables, sysindexes, syscolumns, etc.) e os algoritmos para realizar o parser de queries, recuperação de dados, transacionamento, etc.

Na prática, é muito mais fácil e prático desenvolver usando um banco relacional. Você cria as tabelas, índices para acelerar as buscas mais utilizadas ou que precisam efetivamente de performance, e utiliza recursos do SGDB para monitorar, configurar e administrar o Banco de Dados, como as estatísticas de índices, segmentação de dados, recursos avançados de pesquisa de textos, além de existir uma ampla gama de aplicações de mercado construídas para conectar-se com estes bancos e permitir de forma assistida a recuperação de dados para análises, estatísticas e relatórios.

Como eu mencionei anteriormente, cada um dos tipos de banco de dados possui um diferencial interessante, muito eficaz quando aplicado no seu cenário ideal. Por exemplo, em uma etapa intermediária de um processamento, ou para armazenamento de dados de baixa volatilidade ( pouca manutenção de estrutura e conteúdo), uma API ISAM pode oferecer um diferencial de performance, devido justamente ao fato de não termos presentes alguns overheads que são implícitos em um Banco de Dados relacional. Porém, a decisão de como distribuir os dados e como criar e usar os índices paa acessar as informações da forma mais eficiente possível ficam na mão do desenvolvedor. E se isto não for feito como manda o figurino, o ganho que poderia ser obtido em determinada parte do processo usando o ISAM é engolido pela forma de implementação do algoritmo.

Uso no ERP Microsiga

O ERP Microsiga utiliza um gateway de conexão com bancos de dados relacionais (DBAccess), homologado para uma lista de Bancos de Dados relacionais, como Microsoft SQL, Oracle, DB2, Informix, e inclusive versões Open Source, como o PostgreSQL e o MySQL — com algumas restrições e usos em produtos específicos. E, ao mesmo tempo, utiliza também o c-TreeACE, da Faircom, para acesso a tabelas de meta-dados do ERP (também conhecidos como “SXS”), bem como armazenamento de dados em módulos do ERP concebidos para serem executados em modo Stand-Alone, e para criar tabelas temporárias compartilhadas entre servidores (a partir do RootPath do ambiente, compartilhado pela rede com as demais máquinas servidoras em ambiente balanceado) e exclusivas (tabelas temporárias no HD local da máquina onde o serviço do TOTVS | Application Server está sendo executado).

Conclusão

Na programação contemporânea, quem não conhece mais profundamente a camada ISAM normalmente referem-se a ela como algo ultrapassado. Realmente, usar uma base principal ISAM em um sistema com larga previsão de crescimento de base de dados, dada a amarração intrínseca com o meio físico, fatalmente você iria esbarrar com questões de performance e dependência direta de devices/hardware. Inclusive, por causa disso, a IBM criou um superset do ISAM, chamado VSAM (Virtual Storage Access Method) na década de 70. Porém, a engine ISAM é muito eficiente para determinados casos de uso, muito comuns em etapas de processos intermediárias, como a criação de arquivos de uso temporário.

A criação de arquivos temporários é um tema que merece atenção especial, e será abordado em profundidade em um outro post, diretamente relacionado a performance e escalabilidade.

Até o próximo post, pessoal 😉

Referências

Wikipedia contributors. ISAM. Wikipedia, The Free Encyclopedia. October 20, 2014, 21:33 UTC. Available at: http://en.wikipedia.org/w/index.php?title=ISAM&oldid=630429759. Accessed December 19, 2014.

Performance e escalabilidade – Hash Map – Parte 2

Introdução

No post anterior, vimos um exemplo básico de uso de Hash Map em AdvPL, com foco em um programa comparativo de desempenho, usando um array pequeno como referência, e realizando um número muito elevado de buscas  neste array para tornar visível a diferença de desempenho. Porém, o exemplo em si era meramente didático, não necessariamente refletia um algoritmo que usamos diariamente em nossos programas.

Neste post, vamos ver um exemplo prático de uso do Hash Map, criado “vazio”, onde o caso de uso utilizado é relativamente comum de processamento com dados em array, e através deste exemplo conseguimos determinar e visualizar a partir de que momento o Hash Map faz efetivamente toda a diferença.

Hash Map “from scratch

Um objeto de Hash Map pode ser criado a partir dos dados de um array multi-dimensional já existente, o que o torna muito prático para ser implementado em partes específicas de programas já desenvolvidos que fazem muitas buscas neste Array, ou o objeto Hash Map pode ser criado vazio e populado diretamente, sem a necessidade de usarmos um array já montado como base.

Para criarmos o objeto Hash Map vazio, usamos a função HMNew(). Após criado o objeto, podemos acrescentar as chaves e respectivos valores associados neste Hash Map usando a função HMAdd(). O elemento acrescentado deve ser um array. É possível acrescentar mais de um elemento de array na mesma chave. Quando pesquisamos por uma chave usando HMGet(), caso a chave seja encontrada, o valor retornado por referência no 3o parâmetro é um array, contendo todos os elementos acrescentados naquela chave.

É possível trocar um elemento da chave usando HMSet(), e isto substitui o array de todos os valores referenciados por aquela chave, sem que isso mude o nome da chave de busca. É importante ressaltar que o tipo de dado usado para a troca deve ser um array, seguindo o mesmo formato dos demais. Você pode eliminar a qualquer momento uma chave do Hash Map usando a função HMDel(). E, ainda existe a possibilidade de usar chaves compostas no Hash Map, usando mais de uma coluna do array, onde podemos usar uma função auxiliar chamada HMKey() para garantir que a chave de busca está sendo montada da forma correta.

No caso de uso deste post, usamos apenas as três primeiras funções : HMNew() , HMAdd() e HMGet()

Caso de uso: Array sem elementos repetidos

Normalmente, quando você popula um array vazio, onde uma ou mais colunas são usadas como chave, e não pode haver chaves repetidas, para cada dado a ser inserido no array você faz uma busca com ASCAN() para ver se um dado com aquela chave já não foi inserido, para somente fazer a inserção caso a chave não exista. Usando esta abordagem , o tempo de busca tende a crescer (e muito) conforme o crescimento da quantidade de elementos neste array.

Você pode criar e usar um Hash Map para implementara este algoritmo, ao invés de usar o ASCAN(). Para ilustrar a diferença de desempenho entre ambos, eu escrevi um fonte de testes que usa o aScan() e usa Hash Map, medindo o tempo de ambos. O mesmo teste é executado com um número crescente de chaves não repetidas (pior cenário), iniciando com 2 elementos, e dobrando a quantidade de elementos a cada iteração, até 8292 elementos, e mostrando o desempenho por teste. Segue o fonte abaixo:

User Function TSTDup()
Local aDados := {}
Local aCheck := {}
Local nI
Local nKey
Local aValues
Local nTotal1,nTotal2
Local nElements
Local oHash
nElements := 2
// Testa de 2 a 8192 elementos, dobrando a quantidade de elementos 
// a cada teste ( 2,4,8,16,32,64,128,256,512,1024,2048,4096,8192 )
conout("Resultado ( Elementos / Tempo com Ascan() / Tempo com Hash")
While nElements <= 8192
 
   // Zera os arrays para fazer cada teste
   aSize(aDados,0)
 
   // Cria um array de dados sem valores repetidos
   For nI := 1 to nElements
      aadd(aDados, { nI , "Teste "+cValToChar(nI) } )
   Next
 
    // método 1: Verificar por repetições com Ascan()
    aSize(aCheck,0)
    nTotal1 := seconds()
    For nI := 1 to nElements
       nKey := aDados[nI][1]
       // Busca usando ASCAN
       If ascan( aCheck , {|x| x[1] == nKey }) == 0
          // Nao achou, acrescenta o elemento
          // no array de elementos nao repetidos
          AADD(aCheck,aDados[nI])
       Endif
    Next
    nTotal1 := seconds() - nTotal1
 
    // método 2: Usando Hash Map
    aSize(aCheck,0)
    nTotal2 := seconds()
    oHash := HMNew() // Cria um hash map sem dados 
    For nI := 1 to nElements
       nKey := aDados[nI][1]
       // Busca no Hash Map ( usando chave numérica ) 
       If !hmGet(oHash,nKey,@aValues)
          // Nao achou, acrescenta o elemento no hash 
          // e no array de elementos nao repetidos
          HMAdd(oHash,aDados[nI])
          AADD(aCheck,aDados[nI])
       Endif
    Next
    nTotal2 := seconds() - nTotal2
   // Mostra o resutado deste teste ( elementos, tempo ascan, tempo hash
    conout(str(nElements,5)+" / "+str(nTotal1,6,3)+" s. / "+str(nTotal2,6,3)+" s.")
 
    // Dobra a quantidade de elementos para o próximo teste
    nElements := nElements * 2
 
Enddo
Return

Resultado do teste

No meu equipamento, obtive os resultados abaixo no log de console:

Resultado ( Elementos / Tempo com Ascan() / Tempo com Hash
 2    / 0.000 s. / 0.000 s.
 4    / 0.000 s. / 0.000 s.
 8    / 0.000 s. / 0.000 s.
 16   / 0.000 s. / 0.000 s.
 32   / 0.000 s. / 0.000 s.
 64   / 0.000 s. / 0.000 s.
 128  / 0.016 s. / 0.000 s.
 256  / 0.032 s. / 0.015 s.
 512  / 0.125 s. / 0.015 s.
 1024 / 0.530 s. / 0.032 s.
 2048 / 1.950 s. / 0.046 s.
 4096 / 8.268 s. / 0.078 s.
 8192 / 38.243 s. / 0.188 s.

Agora vamos analisar com calma os resultados acima. Reparem que todos os testes de 2 a 64 foram tão rápidos que não contabilizaram diferença de tempo. Vamos focar nos primeiros resultados, onde não há diferença de tempo: Nos casos com 64 elementos ou menos, o tempo com ASCAN ou com HASH foi menor que 1/100 de segundo.

Agora, a partir de 128 elementos, os tempos do ASCAN() começam a ficar maiores, e a cada vez que dobramos a quantidade de elementos, o tempo de busca cresce desproporcionalmente, aproximadamente 8 x mais lento a cada 2x mais elementos. Enquanto isso, vendo os tempos com o uso do Hash Map: O tempo de busca somente foi superior a 1/100 de segundo após 256 elementos, e quando comparamos este tempo com o tempo do teste de 8192 elementos (32 vezes mais elementos), o tempo do hash cresceu aproximadamente 12 vezes, enquanto o tempo do ASCAN() cresceu 1195 vezes.

Veja o seguinte: A primeira busca com ASCAN() é feita com o array vazio, na segunda busca o array já tem um elemento, na terceira ele tem dois elementos, na ducentésima primeira pesquisa, o array já tem duzentos elementos, e como a pesquisa é sequencial, e para cada elemento do array é feita uma comparação, são feitas 32896 comparações para fazer 256 buscas em um array que cresce um elemento a cada busca.

Se considerarmos tempos ou diferenças de tempo neste tipo de operação abaixo de 1/10 de segundo irrelevantes, somente começa a haver diferença perceptível e ganhos visíveis de uso do Hash após 256 elementos. Na prática, isto significa que, se a sua verificação por duplicidade na montagem de um array onde não serão usados mais de 256 elementos, não faz diferença usa Hash Map ou ASCAN(). Agora, de 512 pra cima, quanto maior o tamanho do array, mais visível é a perda de desempenho em relação a uso do Hash Map.

Conclusão

Estou fadado a escrever o óbvio, e repetir a conclusão do primeiro post … então, conclui-se que “quanto mais elementos tem um array, mais ineficaz é realizar uma busca sequencial neste array”. O que nós não imaginamos, é que existem pontos onde nós achamos que o número de operações internas do sistema possa crescer tanto com o aumento da massa de dados no algoritmo. Para ajudar na descoberta destes pontos, muitas linguagens de mercado e até softwares independentes dos fabricantes da linguagem oferecem uma instrumentação para gerar um “Profiler” da aplicação.

O AdvPL possui um recurso de geração de Profiler nativo muito, muito interessante … mas este assunto fica para um próximo post !!! Até o próximo post, pessoal 😉

Performance e escalabilidade – Hash Map em AdvPL

O que é um Hash Map ( ou Hash Table ) ?

Em computação, uma tabela de hash (Hash Table) ou mapa de hash (Hash Map) é uma estrutura de dados que implementa um array associativo, também conhecido por chave-valor, onde um determinado valor é acessado por uma determinada chave. Em muitas situações, esta estrutura é muito mais eficiente do que utilizar uma árvore de busca, array ordenado, ou qualquer outra estrutura de dados ou mecanismo de pesquisa.

A partir do TOTVS Application Server, build 7.00.131227A, foram disponibilizadas um grupo de funções para a criação e manutenção de um Hash Map, inclusive a criação de um Hash Map a partir dos dados contidos em um array multi-dimensional em AdvPL.

Vantagens

A vantagem da utilização do Hash Map em relação a outras estruturas é a velocidade de localização de um valor a partir da chave fornecida. Esta diferença de desempenho é mais perceptível quando temos um número maior de entradas ou elementos no Array.

Desvantagens

Porém, a manutenção de um Hash Map (acrescentar ou remover elementos) pode ter um custo maior, inclusive dependendo da estrutura interna da implementação do algoritmo de hash, este custo de manutenção pode aumentar proporcionalmente de acordo com a quantidade de elementos no mapa.

Melhor cenário

Logo, o melhor cenário para uso de um Hash Map parte da utilização em um Array de dados na memória, onde durante o processamento haverá pouca ou nenhuma manutenção neste mapa (acrescentar ou remover elementos), e a quantidade de buscas realizadas neste mapa seja a parte significativa do processamento.

Programa de exemplo

Para ilustrar um caso simples de uso do Hash Map em Advpl, vamos a um programa de exemplo, onde será criado um array com relativamente poucos elementos ( 17 ), onde a primeira coluna será a chave de busca, e vamos realizar 50 mil buscas para cada uma das 17 chaves, mais 50 mil buscas por uma chave não existente.

Na primeira parte do programa, as buscas sobre o Array serão feitas com ASCAN(), e na segunda parte do programa, vamos criar um Objeto de Hash Map a partir do Array, usando a função aToHM(), e depois realizar as mesmas buscas usando o objeto de Hash Map.

Ao todo, cada teste vai realizar 900000 (novecentas mil) buscas, onde serão contabilizados os tempos das 50 mil buscas para cada valor por cada método, e o tempo total de todas as buscas por método utilizado.

Segue o programa Advpl abaixo. Após compilado, ele pode ser executado diretamente a partir do SmartClient, através da função U_TSTHASH

Fonte

#include “protheus.ch”

USER Function TstHash()
Local aDados := {}
Local nI , nJ 
Local nTimer, nTotal
Local cBusca 
Local nPos , xValue
Local oHash
// Cria um array de duas colunas, com 17 elementos
aadd(aDados , { "BC","Branco" } )
aadd(aDados , { "AZ","Azul" } )
aadd(aDados , { "VM","Vermelho" } )
aadd(aDados , { "VD","Verde" } )
aadd(aDados , { "RX","Roxo" } )
aadd(aDados , { "AM","Amarelo" } )
aadd(aDados , { "MA","Marrom" } )
aadd(aDados , { "AM","Azul Marinho" } )
aadd(aDados , { "AC","Azul Céu" } )
aadd(aDados , { "AQ","Amarelo Queimado" } )
aadd(aDados , { "AT","Azul Turquesa" } )
aadd(aDados , { "SA","Salmão" } )
aadd(aDados , { "VO","Verde Oliva" } )
aadd(aDados , { "VI","Violeta" } )
aadd(aDados , { "GR","Cinza" } )
aadd(aDados , { "PB","Chumbo" } )
aadd(aDados , { "PT","Preto" } )
// Faz um teste de desempenho buscando pelos elementos usando ASCAN()
// Faz 50 mil buscas para cada um dos elementos cadastrados
// O ultimo loop busca por um elemento que nao existe
nTotal := seconds()
For nI := 1 to len(aDados)+1
  If nI > len(aDados)
    // Busca por um elemento que nao existe 
    cBusca := "NE"
  Else
    // Busca por um elemento que existe na lista 
    cBusca := aDados[nI][1]
  Endif
  nTimer := seconds()
  For nJ := 1 to 50000
    // Realiza 50 mil buscas 
    nPos := ascan( aDados , {|x| x[1] == cBusca })
  Next
  nTimer := seconds()-nTimer
  conout("Busca por ["+cBusca+"] demorou "+cValToChar(nTimer)+" s.")
Next
nTotal := seconds() - nTotal
conout("Tempo Total (ASCAN) = "+cValToChar(nTotal)+" s.")
conout("")
// Agora usando o HASH
nTotal := seconds()
// Cria o Objeto de HASH a partir do Array
oHash := aToHM(aDados)
For nI := 1 to len(aDados)+1
  If nI > len(aDados)
    // Busca por um elemento que nao existe 
    cBusca := "NE"
  Else
    // Busca por um elemento que existe na lista 
    cBusca := aDados[nI][1]
  Endif
  nTimer := seconds()
  For nJ := 1 to 50000
    // Realiza 50 mil buscas 
    lFound := HMGet( oHash , cBusca , @xValue )
  Next
  nTimer := seconds()-nTimer
  conout("Busca por ["+cBusca+"] demorou "+cValToChar(nTimer)+" s.")
Next
nTotal := seconds() - nTotal
conout("Tempo Total (HASH) = "+cValToChar(nTotal)+" s.")
Return

Resultados obtidos

Para visualizar o resultado do programa, verifique o LOG de console do Application Server. Segue abaixo um exemplo do log gerado:

Busca por [BC] demorou 0.359 s.
Busca por [AZ] demorou 0.406 s.
Busca por [VM] demorou 0.421 s.
Busca por [VD] demorou 0.483 s.
Busca por [RX] demorou 0.53 s.
Busca por [AM] demorou 0.593 s.
Busca por [MA] demorou 0.624 s.
Busca por [AM] demorou 0.577 s.
Busca por [AC] demorou 0.702 s.
Busca por [AQ] demorou 0.78 s.
Busca por [AT] demorou 0.796 s.
Busca por [SA] demorou 0.826 s.
Busca por [VO] demorou 0.874 s.
Busca por [VI] demorou 0.92 s.
Busca por [GR] demorou 0.969 s.
Busca por [PB] demorou 1.014 s.
Busca por [PT] demorou 1.061 s.
Busca por [NE] demorou 1.061 s.
Tempo Total (ASCAN) = 13.075 s.
Busca por [BC] demorou 0.094 s.
Busca por [AZ] demorou 0.094 s.
Busca por [VM] demorou 0.093 s.
Busca por [VD] demorou 0.094 s.
Busca por [RX] demorou 0.093 s.
Busca por [AM] demorou 0.094 s.
Busca por [MA] demorou 0.093 s.
Busca por [AM] demorou 0.094 s.
Busca por [AC] demorou 0.093 s.
Busca por [AQ] demorou 0.094 s.
Busca por [AT] demorou 0.109 s.
Busca por [SA] demorou 0.094 s.
Busca por [VO] demorou 0.093 s.
Busca por [VI] demorou 0.094 s.
Busca por [GR] demorou 0.094 s.
Busca por [PB] demorou 0.109 s.
Busca por [PT] demorou 0.093 s.
Busca por [NE] demorou 0.078 s.
Tempo Total (HASH) = 1.716 s.

Reparem que, ao buscar com ASCAN(), como a busca é feita sequencialmente, quanto mais elementos precisam ser comparados até que seja encontrado o nó do array que satisfaz a condição, maior o tempo de busca. O pior cenário é a busca por um elemento que não existe, pois neste caso o ASCAN() varreu todos os elementos do Array para determinar que o valor não existe. No melhor cenário, 50 mil buscas do primeiro valor do array demoraram 0,359 segundos, e no pior cenário, quando o valor buscado era o último valor do array ou quando o valor não existia no array, 50 mil buscas demoraram 1,061 segundos.

Já com o uso do objeto de hash, criado a partir deste mesmo array, o desempenho das buscas foi aproximadamente 7,6 vezes mais rápido, e cada uma das buscas, não importa se a chave de busca estava no início ou no final do mapa, demoraram em média 1/10 de segundo.

Conclusão

Como mencionado no tópico “Mellhor Cenário”, em um processamento onde usamos um Array em memória com baixa ou nenhuma manutenção, e um volume muito grande de buscas, o Hash Map é uma alternativa excelente em custo x benefício.

Porém, lembre-se também da Lei de Amdahl : “O ganho de desempenho que pode ser obtido melhorando uma determinada parte do sistema é limitado pela fração de tempo que essa parte é utilizada pelo sistema durante a sua operação.” Isto é, se o seu caso de uso cria arrays pequenos e para poucas consultas, tomando um tempo ínfimo de processamento, criar um Hash Map só vai te dar trabalho de refatorar seu código, e você não terá um ganho perceptível, ou ainda, no pior cenário, apresentar uma piora no desempenho. Neste exemplo foi usado um array muito pequeno, logo foram necessárias muitas buscas para mostrar a diferença de desempenho. Cada teste completo fez 900 mil buscas no Array e 900 mil usando Hash Map.

As funções para lidar com Hash Map em AdvPL estão documentadas na TDN, a partir do link http://tdn.totvs.com/pages/viewpage.action?pageId=77300615

Let’s Share

Espero que vocês tenham gostado da linha de raciocínio e conteúdo disponibilizados aqui no Blog. Não se acanhe em comentar se ficou alguma dúvida ou se alguma parte do texto ficou sem pé nem cabeça. Se você gostou, e achou este conteúdo útil, compartilhe , e se este lhe for útil, use-o. No próximo post sobre Hash Map, o fonte de exemplo vai utilizar todas as funções de manutenção, e um índice de busca composto.

“A essência do conhecimento consiste em aplicá-lo, uma vez possuído.” — Confúcio

Até o próximo post, pessoal 😉

Referências

Wikipedia contributors. Hash table. Wikipedia, The Free Encyclopedia. December 11, 2014, 06:10 UTC. Available at: http://en.wikipedia.org/w/index.php?title=Hash_table&oldid=637586906. Accessed December 17, 2014.

Escalabilidade e performance – Filas

Introdução

Nos posts anteriores sobre este assunto, umas das técnicas sugeridas era, quando possível, usar técnicas assíncronas, onde foi abordado superficialmente a utilização de filas. Vamos entrar um pouco mais fundo nesse tema, e exemplificá-lo na medida do possível, inicialmente revisando na íntegra o tópico sobre filas e técnicas assíncronas.

Utilização de técnicas assíncronas

Filas podem ser criadas por bons motivos: seja para manter a consistência de dados (ex.: filas de contenção devido à locks), ou para diminuir o risco do uso indiscriminado de recursos.O uso de técnicas assíncronas, quando permitido pelas regras de negócio, traz algumas oportunidades de otimização no uso de recursos. Com o enfileiramento de mensagens em sistemas de filas (Message Queues) , podemos alocar recursos limitados e suficientes para o tratamento das mensagens. Com isto, poderemos consumir aos poucos os elementos da fila sem aumentar os recursos em momentos de pico.

Por exemplo, disponibilizamos 10 threads para tratamento de um tipo de requisição e não estaremos usando mais recursos caso haja um pico de requisições. O tempo médio do tratamento será maior (devido ao tempo de espera), mas estaremos garantindo um limite no uso dos recursos (Princípio 5), além de permitir por exemplo que operações secundárias decorrentes de um processamento de interface, que normalmente precisa ser o mais rápido possível, possam ser realizados após o processamento principal ter sido completo e seu retorno será mais rápido ao cliente ou solicitante do processo enquanto a etapa secundária será processada na fila quando chegar a sua vez.

O que é uma fila ?

Uma fila é uma estrutura de dados, onde os primeiros elementos inseridos nela serão os primeiros a serem removidos. Basicamente uma fila possui apenas as operações de ENQUEUE ( Acrescentar elemento na Fila ) e DEQUEUE ( Remover elemento da Fila ). Uma fila de caixa em um supermercado é um bom exemplo análogo. Você sempre entra no final da fila, um caixa disponível chama o primeiro da fila para ser atendido, ele sai da fila, e o próximo da fila será atendido assim que for chamado, e com a fila andando, em alguns minutos será sua vez.

Filas para etapas complementares de processos

Em determinados casos, e isto é mais comum do que se imagina, um processamento pode gerar demanda de processamentos secundários. Por exemplo, uma rotina de finalização de pedido por telefone, onde a confirmação do pedido deve disparar um ou mais e-mails para outros departamentos. Colocar a rotina de envio de e-mail no final da gravação do pedido pode segurar o sistema quando o servidor SMTP para envio de e-mails esteja carregado ou mesmo indisponível. O envio de e-mail é uma parte importante do processo, mas não necessariamente eu precise fazê-lo neste instance, segurando um processo de interface.

Neste caso, o mais elegante a fazer é criar uma fila para envio de e-mails, onde um ou mais processos dedicados (e especialistas) é colocado no ar em um ou mais serviços, para garantir o envio dos e-mails da fila, e várias partes do programa responsáveis por enviar e-mail podem ser clientes desta fila, bastando acrescentar a requisição na fila, sem aguardar o envio do e-mail para a rotina estar disponível para uma nova inclusão de pedido.

Fila para limitar consumo de recursos

Pegando carona no exexmplo acima, a fila serve também para não sobrecarregar o servidor de e-mails. Você decide quantos processos dedicados serão colocados no ar para enviar e-mails, e ainda pode especializar estes processos, para que caso um servidor não esteja disponível, o processo tente a conexão com outros servidores de SMTP para envio dos e-mais pendentes.

Mesmo que todos os operadores do sistema disparem ao mesmo tempo uma requisição para enviar e-mail, , todas entam na fila muito rápido, e somente um numero fixo de processos dedicados será responsável para processar a fila, sem sobrecarregar o servidor de e-mail.

Manutenção das filas

Para que a fila seja eficiente e segura, alguns procedimentos adicionais são necessários. Por exemplo, caso não exista nenhum processo removendo itens da fila, a fila pode crescer indiscriminadamente. Um processo em fila deve ser capaz de saber quanto tempo ele ficou esperando, isto é relativamente fácil de implementar, e você pode emitir um alerta ou registrar uma advertência caso os processos estejam ficando mais que um tempo determinado em fila, isto pode indicar um aumento de carga do sistema, e apontar uma necessidade de escalar melhor o recurso de processamento utilizado.

A fila deve ser preferencialmente transacionada, isto é, um item pode ser removido da fila somente se seu processamento foi feito com sucesso. Ou, para ela não necessariamente ser transacionada, pode ser criada uma fila auxiliar de processos em andamento, onde o elemento sai da fila de pendências e entra na fila de processos em andamento, saindo na hora da fila principal, e ficará na fila de processos em andamento até o processo ser concluído com sucesso. Neste caso, a fila de processos em andamento deve ser monitorada de tempos em tempos … se tem um processo que entrou e não saiu, algo não saiu como o planejado.

Com filas, você consegue até mesmo parar o sistema e fazer uma manutenção, e quando o sistema for colocado no ar novamente, o que ainda não foi processado em uma fila pode ser retomado do ponto onde parou.

Conclusão

Uma forma muito prática de lidar com filas é criar uma funcionalidade para encapsular a criação e manutenção das filas, e usá-la no sistema onde a mesma for necessária ou útil. O ponto importante so processamento assíncrono é saber se, onde e por quê algum elemento da fila não foi processado. Se você nao projetar a fila e seus mecanismos para ter algum tipo de rastreabilidade, se algo não sair como o esperado, você vai correr em círculos e não vai saber o que aconteceu.

Até o próximo post, pessoal 😉

Referências

ESTRUTURA DE DADOS. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2014. Disponível em: <http://pt.wikipedia.org/w/index.php?title=Estrutura_de_dados&oldid=40608431>. Acesso em: 14 dez. 2014.

Pense fora da caixa e resolva problemas

Hoje não veremos nenhuma linha de código, separei este post para compartilhar um pouco das experiências diárias do profissional de TI em lidar com problemas, e contar um “causo” (pelo menos pra mim) interessante, onde pensar fora da caixa foi fundamental para chega a uma solução elegante.

Problemas existem, e podem acontecer

Na trajetória do desenvolvimento de Software, volta e meia aparecem problemas de diferentes magnitudes e complexidades. Normalmente existe solução para todos os problemas, porém algumas soluções podem ser mais caras do que conviver com o problema ou contorná-lo. A primeira coisa que aprendemos na informática é como utilizá-la para resolver problemas. A segunda coisa é como resolver o problema quando uma falha na implementação de uma solução informatizada torna-se o problema.

A primeira coisa que devemos entender sobre o problema é a sua gravidade. E, é claro que isto é uma medida totalmente relativa. Um cliente não conseguir emitir uma nota fiscal pode parecer algo não grave, porém se este cliente depende da emissão desta nota para liberar um caminhão com produtos perecíveis, com data e horário certos para entrega sob pena de multa ou rescisão do contrato, não emitir essa nota é gravíssimo.

Depois, precisamos pensar em uma solução para o problema. Se o problema é grave e urgente, precisamos de um contorno para o problema. Dá-se preferência a um contorno elegante, mas se o que você têm para o momento é um arame e um durepoxi, e isso vai contornar o problema, apodere-se do espírito McGuyver que existe em você, e contorne. Mas pelo amor de Deus, não venda a gambiarra que você fez como a solução do problema. Explique que você está botando uma fita isolante no problema para o cliente não ficar parado, e que você vai trabalhar na solução definitiva assim que você entender as causas do problema.

Se você já está na fase de trabalhar no problema, ou teve que ir pra ela direto pois nenhum contorno passa pela sua cabeça, pesquise o que você puder sobre o problema. Procure entender o problema e suas causas, a partir de que momento ele manifestou-se, sob que circunstâncias, quando e onde ocorre, com que frequência, se é generalizado ou localizado, se ocorre em outros pontos do sistema, em um servidor ou em todos, em um tipo de programa ou qualquer programa, quais foram os eventos recentes que antecederam o início do surgimento do problema. É quase uma atividade de detetive. Procure as pessoas certas para levantar estas informações, limpe eventuais “sujeiras” na comunicação, e não tenha medo de perguntar “o que você quer dizer com isso ?”, quando você tiver qualquer dúvida a respeito das informações que permeiam o assunto em foco.

Muitas vezes você encontra um contorno para o problema — e posteriormente a solução — durante a análise dos detalhes do problema, ou tomando banho, ou tomando um café depois do almoço — sim, essas coisas acontecem. Se você já avaliou as possíveis causas e tudo parece certo, procure alguém mais experiente para trocar uma ideia, ou divida o problema com seus colegas de equipe, às vezes um detalhe que você não viu nas informações fornecidas  é visto na hora por outro analista, e dá um novo norte na busca pela causa do problema.

Alguns tipos de problemas são mais difíceis de reproduzir em um ambiente controlado do que Tamanduá-Bandeira em cativeiro. A causa pode estar relacionada a alguma particularidade do ambiente ou da configuração do sistema, ou mesmo até um defeito físico em uma parte da infra-estrutura. Quando as causas prováveis foram analisadas sem sucesso, comece a analisar as improváveis. Olhe tudo com uma lupa, revalide as possibilidades que foram eliminadas durante a análise, mas não desista. Existem pequenos problemas que se escondem por trás de um problema maior, e grandes problemas por trás de pequenos problemas. Não está achando o problema grande, verifique se existem problemas menores, eles podem ser consequência ou parte da causa do problema grande.

O caso do CNAB

No final das contas, entrei tanto dentro do problema que quase fugi do foco principal do post, o pensamento “fora da caixa”. Um cliente do ERP estava com dificuldade de utilizar a integração CNAB (padrão Febraban para remessa e retorno de títulos para compensação bancária). Quando o arquivo de remessa era gerado e enviado ao banco, alguns títulos do arquivo eram rejeitados pelo banco, pois o código de verificação do título que era enviado no arquivo, segundo o banco, não estava com os valores corretos.

O código de verificação de cada título era gerado pelo ERP, na geração do arquivo de remessa, e este código era composto por uma série de operações aritméticas de soma e multiplicação a partir de determinadas informações do título, como valor, vencimento, código do cliente e afins. A conta realizada com os valores daquele título resultava em um número inteiro muito grande, que esbarrava em um limite aritmético da linguagem AdvPL. O cálculo feito pela calculadora do Windows batia com o valor esperado pelo banco, que estava correto, mas o cálculo feito dentro do programa usando a aritmética da linguagem não comporta um número com mais de 16 dígitos significativos, havendo perda de precisão e consequentemente valor incorreto do código.

Isto foi antes do uso do Protheus Server, hoje chamado de TOTVS Application Server, muito anos antes de serem implementadas funções no AdvPL para lidar com números decimais de ponto fixo, que possuem precisão de até 64 dígitos. No final das contas, a fórmula matemática de cálculo escrita em AdvPL da forma originalmente proposta somente seria executada corretamente se a linguagem usasse um tipo de dado numérico com precisão superior ao ponto flutuante.

Depois de sair da minha mesa e tomar um café, levei meu caderno de rabiscos para passear, e já estava pensando em fazer um programa em outra linguagem e chamar ele de dentro do ERP para fazer aquela conta, quando eu olhei para os números e percebi o óbvio: Eu consigo fazer essa conta em um pedaço de papel, usando um lápis e as mesmas regrinhas básicas de soma e multiplicação que aprendemos no colégio primário. Se dá pra fazer no papel, dá pra fazer no computador, exatamente da mesma forma.

Eu só precisava de soma e multiplicação, e não importa se a operação ficasse um pouco mais lenta do que um cálculo em ponto flutuante nativo da linguagem, afinal seriam apenas algumas chamadas para cada título do borderô. Então, em menos de uma hora eu fiz duas funções para somar e multiplicar números recebidos como String. Bastou re-escrever as fórmulas para o CNAB daquele banco para pegar os dados do título e passar para as novas funções, trabalhando com os números como String.

Conclusão

Análise, programação e desenvolvimento de sistemas é uma atividade que requer jogo de cintura e criatividade para lidar com o universo de desafios de resolução de problemas, é saber usar o conhecimento e as ferramentas que se têm na mão, ser capaz de apontar mais de uma alternativa para resolver um problema, e escolher a que melhor atende a necessidade.

Pensar fora da caixa é uma expressão onde a caixa normalmente significa o limite do seu pensamento criativo, ou os paradigmas assimilados e embutidos nos problemas que você normalmente lida no dia a dia. A busca por novas soluções é constante, e avaliar um problema por novos ângulos e abordagens pode fazer toda a diferença. Não desista, a resposta está lá fora, esperando que você a encontre !!!

Até o próximo post, pessoal 😉

Informações Adicionais

As funções desenvolvidas para cálculo numérico de soma e multiplicação com strings estão disponíveis no ERP Microsiga, e documentadas na TDN nos links http://tdn.totvs.com/display/public/mp/FUNCAO+GENERICA+-+SOMASTR
e http://tdn.totvs.com/pages/releaseview.action?pageId=6814818 , e foram mantidas por compatibilidade. As funções implementadas na linguagem AdvPL para cálculo com números que exigem precisão maior do que a suportada por variáveis numéricas com ponto flutuante lidam com o tipo numérico “F” do AdvPl (Fixed size decimals), e estão também documentadas na TDN, no link http://tdn.totvs.com/display/tec/Decimais+de+Ponto+Fixo

CodeBlocks em Advpl – Parte 01

Introdução

Este tópico vai abordar exclusivamente de um recurso da Linguagem ADVPL, chamado CodeBlock, ou bloco de código. Trata-se de um recurso muito poderoso, com características interessantes e peculiares, muito utilizada em componentes de interface e algumas funções básicas da linguagem.

O que é

Um ClodeBlock nada mais é do que uma função “inline”, onde podem ser declarados parâmetros de entrada, e dentro dele podemos colocar uma ou mais expressões Advpl separadas por vírgulas, como por exemplo atribuições de valores em variáveis e chamadas de funções ou métodos.

A declaração de um CodeBlock obedece à seguinte sintaxe:

{ | [parm1][,parm2...N] | [expr1] [,expr2...N] }

Os parâmetros são declarados entre um par de pipes “||”. Caso o CodeBlock não receba parâmetros, usamos um par de pipes sem nada entre eles. As expressões ADVPL colocadas após o par de pipes devem ser separadas por vírgulas, e serão executadas da esquerda para a direita.

A última expressão do bloco de código será o seu retorno. Dentro das expressões não é possível usar estruturas de programação, como WHILE , CASE e afins. É permitido usar as funções IF() e/ou IIF(), que veremos posteriormente.

Chamada

Para executar o CodeBlock, você deve usar a função EVAL(), onde informamos no primeiro parâmetro o CodeBLock ou a variável ou propriedade onde o mesmo está contido, e a partir do segundo parâmetro podemos informar os parâmetros que serão repassados para o CodeBlock. A função Eval() retorna o resultado da última expressão do CodeBlock executado.

Exemplo 01

CodeBlock sem parâmetros, chamando uma função de interface para mostrar uma caixa de diálogo com uma mensagem fixa

User Function Ola01()
Local bCB := {|| MsgInfo("Olá CodeBlock") }
Eval(bCB)
Return

Exemplo 02

CodeBlock recebendo um parâmetro, chamando uma função de interface para mostrar uma caixa de diálogo com o texto informado na chamada.

User Function Ola02()
Local bCB := {| cMsg | MsgInfo(cMsg) }
Eval(bCB,"Olá CodeBlock")
Eval(bCB,"Adeus CodeBlock")
Return

Exemplo 03

CodeBlock para retornar o menor argumento, a partir de dois argumentos fornecidos na chamada, mostrando o resultado em uma caixa de diálogo no SmartClient

User Function Ola03()
Local bCB := {| x1,x2 | IIF( X1 < x2 , x1 , x2 ) }
Local nRet
Local cRet
nRet := Eval(bCB,10,5)
MsgInfo( cValToChar(nRet),"Número")
cRet := Eval(bCB,"ABC","ACD")
MsgInfo( cRet,"String")
Return

Flexibilidade

Quando você cria um bloco de código para seu uso, você define se e quantos parâmetros ele deve receber, e se ou qual será seu retorno. Os parâmetros podem ser nomeados usando a regra de nomenclatura de variáveis ADVPL, e o escopo dos parâmetros é restrito ao corpo do CodeBlock. Atribuições em linha são permitidas no CodeBlock, inclusive podemos passar parâmetros por referência na chamada do CodeBlock, e obter assim mais de um retorno.

Exemplo 04

Passagem de parâmetros por referência para múltiplos retornos.

User Function Ola04()
Local bCB := {| x,y,z | y := x/2 , z := x*2 , x%2 == 0 }
Local nTeste := 4
Local lPar
Local nY , nZ
// O bloco de codigo recebe em x o valor de nTeste
// e recebe em y e z a referência das variáveis 
// nY e nZ respectivamente
lPar := Eval( bCB , nTeste , @nY , @nZ )
MsgInfo(lPar , "O numero é par ? ")
MsgInfo(nY , "Numero / 2 ")
MsgInfo(nZ , "Numero * 2 ")
Return

Outros usos

Quando uma função recebe ou requer como parâmetro um CodeBlock, a documentação da função informa se e quantos parâmetros serão informados na chamada do CodeBlock, e se algum retorno específico é esperado. Por exemplo, quando usamos a função aEval(), onde informamos um array e um CodeBlock como parâmetros, onde a função aEval() vai se encarregar de chamar o CodeBlock para cada elemento do array informado, passando cada elemento do array como parâmetro em cada chamada.

Exemplo 05

Cálculo da média de valores em um array de números

User Function Ola05()
Local aValores := {}
Local nSoma := 0
Local bSoma := {| nValor , nPos | nSoma += nValor }
aadd(aValores,3)
aadd(aValores,4)
aadd(aValores,3)
aadd(aValores,10)
aEval(aValores,bSoma)
MsgInfo(nSoma,"Valores Somados")
Return

Repare que no exemplo acima foi feito algo diferente: A variável local nSoma foi declarada e inicializada dentro da função Ola05(), mas não foi passada como parâmetro para o CodeBlock, ela simplesmente foi usada na expressão dentro do CodeBlock para acumular a soma do elemento nValor recebido como parâmetro no CodeBlock. Logo, um code-block declarado dentro de uma função pode acessar diretamente as variáveis locais disponíveis nesta função.

Esta característica permite utilizar o CodeBlock em modelos mais sofisticados, porém isto pode ter consequências no consumo de memória da aplicação, dependendo de como e onde o bloco é criado e utilizado. Este assunto exige uma profundidade maior de abordagem, que será realizada em um tópico posterior e exclusivo para esta fim.

Outra função que usa code-block de forma interessante é a aSort(), onde informamos um array Advpl para ser ordenado, e podemos montar um CodeBlock para definir a regra de ordenação. Este CodeBlock recebe sempre dois parâmetros, correspondendo a dois elementos quaisquer do Array a ser ordenado, e o retorno desse CodeBlock deve ser um valor booleano. Caso o retorno seja .T., isto significa que o primeiro elemento deve preceder o segundo elemento na ordenação. Caso contrário, o retorno .F. indica que o segundo elemento informado deve anteceder o primeiro na ordenação.

Pré-Conclusão

Um post com 5 exemplos é a pontinha do iceberg … O recurso do CodeBlock é equivalente ao uso de funções anônimas (também conhecidas por funções lambda), disponíveis em outras linguagens de mercado de formas distintas, desde 1958 até os dias de hoje. Vamos abordá-lo em maior profundidade nos próximos post.

Até o próximo post, pessoal 🙂