Debugging em ADVPL tem fama de ser limitado. Na prática, o TOTVS Protheus oferece ferramentas mais poderosas do que a maioria dos desenvolvedores conhece. O problema não é a falta de ferramentas — é não saber onde procurar.
Log estruturado antes de console
O padrão oficial TOTVS (regra CA1004) pede log via FWLogMsg() — não via console: o console não é audível em produção distribuída. ConOut() fica reservado para debug temporário, sempre com prefixo [DEBUG] e remoção obrigatória antes do merge.
// Log de produção — FWLogMsg (CA1004). // Adapte a assinatura à sua release — ver TDN. FWLogMsg("INFO", "", "MINHAROTINA", "", "", "", "Pedido " + cNum + " processado") // Debug temporário — ConOut com prefixo [DEBUG], remover antes do merge Static Function DebugVar(cVar, cNome) Local cTipo := ValType(cVar) Local cConteudo := "" Do Case Case cTipo == "C" cConteudo := cVar Case cTipo == "N" cConteudo := AllTrim(Str(cVar)) Case cTipo == "L" // If/Else explícito — regra CA4000 proíbe IF()/IIF() inline If cVar cConteudo := ".T." Else cConteudo := ".F." EndIf Case cTipo == "D" cConteudo := DtoC(cVar) Case cTipo == "A" cConteudo := "Array[" + AllTrim(Str(Len(cVar))) + "]" Case cTipo == "U" cConteudo := "NIL" EndCase ConOut("[DEBUG] " + cNome + " (" + cTipo + ") = " + cConteudo) Return
Para debugging temporário, use o prefixo [DEBUG] no log — facilita encontrar e limpar depois. No Windows, use a busca do TDS/VSCode (busca em pasta) para localizar todos os pontos de debug no final do desenvolvimento — grep não é nativo do ambiente Windows do dev Protheus.
Breakpoints condicionais no TOTVS Studio
O TOTVS Studio (ou ADVPL IDE) suporta breakpoints condicionais — você só pausa quando uma condição é atendida:
// Cenário: bug só acontece quando pedido tem mais de 10 itens // Em vez de pausar em TODOS os pedidos, pare só no problema: Static Function GravaPedido() // ← BREAKPOINT AQUI com condição: Len(aItens) > 10 Local nTotal := Len(aItens) If nTotal > 10 // Ponto suspeito — investigar lógica de rateio ProcessaRateio(aItens) EndIf Return
Rastreamento de performance com elapsed time
Quando uma rotina está lenta, o problema raramente é o código em si — é a consulta SQL ou acesso a disco. Meça antes de otimizar:
Static Function BenchmarkQuery() Local nStart := Seconds() Local cQuery := "" Local nRegs := 0 cQuery := "SELECT COUNT(*) AS TOTAL FROM " + RetSqlName("SC5") cQuery += " WHERE C5_FILIAL = '" + xFilial("SC5") + "'" cQuery += " AND D_E_L_E_T_ = ' '" dbUseArea(.T., "TOPCONN", TcGenQry(,, cQuery), "TMPBENCH", .T., .T.) nRegs := TMPBENCH->TOTAL dbCloseArea() ConOut("[BENCH] Query: " + AllTrim(Str(nRegs)) + " registros em " + ; AllTrim(Str(Round(Seconds() - nStart, 3))) + "s") Return
Tratamento de erros com ErrorBlock e Begin Sequence
Em .prw (ADVPL), o mecanismo é BEGIN SEQUENCE ... RECOVER USING — Try/Catch é TL++ e não compila em ADVPL. O ErrorBlock intercepta o erro e o Break do handler propaga para o RECOVER do escopo chamador:
Static Function ExecutaComErro() Local bOldError := ErrorBlock({|oErr| TrataErro(oErr)}) Local oErr := Nil Begin Sequence // Código que pode falhar ExecutaRotinaCritica() Recover Using oErr // Chegou aqui via Break do handler — logar e decidir ConOut("[ERROR] Rotina recuperada") End Sequence ErrorBlock(bOldError) // Restaurar handler anterior Return Static Function TrataErro(oErr) // Logue o essencial e verificável: Description, Operation, GenCode // (propriedades exatas do objeto de erro variam por release — valide) ConOut("[ERROR] " + oErr:Description) ConOut("[ERROR] Operacao: " + oErr:Operation) // Break dentro do handler do ErrorBlock propaga o erro // para o RECOVER USING do Begin Sequence ativo. // Retry/Break só valem no escopo do próprio Begin Sequence — // nunca em rotina separada fora do handler. Break oErr Return
Memória: as ferramentas certas
Não existe "varredura de variáveis" confiável no runtime ADVPL — truques com laços de GetMemVar() não auditam memória real. Para investigar consumo excessivo:
- Monitor do AppServer: acompanhe uso por thread/serviço no monitor do AppServer (e no monitor de jobs).
- Work Areas orfãs: alias temporário aberto e nunca fechado vaza memória e travas. Feche sempre —
TcCanOpen/dbCloseArea. - FWTemporaryTable: em vez de criar tabelas ISAM físicas de trabalho (regra CA1000), use tabela temporária do framework — o banco cuida da limpeza.
- Batch: prefira processar em blocos a carregar 100k registros em array.
Debugging em Jobs e AppServer
Rotinas que rodam como Job (sem interface gráfica) são as mais difíceis de debugar. Sem botão, sem tela, sem breakpoint visual. A solução: log estruturado — com append no arquivo, nunca MemoWrite (que sobrescreve o arquivo a cada chamada e deixa só a última linha).
// Log de job com APPEND (#Include "fileio.ch" para FO_APPEND/FS_END) Static Function JobLog(cMsg, cLevel) Local nHnd := -1 Local cLog := DToC(Date()) + " " +; Time() + " [" + cLevel + "] " + cMsg // Log estruturado oficial (CA1004) — adapte à sua release FWLogMsg(cLevel, "", "MEUJOB", "", "", "", cMsg) // Arquivo de auditoria — append, não sobrescreve // ATENÇÃO: caminho é do SERVIDOR AppServer, não do cliente nHnd := FOpen("\totvs\logs\job_" +; DTOS(Date()) + ".log", FO_APPEND + FO_READWRITE) If nHnd > 0 FSeek(nHnd, 0, FS_END) FWrite(nHnd, cLog + CRLF) FClose(nHnd) EndIf Return // Uso em um Job (.prw): BEGIN SEQUENCE — Try/Catch é TL++ e não compila em .prw User Function MeuJob() Local oErr := Nil JobLog("Início do processamento", "INFO") Begin Sequence // ... processamento ... JobLog("Processados 1500 registros", "INFO") Recover Using oErr JobLog("ERRO: " + oErr:Description, "ERROR") End Sequence JobLog("Fim do processamento", "INFO") Return
Dica: use prefixos no log ([INFO], [ERROR], [WARN]) para facilitar busca. E cuidado com Seconds() em medições: ela reinicia à meia-noite — se a medição puder atravessar 00:00, use base de tempo com data/hora (ex.: FWTimeStamp).
Checklist de debugging ADVPL
DebugVar() para entender o tipo e conteúdo antes do ponto de falha.Precisa de ajuda com debugging Protheus?
Falar com a ELP