Algoritmos – Conversão Binária – Parte 03

Introdução

No post anterior sobre conversão binária, vimos algumas formas de converter um valor binário representado em uma string de volta a um valor decimal. O algoritmo mais rápido demorou 9,2 segundos para converter 2.560.000 valores. E se eu disser que dá para fazer isso QUATRO VEZES mais rápido, eu devo estar louco ???

Acelerando o AdvPL

No primeiro post sobre conversão binária, o algoritmo têm um desempenho incrível para converter valores de decimal para string binária, pois eu monto um cache com um array armazenando as strings  com o resultado das conversões dos valores de 0 a 255, e ao converter eu acesso diretamente o elemento pela sua posição, o tempo de busca é praticamente o mesmo para qualquer elemento.

Agora, será que é possível aplicar isso da forma inversa ? Usar um array em cache, com as strings binárias e o seu valor decimal correspondente, e buscar o valor desejado a partir da string informada ? Considerando uma string de 8 caracteres ‘0’ ou ‘1’ , com 256 possibilidades diferentes, é mais rápido calcular o número em decimal ou procurar na lista ? Veremos …

Peso do algoritmo de cálculo

Partindo da conversão de uma string binária de 8 caracteres, usando o algoritmo proposto no post anterior, podemos elencar o melhor e pior tempo da rotina, partindo das operações que ela precisa fazer para chegar ao resultado final. Vamos avaliar quantas operações de string, comparação e aritmética são necessárias para converter as strings “00000000” e “11111111” respectivamente:

User Function TST8()
Local nRet 
nRet := BitStoN("00000000")
nRet := BitStoN("11111111")
return

STATIC Function BitsToN(cBitStr)
Local nI, nT := len(cBitStr) 
Local nMult := 1
Local nResult := 0
For nI := nT to 1 STEP -1
    // a linha abaixo é executada 8 vezes, uma para 
    // identificar cada caractere da string binária
    // e compará-lo com '1'
    IF substr(cBitStr,nI,1) = '1'
        // A soma do multiplicador somente é executada para
        // cada caractere '1' encontrado na string. Ela 
        // nao é executada nenhuma vez para "00000000", mas é
        // feita 8 vezes para converter "11111111"
        nResult += nMult
    Endif
    // A Multiplicação abaixo é feita sempre 8 vezes
    nMult *= 2
Next
Return nResult

Passando na ponta do lápis, a busca mais “leve” de uma string binária fará 8 extrações de uma posição da string (substr), 8 comparações de string de 1 caractere e 8 multiplicações de nMult. Para cada dígito encontrado que tenha o  valor ‘1’ ainda será feita uma soma adicionando o valor de nMult no resultado. Logo, com “11111111”, temos mais oito operações aritméticas. Podemos concluir que, o número estimado de operações dessa rotina é de 24 a 32 operações de string e numéricas por conversão.

HASH MAP

Quem se lembra do artigo sobre Performance e Escalabilidade – HASH MAP em AdvPL ? Então, um Hash Map é uma forma otimizada de busca em vetores do tipo chave-valor, que monta internamente um acesso posicional. Se, internamente, a quantidade e peso das operações para fazer a busca usando um HASHMAP em uma lista pré-montada é menor do que calcular o resultado, o HASH vai ser mais rápido. Vamos ao teste:

User Function tststr()
Local cStrBit
Local nTimer
Local nNum, nI , nJ      
Local oHash

// Cria o cache de HASH para a busca ( 0 a 255 )            
oHash := BuildBitHash()

// Roda a carga de teste
nTimer := seconds()
For nJ := 0 to 255              
  // Cria uma string binária para conversão
  cStrBit := nToBit8(nJ)
  For nI := 1 to 10000 
    // Busca o valor no HashMap
    HMGET( oHash,cStrBit ,@nNum)
  Next
Next
// Mostra o tempo do teste
conout(str(seconds()-nTimer,12,3)+"s.")

// Limpa de destroi o HashMap
HMClean(oHash) 
FreeObj(oHash)

Return

// Monta o HashMap com os 256 bytes de cache
STATIC Function BuildBitHash()
Local oHash := HMNew()
For nI := 0 to 255
  // Seta uma tupla chave , valor 
  HMSet(oHash, NTOBIT8(nI) , nI )
Next
Return oHash

Desempenho

Usando a função BitSToN(), 2.560.000 conversões levaram em média de 8,5 a 9 segundos para serem realizadas. Usando o objeto de Hash Map, o mesmo processamento levou em média 2 segundos. Isso significa quase 1280000 conversões por segundo, contra no máximo 320.000 conversões por segundo da BitSToN()

Observações

O tempo obtido foi realmente impressionante, porém para chegar neste desempenho, dada a quantidade de iterações, o objeto de cache foi montado e armazenado em variável local, e as chamadas para a busca do HashMap foram feitas com a função HMGet(), sem encapsulamento em Static Function. Após implementar o encapsulamento do HMGET() por Static Function, o tempo foi para perto de 4 segundos, o que ainda é a metade do tempo do algoritmo anterior. 😀

Conclusão

Sempre dá pra “apertar” mais um pouco o desempenho, usando as técnicas e abordagem adequadas.

Agradeço a todos a audiência, e lhes desejo TERABYTES DE SUCESSO !!! 

Referências

TDN – HashMap

 

 

ZLIB Framework – Parte 01

Introdução

Vamos ver um pouco sobre Bibliotecas de Funções e Framework, com destaque para as funcionalidades em implementação no projeto ZLIB.

Bibliotecas e Frameworks

Com as funções básicas da linguagem, conseguimos criar qualquer programa. Alguns programas podem dar mais trabalho que outros, tudo depende de quantas funcionalidades serão implementadas. Porém, quando você precisa implementar muitas funcionalidades parecidas, é mais eficiente isolar o código comum em classes ou funções parametrizáveis, para não ter que escrever tudo de novo ou copiar-e-colar, replicando código desnecessariamente. Neste ponto, começa o nascimento de uma Biblioteca de funções.

Na ciência da computaçãobiblioteca é uma coleção de subprogramas utilizados no desenvolvimento de software. Bibliotecas contém código e dados auxiliares, que provém serviços a programas independentes, o que permite o compartilhamento e a alteração de código e dados de forma modular. Alguns executáveis são tanto programas independentes quanto bibliotecas, mas a maioria das bibliotecas não são executáveis.

Quando falamos em Framework, não apenas estamos usando funções genéricas de uma biblioteca, mas sim uma abstração de nível mais alto, que impõe um fluxo de controle na aplicação.

Um framework em desenvolvimento de software, é uma abstração que une códigos comuns entre vários projetos de software provendo uma funcionalidade genérica. Um framework pode atingir uma funcionalidade específica, por configuração, durante a programação de uma aplicação. Ao contrário das bibliotecas, é o framework quem dita o fluxo de controle da aplicação, chamado de Inversão de Controle.[1]

Projeto ZLIB

A ideia — necessidade — de uma LIB (Biblioteca) de componentes surgiu com os posts da série do CRUD em AdvPL, que acabou virando uma Agenda de Contatos, feita originalmente atrelada a interface do SmartClient, e depois implementada em uma interface WEB/HTTP.

Muito daquele código é comum a aplicações de mesma funcionalidade — cadastro simples. Inclusão, Alteração, Exclusão, Consulta ordenada, consulta por filtro. Outras funcionalidades, como exibição e cadastro de imagem, envio de email e mapa do endereço não necessariamente são usadas em todos os cadastros, mas podem ser colocadas em componentes de uma biblioteca para reaproveitamento.

A ideia da ZLIB é ser uma Biblioteca de Funções, que vai servir de base para construir um Framework. Ela já está versionada no GITHUB, mas ainda em desenvolvimento e com pouca (nenhuma) documentação, e como os componentes ainda estão nascendo, muitas alterações drásticas estão sendo feitas a cada atualização.

Orientação a Objetos e Abstração

Estas são duas chaves importantes no reaproveitamento de código e desenvolvimento modular. A orientação a objetos nos permite criar classes com uma finalidade (abstração) e implementar para múltiplos cenários ou recursos.

Por exemplo, as classes implementadas para acesso a arquivos DBF e arquivos em memória. Ambas possuem a mesma declaração de métodos para implementar as suas funcionalidades. Logo, o mesmo programa que insere um registro em uma tabela da classe ZDBFFILE pode realizar a mesma operação usando um objeto da ZMEMFILE.

Uma classe de geração de LOG de operação ou execução não precisa saber onde o log será gravado, ou mesmo conhecer a interface de gravação. Ela pode receber como parâmetro um objeto de uma classe de gravação de LOG. Ele pode ser de uma classe que grave os registros emitidos de log em um arquivo TXT, ou em um banco de dados, ou ainda seja um encapsulamento de uma interface “client” de log, que envia os dados gerados para serem gravados remotamente por um Log Server.

Criação de Componentes

Um dos primeiros mandamentos da criação de componentes é : A CRIAÇÃO DE QUALQUER COMPONENTE DEVE SER MOTIVADA PELA NECESSIDADE. Criar componentes adicionais ou agregar funcionalidades demais a um componente só por que vai ser “legal” só engorda código. Limite-se a uma funcionalidade por classe, e coloque nela o que realmente é comum a todos. Exceções são tratadas na implementação, a abstração é genérica.

Quando aos níveis de implementação — ou camadas — normalmente os componentes de alto nível são construídos para usar os de mais baixo nível. Na prática eles são construídos para usar todas as implementações feitas sobre uma abstração. Por exemplo, um componente de CRUD feito para usar a abstração ZISAMFILE pode usar qualquer implementação feita sobre ela, como a ZDBFFILE, ZTOPFILE, ZMEMFILE…

Como a implementação está por baixo da abstração, eu posso por exemplo criar uma abstração de exportação de arquivo, e implementar uma exportação para cada formato, a mesma coisa para importação.

Objetivo Final

Criar um conjunto de funções e funcionalidades que, permitam escrever programas, funções e rotinas, separando totalmente o processamento da interface, focando em SOA utilizando micro-serviços, filas e controladores, com foco em desempenho, escalabilidade, resiliência e alta disponibilidade.

Conclusão

Por hora, a primeira missão das funções em desenvolvimento é permitir a reescrita do programa de Agenda para SmartClient, usando componentes destacados, que permitam um elevado índice de reaproveitamento de código, e uma forma de declarar e executar as validações e procedimentos de cada operação que torne a codificação mais fácil e rápida, usando uma abordagem que permita aproveitar o CORE de cada componente em integrações encapsuladas por APIs (RPC Advpl, REST, SOAP) para serem consumidas por interfaces criadas em AdvPL ou qualquer outra linguagem ou plataforma.

Referências

MemCached Client em AdvPL – Parte 01

Introdução

O MemCached é um aplicativo que provê um cache de objetos em memória, do tipo chave/valor de alto desempenho. Ele possui APIs Client para várias linguagens de mercado, e agora também terá a sua API Client em AdvPL.

O MemCached

Open Source, Free , originalmente desenvolvido para Linux, ele também têm porte para Windows, sobe originalmente com um limite de 64 MB de memória para uso com elementos em cache, aceita conexões na porta 11211, em todas as interfaces de rede da máquina. Existe parametrização para permitir mudar a porta e colocar mais de uma instância de MemCached na mesma máquina, definir uso de mais memória, e inclusive permitir conexões apenas em uma interface de rede ou IP. Recomendo a leitura da documentação — pelo menos a abordagem inicial — disponível no site MemCached.ORG

Classe ZMEMCACHED

Como o mecanismo do MemCached trabalha com TCP/IP, a classe implementa a API de acesso para as funcionalidades do MemCached. Damos o nome de Chave a um identificador de conteúdo, uma string com até 150 bytes, e associamos a esta chave um conteúdo a ser colocado em cache. Este conteúdo pode ser colocado no cache com ou sem um tempo de vida (expires). As funcionalidades básicas incluem acrescentar, atualizar e remover uma tupla chave/valor do cache, incrementar ou decrementar um valor em cache — neste caso o conteúdo em cache deve ser um número — representado como string, obter status do cache, e limpar todo o cache. Vamos ao fonte: (fonte zMemCached,prw)

#include "protheus.ch"
#include "zLibStr2HexDmp.ch"

CLASS ZMEMCACHED FROM LONGNAMECLASS
 
   DATA cMemCacheIP     // IP da instancia Memcached
   DATA nMemCachePort   // Porta da instancia Memcached
   DATA nRvcTimeOut     // Timeout de recebimento em milissegundos ( default 1000 ) 
   DATA oTCPConn        // Objeto Socket Client
   DATA cError	        // Ultimo erro da API 
   DATA cResponse       // Response header da ultima requisicao
   DATA lVerbose        // Modo verbose de operação 
   
   METHOD New()         // Construtor da classe
   METHOD Connect()     // Estabelece conexão com o MemCached
   METHOD GetVersion()  // Recupera a versao do MemCached
   METHOD GetStats()    // Recupera estatisticas da intancia do memcached
   METHOD Disconnect()  // Desconecta do MemCAched

   METHOD Add()         // Acrescenta uma chave / valor ( apenas caso nao exista ) 
   METHOD Replace()     // Troca o valor de uma chave existente
   METHOD Set()         // Armazena uma chave / valor no MemCached 

   METHOD Get()         // Recupera o valor de uma chave armazeanda 
   METHOD Delete()      // Remove do cache um valor pela chave 
   METHOD Increment()   // Incrementa um contador pela chave -- valor em string numerica
   METHOD Decrement()   // Decrementa um contador pela chave -- valor em string numerica
   METHOD Flush()       // Limpa todas as variáveis do cache 

   // ********* METODOS DE USO INTERNO *********
   METHOD _Store( cMode, cKey , cValue, nOptFlag, nOptExpires )
   METHOD _GetTCPError()
  
ENDCLASS

 

Método NEW

Method NEW( cIp , nPorta ) CLASS ZMEMCACHED
::cMemCacheIP := cIp
::nMemCachePort := nPorta
::nRvcTimeOut := 1000
::oTCPConn := tSocketClient():New()
::cError := ''
::cResponse := ''
::lVerbose := .F.
Return self

O construtor da classe Client do MemCached recebe logo de cara o IP e Porta da instância do MemCached que ele deve utilizar. Ele já cria um objeto do tipo tSocketClient() para conversar com o MemCached, mas ainda não estabelele a conexão, apenas inicializa as propriedades de uso da classe.

Método CONNECT

METHOD Connect() CLASS ZMEMCACHED
Local iStat

::cError := ''
::cResponse := ''

IF ::lVerbose
	Conout("zMemCached:Connect() to "+::cMemCacheIP+" Port "+cValToChar(::nMemCachePort))
Endif

If ::oTCPConn:Isconnected()
	::cError := "Memcached client already connected."
	Return .F.
Endif

// Estabelece a conexao com o memcache DB
iStat := ::oTCPConn:Connect( ::nMemCachePort , ::cMemCacheIP, 100 )

If iStat < 0
	::cError := "Memcached connection Error ("+cValToChar(iStat)+")"
	::_GetTCPError()
	Return .F.
Endif

O método Connect apenas estabelece a conexão TCP no IP e Porta especificados no construtor. Nesta etapa, ele não faz nenhum tipo de HandShake — por hora — ele apenas abre a conexão TCP/IP. Em caso de erro, o método retorna .F., e a razão do erro pode ser recuperada na propriedade ::cError.

Método DISCONNECT

METHOD Disconnect() CLASS ZMEMCACHED
Local cSendCmd := 'quit'+CRLF
Local nSend

::cError := ''
::cResponse := ''

If ::oTCPConn == NIL
	::cError := "Memcached client already Done."
	Return .F.
Endif

if( ::oTCPConn:IsConnected() )
	// Se ainda está conectado, manda um "quit"
	// para fechar a conexao de modo elegante
	IF ::lVerbose
		Conout("zMemCached:DONE() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
		Conout(Str2HexDmp(cSendCmd))
	Endif
	nSend := ::oTCPConn:Send( cSendCmd )

	If nSend <= 0 
		::cError := "Memcached client SEND Error."
		::_GetTCPError()
		Return .F.
	Endif
	
Endif

::oTCPConn:CloseConnection()
::oTCPConn := NIL

Return .T.

O método Disconnect() desconecta do MemCached de forma “elegante”, enviando um aviso de desconexão (instrução quit). Simples assim. Após desconectar, a mesma instância pode ser aproveitada para uma nova conexão — por hora no mesmo IP e Porta — na mesma instância do MemCached.

Método GETVERSION

METHOD GetVersion( cVersion ) CLASS ZMEMCACHED
Local nRecv, cRecvBuff := ''
Local cSendCmd := "version" + CRLF
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:GetVersion() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Receive Error"
	::_GetTCPError()
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:GetVersion() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

If Left(cRecvBuff,8)!='VERSION '
	::cError := "Response Error : " + cRecvBuff
	Return .F.
Endif

// Recupera a versão por referencia 
cVersion := ::cResponse

Return .T.

O método GetVersion() deve passar a variável cVersion por referência — prefixada com “@” na chamada da função. Em caso de sucesso, a função retorna .T., e o valor da variável será atualizado. Caso contrário, a variável será NIL, e a mensagem de erro correspondente está na propriedade ::cError

Método GETSTATS

METHOD GetStats( aStats ) CLASS ZMEMCACHED
Local nRecv, cRecvBuff := ''
Local nI , nT , aTmp
Local cSendCmd := "stats" + CRLF
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:GetStats() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Receive stats error"
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Receive stats time-out
	::_GetTCPError()
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:GetStats() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

// Recupera estatisticas
aTmp := strtokarr2( strtran(cRecvBuff,CRLF,chr(10)) , chr(10) )

nT := Len(aTmp)
For nI := 1 to nT
	If Left(aTmp[nI],5)=='STAT '
		aadd(aStats , substr(aTmp[nI],6) )
	Endif
Next

// Limpa o array temporario
aSize(aTmp,0)

Return .T.

Cada instância do MemCached têm seus mecanismos internos de controle. Usando o método GetStats(), podemos perguntar ao MemCached as estatísticas de uso até o momento. As informações são retornadas por referência no Array aStats passado como parâmetro, onde cada linha é uma string contento um identificador e seu respectivo valor, veja o exemplo abaixo:

pid 11128
uptime 10
time 1547326165
version 1.4.5_4_gaa7839e
pointer_size 64
curr_connections 10
total_connections 11
connection_structures 11
cmd_get 0
cmd_set 0
cmd_flush 0
get_hits 0
get_misses 0
delete_misses 0
delete_hits 0
incr_misses 0
incr_hits 0
decr_misses 0
decr_hits 0
cas_misses 0
cas_hits 0
cas_badval 0
auth_cmds 0
auth_errors 0
bytes_read 16
bytes_written 26
limit_maxbytes 67108864
accepting_conns 1
listen_disabled_num 0
threads 4
conn_yields 0
bytes 0
curr_items 0
total_items 0
evictions 0
reclaimed 0

Método _STORE

// ===============================================================================
// Guarda um valor no memcache
// Mode = set, add, replace, append, prepend
// cas ainda nao implementado 

METHOD _Store( cMode, cKey , cValue, nOptFlag, nOptExpires ) CLASS ZMEMCACHED
Local cSendCmd := ''
Local nRecv
Local cRecvBuff := ''
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

If !( ('.'+cMode+'.') $ ('.set.add.replace.append.prepend.cas.') )
	::cError := "Invalid Store mode ["+cMode+"]"
	Return .F.
Endif

// <mode> <key> <flags> <exptime> <bytes>
// ------------------------------------------
cSendCmd += cMode + ' '
cSendCmd += cKey + ' '
If nOptFlag == NIL
	cSendCmd += '0 '
else
	cSendCmd += cValToChar(nOptFlag)+' '
Endif
If nOptExpires == NIL
	cSendCmd += '0 '
else
	cSendCmd += cValToChar(nOptExpires)+' '
Endif
cSendCmd += cValToChar(len(cValue))
cSendCmd += CRLF
// ------------------------------------------

IF ::lVerbose
	Conout("zMemCached:_Store() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Etapa 01 Envia o comando 
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif


// Etapa 02
// Envia o valor a ser armazenado 

nSend := ::oTCPConn:Send(cValue+CRLF)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Store("+cMode+") SEND VALUE ")
	Conout(Str2HexDmp(cValue+CRLF))
Endif

// Se tudo der certo, aqui eu devo receber um "stored"
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Store("+cMode+") failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Store("+cMode+") failed - response time-out"
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Store("+cMode+") RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If cRecvBuff != 'STORED'
	::cError := "Store ["+cMode+"] failed: "+cRecvBuff
	Return .F.
Endif

Return .T.

O método _STORE é de uso interno da classe. Ele é usado pelos métodos públicos ADD, REPLACE e SET. Internamente, a sintaxe dos comandos e o retorno é praticamente o mesmo para estas três ações de armazenamento. Logo, optei por criar um método interno capaz de realizar as três operações, e os três métodos públicos para consumir estas ações no fonte AdvPL.

Métodos ADD, REPLACE e SET

METHOD Add( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED
Return ::_Store("add", cKey , cValue, NIL, nOptExpires)

METHOD Replace( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED
Return ::_Store("replace", cKey , cValue, NIL, nOptExpires)

METHOD Set( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED
Return ::_Store("set", cKey , cValue, NIL, nOptExpires)

Em cada um destes três métodos, informamos a chave de identificação do dado, o valor a ser gravado em cache, e opcionalmente podemos especificar um tempo de vida (expires) em segundos no cache. O default é 0 (zero=no expires). Todos os métodos acima retornam .F. quando a operação não pode ser realizada.

A diferença entre eles é que:

  1. ADD() somente var armazenar o valor caso a chave ainda não tenha sido gravada anteriormente. Ela não atualiza valor de chave existente.
  2. Replace() somente troca o valor de uma chave existente. Caso você tente trocar o valor de uma chave que não existe, ela retorna uma condição de erro.
  3. O método SET sempre atualiza o valor de uma chave, se ela ainda não existe no cache, ela é criada.

Método GET

METHOD Get( cKey , cValue ) CLASS ZMEMCACHED

Local cSendCmd := ''
Local nRecv
Local cRecvBuff := ''
Local nPos
Local cLine
Local aTmp
Local cTeco
Local nSize
Local nSend

::cError := ''
::cResponse := ''

// Limpa o retorno por referencia 
cValue := NIL

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	return -1
Endif

// Monta o comando de recuperacao 
cSendCmd += 'get '+cKey + CRLF

If ::lVerbose
	Conout("zMemCached:Get() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

// Se tudo der certo, aqui eu devo receber os dados ...
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

If nRecv < 0
	::cError := "Get() failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	return -1
Endif

If nRecv == 0
	::cError := "Get() failed - response time-out"
	::_GetTCPError()
	return -1
Endif

If ::lVerbose
	Conout("zMemCached:Get() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

// Parser do retorno

While !empty(cRecvBuff)
	
	// Primeiro pega a linha de status
	nPos := at(CRLF,cRecvBuff)
	If nPos < 1
		::cError := "Get() failed - missing CRLF"
		return -1
	Endif
	
	cLine := left(cRecvBuff,nPos-1)
	cRecvBuff := substr(cRecvBuff,nPos+2)
	
	If cLine == "END"
		// acabaram os dados
		// Sai do loop
		EXIT
	Endif
	
	If Left(cLine,6) == "VALUE "
		
		// Tem valor ... opa ... legal
		aTmp := strtokarr2(cLine,' ')
		
		// varinfo("aTmp",aTmp)
		// [1] "VALUE"
		// [2] <key>
		// [3] <flags>
		// [4] <size> 
		// [5] Optional [uniqueid]
		
		nSize := val(aTmp[4])
		
		While len(cRecvBuff) < nSize
			
			// Se ainda falta coisa pra receber, recebe mais um teco
			// e acrescenta no buffer
			cTeco := ''
			nRecv := ::oTCPConn:Receive(@cTeco,::nRvcTimeOut)
			
			If nRecv < 0
				::cError := "Get() failed - connection error" + cValTochar(nRecv)
				::_GetTCPError()
				return -1
			Endif
			
			If nRecv == 0
				::cError := "Get() failed - response time-out"
				::_GetTCPError()
				return -1
			Endif
			
			If ::lVerbose
				Conout("zMemCached:Get() RECV "+cValToChar(nRecv)+" Byte(s)")
				Conout(Str2HexDmp(cTeco))
			Endif
			
			// So acrescenta o que recebeu
			cRecvBuff += substr(cTeco,1,nRecv)
			
			If ::lVerbose
				Conout("zMemCached:Get() Total ReceivedBuffer ")
				Conout(Str2HexDmp(cRecvBuff))
			Endif
			
		Enddo
		
		// Valor ja foi recebido na integra
		// Coloca o valor recebido no retorno
		cValue := left(cRecvBuff,nSize) 
		
		// Arranca valor recebido do buffer
		// Ja desconsiderando o CRLF
		cRecvBuff := substr(cRecvBuff,nSize+3)
		
		// Limpa o array temporário
		aSize(aTmp,0)

		// Eu só espero recener um valor 		
		EXIT 
		
	Else
		
		// Se nao tem o valor, ou nao tem o "END", deu merda ?!
		::cError := "Get() failed - Unexpected ["+cLine+"]"
		return .F. 
		
	Endif
	
Enddo

If empty(cRecvBuff)
	// Se o buffer esta vazio, 	entao nao chegou nenhum valor 
	// A operação de GET foi feita com sucesso, 
	// naou houve erro, apenas o valor nao foi encontrado. 
	Return .T. 
Endif

If left(cRecvBuff,5) == "END" + CHR(13)+Chr(10)
	// Depois do valor, eu espero um END (CRLF) \
	// Se nao chegou um END, tem mais de um valor na chave ? ....
	Return .T. 
Endif

::cError := "Get() failed - Unexpected Multiple Value(s)"
return .F.

O método GET foi feito para recuperar o valor em cache associado a uma chave. A variável para receber o valor é informado por referência na chamada do método. O fonte é um pouco mais “rebuscado” pois precisa permanecer recebendo dados do MemCache enquanto ele não enviar o conteúdo inteiro armazenado.

Um detalhe importante: A função somente retorna .F. em caso de ERRO, por exemplo perda de conexão ou resposta inesperada ou não tratada do MemCached. Se o valor a ser recuperado na chave não existe no cache, isto não é considerado um erro, logo o método vai retornar .T. , e cabe ao desenvolvedor verificar se o dado retornado por referência não está NIL.

Método DELETE

Usamos o método DELETE para remover uma chave e seu valor associado do cache.

METHOD Delete( cKey ) CLASS ZMEMCACHED
Local cSendCmd 
Local nRecv
Local cRecvBuff := ''
Local nSend

::cError    := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

cSendCmd := 'delete ' + cKey + CRLF

// ------------------------------------------

If ::lVerbose
	Conout("zMemCached:Delete() SEND")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Manda o comando 
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

// Se tudo der certo, aqui eu devo receber DELETED 
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

// Pega apenas a primeira linha do resultado 
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Delete() failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Delete() failed - response time-out"
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Delete() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If cRecvBuff != 'DELETED'
	::cError := "Delete failed - Error: "+cRecvBuff
	Return .F.
Endif

Return .T.

Métodos INCREMENT e DECREMENT

Podemos armazenar no cache — usando Add ou Set — um vamor numérico representado em string em uma determinada chave. E, usando os métodos Increment() e Decrement(), podemos respectivamente aumentar ou diminuir o valor desta chave. Internamente o MemCached não vai deixar duas operações de incremento rodar ao mesmo tempo. Cada operação realizada retorna o novo valor da chave após a operação ser realizada. O valor é recuperado por reverência na chamada do método, no parâmetro nValue.

Method Increment( cKey , nValue , nStep ) CLASS ZMEMCACHED

Local cSendCmd := ''
Local nRecv
Local cRecvBuff := ''     
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

// Monta o comando de recuperacao
cSendCmd += 'incr '+cKey+' '
If nStep == NIL
	cSendCmd += '1'
Else
	cSendCmd += cValToChar(nStep)
Endif

cSendCmd += CRLF 

If ::lVerbose
	Conout("zMemCached:Increment() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

// Se tudo der certo, aqui eu devo receber o valor apos o incremento
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Increment() failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Increment() failed - response time-out"
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Increment() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

// Parser do retorno
cRecvBuff := strtran(cRecvBuff,CRLF,'')

If !(left(cRecvBuff,1)$'0123456789')
	::cError := "Increment() failed - Error "+cRecvBuff
	::_GetTCPError()
	Return .F.
Endif

// Pega e retorna o valor apos o incremento
nValue := val(cRecvBuff)

Return .T.

Method Decrement( cKey , nValue , nStep ) CLASS ZMEMCACHED

Local cSendCmd := ''
Local cRecvBuff := ''
Local nRecv
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

// Monta o comando de recuperacao
cSendCmd += 'decr '+cKey+' '
If nStep == NIL
	cSendCmd += '1'
Else
	cSendCmd += cValToChar(nStep)
Endif

cSendCmd += CRLF

If ::lVerbose
	Conout("zMemCached:Decrement() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

// Se tudo der certo, aqui eu devo receber o valor apos o decremento
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Decrement() failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Decrement() failed - response time-out"
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Decrement() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

// Parser do retorno

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If !(left(cRecvBuff,1)$'0123456789')
	::cError := "Decrement() failed - Error "+cRecvBuff
	Return .F.
Endif

// Pega e retorna o valor apos o decremento
nValue := val(cRecvBuff)

Return .T.

Método FLUSH

E, para finalizar, se eu quiser evaporar com todo o conteúdo em cache — todas as chaves e valores armazenadas — eu chamo o método Flush().

METHOD Flush() CLASS ZMEMCACHED
Local nRecv, cRecvBuff := ''
Local cSendCmd := "flush_all" + CRLF
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:Flush() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv == 0
	::cError := "Receive timed-out"
	::_GetTCPError()
	Return .F.
Endif

If nRecv < 0
	::cError := "Receive Error"
	::_GetTCPError()
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:Flush() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

If Left(cRecvBuff,2)!='OK'
	::cError := "Response Error : " + cRecvBuff
	Return .F.
Endif

Return .T.

Programa de Testes

Para verificar as funcionalidades do Cache, criei um programa de testes da funcionalidade client da API, para confirmar os comportamentos, segue fonte abaixo:

#include 'Protheus.ch'
#include 'zLibStr2HexDmp.ch'

// ---------------------------------------------------------------------------------
// Utiliza direto uma inistancia da classe ZMEMCACHED 
// para ler e gravar valores, testa todas as operações, expires e contador/sequenciador

#define TEST_HOST		'localhost'
#define TEST_PORT		11211

User Function MemTst1()

Local oClient
Local cVersion := ""
Local aStats := {}
Local nI , nX
Local xValue 
Local nNewValue

oClient := ZMEMCACHED():New( TEST_HOST , TEST_PORT )

// Modo verbose apenas para depuração 
// oClient:lVerbose := .t.

IF !oClient:Connect()
   conout("Falha de conexão...")
   conout(oClient:cError)
   return
Endif

// Recupera a versao da instancia atual
If !oClient:GetVersion(@cVersion)
   conout("Falha ao recuperar versao...")
   conout(oClient:cError)
   return
endif

conout("Memcache Version: "+cVersion)

// Pega as estatisticas da instancia atual 
If !oClient:GetStats(@aStats)
   conout("Falha ao recuperar estatisticas...")
   conout(oClient:cError)
   return
endif

conout(padc(" STATISTICS ",79,"-"))
aEval(aStats , {|x| conout(x) })

// Apaga todas as chaves

If !oClient:Flush()
   conout("Falha na limpeza global...")
   conout(oClient:cError)
   return
endif

// Testando armazenamento de valores
cValue1 := RandomStr(64)
cValue2 := RandomStr(64)

// Acrescenta o valor 
// Fuciona apenas caso a chave nao exista 
If !oClient:Add( 'chave' , cValue1 )
	conout("Falha ao adicionar chave ...")
	conout(oClient:cError)
	Return 
Endif

// Agora tenta acrescentar na mesma chave
// isso nao deveria ser possivel 
If oClient:Add( 'chave' , cValue1 )
	UserException("Permitiu adicionar valor de chave ja existente")
Endif

// Troca valor - apenas se a chave existe 

If !oClient:Replace( 'chave' , cValue2 )
	conout("Falha ao trocar chave ...")
	conout(oClient:cError)
	Return
Endif

// Deleta a chave
If !oClient:Delete( 'chave')
	conout("Falha ao deletar chave ...")
	conout(oClient:cError)
	Return
Endif

// agora tenta trocar o valor. 
// deveria falhar, pois a chave nao existe 
If oClient:Replace( 'chave' , cValue1 )
	UserException("Permitiu troca de valor de chave que nao existe")
Endif

// Acrescenta o valor de novo 
// Deve funcionar, pois a chave tinha sido deletada
If !oClient:Add( 'chave' , cValue1 )
	conout("Falha ao adicionar chave ...")
	conout(oClient:cError)
	Return 
Endif

// Mostra no console o valor graavdo 
conout(padc(" STORED VALUE ",79,"-"))
Conout(Str2HexDmp(cValue1))

// Agora le o valor da chave
lOk := oClient:Get('chave' , @xValue )

If !lOk
	conout("Falha ao ler a chave ...")
	conout(oClient:cError)
	Return 
Endif

conout(padc(" READED VALUE ",79,"-"))
Conout(Str2HexDmp(xValue))

If ! (xValue == cValue1 ) 
	UserException("Divergencia de valor")
Endif

// busca uma chave que nao existe 
lOk := oClient:Get('naoexiste' , @xValue )

If !lOk
	conout("Retorno inesperado")
	conout(oClient:cError)
	Return 
Endif
             
// Cria um contador para incremento
// Ja inicializado com um valor

If !oClient:Add( 'contador' , '666' )
	conout("Falha ao adicionar contador ...")
	conout(oClient:cError)
	Return 
Endif

// Agora testa o incremento 
nNewValue := 0 

If !oClient:Increment( 'contador' , @nNewValue )
	conout("Falha ao incrementar contador ...")
	conout(oClient:cError)
	Return 
Endif

conout("nNewValue = "+cValToChaR(nNewValue))

If nNewValue != 667
	UserException("Incr Failed - Expected 667 Reveived "+cvaltochar(nNewValue))
Endif

If !oClient:Decrement( 'contador' , @nNewValue )
	conout("Falha ao incrementar contador ...")
	conout(oClient:cError)
	Return 
Endif

conout("nNewValue = "+cValToChaR(nNewValue))

If nNewValue != 666
	UserException("Decr Failed - Expected 667 Reveived "+cvaltochar(nNewValue))
Endif

// Agora incrementa um contador que nao existe 
If oClient:Increment( 'contador2' , @nNewValue )
	UserException("Nao deveria incrementar algo que nao existe")
Else
	Conout("-- Falha esperada -- contador realmente nao existe ")
Endif

// teste de valor com timeout                       
// expira em (aproximadamente) 2 segundos

If !oClient:Add( 'timer' , 'teste' , 2 )
	conout("Falha ao adicionar contador ...")
	conout(oClient:cError)
	Return 
Endif

// le o valor 4 vezes em intervalos de 1 segundo 
// A partir da terceira leitura o valor 
// já nao deveria existir 

For nX := 1 to 4
	// le direto o valor 
	lOk := oClient:Get('timer' , @xValue )
	conout(padc(" GET VALUE ",79,"-"))
	If xValue = NIL 
		conout("--- NIL --- ")	
	Else
		Conout(Str2HexDmp(xValue))
	Endif
	If !lOk
		conout("Falha ao ler a chave ...")
		conout(oClient:cError)
		Return 
	Endif
	Sleep(1000)
Next

// Pega as estatisticas no final do teste
If !oClient:GetStats(@aStats)
   conout("Falha ao recuperar estatisticas...")
   conout(oClient:cError)
   return
endif

conout(padc(" MEMCACHED STATSISTICS ",79,"-"))
aEval(aStats , {|x| conout(x) })

oClient:Disconnect()
FreeObj(oClient)

Return

// ---------------------------------------------------------------------------------
// Função RandomStr()
// Gera uma string com o tamanho espeficicado, sorteando caracteres 
// ASICI da faixa de 32 a 127, contemplando letras, números e simbolos 

STATIC Function RandomStr(nSize)
Local cRet := ''
While nSize>0
	cRet += chr(randomize(32,128))
	nSize--
enddo
Return cRet

O programa deve apenas emitir echo no log de console, ele apenas vai abortar a execução com um erro caso ele não consiga conexão, ou no caso de algum comportamento inesperado do MemCached. Os testes foram realizados com um MemCached 1.4.5 para Windows, mas a camada TCP é a mesma para Linux.

Cache Distribuído

Quem já usa APIs de MemCached deve estar se perguntando: — Onde eu acrescento os servidores do MemCached? Bem, esta é a primeira versão de client, então ela conecta com apenas uma instância de MemCached. O MemCached foi construído para ser um cache distribuído, onde a capacidade de montar um cluster e dividir os dados entre as instâncias online do MemCached é do Client.

Por hora, a implementação deste client em AdvPL conversa com apenas uma instância, que pode ser colocada por exemplo em uma máquina, onde existam mais de um serviço de Protheus Server para consumir o cache.

Cuidados no Uso

  • Recomendo a leitura das recomendações de uso do MemCached no site da ferramenta, tanto sobre dimensionamento como boas práticas de segurança.
  • Um serviço de MemCached normalmente pode ser acessado por mais de um programa Cliente consumidor do Cache, então não crie nomes “curtos demais” para identificar as suas chaves, inclusive preferencialmente crie um padrão de nomenclatura, como o environment + “_”  + Modulo ou programa + “_”  + identificador do cache.

Conclusão

Por hora a API armazena basicamente um buffer em formato Caractere no AdvPL. Inclusive, pode armazenar formato binário, como uma imagem. Caso seja necessário armazenar por exemplo um Array ou outros valores, é necessário convertê-los para Caractere, e depois convertê-los de volta. Estou estudando uma forma rápida de se fazer isso. Na continuação deste post, espero já ter finalizado esta questão de forma elegante.

Fontes da ZLIB

A partir de hoje —  13/01/2019 — os fontes da ZLIB estão em um repositório separado do GITHUB. Por hora só existe o branch “master”, e ele é o que sempre será atualizado. Os fontes existentes no repositorio do blog permanecem lá, mas sem receber atualização — apenas para não quebrar os links dos posts anteriores. Os fontes e includes da zLib estão na URL https://github.com/siga0984/zLIB

Agradeço a todos novamente pela audiência, e lhes desejo TERABYTES DE SUCESSO !!!

Referências

 

Acelerando o AdvPL – Importação de tabelas

Introdução

Existem muitas situações onde existe a necessidade de alimentar ou importar tabelas para uso do ERP Microsiga / Protheus. Quando esta necessidade envolve um grande número de registros, e um curto espaço de tempo, precisamos fazer esta operação ser o mais rápida possível. Nesse post vamos abordar algumas técnicas para realizar este tipo de operação.

  • Crie os índices da tabela apenas após a importação dos dados. 

Se a tabela está vazia, e os indices de trabalho desta tabela já existem, cada novo registro acrescentado faz o banco de dados atualizar todos os índices durante a inserção dos dados. Sem os índices, o banco apenas insere os dados. A inserção vai ser muito mais rápida, e os índices também são criados mais rápido no final do processo de importação, quando você já inseriu todos os registros na tabela. Se você precisa ter um índice na tabela, no processo de importação, por exemplo para fazer alguma validação ou query na tabela enquanto ela está sendo importada, crie apenas este índice.

  • Trabalhe com blocos de inserções transacionadas

Os bancos de dados relacionais garantem a integridade dos dados através do registro de um “LOG Transacional”. Cada operação de inserção primeiro é gravada no LOG do SGDB, e depois é “efetivada” — o Banco de Dados faz o COMMIT da informação. Quando não estamos usando uma transação explícita, cada instrução executada no Banco de Dados faz COMMIT automaticamente no final da instrução. É mais eficiente abrir uma transação — BEGIN TRANSACTION — pois enquanto a transação está aberta, cada instrução gera apenas o LOG Transacional, e depois de fazer por exemplo 1000 inserções na base de dados, você encerra a transação — END TRANSACTION no AdvPL — e neste momento o Banco de dados faz o COMMIT de todas as linhas do LOG de uma vez.

  • A nova tabela que vai receber os dados deve ser criada pelo DBAccess 

Mesmo que você saiba quais são os tipos dos campos da estrutura da tabela final, onde os dados serão gravados, quem sabe criar a tabela nos padrões e moldes necessários para o correto funcionamento da aplicação é o Protheus, que passa para o DBAccess a estrutura da tabela a ser criada. Internamente, o DBAccess cria constraints e outros elementos, além de alimentar algumas tabelas de controle interno. Deixe o DBAccess fazer a criação da tabela. Você pode até apagar os índices de dados para fazer a importação da tabela após eles terem sido criados, porém não apague o índice da primary key (R_E_C_N_O_), e se a tabela possui índice único (sufixo _UNQ), mantenha ele também, caso você queira que o próprio banco de dados aborte a operação no caso de haver uma chave única duplicada nos dados sendo importados.

  • Usando as funções de baixo nível de tabelas do AdvPL 

Caso a importação dos dados seja realizada por uma aplicação AdvPL, que foi criada para esta finalidade, que não vai concorrer com os processos do ERP, podemos usar diretamente as funções de baixo nível de tabelas do AdvPL. As funções de mais alto nível — como RECLOCK e MSUNLOCK devem ser usadas dentro dos programas do ERP, no ambiente do ERP, pois elas têm tratamentos adicionais ligados ao FrameWork do ERP. Se, ao invés disso, usarmos diretamente as funções diretas de manutenção de dados, em um cenário controlado, podemos obter mais ganhos de desempenho — DBAppend() para acrescentar um novo registro, DBRUnlock() — sem nenhum parâmetro — para soltar o bloqueio (lock) de todos os registros obtidos durante aquele processo (lembrando que eu não posso soltar o lock dentro de transação), abrir e fechar transação sem usar BEGIN e END TRANSACTION — Usando diretamente a função TCCommit().

IMPORTANTE

O Uso das funções básicas do AdvPL para a manutenção de tabelas NÃO DEVE SER MISTURADO com o uso das funções do Framework. Dentro de um processo preparado para o contexto de execução do ERP, BEGIN TRANSACTION trabalha junto com END TRANSACTION, Reclock() e MSUnlock() trabalham juntas, ambas possuem tratamentos diferenciados e automáticos quando você programa dentro do ambiente do ERP e/ou dentro de um bloco transacionado, e todas elas internamente fazem uso das funções de baixo nível do AdvPL. Se você vai fazer uma aplicação que não vai rodar dentro do contexto do ERP, como por exemplo as aplicações que eu publico no BLOG — CRUD em AdvPL por exemplo — você deve usar apenas as funções de baixo nível. Misturar chamadas diretas das funções de Framework com chamadas das funções de baixo nível NO MESMO PROCESSO pode causar efeitos imprevisíveis.

  • Abra a tabela de destino em modo exclusivo

Se a sua aplicação vai usar o ambiente do ERP — Prepare Environment, Reclock e afins, abra a tabela de destino em modo exclusivo — veja como na documentação da função ChkFile() do Framework AdvPL. Caso você vá utilizar as funções de baixo nível de acesso a tabelas, veja como abrir uma tabela em modo exclusivo na documentação da função DBUseArea() da TDN ou do comando USE da TDN. Caso a tabela esteja aberta em modo compartilhado, cada inserção de um novo registro faz o DBAccess registrar um bloqueio desse registro na inserção, e cada desbloqueio gera uma requisição a mais ao DBAccess para soltar o bloqueio. Quando usamos a tabela em modo EXCLUSIVO, somente o meu processo está usando a tabela, e não é feito nenhum lock no DBAccess.

  • Use um BULK INSERT ou ferramenta de apoio do próprio Banco de Dados

Não há forma mais rápida de inserir dados em uma tabela senão usando uma ferramenta do próprio Banco de Dados. Porém, nem sempre é possível fazer isso de forma automática, e alguns cuidados são necessários. Primeiro, a tabela já deve ter sido previamente criada pelo DBAccess. Depois, a gravação dos dados pelo Banco deve ser feita da mesma forma que o DBAccess faria — Qualquer campo do tipo caractere deve estar preenchido com espaços em branco até o final do tamanho do campo, nenhum campo pode ter valor NULL — exceto campo MEMO –, um campo “D” Data em AdvPL em uma tabela do DBAccess é gravado como “C” Caractere com 8 posições, no formato AAAAMMDD, uma data vazia são 8 espaços em branco, um campo “L” lógico do AdvPL é criado pelo DBAccess no SGDB como um campo de 1 Caractere, contento a letra “T” para valor verdadeiro e “F” para falso, um campo numérico têm o valor default = 0 (zero).

  • Diminua a distância entre as informações de origem e destino 

Normalmente uma ferramenta do próprio banco de dados que faz inserção deve ser executada na própria máquina onde está o SGDB. Isto a torna ainda mais rápida. Se você está fazendo uma importação de dados que estão sendo lidos pelo Protheus de alguma fonte de dados, e depois repassadas ao Banco de Dados através do DBAccess, e cada um destes componentes (Protheus, DBAccess e o SGDB) estão em máquinas distintas, onde existe uma interface de rede entre cada um destes componentes, fatalmente a rede vai influenciar no desempenho desta operação. Se for possível eliminar pelo menos uma camada de rede entre as aplicações, pelo menos na hora de fazer a importação, ela tende a ser mais rápida.

  • Se posssível, leia as informações da origem com Query

Se a origem dos dados for uma tabela da base de dados, ao invés de abrir a tabela no modo ISAM — com ChkFile() ou DBUseArea() — leia os dados da tabela com um SELECT — O DBAccess a partir de 2017 passou a trafegar blocos de linhas entre o DBAccess e o Protheus, o que é muito mais performático principalmente quando ambos estão em máquinas diferentes. No acesso ISAM emulado, cada DBSkip() na tabela traz apenas um registro. Quando usamos a Query, várias linhas são trafegadas em uma unica requisição, e cada DBSkip() na Query consome a próxima linha em cache sem usar a rede. Quando as linhas do cache terminarem, o DBAccess envia mais um bloco de linhas.

Inserção direta via TCSqlExec()

Da mesma forma que é possível usar uma aplicação externa para inserir dados direto no banco, é possível também realizar a inserção direta no banco em AdvPL usando TCSqlEXec() — MAS, POREM, TODAVIA, CONTUDO, ENTRETANTO, você precisa tomar os mesmos cuidados como se você estivesse alimentando estes dados por fora, E, você somente vai ter algum ganho de desempenho se você montar uma string de parâmetro com mais de uma inserção — por exemplo, inserindo múltiplas linhas na mesma instrução de insert — caso o banco de dados tenha suporte para isso — ou concatenando mais de uma instrução de inserção na mesma requisição, separando as instruções com “;” — ponto e vírgula.

cStmt := "INSERT INTO TABELA(CPO1,CPO2,R_E_C_N_O_) VALUES ('00001','Nome',1);"
cStmt += "INSERT INTO TABELA(CPO1,CPO2,R_E_C_N_O_) VALUES ('00002','Endereço',2);"
(...)
nRet := TCSqlExec(cStmt)

No exemplo acima, você pode por exemplo montar uma string de inserção com por exemplo 32 KB de dados, e enviar isso de uma vez para o banco de dados. Mas, inserir dados desta forma possui vantagens e desvantagens:

  • Uma requisição insere vários registros na mesma instrução. Usando DBAppend() / Reclock(), é inserido um registro por vez.
  • Uma requisição maior no lunar de várias requisições menores aproveita muito mais a banda de rede disponível.

Por outro lado…

  • Para montar a inserção dos valores de forma literal, você deve tratar manualmente a existência de caracteres especiais, aspas simples como conteúdo de campo, e para alguns bancos tratar “escape sequences” e caracteres especiais.
  • Campos MEMO (BLOB/CLOB/LONGVARBINARY) normalmente não são suportados com envio literal pela instrução de INSERT feita desta forma. Você acaba fazendo a inserção primeiro dos dados dos campos de tamanho fixo, e depois tem que usar o modo ISAM de acesso para alterar o registro em AdvPL, para então alimentar o conteúdo do campo.

IMPORTANTE

Nenhum dos métodos acima é recomendável de ser feito em processos concorrentes, em modo compartilhado, com outros programas inserindo informações na tabela em questão. As sugestões deste post mostram apenas jeitos diferentes e alternativas de importação de dados puros direto para tabelas de um banco de dados. Como nestes exemplos os dados são praticamente escritos “direto” nas tabelas, a aplicação que está importando os dados deve ser a responsável por criticar os dados de entrada, e garantir que a sua escrita esteja em conformidade com as regras do ERP. Mesmo uma operação de cadastro feita no ERP pode disparar integrações e processos auxiliares que alimentam outras tabelas. Mexer diretamente na base de dados sem respeitar as regras de negócio e/ou as validações do produto podem gerar comportamentos inesperados e indesejáveis no produto e nas rotinas que consomem estes dados. A forma mais segura de importar dados em tabelas do ERP é a utilização de rotinas automáticas, onde fatalmente vai existir um pênalti de desempenho para validar todos os valores de todos os campos informados, mas desta forma evita-se a alimentação incompleta ou inconsistente de dados que pode prejudicar o comportamento do produto.

Conclusão

Não existe “almoço grátis”, alguém está sempre pagando a conta. Uma inserção de dados é um processo deveras simples, você obtêm maior desempenho basicamente realizando requisições em blocos, e pode ganhar mais reduzindo as operações intermediárias intrínsecas ao processo — porém este ganho normalmente abre mão de validações que visam garantir a integridade das informações imputadas, ou acabam exigindo mais trabalho e etapas adicionais para a conclusão do processo.

Novamente agradeço pela audiência, curtidas e compartilhamentos, e desejo a todos TERABYTES DE SUCESSO !!! 😀

Referências

 

Algoritmos – Parte 02 – Permutações

Introdução

No post anterior (Algoritmos – Parte 01 – Loterias), vimos a criação de um algoritmo para realizar combinações simples, que pode ser usado na maioria das loterias numéricas. Agora, vamos ver um algoritmo de permutação — Algoritmo de Heap — e ver como fazer a portabilidade de um pseudo-código para AdvPL.

Algoritmo de Heap

O Algoritmo de Heap é até hoje a forma mais optimizada de gerar todas as possibilidades de permutações em um conjunto de elementos. A permutação é um processo pelo qual podemos criar sequências não repedidas dos elementos de um conjunto, alterando a sua ordem. Por exemplo, partindo de um conjunto de três números (1, 2 e 3), podemos criar as seguintes permutações:

1 2 3 
1 3 2 
2 1 3
2 3 1 
3 1 2
3 2 1

Calcula-se o número total de possibilidades de permutação de um conjunto com a fórmula P(m) = m! –> Onde P é o número de possibilidades de permutação e m é o número de elementos do conjunto, e  “!” é o símbolo da operação fatorial. Por exemplo, em um conjunto de 3 elementos, temos 3! ( 3! = 3*2 = 6) conjuntos ordenados resultantes.

Pseudocódigo

A partir da Wikipedia, por exemplo, podemos obter o pseudocódigo do algoritmo — uma representação em linguagem quase natural da sequência de operações nos conjuntos de dados para se chegar ao resultado. Esta versão do pseudo-código é a não-recursiva.

procedure generate(n : integer, A : array of any):
    c : array of int

    for i := 0; i < n; i += 1 do
        c[i] := 0
    end for

    output(A)

    i := 0;
    while i < n do
        if  c[i] < i then
            if i is even then
                swap(A[0], A[i])
            else
                swap(A[c[i]], A[i])
            end if
            output(A)
            c[i] += 1
            i := 0
        else
            c[i] := 0
            i += 1
        end if
    end while

Agora, vamos converter isso para AdvPL, primeiro de uma forma bem simples, depois de uma forma mais elaborada. Inicialmente, vamos fazer uma tradução “crua” para o AdvPL, porém funcional.

STATIC Function Generate( n , A )
Local c := {} , i
For i := 1 to n
  aadd(c,0)
Next
output(A)
i := 0 
While i < n
  If  c[i+1] < i
    if ( i % 2 ) == 0 
      swap(A, 1 , i+1)
    else
      swap(A, c[i+1]+1, i+1)
    end if
    output(A)
    c[i+1]++
    i := 0
  Else
    c[i+1] := 0
    i++
  EndIf
Enddo


STATIC Function swap(aData,nPos1,nPos2)
Local nTemp := aData[nPos1]
aData[nPos1] := aData[nPos2] 
aData[nPos2] := nTemp
Return

STATIC Function output(A)
Local i, R := ''
For i := 1 to len(A)
  If !empty(R)
    R += ', '
  Endif
  R += cValToChaR(A[i])
Next
conout(R)
Return

Diferenças na Implementação

A primeira diferença nós vemos logo de início ao usar os Arrays em AdvPL. O pseudo-código parte da premissa que um Array de N posições é endereçado de 0 a N-1 — Isto é, o primeiro elemento do Array é o elemento 0 (zero.) Já em AdvPL, o primeiro elemento do array é 1 (um). Logo, nós mantemos toda a lógica do programa inicial, inclusive as variáveis como se o array fosse base 0 (zero), porém na hora de endereçar os elementos do array, somamos uma unidade. Logo:

if c[i] < i

foi transformado para

if c[i+1] < i

A função swap() tem como objetivo trocar os elementos do array, um pelo outro. Como em AdvPL os arrays são passados por referência, podemos implementar a função de troca guardando o valor do elemento informado em uma variável local, depois atribuímos o conteúdo do segundo elemento informado sobre o primeiro, e então atribuímos o conteúdo salvo do primeiro elemento no segundo — vide função swap(). O diferencial dela em relação ao pseudocódigo é que eu passo para ela em AdvPL três parâmetros: O Array, e as duas posições a serem trocadas.

No pseudo-código, para verificar se um determinado numero é par (even em inglês), pode ser em AdvPL verificando  se o resto da divisão por dois é zero. Para isso, poderíamos usar a função mod(), ou de forma mais prática, o operador “%”.

if ( i % 2 ) == 0

Outro ponto de atenção é justamente a chamada da função swap() quando o número não for par. Veja no pseudocódigo:

swap(A[c[i]], A[i])

Agora, em AdvPL, a implementação ficou assim:

swap(A, c[i+1]+1, i+1)

Reparem que o array c[] guarda uma posição de um array. Como estamos trabalhando com array em base 1, eu devo somar 1 para recuperar o elemento do array c, e como o seu resultado será usado para indicar uma posição do array A[], eu também preciso somar 1.

Já a função output(), cujo entendimento óbvio é mostrar um dos conjuntos obtidos na permutação, implementamos simplesmente recebendo o Array , e criando uma string com o conteúdo de seus elementos separados por vírgula. Para testar o fonte acima, vamos usar a seguinte função:

User Function Permuta()
Generate( 4 , {'A' ,'B' ,'C' ,'D' } )
Return

Após salvarem, compilarem e executarem o programa acima, o resultado no log de console do Protheus Server deve ser:

A, B, C, D
B, A, C, D
C, A, B, D
A, C, B, D
B, C, A, D
C, B, A, D
D, B, A, C
B, D, A, C
A, D, B, C
D, A, B, C
B, A, D, C
A, B, D, C
A, C, D, B
C, A, D, B
D, A, C, B
A, D, C, B
C, D, A, B
D, C, A, B
D, C, B, A
C, D, B, A
B, D, C, A
D, B, C, A
C, B, D, A
B, C, D, A

Conclusão

Vendo o fonte assim, prontinho, parece fácil. Porém, eu comecei resolvendo trocar o nome das variáveis da implementação em AdvPL ao transcrever o pseudocódigo, e o programa não gerava os números corretamente. Joguei fora a primeira versão e parti do código original, mantendo o nome das variáveis, então funcionou.

Novamente agradeço a todos pela audiência e desejo a todos TERABYTES DE SUCESSO !!! 

Referências

Algoritmos – Parte 01 – Loterias

Introdução

Nos primeiros posts no Blog sobre programação — vide Desmistificando a análise de sistemas e Desmistificando a programação — foi colocada de forma simples a ideia de programar alguma coisa, como sendo apenas uma sequência de instruções e decisões para se realizar uma tarefa. E, realmente é simples assim, o que precisamos fazer é usar corretamente a gramática da linguagem para realizar as tarefas em pequenas etapas, criando tarefas maiores e reaproveitando tarefas menores.

Algoritmos

Uma sequência de instruções para realizar uma tarefa específica pode ser chamada de “Algoritmo” — verifique a definição na Wikipedia – Algoritmo. Não é algo aplicado apenas a programação de computadores.

Consultando também a Wikipédia – Lista de Algoritmos, podemos ver vários exemplos de problemas específicos resolvidos com sequências de operação. Como o algoritmo em si trata apenas da sequência de operações para obter um resultado, ele pode ser implementado em várias linguagens de programação.

Alguns dos mais conhecidos e utilizados são algoritmos de ordenação e busca de dados, compressão, sorteio de números (números randômicos), criptográficos (segurança da informação), entre outros. Na internet podemos obter não apenas vários modelos de algoritmos, mas também várias implementações dos mesmos em várias linguagens de programação.

Algoritmos em AdvPL

Vamos pegar um problema qualquer, e criar um algoritmo para resolvê-lo, um por tópico. Um problema bem interessante de ser resolvido usando um programa de computador, é por exemplo lidar com análise combinatória. Podemos usar um algoritmo de geração de combinações (ou Combinação Simples) para, por exemplo, criar massas de dados explorando as combinações de vários parâmetros para testes de funções, combinar números para cartões de loteria — adoro esse algoritmo — entre outras finalidades. Vamos ver como realizar uma análise combinatória eficiente para loterias em AdvPL.

Combinação para Loterias

Em um determinado cartão de loteria, podemos preencher no mínimo N números diferentes, dentre um total de X números disponíveis, para concorrer a um prêmio caso uma quantidade mínima de números sorteados estejam presentes entre os N números do cartão.

Quando queremos por exemplo, determinar todas as possibilidades de combinação de Y números em cartões de X números, utilizamos um recurso de análise combinatória chamado “Combinação Simples”. Atualmente, para alguns tipos de loterias,o próprio bilhete de apostas já permitem você fazer uma aposta com mais números do que a aposta mínima. Neste caso, a sua aposta equivale a todas as possibilidades de combinação simples dos Y números em um cartão de X números.

MEGA-SENA

Vamos tomar como exemplo a MEGA-SENA, onde a aposta mínima é um bilhete com seis números preenchidos, a um valor de R$ 3,50 . Podemos preencher de 6 a 15 números em um cartão ou bilhete, porém o preço da aposta sobe proporcionalmente ao número de combinações simples da quantidade de números preenchidos em blocos de seis números. Veja a tabela abaixo, de apostas e preços, e a quantidade de apostas de 6 números equivalente.

NUMEROS NO  | VALOR DA     | EQUIVALE A <n> 
BILHETE     | APOSTA       | APOSTAS de 6 números
------------+--------------+------------------------          
6 números   | R$ 3,50      |    1 aposta 
7 números   | R$ 24,50     |    7 apostas 
8 números   | R$ 98,00     |   28 apostas 
9 números   | R$ 294,00    |   84 apostas 
10 números  | R$ 735,00    |  210 apostas 
11 números  | R$ 1617,00   |  462 apostas 
12 números  | R$ 3234,00   |  924 apostas 
13 números  | R$ 6006,00   | 1716 apostas 
14 números  | R$ 10510,50  | 3003 apostas 
15 números  | R$ 17517,50  | 5005 apostas

Quantidade de combinações possíveis

A fórmula para determinar uma combinação simples C, de n números em conjuntos de k elementos é:

C(n,k) = n! / k! * (n-k)!

Onde “!” significa o valor FATORIAL do elemento, “*” é o operador de multiplicação, “/” é o operador de divisão.

Na matemática, o fatorial (AO 1945: factorial) de um número natural n, representado por n!, é o produto de todos os inteiros positivos menores ou iguais a n. A notação n! foi introduzida por Christian Kramp em 1808.

Vamos pegar por exemplo uma combinação de 12 elementos em conjuntos de 6 elementos — equivalente ao cartão de 12 números da MEGA-SENA.

C(12,6) = 12! / 6! * (12-6)!
C(12,6) = 12! / 6! * 6!
C(12,6) = 479001600 / 720 * 720 
C(12,6) = 479001600 / 518400 
C(12,6) = 924

Logo, antes mesmo de começar a fazer a combinação, podemos determinar o número de resultados possíveis.

Combinando elementos

A lógica para realizar a combinação dos elementos é basicamente a mesma, não importa a quantidade de elementos a combinar ou o tamanho do conjunto resultante. Porém, dependendo da forma que ela for implementada, ela tende a ficar mais pesada quanto maior a quantidade de elementos a combinar. Vamos pegar por exemplo a combinação de 7 elementos numéricos, de 1 a 7, em conjuntos de 6 elementos.

C(7,6) = 7! / 6! * (7-6)!
C(7,6) = 7! / 6! * (1)!
C(7,6) = 5040 / 720 * 1
C(7,6) = 7

Logo de antemão, sabemos que esta combinação resultará em 7 resultados. Agora, vamos fazer a combinação. Partindo dos números já ordenados, a primeira combinação é:

1 2 3 4 5 6

Para determinar o próximo resultado, pegamos o próximo numero do conjunto da última posição e incrementamos uma unidade:

1 2 3 4 5 7

Ao tentar fazer a mesma operação para pegar o terceiro resultado, o número 7 é o último elemento do conjunto. Então, precisamos incrementar o número na posição anterior — quinta posição. Quando fazemos isso, o número da posição que atingiu o limite deve ser o próximo elemento relativo a posição anterior. Logo, o próximo resultado será:

1 2 3 4 6 7

Repetindo novamente esta operação, começando da direita para a esquerda, na sexta posição, o elemento 7 já é o último do conjunto. Então vamos para a quinta posição. O próximo elemento desta posição seria o 7, porém ele é o último elemento da combinação, e eu não estou na última posição do conjunto de resultado, isto significa que ele já está em uso em uma posição posterior. Logo, vamos a posição anterior — quarta posição — e incrementamos o número 4 para 5, obtendo o resultado:

1 2 3 5 6 7

Repetindo os passos anteriores, os números 5, 6 e 7 não podem ser incrementados pois já são os elementos finais da combinação. Logo, incrementamos na terceira posição o 3 para 4, depois na segunda posição de 2 para 3, depois na primeira, de 1 para 2, e depois acabou, pois não há como incrementar mais nada.

1 2 4 5 6 7 
1 3 4 5 6 7 
2 3 4 5 6 7

Com isso obtivemos os 7 resultados possíveis. Agora, vamos colocar essa regra em um código fonte. Para facilitar o uso deste recurso, vamos criá-lo como uma Classe em AdvPL. Após construir e otimizar o algoritmo, a primeira versão ficou assim (fonte ACOMB.PRW):

#include "protheus.ch"

CLASS ACOMB FROM LONGNAMECLASS

  DATA nCols
  DATA aElements
  DATA nSize
  DATA aControl
  DATA aMaxVal
  DATA nPos

  METHOD NEW()
  METHOD GETCOMB()
  METHOD NEXTCOMB()
  METHOD GETTOTAL() 

ENDCLASS

METHOD NEW( nCols , aElements ) CLASS ACOMB
Local nI , nMax
::nCols := nCols
::aElements := aElements
::nSize := len(aElements)
::nPos := nCols
::aControl := {}
::aMaxVal := {}
nMax := ::nSize - ::nCols + 1
For nI := 1 to ::nCols
  aadd(::aControl,nI)
  aadd(::aMaxVal, nMax )
  nMax++
Next
Return self

METHOD GETCOMB() CLASS ACOMB
Local nI , aRet := array(::nCols)
For nI := 1 to ::nCols
  aRet[nI] := ::aElements[ ::aControl[nI] ] 
Next 
Return aRet

METHOD NEXTCOMB() CLASS ACOMB
If ::aControl[::nPos] + 1 > ::aMaxVal[::nPos]
  ::nPos := ::nPos - 1 
  If ::nPos < 1 
    Return .F. 
  Endif
  If ::NEXTCOMB()
    ::nPos := ::nPos + 1
    ::aControl[::nPos] := ::aControl[::nPos-1]+1
  Else
    Return .F. 
  Endif
Else
  ::aControl[::nPos]++
Endif
Return .T.

METHOD GETTOTAL() CLASS ACOMB
Local nFat1 := Fatorial( ::nSize )
Local nFat2 := fatorial( ::nCols )
Local nFat3 := Fatorial( ::nSize - ::nCols )
Local nTot := nFat1 / ( nFat2 * nFat3 ) 
Return nTot

STATIC Function Fatorial(nNum)
Local nI := nNum - 1
While nI > 1 
  nNum *= nI 
  nI--
Enddo
Return nNum

A propriedade aControl controla os contadores de cada posição do conjunto de retorno, e a propriedade aMaxVal eu já determino qual é o valor máximo de um determinado contador para a coluna ou posição atual. O Método GetComb() retorna um array com os elementos combinados, e o método NextComb() determina qual a próxima combinação da sequência, atualizando o array aControl. Quando o método NextComb() retornar .F., não há mais combinações possíveis. E, usando o método GetTotal(), eu determino quantas combinações são possíveis.

Para testar a classe acima, vamos usar o seguinte fonte:

User Function ACombTst()
Local nI
Local nResult := 1
Local lEcho := .F. 
Local nTimer
Local nCols , aData := {}

// Monta 35 dezenas para combinar 
For nI := 1 to 35
  aadd(aData,strzero(nI,2))
Next
// Combinação em conjuntos de 6 dezenas 
nCols := 6
oLoto := ACOMB():New( nCols , aData )
nTotal := oLoto:GetTotal()
conout("Elementos ...... "+cValToChar(len(aData)) )
conout("Conjunto ....... "+cValToChar(nCols) )
conout("Combinacoes .... "+cValToChar(nTotal) )
If lEcho
  aRet := oLoto:GETCOMB()
  conout("("+strzero(nResult,6)+") "+aRet[1]+" "+aRet[2]+;
         " "+aRet[3] +" "+aRet[4]+" "+aRet[5]+" "+aRet[6] )
endif
nTimer := seconds()
While oLoto:NEXTCOMB()
  nResult++
  If lEcho
    aRet := oLoto:GETCOMB()
    conout("("+strzero(nResult,6)+") "+aRet[1]+" "+aRet[2]+;
           " "+aRet[3] +" "+aRet[4]+" "+aRet[5]+" "+aRet[6] )
  Endif
Enddo
nTimer := seconds() - nTimer
conout("Resultados ..... "+cValToChar(nResult) )
conout("Tempo .......... "+str(nTimer,12,3)+" s.")
Return

No meu notebook, determinar as 1623160 combinações possíveis de 35 números em blocos de 6 demorou aproximadamente 3,5 segundos. Vejamos o log de console:

Elementos ...... 35
Conjunto ....... 6
Combinacoes .... 1623160
Resultados ..... 1623160
Tempo .......... 3.531 s.

Sim, neste teste eu não peguei os resultados, e não imprimi os resultados, eu apenas chamei em loop o método NextComb() até ele calcular todas as combinações. Para a aplicação imprimir os resultados em console, basta colocar .T. na variável lEcho e recompilar o fonte. Não é necessário dizer que, haverá uma boa diferença de desempenho quando você passar a resgatar cada uma das combinações e mostrar/gravar cada uma no log de console.

Dividindo o número total de combinações pelo tempo que a aplicação demorou, a rotina gerou aproximadamente 459688 resultados por segundo. Eu diria que isso é um tempo fantasticamente rápido. O tempo necessário para gerar estas combinações é simplesmente irrelevante perto do tempo que você deve gastar para, por exemplo, registrar estas combinações em uma tabela ou Banco de Dados.

Outras Loterias

Vamos pegar agora a Lotofácil. Cada cartão tem 25 dezenas, a aposta mínima é um bilhete com 15 dezenas preenchidas, e o prêmio máximo é você acertar os 15 números. As chances de você ganhar jogando um bilhete com a aposta mínima é 1 em 3.268.760. Afinal, se você fizer todas as combinações de 25 dezenas em grupos de 15, é exatamente esse o número de combinações possível. Vamos ver isso rodando o programa ? Basta alterar o programa de teste para criar 25 dezenas, e atribuir 15 para nCols.

Elementos ...... 25
Conjunto ....... 15
Combinacoes .... 3268760
Resultados ..... 3268760
Tempo .......... 12.002 s.

Pode fazer a mesma coisa para a MEGA-SENA, são 60 elementos em conjunto de 6. Garanto que vai demorar bem mais que 12 segundos, afinal a MEGA-SENA são 50.063.860 possibilidades ou combinações, mais de 15 vezes do que a Loto Fácil. Vamos rodar, apenas pra ver quanto tempo demora.

Elementos ...... 60
Conjunto ....... 6
Combinacoes .... 50063860
Resultados ..... 50063860
Tempo .......... 94.201 s.

Conclusão

Mesmo sabendo de tudo isso, até agora eu não ganhei na loteria, pois matematicamente as chances de um número qualquer ser sorteado é uma para o conjunto de números. As chances de acertar todos os números de uma loteria com uma aposta é uma para o conjunto de possibilidades / combinações dos números que eu posso colocar no cartão. Matematicamente, as chances de qualquer aposta — qualquer uma, inclusive por exemplo as dezenas “01 02 03 04 05 06” , ou “05 10 15 20 25 30”, são as mesmas. A única forma de aumentar matematicamente a sua probabilidade ou chance de acerto é jogar mais cartões diferentes. E, no final das contas, você pode jogar um cartão com 16 números e não acertar nenhum.

Agradeço novamente a audiência, curtidas e compartilhamentos, e desejo a todos TERABYTES DE SUCESSO 😀

Referências

 

 

Identificando Problemas – Queries lentas – Parte 04

Introdução

Continuando o assunto de identificação de problemas, vamos ver agora o que e como lidar com queries que não apresentam um bom desempenho. Antes de chamar um DBA, existem alguns procedimentos investigativos e algumas ações que ajudam a resolver uma boa parte destas ocorrências.

Queries, de onde vêm?

Quando utilizamos um Banco de Dados relacional homologado com o ERP Microsiga / Protheus, todas as requisições de acesso a dados passam pela aplicação DBAccess. Como eu havia mencionado em um post anterior, o DBAccess serve de “gateway” de acesso ao Banco de Dados, e serve para emular as instruções ISAM do AdvPL em um banco relacional.

Logo, as queries submetidas pelo DBAccess ao Banco de Dados em uso podem ser de de dois tipos:

  • Queries emitidas (geradas) pelo DBAccess para atender a uma requisição ISAM — DBGoTop(), DBGoBottom(), DbSkip(), DBSeek().
  • Queries abertas pela aplicação AdvPL para recuperar dados diretamente do Banco de Dados.

Quando utilizamos o DBAccess Monitor, para realizar um Trace de uma conexão do Protheus com o DBAccess, podemos visualizar as operações solicitadas do Protheus ao DBAccess, e as respectivas queries submetidas ao Banco de Dados.

Como identificar uma query “lenta”?

Normalmente encontramos uma ou mais queries com baixo desempenho quando estamos procurando a causa da demora de um determinado processo. A descoberta acaba sendo realizada durante a análise de um Log Profiler obtido durante a execução da rotina, ou também através de um trace da conexão do DBAccess usando o DBMonitor.

Agora, podemos também usar o próprio DBAccess para gerar um log das queries que demoraram mais para serem abertas pelo Banco de Dados. Basta utilizar a configuração MaxOperationTimer, onde podemos especificar um número de segundos limite, a partir do qual um log de advertência deve ser gerado pelo DBAccess, caso o retorno da abertura de uma Query ultrapasse o tempo definido.

O que é uma query lenta?

Definir lentidão normalmente não está ligado apenas a medida absoluta de tempo, mas sim é relativa a urgência ou necessidade de obter a informação rapidamente, versus a quantidade de informações a serem avaliadas e retornadas.

Por exemplo, quando a aplicação AdvPL executa uma instrução como DBGoto(N), onde N é o número de um registro da base de dados, o DBAccess vai montar e submeter uma Query contra o banco de dados, para selecionar todas as colunas da tabela em questão, onde o campo R_E_C_N_O_ é igual ao número N informado.

SELECT CPO1,CPO2,CPO3,...N FROM TABELA WHERE R_E_C_N_O_ = N

Meu chapa, essa Query deve rodar normalmente em menos de 1 milissegundo no banco de dados, por duas razões: A primeira é que a coluna R_E_C_N_O_ é a chave primária (Primary Key) de todas as tabelas criadas pelo DBAccess, então naturalmente existe um índice no Banco de Dados usando internamente para achar em qual posição do Banco estão gravadas as colunas correspondentes a esta linha. E a segunda é que, apenas uma linha será retornada.

Se não existisse um índice para a coluna R_E_C_N_O_, o Banco de Dados teria que sair lento a tabela inteira, sequencialmente, até encontrar a linha da tabela que atendesse esta condição de busca. Esta busca na tabela de dados inteira sem uso de índice é conhecida pelo termo “FULL TABLE SCAN”.

Agora, imagine um SELECT com UNION de mais quatro selects, finalizado com um UNION ALL, onde cada Query faz SUB-SELECTS e JOINS no Banco de Dados, e o resultado disso não vai ser pequeno … Mesmo em condições ideais de configuração do Banco de Dados, não é plausível exigir que uma operação deste tamanho seja apenas alguns segundos.

Causas mais comuns de degradação de desempenho em Queries

Entre as mais comuns, podemos mencionar:

  1. Ausência de um ou mais índices — simples ou compostos — no Banco de Dados, que favoreçam um plano de execução otimizado do Banco de Dados para recuperar as informações desejadas.
  2. Estatísticas do Banco de Dados desatualizadas.
  3. Picos de CPU , Disco ou Rede, na máquina onde está o DBAccess e/ou na máquina onde está o Banco de Dados.
  4. Problemas de hardware na máquina do Banco de Dados ou em algum dos componentes da infra-estrutura.
  5. Problemas de configuração ou de comportamento do próprio Banco de Dados sob determinadas condições.
  6. Excesso de operações ou etapas do plano de execução, relacionadas a complexidade da Query ou da forma que a Query foi escrita para chegar ao resultado esperado.

Recomendo adicionalmente uma pesquisa sobre “Full Table Scan” e outras causas possíveis de baixo desempenho em Queries. Quanto mais infirmação, melhor. E, a propósito, mesmo que a tabela não tenha um índice adequado para a busca, se ela for  realmente pequena (poucas linhas) , o Banco de Dados internamente acaba fazendo CACHE da tabela inteira em, memória, então um Full Table Scan acaba sendo muito rápido, quando a tabela é pequena. Esta é mais uma razão por que muitas ocorrências de desempenho relacionados a este evento somente são descobertas após a massa de dados crescer representativamente no ambiente.

Troubleshooting e boas práticas

Normalmente as melhores ferramentas que podem dar pistas sobre as causas do baixo desempenho de uma Query são ferramentas nativas ou ligadas diretamente ao Banco de Dados, onde a ferramenta é capaz de desenhar e retornar — algumas inclusive em modo gráfico — o PLANO DE EXECUÇÃO da Query. Neste plano normalmente as ferramentas de diagnóstico informam quando está havendo FULL TABLE SCAN, e quais são as partes da Query que consomem mais recursos no  plano de execução. Algumas destas ferramentas inclusive são capazes de sugerir a criação de um ou mais índices para optimizar a busca dos dados desejados.

Mesmo sem ter uma ferramenta destas nas mãos, normalmente necessária para analisar queries grandes e mais complexas, podemos avaliar alguns pontos em queries menores “apenas olhando”, por exemplo:

  1. Ao estabelecer os critérios de busca — condições e comparações usadas na cláusula WHERE — procure usar diretamente os nomes dos campos, comparando com um conteúdo fixo,  evitando o uso de funções. É clado, vão existir exceções, mas via de regra procure estar atento neste ponto.
  2. Evite concatenações de campos nas expressões condicionais de busca. Imagine que você tem uma tabela com dois campos, e você tem nas mãos, para fazer a busca, uma string correspondendo a concatenação destes dois valores. Muto prático você fazer SELECT X FROM TABELA WHERE CPO1 || CPO2 = ‘0000010100’, certo ? Sim, mas mesmo que você tenha um índice com os campos CPO1 e CPO2, o Banco de Dados não vai conseguir usar o índice para ajudar nesta Query — e corre o risco de fazer FULLSCAN. Agora, se ao inves disso, você quebrar a informação para as duas colunas, e escrever SELECT X FROM TABELA WHERE CPO1 = ‘000001’ AND CPO2  = ‘01000’ , o Banco de Dados vai descobrir durante a montagem do plano de execução que ele pode usar um índice para estas buscas, e vai selecionar as linhas que atendem esta condição rapidinho.
  3. O Banco de Dados vai analisar a sua Query, e tentar criar um plano de acesso (ou plano de execução) para recuperar as informações desejadas o mais rápido possível. Se todas as condições usadas na cláusula WHERE forem atendidas por um mesmo índice, você ajuda o Banco de Dados a tomar a decisão mais rapidamente de qual índice internamente usar, se você fizer as comparações com os campos na ordem de declaração do índice. Por exemplo, para o índice CPO1, CPO2, CPO3, eu sugiro  uma Query com SELECT XYZ from TABELA WHERE CPO1 = ‘X’ AND CPO2 = ‘Y’ AND CPOC3 >=  ‘Z’

Queries emitidas pelo DBAccess

As queries emitidas pelo DBAccess no Banco de Dados para emular o comportamento de navegação ISAM são por natureza optimizadas. Para emular a navegação de dados em uma determinada ordem de índice, o DBAccess emite queries para preencher um cache de registros — não de dados — usando os dados dos campos como condições de busca, na mesma sequência da criação do índice. E, para recuperar o conteúdo (colunas) de um registro (linha), ele usa um statement preparado, onde o Banco faz o parser da requisição apenas na primeira chamada, e as demais usam o mesmo plano de execução.

Porém, isto não impede de uma ou mais queries emitidas pelo DBAccess acabem ficando lentas. Normalmente isso acontece quando é realizada uma condição de filtro na tabela em AdvPL, onde nem todos — ou nenhum —  os campos utilizados não possuem um índice que favoreça uma busca indexada, fazendo com que o Banco acabe varrendo a tabela inteira — FULL SCAN — para recuperar os dados desejados.

Este tipo de ocorrência também é solúvel, uma vez determinado qual seria a chave de índice que tornaria a navegação com esta condição de filtro optimizada, é possível de duas formas criar este índice.

Criando um índice auxiliar

A forma recomendada de se criar um índice auxiliar é acrescentá-lo via Configurador no arquivo de índices do ERP (SIX), para que ele seja criado e mantido pelas rotinas padrão do sistema. Porém, para isso este índice não pode conter campos de controle do DBAccess no meio das colunas do índice, e para se adequar ao padrão do ERP, seu primeiro campo deveria sempre ser o campo XX_FILIAL da tabela.

Quando esta alternativa não for possível, existe a possibilidade de criar este índice diretamente no Banco de Dados. Porém, a existência desse índice não deve interferir na identificação de índices de emulação ISAM que o DBAccess faz quando qualquer tabela é aberta. Para isso, a escolha do NOME DO ÍNDICE é fundamental.

Um índice criado diretamente no Banco de Dados para estes casos deve ter um nome que seja alfabeticamente POSTERIOR aos índices declarados no dicionário de dados (SIX). Por exemplo, existe uma tabela ABC990, que possui 8 índices. O ERP Microsiga nomeia os índices da tabela no padrão usando o nome da tabela e mais um número ou letra, em ordem alfabética. Logo, os oito primeiros indices da tabela chamam-se, respectivamente, ABC9901, ABC9902 … ABC9908.

Nomeie seu primeiro índice customizado com o nome de ABC990Z1. Caso seja necessário mais um índice, ABC990Z2, e assim sucessivamente. Chegou no 9, precisa de mais um índice, coloque a letra “A” — ABC990ZA. Dessa forma, estes índices ficarão por último na identificação do DBAccess, e por eles provavelmente não se encaixarem no padrão do ERP, você não vai — e não deve — usá-los dentro de fontes customizados AdvPL — Estes índices vão existir apenas no Banco de Dados, para favorecer a execução de queries especificas ou filtros específicos de navegação.

Precauções

Uma vez que um ou mais índices sejam criados dentro do Banco de Dados, sem usar o dicionário ativo de dados (SIX), qualquer alteração estrutural na tabela feita por uma atualização de versão ou outro programa pode apagar estes índices a qualquer momento, caso seja necessário, e isso vai novamente impactar o desempenho da aplicação. Para evitar isto, é possível escrever uma rotina em AdvPL — customização — para verificar usando Queries no banco de dados se os índices auxiliares criados por fora ainda existem, e até recriá-los se for o caso. Pode ser usado para isso o Ponto de Entrada CHKFILE() , junto das funções TCCanOpen() — para testar a existência do índice — e TcSqlExec() — para criar o índice customizado usando uma instrução SQL diretamente no banco de dados.

Outras causas de degradação de desempenho

Existem ocorrências específicas, que podem estar relacionadas desde a configuração do Banco de Dados, até mesmo problemas no mecanismo de montagem ou cache dos planos de acesso criados pelo Banco para resolver as Queries. Outas ocorrências podem estar relacionadas a execução de rotinas automáticas ou agendadas no próprio servidor de Banco de Dados. Normalmente o efeito destas interferências são percebidos como uma lentidão momentânea porém generalizada, por um período determinado de tempo.

Conclusão

Use a tecnologia ao seu favor, e em caso de pânico, chame um DBA! Muitas vezes existe uma forma mais simples ou com menos etapas para trazer as informações desejadas. Outras vezes é mais elegante, rápido e prático escrever mais de uma query menor, do que uma Super-Query-Megazord.

Desejo a todos um ótimo final de semana, e muitos TERABYTES DE SUCESSO 😀

Referências